Skip to content

Commit 91128ac

Browse files
committed
First full draft
1 parent c340d60 commit 91128ac

File tree

1 file changed

+55
-4
lines changed

1 file changed

+55
-4
lines changed

examples/file_like.py

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
`file-like objects <https://docs.python.org/3/glossary.html#term-file-like-object>`_.
1717
Our example uses a video file, so we use the :class:`~torchcodec.decoders.VideoDecoder`
1818
class 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

153153
import 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

184196
from pathlib import Path
185197
import tempfile
186198

199+
# Create a local file to interact with.
187200
temp_dir = tempfile.mkdtemp()
188201
nasa_video_path = Path(temp_dir) / "nasa_video.mp4"
189202
with 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.
193208
class 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.
208226
file_op_counter = FileOpCounter(open(nasa_video_path, "rb"))
209227
counter_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

230273
def decode_from_existing_file_path():
@@ -246,6 +289,14 @@ def decode_from_existing_open_file_object():
246289
bench(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.
249300
import shutil
250301
shutil.rmtree(temp_dir)
251302
# %%

0 commit comments

Comments
 (0)