yta-video-opengl 0.0.8__tar.gz → 0.0.9__tar.gz
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-0.0.8 → yta_video_opengl-0.0.9}/PKG-INFO +1 -1
- {yta_video_opengl-0.0.8 → yta_video_opengl-0.0.9}/pyproject.toml +1 -1
- {yta_video_opengl-0.0.8 → yta_video_opengl-0.0.9}/src/yta_video_opengl/reader/__init__.py +47 -6
- yta_video_opengl-0.0.9/src/yta_video_opengl/reader/cache.py +233 -0
- {yta_video_opengl-0.0.8 → yta_video_opengl-0.0.9}/src/yta_video_opengl/tests.py +1 -1
- {yta_video_opengl-0.0.8 → yta_video_opengl-0.0.9}/src/yta_video_opengl/video.py +15 -9
- yta_video_opengl-0.0.8/src/yta_video_opengl/reader/cache.py +0 -155
- {yta_video_opengl-0.0.8 → yta_video_opengl-0.0.9}/LICENSE +0 -0
- {yta_video_opengl-0.0.8 → yta_video_opengl-0.0.9}/README.md +0 -0
- {yta_video_opengl-0.0.8 → yta_video_opengl-0.0.9}/src/yta_video_opengl/__init__.py +0 -0
- {yta_video_opengl-0.0.8 → yta_video_opengl-0.0.9}/src/yta_video_opengl/classes.py +0 -0
- {yta_video_opengl-0.0.8 → yta_video_opengl-0.0.9}/src/yta_video_opengl/utils.py +0 -0
- {yta_video_opengl-0.0.8 → yta_video_opengl-0.0.9}/src/yta_video_opengl/writer.py +0 -0
@@ -398,9 +398,14 @@ class VideoReader:
|
|
398
398
|
"""
|
399
399
|
The stream that includes the audio.
|
400
400
|
"""
|
401
|
-
self.
|
401
|
+
self.video_cache: VideoFrameCache = None
|
402
402
|
"""
|
403
|
-
The frame cache system to optimize
|
403
|
+
The video frame cache system to optimize
|
404
|
+
the way we access to the frames.
|
405
|
+
"""
|
406
|
+
self.audio_cache: VideoFrameCache = None
|
407
|
+
"""
|
408
|
+
The audio frame cache system to optimize
|
404
409
|
the way we access to the frames.
|
405
410
|
"""
|
406
411
|
|
@@ -434,7 +439,8 @@ class VideoReader:
|
|
434
439
|
self.video_stream.thread_type = 'AUTO'
|
435
440
|
self.audio_stream = self.container.streams.audio[0]
|
436
441
|
self.audio_stream.thread_type = 'AUTO'
|
437
|
-
self.
|
442
|
+
self.video_cache = VideoFrameCache(self.container, self.video_stream)
|
443
|
+
self.audio_cache = VideoFrameCache(self.container, self.audio_stream)
|
438
444
|
|
439
445
|
def seek(
|
440
446
|
self,
|
@@ -564,10 +570,45 @@ class VideoReader:
|
|
564
570
|
index: int
|
565
571
|
) -> 'VideoFrame':
|
566
572
|
"""
|
567
|
-
Get the frame with the given 'index',
|
568
|
-
the cache system.
|
573
|
+
Get the video frame with the given 'index',
|
574
|
+
using the video cache system.
|
575
|
+
"""
|
576
|
+
return self.video_cache.get_frame(index)
|
577
|
+
|
578
|
+
# TODO: Will we use this (?)
|
579
|
+
def get_audio_frame(
|
580
|
+
self,
|
581
|
+
index: int
|
582
|
+
) -> 'VideoFrame':
|
583
|
+
"""
|
584
|
+
Get the audio frame with the given 'index',
|
585
|
+
using the audio cache system.
|
586
|
+
"""
|
587
|
+
return self.video_cache.get_frame(index)
|
588
|
+
|
589
|
+
def get_frames(
|
590
|
+
self,
|
591
|
+
start: float = 0.0,
|
592
|
+
end: Union[float, None] = None
|
593
|
+
):
|
569
594
|
"""
|
570
|
-
|
595
|
+
Iterator to get the video frames in between
|
596
|
+
the provided 'start' and 'end' time moments.
|
597
|
+
"""
|
598
|
+
for frame in self.video_cache.get_frames(start, end):
|
599
|
+
yield frame
|
600
|
+
|
601
|
+
def get_audio_frames(
|
602
|
+
self,
|
603
|
+
start: float = 0.0,
|
604
|
+
end: Union[float, None] = None
|
605
|
+
):
|
606
|
+
"""
|
607
|
+
Iterator to get the audio frames in between
|
608
|
+
the provided 'start' and 'end' time moments.
|
609
|
+
"""
|
610
|
+
for frame in self.audio_cache.get_frames(start, end):
|
611
|
+
yield frame
|
571
612
|
|
572
613
|
def close(
|
573
614
|
self
|
@@ -0,0 +1,233 @@
|
|
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 yta_video_opengl.utils import t_to_pts, pts_to_t, pts_to_index
|
20
|
+
from av.container import InputContainer
|
21
|
+
from av.video.stream import VideoStream
|
22
|
+
from av.audio.stream import AudioStream
|
23
|
+
from av.video.frame import VideoFrame
|
24
|
+
from av.audio.frame import AudioFrame
|
25
|
+
from yta_validation.parameter import ParameterValidator
|
26
|
+
from fractions import Fraction
|
27
|
+
from collections import OrderedDict
|
28
|
+
from typing import Union
|
29
|
+
|
30
|
+
|
31
|
+
class VideoFrameCache:
|
32
|
+
"""
|
33
|
+
Class to manage the frames cache of a video
|
34
|
+
within a video reader instance.
|
35
|
+
"""
|
36
|
+
|
37
|
+
@property
|
38
|
+
def fps(
|
39
|
+
self
|
40
|
+
) -> float:
|
41
|
+
"""
|
42
|
+
The frames per second as a float.
|
43
|
+
"""
|
44
|
+
return (
|
45
|
+
float(self.stream.average_rate)
|
46
|
+
if self.stream.type == 'video' else
|
47
|
+
float(self.stream.rate)
|
48
|
+
)
|
49
|
+
|
50
|
+
@property
|
51
|
+
def time_base(
|
52
|
+
self
|
53
|
+
) -> Union[Fraction, None]:
|
54
|
+
"""
|
55
|
+
The time base of the stream.
|
56
|
+
"""
|
57
|
+
return self.stream.time_base
|
58
|
+
|
59
|
+
def __init__(
|
60
|
+
self,
|
61
|
+
container: InputContainer,
|
62
|
+
stream: Union[VideoStream, AudioStream],
|
63
|
+
size: int = 50
|
64
|
+
):
|
65
|
+
ParameterValidator.validate_mandatory_instance_of('container', container, InputContainer)
|
66
|
+
ParameterValidator.validate_mandatory_instance_of('stream', stream, [VideoStream, AudioStream])
|
67
|
+
ParameterValidator.validate_mandatory_positive_int('size', size)
|
68
|
+
|
69
|
+
self.container: InputContainer = container
|
70
|
+
"""
|
71
|
+
The pyav container.
|
72
|
+
"""
|
73
|
+
self.stream: Union[VideoStream, AudioStream] = stream
|
74
|
+
"""
|
75
|
+
The pyav stream.
|
76
|
+
"""
|
77
|
+
self.cache: OrderedDict = OrderedDict()
|
78
|
+
"""
|
79
|
+
The cache ordered dictionary.
|
80
|
+
"""
|
81
|
+
self.size = size
|
82
|
+
"""
|
83
|
+
The size (in number of frames) of the cache.
|
84
|
+
"""
|
85
|
+
self.key_frames_pts: list[int] = []
|
86
|
+
"""
|
87
|
+
The list that contains the timestamps of the
|
88
|
+
key frame packets, ordered from begining to
|
89
|
+
end.
|
90
|
+
"""
|
91
|
+
|
92
|
+
self._prepare()
|
93
|
+
|
94
|
+
def _prepare(
|
95
|
+
self
|
96
|
+
):
|
97
|
+
# Index key frames
|
98
|
+
for packet in self.container.demux(self.stream):
|
99
|
+
if packet.is_keyframe:
|
100
|
+
self.key_frames_pts.append(packet.pts)
|
101
|
+
|
102
|
+
self.container.seek(0)
|
103
|
+
|
104
|
+
def _get_nearest_keyframe_fps(
|
105
|
+
self,
|
106
|
+
pts: int
|
107
|
+
):
|
108
|
+
"""
|
109
|
+
Get the fps of the keyframe that is the
|
110
|
+
nearest to the provided 'pts'. Useful to
|
111
|
+
seek and start decoding frames from that
|
112
|
+
keyframe.
|
113
|
+
"""
|
114
|
+
return max([
|
115
|
+
key_frame_pts
|
116
|
+
for key_frame_pts in self.key_frames_pts
|
117
|
+
if key_frame_pts <= pts
|
118
|
+
])
|
119
|
+
|
120
|
+
def _get_frame_by_pts(
|
121
|
+
self,
|
122
|
+
pts: int
|
123
|
+
):
|
124
|
+
"""
|
125
|
+
Get the frame that has the provided 'pts'.
|
126
|
+
|
127
|
+
This method will start decoding frames from the
|
128
|
+
most near key frame (the one with the nearer
|
129
|
+
pts) until the one requested is found. All those
|
130
|
+
frames will be stored in cache.
|
131
|
+
|
132
|
+
This method must be called when the frame
|
133
|
+
requested is not stored in the caché.
|
134
|
+
"""
|
135
|
+
# Look for the most near key frame
|
136
|
+
key_frame_pts = self._get_nearest_keyframe_fps(pts)
|
137
|
+
|
138
|
+
# Go to the key frame that includes it
|
139
|
+
self.container.seek(key_frame_pts, stream = self.stream)
|
140
|
+
|
141
|
+
decoded = None
|
142
|
+
for frame in self.container.decode(self.stream):
|
143
|
+
# TODO: Could 'frame' be None (?)
|
144
|
+
if frame.pts is None:
|
145
|
+
continue
|
146
|
+
|
147
|
+
# Store in cache if needed
|
148
|
+
if frame.pts not in self.cache:
|
149
|
+
# TODO: The 'format' must be dynamic
|
150
|
+
self.cache[frame.pts] = frame.to_ndarray(format = "rgb24")
|
151
|
+
|
152
|
+
# Clean cache if full
|
153
|
+
if len(self.cache) > self.size:
|
154
|
+
self.cache.popitem(last = False)
|
155
|
+
|
156
|
+
if frame.pts >= pts:
|
157
|
+
decoded = self.cache[frame.pts]
|
158
|
+
break
|
159
|
+
|
160
|
+
return decoded
|
161
|
+
|
162
|
+
def get_frame(
|
163
|
+
self,
|
164
|
+
index: int
|
165
|
+
) -> Union[VideoFrame, AudioFrame]:
|
166
|
+
"""
|
167
|
+
Get the frame with the given 'index' from
|
168
|
+
the cache.
|
169
|
+
"""
|
170
|
+
# TODO: Maybe we can accept 't' and 'pts' also
|
171
|
+
target_pts = int(index / self.fps / self.time_base)
|
172
|
+
|
173
|
+
return (
|
174
|
+
self.cache[target_pts]
|
175
|
+
if target_pts in self.cache else
|
176
|
+
self._get_frame_by_pts(target_pts)
|
177
|
+
)
|
178
|
+
|
179
|
+
def get_frames(
|
180
|
+
self,
|
181
|
+
start: float = 0,
|
182
|
+
end: Union[float, None] = None
|
183
|
+
):
|
184
|
+
"""
|
185
|
+
Get all the frames in the range between
|
186
|
+
the provided 'start' and 'end' time in
|
187
|
+
seconds.
|
188
|
+
"""
|
189
|
+
# TODO: I create this method by default using
|
190
|
+
# the cache. Think about how to implement it
|
191
|
+
# and apply it here, please.
|
192
|
+
# Go to the nearest key frame
|
193
|
+
start = t_to_pts(start, self.time_base)
|
194
|
+
end = (
|
195
|
+
t_to_pts(end, self.time_base)
|
196
|
+
if end is not None else
|
197
|
+
None
|
198
|
+
)
|
199
|
+
key_frame_pts = self._get_nearest_keyframe_fps(start)
|
200
|
+
|
201
|
+
# Go to the nearest key frame to start decoding
|
202
|
+
self.container.seek(key_frame_pts, stream = self.stream)
|
203
|
+
|
204
|
+
for packet in self.container.demux(self.stream):
|
205
|
+
for frame in packet.decode():
|
206
|
+
if frame.pts is None:
|
207
|
+
continue
|
208
|
+
|
209
|
+
if frame.pts < start:
|
210
|
+
continue
|
211
|
+
|
212
|
+
if (
|
213
|
+
end is not None and
|
214
|
+
frame.pts > end
|
215
|
+
):
|
216
|
+
return
|
217
|
+
|
218
|
+
# TODO: Maybe send a @dataclass instead (?)
|
219
|
+
yield (
|
220
|
+
frame,
|
221
|
+
pts_to_t(frame.pts, self.time_base),
|
222
|
+
pts_to_index(frame.pts, self.time_base, self.fps)
|
223
|
+
)
|
224
|
+
|
225
|
+
def clear(
|
226
|
+
self
|
227
|
+
) -> 'VideoFrameCache':
|
228
|
+
"""
|
229
|
+
Clear the cache by removing all the items.
|
230
|
+
"""
|
231
|
+
self.cache.clear()
|
232
|
+
|
233
|
+
return self
|
@@ -583,7 +583,7 @@ def video_modified_stored():
|
|
583
583
|
from yta_video_opengl.utils import texture_to_frame, frame_to_texture
|
584
584
|
from yta_video_opengl.video import Video
|
585
585
|
|
586
|
-
Video(VIDEO_PATH, 0, 0.
|
586
|
+
Video(VIDEO_PATH, 0.25, 0.75).save_as(OUTPUT_PATH)
|
587
587
|
|
588
588
|
return
|
589
589
|
|
@@ -89,17 +89,23 @@ class Video:
|
|
89
89
|
- `t` as the frame time moment
|
90
90
|
- `index` as the frame index
|
91
91
|
"""
|
92
|
-
for frame in
|
93
|
-
container = self.reader.container,
|
94
|
-
video_stream = self.reader.video_stream,
|
95
|
-
audio_stream = self.reader.audio_stream,
|
96
|
-
video_start_pts = self.start_pts,
|
97
|
-
video_end_pts = self.end_pts,
|
98
|
-
audio_start_pts = self.audio_start_pts,
|
99
|
-
audio_end_pts = self.audio_end_pts
|
100
|
-
):
|
92
|
+
for frame in self.reader.get_frames(self.start, self.end):
|
101
93
|
yield frame
|
102
94
|
|
95
|
+
for frame in self.reader.get_audio_frames(self.start, self.end):
|
96
|
+
yield frame
|
97
|
+
|
98
|
+
# for frame in iterate_stream_frames_demuxing(
|
99
|
+
# container = self.reader.container,
|
100
|
+
# video_stream = self.reader.video_stream,
|
101
|
+
# audio_stream = self.reader.audio_stream,
|
102
|
+
# video_start_pts = self.start_pts,
|
103
|
+
# video_end_pts = self.end_pts,
|
104
|
+
# audio_start_pts = self.audio_start_pts,
|
105
|
+
# audio_end_pts = self.audio_end_pts
|
106
|
+
# ):
|
107
|
+
# yield frame
|
108
|
+
|
103
109
|
def __init__(
|
104
110
|
self,
|
105
111
|
filename: str,
|
@@ -1,155 +0,0 @@
|
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|