Jump to content

GStreamer In-Band Metadata for MPEG Transport Stream - Examples - Python

From RidgeRun Developer Wiki


Previous: Examples/GstLaunch Index Next: Examples/Gstd






Write Metadata to TS

The following Python script generates an MPEG-TS file containing an H.264 video stream together with dynamic KLV metadata. The metadata is generated during execution and injected into the transport stream at a configurable frame interval using the metasrc element.

The script builds the complete GStreamer pipeline using the Python GStreamer bindings (PyGObject) and records the resulting transport stream into a TS file.

#!/usr/bin/env python3
"""Create an MPEG-TS file with video and dynamic metadata from Python."""

import argparse
import json
import signal
import sys
import time

import gi

gi.require_version("Gst", "1.0")
from gi.repository import GLib, Gst


PLUGIN_HINT = (
    "Build/install gst-plugin-meta, or run from the source tree with "
    'GST_PLUGIN_PATH="$PWD/src/.libs". Use the same GStreamer runtime that '
    "was used to build the plugin."
)


def require_factories(*names):
    missing = [name for name in names if Gst.ElementFactory.find(name) is None]
    if missing:
        raise SystemExit(
            f"Missing GStreamer element(s): {', '.join(missing)}. {PLUGIN_HINT}"
        )


def make_element(factory, name):
    element = Gst.ElementFactory.make(factory, name)
    if element is None:
        raise RuntimeError(f"Could not create GStreamer element {factory!r}")
    return element


def link_many(*elements):
    for upstream, downstream in zip(elements, elements[1:]):
        if not upstream.link(downstream):
            raise RuntimeError(
                f"Could not link {upstream.get_name()} to {downstream.get_name()}"
            )


def format_clock_time(value):
    if value == Gst.CLOCK_TIME_NONE:
        return None
    return int(value)


def on_bus_message(bus, message, loop):
    if message.type == Gst.MessageType.ERROR:
        error, debug = message.parse_error()
        print(f"ERROR from {message.src.get_name()}: {error}", file=sys.stderr)
        if debug:
            print(debug, file=sys.stderr)
        loop.quit()
    elif message.type == Gst.MessageType.EOS:
        loop.quit()
    return True


def build_pipeline(output, every_n_frames):
    pipeline = Gst.Pipeline.new("dynamic-metadata-ts-writer")

    metadata_src = make_element("metasrc", "metadata_src")
    metadata_caps = make_element("capsfilter", "metadata_caps")
    metadata_queue = make_element("queue", "metadata_queue")

    video_src = make_element("videotestsrc", "video_src")
    raw_caps = make_element("capsfilter", "raw_video_caps")
    overlay = make_element("timeoverlay", "time_overlay")
    convert = make_element("videoconvert", "video_convert")
    encoder = make_element("x264enc", "h264_encoder")
    parser = make_element("h264parse", "h264_parser")
    h264_caps = make_element("capsfilter", "h264_caps")
    video_queue = make_element("queue", "video_queue")

    mux = make_element("mpegtsmux", "mux")
    sink = make_element("filesink", "ts_file")

    metadata_src.set_property("metadata", '{"event":"pipeline-start"}')
    metadata_caps.set_property("caps", Gst.Caps.from_string("meta/x-klv,parsed=true"))
    video_src.set_property("is-live", True)
    raw_caps.set_property(
        "caps",
        Gst.Caps.from_string("video/x-raw,width=640,height=360,framerate=30/1"),
    )
    encoder.set_property("tune", "zerolatency")
    encoder.set_property("speed-preset", "ultrafast")
    encoder.set_property("key-int-max", 30)
    parser.set_property("config-interval", -1)
    h264_caps.set_property(
        "caps",
        Gst.Caps.from_string("video/x-h264,stream-format=byte-stream,alignment=au"),
    )
    sink.set_property("location", output)
    sink.set_property("sync", False)
    sink.set_property("async", False)

    elements = (
        metadata_src,
        metadata_caps,
        metadata_queue,
        video_src,
        raw_caps,
        overlay,
        convert,
        encoder,
        parser,
        h264_caps,
        video_queue,
        mux,
        sink,
    )
    for element in elements:
        pipeline.add(element)

    link_many(metadata_src, metadata_caps, metadata_queue)
    if not metadata_queue.link(mux):
        raise RuntimeError("Could not link metadata stream to mpegtsmux")

    link_many(
        video_src,
        raw_caps,
        overlay,
        convert,
        encoder,
        parser,
        h264_caps,
        video_queue,
    )
    if not video_queue.link(mux):
        raise RuntimeError("Could not link video stream to mpegtsmux")

    if not mux.link(sink):
        raise RuntimeError("Could not link mpegtsmux to filesink")

    frame_state = {"count": 0}

    def push_metadata_for_video_buffer(pad, info):
        buffer = info.get_buffer()
        if buffer is None:
            return Gst.PadProbeReturn.OK

        frame_state["count"] += 1
        frame_number = frame_state["count"]
        if frame_number % every_n_frames != 0:
            return Gst.PadProbeReturn.OK

        payload = {
            "source": "python-dynamic-metadata-ts",
            "frame": frame_number,
            "pts_ns": format_clock_time(buffer.pts),
            "unix_time_ns": time.time_ns(),
        }
        data = json.dumps(payload, separators=(",", ":"))
        metadata_src.set_property("metadata", data)
        return Gst.PadProbeReturn.OK

    video_src.get_static_pad("src").add_probe(
        Gst.PadProbeType.BUFFER, push_metadata_for_video_buffer
    )

    return pipeline


