Streaming video processing

This example shows how to process video in streaming fashion.

For the resulting video to be playable, audio data and video data must be written in small chunks in alternating manner.

block-beta columns 7 block:e:7 p1["Audio t1"] p2("Video t1") p3("Video t2") p4["Audio t2"] p5("Video t3") p6["Audio t3"] p7["..."] end

The following diagram illustrates how audio/video data are processed.

flowchart TD In[Input Video] -->|1\. Demux| AP(Audio Packets) AP --> |2\. Remux| Out[Output Video] In[Input Video] -->|1\. Demux| VP(Video Packets) VP --> |3\. Decode| VF("Video Frames (YUV)") VF --> |4\. Filter| VF2("Video Frames (RGB)") VF2 --> |5\. Convert| ARR(Array) ARR --> |6\. Wrap| VF3("Video Frames (RGB)") VF3 --> |7\. Filter| VF4("Video Frames (YUV)") VF4 --> |8\. Encode| VP2(Video Packets) VP2 --> |9\. Mux| Out

We use spdl.io.Demuxer to extract audio/video data from the source. (1)

In this example, we do not modify audio data, so audio packets are sent to muxer (an instance of spdl.io.Muxer) directly. (2)

To modify video data, first we decode videos packets and obtain frames, using spdl.io.VideoDecoder. (3)

Usually, video frames are stored as YUV420 format, so we convert it to RGB using spdl.io.FilterGraph. (4) Then the resulting frame data are extracted as NumPy array. (5)

Though omitted in this example, let’s pretend that the array data is modified with some sort of AI model. Now we convert the array back to packet, by applying a reverse operation one by one.

To convert array back to frames, we use spdl.io.create_reference_video_frame(). This function creates a VideoFrames object that references the data of the array. (6)

We convert RGB into YUV420 using another FilterGraph instance. (7)

The YUV frame is encoded using spdl.io.VideoEncoder. (8)

Finally, the encoded data is written to the multiplexer. (9)

Note on component states

All the media processing components used in this example, (Demuxer/Decoder/FilterGraph/Encoder/Muxer) maintain its own internal state, and do not necessarily process the input data immediately.

Therefore, the number of input/output frames/packets do not necessarily match, and you need to call flush() at the end for each component.

Source

Source

