yta-video-opengl 0.0.11__py3-none-any.whl → 0.0.13__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,9 +1,157 @@
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_opengl.t 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
5
+ from yta_video_opengl.t import fps_to_time_base
3
6
  from yta_validation.parameter import ParameterValidator
7
+ from quicktions import Fraction
4
8
  from typing import Union
5
9
 
6
10
 
11
+ NON_LIMITED_EMPTY_PART_END = 999
12
+ """
13
+ A value to indicate that the empty part
14
+ has no end because it is in the last
15
+ position and there is no video after it.
16
+ """
17
+ class _Part:
18
+ """
19
+ Class to represent an element that is on the
20
+ track, that can be an empty space or a video
21
+ (with audio).
22
+ """
23
+
24
+ @property
25
+ def is_empty_part(
26
+ self
27
+ ) -> bool:
28
+ """
29
+ Flag to indicate if the part is an empty part,
30
+ which means that there is no video associated
31
+ but an empty space.
32
+ """
33
+ return self.video is None
34
+
35
+ def __init__(
36
+ self,
37
+ track: 'Track',
38
+ start: Union[int, float, Fraction],
39
+ end: Union[int, float, Fraction],
40
+ video: Union[VideoOnTrack, None] = None
41
+ ):
42
+ # TODO: We need to accept Fraction as number
43
+ # ParameterValidator.validate_mandatory_positive_number('start', start, do_include_zero = True)
44
+ # TODO: We need to accept Fraction as number
45
+ # ParameterValidator.validate_mandatory_positive_number('end', end, do_include_zero = False)
46
+ ParameterValidator.validate_instance_of('video', video, VideoOnTrack)
47
+
48
+ self._track: Track = track
49
+ """
50
+ The instance of the track this part belongs
51
+ to.
52
+ """
53
+ self.start: Fraction = Fraction(start)
54
+ """
55
+ The start 't' time moment of the part.
56
+ """
57
+ self.end: Fraction = Fraction(end)
58
+ """
59
+ The end 't' time moment of the part.
60
+ """
61
+ self.video: Union[VideoOnTrack, None] = video
62
+ """
63
+ The video associated, if existing, or
64
+ None if it is an empty space that we need
65
+ to fulfill with a black background and
66
+ silent audio.
67
+ """
68
+
69
+ def get_frame_at(
70
+ self,
71
+ t: Union[int, float, Fraction]
72
+ ) -> 'VideoFrame':
73
+ """
74
+ Get the frame that must be displayed at
75
+ the given 't' time moment.
76
+ """
77
+ if self.is_empty_part:
78
+ # TODO: What about the 'format' (?)
79
+ # TODO: Maybe I shouldn't set the 'time_base'
80
+ # here and do it just in the Timeline 'render'
81
+ #return get_black_background_video_frame(self._track.size)
82
+ # TODO: This 'time_base' maybe has to be related
83
+ # to a Timeline general 'time_base' and not the fps
84
+ return get_black_background_video_frame(self._track.size, time_base = fps_to_time_base(self._track.fps))
85
+
86
+ frame = self.video.get_frame_at(t)
87
+
88
+ # TODO: This should not happen because of
89
+ # the way we handle the videos here but the
90
+ # video could send us a None frame here, so
91
+ # do we raise exception (?)
92
+ if frame is None:
93
+ #frame = get_black_background_video_frame(self._track.size)
94
+ # TODO: By now I'm raising exception to check if
95
+ # this happens or not because I think it would
96
+ # be malfunctioning
97
+ raise Exception(f'Video is returning None frame at t={str(t)}.')
98
+
99
+ return frame
100
+
101
+ # TODO: I'm not sure if we need this
102
+ def get_audio_frames_at(
103
+ self,
104
+ t: Union[int, float, Fraction]
105
+ ):
106
+ if not self.is_empty_part:
107
+ frames = self.video.get_audio_frames_at(t)
108
+ else:
109
+ # TODO: Transform this below to a utils in
110
+ # which I obtain the array directly
111
+ # Check many full and partial silent frames we need
112
+ number_of_frames, number_of_remaining_samples = audio_frames_and_remainder_per_video_frame(
113
+ video_fps = self._track.fps,
114
+ sample_rate = self._track.audio_fps,
115
+ number_of_samples_per_audio_frame = self._track.audio_samples_per_frame
116
+ )
117
+
118
+ # TODO: I need to set the pts, but here (?)
119
+ # The complete silent frames we need
120
+ frames = (
121
+ [
122
+ get_silent_audio_frame(
123
+ sample_rate = self._track.audio_fps,
124
+ # TODO: Check where do we get this value from
125
+ layout = 'stereo',
126
+ number_of_samples = self._track.audio_samples_per_frame,
127
+ # TODO: Check where do we get this value from
128
+ format = 'fltp'
129
+ )
130
+ ] * number_of_frames
131
+ if number_of_frames > 0 else
132
+ []
133
+ )
134
+
135
+ # The remaining partial silent frames we need
136
+ if number_of_remaining_samples > 0:
137
+ frames.append(
138
+ get_silent_audio_frame(
139
+ sample_rate = self._track.audio_fps,
140
+ # TODO: Check where do we get this value from
141
+ layout = 'stereo',
142
+ number_of_samples = number_of_remaining_samples,
143
+ # TODO: Check where do we get this value from
144
+ format = 'fltp'
145
+ )
146
+ )
147
+
148
+ # TODO: Return or yield (?)
149
+ for frame in frames:
150
+ yield frame
151
+ #return frames
152
+
153
+ # TODO: I don't like using t as float,
154
+ # we need to implement fractions.Fraction
7
155
  # TODO: This is called Track but it is
