yta-video-opengl 0.0.19__py3-none-any.whl → 0.0.21__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/audio.py +219 -0
- yta_video_opengl/complete/frame_combinator.py +1 -90
- yta_video_opengl/complete/frame_generator.py +40 -0
- yta_video_opengl/complete/frame_wrapper.py +13 -0
- yta_video_opengl/complete/timeline.py +200 -116
- yta_video_opengl/complete/track/__init__.py +500 -0
- yta_video_opengl/complete/{video_on_track.py → track/media/__init__.py} +112 -47
- yta_video_opengl/complete/track/parts.py +267 -0
- yta_video_opengl/complete/track/utils.py +78 -0
- yta_video_opengl/reader/__init__.py +0 -19
- yta_video_opengl/reader/cache/__init__.py +9 -5
- yta_video_opengl/reader/cache/utils.py +1 -1
- yta_video_opengl/tests.py +29 -1
- yta_video_opengl/video.py +9 -13
- {yta_video_opengl-0.0.19.dist-info → yta_video_opengl-0.0.21.dist-info}/METADATA +1 -1
- yta_video_opengl-0.0.21.dist-info/RECORD +30 -0
- yta_video_opengl/complete/track.py +0 -562
- yta_video_opengl-0.0.19.dist-info/RECORD +0 -27
- {yta_video_opengl-0.0.19.dist-info → yta_video_opengl-0.0.21.dist-info}/LICENSE +0 -0
- {yta_video_opengl-0.0.19.dist-info → yta_video_opengl-0.0.21.dist-info}/WHEEL +0 -0
@@ -10,11 +10,12 @@ an important property or will make ffmpeg
|
|
10
10
|
become crazy and deny packets (that means no
|
11
11
|
video written).
|
12
12
|
"""
|
13
|
-
from yta_video_opengl.complete.track import
|
13
|
+
from yta_video_opengl.complete.track import VideoTrack, AudioTrack
|
14
14
|
from yta_video_opengl.video import Video
|
15
15
|
from yta_video_opengl.t import get_ts, fps_to_time_base, T
|
16
|
-
from yta_video_opengl.complete.frame_wrapper import
|
16
|
+
from yta_video_opengl.complete.frame_wrapper import AudioFrameWrapped
|
17
17
|
from yta_video_opengl.complete.frame_combinator import AudioFrameCombinator
|
18
|
+
from yta_video_opengl.writer import VideoWriter
|
18
19
|
from yta_validation.parameter import ParameterValidator
|
19
20
|
from yta_validation import PythonValidator
|
20
21
|
from av.video.frame import VideoFrame
|
@@ -46,6 +47,80 @@ class Timeline:
|
|
46
47
|
track.end
|
47
48
|
for track in self.tracks
|
48
49
|
)
|
50
|
+
|
51
|
+
@property
|
52
|
+
def tracks(
|
53
|
+
self
|
54
|
+
) -> list[Union['AudioTrack', 'VideoTrack']]:
|
55
|
+
"""
|
56
|
+
All the tracks we have but ordered by
|
57
|
+
their indexes, from lower index (highest
|
58
|
+
priority) to highest index (lowest
|
59
|
+
priority).
|
60
|
+
"""
|
61
|
+
return sorted(self._tracks, key = lambda track: track.index)
|
62
|
+
|
63
|
+
@property
|
64
|
+
def video_tracks(
|
65
|
+
self
|
66
|
+
) -> list['VideoTrack']:
|
67
|
+
"""
|
68
|
+
All the video tracks we have but ordered
|
69
|
+
by their indexes, from lower index
|
70
|
+
(highest priority) to highest index
|
71
|
+
(lowest priority).
|
72
|
+
"""
|
73
|
+
return [
|
74
|
+
track
|
75
|
+
for track in self.tracks
|
76
|
+
if PythonValidator.is_instance_of(track, 'VideoTrack')
|
77
|
+
]
|
78
|
+
|
79
|
+
@property
|
80
|
+
def audio_tracks(
|
81
|
+
self
|
82
|
+
) -> list['AudioTrack']:
|
83
|
+
"""
|
84
|
+
All the audio tracks we have but ordered
|
85
|
+
by their indexes, from lower index
|
86
|
+
(highest priority) to highest index
|
87
|
+
(lowest priority).
|
88
|
+
"""
|
89
|
+
return [
|
90
|
+
track
|
91
|
+
for track in self.tracks
|
92
|
+
if PythonValidator.is_instance_of(track, 'AudioTrack')
|
93
|
+
]
|
94
|
+
|
95
|
+
@property
|
96
|
+
def number_of_tracks(
|
97
|
+
self
|
98
|
+
) -> int:
|
99
|
+
"""
|
100
|
+
The number of tracks we have in the
|
101
|
+
timeline.
|
102
|
+
"""
|
103
|
+
return len(self.tracks)
|
104
|
+
|
105
|
+
@property
|
106
|
+
def number_of_video_tracks(
|
107
|
+
self
|
108
|
+
) -> int:
|
109
|
+
"""
|
110
|
+
The number of video tracks we have in the
|
111
|
+
timeline.
|
112
|
+
"""
|
113
|
+
return len(self.video_tracks)
|
114
|
+
|
115
|
+
@property
|
116
|
+
def number_of_audio_tracks(
|
117
|
+
self
|
118
|
+
) -> int:
|
119
|
+
"""
|
120
|
+
The number of audio tracks we have in the
|
121
|
+
timeline.
|
122
|
+
"""
|
123
|
+
return len(self.audio_tracks)
|
49
124
|
|
50
125
|
def __init__(
|
51
126
|
self,
|
@@ -57,12 +132,14 @@ class Timeline:
|
|
57
132
|
audio_samples_per_frame: int = 1024,
|
58
133
|
video_codec: str = 'h264',
|
59
134
|
video_pixel_format: str = 'yuv420p',
|
60
|
-
audio_codec: str = 'aac'
|
135
|
+
audio_codec: str = 'aac',
|
136
|
+
# TODO: What about this below (?)
|
137
|
+
# audio_layout = 'stereo',
|
138
|
+
# audio_format = 'fltp'
|
61
139
|
):
|
62
|
-
# TODO:
|
63
|
-
#
|
64
|
-
|
65
|
-
self.tracks: list[Track] = []
|
140
|
+
# TODO: By now I'm having only video
|
141
|
+
# tracks
|
142
|
+
self._tracks: list[VideoTrack] = []
|
66
143
|
"""
|
67
144
|
All the video tracks we are handling.
|
68
145
|
"""
|
@@ -97,59 +174,112 @@ class Timeline:
|
|
97
174
|
The audio codec for the audio exported.
|
98
175
|
"""
|
99
176
|
|
100
|
-
# We will have 2 tracks by now
|
101
|
-
self.
|
177
|
+
# We will have 2 video tracks by now
|
178
|
+
self.add_video_track().add_video_track()
|
102
179
|
|
103
|
-
def
|
180
|
+
def _add_track(
|
104
181
|
self,
|
105
|
-
index: Union[int, None] = None
|
182
|
+
index: Union[int, None] = None,
|
183
|
+
is_audio: bool = False
|
106
184
|
) -> 'Timeline':
|
107
185
|
"""
|
108
|
-
Add a new track to the timeline
|
109
|
-
be placed in the last position (
|
110
|
-
priority).
|
186
|
+
Add a new track to the timeline that will
|
187
|
+
be placed in the last position (highest
|
188
|
+
index, lowest priority).
|
111
189
|
|
112
|
-
It will be a video track unless you
|
113
|
-
'
|
190
|
+
It will be a video track unless you send
|
191
|
+
the 'is_audio' parameter as True.
|
114
192
|
"""
|
193
|
+
number_of_tracks = (
|
194
|
+
self.number_of_audio_tracks
|
195
|
+
if is_audio else
|
196
|
+
self.number_of_video_tracks
|
197
|
+
)
|
198
|
+
|
199
|
+
tracks = (
|
200
|
+
self.audio_tracks
|
201
|
+
if is_audio else
|
202
|
+
self.video_tracks
|
203
|
+
)
|
204
|
+
|
115
205
|
index = (
|
116
206
|
index
|
117
207
|
if (
|
118
208
|
index is not None and
|
119
|
-
index <=
|
209
|
+
index <= number_of_tracks
|
120
210
|
) else
|
121
|
-
|
211
|
+
number_of_tracks
|
122
212
|
)
|
123
213
|
|
124
214
|
# We need to change the index of the
|
125
215
|
# affected tracks (the ones that are
|
126
216
|
# in that index and after it)
|
127
|
-
if index <
|
128
|
-
for track in
|
217
|
+
if index < number_of_tracks:
|
218
|
+
for track in tracks:
|
129
219
|
if track.index >= index:
|
130
220
|
track.index += 1
|
131
221
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
222
|
+
track = (
|
223
|
+
AudioTrack(
|
224
|
+
index = index,
|
225
|
+
fps = self.fps,
|
226
|
+
audio_fps = self.audio_fps,
|
227
|
+
audio_samples_per_frame = self.audio_samples_per_frame,
|
228
|
+
# TODO: Where do we obtain this from (?)
|
229
|
+
audio_layout = 'stereo',
|
230
|
+
audio_format = 'fltp'
|
231
|
+
)
|
232
|
+
if is_audio else
|
233
|
+
VideoTrack(
|
234
|
+
index = index,
|
235
|
+
size = self.size,
|
236
|
+
fps = self.fps,
|
237
|
+
audio_fps = self.audio_fps,
|
238
|
+
audio_samples_per_frame = self.audio_samples_per_frame,
|
239
|
+
# TODO: Where do we obtain this from (?)
|
240
|
+
audio_layout = 'stereo',
|
241
|
+
audio_format = 'fltp'
|
242
|
+
)
|
243
|
+
)
|
244
|
+
|
245
|
+
self._tracks.append(track)
|
144
246
|
|
145
247
|
return self
|
248
|
+
|
249
|
+
def add_video_track(
|
250
|
+
self,
|
251
|
+
index: Union[int, None] = None
|
252
|
+
) -> 'Timeline':
|
253
|
+
"""
|
254
|
+
Add a new video track to the timeline, that
|
255
|
+
will be placed in the last position (highest
|
256
|
+
index, lowest priority).
|
257
|
+
"""
|
258
|
+
return self._add_track(
|
259
|
+
index = index,
|
260
|
+
is_audio = False
|
261
|
+
)
|
262
|
+
|
263
|
+
def add_audio_track(
|
264
|
+
self,
|
265
|
+
index: Union[int, None] = None
|
266
|
+
) -> 'Timeline':
|
267
|
+
"""
|
268
|
+
Add a new audio track to the timeline, that
|
269
|
+
will be placed in the last position (highest
|
270
|
+
index, lowest priority).
|
271
|
+
"""
|
272
|
+
return self._add_track(
|
273
|
+
index = index,
|
274
|
+
is_audio = True
|
275
|
+
)
|
146
276
|
|
147
277
|
# TODO: Create a 'remove_track'
|
148
278
|
|
149
279
|
def add_video(
|
150
280
|
self,
|
151
281
|
video: Video,
|
152
|
-
t: Union[int, float, Fraction],
|
282
|
+
t: Union[int, float, Fraction, None] = None,
|
153
283
|
track_index: int = 0
|
154
284
|
) -> 'Timeline':
|
155
285
|
"""
|
@@ -159,17 +289,23 @@ class Timeline:
|
|
159
289
|
TODO: The 'do_use_second_track' parameter
|
160
290
|
is temporary.
|
161
291
|
"""
|
162
|
-
ParameterValidator.validate_mandatory_number_between('track_index', track_index, 0,
|
292
|
+
ParameterValidator.validate_mandatory_number_between('track_index', track_index, 0, self.number_of_tracks)
|
293
|
+
|
294
|
+
if track_index >= self.number_of_video_tracks:
|
295
|
+
raise Exception(f'The "track_index" {str(track_index)} provided does not exist in this timeline.')
|
163
296
|
|
164
|
-
|
297
|
+
# TODO: This should be, maybe, looking for
|
298
|
+
# tracks by using the index property, not
|
299
|
+
# as array index, but by now it is like
|
300
|
+
# this as it is not very robust yet
|
301
|
+
self.video_tracks[track_index].add_media(video, t)
|
165
302
|
|
166
303
|
return self
|
167
304
|
|
168
|
-
# TODO: Create a 'remove_video'
|
305
|
+
# TODO: Create a 'remove_video'
|
306
|
+
# TODO: Create a 'add_audio'
|
307
|
+
# TODO: Create a 'remove_audio'
|
169
308
|
|
170
|
-
# TODO: This method is not for the Track but
|
171
|
-
# for the timeline, as one track can only
|
172
|
-
# have consecutive elements
|
173
309
|
def get_frame_at(
|
174
310
|
self,
|
175
311
|
t: Union[int, float, Fraction]
|
@@ -180,26 +316,17 @@ class Timeline:
|
|
180
316
|
"""
|
181
317
|
frames = list(
|
182
318
|
track.get_frame_at(t)
|
183
|
-
for track in self.
|
319
|
+
for track in self.video_tracks
|
184
320
|
)
|
185
|
-
# TODO: Here I receive black frames because
|
186
|
-
# it was empty, but I don't have a way to
|
187
|
-
# detect those black empty frames because
|
188
|
-
# they are just VideoFrame instances... I
|
189
|
-
# need a way to know so I can skip them if
|
190
|
-
# other frame in other track, or to know if
|
191
|
-
# I want them as transparent or something
|
192
|
-
|
193
321
|
# TODO: Combinate frames, we force them to
|
194
322
|
# rgb24 to obtain them with the same shape,
|
195
323
|
# but maybe we have to change this because
|
196
324
|
# we also need to handle alphas
|
197
325
|
|
198
|
-
# TODO: We need to ignore the ones that are
|
199
|
-
# tagged with
|
200
|
-
# .metadata['is_from_empty_part'] = 'True'
|
201
|
-
|
202
326
|
"""
|
327
|
+
We need to ignore the frames that are tagged
|
328
|
+
as coming from an empty part, so we can have:
|
329
|
+
|
203
330
|
1. Only empty frames
|
204
331
|
-> Black background, keep one
|
205
332
|
2. Empty frames but other frames:
|
@@ -251,12 +378,16 @@ class Timeline:
|
|
251
378
|
"""
|
252
379
|
# TODO: What if the different audio streams
|
253
380
|
# have also different fps (?)
|
381
|
+
# We use both tracks because videos and
|
382
|
+
# audio tracks have both audios
|
254
383
|
for track in self.tracks:
|
255
384
|
# TODO: Make this work properly
|
256
385
|
audio_frames.append(list(track.get_audio_frames_at(t)))
|
257
|
-
|
258
|
-
|
259
|
-
|
386
|
+
|
387
|
+
# TODO: I am receiving empty array here []
|
388
|
+
# that doesn't include any frame in a specific
|
389
|
+
# track that contains a video, why (?)
|
390
|
+
print(audio_frames)
|
260
391
|
|
261
392
|
# We need only 1 single audio frame per column
|
262
393
|
collapsed_frames = [
|
@@ -268,6 +399,7 @@ class Timeline:
|
|
268
399
|
# things? They should be ok because they are
|
269
400
|
# based on our output but I'm not completely
|
270
401
|
# sure here..
|
402
|
+
print(collapsed_frames)
|
271
403
|
|
272
404
|
# We keep only the non-silent frames because
|
273
405
|
# we will sum them after and keeping them
|
@@ -296,9 +428,9 @@ class Timeline:
|
|
296
428
|
|
297
429
|
def render(
|
298
430
|
self,
|
299
|
-
|
431
|
+
output_filename: str = 'test_files/output_render.mp4',
|
300
432
|
start: Union[int, float, Fraction] = 0.0,
|
301
|
-
end: Union[int, float, Fraction, None] = None
|
433
|
+
end: Union[int, float, Fraction, None] = None,
|
302
434
|
) -> 'Timeline':
|
303
435
|
"""
|
304
436
|
Render the time range in between the given
|
@@ -308,122 +440,74 @@ class Timeline:
|
|
308
440
|
If no 'start' and 'end' provided, the whole
|
309
441
|
project will be rendered.
|
310
442
|
"""
|
311
|
-
ParameterValidator.validate_mandatory_string('
|
443
|
+
ParameterValidator.validate_mandatory_string('output_filename', output_filename, do_accept_empty = False)
|
312
444
|
ParameterValidator.validate_mandatory_positive_number('start', start, do_include_zero = True)
|
313
445
|
ParameterValidator.validate_positive_number('end', end, do_include_zero = False)
|
314
446
|
|
315
|
-
# TODO: Limitate 'end' a bit...
|
316
447
|
end = (
|
317
448
|
self.end
|
318
449
|
if end is None else
|
319
450
|
end
|
320
451
|
)
|
321
452
|
|
453
|
+
# Limit 'end' a bit...
|
454
|
+
if end >= 300:
|
455
|
+
raise Exception('More than 5 minutes not supported yet.')
|
456
|
+
|
322
457
|
if start >= end:
|
323
458
|
raise Exception('The provided "start" cannot be greater or equal to the "end" provided.')
|
324
459
|
|
325
|
-
|
460
|
+
writer = VideoWriter(output_filename)
|
326
461
|
|
327
|
-
writer = VideoWriter('test_files/output_render.mp4')
|
328
462
|
# TODO: This has to be dynamic according to the
|
329
|
-
# video we are writing
|
463
|
+
# video we are writing (?)
|
330
464
|
writer.set_video_stream(
|
331
|
-
codec_name =
|
465
|
+
codec_name = self.video_codec,
|
332
466
|
fps = self.fps,
|
333
467
|
size = self.size,
|
334
|
-
pixel_format =
|
468
|
+
pixel_format = self.video_pixel_format
|
335
469
|
)
|
336
470
|
|
337
471
|
writer.set_audio_stream(
|
338
|
-
codec_name =
|
472
|
+
codec_name = self.audio_codec,
|
339
473
|
fps = self.audio_fps
|
340
474
|
)
|
341
475
|
|
342
476
|
time_base = fps_to_time_base(self.fps)
|
343
477
|
audio_time_base = fps_to_time_base(self.audio_fps)
|
344
478
|
|
345
|
-
"""
|
346
|
-
We are trying to render this:
|
347
|
-
-----------------------------
|
348
|
-
[0 a 0.5) => Frames negros
|
349
|
-
[0.5 a 1.25) => [0.25 a 1.0) de Video1
|
350
|
-
[1.25 a 1.75) => Frames negros
|
351
|
-
[1.75 a 2.25) => [0.25 a 0.75) de Video1
|
352
|
-
[2.25 a 3.0) => Frames negros
|
353
|
-
[3.0 a 3.75) => [2.25 a 3.0) de Video2
|
354
|
-
"""
|
355
|
-
|
356
479
|
audio_pts = 0
|
357
480
|
for t in get_ts(start, end, self.fps):
|
358
481
|
frame = self.get_frame_at(t)
|
359
482
|
|
360
483
|
print(f'Getting t:{str(float(t))}')
|
361
|
-
#print(frame)
|
362
484
|
|
363
485
|
# We need to adjust our output elements to be
|
364
486
|
# consecutive and with the right values
|
365
487
|
# TODO: We are using int() for fps but its float...
|
366
488
|
frame.time_base = time_base
|
367
|
-
#frame.pts = int(video_frame_index / frame.time_base)
|
368
489
|
frame.pts = T(t, time_base).truncated_pts
|
369
490
|
|
370
|
-
# TODO: We need to handle the audio
|
371
491
|
writer.mux_video_frame(
|
372
492
|
frame = frame
|
373
493
|
)
|
374
494
|
|
375
|
-
#print(f' [VIDEO] Here in t:{str(t)} -> pts:{str(frame.pts)} - dts:{str(frame.dts)}')
|
376
|
-
|
377
|
-
# TODO: Uncomment all this below for the audio
|
378
|
-
num_of_audio_frames = 0
|
379
495
|
for audio_frame in self.get_audio_frames_at(t):
|
380
|
-
# TODO: The track gives us empty (black)
|
381
|
-
# frames by default but maybe we need a
|
382
|
-
# @dataclass in the middle to handle if
|
383
|
-
# we want transparent frames or not and/or
|
384
|
-
# to detect them here because, if not,
|
385
|
-
# they are just simple VideoFrames and we
|
386
|
-
# don't know they are 'empty' frames
|
387
|
-
|
388
496
|
# We need to adjust our output elements to be
|
389
497
|
# consecutive and with the right values
|
390
498
|
# TODO: We are using int() for fps but its float...
|
391
499
|
audio_frame.time_base = audio_time_base
|
392
|
-
#audio_frame.pts = int(audio_frame_index / audio_frame.time_base)
|
393
500
|
audio_frame.pts = audio_pts
|
501
|
+
|
394
502
|
# We increment for the next iteration
|
395
503
|
audio_pts += audio_frame.samples
|
396
|
-
#audio_frame.pts = int(t + (audio_frame_index * audio_frame.time_base) / audio_frame.time_base)
|
397
|
-
|
398
|
-
#print(f'[AUDIO] Here in t:{str(t)} -> pts:{str(audio_frame.pts)} - dts:{str(audio_frame.dts)}')
|
399
504
|
|
400
|
-
#num_of_audio_frames += 1
|
401
|
-
#print(audio_frame)
|
402
505
|
writer.mux_audio_frame(audio_frame)
|
403
|
-
#print(f'Num of audio frames: {str(num_of_audio_frames)}')
|
404
506
|
|
405
507
|
writer.mux_video_frame(None)
|
406
508
|
writer.mux_audio_frame(None)
|
407
509
|
writer.output.close()
|
408
510
|
|
409
|
-
def _is_empty_part_frame(
|
410
|
-
frame: Union['VideoFrameWrapped', 'AudioFrameWrapped']
|
411
|
-
) -> bool:
|
412
|
-
"""
|
413
|
-
Flag to indicate if the frame comes from
|
414
|
-
an empty part or not.
|
415
|
-
|
416
|
-
TODO: The 'metadata' is included in our
|
417
|
-
wrapper class, not in VideoFrame or
|
418
|
-
AudioFrame classes. I should be sending
|
419
|
-
the wrapper in all the code, but by now
|
420
|
-
I'm doing it just in specific cases.
|
421
|
-
"""
|
422
|
-
return (
|
423
|
-
hasattr(frame, 'metadata') and
|
424
|
-
frame.is_from_empty_part
|
425
|
-
)
|
426
|
-
|
427
511
|
# TODO: Refactor and move please
|
428
512
|
# TODO: This has to work for AudioFrame
|
429
513
|
# also, but I need it working for Wrapped
|