Skip to content

Commit 8102158

Browse files
authored
Miscellaneous fixes and improvements for video reading (#1161)
* Miscellaneous fixes and improvements * Guard against videos without video stream * Fix lint * Add test for packed b-frames videos * Fix missing import
1 parent 7c95f97 commit 8102158

File tree

3 files changed

+51
-5
lines changed

3 files changed

+51
-5
lines changed

test/test_io.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,18 @@
22
import contextlib
33
import tempfile
44
import torch
5+
import torchvision.datasets.utils as utils
56
import torchvision.io as io
67
import unittest
8+
import sys
9+
import warnings
710

11+
from common_utils import get_tmp_dir
12+
13+
if sys.version_info < (3,):
14+
from urllib2 import URLError
15+
else:
16+
from urllib.error import URLError
817

918
try:
1019
import av
@@ -104,6 +113,22 @@ def test_read_partial_video_bframes(self):
104113
self.assertEqual(len(lv), 4)
105114
self.assertTrue((data[4:8].float() - lv.float()).abs().max() < self.TOLERANCE)
106115

116+
@unittest.skipIf(av is None, "PyAV unavailable")
117+
def test_read_packed_b_frames_divx_file(self):
118+
with get_tmp_dir() as temp_dir:
119+
name = "hmdb51_Turnk_r_Pippi_Michel_cartwheel_f_cm_np2_le_med_6.avi"
120+
f_name = os.path.join(temp_dir, name)
121+
url = "https://download.pytorch.org/vision_tests/io/" + name
122+
try:
123+
utils.download_url(url, temp_dir)
124+
pts, fps = io.read_video_timestamps(f_name)
125+
self.assertEqual(pts, sorted(pts))
126+
self.assertEqual(fps, 30)
127+
except URLError:
128+
msg = "could not download test file '{}'".format(url)
129+
warnings.warn(msg, RuntimeWarning)
130+
raise unittest.SkipTest(msg)
131+
107132
# TODO add tests for audio
108133

109134

torchvision/datasets/video_utils.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ def subset(self, indices):
8484

8585
@staticmethod
8686
def compute_clips_for_video(video_pts, num_frames, step, fps, frame_rate):
87+
if fps is None:
88+
# if for some reason the video doesn't have fps (because doesn't have a video stream)
89+
# set the fps to 1. The value doesn't matter, because video_pts is empty anyway
90+
fps = 1
8791
if frame_rate is None:
8892
frame_rate = fps
8993
total_frames = len(video_pts) * (float(frame_rate) / fps)

torchvision/io/video.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import re
12
import gc
23
import torch
34
import numpy as np
@@ -68,18 +69,34 @@ def _read_from_stream(container, start_offset, end_offset, stream, stream_name):
6869
should_buffer = False
6970
max_buffer_size = 5
7071
if stream.type == "video":
71-
# TODO consider also using stream.codec_context.codec.reorder
72-
# videos with b frames can have out-of-order pts
72+
# DivX-style packed B-frames can have out-of-order pts (2 frames in a single pkt)
7373
# so need to buffer some extra frames to sort everything
7474
# properly
75-
should_buffer = stream.codec_context.has_b_frames
75+
extradata = stream.codec_context.extradata
76+
# overly complicated way of finding if `divx_packed` is set, following
77+
# https://github.com/FFmpeg/FFmpeg/commit/d5a21172283572af587b3d939eba0091484d3263
78+
if extradata and b"DivX" in extradata:
79+
# can't use regex directly because of some weird characters sometimes...
80+
pos = extradata.find(b"DivX")
81+
d = extradata[pos:]
82+
o = re.search(br"DivX(\d+)Build(\d+)(\w)", d)
83+
if o is None:
84+
o = re.search(br"DivX(\d+)b(\d+)(\w)", d)
85+
if o is not None:
86+
should_buffer = o.group(3) == b"p"
7687
seek_offset = start_offset
88+
# some files don't seek to the right location, so better be safe here
89+
seek_offset = max(seek_offset - 1, 0)
7790
if should_buffer:
7891
# FIXME this is kind of a hack, but we will jump to the previous keyframe
7992
# so this will be safe
8093
seek_offset = max(seek_offset - max_buffer_size, 0)
81-
# TODO check if stream needs to always be the video stream here or not
82-
container.seek(seek_offset, any_frame=False, backward=True, stream=stream)
94+
try:
95+
# TODO check if stream needs to always be the video stream here or not
96+
container.seek(seek_offset, any_frame=False, backward=True, stream=stream)
97+
except av.AVError:
98+
print("Corrupted file?", container.name)
99+
return []
83100
buffer_count = 0
84101
for idx, frame in enumerate(container.decode(**stream_name)):
85102
frames[frame.pts] = frame

0 commit comments

Comments
 (0)