"""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)=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>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)