def main():
    parser = argparse.ArgumentParser(
        description="Write an MPEG-TS file with dynamic metadata from Python."
    )
    parser.add_argument("--output", default="metadata.ts", help="Output TS file")
    parser.add_argument(
        "--seconds", type=int, default=8, help="Recording duration in seconds"
    )
    parser.add_argument(
        "--every-n-frames",
        type=int,
        default=30,
        help="Push one metadata buffer for every N video frames",
    )
    args = parser.parse_args()

    if args.every_n_frames <= 0:
        raise SystemExit("--every-n-frames must be greater than zero")

    Gst.init(None)
    require_factories(
        "metasrc",
        "capsfilter",
        "queue",
        "videotestsrc",
        "timeoverlay",
        "videoconvert",
        "x264enc",
        "h264parse",
        "mpegtsmux",
        "filesink",
    )

    loop = GLib.MainLoop()
    pipeline = build_pipeline(args.output, args.every_n_frames)

    bus = pipeline.get_bus()
    bus.add_signal_watch()
    bus.connect("message", on_bus_message, loop)

    def stop_pipeline(*unused):
        pipeline.send_event(Gst.Event.new_eos())
        return False

    signal.signal(signal.SIGINT, stop_pipeline)

    result = pipeline.set_state(Gst.State.PLAYING)
    if result == Gst.StateChangeReturn.FAILURE:
        raise RuntimeError("Could not set pipeline to PLAYING")
    GLib.timeout_add_seconds(args.seconds, stop_pipeline)
    try:
        loop.run()
    finally:
        pipeline.set_state(Gst.State.NULL)
        bus.remove_signal_watch()

    print(f"Wrote MPEG-TS with dynamic metadata to {args.output}")


if __name__ == "__main__":
    main()

Once generated the script execute it as follows.

python3 write_dynamic_metadata_ts.py --output metadata.ts

Read Metadata

The following Python script generates an MPEG-TS file containing an H.264 video stream together with dynamic KLV metadata. The metadata is generated during execution and injected into the transport stream at a configurable frame interval using the metasrc element.

The script builds the complete GStreamer pipeline using the Python GStreamer bindings (PyGObject) and records the resulting transport stream into a TS file.

import argparse
import string
import sys

import gi

gi.require_version("Gst", "1.0")
from gi.repository import GLib, Gst


PLUGIN_HINT = (
    "Build/install gst-plugin-meta, or run from the source tree with "
    'GST_PLUGIN_PATH="$PWD/src/.libs". Use the same GStreamer runtime that '
    "was used to build the plugin."
)


def require_factories(*names):
    missing = [name for name in names if Gst.ElementFactory.find(name) is None]
    if missing:
        raise SystemExit(
            f"Missing GStreamer element(s): {', '.join(missing)}. {PLUGIN_HINT}"
        )


def make_element(factory, name):
    element = Gst.ElementFactory.make(factory, name)
    if element is None:
        raise RuntimeError(f"Could not create GStreamer element {factory!r}")
    return element


