Hode / flv_bridge.py
Reooiq's picture
Replace app entirely with Reoo88/Tktktk (TikTok->YouTube relay manager)
35c2217
Raw History Blame Contribute Delete
4.8 kB
"""Streaming legacy HEVC FLV (codec-id 12) -> Enhanced FLV hvc1.
Only tag headers/container fields are changed. Encoded video payload, CTS and DTS
are preserved. H.264 and already-enhanced FLV tags pass through unchanged.
No whole-recording buffering, decoding, rescaling or frame-rate conversion.
Extra guard for YouTube ingestion:
FormatChanged is raised the moment a *different* video decoder configuration
record (AVCDecoderConfigurationRecord / HEVCDecoderConfigurationRecord, i.e.
SPS/PPS or VPS/SPS/PPS) shows up in the middle of a session. That is exactly
what a phone does when the layout changes (solo <-> PK round) or when the
encoder restarts after the app was backgrounded and resumed.
Why it matters: `ffmpeg -c:v copy` cannot detect an input parameter change, so
it silently forwards the new frames while the FLV it already sent to YouTube
declares the old width/height in onMetaData and in the first sequence header.
YouTube then stretches/re-locks the picture -> fast zoom in-out, stutter,
a long "repeated clip", freeze, black flash. The caller restarts the sender on
this exception; the fresh RTMPS session re-declares the new size cleanly
(a ~1-3 s gap instead of minutes of broken video).
This is pure byte inspection of tag headers: no decoding, no re-encoding, no
rescaling, so the original picture quality and the original source are untouched.
"""
class FormatChanged(Exception):
"""Source video decoder configuration changed mid-stream."""
def _video_signature(body):
"""Fingerprint of a video sequence header, or None if `body` is not one."""
if not body:return None
if body[0]&0x80: # Enhanced RTMP
if body[0]&0x0F:return None # PacketType 0 == sequence header
return bytes(body[1:5])+body[5:] # FourCC + decoder config record
if (body[0]&15)==7 and len(body)>1 and body[1]==0: # legacy AVC sequence header
return b'avc1'+body[5:]
return None
class FLVBridge:
def __init__(self,detect_format_change=True):
self.buffer=bytearray();self.header_done=False;self.converted_tags=0
self.detect_format_change=detect_format_change
self.video_cfg=None;self.format_changes=0
def feed(self,chunk):
self.buffer.extend(chunk);output=bytearray()
if not self.header_done:
if len(self.buffer)<9:return b''
if self.buffer[:3]!=b'FLV':raise ValueError('Source is not FLV')
header_size=int.from_bytes(self.buffer[5:9],'big')+4
if not 13<=header_size<=1048576:raise ValueError('Invalid FLV header size')
if len(self.buffer)<header_size:return b''
output.extend(self.buffer[:header_size]);del self.buffer[:header_size];self.header_done=True
offset=0
while len(self.buffer)-offset>=11:
header=bytearray(self.buffer[offset:offset+11]);size=int.from_bytes(header[1:4],'big');total=11+size+4
if len(self.buffer)-offset<total:break
body=bytes(self.buffer[offset+11:offset+11+size]);tag_type=header[0]
if tag_type & 0x20:raise ValueError('Filtered/encrypted FLV tag is unsupported')
if (tag_type & 31)==9 and body and not (body[0]&0x80) and (body[0]&15)==12:
if len(body)<5:raise ValueError('Truncated HEVC FLV video header')
frame=(body[0]>>4)&7;packet=body[1]
if packet==0: # HEVC configuration record, no composition time in SequenceStart
body=bytes([0x80|(frame<<4)])+b'hvc1'+body[5:]
elif packet==1: # CodedFrames includes the same signed 24-bit composition offset
body=bytes([0x80|(frame<<4)|1])+b'hvc1'+body[2:]
elif packet==2:
body=bytes([0x80|(frame<<4)|2])+b'hvc1'+body[5:]
else:raise ValueError('Unsupported legacy HEVC packet type')
self.converted_tags+=1
if (tag_type & 31)==9:
signature=_video_signature(body)
if signature is not None:
if self.video_cfg is None:self.video_cfg=signature
elif signature!=self.video_cfg:
self.format_changes+=1
if self.detect_format_change:
raise FormatChanged('video decoder configuration changed mid-stream')
self.video_cfg=signature
header[1:4]=len(body).to_bytes(3,'big')
output.extend(header);output.extend(body);output.extend((11+len(body)).to_bytes(4,'big'))
offset+=total
if offset:del self.buffer[:offset]
if len(self.buffer)>17*1024*1024:raise ValueError('FLV packet buffer exceeds limit')
return bytes(output)