Skip to content

Commit dbbf62e

Browse files
committed
Current draft of file like tutorial
1 parent ab3ea25 commit dbbf62e

File tree

2 files changed

+166
-9
lines changed

2 files changed

+166
-9
lines changed

docs/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ sphinx-tabs
66
matplotlib
77
torchvision
88
ipython
9+
fsspec
10+
aiohttp
911
-e git+https://github.com/pytorch/pytorch_sphinx_theme.git#egg=pytorch_sphinx_theme

examples/file_like.py

Lines changed: 164 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,22 @@
99
Streaming 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

1823
import torch
1924
import requests
2025
from time import perf_counter_ns
2126

27+
2228
def 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

4665
from torchcodec.decoders import VideoDecoder
4766

@@ -54,39 +73,175 @@ def bench(f, average_over=20, warmup=2):
5473
print(decoder.metadata)
5574
print()
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+
5798
def 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+
61106
def 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+
66115
def 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:")
71124
bench(decode_from_existing_download)
72125
print()
73126

74-
print("Download before decode: ")
127+
print("Download before decode:")
75128
bench(download_before_decode)
76129
print()
77130

78-
print("Direct url to FFmpeg: ")
131+
print("Direct url to FFmpeg:")
79132
bench(direct_url_to_ffmpeg)
80133
print()
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+
82147
import fsspec
83-
# Note: we also need: aiohttp
84148

85149
def 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+
90159
print("Stream while decode: ")
91160
bench(stream_while_decode)
92161
print()
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

Comments
 (0)