8
156
  # handling videos only. Should I have
9
157
  # VideoTrack and AudioTrack (?)
@@ -14,17 +162,37 @@ class Track:
14
162
  project.
15
163
  """
16
164
 
165
+ @property
166
+ def parts(
167
+ self
168
+ ) -> list[_Part]:
169
+ """
170
+ The list of parts that build this track,
171
+ but with the empty parts detected to
172
+ be fulfilled with black frames and silent
173
+ audios.
174
+
175
+ A part can be a video or an empty space.
176
+ """
177
+ if (
178
+ not hasattr(self, '_parts') or
179
+ self._parts is None
180
+ ):
181
+ self._recalculate_parts()
182
+
183
+ return self._parts
184
+
17
185
  @property
18
186
  def end(
19
187
  self
20
- ) -> float:
188
+ ) -> Fraction:
21
189
  """
22
190
  The end of the last video of this track,
23
191
  which is also the end of the track. This
24
192
  is the last time moment that has to be
25
193
  rendered.
26
194
  """
27
- return (
195
+ return Fraction(
28
196
  0.0
29
197
  if len(self.videos) == 0 else
30
198
  max(
@@ -34,18 +202,45 @@ class Track:
34
202
  )
35
203
 
36
204
  def __init__(
37
- self
205
+ self,
206
+ # TODO: I need the general settings of the
207
+ # project to be able to make audio also, not
208
+ # only the empty frames
209
+ size: tuple[int, int],
210
+ fps: float,
211
+ audio_fps: float,
212
+ # TODO: Where does it come from (?)
213
+ audio_samples_per_frame: int
38
214
  ):
39
215
  self.videos: list[VideoOnTrack] = []
40
216
  """
41
217
  The list of 'VideoOnTrack' instances that
42
218
  must play on this track.
43
219
  """
220
+ self.size: tuple[int, int] = size
221
+ """
222
+ The size of the videos of this track.
223
+ """
224
+ self.fps: float = float(fps)
225
+ """
226
+ The fps of the track, needed to calculate
227
+ the base t time moments to be precise and
228
+ to obtain or generate the frames.
229
+ """
230
+ self.audio_fps: float = float(audio_fps)
231
+ """
232
+ The fps of the audio track, needed to
233
+ generate silent audios for the empty parts.
234
+ """
235
+ self.audio_samples_per_frame: int = audio_samples_per_frame
236
+ """
237
+ The number of samples per audio frame.
238
+ """
44
239
 
45
240
  def _is_free(
46
241
  self,
47
- start: float,
48
- end: float
242
+ start: Union[int, float, Fraction],
243
+ end: Union[int, float, Fraction]
49
244
  ) -> bool:
50
245
  """
51
246
  Check if the time range in between the
@@ -60,47 +255,77 @@ class Track:
60
255
  for video in self.videos
61
256
  )
62
257
 
63
- def _get_video_at_t(
258
+ def _get_part_at_t(
64
259
  self,
65
- t: float
66
- ) -> Union[VideoOnTrack, None]:
260
+ t: Union[int, float, Fraction]
261
+ ) -> _Part:
67
262
  """
68
- Get the video that is being played at
69
- the 't' time moment provided.
263
+ Get the part at the given 't' time
264
+ moment, that will always exist because
265
+ we have an special non ended last
266
+ empty part that would be returned if
267
+ accessing to an empty 't'.
70
268
  """
71
- for video in self.videos:
72
- if video.start <= t < video.end:
73
- return video
269
+ for part in self.parts:
270
+ if part.start <= t < part.end:
271
+ return part
74
272
 
273
+ # TODO: This will only happen if they are
274
+ # asking for a value greater than the
275
+ # NON_LIMITED_EMPTY_PART_END...
276
+ raise Exception('NON_LIMITED_EMPTY_PART_END exceeded.')
75
277
  return None
