yta-video-opengl 0.0.22__py3-none-any.whl → 0.0.24__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.
Files changed (33) hide show
  1. yta_video_opengl/editor.py +333 -0
  2. yta_video_opengl/nodes/__init__.py +32 -28
  3. yta_video_opengl/nodes/audio/__init__.py +164 -55
  4. yta_video_opengl/nodes/video/__init__.py +27 -1
  5. yta_video_opengl/nodes/video/{opengl.py → opengl/__init__.py} +8 -4
  6. yta_video_opengl/nodes/video/opengl/experimental.py +760 -0
  7. yta_video_opengl/tests.py +236 -358
  8. yta_video_opengl/utils.py +9 -421
  9. {yta_video_opengl-0.0.22.dist-info → yta_video_opengl-0.0.24.dist-info}/METADATA +2 -6
  10. yta_video_opengl-0.0.24.dist-info/RECORD +13 -0
  11. yta_video_opengl/audio.py +0 -219
  12. yta_video_opengl/classes.py +0 -1276
  13. yta_video_opengl/complete/__init__.py +0 -0
  14. yta_video_opengl/complete/frame_combinator.py +0 -204
  15. yta_video_opengl/complete/frame_generator.py +0 -319
  16. yta_video_opengl/complete/frame_wrapper.py +0 -135
  17. yta_video_opengl/complete/timeline.py +0 -571
  18. yta_video_opengl/complete/track/__init__.py +0 -500
  19. yta_video_opengl/complete/track/media/__init__.py +0 -222
  20. yta_video_opengl/complete/track/parts.py +0 -267
  21. yta_video_opengl/complete/track/utils.py +0 -78
  22. yta_video_opengl/media.py +0 -347
  23. yta_video_opengl/reader/__init__.py +0 -710
  24. yta_video_opengl/reader/cache/__init__.py +0 -253
  25. yta_video_opengl/reader/cache/audio.py +0 -195
  26. yta_video_opengl/reader/cache/utils.py +0 -48
  27. yta_video_opengl/reader/cache/video.py +0 -113
  28. yta_video_opengl/t.py +0 -233
  29. yta_video_opengl/video.py +0 -277
  30. yta_video_opengl/writer.py +0 -278
  31. yta_video_opengl-0.0.22.dist-info/RECORD +0 -31
  32. {yta_video_opengl-0.0.22.dist-info → yta_video_opengl-0.0.24.dist-info}/LICENSE +0 -0
  33. {yta_video_opengl-0.0.22.dist-info → yta_video_opengl-0.0.24.dist-info}/WHEEL +0 -0