def link_many(*elements):
    for upstream, downstream in zip(elements, elements[1:]):
        if not upstream.link(downstream):
            raise RuntimeError(
                f"Could not link {upstream.get_name()} to {downstream.get_name()}"
            )


def clock_to_string(value):
    if value == Gst.CLOCK_TIME_NONE:
        return "none"
    seconds, ns = divmod(int(value), Gst.SECOND)
    return f"{seconds}.{ns:09d}s"


def printable(data):
    allowed = set(string.printable.encode("ascii"))
    return "".join(
        chr(byte) if byte in allowed and byte not in (0x0B, 0x0C) else "."
        for byte in data
    )


def on_bus_message(bus, message, loop):
    if message.type == Gst.MessageType.ERROR:
        error, debug = message.parse_error()
        print(f"ERROR from {message.src.get_name()}: {error}", file=sys.stderr)
        if debug:
            print(debug, file=sys.stderr)
        loop.quit()
    elif message.type == Gst.MessageType.EOS:
        loop.quit()
    return True


def make_appsink(on_sample):
    appsink = make_element("appsink", "metadata_sink")
    appsink.set_property("emit-signals", True)
    appsink.set_property("sync", False)
    appsink.connect("new-sample", on_sample)
    return appsink


def build_raw_pipeline(input_file, appsink):
    pipeline = Gst.Pipeline.new("raw-metadata-reader")
    src = make_element("filesrc", "input")
    capsfilter = make_element("capsfilter", "metadata_caps")

    src.set_property("location", input_file)
    capsfilter.set_property("caps", Gst.Caps.from_string("meta/x-klv,parsed=true"))

    for element in (src, capsfilter, appsink):
        pipeline.add(element)
    link_many(src, capsfilter, appsink)
    return pipeline


def build_ts_pipeline(input_file, appsink, parse_misb):
    pipeline = Gst.Pipeline.new("ts-metadata-reader")
    src = make_element("filesrc", "input")
    demux = make_element("tsdemux", "demux")
    queue = make_element("queue", "metadata_queue")

    src.set_property("location", input_file)

    elements = [src, demux, queue]
    parser = None
    if parse_misb:
        parser = make_element("misbparser", "misb_parser")
        parser.set_property("dump", False)
        elements.append(parser)
    elements.append(appsink)

    for element in elements:
        pipeline.add(element)

    if not src.link(demux):
        raise RuntimeError("Could not link filesrc to tsdemux")
    if parser is None:
        link_many(queue, appsink)
    else:
        link_many(queue, parser, appsink)

    def on_pad_added(demux, pad):
        caps = pad.get_current_caps() or pad.query_caps(None)
        caps_name = caps.to_string() if caps is not None else ""
        if not caps_name.startswith("meta/x-klv"):
            return

        sink_pad = queue.get_static_pad("sink")
        if sink_pad.is_linked():
            return

        result = pad.link(sink_pad)
        if result != Gst.PadLinkReturn.OK:
            print(f"Could not link metadata pad: {result.value_nick}", file=sys.stderr)

    demux.connect("pad-added", on_pad_added)
    return pipeline


