H.264 SEI (Supplemental Enhancement Information) is optional metadata carried
inside an H.264 video bitstream. SEI messages do not change the decoded pixels,
but they let encoders, decoders, or downstream applications attach extra
information to frames.
Common uses include timestamps, camera or sensor metadata, HDR/display
information, captions, stream identifiers, and custom application data. In a
GStreamer pipeline, SEI is useful when metadata must stay synchronized with the
video frames and travel together with the encoded stream.
h264_sei_pipe.py
h264_sei_pipe.py demonstrates a small app bridge: one GStreamer pipeline
encodes raw video to Annex B H.264 access units, an appsink gives Python each
encoded frame, Python inserts a user_data_unregistered SEI NAL unit, and an
appsrc pushes the modified H.264 frame to an RTP/UDP pipeline.
The SEI is injected before the first VCL NAL unit, so metadata is carried in the
same access unit as the frame without changing the decoded image.
The payload is wrapped as user_data_unregistered SEI, identified by a UUID, then inserted into the H.264 access unit before the actual video slice.
The UUID acts like an identifier for your custom metadata format. Many tools or apps can insert SEI data into the same H.264 stream, so the receiver needs a way to know which SEI messages belong to your application.
The probe sees each encoded access unit while the SEI NAL units are still
present. It maps the Gst.Buffer, scans Annex B NAL units for SEI NAL type 6,
parses user_data_unregistered payloads, keeps only messages whose first 16
bytes match the application UUID, and then decodes the remaining bytes as the
application payload:
This keeps metadata extraction outside the decoder and does not alter the video
frames. The cost is still on the streaming path: every probed buffer is mapped
and scanned, and every matching SEI payload is decoded. Keep the probe work
small, avoid blocking I/O or heavy parsing in the callback, and move expensive
processing to another thread or queue if the receiver must sustain high
framerate or high-bitrate streams.
H.265 / HEVC version
The same idea is implemented in h265_sei.py, h265_sei_pipe.py, and
h265_sei_receiver.py. The GStreamer shape is the same, but the codec elements
change to HEVC:
The parser differences are in the NAL-unit header. H.265 NAL units have a
two-byte header, the prefix SEI NAL type is 39, and VCL slice NAL types are
0..31. The SEI payload itself still uses user_data_unregistered:
#!/usr/bin/env python3from__future__importannotationsfromcollections.abcimportIteratorUSER_DATA_UNREGISTERED=5H264_SEI_NAL_TYPE=6BT_SEI_UUID=b"BTGSTSEI01234567"defmake_user_data_unregistered_sei(payload:bytes,uuid:bytes=BT_SEI_UUID)->bytes:iflen(uuid)!=16:raiseValueError("H.264 user_data_unregistered SEI UUID must be 16 bytes")sei_payload=uuid+payloadrbsp=(_encode_sei_value(USER_DATA_UNREGISTERED)+_encode_sei_value(len(sei_payload))+sei_payload+b"\x80")returnb"\x00\x00\x00\x01"+bytes([H264_SEI_NAL_TYPE])+_escape_rbsp(rbsp)definsert_sei_before_first_vcl(access_unit:bytes,payload:bytes,uuid:bytes=BT_SEI_UUID,)->bytes:sei_nal=make_user_data_unregistered_sei(payload,uuid)insert_at=_first_vcl_start(access_unit)ifinsert_atisNone:returnsei_nal+access_unitreturnaccess_unit[:insert_at]+sei_nal+access_unit[insert_at:]defextract_user_data_unregistered(access_unit:bytes,uuid:bytes=BT_SEI_UUID,)->list[bytes]:payloads=[]for_start,nal_header,nal_endiniter_annexb_nalus(access_unit):ifaccess_unit[nal_header]&0x1F!=H264_SEI_NAL_TYPE:continuerbsp=_unescape_ebsp(access_unit[nal_header+1:nal_end])payloads.extend(_extract_matching_sei_payloads(rbsp,uuid))returnpayloadsdefiter_annexb_nalus(data:bytes)->Iterator[tuple[int,int,int]]:offset=0whileTrue:start=_find_start_code(data,offset)ifstartisNone:returnnal_header=start+_start_code_size(data,start)ifnal_header>=len(data):returnnext_start=_find_start_code(data,nal_header+1)nal_end=next_startifnext_startisnotNoneelselen(data)yieldstart,nal_header,nal_endifnext_startisNone:returnoffset=next_startdef_first_vcl_start(data:bytes)->int|None:forstart,nal_header,_nal_endiniter_annexb_nalus(data):nal_type=data[nal_header]&0x1Fif1<=nal_type<=5:returnstartreturnNonedef_find_start_code(data:bytes,offset:int)->int|None:three_byte=data.find(b"\x00\x00\x01",offset)four_byte=data.find(b"\x00\x00\x00\x01",offset)ifthree_byte==-1andfour_byte==-1:returnNoneifthree_byte==-1:returnfour_byteiffour_byte==-1:returnthree_bytereturnmin(three_byte,four_byte)def_start_code_size(data:bytes,start:int)->int:ifdata[start:start+4]==b"\x00\x00\x00\x01":return4return3def_encode_sei_value(value:int)->bytes:chunks=bytearray()whilevalue>=255:chunks.append(255)value-=255chunks.append(value)returnbytes(chunks)def_escape_rbsp(rbsp:bytes)->bytes:escaped=bytearray()zero_count=0forbyteinrbsp:ifzero_count>=2andbyte<=3:escaped.append(3)zero_count=0escaped.append(byte)zero_count=zero_count+1ifbyte==0else0returnbytes(escaped)def_unescape_ebsp(ebsp:bytes)->bytes:rbsp=bytearray()zero_count=0index=0whileindex<len(ebsp):byte=ebsp[index]ifzero_count>=2andbyte==3:index+=1zero_count=0continuerbsp.append(byte)zero_count=zero_count+1ifbyte==0else0index+=1returnbytes(rbsp)def_extract_matching_sei_payloads(rbsp:bytes,uuid:bytes)->list[bytes]:payloads=[]offset=0whileoffset+1<len(rbsp):ifrbsp[offset]==0x80:breakpayload_type,offset=_read_sei_value(rbsp,offset)payload_size,offset=_read_sei_value(rbsp,offset)end=offset+payload_sizeifend>len(rbsp):breakpayload=rbsp[offset:end]if(payload_type==USER_DATA_UNREGISTEREDandlen(payload)>=16andpayload[:16]==uuid):payloads.append(payload[16:])offset=endreturnpayloadsdef_read_sei_value(rbsp:bytes,offset:int)->tuple[int,int]:value=0whileoffset<len(rbsp):byte=rbsp[offset]offset+=1value+=byteifbyte!=255:returnvalue,offsetreturnvalue,offset
#!/usr/bin/env python3from__future__importannotationsfromcollections.abcimportIteratorUSER_DATA_UNREGISTERED=5H265_PREFIX_SEI_NAL_TYPE=39BT_SEI_UUID=b"BTGSTSEI01234567"defmake_user_data_unregistered_sei(payload:bytes,uuid:bytes=BT_SEI_UUID)->bytes:iflen(uuid)!=16:raiseValueError("H.265 user_data_unregistered SEI UUID must be 16 bytes")sei_payload=uuid+payloadrbsp=(_encode_sei_value(USER_DATA_UNREGISTERED)+_encode_sei_value(len(sei_payload))+sei_payload+b"\x80")nal_header=bytes([(H265_PREFIX_SEI_NAL_TYPE<<1)&0x7E,0x01])returnb"\x00\x00\x00\x01"+nal_header+_escape_rbsp(rbsp)definsert_sei_before_first_vcl(access_unit:bytes,payload:bytes,uuid:bytes=BT_SEI_UUID,)->bytes:sei_nal=make_user_data_unregistered_sei(payload,uuid)insert_at=_first_vcl_start(access_unit)ifinsert_atisNone:returnsei_nal+access_unitreturnaccess_unit[:insert_at]+sei_nal+access_unit[insert_at:]defextract_user_data_unregistered(access_unit:bytes,uuid:bytes=BT_SEI_UUID,)->list[bytes]:payloads=[]for_start,nal_header,nal_endiniter_annexb_nalus(access_unit):ifnal_header+2>nal_end:continueif_nal_type(access_unit[nal_header])!=H265_PREFIX_SEI_NAL_TYPE:continuerbsp=_unescape_ebsp(access_unit[nal_header+2:nal_end])payloads.extend(_extract_matching_sei_payloads(rbsp,uuid))returnpayloadsdefiter_annexb_nalus(data:bytes)->Iterator[tuple[int,int,int]]:offset=0whileTrue:start=_find_start_code(data,offset)ifstartisNone:returnnal_header=start+_start_code_size(data,start)ifnal_header>=len(data):returnnext_start=_find_start_code(data,nal_header+2)nal_end=next_startifnext_startisnotNoneelselen(data)yieldstart,nal_header,nal_endifnext_startisNone:returnoffset=next_startdef_first_vcl_start(data:bytes)->int|None:forstart,nal_header,nal_endiniter_annexb_nalus(data):ifnal_header+2>nal_end:continueif0<=_nal_type(data[nal_header])<=31:returnstartreturnNonedef_nal_type(first_header_byte:int)->int:return(first_header_byte>>1)&0x3Fdef_find_start_code(data:bytes,offset:int)->int|None:three_byte=data.find(b"\x00\x00\x01",offset)four_byte=data.find(b"\x00\x00\x00\x01",offset)ifthree_byte==-1andfour_byte==-1:returnNoneifthree_byte==-1:returnfour_byteiffour_byte==-1:returnthree_bytereturnmin(three_byte,four_byte)def_start_code_size(data:bytes,start:int)->int:ifdata[start:start+4]==b"\x00\x00\x00\x01":return4return3def_encode_sei_value(value:int)->bytes:chunks=bytearray()whilevalue>=255:chunks.append(255)value-=255chunks.append(value)returnbytes(chunks)def_escape_rbsp(rbsp:bytes)->bytes:escaped=bytearray()zero_count=0forbyteinrbsp:ifzero_count>=2andbyte<=3:escaped.append(3)zero_count=0escaped.append(byte)zero_count=zero_count+1ifbyte==0else0returnbytes(escaped)def_unescape_ebsp(ebsp:bytes)->bytes:rbsp=bytearray()zero_count=0index=0whileindex<len(ebsp):byte=ebsp[index]ifzero_count>=2andbyte==3:index+=1zero_count=0continuerbsp.append(byte)zero_count=zero_count+1ifbyte==0else0index+=1returnbytes(rbsp)def_extract_matching_sei_payloads(rbsp:bytes,uuid:bytes)->list[bytes]:payloads=[]offset=0whileoffset+1<len(rbsp):ifrbsp[offset]==0x80:breakpayload_type,offset=_read_sei_value(rbsp,offset)payload_size,offset=_read_sei_value(rbsp,offset)end=offset+payload_sizeifend>len(rbsp):breakpayload=rbsp[offset:end]if(payload_type==USER_DATA_UNREGISTEREDandlen(payload)>=16andpayload[:16]==uuid):payloads.append(payload[16:])offset=endreturnpayloadsdef_read_sei_value(rbsp:bytes,offset:int)->tuple[int,int]:value=0whileoffset<len(rbsp):byte=rbsp[offset]offset+=1value+=byteifbyte!=255:returnvalue,offsetreturnvalue,offset
BtH264Sei is a non-in-place GstBase.BaseTransform, so it does not edit the
incoming encoded buffer directly. do_prepare_output_buffer is where the plugin
builds the replacement output buffer for each H.264 access unit:
The method maps the input Gst.Buffer to bytes, creates the JSON metadata
payload, inserts it as an SEI NAL unit before the first VCL NAL unit, then wraps
the modified bytes in a new Gst.Buffer. Because this creates a fresh buffer,
the original PTS, DTS, duration, offsets, and buffer flags are copied to keep the
encoded frame synchronized with the rest of the pipeline.
If the input buffer cannot be mapped, the method returns Gst.FlowReturn.ERROR.
Otherwise it returns Gst.FlowReturn.OK with the prepared output buffer. The
do_transform method can then be a no-op because the output buffer has already
been fully produced by do_prepare_output_buffer.