yta-video-opengl 0.0.11__py3-none-any.whl → 0.0.12__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.
@@ -1,12 +1,21 @@
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
+ """
1
13
  from yta_video_opengl.complete.track import Track
2
14
  from yta_video_opengl.video import Video
3
15
  from yta_validation.parameter import ParameterValidator
4
16
  from typing import Union
5
17
  from fractions import Fraction
6
18
 
7
- import numpy as np
8
- import av
9
-
10
19
 
11
20
  class Timeline:
12
21
  """
@@ -24,25 +33,50 @@ class Timeline:
24
33
  that lasts longer. This is the last time
25
34
  moment that has to be rendered.
26
35
  """
27
- return max(track.end for track in self.tracks)
36
+ return max(
37
+ track.end
38
+ for track in self.tracks
39
+ )
28
40
 
29
41
  def __init__(
30
42
  self,
31
- size: tuple[int, int] = (1920, 1080),
32
- fps: float = 60.0
43
+ size: tuple[int, int] = (1_920, 1_080),
44
+ fps: float = 60.0,
45
+ audio_fps: float = 44_100.0, # 48_000.0 for aac
46
+ # TODO: I don't like this name
47
+ # TODO: Where does this come from (?)
48
+ audio_nb_samples: int = 1024
33
49
  ):
34
50
  # TODO: By now we are using just two video
35
51
  # tracks to test the composition
36
52
  # TODO: We need to be careful with the
37
53
  # priority, by now its defined by its
38
54
  # position in the array
39
- self.tracks: list[Track] = [Track(), Track()]
55
+ self.tracks: list[Track] = [
56
+ Track(
57
+ size = size,
58
+ fps = fps,
59
+ audio_fps = audio_fps,
60
+ # TODO: I need more info about the audio
61
+ # I think
62
+ audio_nb_samples = audio_nb_samples
63
+ ),
64
+ Track(
65
+ size = size,
66
+ fps = fps,
67
+ audio_fps = audio_fps,
68
+ # TODO: I need more info about the audio
69
+ # I think
70
+ audio_nb_samples = audio_nb_samples
71
+ )
72
+ ]
40
73
  """
41
74
  All the video tracks we are handling.
42
75
  """
43
- # TODO: Handle size and fps
76
+ # TODO: Handle the other properties
44
77
  self.size = size
45
78
  self.fps = fps
79
+ self.audio_fps = audio_fps
46
80
 
47
81
  # TODO: Create 'add_track' method, but by now
48
82
  # we hare handling only one
@@ -61,19 +95,23 @@ class Timeline:
61
95
  TODO: The 'do_use_second_track' parameter
62
96
  is temporary.
63
97
  """
98
+ # TODO: This is temporary logic by now
99
+ # just to be able to test mixing frames
100
+ # from 2 different tracks at the same
101
+ # time
64
102
  index = 1 * do_use_second_track
65
103
 
66
104
  self.tracks[index].add_video(video, t)
67
105
 
68
106
  return self
69
-
107
+
70
108
  # TODO: This method is not for the Track but
71
109
  # for the timeline, as one track can only
72
110
  # have consecutive elements
