yta-video-opengl 0.0.4__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
@@ -350,17 +386,8 @@ class VideoReader:
350
386
  frame individually as a VideoReaderFrame
351
387
  instance. If not, the whole packet as a
352
388
  VideoReaderPacket instance.
353
-
354
- If the frame is the last one, with size == 0,
355
- it will return None as it must not be passed
356
- to the muxer '.mux()' method.
357
389
  """
358
390
  for packet in self.packet_with_audio_iterator:
359
- if packet.size == 0:
360
- # End packet, not for muxer
361
- yield None
362
- continue
363
-
364
391
  is_video = packet.stream.type == 'video'
365
392
 
366
393
  do_decode = (
@@ -382,38 +409,29 @@ class VideoReader:
382
409
  # Return the packet as it is
383
410
  yield VideoReaderPacket(packet)
384
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)
385
422
 
386
423
 
387
424
 
388
- """
389
- Read this below if you can to combine videos
390
- that have not been written yet to the disk
391
- (maybe a composition in moviepy or I don't
392
- know).
393
-
394
- Usar un pipe (sin escribir archivo completo)
395
- Puedes lanzar un proceso FFmpeg que envíe el vídeo a PyAV por stdin como flujo sin codificar (por ejemplo en rawvideo), así no tienes que escribir el archivo final.
396
- Ejemplo:
397
-
398
- PYTHON_CODE:
399
- import subprocess
400
- import av
401
-
402
- # FFmpeg produce frames en crudo por stdout
403
- ffmpeg_proc = subprocess.Popen(
404
- [
405
- "ffmpeg",
406
- "-i", "-", # Lee de stdin
407
- "-f", "rawvideo",
408
- "-pix_fmt", "rgba",
409
- "-"
410
- ],
411
- stdin=subprocess.PIPE,
412
- stdout=subprocess.PIPE
413
- )
414
425
 
415
- # Aquí enviarías los datos combinados desde tu programa al ffmpeg_proc.stdin
416
- # y podrías leer con PyAV o directamente procesar arrays de píxeles
426
+ """
427
+ When reading packets directly from the stream
428
+ we can receive packets with size=0, but we need
429
+ to process them and decode (or yield them). It
430
+ is only when we are passing packets to the mux
431
+ when we need to ignore teh ones thar are empty
432
+ (size=0).
417
433
 
418
- Esto es lo más usado para pipeline de vídeo en tiempo real.
434
+ TODO: Do we need to ignore all? By now, ignoring
435
+ not is causing exceptions, and ignoring them is
436
+ making it work perfectly.
419
437
  """
@@ -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