yta-video-opengl 0.0.11__py3-none-any.whl → 0.0.13__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/complete/timeline.py +147 -59
- yta_video_opengl/complete/track.py +302 -27
- yta_video_opengl/complete/video_on_track.py +72 -9
- yta_video_opengl/reader/__init__.py +190 -89
- yta_video_opengl/reader/cache.py +258 -32
- yta_video_opengl/t.py +185 -0
- yta_video_opengl/tests.py +4 -2
- yta_video_opengl/utils.py +169 -8
- yta_video_opengl/video.py +85 -12
- yta_video_opengl/writer.py +23 -14
- {yta_video_opengl-0.0.11.dist-info → yta_video_opengl-0.0.13.dist-info}/METADATA +2 -1
- yta_video_opengl-0.0.13.dist-info/RECORD +21 -0
- yta_video_opengl-0.0.11.dist-info/RECORD +0 -20
- {yta_video_opengl-0.0.11.dist-info → yta_video_opengl-0.0.13.dist-info}/LICENSE +0 -0
- {yta_video_opengl-0.0.11.dist-info → yta_video_opengl-0.0.13.dist-info}/WHEEL +0 -0
@@ -1,9 +1,157 @@
|
|
1
1
|
from yta_video_opengl.complete.video_on_track import VideoOnTrack
|
2
2
|
from yta_video_opengl.video import Video
|
3
|
+
from yta_video_opengl.t import T
|
4
|
+
from yta_video_opengl.utils import get_black_background_video_frame, get_silent_audio_frame, audio_frames_and_remainder_per_video_frame
|
5
|
+
from yta_video_opengl.t import fps_to_time_base
|
3
6
|
from yta_validation.parameter import ParameterValidator
|
7
|
+
from quicktions import Fraction
|
4
8
|
from typing import Union
|
5
9
|
|
6
10
|
|
11
|
+
NON_LIMITED_EMPTY_PART_END = 999
|
12
|
+
"""
|
13
|
+
A value to indicate that the empty part
|
14
|
+
has no end because it is in the last
|
15
|
+
position and there is no video after it.
|
16
|
+
"""
|
17
|
+
class _Part:
|
18
|
+
"""
|
19
|
+
Class to represent an element that is on the
|
20
|
+
track, that can be an empty space or a video
|
21
|
+
(with audio).
|
22
|
+
"""
|
23
|
+
|
24
|
+
@property
|
25
|
+
def is_empty_part(
|
26
|
+
self
|
27
|
+
) -> bool:
|
28
|
+
"""
|
29
|
+
Flag to indicate if the part is an empty part,
|
30
|
+
which means that there is no video associated
|
31
|
+
but an empty space.
|
32
|
+
"""
|
33
|
+
return self.video is None
|
34
|
+
|
35
|
+
def __init__(
|
36
|
+
self,
|
37
|
+
track: 'Track',
|
38
|
+
start: Union[int, float, Fraction],
|
39
|
+
end: Union[int, float, Fraction],
|
40
|
+
video: Union[VideoOnTrack, None] = None
|
41
|
+
):
|
42
|
+
# TODO: We need to accept Fraction as number
|
43
|
+
# ParameterValidator.validate_mandatory_positive_number('start', start, do_include_zero = True)
|
44
|
+
# TODO: We need to accept Fraction as number
|
45
|
+
# ParameterValidator.validate_mandatory_positive_number('end', end, do_include_zero = False)
|
46
|
+
ParameterValidator.validate_instance_of('video', video, VideoOnTrack)
|
47
|
+
|
48
|
+
self._track: Track = track
|
49
|
+
"""
|
50
|
+
The instance of the track this part belongs
|
51
|
+
to.
|
52
|
+
"""
|
53
|
+
self.start: Fraction = Fraction(start)
|
54
|
+
"""
|
55
|
+
The start 't' time moment of the part.
|
56
|
+
"""
|
57
|
+
self.end: Fraction = Fraction(end)
|
58
|
+
"""
|
59
|
+
The end 't' time moment of the part.
|
60
|
+
"""
|
61
|
+
self.video: Union[VideoOnTrack, None] = video
|
62
|
+
"""
|
63
|
+
The video associated, if existing, or
|
64
|
+
None if it is an empty space that we need
|
65
|
+
to fulfill with a black background and
|
66
|
+
silent audio.
|
67
|
+
"""
|
68
|
+
|
69
|
+
def get_frame_at(
|
70
|
+
self,
|
71
|
+
t: Union[int, float, Fraction]
|
72
|
+
) -> 'VideoFrame':
|
73
|
+
"""
|
74
|
+
Get the frame that must be displayed at
|
75
|
+
the given 't' time moment.
|
76
|
+
"""
|
77
|
+
if self.is_empty_part:
|
78
|
+
# TODO: What about the 'format' (?)
|
79
|
+
# TODO: Maybe I shouldn't set the 'time_base'
|
80
|
+
# here and do it just in the Timeline 'render'
|
81
|
+
#return get_black_background_video_frame(self._track.size)
|
82
|
+
# TODO: This 'time_base' maybe has to be related
|
83
|
+
# to a Timeline general 'time_base' and not the fps
|
84
|
+
return get_black_background_video_frame(self._track.size, time_base = fps_to_time_base(self._track.fps))
|
85
|
+
|
86
|
+
frame = self.video.get_frame_at(t)
|
87
|
+
|
88
|
+
# TODO: This should not happen because of
|
89
|
+
# the way we handle the videos here but the
|
90
|
+
# video could send us a None frame here, so
|
91
|
+
# do we raise exception (?)
|
92
|
+
if frame is None:
|
93
|
+
#frame = get_black_background_video_frame(self._track.size)
|
94
|
+
# TODO: By now I'm raising exception to check if
|
95
|
+
# this happens or not because I think it would
|
96
|
+
# be malfunctioning
|
97
|
+
raise Exception(f'Video is returning None frame at t={str(t)}.')
|
98
|
+
|
99
|
+
return frame
|
100
|
+
|
101
|
+
# TODO: I'm not sure if we need this
|
102
|
+
def get_audio_frames_at(
|
103
|
+
self,
|
104
|
+
t: Union[int, float, Fraction]
|
105
|
+
):
|
106
|
+
if not self.is_empty_part:
|
107
|
+
frames = self.video.get_audio_frames_at(t)
|
108
|
+
else:
|
109
|
+
# TODO: Transform this below to a utils in
|
110
|
+
# which I obtain the array directly
|
111
|
+
# Check many full and partial silent frames we need
|
112
|
+
number_of_frames, number_of_remaining_samples = audio_frames_and_remainder_per_video_frame(
|
113
|
+
video_fps = self._track.fps,
|
114
|
+
sample_rate = self._track.audio_fps,
|
115
|
+
number_of_samples_per_audio_frame = self._track.audio_samples_per_frame
|
116
|
+
)
|
117
|
+
|
118
|
+
# TODO: I need to set the pts, but here (?)
|
119
|
+
# The complete silent frames we need
|
120
|
+
frames = (
|
121
|
+
[
|
122
|
+
get_silent_audio_frame(
|
123
|
+
sample_rate = self._track.audio_fps,
|
124
|
+
# TODO: Check where do we get this value from
|
125
|
+
layout = 'stereo',
|
126
|
+
number_of_samples = self._track.audio_samples_per_frame,
|
127
|
+
# TODO: Check where do we get this value from
|
128
|
+
format = 'fltp'
|
129
|
+
)
|
130
|
+
] * number_of_frames
|
131
|
+
if number_of_frames > 0 else
|
132
|
+
[]
|
133
|
+
)
|
134
|
+
|
135
|
+
# The remaining partial silent frames we need
|
136
|
+
if number_of_remaining_samples > 0:
|
137
|
+
frames.append(
|
138
|
+
get_silent_audio_frame(
|
139
|
+
sample_rate = self._track.audio_fps,
|
140
|
+
# TODO: Check where do we get this value from
|
141
|
+
layout = 'stereo',
|
142
|
+
number_of_samples = number_of_remaining_samples,
|
143
|
+
# TODO: Check where do we get this value from
|
144
|
+
format = 'fltp'
|
145
|
+
)
|
146
|
+
)
|
147
|
+
|
148
|
+
# TODO: Return or yield (?)
|
149
|
+
for frame in frames:
|
150
|
+
yield frame
|
151
|
+
#return frames
|
152
|
+
|
153
|
+
# TODO: I don't like using t as float,
|
154
|
+
# we need to implement fractions.Fraction
|
7
155
|
# TODO: This is called Track but it is
|
8
156
|
# handling videos only. Should I have
|
9
157
|
# VideoTrack and AudioTrack (?)
|
@@ -14,17 +162,37 @@ class Track:
|
|
14
162
|
project.
|
15
163
|
"""
|
16
164
|
|
165
|
+
@property
|
166
|
+
def parts(
|
167
|
+
self
|
168
|
+
) -> list[_Part]:
|
169
|
+
"""
|
170
|
+
The list of parts that build this track,
|
171
|
+
but with the empty parts detected to
|
172
|
+
be fulfilled with black frames and silent
|
173
|
+
audios.
|
174
|
+
|
175
|
+
A part can be a video or an empty space.
|
176
|
+
"""
|
177
|
+
if (
|
178
|
+
not hasattr(self, '_parts') or
|
179
|
+
self._parts is None
|
180
|
+
):
|
181
|
+
self._recalculate_parts()
|
182
|
+
|
183
|
+
return self._parts
|
184
|
+
|
17
185
|
@property
|
18
186
|
def end(
|
19
187
|
self
|
20
|
-
) ->
|
188
|
+
) -> Fraction:
|
21
189
|
"""
|
22
190
|
The end of the last video of this track,
|
23
191
|
which is also the end of the track. This
|
24
192
|
is the last time moment that has to be
|
25
193
|
rendered.
|
26
194
|
"""
|
27
|
-
return (
|
195
|
+
return Fraction(
|
28
196
|
0.0
|
29
197
|
if len(self.videos) == 0 else
|
30
198
|
max(
|
@@ -34,18 +202,45 @@ class Track:
|
|
34
202
|
)
|
35
203
|
|
36
204
|
def __init__(
|
37
|
-
self
|
205
|
+
self,
|
206
|
+
# TODO: I need the general settings of the
|
207
|
+
# project to be able to make audio also, not
|
208
|
+
# only the empty frames
|
209
|
+
size: tuple[int, int],
|
210
|
+
fps: float,
|
211
|
+
audio_fps: float,
|
212
|
+
# TODO: Where does it come from (?)
|
213
|
+
audio_samples_per_frame: int
|
38
214
|
):
|
39
215
|
self.videos: list[VideoOnTrack] = []
|
40
216
|
"""
|
41
217
|
The list of 'VideoOnTrack' instances that
|
42
218
|
must play on this track.
|
43
219
|
"""
|
220
|
+
self.size: tuple[int, int] = size
|
221
|
+
"""
|
222
|
+
The size of the videos of this track.
|
223
|
+
"""
|
224
|
+
self.fps: float = float(fps)
|
225
|
+
"""
|
226
|
+
The fps of the track, needed to calculate
|
227
|
+
the base t time moments to be precise and
|
228
|
+
to obtain or generate the frames.
|
229
|
+
"""
|
230
|
+
self.audio_fps: float = float(audio_fps)
|
231
|
+
"""
|
232
|
+
The fps of the audio track, needed to
|
233
|
+
generate silent audios for the empty parts.
|
234
|
+
"""
|
235
|
+
self.audio_samples_per_frame: int = audio_samples_per_frame
|
236
|
+
"""
|
237
|
+
The number of samples per audio frame.
|
238
|
+
"""
|
44
239
|
|
45
240
|
def _is_free(
|
46
241
|
self,
|
47
|
-
start: float,
|
48
|
-
end: float
|
242
|
+
start: Union[int, float, Fraction],
|
243
|
+
end: Union[int, float, Fraction]
|
49
244
|
) -> bool:
|
50
245
|
"""
|
51
246
|
Check if the time range in between the
|
@@ -60,47 +255,77 @@ class Track:
|
|
60
255
|
for video in self.videos
|
61
256
|
)
|
62
257
|
|
63
|
-
def
|
258
|
+
def _get_part_at_t(
|
64
259
|
self,
|
65
|
-
t: float
|
66
|
-
) ->
|
260
|
+
t: Union[int, float, Fraction]
|
261
|
+
) -> _Part:
|
67
262
|
"""
|
68
|
-
Get the
|
69
|
-
|
263
|
+
Get the part at the given 't' time
|
264
|
+
moment, that will always exist because
|
265
|
+
we have an special non ended last
|
266
|
+
empty part that would be returned if
|
267
|
+
accessing to an empty 't'.
|
70
268
|
"""
|
71
|
-
for
|
72
|
-
if
|
73
|
-
return
|
269
|
+
for part in self.parts:
|
270
|
+
if part.start <= t < part.end:
|
271
|
+
return part
|
74
272
|
|
273
|
+
# TODO: This will only happen if they are
|
274
|
+
# asking for a value greater than the
|
275
|
+
# NON_LIMITED_EMPTY_PART_END...
|
276
|
+
raise Exception('NON_LIMITED_EMPTY_PART_END exceeded.')
|
75
277
|
return None
|
76
|
-
|
278
|
+
|
77
279
|
def get_frame_at(
|
78
280
|
self,
|
79
|
-
t: float
|
80
|
-
) ->
|
281
|
+
t: Union[int, float, Fraction]
|
282
|
+
) -> 'VideoFrame':
|
81
283
|
"""
|
82
284
|
Get the frame that must be displayed at
|
83
285
|
the 't' time moment provided, which is
|
84
|
-
a frame from the video that is
|
85
|
-
played at that time moment.
|
286
|
+
a frame from the video audio that is
|
287
|
+
being played at that time moment.
|
86
288
|
|
87
289
|
Remember, this 't' time moment provided
|
88
290
|
is about the track, and we make the
|
89
291
|
conversion to the actual video 't' to
|
90
292
|
get the frame.
|
91
293
|
"""
|
92
|
-
|
294
|
+
# TODO: What if the frame, that comes from
|
295
|
+
# a video, doesn't have the expected size (?)
|
296
|
+
return self._get_part_at_t(t).get_frame_at(t)
|
297
|
+
|
298
|
+
# TODO: This is not working well...
|
299
|
+
def get_audio_frames_at(
|
300
|
+
self,
|
301
|
+
t: Union[int, float, Fraction]
|
302
|
+
):
|
303
|
+
"""
|
304
|
+
Get the sequence of audio frames that
|
305
|
+
must be displayed at the 't' time
|
306
|
+
moment provided, which the collection
|
307
|
+
of audio frames corresponding to the
|
308
|
+
video frame that is being played at
|
309
|
+
that time moment.
|
93
310
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
)
|
311
|
+
Remember, this 't' time moment provided
|
312
|
+
is about the track, and we make the
|
313
|
+
conversion to the actual video 't' to
|
314
|
+
get the frame.
|
99
315
|
|
316
|
+
This is useful when we want to write a
|
317
|
+
video frame with its audio, so we obtain
|
318
|
+
all the audio frames associated to it
|
319
|
+
(remember that a video frame is associated
|
320
|
+
with more than 1 audio frame).
|
321
|
+
"""
|
322
|
+
for frame in self._get_part_at_t(t).get_audio_frames_at(t):
|
323
|
+
yield frame
|
324
|
+
|
100
325
|
def add_video(
|
101
326
|
self,
|
102
327
|
video: Video,
|
103
|
-
t: Union[float, None] = None
|
328
|
+
t: Union[int, float, Fraction, None] = None
|
104
329
|
) -> 'Track':
|
105
330
|
"""
|
106
331
|
Add the 'video' provided to the track. If
|
@@ -117,13 +342,15 @@ class Track:
|
|
117
342
|
if no video, or the end of the last video.
|
118
343
|
"""
|
119
344
|
ParameterValidator.validate_mandatory_instance_of('video', video, Video)
|
120
|
-
ParameterValidator.
|
345
|
+
ParameterValidator.validate_positive_number('t', t, do_include_zero = True)
|
121
346
|
|
122
347
|
if t is not None:
|
123
348
|
# TODO: We can have many different strategies
|
124
349
|
# that we could define in the '__init__' maybe
|
125
|
-
|
350
|
+
t: T = T.from_fps(t, self.fps)
|
351
|
+
if not self._is_free(t.truncated, t.next(1).truncated):
|
126
352
|
raise Exception('The video cannot be added at the "t" time moment, something blocks it.')
|
353
|
+
t = t.truncated
|
127
354
|
else:
|
128
355
|
t = self.end
|
129
356
|
|
@@ -132,5 +359,53 @@ class Track:
|
|
132
359
|
t
|
133
360
|
))
|
134
361
|
|
362
|
+
self._recalculate_parts()
|
363
|
+
|
135
364
|
# TODO: Maybe return the VideoOnTrack instead (?)
|
365
|
+
return self
|
366
|
+
|
367
|
+
def _recalculate_parts(
|
368
|
+
self
|
369
|
+
) -> 'Track':
|
370
|
+
"""
|
371
|
+
Check the track and get all the parts. A
|
372
|
+
part can be empty (non video nor audio on
|
373
|
+
that time period, which means black
|
374
|
+
background and silence audio), or a video
|
375
|
+
with (or without) audio.
|
376
|
+
"""
|
377
|
+
parts = []
|
378
|
+
cursor = 0.0
|
379
|
+
|
380
|
+
for video in self.videos:
|
381
|
+
# Empty space between cursor and start of
|
382
|
+
# the next clip
|
383
|
+
if video.start > cursor:
|
384
|
+
parts.append(_Part(
|
385
|
+
track = self,
|
386
|
+
start = cursor,
|
387
|
+
end = video.start,
|
388
|
+
video = None
|
389
|
+
))
|
390
|
+
|
391
|
+
# The video itself
|
392
|
+
parts.append(_Part(
|
393
|
+
track = self,
|
394
|
+
start = video.start,
|
395
|
+
end = video.end,
|
396
|
+
video = video
|
397
|
+
))
|
398
|
+
|
399
|
+
cursor = video.end
|
400
|
+
|
401
|
+
# Add the non limited last empty part
|
402
|
+
parts.append(_Part(
|
403
|
+
track = self,
|
404
|
+
start = cursor,
|
405
|
+
end = NON_LIMITED_EMPTY_PART_END,
|
406
|
+
video = None
|
407
|
+
))
|
408
|
+
|
409
|
+
self._parts = parts
|
410
|
+
|
136
411
|
return self
|
@@ -16,6 +16,8 @@ finished at `t=4`
|
|
16
16
|
from yta_video_opengl.video import Video
|
17
17
|
from yta_validation.parameter import ParameterValidator
|
18
18
|
from av.video.frame import VideoFrame
|
19
|
+
from av.audio.frame import AudioFrame
|
20
|
+
from quicktions import Fraction
|
19
21
|
from typing import Union
|
20
22
|
|
21
23
|
|
@@ -27,7 +29,7 @@ class VideoOnTrack:
|
|
27
29
|
@property
|
28
30
|
def end(
|
29
31
|
self
|
30
|
-
) ->
|
32
|
+
) -> Fraction:
|
31
33
|
"""
|
32
34
|
The end time moment 't' of the video once
|
33
35
|
once its been placed on the track, which
|
@@ -41,17 +43,20 @@ class VideoOnTrack:
|
|
41
43
|
def __init__(
|
42
44
|
self,
|
43
45
|
video: Video,
|
44
|
-
start: float = 0.0
|
46
|
+
start: Union[int, float, Fraction] = 0.0
|
45
47
|
):
|
46
48
|
ParameterValidator.validate_mandatory_instance_of('video', video, Video)
|
47
|
-
|
49
|
+
# TODO: Now we need to accept 'Fraction',
|
50
|
+
# from 'fractions' or 'quicktions', as a
|
51
|
+
# number
|
52
|
+
#ParameterValidator.validate_mandatory_positive_number('start', start, do_include_zero = True)
|
48
53
|
|
49
54
|
self.video: Video = video
|
50
55
|
"""
|
51
56
|
The video source, with all its properties,
|
52
57
|
that is placed in the timeline.
|
53
58
|
"""
|
54
|
-
self.start:
|
59
|
+
self.start: Fraction = Fraction(start)
|
55
60
|
"""
|
56
61
|
The time moment in which the video should
|
57
62
|
start playing, within the timeline.
|
@@ -63,7 +68,7 @@ class VideoOnTrack:
|
|
63
68
|
|
64
69
|
def _get_video_t(
|
65
70
|
self,
|
66
|
-
t: float
|
71
|
+
t: Union[int, float, Fraction]
|
67
72
|
) -> float:
|
68
73
|
"""
|
69
74
|
The video 't' time moment for the given
|
@@ -71,30 +76,88 @@ class VideoOnTrack:
|
|
71
76
|
to use inside the video content to display
|
72
77
|
its frame.
|
73
78
|
"""
|
79
|
+
# TODO: Use 'T' here to be precise or the
|
80
|
+
# argument itself must be precise (?)
|
74
81
|
return t - self.start
|
75
82
|
|
76
83
|
def is_playing(
|
77
84
|
self,
|
78
|
-
t: float
|
85
|
+
t: Union[int, float, Fraction]
|
79
86
|
) -> bool:
|
80
87
|
"""
|
81
88
|
Check if this video is playing at the general
|
82
89
|
't' time moment, which is a global time moment
|
83
90
|
for the whole project.
|
84
91
|
"""
|
92
|
+
# TODO: Use 'T' here to be precise or the
|
93
|
+
# argument itself must be precise (?)
|
85
94
|
return self.start <= t < self.end
|
86
95
|
|
87
96
|
def get_frame_at(
|
88
97
|
self,
|
89
|
-
t: float
|
98
|
+
t: Union[int, float, Fraction]
|
90
99
|
) -> Union[VideoFrame, None]:
|
91
100
|
"""
|
92
101
|
Get the frame for the 't' time moment provided,
|
93
102
|
that could be None if the video is not playing
|
94
103
|
in that moment.
|
95
104
|
"""
|
105
|
+
# TODO: Use 'T' here to be precise or the
|
106
|
+
# argument itself must be precise (?)
|
107
|
+
return (
|
108
|
+
self.video.get_frame_from_t(self._get_video_t(t))
|
109
|
+
if self.is_playing(t) else
|
110
|
+
None
|
111
|
+
)
|
112
|
+
|
113
|
+
def get_audio_frame_at(
|
114
|
+
self,
|
115
|
+
t: Union[int, float, Fraction]
|
116
|
+
) -> Union[AudioFrame, None]:
|
117
|
+
"""
|
118
|
+
Get the audio frame for the 't' time moment
|
119
|
+
provided, that could be None if the video
|
120
|
+
is not playing in that moment.
|
121
|
+
"""
|
122
|
+
# TODO: Use 'T' here to be precise or the
|
123
|
+
# argument itself must be precise (?)
|
96
124
|
return (
|
97
|
-
self.video.
|
125
|
+
self.video.get_audio_frame_from_t(self._get_video_t(t))
|
98
126
|
if self.is_playing(t) else
|
99
127
|
None
|
100
|
-
)
|
128
|
+
)
|
129
|
+
|
130
|
+
def get_audio_frames_at(
|
131
|
+
self,
|
132
|
+
t: Union[int, float, Fraction]
|
133
|
+
) -> Union[any, None]:
|
134
|
+
"""
|
135
|
+
Get the audio frames that must be played at
|
136
|
+
the 't' time moment provided, that could be
|
137
|
+
None if the video is not playing at that
|
138
|
+
moment.
|
139
|
+
|
140
|
+
This method will return None if no audio
|
141
|
+
frames found in that 't' time moment, or an
|
142
|
+
iterator if yes.
|
143
|
+
"""
|
144
|
+
# TODO: Use 'T' here to be precise or the
|
145
|
+
# argument itself must be precise (?)
|
146
|
+
frames = (
|
147
|
+
self.video.get_audio_frames_from_t(self._get_video_t(t))
|
148
|
+
if self.is_playing(t) else
|
149
|
+
[]
|
150
|
+
)
|
151
|
+
|
152
|
+
for frame in frames:
|
153
|
+
# TODO: I am generating a tuple in the
|
154
|
+
# src\yta_video_opengl\reader\cache.py
|
155
|
+
# get_frames method... maybe remove it (?)
|
156
|
+
yield frame[0]
|
157
|
+
|
158
|
+
# # TODO: This was a simple return before
|
159
|
+
# return (
|
160
|
+
# self.video.reader.get_audio_frames_from_t(self._get_video_t(t))
|
161
|
+
# if self.is_playing(t) else
|
162
|
+
# None
|
163
|
+
# )
|