73
111
  def get_frame_at(
74
112
  self,
75
113
  t: float
76
- ) -> Union['VideoFrame', None]:
114
+ ) -> 'VideoFrame':
77
115
  """
78
116
  Get all the frames that are played at the
79
117
  't' time provided, but combined in one.
@@ -82,24 +120,38 @@ class Timeline:
82
120
  track.get_frame_at(t)
83
121
  for track in self.tracks
84
122
  )
123
+ # TODO: Here I receive black frames because
124
+ # it was empty, but I don't have a way to
125
+ # detect those black empty frames because
126
+ # they are just VideoFrame instances... I
127
+ # need a way to know so I can skip them if
128
+ # other frame in other track, or to know if
129
+ # I want them as transparent or something
85
130
 
86
- frames = [
87
- frame
88
- for frame in frames
89
- if frame is not None
90
- ]
91
-
92
- return (
93
- # TODO: Combinate them, I send first by now
94
- frames[0]
95
- if len(frames) > 0 else
96
- # TODO: Should I send None or a full
97
- # black (or transparent) frame? I think
98
- # None is better because I don't know
99
- # the size here (?)
100
- None
101
- )
131
+ # TODO: Combinate them, I send first by now
132
+ return next(frames)
102
133
 
134
+ def get_audio_frames_at(
135
+ self,
136
+ t: float
137
+ ):
138
+ # TODO: What if the different audio streams
139
+ # have also different fps (?)
140
+ frames = []
141
+ for track in self.tracks:
142
+ # TODO: Make this work properly
143
+ audio_frames = track.get_audio_frames_at(t)
144
+
145
+ # TODO: Combine them
146
+ if audio_frames is not None:
147
+ frames = audio_frames
148
+ break
149
+
150
+ #from yta_video_opengl.utils import get_silent_audio_frame
151
+ #make_silent_audio_frame()
152
+ for frame in frames:
153
+ yield frame
154
+
103
155
  def render(
104
156
  self,
105
157
  filename: str,
@@ -127,47 +179,34 @@ class Timeline:
127
179
 
128
180
  if start >= end:
129
181
  raise Exception('The provided "start" cannot be greater or equal to the "end" provided.')
130
- # TODO: Obtain all the 't', based on 'fps'
131
- # that we need to render from 'start' to
132
- # 'end'
133
- # TODO: I don't want to have this here
134
- def generate_times(start: float, end: float, fps: int):
135
- dt = 1.0 / fps
136
- times = []
137
-
138
- t = start
139
- while t <= end:
140
- times.append(t + 0.000001)
141
- t += dt
142
-
143
- return times
144
182
 
145
183
  from yta_video_opengl.writer import VideoWriter
184
+ from yta_video_opengl.utils import get_black_background_video_frame, get_silent_audio_frame
146
185
 
147
186
  writer = VideoWriter('test_files/output_render.mp4')
148
187
  # TODO: This has to be dynamic according to the
149
188
  # video we are writing
150
189
  writer.set_video_stream(
151
190
  codec_name = 'h264',
152
- fps = 60,
153
- size = (1920, 1080),
191
+ fps = self.fps,
192
+ size = self.size,
154
193
  pixel_format = 'yuv420p'
155
194
  )
156
195
 
157
- for t in generate_times(start, end, self.fps):
196
+ writer.set_audio_stream(
197
+ codec_name = 'aac',
198
+ fps = self.audio_fps
199
+ )
200
+
201
+ audio_pts = 0
202
+ for t in get_ts(start, end, self.fps):
158
203
  frame = self.get_frame_at(t)
159
204
 
160
- if frame is None:
161
- # Replace with black background if no frame
162
- frame = av.VideoFrame.from_ndarray(
163
- array = np.zeros((1920, 1080, 3), dtype = np.uint8),
164
- format = 'rgb24'
165
- )
166
-
167
205
  # We need to adjust our output elements to be
168
206
  # consecutive and with the right values
169
207
  # TODO: We are using int() for fps but its float...
170
208
  frame.time_base = Fraction(1, int(self.fps))
209
+ #frame.pts = int(video_frame_index / frame.time_base)
171
210
  frame.pts = int(t / frame.time_base)
172
211
 
173
212
  # TODO: We need to handle the audio
@@ -175,5 +214,58 @@ class Timeline:
175
214
  frame = frame
176
215
  )
177
216
 
217
+ #print(f' [VIDEO] Here in t:{str(t)} -> pts:{str(frame.pts)} - dts:{str(frame.dts)}')
218
+
219
+ num_of_audio_frames = 0
220
+ for audio_frame in self.get_audio_frames_at(t):
221
+ # TODO: The track gives us empty (black)
222
+ # frames by default but maybe we need a
223
+ # @dataclass in the middle to handle if
224
+ # we want transparent frames or not and/or
225
+ # to detect them here because, if not,
226
+ # they are just simple VideoFrames and we
227
+ # don't know they are 'empty' frames
228
+
229
+ # We need to adjust our output elements to be
230
+ # consecutive and with the right values
231
+ # TODO: We are using int() for fps but its float...
232
+ audio_frame.time_base = Fraction(1, int(self.audio_fps))
233
+ #audio_frame.pts = int(audio_frame_index / audio_frame.time_base)
234
+ audio_frame.pts = audio_pts
235
+ # We increment for the next iteration
236
+ audio_pts += audio_frame.samples
237
+ #audio_frame.pts = int(t + (audio_frame_index * audio_frame.time_base) / audio_frame.time_base)
238
+
239
+ #print(f'[AUDIO] Here in t:{str(t)} -> pts:{str(audio_frame.pts)} - dts:{str(audio_frame.dts)}')
240
+
241
+ num_of_audio_frames += 1
242
+ print(audio_frame)
243
+ writer.mux_audio_frame(audio_frame)
244
+ print(f'Num of audio frames: {str(num_of_audio_frames)}')
245
+
178
246
  writer.mux_video_frame(None)
179
- writer.output.close()
247
+ writer.mux_audio_frame(None)
248
+ writer.output.close()
249
+
250
+
251
+ # TODO: I don't want to have this here
252
+ def get_ts(
253
+ start: float,
254
+ end: float,
255
+ fps: int
256
+ ):
257
+ """
258
+ Obtain, without using a Progression class and
259
+ importing the library, a list of 't' time
260
+ moments from the provided 'start' to the also
261
+ given 'end', with the 'fps' given as parameter.
262
+ """
263
+ dt = 1.0 / fps
264
+ times = []
265
+
266
+ t = start
267
+ while t <= end:
268
+ times.append(t + 0.000001)
269
+ t += dt
270
+
271
+ return times
@@ -1,9 +1,148 @@
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_frame_time 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
3
5
  from yta_validation.parameter import ParameterValidator
4
6
  from typing import Union
5
7
 
6
8
 
9
+ NON_LIMITED_EMPTY_PART_END = 999
10
+ """
11
+ A value to indicate that the empty part
12
+ has no end because it is in the last
13
+ position and there is no video after it.
14
+ """
15
+ class _Part:
16
+ """
17
+ Class to represent an element that is on the
18
+ track, that can be an empty space or a video
19
+ (with audio).
20
+ """
21
+
22
+ @property
23
+ def is_empty_part(
24
+ self
25
+ ) -> bool:
26
+ """
27
+ Flag to indicate if the part is an empty part,
28
+ which means that there is no video associated
29
+ but an empty space.
30
+ """
31
+ return self.video is None
32
+
33
+ def __init__(
34
+ self,
35
+ track: 'Track',
36
+ start: float,
37
+ end: float,
38
+ video: Union[VideoOnTrack, None] = None
39
+ ):
40
+ ParameterValidator.validate_mandatory_positive_number('start', start, do_include_zero = True)
41
+ ParameterValidator.validate_mandatory_positive_number('end', end, do_include_zero = False)
42
+ ParameterValidator.validate_instance_of('video', video, VideoOnTrack)
43
+
44
+ self._track: Track = track
45
+ """
46
+ The instance of the track this part belongs
47
+ to.
48
+ """
49
+ self.start: float = float(start)
50
+ """
51
+ The start 't' time moment of the part.
52
+ """
53
+ self.end: float = float(end)
54
+ """
55
+ The end 't' time moment of the part.
56
+ """
57
+ self.video: Union[VideoOnTrack, None] = video
58
+ """
59
+ The video associated, if existing, or
60
+ None if it is an empty space that we need
61
+ to fulfill with a black background and
62
+ silent audio.
63
+ """
64
+
65
+ def get_frame_at(
66
+ self,
67
+ t: float
68
+ ) -> 'VideoFrame':
69
+ """
70
+ Get the frame that must be displayed at
71
+ the given 't' time moment.
72
+ """
73
+ if self.is_empty_part:
74
+ # TODO: What about the 'format' (?)
75
+ return get_black_background_video_frame(self._track.size)
76
+
77
+ frame = self.video.get_frame_at(t)
78
+
79
+ # TODO: This should not happen because of
80
+ # the way we handle the videos here but the
81
+ # video could send us a None frame here, so
82
+ # do we raise exception (?)
83
+ if frame is None:
84
+ #frame = get_black_background_video_frame(self._track.size)
85
+ # TODO: By now I'm raising exception to check if
86
+ # this happens or not because I think it would
87
+ # be malfunctioning
88
+ raise Exception(f'Video is returning None frame at t={str(t)}.')
89
+
90
+ return frame
91
+
92
+ # TODO: I'm not sure if we need this
93
+ def get_audio_frames_at(
94
+ self,
95
+ t: float
96
+ ):
97
+ if not self.is_empty_part:
98
+ frames = self.video.get_audio_frames_at(t)
99
+ else:
100
+ # TODO: Transform this below to a utils in
101
+ # which I obtain the array directly
102
+ # Check many full and partial silent frames we need
103
+ number_of_frames, number_of_remaining_samples = audio_frames_and_remainder_per_video_frame(
104
+ fps = self._track.fps,
105
+ sample_rate = self._track.audio_fps,
106
+ nb_samples = self._track.audio_nb_samples
107
+ )
108
+
109
+ # TODO: I need to set the pts, but here (?)
110
+ # The complete silent frames we need
111
+ frames = (
112
+ [
113
+ get_silent_audio_frame(
114
+ sample_rate = self._track.audio_fps,
115
+ # TODO: Check where do we get this value from
116
+ layout = 'stereo',
117
+ nb_samples = self._track.audio_nb_samples,
118
+ # TODO: Check where do we get this value from
119
+ format = 'fltp'
120
+ )
121
+ ] * number_of_frames
122
+ if number_of_frames > 0 else
123
+ []
124
+ )
125
+
126
+ # The remaining partial silent frames we need
127
+ if number_of_remaining_samples > 0:
128
+ frames.append(
129
+ get_silent_audio_frame(
130
+ sample_rate = self._track.audio_fps,
131
+ # TODO: Check where do we get this value from
132
+ layout = 'stereo',
133
+ nb_samples = number_of_remaining_samples,
134
+ # TODO: Check where do we get this value from
135
+ format = 'fltp'
136
+ )
137
+ )
138
+
139
+ # TODO: Return or yield (?)
140
+ for frame in frames:
141
+ yield frame
142
+ #return frames
143
+
144
+ # TODO: I don't like using t as float,
145
+ # we need to implement fractions.Fraction
7
146
  # TODO: This is called Track but it is
8
147
  # handling videos only. Should I have
9
148
  # VideoTrack and AudioTrack (?)
@@ -14,6 +153,26 @@ class Track:
14
153
  project.
15
154
  """