def main():
    parser = argparse.ArgumentParser(description="Read metadata buffers with appsink.")
    parser.add_argument("--input", required=True, help="Input raw KLV or MPEG-TS file")
    parser.add_argument(
        "--raw",
        action="store_true",
        help="Treat input as a raw metadata file instead of MPEG-TS",
    )
    parser.add_argument(
        "--parse-misb",
        action="store_true",
        help="Run TS metadata through misbparser before appsink",
    )
    args = parser.parse_args()

    if args.raw and args.parse_misb:
        raise SystemExit("--parse-misb is only supported for MPEG-TS input")

    Gst.init(None)
    required = ["filesrc", "appsink"]
    if args.raw:
        required.append("capsfilter")
    else:
        required.extend(["tsdemux", "queue"])
        if args.parse_misb:
            required.append("misbparser")
    require_factories(*required)

    loop = GLib.MainLoop()
    state = {"count": 0}

    def on_sample(appsink):
        sample = appsink.emit("pull-sample")
        if sample is None:
            return Gst.FlowReturn.ERROR

        buffer = sample.get_buffer()
        success, info = buffer.map(Gst.MapFlags.READ)
        if not success:
            return Gst.FlowReturn.ERROR

        try:
            data = bytes(info.data)
        finally:
            buffer.unmap(info)

        state["count"] += 1
        print(
            f"{state['count']:04d} pts={clock_to_string(buffer.pts)} "
            f"size={len(data)} ascii={printable(data)} hex={data.hex(' ')}"
        )
        return Gst.FlowReturn.OK

    appsink = make_appsink(on_sample)
    if args.raw:
        pipeline = build_raw_pipeline(args.input, appsink)
    else:
        pipeline = build_ts_pipeline(args.input, appsink, args.parse_misb)

    bus = pipeline.get_bus()
    bus.add_signal_watch()
    bus.connect("message", on_bus_message, loop)

    pipeline.set_state(Gst.State.PLAYING)
    try:
        loop.run()
    finally:
        pipeline.set_state(Gst.State.NULL)
        bus.remove_signal_watch()

    print(f"Read {state['count']} metadata buffer(s)")


if __name__ == "__main__":
    main()

Execute the script as follows.

python3 read_metadata.py --input metadata.ts


The output will look as follows


Setting pipeline to PAUSED ...
Pipeline is PREROLLING ...
Pipeline is PREROLLED ...
Setting pipeline to PLAYING ...
00000000 (0x7e0ae4004b88): 70 79 74 68 6f 6e 2d 70 65 72 69 6f 64 69 63 20  python-periodic 
00000010 (0x7e0ae4004b98): 32 30 32 36 2d 30 36 2d 32 39 54 31 37 3a 31 33  2026-06-29T17:13
00000020 (0x7e0ae4004ba8): 3a 31 37 2d 30 36 30 30 00 70 79 74 68 6f 6e 2d  :17-0600.python-
00000030 (0x7e0ae4004bb8): 70 65 72 69 6f 64 69 63 20 32 30 32 36 2d 30 36  periodic 2026-06
00000040 (0x7e0ae4004bc8): 2d 32 39 54 31 37 3a 31 33 3a 31 38 2d 30 36 30  -29T17:13:18-060
Redistribute latency...
00000050 (0x7e0ae4004bd8): 30 00 70 79 74 68 6f 6e 2d 70 65 72 69 6f 64 69  0.python-periodi
00000060 (0x7e0ae4004be8): 63 20 32 30 32 36 2d 30 36 2d 32 39 54 31 37 3a  c 2026-06-29T17:
00000070 (0x7e0ae4004bf8): 31 33 3a 31 39 2d 30 36 30 30 00 70 79 74 68 6f  13:19-0600.pytho
00000080 (0x7e0ae4004c08): 6e 2d 70 65 72 69 6f 64 69 63 20 32 30 32 36 2d  n-periodic 2026-
00000090 (0x7e0ae4004c18): 30 36 2d 32 39 54 31 37 3a 31 33 3a 32 30 2d 30  06-29T17:13:20-0
New clock: GstSystemClock
000000a0 (0x7e0ae4004c28): 36 30 30 00 70 79 74 68 6f 6e 2d 70 65 72 69 6f  600.python-perio
000000b0 (0x7e0ae4004c38): 64 69 63 20 32 30 32 36 2d 30 36 2d 32 39 54 31  dic 2026-06-29T1
000000c0 (0x7e0ae4004c48): 37 3a 31 33 3a 32 31 2d 30 36 30 30 00 70 79 74  7:13:21-0600.pyt
000000d0 (0x7e0ae4004c58): 68 6f 6e 2d 70 65 72 69 6f 64 69 63 20 32 30 32  hon-periodic 202
000000e0 (0x7e0ae4004c68): 36 2d 30 36 2d 32 39 54 31 37 3a 31 33 3a 32 32  6-06-29T17:13:22
000000f0 (0x7e0ae4004c78): 2d 30 36 30 30 00                                -0600.          
Got EOS from element "pipeline0".
EOS received - stopping pipeline...
Execution ended after 0:00:00.000538260
Setting pipeline to NULL ...



Previous: Examples/GstLaunch Index Next: Examples/Gstd



Cookies help us deliver our services. By using our services, you agree to our use of cookies.