76
-
278
+
77
279
  def get_frame_at(
78
280
  self,
79
- t: float
80
- ) -> Union['VideoFrame', None]:
281
+ t: Union[int, float, Fraction]
282
+ ) -> 'VideoFrame':
81
283
  """
82
284
  Get the frame that must be displayed at
83
285
  the 't' time moment provided, which is
84
- a frame from the video that is being
85
- played at that time moment.
286
+ a frame from the video audio that is
287
+ being played at that time moment.
86
288
 
87
289
  Remember, this 't' time moment provided
88
290
  is about the track, and we make the
89
291
  conversion to the actual video 't' to
90
292
  get the frame.
91
293
  """
92
- video = self._get_video_at_t(t)
294
+ # TODO: What if the frame, that comes from
295
+ # a video, doesn't have the expected size (?)
296
+ return self._get_part_at_t(t).get_frame_at(t)
297
+
298
+ # TODO: This is not working well...
299
+ def get_audio_frames_at(
300
+ self,
301
+ t: Union[int, float, Fraction]
302
+ ):
303
+ """
304
+ Get the sequence of audio frames that
305
+ must be displayed at the 't' time
306
+ moment provided, which the collection
307
+ of audio frames corresponding to the
308
+ video frame that is being played at
309
+ that time moment.
93
310
 
94
- return (
95
- video.get_frame_at(t)
96
- if video is not None else
97
- None
98
- )
311
+ Remember, this 't' time moment provided
312
+ is about the track, and we make the
313
+ conversion to the actual video 't' to
314
+ get the frame.
99
315
 
316
+ This is useful when we want to write a
317
+ video frame with its audio, so we obtain
318
+ all the audio frames associated to it
319
+ (remember that a video frame is associated
320
+ with more than 1 audio frame).
321
+ """
322
+ for frame in self._get_part_at_t(t).get_audio_frames_at(t):
323
+ yield frame
324
+
100
325
  def add_video(
101
326
  self,
102
327
  video: Video,
103
- t: Union[float, None] = None
328
+ t: Union[int, float, Fraction, None] = None
104
329
  ) -> 'Track':
105
330
  """
106
331
  Add the 'video' provided to the track. If
@@ -117,13 +342,15 @@ class Track:
117
342
  if no video, or the end of the last video.
118
343
  """
119
344
  ParameterValidator.validate_mandatory_instance_of('video', video, Video)
120
- ParameterValidator.validate_positive_float('t', t, do_include_zero = True)
345
+ ParameterValidator.validate_positive_number('t', t, do_include_zero = True)
121
346
 
122
347
  if t is not None:
123
348
  # TODO: We can have many different strategies
124
349
  # that we could define in the '__init__' maybe
125
- if not self._is_free(t, (t + video.end)):
350
+ t: T = T.from_fps(t, self.fps)
351
+ if not self._is_free(t.truncated, t.next(1).truncated):
126
352
  raise Exception('The video cannot be added at the "t" time moment, something blocks it.')
353
+ t = t.truncated
127
354
  else:
128
355
  t = self.end
129
356
 
@@ -132,5 +359,53 @@ class Track:
132
359
  t
133
360
  ))
134
361
 
362
+ self._recalculate_parts()
363
+
135
364
  # TODO: Maybe return the VideoOnTrack instead (?)
365
+ return self
366
+
367
+ def _recalculate_parts(
368
+ self
369
+ ) -> 'Track':
370
+ """
371
+ Check the track and get all the parts. A
372
+ part can be empty (non video nor audio on
373
+ that time period, which means black
374
+ background and silence audio), or a video
375
+ with (or without) audio.
376
+ """
377
+ parts = []
378
+ cursor = 0.0
379
+
380
+ for video in self.videos:
381
+ # Empty space between cursor and start of
382
+ # the next clip
383
+ if video.start > cursor:
384
+ parts.append(_Part(
385
+ track = self,
386
+ start = cursor,
387
+ end = video.start,
388
+ video = None
389
+ ))
390
+
391
+ # The video itself
392
+ parts.append(_Part(
393
+ track = self,
394
+ start = video.start,
395
+ end = video.end,
396
+ video = video
397
+ ))
398
+
399
+ cursor = video.end
400
+
401
+ # Add the non limited last empty part
402
+ parts.append(_Part(
403
+ track = self,
404
+ start = cursor,
405
+ end = NON_LIMITED_EMPTY_PART_END,
406
+ video = None
407
+ ))
408
+
409
+ self._parts = parts
410
+
136
411
  return self
@@ -16,6 +16,8 @@ finished at `t=4`
16
16
  from yta_video_opengl.video import Video
17
17
  from yta_validation.parameter import ParameterValidator
18
18
  from av.video.frame import VideoFrame
19
+ from av.audio.frame import AudioFrame
20
+ from quicktions import Fraction
19
21
  from typing import Union
20
22
 
21
23
 