@@ -1,571 +0,0 @@
1
- """
2
- When we are reading from a source, the reader
3
- has its own time base and properties. When we
4
- are writing, the writer has different time
5
- base and properties. We need to adjust our
6
- writer to be able to write, because the videos
7
- we read can be different, and the video we are
8
- writing is defined by us. The 'time_base' is
9
- an important property or will make ffmpeg
10
- become crazy and deny packets (that means no
11
- video written).
12
- """
13
- from yta_video_opengl.complete.track import VideoTrack, AudioTrack
14
- from yta_video_opengl.video import Video
15
- from yta_video_opengl.t import get_ts, fps_to_time_base, T
16
- from yta_video_opengl.complete.frame_wrapper import AudioFrameWrapped
17
- from yta_video_opengl.complete.frame_combinator import AudioFrameCombinator
18
- from yta_video_opengl.writer import VideoWriter
19
- from yta_validation.parameter import ParameterValidator
20
- from yta_validation import PythonValidator
21
- from av.video.frame import VideoFrame
22
- from av.audio.frame import AudioFrame
23
- from quicktions import Fraction
24
- from functools import reduce
25
- from typing import Union
26
-
27
- import numpy as np
28
-
29
-
30
- class Timeline:
31
- """
32
- Class to represent all the tracks that
33
- exist on the project and to handle the
34
- combination of all their frames.
35
- """
36
-
37
- @property
38
- def end(
39
- self
40
- ) -> Fraction:
41
- """
42
- The end of the last video of the track
43
- that lasts longer. This is the last time
44
- moment that has to be rendered.
45
- """
46
- return max(
47
- track.end
48
- for track in self.tracks
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)
124
-
125
- def __init__(
126
- self,
127
- size: tuple[int, int] = (1_920, 1_080),
128
- fps: Union[int, float, Fraction] = 60.0,
129
- audio_fps: Union[int, Fraction] = 44_100.0, # 48_000.0 for aac
130
- # TODO: I don't like this name
131
- # TODO: Where does this come from (?)
132
- audio_samples_per_frame: int = 1024,
133
- video_codec: str = 'h264',
134
- video_pixel_format: str = 'yuv420p',
135
- audio_codec: str = 'aac',
136
- # TODO: What about this below (?)
137
- # audio_layout = 'stereo',
138
- # audio_format = 'fltp'
139
- ):
140
- # TODO: By now I'm having only video
141
- # tracks
142
- self._tracks: list[VideoTrack] = []
143
- """
144
- All the video tracks we are handling.
145
- """
146
-
147
- self.size: tuple[int, int] = size
148
- """
149
- The size that the final video must have.
150
- """
151
- self.fps: Union[int, float, Fraction] = fps
152
- """
153
- The fps of the output video.
154
- """
155
- self.audio_fps: Union[int, Fraction] = audio_fps
156
- """
157
- The fps of the output audio.
158
- """
159
- self.audio_samples_per_frame: int = audio_samples_per_frame
160
- """
161
- The audio samples each audio frame must
162
- have.
163
- """
164
- self.video_codec: str = video_codec
165
- """
166
- The video codec for the video exported.
167
- """
168
- self.video_pixel_format: str = video_pixel_format
169
- """
170
- The pixel format for the video exported.
171
- """
172
- self.audio_codec: str = audio_codec
173
- """
174
- The audio codec for the audio exported.
175
- """
176
-
177
- # We will have 2 video tracks by now
178
- self.add_video_track().add_video_track()
179
-
180
- def _add_track(
181
- self,
182
- index: Union[int, None] = None,
183
- is_audio: bool = False
184
- ) -> 'Timeline':
185
- """
186
- Add a new track to the timeline that will
187
- be placed in the last position (highest
188
- index, lowest priority).
189
-
190
- It will be a video track unless you send
191
- the 'is_audio' parameter as True.
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
-
205
- index = (
206
- index
207
- if (
208
- index is not None and
209
- index <= number_of_tracks
210
- ) else
211
- number_of_tracks
212
- )
213
-
214
- # We need to change the index of the
215
- # affected tracks (the ones that are
216
- # in that index and after it)
217
- if index < number_of_tracks:
218
- for track in tracks:
219
- if track.index >= index:
220
- track.index += 1
221
-
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)
246
-
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
- )
276
-
277
- # TODO: Create a 'remove_track'
278
-
279
- def add_video(
280
- self,
281
- video: Video,
282
- t: Union[int, float, Fraction, None] = None,
283
- track_index: int = 0
284
- ) -> 'Timeline':
285
- """
286
- Add the provided 'video' to the timeline,
287
- starting at the provided 't' time moment.
288
-
289
- TODO: The 'do_use_second_track' parameter
290
- is temporary.
291
- """
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.')
296
-
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)
302
-
303
- return self
304
-
305
- # TODO: Create a 'remove_video'
306
- # TODO: Create a 'add_audio'
307
- # TODO: Create a 'remove_audio'
308
-
309
- def get_frame_at(
310
- self,
311
- t: Union[int, float, Fraction]
312
- ) -> 'VideoFrame':
313
- """
314
- Get all the frames that are played at the
315
- 't' time provided, but combined in one.
316
- """
317
- frames = list(
318
- track.get_frame_at(t)
319
- for track in self.video_tracks
320
- )
321
- # TODO: Combinate frames, we force them to
322
- # rgb24 to obtain them with the same shape,
323
- # but maybe we have to change this because
324
- # we also need to handle alphas
325
-
326
- """
327
- We need to ignore the frames that are tagged
328
- as coming from an empty part, so we can have:
329
-
330
- 1. Only empty frames
331
- -> Black background, keep one
332
- 2. Empty frames but other frames:
333
- -> Skip all empty frames and apply
334
- track orders
335
- """
336
-
337
- output_frame = frames[0]._frame.to_ndarray(format = 'rgb24')
338
-
339
- for frame in frames:
340
- # We just need the first non-empty frame,
341
- # that must be from the track with the
342
- # bigger priority
343
- # TODO: I assume, by now, that the frames
344
- # come in order (bigger priority first)
345
- if not frame.is_from_empty_part:
346
- # TODO: By now I'm just returning the first
347
- # one but we will need to check the alpha
348
- # layer to combine if possible
349
- output_frame = frame._frame.to_ndarray(format = 'rgb24')
350
- break
351
-
352
- # # TODO: This code below is to combine the
353
- # # frames but merging all of them, that is
354
- # # unexpected in a video editor but we have
355
- # # the way to do it
356
- # from yta_video_opengl.complete.frame_combinator import VideoFrameCombinator
357
- # # TODO: What about the 'format' (?)
358
- # output_frame = VideoFrameCombinator.blend_add(output_frame, frame.to_ndarray(format = 'rgb24'))
359
-
360
- # TODO: How to build this VideoFrame correctly
361
- # and what about the 'format' (?)
362
- # We don't handle pts here, just the image
363
- return VideoFrame.from_ndarray(output_frame, format = 'rgb24')
364
-
365
- def get_audio_frames_at(
366
- self,
367
- t: float
368
- ):
369
- audio_frames: list[AudioFrameWrapped] = []
370
- """
371
- Matrix in which the rows are the different
372
- tracks we have, and the column includes all
373
- the audio frames for this 't' time moment
374
- for the track of that row. We can have more
375
- than one frame per column per row (track)
376
- but we need a single frame to combine all
377
- the tracks.
378
- """
379
- # TODO: What if the different audio streams
380
- # have also different fps (?)
381
- # We use both tracks because videos and
382
- # audio tracks have both audios
383
- for track in self.tracks:
384
- # TODO: Make this work properly
385
- audio_frames.append(list(track.get_audio_frames_at(t)))
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)
391
-
392
- # We need only 1 single audio frame per column
393
- collapsed_frames = [
394
- concatenate_audio_frames(frames)
395
- for frames in audio_frames
396
- ]
397
-
398
- # TODO: What about the lenghts and those
399
- # things? They should be ok because they are
400
- # based on our output but I'm not completely
401
- # sure here..
402
- print(collapsed_frames)
403
-
404
- # We keep only the non-silent frames because
405
- # we will sum them after and keeping them
406
- # will change the results.
407
- non_empty_collapsed_frames = [
408
- frame._frame
409
- for frame in collapsed_frames
410
- if not frame.is_from_empty_part
411
- ]
412
-
413
- if len(non_empty_collapsed_frames) == 0:
414
- # If they were all silent, just keep one
415
- non_empty_collapsed_frames = [collapsed_frames[0]._frame]
416
-
417
- # Now, mix column by column (track by track)
418
- # TODO: I do this to have an iterator, but
419
- # maybe we need more than one single audio
420
- # frame because of the size at the original
421
- # video or something...
422
- frames = [
423
- AudioFrameCombinator.sum_tracks_frames(non_empty_collapsed_frames, self.audio_fps)
424
- ]
425
-
426
- for audio_frame in frames:
427
- yield audio_frame
428
-
429
- def render(
430
- self,
431
- output_filename: str = 'test_files/output_render.mp4',
432
- start: Union[int, float, Fraction] = 0.0,
433
- end: Union[int, float, Fraction, None] = None,
434
- ) -> 'Timeline':
435
- """
436
- Render the time range in between the given
437
- 'start' and 'end' and store the result with
438
- the also provided 'fillename'.
439
-
440
- If no 'start' and 'end' provided, the whole
441
- project will be rendered.
442
- """
443
- ParameterValidator.validate_mandatory_string('output_filename', output_filename, do_accept_empty = False)
444
- ParameterValidator.validate_mandatory_positive_number('start', start, do_include_zero = True)
445
- ParameterValidator.validate_positive_number('end', end, do_include_zero = False)
446
-
447
- end = (
448
- self.end
449
- if end is None else
450
- end
451
- )
452
-
453
- # Limit 'end' a bit...
454
- if end >= 300:
455
- raise Exception('More than 5 minutes not supported yet.')
456
-
457
- if start >= end:
458
- raise Exception('The provided "start" cannot be greater or equal to the "end" provided.')
459
-
460
- writer = VideoWriter(output_filename)
461
-
462
- # TODO: This has to be dynamic according to the
463
- # video we are writing (?)
464
- writer.set_video_stream(
465
- codec_name = self.video_codec,
466
- fps = self.fps,
467
- size = self.size,
468
- pixel_format = self.video_pixel_format
469
- )
470
-
471
- writer.set_audio_stream(
472
- codec_name = self.audio_codec,
473
- fps = self.audio_fps
474
- )
475
-
476
- time_base = fps_to_time_base(self.fps)
477
- audio_time_base = fps_to_time_base(self.audio_fps)
478
-
479
- audio_pts = 0
480
- for t in get_ts(start, end, self.fps):
481
- frame = self.get_frame_at(t)
482
-
483
- print(f'Getting t:{str(float(t))}')
484
-
485
- # We need to adjust our output elements to be
486
- # consecutive and with the right values
487
- # TODO: We are using int() for fps but its float...
488
- frame.time_base = time_base
489
- frame.pts = T(t, time_base).truncated_pts
490
-
491
- writer.mux_video_frame(
492
- frame = frame
493
- )
494
-
495
- for audio_frame in self.get_audio_frames_at(t):
496
- # We need to adjust our output elements to be
497
- # consecutive and with the right values
498
- # TODO: We are using int() for fps but its float...
499
- audio_frame.time_base = audio_time_base
500
- audio_frame.pts = audio_pts
501
-
502
- # We increment for the next iteration
503
- audio_pts += audio_frame.samples
504
-
505
- writer.mux_audio_frame(audio_frame)
506
-
507
- writer.mux_video_frame(None)
508
- writer.mux_audio_frame(None)
509
- writer.output.close()
510
-
511
- # TODO: Refactor and move please
512
- # TODO: This has to work for AudioFrame
513
- # also, but I need it working for Wrapped
514
- def concatenate_audio_frames(
515
- frames: list[AudioFrameWrapped]
516
- ) -> AudioFrameWrapped:
517
- """
518
- Concatenate all the given 'frames' in one
519
- single audio frame and return it.
520
-
521
- The audio frames must have the same layout
522
- and sample rate.
523
- """
524
- if not frames:
525
- # TODO: This should not happen
526
- return None
527
-
528
- if len(frames) == 1:
529
- return frames[0]
530
-
531
- # We need to preserve the metadata
532
- is_from_empty_part = all(
533
- frame.is_from_empty_part
534
- for frame in frames
535
- )
536
- metadata = reduce(lambda key_values, frame: {**key_values, **frame.metadata}, frames, {})
537
-
538
- sample_rate = frames[0]._frame.sample_rate
539
- layout = frames[0]._frame.layout.name
540
-
541
- arrays = []
542
- # TODO: What about 'metadata' (?)
543
- for frame in frames:
544
- if (
545
- frame._frame.sample_rate != sample_rate or
546
- frame._frame.layout.name != layout
547
- ):
548
- raise ValueError("Los frames deben tener mismo sample_rate y layout")
549
-
550
- # arr = frame.to_ndarray() # (channels, samples)
551
- # if arr.dtype == np.int16:
552
- # arr = arr.astype(np.float32) / 32768.0
553
- # elif arr.dtype != np.float32:
554
- # arr = arr.astype(np.float32)
555
-
556
- arrays.append(frame._frame.to_ndarray())
557
-
558
- combined = np.concatenate(arrays, axis = 1)
559
-
560
- out = AudioFrame.from_ndarray(
561
- array = combined,
562
- format = frames[0].format,
563
- layout = layout
564
- )
565
- out.sample_rate = sample_rate
566
-
567
- return AudioFrameWrapped(
568
- frame = out,
569
- metadata = metadata,
570
- is_from_empty_part = is_from_empty_part
571
- )