yta-video-opengl 0.0.5__py3-none-any.whl → 0.0.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- yta_video_opengl/classes.py +1091 -0
- yta_video_opengl/{reader.py → reader/__init__.py} +52 -5
- yta_video_opengl/reader/cache.py +155 -0
- yta_video_opengl/tests.py +675 -55
- yta_video_opengl/utils.py +24 -0
- yta_video_opengl/writer.py +40 -1
- {yta_video_opengl-0.0.5.dist-info → yta_video_opengl-0.0.6.dist-info}/METADATA +1 -1
- yta_video_opengl-0.0.6.dist-info/RECORD +11 -0
- yta_video_opengl-0.0.5.dist-info/RECORD +0 -8
- {yta_video_opengl-0.0.5.dist-info → yta_video_opengl-0.0.6.dist-info}/LICENSE +0 -0
- {yta_video_opengl-0.0.5.dist-info → yta_video_opengl-0.0.6.dist-info}/WHEEL +0 -0
@@ -2,6 +2,7 @@
|
|
2
2
|
A video reader using the PyAv (av) library
|
3
3
|
that, using ffmpeg, detects the video.
|
4
4
|
"""
|
5
|
+
from yta_video_opengl.reader.cache import VideoFrameCache
|
5
6
|
from yta_validation import PythonValidator
|
6
7
|
from av.video.frame import VideoFrame
|
7
8
|
from av.audio.frame import AudioFrame
|
@@ -308,23 +309,58 @@ class VideoReader:
|
|
308
309
|
"""
|
309
310
|
The filename of the video source.
|
310
311
|
"""
|
311
|
-
self.container: InputContainer =
|
312
|
+
self.container: InputContainer = None
|
312
313
|
"""
|
313
314
|
The av input general container of the
|
314
315
|
video (that also includes the audio) we
|
315
316
|
are reading.
|
316
317
|
"""
|
317
|
-
self.video_stream: VideoStream =
|
318
|
+
self.video_stream: VideoStream = None
|
318
319
|
"""
|
319
320
|
The stream that includes the video.
|
320
321
|
"""
|
321
|
-
self.video_stream.thread_type = 'AUTO'
|
322
322
|
# TODO: What if no audio (?)
|
323
|
-
self.audio_stream: AudioStream =
|
323
|
+
self.audio_stream: AudioStream = None
|
324
324
|
"""
|
325
325
|
The stream that includes the audio.
|
326
326
|
"""
|
327
|
-
self.
|
327
|
+
self.cache: VideoFrameCache = None
|
328
|
+
"""
|
329
|
+
The frame cache system to optimize
|
330
|
+
the way we access to the frames.
|
331
|
+
"""
|
332
|
+
|
333
|
+
# TODO: Maybe we can read the first
|
334
|
+
# frame, store it and reset, so we have
|
335
|
+
# it in memory since the first moment.
|
336
|
+
# We should do it here because if we
|
337
|
+
# iterate in some moment and then we
|
338
|
+
# want to obtain it... it will be
|
339
|
+
# difficult.
|
340
|
+
# Lets load the variables
|
341
|
+
self.reset()
|
342
|
+
|
343
|
+
def reset(
|
344
|
+
self
|
345
|
+
) -> 'VideoReader':
|
346
|
+
"""
|
347
|
+
Reset all the instances, closing the file
|
348
|
+
and opening again.
|
349
|
+
|
350
|
+
This will also return to the first frame.
|
351
|
+
"""
|
352
|
+
if self.container is not None:
|
353
|
+
# TODO: Maybe accept forcing it (?)
|
354
|
+
self.container.seek(0)
|
355
|
+
#self.container.close()
|
356
|
+
else:
|
357
|
+
self.container = av_open(self.filename)
|
358
|
+
# TODO: Should this be 'AUTO' (?)
|
359
|
+
self.video_stream = self.container.streams.video[0]
|
360
|
+
self.video_stream.thread_type = 'AUTO'
|
361
|
+
self.audio_stream = self.container.streams.audio[0]
|
362
|
+
self.audio_stream.thread_type = 'AUTO'
|
363
|
+
self.cache = VideoFrameCache(self)
|
328
364
|
|
329
365
|
def iterate(
|
330
366
|
self
|
@@ -373,6 +409,17 @@ class VideoReader:
|
|
373
409
|
# Return the packet as it is
|
374
410
|
yield VideoReaderPacket(packet)
|
375
411
|
|
412
|
+
# TODO: Will we use this (?)
|
413
|
+
def get_frame(
|
414
|
+
self,
|
415
|
+
index: int
|
416
|
+
) -> 'VideoFrame':
|
417
|
+
"""
|
418
|
+
Get the frame with the given 'index', using
|
419
|
+
the cache system.
|
420
|
+
"""
|
421
|
+
return self.cache.get_frame(index)
|
422
|
+
|
376
423
|
|
377
424
|
|
378
425
|
|
@@ -0,0 +1,155 @@
|
|
1
|
+
"""
|
2
|
+
The pyav container stores the information based
|
3
|
+
on the packets timestamps (called 'pts'). Some
|
4
|
+
of the packets are considered key_frames because
|
5
|
+
they include those key frames.
|
6
|
+
|
7
|
+
Also, this library uses those key frames to start
|
8
|
+
decodifying from there to the next one, obtaining
|
9
|
+
all the frames in between able to be read and
|
10
|
+
modified.
|
11
|
+
|
12
|
+
This cache system will look for the range of
|
13
|
+
frames that belong to the key frame related to the
|
14
|
+
frame we are requesting in the moment, keeping in
|
15
|
+
memory all those frames to be handled fast. It
|
16
|
+
will remove the old frames if needed to use only
|
17
|
+
the 'size' we set when creating it.
|
18
|
+
"""
|
19
|
+
from collections import OrderedDict
|
20
|
+
|
21
|
+
|
22
|
+
class VideoFrameCache:
|
23
|
+
"""
|
24
|
+
Class to manage the frames cache of a video
|
25
|
+
within a video reader instance.
|
26
|
+
"""
|
27
|
+
|
28
|
+
@property
|
29
|
+
def container(
|
30
|
+
self
|
31
|
+
) -> 'InputContainer':
|
32
|
+
"""
|
33
|
+
Shortcut to the video reader instance container.
|
34
|
+
"""
|
35
|
+
return self.reader_instance.container
|
36
|
+
|
37
|
+
@property
|
38
|
+
def stream(
|
39
|
+
self
|
40
|
+
) -> 'VideoStream':
|
41
|
+
"""
|
42
|
+
Shortcut to the video reader instance video
|
43
|
+
stream.
|
44
|
+
"""
|
45
|
+
return self.reader_instance.video_stream
|
46
|
+
|
47
|
+
def __init__(
|
48
|
+
self,
|
49
|
+
reader: 'VideoReader',
|
50
|
+
size: int = 50
|
51
|
+
):
|
52
|
+
self.reader_instance: 'VideoReader' = reader
|
53
|
+
"""
|
54
|
+
The video reader instance this cache belongs
|
55
|
+
to.
|
56
|
+
"""
|
57
|
+
self.cache: OrderedDict = OrderedDict()
|
58
|
+
"""
|
59
|
+
The cache ordered dictionary.
|
60
|
+
"""
|
61
|
+
self.size = size
|
62
|
+
"""
|
63
|
+
The size (in number of frames) of the cache.
|
64
|
+
"""
|
65
|
+
self.key_frames_pts: list[int] = []
|
66
|
+
"""
|
67
|
+
The list that contains the timestamps of the
|
68
|
+
key frame packets, ordered from begining to
|
69
|
+
end.
|
70
|
+
"""
|
71
|
+
|
72
|
+
# Index key frames
|
73
|
+
for packet in self.container.demux(self.stream):
|
74
|
+
if packet.is_keyframe:
|
75
|
+
self.key_frames_pts.append(packet.pts)
|
76
|
+
|
77
|
+
self.container.seek(0)
|
78
|
+
# TODO: Maybe this is better (?)
|
79
|
+
#self.reader_instance.reset()
|
80
|
+
|
81
|
+
def _get_frame_by_pts(
|
82
|
+
self,
|
83
|
+
target_pts
|
84
|
+
):
|
85
|
+
"""
|
86
|
+
Get the frame that has the provided 'target_pts'.
|
87
|
+
|
88
|
+
This method will start decoding frames from the
|
89
|
+
most near key frame (the one with the nearer
|
90
|
+
pts) until the one requested is found. All those
|
91
|
+
frames will be stored in cache.
|
92
|
+
|
93
|
+
This method must be called when the frame
|
94
|
+
requested is not stored in the caché.
|
95
|
+
"""
|
96
|
+
# Look for the most near key frame
|
97
|
+
key_frame_pts = max([
|
98
|
+
key_frame_pts
|
99
|
+
for key_frame_pts in self.key_frames_pts
|
100
|
+
if key_frame_pts <= target_pts
|
101
|
+
])
|
102
|
+
|
103
|
+
# Go to the key frame that includes it
|
104
|
+
self.container.seek(key_frame_pts, stream = self.stream)
|
105
|
+
|
106
|
+
decoded = None
|
107
|
+
for frame in self.container.decode(self.stream):
|
108
|
+
# TODO: Could 'frame' be None (?)
|
109
|
+
pts = frame.pts
|
110
|
+
if pts is None:
|
111
|
+
continue
|
112
|
+
|
113
|
+
# Store in cache if needed
|
114
|
+
if pts not in self.cache:
|
115
|
+
# TODO: The 'format' must be dynamic
|
116
|
+
self.cache[pts] = frame.to_ndarray(format = "rgb24")
|
117
|
+
|
118
|
+
# Clean cache if full
|
119
|
+
if len(self.cache) > self.size:
|
120
|
+
self.cache.popitem(last = False)
|
121
|
+
|
122
|
+
if pts >= target_pts:
|
123
|
+
decoded = self.cache[pts]
|
124
|
+
break
|
125
|
+
|
126
|
+
return decoded
|
127
|
+
|
128
|
+
def get_frame(
|
129
|
+
self,
|
130
|
+
index: int
|
131
|
+
) -> 'VideoFrame':
|
132
|
+
"""
|
133
|
+
Get the frame with the given 'index' from
|
134
|
+
the cache.
|
135
|
+
"""
|
136
|
+
# convertir frame_number a PTS (timestamps internos)
|
137
|
+
time_base = self.stream.time_base
|
138
|
+
fps = float(self.stream.average_rate)
|
139
|
+
target_pts = int(index / fps / time_base)
|
140
|
+
|
141
|
+
return (
|
142
|
+
self.cache[target_pts]
|
143
|
+
if target_pts in self.cache else
|
144
|
+
self._get_frame_by_pts(target_pts)
|
145
|
+
)
|
146
|
+
|
147
|
+
def clear(
|
148
|
+
self
|
149
|
+
) -> 'VideoFrameCache':
|
150
|
+
"""
|
151
|
+
Clear the cache by removing all the items.
|
152
|
+
"""
|
153
|
+
self.cache.clear()
|
154
|
+
|
155
|
+
return self
|