16
155
 
156
+ @property
157
+ def parts(
158
+ self
159
+ ) -> list[_Part]:
160
+ """
161
+ The list of parts that build this track,
162
+ but with the empty parts detected to
163
+ be fulfilled with black frames and silent
164
+ audios.
165
+
166
+ A part can be a video or an empty space.
167
+ """
168
+ if (
169
+ not hasattr(self, '_parts') or
170
+ self._parts is None
171
+ ):
172
+ self._recalculate_parts()
173
+
174
+ return self._parts
175
+
17
176
  @property
18
177
  def end(
19
178
  self
@@ -34,13 +193,40 @@ class Track:
34
193
  )
35
194
 
36
195
  def __init__(
37
- self
196
+ self,
197
+ # TODO: I need the general settings of the
198
+ # project to be able to make audio also, not
199
+ # only the empty frames
200
+ size: tuple[int, int],
201
+ fps: float,
202
+ audio_fps: float,
203
+ # TODO: Change the name
204
+ audio_nb_samples: int
38
205
  ):
39
206
  self.videos: list[VideoOnTrack] = []
40
207
  """
41
208
  The list of 'VideoOnTrack' instances that
42
209
  must play on this track.
43
210
  """
211
+ self.size: tuple[int, int] = size
212
+ """
213
+ The size of the videos of this track.
214
+ """
215
+ self.fps: float = fps
216
+ """
217
+ The fps of the track, needed to calculate
218
+ the base t time moments to be precise and
219
+ to obtain or generate the frames.
220
+ """
221
+ self.audio_fps: float = audio_fps
222
+ """
223
+ The fps of the audio track, needed to
224
+ generate silent audios for the empty parts.
225
+ """
226
+ self.audio_nb_samples: int = audio_nb_samples
227
+ """
228
+ The number of samples per audio frame.
229
+ """
44
230
 
