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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: yta-video-opengl
3
- Version: 0.0.8
3
+ Version: 0.0.9
4
4
  Summary: Youtube Autonomous Video OpenGL Module
5
5
  Author: danialcala94
6
6
  Author-email: danielalcalavalera@gmail.com
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "yta-video-opengl"
3
- version = "0.0.8"
3
+ version = "0.0.9"
4
4
  description = "Youtube Autonomous Video OpenGL Module"
5
5
  authors = [
6
6
  {name = "danialcala94",email = "danielalcalavalera@gmail.com"}
@@ -398,9 +398,14 @@ class VideoReader:
398
398
  """
399
399
  The stream that includes the audio.
400
400
  """
401
- self.cache: VideoFrameCache = None
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.cache = VideoFrameCache(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', using
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
- return self.cache.get_frame(index)
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.5).save_as(OUTPUT_PATH)
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 iterate_stream_frames_demuxing(
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