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.
@@ -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 Track
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 VideoFrameWrapped, AudioFrameWrapped
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: We need to be careful with the
63
- # priority, by now its defined by its
64
- # position in the array
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.add_track().add_track()
177
+ # We will have 2 video tracks by now
178
+ self.add_video_track().add_video_track()
102
179
 
103
- def add_track(
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, that will
109
- be placed in the last position (last
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 provide
113
- 'is_audio_track' parameter as True.
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 <= len(self.tracks)
209
+ index <= number_of_tracks
120
210
  ) else
121
- len(self.tracks)
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 < len(self.tracks):
128
- for track in self.tracks:
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
- self.tracks.append(Track(
133
- size = self.size,
134
- index = index,
135
- fps = self.fps,
136
- audio_fps = self.audio_fps,
137
- # TODO: I need more info about the audio
138
- # I think
139
- audio_samples_per_frame = self.audio_samples_per_frame,
140
- # TODO: Where do we obtain this from (?)
141
- audio_layout = 'stereo',
142
- audio_format = 'fltp'
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, len(self.tracks))
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
- self.tracks[track_index].add_video(video, t)
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.tracks
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
- # TODO: We need to ignore the frames that
258
- # are just empty black frames and use them
259
- # not in the combination process
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
- filename: str,
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('filename', filename, do_accept_empty = False)
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
- from yta_video_opengl.writer import VideoWriter
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 = 'h264',
465
+ codec_name = self.video_codec,
332
466
  fps = self.fps,
333
467
  size = self.size,
334
- pixel_format = 'yuv420p'
468
+ pixel_format = self.video_pixel_format
335
469
  )
336
470
 
337
471
  writer.set_audio_stream(
338
- codec_name = 'aac',
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