Click here to see the source.
  1#!/usr/bin/env python3
  2# Copyright (c) Meta Platforms, Inc. and affiliates.
  3# All rights reserved.
  4#
  5# This source code is licensed under the BSD-style license found in the
  6# LICENSE file in the root directory of this source tree.
  7
  8"""This example shows how to process video in streaming fashion.
  9
 10For the resulting video to be playable, audio data and video data must be
 11written in small chunks in alternating manner.
 12
 13.. include:: ../plots/streaming_video_processing_block.txt
 14
 15The following diagram illustrates how audio/video data are processed.
 16
 17.. include:: ../plots/streaming_video_processing_chart.txt
 18
 19We use :py:class:`spdl.io.Demuxer` to extract audio/video data from the
 20source. (1)
 21
 22In this example, we do not modify audio data, so audio packets are sent to
 23muxer (an instance of :py:class:`spdl.io.Muxer`) directly. (2)
 24
 25To modify video data, first we decode videos packets and obtain frames,
 26using :py:class:`spdl.io.VideoDecoder`. (3)
 27
 28Usually, video frames are stored as YUV420 format, so we convert
 29it to RGB using :py:class:`spdl.io.FilterGraph`. (4) Then the resulting
 30frame data are extracted as NumPy array. (5)
 31
 32Though omitted in this example, let's pretend that the array data is
 33modified with some sort of AI model. Now we convert the array back
 34to packet, by applying a reverse operation one by one.
 35
 36To convert array back to frames, we use
 37:py:func:`spdl.io.create_reference_video_frame`. This function creates a
 38:py:class:`~spdl.io.VideoFrames` object that references the data of the
 39array. (6)
 40
 41We convert RGB into YUV420 using another :py:class:`~spdl.io.FilterGraph`
 42instance. (7)
 43
 44The YUV frame is encoded using :py:class:`spdl.io.VideoEncoder`. (8)
 45
 46Finally, the encoded data is written to the multiplexer. (9)
 47
 48.. admonition:: Note on component states
 49   :class: note
 50
 51   All the media processing components used in this example,
 52   (Demuxer/Decoder/FilterGraph/Encoder/Muxer) maintain its own internal
 53   state, and do not necessarily process the input data immediately.
 54
 55   Therefore, the number of input/output frames/packets do not necessarily
 56   match, and you need to call ``flush()`` at the end for each component.
 57
 58"""
 59
 60__all__ = [
 61    "main",
 62    "parse_args",
 63    "get_filter_desc",
 64    "process",
 65    "build_components",
 66    "main",
 67]
 68
 69import argparse
 70from pathlib import Path
 71
 72import spdl.io
 73from spdl.io import (
 74    Demuxer,
 75    FilterGraph,
 76    Muxer,
 77    VideoDecoder,
 78    VideoEncoder,
 79    VideoPackets,
 80)
 81
 82
 83def parse_args() -> argparse.Namespace:
 84    """Parse the command line arguments."""
 85
 86    parser = argparse.ArgumentParser(
 87        description=__doc__,
 88    )
 89    parser.add_argument("--input-path", "-i", required=True, type=Path)
 90    parser.add_argument("--output-path", "-o", required=True, type=Path)
 91    return parser.parse_args()
 92
 93
 94def get_filter_desc(
 95    input_pix_fmt: str,
 96    input_width: int,
 97    input_height: int,
 98    frame_rate: tuple[int, int],
 99    output_pix_fmt: str,
100    output_width: int | None = None,
101    output_height: int | None = None,
102) -> str:
103    """Build a filter description that performs format conversion and optional scaling
104
105    Args:
106        input_pix_fmt: The input pixel format. Usually ``"rgb24"``.
107        input_width,input_height: The input frame resolution.
108        frame_rate: The frame rate of the video.
109        output_pix_fmt: The output pixel format. It is the pixel format used by
110            the encoder.
111        output_width,output_height: The output frame resolution.
112
113    Returns:
114        The filter description.
115    """
116    # filter graph for converting RGB into YUV420p
117    buffer_arg = ":".join(
118        [
119            f"video_size={input_width}x{input_height}",
120            f"pix_fmt={input_pix_fmt}",
121            f"time_base={frame_rate[1]}/{frame_rate[0]}",
122            "pixel_aspect=1/1",
123        ]
124    )
125    filter_arg = ",".join(
126        [
127            f"format=pix_fmts={output_pix_fmt}",
128            f"scale=w={output_width or 'iw'}:h={output_height or 'ih'}",
129        ]
130    )
131    return f"buffer={buffer_arg},{filter_arg},buffersink"
132
133
134def process(
135    demuxer: Demuxer,
136    video_decoder: VideoDecoder,
137    filter_graph: FilterGraph,
138    video_encoder: VideoEncoder,
139    muxer: Muxer,
140) -> None:
141    """The main processing logic.
142
143    Args:
144        demuxer: Demux audio/video streams from the source.
145        video_decoder: Decode the video packets.
146        filter_graph: Transform applied to the array data before encoding.
147        video_encoder: Encode the processed video array.
148        muxer: Multiplexer for remuxing audio packets and processed video packets.
149    """
150    src_pix_fmt = "rgb24"
151    frame_rate = demuxer.video_codec.frame_rate
152    video_index = demuxer.video_stream_index
153    audio_index = demuxer.audio_stream_index
154
155    streaming_demuxing = demuxer.streaming_demux([video_index, audio_index], duration=1)
156    with muxer.open():
157        num_video_frames = 0
158        for packets in streaming_demuxing:
159            if (audio_packets := packets.get(audio_index)) is not None:
160                muxer.write(1, audio_packets)
161
162            if (video_packets := packets.get(video_index)) is None:
163                continue
164
165            assert isinstance(video_packets, VideoPackets)
166            for frames in video_decoder.streaming_decode_packets(video_packets):
167                buffer = spdl.io.convert_frames(frames)
168                array = spdl.io.to_numpy(buffer)
169
170                ##############################################################
171                # <ADD FRAME PROCESSING HERE>
172                ##############################################################
173
174                frames = spdl.io.create_reference_video_frame(
175                    array,
176                    pix_fmt=src_pix_fmt,
177                    frame_rate=frame_rate,
178                    pts=num_video_frames,
179                )
180                num_video_frames += len(array)
181
182                filter_graph.add_frames(frames)
183
184                if (frames := filter_graph.get_frames()) is not None:
185                    if (
186                        packets := video_encoder.encode(frames)  # pyre-ignore
187                    ) is not None:
188                        muxer.write(0, packets)
189
190        # -------------------------------------------------------------
191        # Drain mode
192        # -------------------------------------------------------------
193
194        # Flush decoder
195        for frames in video_decoder.flush():
196            buffer = spdl.io.convert_frames(frames)
197            array = spdl.io.to_numpy(buffer)
198
199            ##############################################################
200            # <ADD FRAME PROCESSING HERE>
201            ##############################################################
202
203            frames = spdl.io.create_reference_video_frame(
204                array,
205                pix_fmt=src_pix_fmt,
206                frame_rate=frame_rate,
207                pts=num_video_frames,
208            )
209            num_video_frames += len(frames)
210
211            filter_graph.add_frames(frames)
212            if (frames := filter_graph.get_frames()) is not None:
213                if (packets := video_encoder.encode(frames)) is not None:  # pyre-ignore
214                    muxer.write(0, packets)
215
216        # Flush filter graph
217        if (frames := filter_graph.flush()) is not None:
218            if (packets := video_encoder.encode(frames)) is not None:
219                muxer.write(0, packets)
220
221        # Flush encoder
222        if (packets := video_encoder.flush()) is not None:
223            muxer.write(0, packets)
224
225
226def build_components(
227    input_path: Path, output_path: Path
228) -> tuple[Demuxer, VideoDecoder, FilterGraph, VideoEncoder, Muxer]:
229    """"""
230    demuxer = spdl.io.Demuxer(input_path)
231    muxer = spdl.io.Muxer(output_path)
232
233    # Fetch the input config
234    audio_codec = demuxer.audio_codec
235
236    video_codec = demuxer.video_codec
237    frame_rate = video_codec.frame_rate
238    src_width = video_codec.width
239    src_height = video_codec.height
240
241    # Create decoder
242    video_decoder = spdl.io.Decoder(demuxer.video_codec)
243
244    # Configure output
245    src_pix_fmt = "rgb24"
246    enc_pix_fmt = "yuv420p"
247    enc_height = src_height // 2
248    enc_width = src_width // 2
249    filter_desc = get_filter_desc(
250        input_pix_fmt=src_pix_fmt,
251        input_width=src_width,
252        input_height=src_height,
253        frame_rate=frame_rate,
254        output_pix_fmt=enc_pix_fmt,
255        output_width=enc_width,
256        output_height=enc_height,
257    )
258    print(filter_desc)
259    filter_graph = spdl.io.FilterGraph(filter_desc)
260
261    video_encoder = muxer.add_encode_stream(
262        config=spdl.io.video_encode_config(
263            pix_fmt=enc_pix_fmt,
264            frame_rate=frame_rate,
265            height=enc_height,
266            width=enc_width,
267            colorspace="bt709",
268            color_primaries="bt709",
269            color_trc="bt709",
270        ),
271    )
272    muxer.add_remux_stream(audio_codec)
273    return demuxer, video_decoder, filter_graph, video_encoder, muxer
274
275
276def main() -> None:
277    """Entrypoint from the command line."""
278    args = parse_args()
279
280    demuxer, video_decoder, filter_graph, video_encoder, muxer = build_components(
281        args.input_path, args.output_path
282    )
283
284    process(
285        demuxer,
286        video_decoder,
287        filter_graph,
288        video_encoder,
289        muxer,
290    )
291
292
293if __name__ == "__main__":
294    main()

