1616`file-like objects <https://docs.python.org/3/glossary.html#term-file-like-object>`_.
1717Our example uses a video file, so we use the :class:`~torchcodec.decoders.VideoDecoder`
1818class to decode it. But all of the lessons here also apply to audio files and the
19- :class:`~torchcodec.decoders.AudioDecoder` class."""
19+ :class:`~torchcodec.decoders.AudioDecoder` class as well ."""
2020
2121# %%
2222# First, a bit of boilerplate. We define two functions: one to download content
@@ -146,9 +146,9 @@ def direct_url_to_ffmpeg():
146146# needed. Rather than implementing our own, we can use such objects from the
147147# `fsspec <https://github.com/fsspec/filesystem_spec>`_ module that provides
148148# `Filesystem interfaces for Python <https://filesystem-spec.readthedocs.io/en/latest/?badge=latest>`_.
149- # Note that using these capabilities from the ` fsspec` library also requires the
149+ # Note that using these capabilities from the fsspec` library also requires the
150150# `aiohttp <https://docs.aiohttp.org/en/stable/>`_ module. You can install both with
151- # `pip install fsspec aiohttp`.
151+ # `` pip install fsspec aiohttp` `.
152152
153153import fsspec
154154
@@ -179,17 +179,32 @@ def stream_while_decode():
179179# %%
180180# How it works
181181# ------------
182+ # In Python, a `file-like object <https://docs.python.org/3/glossary.html#term-file-like-object>`_
183+ # is any object that exposes special methods for reading, writing and seeking.
184+ # While such methods are obviously file oriented, it's not required that
185+ # a file-like object is backed by an actual file. As far as Python is concerned,
186+ # if an object acts like a file, it's a file. This is a powerful concept, as
187+ # it enables libraries that read or write data to assume a file-like interface.
188+ # Other libraries that present novel resources can then be easily used by
189+ # providing a file-like wrapper for their resource.
182190#
191+ # For our case, we only need the read and seek methods for decoding. The exact
192+ # method signature needed is in the example below. Rather than wrap a novel
193+ # resource, we demonstrate this capability by wrapping an actual file while
194+ # counting how often each method is called.
183195
184196from pathlib import Path
185197import tempfile
186198
199+ # Create a local file to interact with.
187200temp_dir = tempfile .mkdtemp ()
188201nasa_video_path = Path (temp_dir ) / "nasa_video.mp4"
189202with open (nasa_video_path , "wb" ) as f :
190203 f .write (pre_downloaded_raw_video_bytes )
191204
192205
206+ # A file-like class that is backed by an actual file, but it intercepts reads
207+ # and seeks to maintain counts.
193208class FileOpCounter :
194209 def __init__ (self , file ):
195210 self ._file = file
@@ -205,6 +220,9 @@ def seek(self, offset: int, whence: int) -> bytes:
205220 return self ._file .seek (offset , whence )
206221
207222
223+ # Let's now get a file-like object from our class defined above, providing it a
224+ # reference to the file we created. We pass our file-like object to the decoder
225+ # rather than the file itself.
208226file_op_counter = FileOpCounter (open (nasa_video_path , "rb" ))
209227counter_decoder = VideoDecoder (file_op_counter , seek_mode = "approximate" )
210228
@@ -221,10 +239,35 @@ def seek(self, offset: int, whence: int) -> bytes:
221239 f"{ file_op_counter .num_reads - init_reads } additional reads and "
222240 f"{ file_op_counter .num_seeks - init_seeks } additional seeks." )
223241
242+ # %%
243+ # While we defined a simple class primarily for demonstration, it's actually
244+ # useful for diagnosing how much reading and seeking are required for different
245+ # decoding operations. We've also introduced a mystery that we should answer:
246+ # why does *initializing* the decoder take more reads and seeks than decoding
247+ # the first frame? The answer is that in our decoder implementation, we're
248+ # actually calling a special
249+ # `FFmpeg function <https://ffmpeg.org/doxygen/6.1/group__lavf__decoding.html#gad42172e27cddafb81096939783b157bb>`_
250+ # that decodes the first few frames to return more robust metadata.
251+ #
252+ # It's also worth noting that the Python file-like interface is only half of
253+ # the story. FFmpeg also has its own mechanism for directing reads and seeks
254+ # during decoding to user-define functions. TorchCodec does the work of
255+ # connecting the Python methods you define to FFmpeg. All you have to do is
256+ # define your methods in Python, and TorchCodec handles the rest.
257+
224258# %%
225259# Performance: local file path vs. local file-like object
226- # ----------------------------------------------------------
260+ # -------------------------------------------------------
261+ #
262+ # Since we have a local file defined, let's do a bonus performance test. We now
263+ # have two means of providing a local file to TorchCodec:
227264#
265+ # 1. Through a path, where TorchCodec will then do the work of opening the
266+ # local file at that path.
267+ # 2. Through a file-like object, where you open the file yourself and provide
268+ # the file-like object to TorchCodec.
269+ #
270+ # An obvious question is: which is faster? The code below tests that question.
228271
229272
230273def decode_from_existing_file_path ():
@@ -246,6 +289,14 @@ def decode_from_existing_open_file_object():
246289bench (decode_from_existing_open_file_object )
247290
248291# %%
292+ # Thankfully, the answer is both means of decoding from a local file take about
293+ # the same amount of time. This result means that in your own code, you can use
294+ # whichever method is more convienient. What this result implies is that the
295+ # cost of actually reading and copying data dominates the cost of calling Python
296+ # methods while decoding.
297+
298+ # %%
299+ # Finally, let's clean up the local resources we created.
249300import shutil
250301shutil .rmtree (temp_dir )
251302# %%
0 commit comments