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.
@@ -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 = av_open(filename)
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 = self.container.streams.video[0]
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 = self.container.streams.audio[0]
323
+ self.audio_stream: AudioStream = None
324
324
  """
325
325
  The stream that includes the audio.
326
326
  """
327
- self.audio_stream.thread_type = 'AUTO'
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