@@ -27,7 +29,7 @@ class VideoOnTrack:
27
29
  @property
28
30
  def end(
29
31
  self
30
- ) -> float:
32
+ ) -> Fraction:
31
33
  """
32
34
  The end time moment 't' of the video once
33
35
  once its been placed on the track, which
@@ -41,17 +43,20 @@ class VideoOnTrack:
41
43
  def __init__(
42
44
  self,
43
45
  video: Video,
44
- start: float = 0.0
46
+ start: Union[int, float, Fraction] = 0.0
45
47
  ):
46
48
  ParameterValidator.validate_mandatory_instance_of('video', video, Video)
47
- ParameterValidator.validate_mandatory_positive_number('start', start, do_include_zero = True)
49
+ # TODO: Now we need to accept 'Fraction',
50
+ # from 'fractions' or 'quicktions', as a
51
+ # number
52
+ #ParameterValidator.validate_mandatory_positive_number('start', start, do_include_zero = True)
48
53
 
49
54
  self.video: Video = video
50
55
  """
51
56
  The video source, with all its properties,
52
57
  that is placed in the timeline.
53
58
  """
54
- self.start: float = float(start)
59
+ self.start: Fraction = Fraction(start)
55
60
  """
56
61
  The time moment in which the video should
57
62
  start playing, within the timeline.
@@ -63,7 +68,7 @@ class VideoOnTrack:
63
68
 
64
69
  def _get_video_t(
65
70
  self,
66
- t: float
71
+ t: Union[int, float, Fraction]
67
72
  ) -> float:
68
73
  """
69
74
  The video 't' time moment for the given
@@ -71,30 +76,88 @@ class VideoOnTrack:
71
76
  to use inside the video content to display
72
77
  its frame.
73
78
  """
79
+ # TODO: Use 'T' here to be precise or the
80
+ # argument itself must be precise (?)
74
81
  return t - self.start
75
82
 
76
83
  def is_playing(
77
84
  self,
78
- t: float
85
+ t: Union[int, float, Fraction]
79
86
  ) -> bool:
80
87
  """
81
88
  Check if this video is playing at the general
82
89
  't' time moment, which is a global time moment
83
90
  for the whole project.
84
91
  """
92
+ # TODO: Use 'T' here to be precise or the
93
+ # argument itself must be precise (?)
85
94
  return self.start <= t < self.end
86
95
 
87
96
  def get_frame_at(
88
97
  self,
89
- t: float
98
+ t: Union[int, float, Fraction]
90
99
  ) -> Union[VideoFrame, None]:
91
100
  """
92
101
  Get the frame for the 't' time moment provided,
93
102
  that could be None if the video is not playing
94
103
  in that moment.
95
104
  """
105
+ # TODO: Use 'T' here to be precise or the
106
+ # argument itself must be precise (?)
107
+ return (
108
+ self.video.get_frame_from_t(self._get_video_t(t))
109
+ if self.is_playing(t) else
110
+ None
111
+ )
112
+
113
+ def get_audio_frame_at(
114
+ self,
115
+ t: Union[int, float, Fraction]
116
+ ) -> Union[AudioFrame, None]:
117
+ """
118
+ Get the audio frame for the 't' time moment
119
+ provided, that could be None if the video
120
+ is not playing in that moment.
121
+ """
122
+ # TODO: Use 'T' here to be precise or the
123
+ # argument itself must be precise (?)
96
124
  return (
97
- self.video.reader.get_frame_from_t(self._get_video_t(t))
125
+ self.video.get_audio_frame_from_t(self._get_video_t(t))
98
126
  if self.is_playing(t) else
99
127
  None
100
- )
128
+ )
129
+
130
+ def get_audio_frames_at(
131
+ self,
132
+ t: Union[int, float, Fraction]
133
+ ) -> Union[any, None]:
134
+ """
135
+ Get the audio frames that must be played at
136
+ the 't' time moment provided, that could be
137
+ None if the video is not playing at that
138
+ moment.
139
+
140
+ This method will return None if no audio
141
+ frames found in that 't' time moment, or an
142
+ iterator if yes.
143
+ """
144
+ # TODO: Use 'T' here to be precise or the
145
+ # argument itself must be precise (?)
146
+ frames = (
147
+ self.video.get_audio_frames_from_t(self._get_video_t(t))
148
+ if self.is_playing(t) else
149
+ []
150
+ )
151
+
152
+ for frame in frames:
153
+ # TODO: I am generating a tuple in the
154
+ # src\yta_video_opengl\reader\cache.py
155
+ # get_frames method... maybe remove it (?)
156
+ yield frame[0]
157
+
158
+ # # TODO: This was a simple return before
159
+ # return (
160
+ # self.video.reader.get_audio_frames_from_t(self._get_video_t(t))
161
+ # if self.is_playing(t) else
162
+ # None
163
+ # )