45
231
  def _is_free(
46
232
  self,
@@ -60,43 +246,73 @@ class Track:
60
246
  for video in self.videos
61
247
  )
62
248
 
63
- def _get_video_at_t(
249
+ def _get_part_at_t(
64
250
  self,
65
251
  t: float
66
- ) -> Union[VideoOnTrack, None]:
252
+ ) -> _Part:
67
253
  """
68
- Get the video that is being played at
69
- the 't' time moment provided.
254
+ Get the part at the given 't' time
255
+ moment, that will always exist because
256
+ we have an special non ended last
257
+ empty part that would be returned if
258
+ accessing to an empty 't'.
70
259
  """
71
- for video in self.videos:
72
- if video.start <= t < video.end:
73
- return video
260
+ for part in self.parts:
261
+ if part.start <= t < part.end:
262
+ return part
74
263
 
264
+ # TODO: This will only happen if they are
265
+ # asking for a value greater than the
266
+ # NON_LIMITED_EMPTY_PART_END...
267
+ raise Exception('NON_LIMITED_EMPTY_PART_END exceeded.')
75
268
  return None
76
-
269
+
77
270
  def get_frame_at(
78
271
  self,
79
272
  t: float
80
- ) -> Union['VideoFrame', None]:
273
+ ) -> 'VideoFrame':
81
274
  """
82
275
  Get the frame that must be displayed at
83
276
  the 't' time moment provided, which is
84
- a frame from the video that is being
85
- played at that time moment.
277
+ a frame from the video audio that is
278
+ being played at that time moment.
86
279
 
87
280
  Remember, this 't' time moment provided
88
281
  is about the track, and we make the
89
282
  conversion to the actual video 't' to
90
283
  get the frame.
91
284
  """