API Reference

Functions

main() None[source]

Entrypoint from the command line.

parse_args() Namespace[source]

Parse the command line arguments.

get_filter_desc(input_pix_fmt: str, input_width: int, input_height: int, frame_rate: tuple[int, int], output_pix_fmt: str, output_width: int | None = None, output_height: int | None = None) str[source]

Build a filter description that performs format conversion and optional scaling

Parameters:
  • input_pix_fmt – The input pixel format. Usually "rgb24".

  • input_width – The input frame resolution.

  • input_height – The input frame resolution.

  • frame_rate – The frame rate of the video.

  • output_pix_fmt – The output pixel format. It is the pixel format used by the encoder.

  • output_width – The output frame resolution.

  • output_height – The output frame resolution.

Returns:

The filter description.

process(demuxer: Demuxer, video_decoder: VideoDecoder, filter_graph: FilterGraph, video_encoder: VideoEncoder, muxer: Muxer) None[source]

The main processing logic.

Parameters:
  • demuxer – Demux audio/video streams from the source.

  • video_decoder – Decode the video packets.

  • filter_graph – Transform applied to the array data before encoding.

  • video_encoder – Encode the processed video array.

  • muxer – Multiplexer for remuxing audio packets and processed video packets.

build_components(input_path: Path, output_path: Path) tuple[Demuxer, VideoDecoder, FilterGraph, VideoEncoder, Muxer][source]