yta-video-opengl 0.0.14__tar.gz → 0.0.15__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.14 → yta_video_opengl-0.0.15}/PKG-INFO +1 -1
- {yta_video_opengl-0.0.14 → yta_video_opengl-0.0.15}/pyproject.toml +1 -1
- {yta_video_opengl-0.0.14 → yta_video_opengl-0.0.15}/src/yta_video_opengl/complete/timeline.py +2 -4
- {yta_video_opengl-0.0.14 → yta_video_opengl-0.0.15}/src/yta_video_opengl/complete/track.py +2 -4
- {yta_video_opengl-0.0.14 → yta_video_opengl-0.0.15}/src/yta_video_opengl/complete/video_on_track.py +1 -4
- {yta_video_opengl-0.0.14 → yta_video_opengl-0.0.15}/src/yta_video_opengl/reader/__init__.py +25 -24
- yta_video_opengl-0.0.15/src/yta_video_opengl/reader/cache/__init__.py +249 -0
- yta_video_opengl-0.0.15/src/yta_video_opengl/reader/cache/audio.py +195 -0
- yta_video_opengl-0.0.15/src/yta_video_opengl/reader/cache/utils.py +48 -0
- yta_video_opengl-0.0.15/src/yta_video_opengl/reader/cache/video.py +110 -0
- {yta_video_opengl-0.0.14 → yta_video_opengl-0.0.15}/src/yta_video_opengl/video.py +2 -2
- yta_video_opengl-0.0.14/src/yta_video_opengl/reader/cache.py +0 -512
- {yta_video_opengl-0.0.14 → yta_video_opengl-0.0.15}/LICENSE +0 -0
- {yta_video_opengl-0.0.14 → yta_video_opengl-0.0.15}/README.md +0 -0
- {yta_video_opengl-0.0.14 → yta_video_opengl-0.0.15}/src/yta_video_opengl/__init__.py +0 -0
- {yta_video_opengl-0.0.14 → yta_video_opengl-0.0.15}/src/yta_video_opengl/classes.py +0 -0
- {yta_video_opengl-0.0.14 → yta_video_opengl-0.0.15}/src/yta_video_opengl/complete/__init__.py +0 -0
- {yta_video_opengl-0.0.14 → yta_video_opengl-0.0.15}/src/yta_video_opengl/nodes/__init__.py +0 -0
- {yta_video_opengl-0.0.14 → yta_video_opengl-0.0.15}/src/yta_video_opengl/nodes/audio/__init__.py +0 -0
- {yta_video_opengl-0.0.14 → yta_video_opengl-0.0.15}/src/yta_video_opengl/nodes/video/__init__.py +0 -0
- {yta_video_opengl-0.0.14 → yta_video_opengl-0.0.15}/src/yta_video_opengl/nodes/video/opengl.py +0 -0
- {yta_video_opengl-0.0.14 → yta_video_opengl-0.0.15}/src/yta_video_opengl/t.py +0 -0
- {yta_video_opengl-0.0.14 → yta_video_opengl-0.0.15}/src/yta_video_opengl/tests.py +0 -0
- {yta_video_opengl-0.0.14 → yta_video_opengl-0.0.15}/src/yta_video_opengl/utils.py +0 -0
- {yta_video_opengl-0.0.14 → yta_video_opengl-0.0.15}/src/yta_video_opengl/writer.py +0 -0
{yta_video_opengl-0.0.14 → yta_video_opengl-0.0.15}/src/yta_video_opengl/complete/timeline.py
RENAMED
@@ -168,10 +168,8 @@ class Timeline:
|
|
168
168
|
project will be rendered.
|
169
169
|
"""
|
170
170
|
ParameterValidator.validate_mandatory_string('filename', filename, do_accept_empty = False)
|
171
|
-
|
172
|
-
|
173
|
-
# TODO: We need to accept Fraction as number
|
174
|
-
#ParameterValidator.validate_positive_number('end', end, do_include_zero = False)
|
171
|
+
ParameterValidator.validate_mandatory_positive_number('start', start, do_include_zero = True)
|
172
|
+
ParameterValidator.validate_positive_number('end', end, do_include_zero = False)
|
175
173
|
|
176
174
|
# TODO: Limitate 'end' a bit...
|
177
175
|
end = (
|
@@ -39,10 +39,8 @@ class _Part:
|
|
39
39
|
end: Union[int, float, Fraction],
|
40
40
|
video: Union[VideoOnTrack, None] = None
|
41
41
|
):
|
42
|
-
|
43
|
-
|
44
|
-
# TODO: We need to accept Fraction as number
|
45
|
-
# ParameterValidator.validate_mandatory_positive_number('end', end, do_include_zero = False)
|
42
|
+
ParameterValidator.validate_mandatory_positive_number('start', start, do_include_zero = True)
|
43
|
+
ParameterValidator.validate_mandatory_positive_number('end', end, do_include_zero = False)
|
46
44
|
ParameterValidator.validate_instance_of('video', video, VideoOnTrack)
|
47
45
|
|
48
46
|
self._track: Track = track
|
{yta_video_opengl-0.0.14 → yta_video_opengl-0.0.15}/src/yta_video_opengl/complete/video_on_track.py
RENAMED
@@ -46,10 +46,7 @@ class VideoOnTrack:
|
|
46
46
|
start: Union[int, float, Fraction] = 0.0
|
47
47
|
):
|
48
48
|
ParameterValidator.validate_mandatory_instance_of('video', video, Video)
|
49
|
-
|
50
|
-
# from 'fractions' or 'quicktions', as a
|
51
|
-
# number
|
52
|
-
#ParameterValidator.validate_mandatory_positive_number('start', start, do_include_zero = True)
|
49
|
+
ParameterValidator.validate_mandatory_positive_number('start', start, do_include_zero = True)
|
53
50
|
|
54
51
|
self.video: Video = video
|
55
52
|
"""
|
@@ -2,7 +2,8 @@
|
|
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
|
+
from yta_video_opengl.reader.cache.video import VideoFrameCache
|
6
|
+
from yta_video_opengl.reader.cache.audio import AudioFrameCache
|
6
7
|
from yta_video_opengl.utils import iterate_stream_frames_demuxing
|
7
8
|
from yta_video_opengl.t import T
|
8
9
|
from yta_validation import PythonValidator
|
@@ -435,7 +436,7 @@ class VideoReader:
|
|
435
436
|
The video frame cache system to optimize
|
436
437
|
the way we access to the frames.
|
437
438
|
"""
|
438
|
-
self.audio_cache:
|
439
|
+
self.audio_cache: AudioFrameCache = None
|
439
440
|
"""
|
440
441
|
The audio frame cache system to optimize
|
441
442
|
the way we access to the frames.
|
@@ -490,7 +491,7 @@ class VideoReader:
|
|
490
491
|
raise Exception(f'No video nor audio stream found in the "{self.filename}" file.')
|
491
492
|
|
492
493
|
self.video_cache = VideoFrameCache(self.container, self.video_stream)
|
493
|
-
self.audio_cache =
|
494
|
+
self.audio_cache = AudioFrameCache(self.container, self.audio_stream)
|
494
495
|
|
495
496
|
def seek(
|
496
497
|
self,
|
@@ -620,16 +621,28 @@ class VideoReader:
|
|
620
621
|
):
|
621
622
|
yield frame
|
622
623
|
|
623
|
-
def
|
624
|
+
def get_frame(
|
624
625
|
self,
|
625
626
|
t: Union[int, float, Fraction]
|
626
|
-
) ->
|
627
|
+
) -> VideoFrame:
|
627
628
|
"""
|
628
|
-
Get the video frame
|
629
|
-
moment
|
629
|
+
Get the video frame that is in the 't' time
|
630
|
+
moment provided.
|
630
631
|
"""
|
631
|
-
return self.video_cache.
|
632
|
-
|
632
|
+
return self.video_cache.get_frame(t)
|
633
|
+
|
634
|
+
def get_frames(
|
635
|
+
self,
|
636
|
+
start: Union[int, float, Fraction] = 0.0,
|
637
|
+
end: Union[int, float, Fraction, None] = None
|
638
|
+
):
|
639
|
+
"""
|
640
|
+
Iterator to get the video frames in between
|
641
|
+
the provided 'start' and 'end' time moments.
|
642
|
+
"""
|
643
|
+
for frame in self.video_cache.get_frames(start, end):
|
644
|
+
yield frame
|
645
|
+
|
633
646
|
def get_audio_frame_from_t(
|
634
647
|
self,
|
635
648
|
t: Union[int, float, Fraction]
|
@@ -638,7 +651,7 @@ class VideoReader:
|
|
638
651
|
Get the audio frame with the given 't' time
|
639
652
|
moment, using the audio cache system.
|
640
653
|
"""
|
641
|
-
return self.audio_cache.
|
654
|
+
return self.audio_cache.get_frame(t)
|
642
655
|
|
643
656
|
def get_audio_frames_from_t(
|
644
657
|
self,
|
@@ -659,19 +672,7 @@ class VideoReader:
|
|
659
672
|
# We want all the audios that must be played
|
660
673
|
# during the video frame that starts in the
|
661
674
|
# 't' time moment
|
662
|
-
for frame in self.
|
663
|
-
yield frame
|
664
|
-
|
665
|
-
def get_frames(
|
666
|
-
self,
|
667
|
-
start: Union[int, float, Fraction] = 0.0,
|
668
|
-
end: Union[int, float, Fraction, None] = None
|
669
|
-
):
|
670
|
-
"""
|
671
|
-
Iterator to get the video frames in between
|
672
|
-
the provided 'start' and 'end' time moments.
|
673
|
-
"""
|
674
|
-
for frame in self.video_cache.get_frames(start, end):
|
675
|
+
for frame in self.get_audio_frames(t.truncated, t.next(1).truncated):
|
675
676
|
yield frame
|
676
677
|
|
677
678
|
def get_audio_frames(
|
@@ -683,7 +684,7 @@ class VideoReader:
|
|
683
684
|
Iterator to get the audio frames in between
|
684
685
|
the provided 'start' and 'end' time moments.
|
685
686
|
"""
|
686
|
-
for frame in self.audio_cache.
|
687
|
+
for frame in self.audio_cache.get_frames(start, end):
|
687
688
|
yield frame
|
688
689
|
|
689
690
|
def close(
|
@@ -0,0 +1,249 @@
|
|
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
|
+
A stream can have 'fps = 60' but use another
|
20
|
+
different time base that make the pts values go 0,
|
21
|
+
256, 512... for example. The 'time_base' is the
|
22
|
+
only accurate way to obtain the pts.
|
23
|
+
|
24
|
+
Feel free to move this explanation to other
|
25
|
+
place, its about the duration.
|
26
|
+
|
27
|
+
The stream 'duration' parameter is measured
|
28
|
+
on ticks, the amount of ticks that the
|
29
|
+
stream lasts. Here below is an example:
|
30
|
+
|
31
|
+
- Duration raw: 529200
|
32
|
+
- Time base: 1/44100
|
33
|
+
- Duration (seconds): 12.0
|
34
|
+
"""
|
35
|
+
from av.container import InputContainer
|
36
|
+
from av.video.stream import VideoStream
|
37
|
+
from av.audio.stream import AudioStream
|
38
|
+
from av.video.frame import VideoFrame
|
39
|
+
from av.audio.frame import AudioFrame
|
40
|
+
from av.packet import Packet
|
41
|
+
from yta_validation.parameter import ParameterValidator
|
42
|
+
from quicktions import Fraction
|
43
|
+
from collections import OrderedDict
|
44
|
+
from typing import Union
|
45
|
+
from abc import abstractmethod, ABC
|
46
|
+
|
47
|
+
import numpy as np
|
48
|
+
import math
|
49
|
+
|
50
|
+
|
51
|
+
class FrameCache(ABC):
|
52
|
+
"""
|
53
|
+
Class to manage the frames cache of a video
|
54
|
+
or audio.
|
55
|
+
"""
|
56
|
+
|
57
|
+
@property
|
58
|
+
@abstractmethod
|
59
|
+
def fps(
|
60
|
+
self
|
61
|
+
) -> Union[int, Fraction, None]:
|
62
|
+
"""
|
63
|
+
The frames per second.
|
64
|
+
"""
|
65
|
+
pass
|
66
|
+
|
67
|
+
@property
|
68
|
+
def time_base(
|
69
|
+
self
|
70
|
+
) -> Union[Fraction, None]:
|
71
|
+
"""
|
72
|
+
The time base of the stream.
|
73
|
+
"""
|
74
|
+
return self.stream.time_base
|
75
|
+
|
76
|
+
def __init__(
|
77
|
+
self,
|
78
|
+
container: InputContainer,
|
79
|
+
stream: Union[VideoStream, AudioStream],
|
80
|
+
size: Union[int, None] = None
|
81
|
+
):
|
82
|
+
ParameterValidator.validate_mandatory_instance_of('container', container, InputContainer)
|
83
|
+
ParameterValidator.validate_mandatory_instance_of('stream', stream, [VideoStream, AudioStream])
|
84
|
+
ParameterValidator.validate_number_between('size', size, 1, 120)
|
85
|
+
|
86
|
+
self.container: InputContainer = container
|
87
|
+
"""
|
88
|
+
The pyav container.
|
89
|
+
"""
|
90
|
+
self.stream: Union[VideoStream, AudioStream] = stream
|
91
|
+
"""
|
92
|
+
The pyav stream.
|
93
|
+
"""
|
94
|
+
self.cache: OrderedDict = OrderedDict()
|
95
|
+
"""
|
96
|
+
The cache ordered dictionary.
|
97
|
+
"""
|
98
|
+
self.key_frames_pts: list[int] = []
|
99
|
+
"""
|
100
|
+
The list that contains the timestamps of the
|
101
|
+
key frame packets, ordered from begining to
|
102
|
+
end.
|
103
|
+
"""
|
104
|
+
self.size: int = size
|
105
|
+
"""
|
106
|
+
The size of the cache.
|
107
|
+
"""
|
108
|
+
|
109
|
+
self._last_packet_accessed: Union[Packet, None] = None
|
110
|
+
"""
|
111
|
+
The last packet that has been accessed
|
112
|
+
"""
|
113
|
+
|
114
|
+
self._prepare()
|
115
|
+
|
116
|
+
def _prepare(
|
117
|
+
self
|
118
|
+
):
|
119
|
+
# Index key frames
|
120
|
+
for packet in self.container.demux(self.stream):
|
121
|
+
if packet.is_keyframe:
|
122
|
+
self.key_frames_pts.append(packet.pts)
|
123
|
+
|
124
|
+
# The cache size will be auto-calculated to
|
125
|
+
# use the amount of frames of the biggest
|
126
|
+
# interval of frames that belongs to a key
|
127
|
+
# frame, or a value by default
|
128
|
+
# TODO: Careful if this is too big
|
129
|
+
# Intervals, but in number of frames
|
130
|
+
intervals = np.diff(
|
131
|
+
# Intervals of time between keyframes
|
132
|
+
np.array(self.key_frames_pts) * self.time_base
|
133
|
+
) * self.fps
|
134
|
+
|
135
|
+
self.size = (
|
136
|
+
math.ceil(np.max(intervals))
|
137
|
+
if intervals.size > 0 else
|
138
|
+
(
|
139
|
+
self.size
|
140
|
+
if self.size is not None else
|
141
|
+
# TODO: Make this a setting (?)
|
142
|
+
60
|
143
|
+
)
|
144
|
+
)
|
145
|
+
|
146
|
+
self.container.seek(0)
|
147
|
+
|
148
|
+
def _get_nearest_keyframe_pts(
|
149
|
+
self,
|
150
|
+
pts: int
|
151
|
+
):
|
152
|
+
"""
|
153
|
+
Get the fps of the keyframe that is the
|
154
|
+
nearest to the provided 'pts'. Useful to
|
155
|
+
seek and start decoding frames from that
|
156
|
+
keyframe.
|
157
|
+
"""
|
158
|
+
return max([
|
159
|
+
key_frame_pts
|
160
|
+
for key_frame_pts in self.key_frames_pts
|
161
|
+
if key_frame_pts <= pts
|
162
|
+
])
|
163
|
+
|
164
|
+
def _store_frame_in_cache(
|
165
|
+
self,
|
166
|
+
frame: Union[VideoFrame, AudioFrame]
|
167
|
+
) -> Union[VideoFrame, AudioFrame]:
|
168
|
+
"""
|
169
|
+
Store the provided 'frame' in cache if it
|
170
|
+
is not on it, removing the first item of
|
171
|
+
the cache if full.
|
172
|
+
"""
|
173
|
+
if frame.pts not in self.cache:
|
174
|
+
self.cache[frame.pts] = frame
|
175
|
+
|
176
|
+
# Clean cache if full
|
177
|
+
if len(self.cache) > self.size:
|
178
|
+
self.cache.popitem(last = False)
|
179
|
+
|
180
|
+
return frame
|
181
|
+
|
182
|
+
def _seek(
|
183
|
+
self,
|
184
|
+
pts: int
|
185
|
+
):
|
186
|
+
"""
|
187
|
+
Seek to the given 'pts' This is useful
|
188
|
+
when working with 'container.demux' and
|
189
|
+
iterating over packets, not when using
|
190
|
+
'stream.decode' and getting frames
|
191
|
+
directly.
|
192
|
+
"""
|
193
|
+
self.container.seek(
|
194
|
+
offset = pts,
|
195
|
+
stream = self.stream
|
196
|
+
)
|
197
|
+
|
198
|
+
def clear(
|
199
|
+
self
|
200
|
+
) -> 'VideoFrameCache':
|
201
|
+
"""
|
202
|
+
Clear the cache by removing all the items.
|
203
|
+
"""
|
204
|
+
self.cache.clear()
|
205
|
+
|
206
|
+
return self
|
207
|
+
|
208
|
+
def get_frame(
|
209
|
+
self,
|
210
|
+
t: Union[int, float, Fraction]
|
211
|
+
) -> Union[VideoFrame, AudioFrame]:
|
212
|
+
"""
|
213
|
+
Get the single frame that is in the 't'
|
214
|
+
time moment provided.
|
215
|
+
"""
|
216
|
+
for frame in self.get_frames(t):
|
217
|
+
return frame
|
218
|
+
|
219
|
+
@abstractmethod
|
220
|
+
def get_frames(
|
221
|
+
self,
|
222
|
+
start: Union[int, float, Fraction],
|
223
|
+
end: Union[int, float, Fraction]
|
224
|
+
):
|
225
|
+
pass
|
226
|
+
|
227
|
+
|
228
|
+
"""
|
229
|
+
There is a way of editing videos being
|
230
|
+
able to arbitrary access to frames, that
|
231
|
+
is transforming the source videos to
|
232
|
+
intra-frame videos. This is a ffmpeg
|
233
|
+
command that can do it:
|
234
|
+
|
235
|
+
- `ffmpeg -i input.mp4 -c:v libx264 -x264opts keyint=1 -preset fast -crf 18 -c:a copy output_intra.mp4`
|
236
|
+
|
237
|
+
Once you have the 'output_intra.mp4',
|
238
|
+
each packet can decodify its frame
|
239
|
+
depending not on the previous one, being
|
240
|
+
able to seek and jump easy.
|
241
|
+
|
242
|
+
There are 3 type of video codifications,
|
243
|
+
the I-frame (intra-coded), in which any
|
244
|
+
frame can be decoded by itself, P-frame
|
245
|
+
(predicted), that need one or more
|
246
|
+
previous frames to be decoded, and
|
247
|
+
B-frame (bidirectional predicted), that
|
248
|
+
needs previous and future frames.
|
249
|
+
"""
|
@@ -0,0 +1,195 @@
|
|
1
|
+
|
2
|
+
from yta_video_opengl.reader.cache import FrameCache
|
3
|
+
from yta_video_opengl.reader.cache.utils import trim_audio_frame
|
4
|
+
from yta_video_opengl.t import T
|
5
|
+
from yta_validation.parameter import ParameterValidator
|
6
|
+
from av.container import InputContainer
|
7
|
+
from av.audio.stream import AudioStream
|
8
|
+
from av.audio.frame import AudioFrame
|
9
|
+
from quicktions import Fraction
|
10
|
+
from typing import Union
|
11
|
+
|
12
|
+
|
13
|
+
class AudioFrameCache(FrameCache):
|
14
|
+
"""
|
15
|
+
Cache for the audio frames.
|
16
|
+
"""
|
17
|
+
|
18
|
+
@property
|
19
|
+
def fps(
|
20
|
+
self
|
21
|
+
) -> Union[Fraction, int]:
|
22
|
+
"""
|
23
|
+
The frames per second.
|
24
|
+
"""
|
25
|
+
return self.stream.rate
|
26
|
+
|
27
|
+
@property
|
28
|
+
def frame_duration(
|
29
|
+
self
|
30
|
+
) -> int:
|
31
|
+
"""
|
32
|
+
The frame duration in ticks, which is the
|
33
|
+
minimum amount of time, 1 / time_base.
|
34
|
+
"""
|
35
|
+
return self.stream.frames
|
36
|
+
|
37
|
+
def __init__(
|
38
|
+
self,
|
39
|
+
container: InputContainer,
|
40
|
+
stream: AudioStream,
|
41
|
+
size: Union[int, None] = None
|
42
|
+
):
|
43
|
+
ParameterValidator.validate_mandatory_instance_of('stream', stream, AudioStream)
|
44
|
+
|
45
|
+
super().__init__(container, stream, size)
|
46
|
+
|
47
|
+
def _seek(
|
48
|
+
self,
|
49
|
+
pts: int
|
50
|
+
):
|
51
|
+
"""
|
52
|
+
Seek to the given 'pts' only if it is not
|
53
|
+
the next 'pts' to the last read, and it
|
54
|
+
will also apply a pad to avoid problems
|
55
|
+
when reading audio frames.
|
56
|
+
"""
|
57
|
+
# I found that it is recommended to
|
58
|
+
# read ~100ms before the pts we want to
|
59
|
+
# actually read so we obtain the frames
|
60
|
+
# clean (this is important in audio).
|
61
|
+
# This solves a problem I had related
|
62
|
+
# to some artifacts on the audio when
|
63
|
+
# trimming exactly without this pad.
|
64
|
+
pts_pad = int(0.1 / self.time_base)
|
65
|
+
self.container.seek(
|
66
|
+
offset = max(0, pts - pts_pad),
|
67
|
+
stream = self.stream
|
68
|
+
)
|
69
|
+
|
70
|
+
def get_frame(
|
71
|
+
self,
|
72
|
+
t: Union[int, float, Fraction]
|
73
|
+
) -> AudioFrame:
|
74
|
+
"""
|
75
|
+
Get the video frame that is in the 't'
|
76
|
+
time moment provided.
|
77
|
+
"""
|
78
|
+
t: T = T.from_fps(t, self.fps)
|
79
|
+
for frame in self.get_frames(t.truncated, t.next(1).truncated):
|
80
|
+
return frame
|
81
|
+
|
82
|
+
def get_frames(
|
83
|
+
self,
|
84
|
+
start: Union[int, float, Fraction],
|
85
|
+
end: Union[int, float, Fraction]
|
86
|
+
):
|
87
|
+
"""
|
88
|
+
Get all the audio frames in the range
|
89
|
+
between the provided 'start' and 'end'
|
90
|
+
time (in seconds).
|
91
|
+
|
92
|
+
This method is an iterator that yields
|
93
|
+
the frame, its t and its index.
|
94
|
+
"""
|
95
|
+
# TODO: Validate 'start' and 'end' are mandatory
|
96
|
+
# positive numbers
|
97
|
+
# Make sure the 'start' and 'end' time moments
|
98
|
+
# provided are truncated values based on the
|
99
|
+
# stream time base
|
100
|
+
start = T(start, self.time_base).truncated
|
101
|
+
end = T(end, self.time_base).truncated
|
102
|
+
|
103
|
+
if end <= start:
|
104
|
+
raise Exception(f'The time range start:{str(float(start))} - end:{str(float(end))}) is not valid.')
|
105
|
+
|
106
|
+
key_frame_pts = self._get_nearest_keyframe_pts(start / self.time_base)
|
107
|
+
|
108
|
+
if (
|
109
|
+
self._last_packet_accessed is None or
|
110
|
+
self._last_packet_accessed.pts != key_frame_pts
|
111
|
+
):
|
112
|
+
self._seek(key_frame_pts)
|
113
|
+
|
114
|
+
for packet in self.container.demux(self.stream):
|
115
|
+
if packet.pts is None:
|
116
|
+
continue
|
117
|
+
|
118
|
+
self._last_packet_accessed = packet
|
119
|
+
|
120
|
+
for frame in packet.decode():
|
121
|
+
if frame.pts is None:
|
122
|
+
continue
|
123
|
+
|
124
|
+
# We store all the frames in cache
|
125
|
+
self._store_frame_in_cache(frame)
|
126
|
+
|
127
|
+
current_frame_time = frame.pts * self.time_base
|
128
|
+
# End is not included, its the start of the
|
129
|
+
# next frame actually
|
130
|
+
frame_end = current_frame_time + (frame.samples / self.stream.sample_rate)
|
131
|
+
|
132
|
+
# For the next comments imagine we are looking
|
133
|
+
# for the [1.0, 2.0) audio time range
|
134
|
+
# Previous frame and nothing is inside
|
135
|
+
if frame_end <= start:
|
136
|
+
# From 0.25 to 1.0
|
137
|
+
continue
|
138
|
+
|
139
|
+
# We finished, nothing is inside and its after
|
140
|
+
if current_frame_time >= end:
|
141
|
+
# From 2.0 to 2.75
|
142
|
+
return
|
143
|
+
|
144
|
+
"""
|
145
|
+
If we need audio from 1 to 2, audio is:
|
146
|
+
- from 0 to 0.75 (Not included, omit)
|
147
|
+
- from 0.5 to 1.5 (Included, take 1.0 to 1.5)
|
148
|
+
- from 0.5 to 2.5 (Included, take 1.0 to 2.0)
|
149
|
+
- from 1.25 to 1.5 (Included, take 1.25 to 1.5)
|
150
|
+
- from 1.25 to 2.5 (Included, take 1.25 to 2.0)
|
151
|
+
- from 2.5 to 3.5 (Not included, omit)
|
152
|
+
"""
|
153
|
+
|
154
|
+
# Here below, at least a part is inside
|
155
|
+
if (
|
156
|
+
current_frame_time < start and
|
157
|
+
frame_end > start
|
158
|
+
):
|
159
|
+
# A part at the end is included
|
160
|
+
end_time = (
|
161
|
+
# From 0.5 to 1.5 0> take 1.0 to 1.5
|
162
|
+
frame_end
|
163
|
+
if frame_end <= end else
|
164
|
+
# From 0.5 to 2.5 => take 1.0 to 2.0
|
165
|
+
end
|
166
|
+
)
|
167
|
+
#print('A part at the end is included.')
|
168
|
+
frame = trim_audio_frame(
|
169
|
+
frame = frame,
|
170
|
+
start = start,
|
171
|
+
end = end_time,
|
172
|
+
time_base = self.time_base
|
173
|
+
)
|
174
|
+
elif (
|
175
|
+
current_frame_time >= start and
|
176
|
+
current_frame_time < end
|
177
|
+
):
|
178
|
+
end_time = (
|
179
|
+
# From 1.25 to 1.5 => take 1.25 to 1.5
|
180
|
+
frame_end
|
181
|
+
if frame_end <= end else
|
182
|
+
# From 1.25 to 2.5 => take 1.25 to 2.0
|
183
|
+
end
|
184
|
+
)
|
185
|
+
# A part at the begining is included
|
186
|
+
#print('A part at the begining is included.')
|
187
|
+
frame = trim_audio_frame(
|
188
|
+
frame = frame,
|
189
|
+
start = current_frame_time,
|
190
|
+
end = end_time,
|
191
|
+
time_base = self.time_base
|
192
|
+
)
|
193
|
+
|
194
|
+
# If the whole frame is in, past as it is
|
195
|
+
yield frame
|
@@ -0,0 +1,48 @@
|
|
1
|
+
from av.audio.frame import AudioFrame
|
2
|
+
from quicktions import Fraction
|
3
|
+
from typing import Union
|
4
|
+
|
5
|
+
|
6
|
+
def trim_audio_frame(
|
7
|
+
frame: AudioFrame,
|
8
|
+
start: Union[int, float, Fraction],
|
9
|
+
end: Union[int, float, Fraction],
|
10
|
+
time_base: Fraction
|
11
|
+
) -> AudioFrame:
|
12
|
+
"""
|
13
|
+
Trim an audio frame to obtain the part between
|
14
|
+
[start, end), that is provided in seconds.
|
15
|
+
"""
|
16
|
+
# (channels, n_samples)
|
17
|
+
samples = frame.to_ndarray()
|
18
|
+
n_samples = samples.shape[1]
|
19
|
+
|
20
|
+
# In seconds
|
21
|
+
frame_start = frame.pts * float(time_base)
|
22
|
+
frame_end = frame_start + (n_samples / frame.sample_rate)
|
23
|
+
|
24
|
+
# Overlapping
|
25
|
+
cut_start = max(frame_start, float(start))
|
26
|
+
cut_end = min(frame_end, float(end))
|
27
|
+
|
28
|
+
if cut_start >= cut_end:
|
29
|
+
# No overlapping
|
30
|
+
return None
|
31
|
+
|
32
|
+
# To sample indexes
|
33
|
+
start_index = int(round((cut_start - frame_start) * frame.sample_rate))
|
34
|
+
end_index = int(round((cut_end - frame_start) * frame.sample_rate))
|
35
|
+
|
36
|
+
new_frame = AudioFrame.from_ndarray(
|
37
|
+
# end_index is not included: so [start, end)
|
38
|
+
array = samples[:, start_index:end_index],
|
39
|
+
format = frame.format,
|
40
|
+
layout = frame.layout
|
41
|
+
)
|
42
|
+
|
43
|
+
# Set attributes
|
44
|
+
new_frame.sample_rate = frame.sample_rate
|
45
|
+
new_frame.time_base = time_base
|
46
|
+
new_frame.pts = int(round(cut_start / float(time_base)))
|
47
|
+
|
48
|
+
return new_frame
|
@@ -0,0 +1,110 @@
|
|
1
|
+
|
2
|
+
from yta_video_opengl.reader.cache import FrameCache
|
3
|
+
from yta_video_opengl.t import T
|
4
|
+
from yta_validation.parameter import ParameterValidator
|
5
|
+
from av.container import InputContainer
|
6
|
+
from av.video.stream import VideoStream
|
7
|
+
from av.video.frame import VideoFrame
|
8
|
+
from quicktions import Fraction
|
9
|
+
from typing import Union
|
10
|
+
|
11
|
+
|
12
|
+
class VideoFrameCache(FrameCache):
|
13
|
+
"""
|
14
|
+
Cache for the video frames.
|
15
|
+
"""
|
16
|
+
|
17
|
+
@property
|
18
|
+
def fps(
|
19
|
+
self
|
20
|
+
) -> Union[Fraction, None]:
|
21
|
+
"""
|
22
|
+
The frames per second.
|
23
|
+
"""
|
24
|
+
return self.stream.average_rate
|
25
|
+
|
26
|
+
@property
|
27
|
+
def frame_duration(
|
28
|
+
self
|
29
|
+
) -> int:
|
30
|
+
"""
|
31
|
+
The frame duration in ticks, which is the
|
32
|
+
minimum amount of time, 1 / time_base.
|
33
|
+
"""
|
34
|
+
return self.stream.duration / self.stream.frames
|
35
|
+
|
36
|
+
def __init__(
|
37
|
+
self,
|
38
|
+
container: InputContainer,
|
39
|
+
stream: VideoStream,
|
40
|
+
size: Union[int, None] = None
|
41
|
+
):
|
42
|
+
ParameterValidator.validate_mandatory_instance_of('stream', stream, VideoStream)
|
43
|
+
|
44
|
+
super().__init__(container, stream, size)
|
45
|
+
|
46
|
+
def get_frame(
|
47
|
+
self,
|
48
|
+
t: Union[int, float, Fraction]
|
49
|
+
) -> VideoFrame:
|
50
|
+
"""
|
51
|
+
Get the video frame that is in the 't'
|
52
|
+
time moment provided.
|
53
|
+
"""
|
54
|
+
t: T = T.from_fps(t, self.fps)
|
55
|
+
for frame in self.get_frames(t.truncated, t.next(1).truncated):
|
56
|
+
return frame
|
57
|
+
|
58
|
+
def get_frames(
|
59
|
+
self,
|
60
|
+
start: Union[int, float, Fraction],
|
61
|
+
end: Union[int, float, Fraction]
|
62
|
+
):
|
63
|
+
"""
|
64
|
+
Get all the frames in the range between
|
65
|
+
the provided 'start' and 'end' time in
|
66
|
+
seconds.
|
67
|
+
|
68
|
+
This method is an iterator that yields
|
69
|
+
the frame, its t and its index.
|
70
|
+
"""
|
71
|
+
# TODO: Validate 'start' and 'end' are mandatory
|
72
|
+
# positive numbers
|
73
|
+
# Make sure the 'start' and 'end' time moments
|
74
|
+
# provided are truncated values based on the
|
75
|
+
# stream time base
|
76
|
+
start = T(start, self.time_base).truncated
|
77
|
+
end = T(end, self.time_base).truncated
|
78
|
+
|
79
|
+
if end <= start:
|
80
|
+
raise Exception(f'The time range start:{str(float(start))} - end:{str(float(end))}) is not valid.')
|
81
|
+
|
82
|
+
key_frame_pts = self._get_nearest_keyframe_pts(start / self.time_base)
|
83
|
+
|
84
|
+
if (
|
85
|
+
self._last_packet_accessed is None or
|
86
|
+
self._last_packet_accessed.pts != key_frame_pts
|
87
|
+
):
|
88
|
+
self._seek(key_frame_pts)
|
89
|
+
|
90
|
+
for packet in self.container.demux(self.stream):
|
91
|
+
if packet.pts is None:
|
92
|
+
continue
|
93
|
+
|
94
|
+
self._last_packet_accessed = packet
|
95
|
+
|
96
|
+
for frame in packet.decode():
|
97
|
+
if frame.pts is None:
|
98
|
+
continue
|
99
|
+
|
100
|
+
# We store all the frames in cache
|
101
|
+
self._store_frame_in_cache(frame)
|
102
|
+
|
103
|
+
current_frame_time = frame.pts * self.time_base
|
104
|
+
|
105
|
+
# We want the range [start, end)
|
106
|
+
if start <= current_frame_time < end:
|
107
|
+
yield frame
|
108
|
+
|
109
|
+
if current_frame_time >= end:
|
110
|
+
break
|
@@ -182,7 +182,7 @@ class Video:
|
|
182
182
|
Get the video frame with the given 't' time
|
183
183
|
moment, using the video cache system.
|
184
184
|
"""
|
185
|
-
return self.reader.video_cache.
|
185
|
+
return self.reader.video_cache.get_frame(self._get_real_t(t))
|
186
186
|
|
187
187
|
def get_audio_frame_from_t(
|
188
188
|
self,
|
@@ -197,7 +197,7 @@ class Video:
|
|
197
197
|
|
198
198
|
TODO: Is this actually necessary (?)
|
199
199
|
"""
|
200
|
-
return self.reader.
|
200
|
+
return self.reader.get_audio_frame_from_t(self._get_real_t(t))
|
201
201
|
|
202
202
|
def get_audio_frames_from_t(
|
203
203
|
self,
|
@@ -1,512 +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
|
-
A stream can have 'fps = 60' but use another
|
20
|
-
different time base that make the pts values go 0,
|
21
|
-
256, 512... for example. The 'time_base' is the
|
22
|
-
only accurate way to obtain the pts.
|
23
|
-
|
24
|
-
Feel free to move this explanation to other
|
25
|
-
place, its about the duration.
|
26
|
-
|
27
|
-
The stream 'duration' parameter is measured
|
28
|
-
on ticks, the amount of ticks that the
|
29
|
-
stream lasts. Here below is an example:
|
30
|
-
|
31
|
-
- Duration raw: 529200
|
32
|
-
- Time base: 1/44100
|
33
|
-
- Duration (seconds): 12.0
|
34
|
-
"""
|
35
|
-
from yta_video_opengl.t import T
|
36
|
-
from av.container import InputContainer
|
37
|
-
from av.video.stream import VideoStream
|
38
|
-
from av.audio.stream import AudioStream
|
39
|
-
from av.video.frame import VideoFrame
|
40
|
-
from av.audio.frame import AudioFrame
|
41
|
-
from av.packet import Packet
|
42
|
-
from yta_validation.parameter import ParameterValidator
|
43
|
-
from yta_validation import PythonValidator
|
44
|
-
from quicktions import Fraction
|
45
|
-
from collections import OrderedDict
|
46
|
-
from typing import Union
|
47
|
-
|
48
|
-
import numpy as np
|
49
|
-
import math
|
50
|
-
|
51
|
-
|
52
|
-
# TODO: This is not actually a Video
|
53
|
-
# cache, is a FrameCache because we
|
54
|
-
# create one for video but another
|
55
|
-
# one for audio. Rename it please.
|
56
|
-
class VideoFrameCache:
|
57
|
-
"""
|
58
|
-
Class to manage the frames cache of a video
|
59
|
-
within a video reader instance.
|
60
|
-
"""
|
61
|
-
|
62
|
-
@property
|
63
|
-
def fps(
|
64
|
-
self
|
65
|
-
) -> Union[int, Fraction, None]:
|
66
|
-
"""
|
67
|
-
The frames per second.
|
68
|
-
"""
|
69
|
-
return (
|
70
|
-
self.stream.average_rate
|
71
|
-
if self.stream.type == 'video' else
|
72
|
-
self.stream.rate
|
73
|
-
)
|
74
|
-
|
75
|
-
@property
|
76
|
-
def time_base(
|
77
|
-
self
|
78
|
-
) -> Union[Fraction, None]:
|
79
|
-
"""
|
80
|
-
The time base of the stream.
|
81
|
-
"""
|
82
|
-
return self.stream.time_base
|
83
|
-
|
84
|
-
def __init__(
|
85
|
-
self,
|
86
|
-
container: InputContainer,
|
87
|
-
stream: Union[VideoStream, AudioStream],
|
88
|
-
size: Union[int, None] = None
|
89
|
-
):
|
90
|
-
ParameterValidator.validate_mandatory_instance_of('container', container, InputContainer)
|
91
|
-
ParameterValidator.validate_mandatory_instance_of('stream', stream, [VideoStream, AudioStream])
|
92
|
-
ParameterValidator.validate_positive_int('size', size)
|
93
|
-
|
94
|
-
self.container: InputContainer = container
|
95
|
-
"""
|
96
|
-
The pyav container.
|
97
|
-
"""
|
98
|
-
self.stream: Union[VideoStream, AudioStream] = stream
|
99
|
-
"""
|
100
|
-
The pyav stream.
|
101
|
-
"""
|
102
|
-
self.cache: OrderedDict = OrderedDict()
|
103
|
-
"""
|
104
|
-
The cache ordered dictionary.
|
105
|
-
"""
|
106
|
-
self.size: Union[int, None] = size
|
107
|
-
"""
|
108
|
-
The size (in number of frames) of the cache.
|
109
|
-
"""
|
110
|
-
self.key_frames_pts: list[int] = []
|
111
|
-
"""
|
112
|
-
The list that contains the timestamps of the
|
113
|
-
key frame packets, ordered from begining to
|
114
|
-
end.
|
115
|
-
"""
|
116
|
-
|
117
|
-
# TODO: This is new, remove this comment if
|
118
|
-
# it is ok
|
119
|
-
# TODO: This way of obtaining the duration
|
120
|
-
# in ticks must be a utils
|
121
|
-
self.frame_duration: int = (
|
122
|
-
self.stream.duration / self.stream.frames
|
123
|
-
if PythonValidator.is_instance_of(stream, VideoStream) else
|
124
|
-
# TODO: Is this below ok (?)
|
125
|
-
self.stream.frames
|
126
|
-
)
|
127
|
-
"""
|
128
|
-
The duration (in ticks) of the frame, that
|
129
|
-
is the step between the different pts.
|
130
|
-
"""
|
131
|
-
self._last_packet_accessed: Union[Packet, None] = None
|
132
|
-
"""
|
133
|
-
The last packet that has been accessed
|
134
|
-
"""
|
135
|
-
self._last_frame_read: Union[VideoFrame, AudioFrame, None] = None
|
136
|
-
"""
|
137
|
-
The last frame we have read when decoding.
|
138
|
-
Useful to avoid seeking all the time when we
|
139
|
-
don't need it.
|
140
|
-
"""
|
141
|
-
|
142
|
-
self._prepare()
|
143
|
-
|
144
|
-
def _prepare(
|
145
|
-
self
|
146
|
-
):
|
147
|
-
# Index key frames
|
148
|
-
for packet in self.container.demux(self.stream):
|
149
|
-
if packet.is_keyframe:
|
150
|
-
self.key_frames_pts.append(packet.pts)
|
151
|
-
|
152
|
-
# The cache size will be auto-calculated to
|
153
|
-
# use the amount of frames of the biggest
|
154
|
-
# interval of frames that belongs to a key
|
155
|
-
# frame, or a value by default
|
156
|
-
# TODO: Careful if this is too big
|
157
|
-
fps = (
|
158
|
-
float(self.stream.average_rate)
|
159
|
-
if PythonValidator.is_instance_of(self.stream, VideoStream) else
|
160
|
-
float(self.stream.rate)
|
161
|
-
)
|
162
|
-
# Intervals, but in number of frames
|
163
|
-
intervals = np.diff(
|
164
|
-
# Intervals of time between keyframes
|
165
|
-
np.array(self.key_frames_pts) * self.time_base
|
166
|
-
) * fps
|
167
|
-
|
168
|
-
self.size = (
|
169
|
-
math.ceil(np.max(intervals))
|
170
|
-
if intervals.size > 0 else
|
171
|
-
(
|
172
|
-
self.size or
|
173
|
-
# TODO: Make this 'default_size' a setting or something
|
174
|
-
60
|
175
|
-
)
|
176
|
-
)
|
177
|
-
|
178
|
-
self.container.seek(0)
|
179
|
-
|
180
|
-
def _get_nearest_keyframe_pts(
|
181
|
-
self,
|
182
|
-
pts: int
|
183
|
-
):
|
184
|
-
"""
|
185
|
-
Get the fps of the keyframe that is the
|
186
|
-
nearest to the provided 'pts'. Useful to
|
187
|
-
seek and start decoding frames from that
|
188
|
-
keyframe.
|
189
|
-
"""
|
190
|
-
return max([
|
191
|
-
key_frame_pts
|
192
|
-
for key_frame_pts in self.key_frames_pts
|
193
|
-
if key_frame_pts <= pts
|
194
|
-
])
|
195
|
-
|
196
|
-
def _store_frame_in_cache(
|
197
|
-
self,
|
198
|
-
frame: Union[VideoFrame, AudioFrame]
|
199
|
-
) -> Union[VideoFrame, AudioFrame]:
|
200
|
-
"""
|
201
|
-
Store the provided 'frame' in cache if it
|
202
|
-
is not on it, removing the first item of
|
203
|
-
the cache if full.
|
204
|
-
"""
|
205
|
-
if frame.pts not in self.cache:
|
206
|
-
self.cache[frame.pts] = frame
|
207
|
-
|
208
|
-
# Clean cache if full
|
209
|
-
if len(self.cache) > self.size:
|
210
|
-
self.cache.popitem(last = False)
|
211
|
-
|
212
|
-
return frame
|
213
|
-
|
214
|
-
def _seek(
|
215
|
-
self,
|
216
|
-
pts: int
|
217
|
-
):
|
218
|
-
"""
|
219
|
-
Seek to the given 'pts' only if it is not
|
220
|
-
the next 'pts' to the last read, and it
|
221
|
-
will also apply a pad to avoid problems
|
222
|
-
when reading audio frames.
|
223
|
-
|
224
|
-
TODO: Apply the padding only to audio
|
225
|
-
frame reading (?)
|
226
|
-
"""
|
227
|
-
# I found that it is recommended to
|
228
|
-
# read ~100ms before the pts we want to
|
229
|
-
# actually read so we obtain the frames
|
230
|
-
# clean (this is important in audio)
|
231
|
-
# TODO: This is maybe too much for a
|
232
|
-
# video and not needed
|
233
|
-
pts_pad = int(0.1 / self.time_base)
|
234
|
-
self.container.seek(
|
235
|
-
offset = max(0, pts - pts_pad),
|
236
|
-
stream = self.stream
|
237
|
-
)
|
238
|
-
|
239
|
-
def get_video_frame(
|
240
|
-
self,
|
241
|
-
t: Union[int, float, Fraction]
|
242
|
-
) -> VideoFrame:
|
243
|
-
"""
|
244
|
-
Get the video frame that is in the 't'
|
245
|
-
time moment provided.
|
246
|
-
"""
|
247
|
-
for frame in self.get_video_frames(t):
|
248
|
-
return frame
|
249
|
-
|
250
|
-
def get_video_frames(
|
251
|
-
self,
|
252
|
-
start: Union[int, float, Fraction] = 0,
|
253
|
-
end: Union[int, float, Fraction, None] = None
|
254
|
-
):
|
255
|
-
"""
|
256
|
-
Get all the frames in the range between
|
257
|
-
the provided 'start' and 'end' time in
|
258
|
-
seconds.
|
259
|
-
|
260
|
-
This method is an iterator that yields
|
261
|
-
the frame, its t and its index.
|
262
|
-
"""
|
263
|
-
start = T(start, self.time_base).truncated
|
264
|
-
end = (
|
265
|
-
T(end, self.time_base).truncated
|
266
|
-
if end is not None else
|
267
|
-
# The next frame
|
268
|
-
start + (1 / self.fps)
|
269
|
-
)
|
270
|
-
|
271
|
-
key_frame_pts = self._get_nearest_keyframe_pts(start / self.time_base)
|
272
|
-
|
273
|
-
if (
|
274
|
-
self._last_packet_accessed is None or
|
275
|
-
self._last_packet_accessed.pts != key_frame_pts
|
276
|
-
):
|
277
|
-
self._seek(key_frame_pts)
|
278
|
-
|
279
|
-
for packet in self.container.demux(self.stream):
|
280
|
-
if packet.pts is None:
|
281
|
-
continue
|
282
|
-
|
283
|
-
self._last_packet_accessed = packet
|
284
|
-
|
285
|
-
for frame in packet.decode():
|
286
|
-
if frame.pts is None:
|
287
|
-
continue
|
288
|
-
|
289
|
-
# We store all the frames in cache
|
290
|
-
self._store_frame_in_cache(frame)
|
291
|
-
|
292
|
-
current_frame_time = frame.pts * self.time_base
|
293
|
-
|
294
|
-
# We want the range [start, end)
|
295
|
-
if start <= current_frame_time < end:
|
296
|
-
yield frame
|
297
|
-
|
298
|
-
if current_frame_time >= end:
|
299
|
-
break
|
300
|
-
|
301
|
-
def get_audio_frame_from_t(
|
302
|
-
self,
|
303
|
-
t: Union[int, float, Fraction]
|
304
|
-
):
|
305
|
-
"""
|
306
|
-
Get the single audio frame that must be
|
307
|
-
played at the 't' time moment provided.
|
308
|
-
This method is useful to get the single
|
309
|
-
audio frame that we need to combine
|
310
|
-
when using it in a composition.
|
311
|
-
|
312
|
-
TODO: Are we actually using this method (?)
|
313
|
-
"""
|
314
|
-
t: T = T(t, self.time_base)
|
315
|
-
# We need the just one audio frame
|
316
|
-
for frame in self.get_audio_frames(t.truncated, t.next(1).truncated):
|
317
|
-
return frame
|
318
|
-
|
319
|
-
def get_audio_frames_from_t(
|
320
|
-
self,
|
321
|
-
t: Union[int, float, Fraction]
|
322
|
-
):
|
323
|
-
"""
|
324
|
-
Get all the audio frames that must be
|
325
|
-
played at the 't' time moment provided.
|
326
|
-
"""
|
327
|
-
for frame in self.get_audio_frames(t):
|
328
|
-
yield frame
|
329
|
-
|
330
|
-
def get_audio_frames(
|
331
|
-
self,
|
332
|
-
start: Union[int, float, Fraction] = 0,
|
333
|
-
end: Union[int, float, Fraction, None] = None
|
334
|
-
):
|
335
|
-
"""
|
336
|
-
Get all the audio frames in the range
|
337
|
-
between the provided 'start' and 'end'
|
338
|
-
time (in seconds).
|
339
|
-
|
340
|
-
This method is an iterator that yields
|
341
|
-
the frame, its t and its index.
|
342
|
-
"""
|
343
|
-
# TODO: Is this ok? We are trying to obtain
|
344
|
-
# the audio frames for a video frame, so
|
345
|
-
# should we use the 'self.time_base' to
|
346
|
-
# truncate (?)
|
347
|
-
start = T(start, self.time_base).truncated
|
348
|
-
end = (
|
349
|
-
T(end, self.time_base).truncated
|
350
|
-
if end is not None else
|
351
|
-
start + (1 / self.fps)
|
352
|
-
)
|
353
|
-
|
354
|
-
key_frame_pts = self._get_nearest_keyframe_pts(start / self.time_base)
|
355
|
-
|
356
|
-
if (
|
357
|
-
self._last_packet_accessed is None or
|
358
|
-
self._last_packet_accessed.pts != key_frame_pts
|
359
|
-
):
|
360
|
-
self._seek(key_frame_pts)
|
361
|
-
|
362
|
-
for packet in self.container.demux(self.stream):
|
363
|
-
if packet.pts is None:
|
364
|
-
continue
|
365
|
-
|
366
|
-
self._last_packet_accessed = packet
|
367
|
-
|
368
|
-
for frame in packet.decode():
|
369
|
-
if frame.pts is None:
|
370
|
-
continue
|
371
|
-
|
372
|
-
# We store all the frames in cache
|
373
|
-
self._store_frame_in_cache(frame)
|
374
|
-
|
375
|
-
current_frame_time = frame.pts * self.time_base
|
376
|
-
# End is not included, its the start of the
|
377
|
-
# next frame actually
|
378
|
-
frame_end = current_frame_time + (frame.samples / self.stream.sample_rate)
|
379
|
-
|
380
|
-
# For the next comments imagine we are looking
|
381
|
-
# for the [1.0, 2.0) audio time range
|
382
|
-
# Previous frame and nothing is inside
|
383
|
-
if frame_end <= start:
|
384
|
-
# From 0.25 to 1.0
|
385
|
-
continue
|
386
|
-
|
387
|
-
# We finished, nothing is inside and its after
|
388
|
-
if current_frame_time >= end:
|
389
|
-
# From 2.0 to 2.75
|
390
|
-
return
|
391
|
-
|
392
|
-
# If we need audio from 1 to 2, audio is:
|
393
|
-
# - from 0 to 0.75 (Not included, omit)
|
394
|
-
# - from 0.5 to 1.5 (Included, take 1.0 to 1.5)
|
395
|
-
# - from 0.5 to 2.5 (Included, take 1.0 to 2.0)
|
396
|
-
# - from 1.25 to 1.5 (Included, take 1.25 to 1.5)
|
397
|
-
# - from 1.25 to 2.5 (Included, take 1.25 to 2.0)
|
398
|
-
# - from 2.5 to 3.5 (Not included, omit)
|
399
|
-
|
400
|
-
# Here below, at least a part is inside
|
401
|
-
if (
|
402
|
-
current_frame_time < start and
|
403
|
-
frame_end > start
|
404
|
-
):
|
405
|
-
# A part at the end is included
|
406
|
-
end_time = (
|
407
|
-
# From 0.5 to 1.5 0> take 1.0 to 1.5
|
408
|
-
frame_end
|
409
|
-
if frame_end <= end else
|
410
|
-
# From 0.5 to 2.5 => take 1.0 to 2.0
|
411
|
-
end
|
412
|
-
)
|
413
|
-
#print('A part at the end is included.')
|
414
|
-
frame = trim_audio_frame(
|
415
|
-
frame = frame,
|
416
|
-
start = start,
|
417
|
-
end = end_time,
|
418
|
-
time_base = self.time_base
|
419
|
-
)
|
420
|
-
elif (
|
421
|
-
current_frame_time >= start and
|
422
|
-
current_frame_time < end
|
423
|
-
):
|
424
|
-
end_time = (
|
425
|
-
# From 1.25 to 1.5 => take 1.25 to 1.5
|
426
|
-
frame_end
|
427
|
-
if frame_end <= end else
|
428
|
-
# From 1.25 to 2.5 => take 1.25 to 2.0
|
429
|
-
end
|
430
|
-
)
|
431
|
-
# A part at the begining is included
|
432
|
-
#print('A part at the begining is included.')
|
433
|
-
frame = trim_audio_frame(
|
434
|
-
frame = frame,
|
435
|
-
start = current_frame_time,
|
436
|
-
end = end_time,
|
437
|
-
time_base = self.time_base
|
438
|
-
)
|
439
|
-
|
440
|
-
# If the whole frame is in, past as it is
|
441
|
-
yield frame
|
442
|
-
|
443
|
-
def clear(
|
444
|
-
self
|
445
|
-
) -> 'VideoFrameCache':
|
446
|
-
"""
|
447
|
-
Clear the cache by removing all the items.
|
448
|
-
"""
|
449
|
-
self.cache.clear()
|
450
|
-
|
451
|
-
return self
|
452
|
-
|
453
|
-
def trim_audio_frame(
|
454
|
-
frame: AudioFrame,
|
455
|
-
start: Union[int, float, Fraction],
|
456
|
-
end: Union[int, float, Fraction],
|
457
|
-
time_base: Fraction
|
458
|
-
) -> AudioFrame:
|
459
|
-
"""
|
460
|
-
Trim an audio frame to obtain the part between
|
461
|
-
[start, end), that is provided in seconds.
|
462
|
-
"""
|
463
|
-
# (channels, n_samples)
|
464
|
-
samples = frame.to_ndarray()
|
465
|
-
n_samples = samples.shape[1]
|
466
|
-
|
467
|
-
# In seconds
|
468
|
-
frame_start = frame.pts * float(time_base)
|
469
|
-
frame_end = frame_start + (n_samples / frame.sample_rate)
|
470
|
-
|
471
|
-
# Overlapping
|
472
|
-
cut_start = max(frame_start, float(start))
|
473
|
-
cut_end = min(frame_end, float(end))
|
474
|
-
|
475
|
-
if cut_start >= cut_end:
|
476
|
-
# No overlapping
|
477
|
-
return None
|
478
|
-
|
479
|
-
# To sample indexes
|
480
|
-
start_index = int(round((cut_start - frame_start) * frame.sample_rate))
|
481
|
-
end_index = int(round((cut_end - frame_start) * frame.sample_rate))
|
482
|
-
|
483
|
-
new_frame = AudioFrame.from_ndarray(
|
484
|
-
# end_index is not included: so [start, end)
|
485
|
-
array = samples[:, start_index:end_index],
|
486
|
-
format = frame.format,
|
487
|
-
layout = frame.layout
|
488
|
-
)
|
489
|
-
|
490
|
-
# Set attributes
|
491
|
-
new_frame.sample_rate = frame.sample_rate
|
492
|
-
new_frame.time_base = time_base
|
493
|
-
new_frame.pts = int(round(cut_start / float(time_base)))
|
494
|
-
|
495
|
-
return new_frame
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
"""
|
500
|
-
There is a way of editing videos being
|
501
|
-
able to arbitrary access to frames, that
|
502
|
-
is transforming the source videos to
|
503
|
-
intra-frame videos. This is a ffmpeg
|
504
|
-
command that can do it:
|
505
|
-
|
506
|
-
- `ffmpeg -i input.mp4 -c:v libx264 -x264opts keyint=1 -preset fast -crf 18 -c:a copy output_intra.mp4`
|
507
|
-
|
508
|
-
Once you have the 'output_intra.mp4',
|
509
|
-
each packet can decodify its frame
|
510
|
-
depending not on the previous one, being
|
511
|
-
able to seek and jump easy.
|
512
|
-
"""
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{yta_video_opengl-0.0.14 → yta_video_opengl-0.0.15}/src/yta_video_opengl/complete/__init__.py
RENAMED
File without changes
|
File without changes
|
{yta_video_opengl-0.0.14 → yta_video_opengl-0.0.15}/src/yta_video_opengl/nodes/audio/__init__.py
RENAMED
File without changes
|
{yta_video_opengl-0.0.14 → yta_video_opengl-0.0.15}/src/yta_video_opengl/nodes/video/__init__.py
RENAMED
File without changes
|
{yta_video_opengl-0.0.14 → yta_video_opengl-0.0.15}/src/yta_video_opengl/nodes/video/opengl.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|