99Streaming data through file-like support
1010===================================================================
1111
12- In this example, we will describe the feature with references to its docs."""
12+ In this example, we will show how to decode streaming data. That is, when files
13+ do not reside locally, we will show how to only download the data segments that
14+ are needed to decode the frames you care about. We accomplish this capability
15+ with Python
16+ `file-like objects <https://docs.python.org/3/glossary.html#term-file-like-object>`_."""
1317
1418# %%
15- # First, a bit of boilerplate: TODO.
19+ # First, a bit of boilerplate. We define two functions: one to download content
20+ # from a given URL, and another to time the execution of a given function.
1621
1722
1823import torch
1924import requests
2025from time import perf_counter_ns
2126
27+
2228def get_url_content (url ):
2329 response = requests .get (url , headers = {"User-Agent" : "" })
2430 if response .status_code != 200 :
@@ -42,6 +48,19 @@ def bench(f, average_over=20, warmup=2):
4248 med = times .median ().item ()
4349 print (f"{ med = :.2f} ms +- { std :.2f} " )
4450
51+ # %%
52+ # Performance: downloading first versus streaming
53+ # -----------------------------------------------
54+ #
55+ # We are going to investigate the cost of having to download an entire video
56+ # before decoding any frames versus being able to stream the video's data
57+ # while decoding. To demonsrate an extreme case, we're going to always decode
58+ # just the first frame of the video, while we vary how we get that video's
59+ # data.
60+ #
61+ # The video we're going to use in this tutorial is publicly available on the
62+ # internet. We perform an initial download of it so that we can understand
63+ # its size and content:
4564
4665from torchcodec .decoders import VideoDecoder
4766
@@ -54,39 +73,175 @@ def bench(f, average_over=20, warmup=2):
5473print (decoder .metadata )
5574print ()
5675
76+ # %%
77+ # We can see that the video is about 253 MB, has the resolution 1920x1080, is
78+ # about 30 frames per second and is almost 3 and a half minutes long. As we
79+ # only want to decode the first frame, we would clearly benefit from not having
80+ # to download the entire video!
81+ #
82+ # Let's first test three scenarios:
83+ #
84+ # 1. Decode from the *existing* video we just downloaded. This is our baseline
85+ # performance, as we've reduced the downloading cost to 0.
86+ # 2. Download the entire video before decoding. This is the worst case
87+ # that we want to avoid.
88+ # 3. Provde the URL directly to the :class:`~torchcodec.decoders.VideoDecoder` class, which will pass
89+ # the URL on to FFmpeg. Then FFmpeg will decide how much of the video to
90+ # download before decoding.
91+ #
92+ # Note that in our scenarios, we are always setting the ``seek_mode`` parameter of
93+ # the :class:`~torchcodec.decoders.VideoDecoder` class to ``"approximate"``. We do
94+ # this to avoid scanning the entire video during initialization, which would
95+ # require downloading the entire video even if we only want to decode the first
96+ # frame. See :ref:`sphx_glr_generated_examples_approximate_mode.py` for more.
97+
5798def decode_from_existing_download ():
58- decoder = VideoDecoder (pre_downloaded_raw_video_bytes , seek_mode = "approximate" )
99+ decoder = VideoDecoder (
100+ source = pre_downloaded_raw_video_bytes ,
101+ seek_mode = "approximate" ,
102+ )
59103 return decoder [0 ]
60104
105+
61106def download_before_decode ():
62107 raw_video_bytes = get_url_content (nasa_url )
63- decoder = VideoDecoder (raw_video_bytes , seek_mode = "approximate" )
108+ decoder = VideoDecoder (
109+ source = raw_video_bytes ,
110+ seek_mode = "approximate" ,
111+ )
64112 return decoder [0 ]
65113
114+
66115def direct_url_to_ffmpeg ():
67- decoder = VideoDecoder (nasa_url , seek_mode = "approximate" )
116+ decoder = VideoDecoder (
117+ source = nasa_url ,
118+ seek_mode = "approximate" ,
119+ )
68120 return decoder [0 ]
69121
70- print ("Decode from existing download" )
122+
123+ print ("Decode from existing download:" )
71124bench (decode_from_existing_download )
72125print ()
73126
74- print ("Download before decode: " )
127+ print ("Download before decode:" )
75128bench (download_before_decode )
76129print ()
77130
78- print ("Direct url to FFmpeg: " )
131+ print ("Direct url to FFmpeg:" )
79132bench (direct_url_to_ffmpeg )
80133print ()
81134
135+ # %%
136+ # Decoding the already downloaded video is clearly the fastest. Having to
137+ # download the entire video each time we want to decode just the first frame
138+ # is over 4x slower than decoding an existing video. Providing a direct URL
139+ # is much better, as its about 2.5x faster than downloding the video first.
140+ #
141+ # We can do better, and the way how is to use a file-like object which
142+ # implements its own read and seek methods that only download data from a URL as
143+ # needed. Rather than implementing our own, we can use such objects from the
144+ # `fsspec <https://github.com/fsspec/filesystem_spec>`_ module that provides
145+ # `Filesystem interfaces for Python <https://filesystem-spec.readthedocs.io/en/latest/?badge=latest>`_.
146+
82147import fsspec
83- # Note: we also need: aiohttp
84148
85149def stream_while_decode ():
150+ # The `client_kwargs` are passed down to the aiohttp module's client
151+ # session; we need to indicate that we need to trust the environment
152+ # settings for proxy configuration. Depending on your environment, you may
153+ # not need this setting.
86154 with fsspec .open (nasa_url , client_kwargs = {'trust_env' : True }) as file :
87155 decoder = VideoDecoder (file , seek_mode = "approximate" )
88156 return decoder [0 ]
89157
158+
90159print ("Stream while decode: " )
91160bench (stream_while_decode )
92161print ()
162+
163+ # %%
164+ # Streaming the data through a file-like object is about 4.3x faster than
165+ # downloading the video first. And not only is it about 1.7x faster than
166+ # providing a direct URL, it's more general. :class:`~torchcodec.decoders.VideoDecoder` supports
167+ # direct URLs because the underlying FFmpeg functions support them. But the
168+ # kinds of protocols supported are determined by what that version of FFmpeg
169+ # supports. A file-like object can adapt any kind of resource, including ones
170+ # that are specific to your own infrastructure and are unknown to FFmpeg.
171+
172+
173+ # %%
174+ # How it works
175+ # ------------
176+ #
177+
178+ from pathlib import Path
179+ import tempfile
180+
181+ temp_dir = tempfile .mkdtemp ()
182+ nasa_video_path = Path (temp_dir ) / "nasa_video.mp4"
183+ with open (nasa_video_path , "wb" ) as f :
184+ f .write (pre_downloaded_raw_video_bytes )
185+
186+
187+ class FileOpCounter :
188+ def __init__ (self , file ):
189+ self ._file = file
190+ self .num_reads = 0
191+ self .num_seeks = 0
192+
193+ def read (self , size : int ) -> bytes :
194+ self .num_reads += 1
195+ return self ._file .read (size )
196+
197+ def seek (self , offset : int , whence : int ) -> bytes :
198+ self .num_seeks += 1
199+ return self ._file .seek (offset , whence )
200+
201+
202+ file_op_counter = FileOpCounter (open (nasa_video_path , "rb" ))
203+ counter_decoder = VideoDecoder (file_op_counter , seek_mode = "approximate" )
204+
205+ print ("Decoder initialization required "
206+ f"{ file_op_counter .num_reads } reads and "
207+ f"{ file_op_counter .num_seeks } seeks." )
208+
209+ init_reads = file_op_counter .num_reads
210+ init_seeks = file_op_counter .num_seeks
211+
212+ first_frame = counter_decoder [0 ]
213+
214+ print ("Decoding the first frame required "
215+ f"{ file_op_counter .num_reads - init_reads } additional reads and "
216+ f"{ file_op_counter .num_seeks - init_seeks } additional seeks." )
217+ print ()
218+
219+ # %%
220+ # Performance: local file path versus local file-like object
221+ # ----------------------------------------------------------
222+ #
223+
224+
225+ def decode_from_existing_file_path ():
226+ decoder = VideoDecoder (nasa_video_path , seek_mode = "approximate" )
227+ return decoder [0 ]
228+
229+
230+ def decode_from_existing_open_file_object ():
231+ with open (nasa_video_path , "rb" ) as f :
232+ decoder = VideoDecoder (f , seek_mode = "approximate" )
233+ return decoder [0 ]
234+
235+
236+ print ("Decode from existing file path:" )
237+ bench (decode_from_existing_file_path )
238+ print ()
239+
240+ print ("Decode from existing open file object:" )
241+ bench (decode_from_existing_open_file_object )
242+ print ()
243+
244+ # %%
245+ import shutil
246+ shutil .rmtree (temp_dir )
247+ # %%
0 commit comments