92
- video = self._get_video_at_t(t)
285
+ # TODO: What if the frame, that comes from
286
+ # a video, doesn't have the expected size (?)
287
+ return self._get_part_at_t(t).get_frame_at(t)
288
+
289
+ # TODO: This is not working well...
290
+ def get_audio_frames_at(
291
+ self,
292
+ t: float
293
+ ):
294
+ """
295
+ Get the sequence of audio frames that
296
+ must be displayed at the 't' time
297
+ moment provided, which the collection
298
+ of audio frames corresponding to the
299
+ video frame that is being played at
300
+ that time moment.
93
301
 
94
- return (
95
- video.get_frame_at(t)
96
- if video is not None else
97
- None
98
- )
302
+ Remember, this 't' time moment provided
303
+ is about the track, and we make the
304
+ conversion to the actual video 't' to
305
+ get the frame.
99
306
 
307
+ This is useful when we want to write a
308
+ video frame with its audio, so we obtain
309
+ all the audio frames associated to it
310
+ (remember that a video frame is associated
311
+ with more than 1 audio frame).
312
+ """
313
+ for frame in self._get_part_at_t(t).get_audio_frames_at(t):
314
+ yield frame
315
+
100
316
  def add_video(
101
317
  self,
102
318
  video: Video,
@@ -117,11 +333,14 @@ class Track:
117
333
  if no video, or the end of the last video.
118
334
  """
119
335
  ParameterValidator.validate_mandatory_instance_of('video', video, Video)
120
- ParameterValidator.validate_positive_float('t', t, do_include_zero = True)
336
+ ParameterValidator.validate_positive_number('t', t, do_include_zero = True)
121
337
 
122
338
  if t is not None:
123
339
  # TODO: We can have many different strategies
124
340
  # that we could define in the '__init__' maybe
341
+ # TODO: I don't like using float 't', but we
342
+ # need to make sure it is a multiple of 1 / fps
343
+ t = T.get_frame_time_base(float(t), self.fps)
125
344
  if not self._is_free(t, (t + video.end)):
126
345
  raise Exception('The video cannot be added at the "t" time moment, something blocks it.')
127
346
  else:
@@ -132,5 +351,53 @@ class Track:
132
351
  t
133
352
  ))
134
353
 
354
+ self._recalculate_parts()
355
+
135
356
  # TODO: Maybe return the VideoOnTrack instead (?)
357
+ return self
358
+
359
+ def _recalculate_parts(
360
+ self
361
+ ) -> 'Track':
362
+ """
363
+ Check the track and get all the parts. A
364
+ part can be empty (non video nor audio on
365
+ that time period, which means black
366
+ background and silence audio), or a video
367
+ with (or without) audio.
368
+ """
369
+ parts = []
370
+ cursor = 0.0
371
+
372
+ for video in self.videos:
373
+ # Empty space between cursor and start of
374
+ # the next clip
375
+ if video.start > cursor:
376
+ parts.append(_Part(
377
+ track = self,
378
+ start = cursor,
379
+ end = video.start,
380
+ video = None
381
+ ))
382
+
383
+ # The video itself
384
+ parts.append(_Part(
385
+ track = self,
386
+ start = video.start,
387
+ end = video.end,
388
+ video = video
389
+ ))
390
+
391
+ cursor = video.end
392
+
393
+ # Add the non limited last empty part
394
+ parts.append(_Part(
395
+ track = self,
396
+ start = cursor,
397
+ end = NON_LIMITED_EMPTY_PART_END,
398
+ video = None
399
+ ))
400
+
401
+ self._parts = parts
402
+
136
403
  return self