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.
- yta_video_opengl/classes.py +1091 -0
- yta_video_opengl/{reader.py → reader/__init__.py} +61 -43
- yta_video_opengl/reader/cache.py +155 -0
- yta_video_opengl/tests.py +682 -58
- yta_video_opengl/utils.py +24 -0
- yta_video_opengl/writer.py +40 -1
- {yta_video_opengl-0.0.4.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.4.dist-info/RECORD +0 -8
- {yta_video_opengl-0.0.4.dist-info → yta_video_opengl-0.0.6.dist-info}/LICENSE +0 -0
- {yta_video_opengl-0.0.4.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
|
@@ -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
|
-
|
416
|
-
|
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
|
-
|
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
|