-
Notifications
You must be signed in to change notification settings - Fork 3.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix task creation with video file when there are no valid keyframes #7838
Changes from 1 commit
e54db49
07567c9
8d16933
ee90696
20741d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -4,7 +4,7 @@ | |||||||||||||||||
# SPDX-License-Identifier: MIT | ||||||||||||||||||
|
||||||||||||||||||
from enum import Enum | ||||||||||||||||||
from io import StringIO | ||||||||||||||||||
from io import StringIO, BytesIO | ||||||||||||||||||
import av | ||||||||||||||||||
import json | ||||||||||||||||||
import os | ||||||||||||||||||
|
@@ -13,12 +13,11 @@ | |||||||||||||||||
from contextlib import closing | ||||||||||||||||||
from PIL import Image | ||||||||||||||||||
from json.decoder import JSONDecodeError | ||||||||||||||||||
from io import BytesIO | ||||||||||||||||||
|
||||||||||||||||||
from .errors import InvalidManifestError, InvalidVideoFrameError | ||||||||||||||||||
from .errors import InvalidManifestError, InvalidVideoError | ||||||||||||||||||
from .utils import SortingMethod, md5_hash, rotate_image, sort | ||||||||||||||||||
|
||||||||||||||||||
from typing import Dict, List, Union, Optional | ||||||||||||||||||
from typing import Dict, List, Union, Optional, Iterator, Tuple | ||||||||||||||||||
|
||||||||||||||||||
class VideoStreamReader: | ||||||||||||||||||
def __init__(self, source_path, chunk_size, force): | ||||||||||||||||||
|
@@ -33,7 +32,7 @@ def __init__(self, source_path, chunk_size, force): | |||||||||||||||||
for frame in packet.decode(): | ||||||||||||||||||
# check type of first frame | ||||||||||||||||||
if not frame.pict_type.name == 'I': | ||||||||||||||||||
raise InvalidVideoFrameError('First frame is not key frame') | ||||||||||||||||||
raise InvalidVideoError('First frame is not key frame') | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure that the error message is clear and actionable. Consider providing more context about why the first frame must be a key frame. - raise InvalidVideoError('First frame is not key frame')
+ raise InvalidVideoError('The first frame is not a key frame, which is required for processing.') Committable suggestion
Suggested change
|
||||||||||||||||||
|
||||||||||||||||||
# get video resolution | ||||||||||||||||||
if video_stream.metadata.get('rotate'): | ||||||||||||||||||
|
@@ -75,40 +74,66 @@ def validate_key_frame(self, container, video_stream, key_frame): | |||||||||||||||||
return False | ||||||||||||||||||
return True | ||||||||||||||||||
|
||||||||||||||||||
def __iter__(self): | ||||||||||||||||||
with closing(av.open(self.source_path, mode='r')) as container: | ||||||||||||||||||
video_stream = self._get_video_stream(container) | ||||||||||||||||||
frame_pts, frame_dts = -1, -1 | ||||||||||||||||||
index, key_frame_number = 0, 0 | ||||||||||||||||||
for packet in container.demux(video_stream): | ||||||||||||||||||
def __iter__(self) -> Iterator[Union[int, Tuple[int, int, str]]]: | ||||||||||||||||||
""" | ||||||||||||||||||
Iterate over video frames and yield key frames or indexes. | ||||||||||||||||||
|
||||||||||||||||||
Yields: | ||||||||||||||||||
Union[Tuple[int, int, str], int]: (frame index, frame timestamp, frame MD5) or frame index. | ||||||||||||||||||
""" | ||||||||||||||||||
# Open containers for reading frames and checking movement on them | ||||||||||||||||||
with ( | ||||||||||||||||||
closing(av.open(self.source_path, mode='r')) as reading_container, | ||||||||||||||||||
closing(av.open(self.source_path, mode='r')) as checking_container | ||||||||||||||||||
): | ||||||||||||||||||
reading_v_stream = self._get_video_stream(reading_container) | ||||||||||||||||||
checking_v_stream = self._get_video_stream(checking_container) | ||||||||||||||||||
prev_pts: Optional[int] = None | ||||||||||||||||||
prev_dts: Optional[int] = None | ||||||||||||||||||
index, key_frame_count = 0, 0 | ||||||||||||||||||
|
||||||||||||||||||
for packet in reading_container.demux(reading_v_stream): | ||||||||||||||||||
for frame in packet.decode(): | ||||||||||||||||||
if None not in {frame.pts, frame_pts} and frame.pts <= frame_pts: | ||||||||||||||||||
raise InvalidVideoFrameError('Invalid pts sequences') | ||||||||||||||||||
if None not in {frame.dts, frame_dts} and frame.dts <= frame_dts: | ||||||||||||||||||
raise InvalidVideoFrameError('Invalid dts sequences') | ||||||||||||||||||
frame_pts, frame_dts = frame.pts, frame.dts | ||||||||||||||||||
# Check PTS and DTS sequences for validity | ||||||||||||||||||
if None not in {frame.pts, prev_pts} and frame.pts <= prev_pts: | ||||||||||||||||||
raise InvalidVideoError('Invalid pts sequences') | ||||||||||||||||||
if None not in {frame.dts, prev_dts} and frame.dts <= prev_dts: | ||||||||||||||||||
raise InvalidVideoError('Invalid dts sequences') | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The error handling for invalid PTS and DTS sequences could be more descriptive. Provide details about what makes the sequences invalid. - raise InvalidVideoError('Invalid pts sequences')
- raise InvalidVideoError('Invalid dts sequences')
+ raise InvalidVideoError('Detected non-increasing PTS sequence, which is invalid.')
+ raise InvalidVideoError('Detected non-increasing DTS sequence, which is invalid.') Committable suggestion
Suggested change
|
||||||||||||||||||
prev_pts, prev_dts = frame.pts, frame.dts | ||||||||||||||||||
|
||||||||||||||||||
if frame.key_frame: | ||||||||||||||||||
key_frame_number += 1 | ||||||||||||||||||
ratio = (index + 1) // key_frame_number | ||||||||||||||||||
|
||||||||||||||||||
if ratio >= self._upper_bound and not self._force: | ||||||||||||||||||
raise AssertionError('Too few keyframes') | ||||||||||||||||||
|
||||||||||||||||||
key_frame = { | ||||||||||||||||||
'index': index, | ||||||||||||||||||
key_frame_data = { | ||||||||||||||||||
'pts': frame.pts, | ||||||||||||||||||
'md5': md5_hash(frame) | ||||||||||||||||||
'md5': md5_hash(frame), | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
with closing(av.open(self.source_path, mode='r')) as checked_container: | ||||||||||||||||||
checked_container.seek(offset=key_frame['pts'], stream=video_stream) | ||||||||||||||||||
isValid = self.validate_key_frame(checked_container, video_stream, key_frame) | ||||||||||||||||||
if isValid: | ||||||||||||||||||
yield (index, key_frame['pts'], key_frame['md5']) | ||||||||||||||||||
# Check that it is possible to seek to this key frame using frame.pts | ||||||||||||||||||
checking_container.seek( | ||||||||||||||||||
offset=key_frame_data['pts'], | ||||||||||||||||||
stream=checking_v_stream, | ||||||||||||||||||
) | ||||||||||||||||||
is_valid_key_frame = self.validate_key_frame( | ||||||||||||||||||
checking_container, | ||||||||||||||||||
checking_v_stream, | ||||||||||||||||||
key_frame_data, | ||||||||||||||||||
) | ||||||||||||||||||
|
||||||||||||||||||
if is_valid_key_frame: | ||||||||||||||||||
key_frame_count += 1 | ||||||||||||||||||
yield (index, key_frame_data['pts'], key_frame_data['md5']) | ||||||||||||||||||
else: | ||||||||||||||||||
yield index | ||||||||||||||||||
else: | ||||||||||||||||||
yield index | ||||||||||||||||||
|
||||||||||||||||||
index += 1 | ||||||||||||||||||
key_frame_ratio = index // (key_frame_count or 1) | ||||||||||||||||||
|
||||||||||||||||||
# Check if the number of key frames meets the upper bound | ||||||||||||||||||
if key_frame_ratio >= self._upper_bound and not self._force: | ||||||||||||||||||
raise InvalidVideoError('Too few keyframes') | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider handling the scenario where there are too few keyframes more gracefully, possibly by logging a warning and continuing with a fallback mechanism. - raise InvalidVideoError('Too few keyframes')
+ logging.warning('Detected too few keyframes. Proceeding with reduced functionality.') Committable suggestion
Suggested change
|
||||||||||||||||||
|
||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The iterator method is complex and handles multiple scenarios. Consider refactoring to improve readability and maintainability. |
||||||||||||||||||
# Update frames number if not already set | ||||||||||||||||||
if not self._frames_number: | ||||||||||||||||||
self._frames_number = index | ||||||||||||||||||
|
||||||||||||||||||
|
@@ -317,6 +342,9 @@ def __getitem__(self, number): | |||||||||||||||||
def __len__(self): | ||||||||||||||||||
return len(self._index) | ||||||||||||||||||
|
||||||||||||||||||
def is_empty(self) -> bool: | ||||||||||||||||||
return not bool(self) | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't it be better to explicitly call |
||||||||||||||||||
|
||||||||||||||||||
class _ManifestManager(ABC): | ||||||||||||||||||
BASE_INFORMATION = { | ||||||||||||||||||
'version' : 1, | ||||||||||||||||||
|
@@ -405,10 +433,12 @@ def manifest(self): | |||||||||||||||||
return self._manifest | ||||||||||||||||||
|
||||||||||||||||||
def __len__(self): | ||||||||||||||||||
if hasattr(self, '_index'): | ||||||||||||||||||
return len(self._index) | ||||||||||||||||||
else: | ||||||||||||||||||
return None | ||||||||||||||||||
return len(self._index) | ||||||||||||||||||
|
||||||||||||||||||
def is_empty(self) -> bool: | ||||||||||||||||||
if self._index.is_empty(): | ||||||||||||||||||
self._index.load() | ||||||||||||||||||
return self._index.is_empty() | ||||||||||||||||||
|
||||||||||||||||||
def __getitem__(self, item): | ||||||||||||||||||
if isinstance(item, slice): | ||||||||||||||||||
|
@@ -482,6 +512,9 @@ def create(self, *, _tqdm=None): # pylint: disable=arguments-differ | |||||||||||||||||
|
||||||||||||||||||
self.set_index() | ||||||||||||||||||
|
||||||||||||||||||
if self.is_empty() and not self._reader._force: | ||||||||||||||||||
raise InvalidManifestError('Empty manifest file has been created') | ||||||||||||||||||
|
||||||||||||||||||
Comment on lines
+515
to
+517
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Raising an error for an empty manifest creation could be handled more gracefully. Consider allowing the user to decide how to proceed. - raise InvalidManifestError('Empty manifest file has been created')
+ if self._user_prompt:
+ user_decision = input('Empty manifest file has been created. Would you like to proceed? (y/n): ')
+ if user_decision.lower() != 'y':
+ raise InvalidManifestError('Operation cancelled by the user.') Committable suggestion
Suggested change
|
||||||||||||||||||
def partial_update(self, number, properties): | ||||||||||||||||||
pass | ||||||||||||||||||
|
||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider removing unused imports to clean up the code.
Committable suggestion