yta-video-opengl 0.0.19__py3-none-any.whl → 0.0.20__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.
@@ -0,0 +1,493 @@
1
+
2
+ from yta_video_opengl.complete.track.media import AudioOnTrack, VideoOnTrack
3
+ from yta_video_opengl.complete.track.parts import _AudioPart, _VideoPart, NON_LIMITED_EMPTY_PART_END
4
+ from yta_video_opengl.complete.track.utils import generate_silent_frames
5
+ from yta_video_opengl.audio import Audio
6
+ from yta_video_opengl.t import T
7
+ from yta_validation.parameter import ParameterValidator
8
+ from quicktions import Fraction
9
+ from typing import Union
10
+ from abc import ABC, abstractmethod
11
+
12
+
13
+ class _Track(ABC):
14
+ """
15
+ Abstract class to be inherited by the
16
+ different track classes we implement.
17
+ """
18
+
19
+ @property
20
+ def parts(
21
+ self
22
+ ) -> list[Union['_AudioPart', '_VideoPart']]:
23
+ """
24
+ The list of parts that build this track,
25
+ but with the empty parts detected to
26
+ be fulfilled with black frames and silent
27
+ audios.
28
+
29
+ A part can be a video or an empty space.
30
+ """
31
+ if (
32
+ not hasattr(self, '_parts') or
33
+ self._parts is None
34
+ ):
35
+ self._recalculate_parts()
36
+
37
+ return self._parts
38
+
39
+ @property
40
+ def end(
41
+ self
42
+ ) -> Fraction:
43
+ """
44
+ The end of the last audio of this track,
45
+ which is also the end of the track. This
46
+ is the last time moment that has to be
47
+ rendered.
48
+ """
49
+ return Fraction(
50
+ 0.0
51
+ if len(self.medias) == 0 else
52
+ max(
53
+ media.end
54
+ for media in self.medias
55
+ )
56
+ )
57
+
58
+ @property
59
+ def medias(
60
+ self
61
+ ) -> list[AudioOnTrack]:
62
+ """
63
+ The list of medias we have in the track
64
+ but ordered using the 'start' attribute
65
+ from first to last.
66
+ """
67
+ return sorted(self._medias, key = lambda media: media.start)
68
+
69
+ @property
70
+ def is_muted(
71
+ self
72
+ ) -> bool:
73
+ """
74
+ Flag to indicate if the track is muted or
75
+ not. Being muted means that no audio frames
76
+ will be retured from this track.
77
+ """
78
+ return self._is_muted
79
+
80
+ def __init__(
81
+ self,
82
+ index: int
83
+ ):
84
+ self._medias: list['AudioOnTrack', 'VideoOnTrack'] = []
85
+ """
86
+ The list of 'AudioOnTrack' or 'VideoOnTrack'
87
+ instances that must be played on this track.
88
+ """
89
+ self._is_muted: bool = False
90
+ """
91
+ Internal flag to indicate if the track is
92
+ muted or not.
93
+ """
94
+ self.index: int = index
95
+ """
96
+ The index of the track within the timeline.
97
+ """
98
+
99
+ def _is_free(
100
+ self,
101
+ start: Union[int, float, Fraction],
102
+ end: Union[int, float, Fraction]
103
+ ) -> bool:
104
+ """
105
+ Check if the time range in between the
106
+ 'start' and 'end' time given is free or
107
+ there is some media playing at any moment.
108
+ """
109
+ return not any(
110
+ (
111
+ media.start < end and
112
+ media.end > start
113
+ )
114
+ for media in self.medias
115
+ )
116
+
117
+ def _get_part_at_t(
118
+ self,
119
+ t: Union[int, float, Fraction]
120
+ ) -> Union['_AudioPart', '_VideoPart']:
121
+ """
122
+ Get the part at the given 't' time
123
+ moment, that will always exist because
124
+ we have an special non ended last
125
+ empty part that would be returned if
126
+ accessing to an empty 't'.
127
+ """
128
+ for part in self.parts:
129
+ if part.start <= t < part.end:
130
+ return part
131
+
132
+ # TODO: This will only happen if they are
133
+ # asking for a value greater than the
134
+ # NON_LIMITED_EMPTY_PART_END...
135
+ raise Exception('NON_LIMITED_EMPTY_PART_END exceeded.')
136
+ return None
137
+
138
+ def mute(
139
+ self
140
+ ) -> 'AudioTrack':
141
+ """
142
+ Set the track as muted so no audio frame will
143
+ be played from this track.
144
+ """
145
+ self._is_muted = True
146
+
147
+ def unmute(
148
+ self
149
+ ) -> 'AudioTrack':
150
+ """
151
+ Set the track as unmuted so the audio frames
152
+ will be played as normal.
153
+ """
154
+ self._is_muted = False
155
+
156
+ def add_media(
157
+ self,
158
+ media: Union['Audio', 'Video'],
159
+ t: Union[int, float, Fraction, None] = None
160
+ ) -> '_Track':
161
+ """
162
+ Add the 'media' provided to the track. If
163
+ a 't' time moment is provided, the media
164
+ will be added to that time moment if
165
+ possible. If there is no other media
166
+ placed in the time gap between the given
167
+ 't' and the provided 'media' duration, it
168
+ will be added succesfully. In the other
169
+ case, an exception will be raised.
170
+
171
+ If 't' is None, the first available 't'
172
+ time moment will be used, that will be 0.0
173
+ if no media, or the end of the last media.
174
+ """
175
+ ParameterValidator.validate_mandatory_instance_of('media', media, ['Audio', 'Video'])
176
+ ParameterValidator.validate_positive_number('t', t, do_include_zero = True)
177
+
178
+ if t is not None:
179
+ # TODO: We can have many different strategies
180
+ # that we could define in the '__init__' maybe
181
+ t: T = T.from_fps(t, self.fps)
182
+ #if not self._is_free(t.truncated, t.next(1).truncated):
183
+ if not self._is_free(t.truncated, t.truncated + media.duration):
184
+ raise Exception('The media cannot be added at the "t" time moment, something blocks it.')
185
+ t = t.truncated
186
+ else:
187
+ t = self.end
188
+
189
+ self._medias.append(self._make_media_on_track(
190
+ media = media,
191
+ start = t
192
+ ))
193
+
194
+ self._recalculate_parts()
195
+
196
+ # TODO: Maybe return the AudioOnTrack instead (?)
197
+ return self
198
+
199
+ @abstractmethod
200
+ def _make_part(
201
+ self,
202
+ **kwargs
203
+ ):
204
+ """
205
+ Factory method to return an instance of the
206
+ '_Part' class that the specific '_Track' class
207
+ is using.
208
+ """
209
+ pass
210
+
211
+ @abstractmethod
212
+ def _make_media_on_track(
213
+ self,
214
+ **kwargs
215
+ ):
216
+ """
217
+ Factory method to return an instance of the
218
+ 'MediaOnTrack' class that the specific '_Track'
219
+ class is using.
220
+ """
221
+ pass
222
+
223
+ def _recalculate_parts(
224
+ self
225
+ ) -> '_Track':
226
+ """
227
+ Check the track and get all the parts. A
228
+ part can be empty (no audio or video on
229
+ that time period, which means silent audio
230
+ or black frame), or a video or audio.
231
+ """
232
+ parts = []
233
+ cursor = 0.0
234
+
235
+ for media in self.medias:
236
+ # Empty space between cursor and start of
237
+ # the next clip
238
+ if media.start > cursor:
239
+ parts.append(self._make_part(
240
+ track = self,
241
+ start = cursor,
242
+ end = media.start,
243
+ media = None
244
+ ))
245
+
246
+ # The media itself
247
+ parts.append(self._make_part(
248
+ track = self,
249
+ start = media.start,
250
+ end = media.end,
251
+ media = media
252
+ ))
253
+
254
+ cursor = media.end
255
+
256
+ # Add the non limited last empty part
257
+ parts.append(self._make_part(
258
+ track = self,
259
+ start = cursor,
260
+ end = NON_LIMITED_EMPTY_PART_END,
261
+ media = None
262
+ ))
263
+
264
+ self._parts = parts
265
+
266
+ return self
267
+
268
+ class _TrackWithAudio(_Track):
269
+ """
270
+ Class that has the ability to obtain the
271
+ audio frames from the source and must be
272
+ inherited by those tracks that have audio
273
+ (or video, that includes audio).
274
+ """
275
+
276
+ def __init__(
277
+ self,
278
+ index: int,
279
+ fps: float,
280
+ audio_fps: float,
281
+ # TODO: Where does it come from (?)
282
+ audio_samples_per_frame: int,
283
+ audio_layout: str = 'stereo',
284
+ audio_format: str = 'fltp'
285
+ ):
286
+ _Track.__init__(
287
+ self,
288
+ index = index
289
+ )
290
+
291
+ self.fps: float = float(fps)
292
+ """
293
+ The fps of the the video that is associated
294
+ with the Timeline this track belongs to,
295
+ needed to calculate the base t time moments
296
+ to be precise and to obtain or generate the
297
+ frames.
298
+ """
299
+ self.audio_fps: float = float(audio_fps)
300
+ """
301
+ The fps of the audio track, needed to
302
+ generate silent audios for the empty parts.
303
+ """
304
+ self.audio_samples_per_frame: int = audio_samples_per_frame
305
+ """
306
+ The number of samples per audio frame.
307
+ """
308
+ self.audio_layout: str = audio_layout
309
+ """
310
+ The layout of the audio, that can be 'mono'
311
+ or 'stereo'.
312
+ """
313
+ self.audio_format: str = audio_format
314
+ """
315
+ The format of the audio, that can be 's16',
316
+ 'flt', 'fltp', etc.
317
+ """
318
+
319
+ # TODO: This is not working well when
320
+ # source has different fps
321
+ def get_audio_frames_at(
322
+ self,
323
+ t: Union[int, float, Fraction]
324
+ ):
325
+ """
326
+ Get the sequence of audio frames that
327
+ must be displayed at the 't' time
328
+ moment provided, which the collection
329
+ of audio frames corresponding to the
330
+ audio that is being played at that time
331
+ moment.
332
+
333
+ Remember, this 't' time moment provided
334
+ is about the track, and we make the
335
+ conversion to the actual audio 't' to
336
+ get the frame.
337
+ """
338
+ frames = (
339
+ generate_silent_frames(
340
+ fps = self.fps,
341
+ audio_fps = self.audio_fps,
342
+ audio_samples_per_frame = self.audio_samples_per_frame,
343
+ layout = self.audio_layout,
344
+ format = self.audio_format
345
+ )
346
+ if self.is_muted else
347
+ self._get_part_at_t(t).get_audio_frames_at(t)
348
+ )
349
+
350
+ for frame in frames:
351
+ yield frame
352
+
353
+ class _TrackWithVideo(_Track):
354
+ """
355
+ Class that has the ability to obtain the
356
+ video frames from the source and must be
357
+ inherited by those tracks that have video.
358
+ """
359
+
360
+ def __init__(
361
+ self,
362
+ index: int,
363
+ size: tuple[int, int],
364
+ fps: float
365
+ ):
366
+ _Track.__init__(
367
+ self,
368
+ index = index
369
+ )
370
+
371
+ # TODO: This is not needed actually...
372
+ self.fps: float = float(fps)
373
+ """
374
+ The fps of the the video that is associated
375
+ with the Timeline this track belongs to,
376
+ needed to calculate the base t time moments
377
+ to be precise and to obtain or generate the
378
+ frames.
379
+ """
380
+ self.size: tuple[int, int] = size
381
+ """
382
+ The size that the output video will have.
383
+ """
384
+
385
+ def get_frame_at(
386
+ self,
387
+ t: Union[int, float, Fraction]
388
+ ) -> 'VideoFrameWrapped':
389
+ """
390
+ Get the frame that must be displayed at
391
+ the 't' time moment provided, which is
392
+ a frame from the video audio that is
393
+ being played at that time moment.
394
+
395
+ Remember, this 't' time moment provided
396
+ is about the track, and we make the
397
+ conversion to the actual video 't' to
398
+ get the frame.
399
+ """
400
+ # TODO: What if the frame, that comes from
401
+ # a video, doesn't have the expected size (?)
402
+ return self._get_part_at_t(t).get_frame_at(t)
403
+
404
+ class AudioTrack(_TrackWithAudio):
405
+ """
406
+ Class to represent a track in which we place
407
+ audios to build a video project.
408
+ """
409
+
410
+ def __init__(
411
+ self,
412
+ index: int,
413
+ fps: float,
414
+ audio_fps: float,
415
+ # TODO: Where does it come from (?)
416
+ audio_samples_per_frame: int,
417
+ audio_layout: str = 'stereo',
418
+ audio_format: str = 'fltp'
419
+ ):
420
+ _TrackWithAudio.__init__(
421
+ index = index,
422
+ fps = fps,
423
+ audio_fps = audio_fps,
424
+ audio_samples_per_frame = audio_samples_per_frame,
425
+ audio_layout = audio_layout,
426
+ audio_format = audio_format
427
+ )
428
+
429
+ def _make_part(
430
+ self,
431
+ **kwargs
432
+ ):
433
+ return _AudioPart(**kwargs)
434
+
435
+ def _make_media_on_track(
436
+ self,
437
+ **kwargs
438
+ ):
439
+ return AudioOnTrack(**kwargs)
440
+
441
+ class VideoTrack(_TrackWithVideo, _TrackWithAudio):
442
+ """
443
+ Class to represent a track in which we place
444
+ videos to build a video project.
445
+ """
446
+
447
+ def __init__(
448
+ self,
449
+ index: int,
450
+ size: tuple[int, int],
451
+ fps: float,
452
+ audio_fps: float,
453
+ # TODO: Where does it come from (?)
454
+ audio_samples_per_frame: int,
455
+ audio_layout: str = 'stereo',
456
+ audio_format: str = 'fltp',
457
+ ):
458
+ # super().__init__(
459
+ # index = index,
460
+ # size = size,
461
+ # fps = fps,
462
+ # audio_fps = audio_fps,
463
+ # audio_samples_per_frame = audio_samples_per_frame,
464
+ # audio_layout = audio_layout,
465
+ # audio_format = audio_format
466
+ # )
467
+ _TrackWithVideo.__init__(
468
+ self,
469
+ index = index,
470
+ size = size,
471
+ fps = fps
472
+ )
473
+ _TrackWithAudio.__init__(
474
+ self,
475
+ index = index,
476
+ fps = fps,
477
+ audio_fps = audio_fps,
478
+ audio_samples_per_frame = audio_samples_per_frame,
479
+ audio_layout = audio_layout,
480
+ audio_format = audio_format
481
+ )
482
+
483
+ def _make_part(
484
+ self,
485
+ **kwargs
486
+ ):
487
+ return _VideoPart(**kwargs)
488
+
489
+ def _make_media_on_track(
490
+ self,
491
+ **kwargs
492
+ ):
493
+ return VideoOnTrack(**kwargs)