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.
The following diagram illustrates how audio/video data are processed.
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
- 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]¶