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.
@@ -15,9 +15,14 @@ frame we are requesting in the moment, keeping in
15
15
  memory all those frames to be handled fast. It
16
16
  will remove the old frames if needed to use only
17
17
  the 'size' we set when creating it.
18
+
19
+ A stream can have 'fps = 60' but use another
20
+ different time base that make the pts values go 0,
21
+ 256, 512... for example. The 'time_base' is the
22
+ only accurate way to obtain the pts.
18
23
  """
19
24
  from yta_video_opengl.utils import t_to_pts, pts_to_t, pts_to_index, index_to_pts
20
- from yta_video_frame_time import T
25
+ from yta_video_opengl.t import T
21
26
  from av.container import InputContainer
22
27
  from av.video.stream import VideoStream
23
28
  from av.audio.stream import AudioStream
@@ -25,14 +30,19 @@ from av.video.frame import VideoFrame
25
30
  from av.audio.frame import AudioFrame
26
31
  from yta_validation.parameter import ParameterValidator
27
32
  from yta_validation import PythonValidator
28
- from fractions import Fraction
33
+ from quicktions import Fraction
29
34
  from collections import OrderedDict
30
35
  from typing import Union
31
36
 
32
37
  import numpy as np
38
+ import av
33
39
  import math
34
40
 
35
41
 
42
+ # TODO: This is not actually a Video
43
+ # cache, is a FrameCache because we
44
+ # create one for video but another
45
+ # one for audio. Rename it please.
36
46
  class VideoFrameCache:
37
47
  """
38
48
  Class to manage the frames cache of a video
@@ -108,6 +118,7 @@ class VideoFrameCache:
108
118
  # use the amount of frames of the biggest
109
119
  # interval of frames that belongs to a key
110
120
  # frame, or a value by default
121
+ # TODO: Careful if this is too big
111
122
  fps = (
112
123
  float(self.stream.average_rate)
113
124
  if PythonValidator.is_instance_of(self.stream, VideoStream) else
@@ -116,7 +127,7 @@ class VideoFrameCache:
116
127
  # Intervals, but in number of frames
117
128
  intervals = np.diff(
118
129
  # Intervals of time between keyframes
119
- np.array(self.key_frames_pts) * self.stream.time_base
130
+ np.array(self.key_frames_pts) * self.time_base
120
131
  ) * fps
121
132
 
122
133
  self.size = (
@@ -131,7 +142,7 @@ class VideoFrameCache:
131
142
 
132
143
  self.container.seek(0)
133
144
 
134
- def _get_nearest_keyframe_fps(
145
+ def _get_nearest_keyframe_pts(
135
146
  self,
136
147
  pts: int
137
148
  ):
@@ -157,7 +168,6 @@ class VideoFrameCache:
157
168
  the cache if full.
158
169
  """
159
170
  if frame.pts not in self.cache:
160
- # TODO: The 'format' must be dynamic
161
171
  self.cache[frame.pts] = frame
162
172
 
163
173
  # Clean cache if full
@@ -166,7 +176,7 @@ class VideoFrameCache:
166
176
 
167
177
  return frame
168
178
 
169
- def _get_frame_by_pts(
179
+ def get_frame_from_pts(
170
180
  self,
171
181
  pts: int
172
182
  ) -> Union[VideoFrame, AudioFrame, None]:
@@ -181,11 +191,23 @@ class VideoFrameCache:
181
191
  This method must be called when the frame
182
192
  requested is not stored in the caché.
183
193
  """
194
+ if pts in self.cache:
195
+ return self.cache[pts]
196
+
184
197
  # Look for the most near key frame
185
- key_frame_pts = self._get_nearest_keyframe_fps(pts)
198
+ key_frame_pts = self._get_nearest_keyframe_pts(pts)
186
199
 
187
200
  # Go to the key frame that includes it
188
- self.container.seek(key_frame_pts, stream = self.stream)
201
+ # but I read that it is recommended to
202
+ # read ~100ms before the pts we want to
203
+ # actually read so we obtain the frames
204
+ # clean (this is important in audio)
205
+ # TODO: This code is repeated, refactor
206
+ pts_pad = int(0.1 / self.time_base)
207
+ self.container.seek(
208
+ offset = max(0, key_frame_pts - pts_pad),
209
+ stream = self.stream
210
+ )
189
211
 
190
212
  decoded = None
191
213
  for frame in self.container.decode(self.stream):
@@ -196,6 +218,15 @@ class VideoFrameCache:
196
218
  # Store in cache if needed
197
219
  self._store_frame_in_cache(frame)
198
220
 
221
+ """
222
+ The 'frame.pts * frame.time_base' will give
223
+ us the index of the frame, and actually the
224
+ 'pts' que are looking for seems to be the
225
+ index and not a pts.
226
+
227
+ TODO: Review all this in all the logic
228
+ please.
229
+ """
199
230
  if frame.pts >= pts:
200
231
  decoded = self.cache[frame.pts]
201
232
  break
@@ -204,6 +235,7 @@ class VideoFrameCache:
204
235
  # frames to be able to decode...
205
236
  return decoded
206
237
 
238
+ # TODO: I'm not using this method...
207
239
  def get_frame(
208
240
  self,
209
241
  index: int
@@ -218,55 +250,89 @@ class VideoFrameCache:
218
250
  return (
219
251
  self.cache[pts]
220
252
  if pts in self.cache else
221
- self._get_frame_by_pts(pts)
253
+ self.get_frame_from_pts(pts)
222
254
  )
223
255
 
224
256
  def get_frame_from_t(
225
257
  self,
226
- t: float
258
+ t: Union[int, float, Fraction]
227
259
  ) -> Union[VideoFrame, AudioFrame]:
228
260
  """
229
261
  Get the frame with the given 't' time moment
230
262
  from the cache.
231
263
  """
232
- return self.get_frame(T.video_frame_time_to_video_frame_index(t, self.fps))
264
+ return self.get_frame_from_pts(T(t, self.time_base).truncated_pts)
233
265
 
234
266
  def get_frames(
235
267
  self,
236
- start: float = 0,
237
- end: Union[float, None] = None
268
+ start: Union[int, float, Fraction] = 0,
269
+ end: Union[int, float, Fraction, None] = None
238
270
  ):
239
271
  """
240
272
  Get all the frames in the range between
241
273
  the provided 'start' and 'end' time in
242
274
  seconds.
275
+
276
+ This method is an iterator that yields
277
+ the frame, its t and its index.
243
278
  """
244
279
  # We use the cache as iterator if all the frames
245
280
  # requested are stored there
246
- pts_list = [
247
- t_to_pts(t, self.time_base)
248
- for t in T.get_frame_indexes(self.stream.duration, self.fps, start, end)
249
- ]
250
-
251
- if all(
252
- pts in self.cache
253
- for pts in pts_list
254
- ):
255
- for pts in pts_list:
256
- yield self.cache[pts]
281
+ # TODO: I think this is not ok... I will never
282
+ # have all the pts form here stored, as they come
283
+ # from 't' that is different...
284
+
285
+ """
286
+ Feel free to move this explanation to other
287
+ place, its about the duration.
288
+
289
+ The stream 'duration' parameter is measured
290
+ on ticks, the amount of ticks that the
291
+ stream lasts. Here below is an example:
292
+
293
+ - Duration raw: 529200
294
+ - Time base: 1/44100
295
+ - Duration (seconds): 12.0
296
+ """
297
+
298
+ # The 'duration' is on pts ticks
299
+ duration = float(self.stream.duration * self.time_base)
300
+ # TODO: I think it would be better to
301
+ # receive and work with pts instead of
302
+ # 't' time moments...
303
+ # pts_list = [
304
+ # t_to_pts(t, self.time_base)
305
+ # for t in T.get_frame_indexes(duration, self.fps, start, end)
306
+ # ]
307
+
308
+ # if all(
309
+ # pts in self.cache
310
+ # for pts in pts_list
311
+ # ):
312
+ # for pts in pts_list:
313
+ # yield self.cache[pts]
257
314
 
258
315
  # If not all, we ignore the cache because we
259
316
  # need to decode and they are all consecutive
260
- start = t_to_pts(start, self.time_base)
317
+ start = T(start, self.time_base).truncated_pts
261
318
  end = (
262
- t_to_pts(end, self.time_base)
319
+ T(end, self.time_base).truncated_pts
263
320
  if end is not None else
264
321
  None
265
322
  )
266
- key_frame_pts = self._get_nearest_keyframe_fps(start)
323
+ key_frame_pts = self._get_nearest_keyframe_pts(start)
267
324
 
268
- # Go to the nearest key frame to start decoding
269
- self.container.seek(key_frame_pts, stream = self.stream)
325
+ # Go to the key frame that includes it
326
+ # but I read that it is recommended to
327
+ # read ~100ms before the pts we want to
328
+ # actually read so we obtain the frames
329
+ # clean (this is important in audio)
330
+ # TODO: This code is repeated, refactor
331
+ pts_pad = int(0.1 / self.time_base)
332
+ self.container.seek(
333
+ offset = max(0, key_frame_pts - pts_pad),
334
+ stream = self.stream
335
+ )
270
336
 
271
337
  for packet in self.container.demux(self.stream):
272
338
  for frame in packet.decode():
@@ -276,16 +342,80 @@ class VideoFrameCache:
276
342
  # We store all the frames in cache
277
343
  self._store_frame_in_cache(frame)
278
344
 
279
- if frame.pts < start:
345
+ frame_end_pts = frame.pts + int(frame.samples * (1 / self.stream.sample_rate) / self.time_base)
346
+ #frame_end_pts = frame.pts + int(frame.samples)
347
+ #frame_end_pts = frame.pts + int(frame.samples / (self.stream.sample_rate * self.time_base))
348
+
349
+ # For the next comments imagine we are looking
350
+ # for the [1.0, 2.0) audio time range
351
+ # Previous frame and nothing is inside
352
+ if frame_end_pts <= start:
353
+ # From 0.25 to 1.0
280
354
  continue
281
355
 
356
+ # We finished, nothing is inside and its after
282
357
  if (
283
358
  end is not None and
284
- frame.pts > end
359
+ frame.pts >= end
285
360
  ):
361
+ # From 2.0 to 2.75
286
362
  return
363
+
364
+ # We need: from 1 to 2
365
+ # Audio is:
366
+ # - from 0 to 0.75 (Not included, omit)
367
+ # - from 0.5 to 1.5 (Included, take 1.0 to 1.5)
368
+ # - from 0.5 to 2.5 (Included, take 1.0 to 2.0)
369
+ # - from 1.25 to 1.5 (Included, take 1.25 to 1.5)
370
+ # - from 1.25 to 2.5 (Included, take 1.25 to 2.0)
371
+ # - from 2.5 to 3.5 (Not included, omit)
372
+
373
+ # Here below, at least a part is inside
374
+ if (
375
+ frame.pts < start and
376
+ frame_end_pts > start
377
+ ):
378
+ # A part at the end is included
379
+ end_time = (
380
+ # From 0.5 to 1.5 0> take 1.0 to 1.5
381
+ frame_end_pts
382
+ if frame_end_pts <= end else
383
+ # From 0.5 to 2.5 => take 1.0 to 2.0
384
+ end
385
+ )
386
+ #print('A part at the end is included.')
387
+ # TODO: I'm using too much 'pts_to_t'
388
+ frame = trim_audio_frame_pts(
389
+ frame = frame,
390
+ start_pts = start,
391
+ end_pts = end_time,
392
+ time_base = self.time_base
393
+ )
394
+ elif (
395
+ frame.pts >= start and
396
+ frame.pts < end
397
+ ):
398
+ end_time = (
399
+ # From 1.25 to 1.5 => take 1.25 to 1.5
400
+ frame_end_pts
401
+ if frame_end_pts <= end else
402
+ # From 1.25 to 2.5 => take 1.25 to 2.0
403
+ end
404
+ )
405
+ # A part at the begining is included
406
+ #print('A part at the begining is included.')
407
+ # TODO: I'm using too much 'pts_to_t'
408
+ frame = trim_audio_frame_pts(
409
+ frame = frame,
410
+ start_pts = frame.pts,
411
+ end_pts = end_time,
412
+ time_base = self.time_base
413
+ )
414
+
415
+ # If the whole frame is in, past as it is
287
416
 
288
417
  # TODO: Maybe send a @dataclass instead (?)
418
+ # TODO: Do I really need these 't' and 'index' (?)
289
419
  yield (
290
420
  frame,
291
421
  pts_to_t(frame.pts, self.time_base),
@@ -300,4 +430,100 @@ class VideoFrameCache:
300
430
  """
301
431
  self.cache.clear()
302
432
 
303
- return self
433
+ return self
434
+
435
+
436
+ # TODO: Move this to a utils when refactored
437
+ def trim_audio_frame_pts(
438
+ frame: av.AudioFrame,
439
+ start_pts: int,
440
+ end_pts: int,
441
+ time_base
442
+ ) -> av.AudioFrame:
443
+ """
444
+ Recorta un AudioFrame para quedarse solo con la parte entre [start_pts, end_pts] en ticks (PTS).
445
+ """
446
+ samples = frame.to_ndarray() # (channels, n_samples)
447
+ n_channels, n_samples = samples.shape
448
+ sr = frame.sample_rate
449
+
450
+ #frame_end_pts = frame.pts + int((n_samples / sr) / time_base)
451
+ # TODO: This could be wrong
452
+ frame_end_pts = frame.pts + int(frame.samples)
453
+
454
+ # solapamiento en PTS
455
+ cut_start_pts = max(frame.pts, start_pts)
456
+ cut_end_pts = min(frame_end_pts, end_pts)
457
+
458
+ if cut_start_pts >= cut_end_pts:
459
+ raise Exception('Oops...')
460
+ return None # no hay solapamiento
461
+
462
+ # convertir a índices de samples (en ticks → segundos → samples)
463
+ cut_start_time = (cut_start_pts - frame.pts) * time_base
464
+ cut_end_time = (cut_end_pts - frame.pts) * time_base
465
+
466
+ start_idx = int(cut_start_time * sr)
467
+ end_idx = int(cut_end_time * sr)
468
+
469
+ # print(
470
+ # f"cutting [{frame.pts}, {frame_end_pts}] "
471
+ # f"to [{cut_start_pts}, {cut_end_pts}] "
472
+ # f"({start_idx}:{end_idx} / {frame.samples})"
473
+ # #f"({start_idx}:{end_idx} / {n_samples})"
474
+ # )
475
+
476
+ cut_samples = samples[:, start_idx:end_idx]
477
+
478
+ # crear nuevo AudioFrame
479
+ new_frame = av.AudioFrame.from_ndarray(cut_samples, format=frame.format, layout=frame.layout)
480
+ new_frame.sample_rate = sr
481
+
482
+ # ajustar PTS → corresponde al inicio real del recorte
483
+ new_frame.pts = cut_start_pts
484
+ new_frame.time_base = time_base
485
+
486
+ return new_frame
487
+
488
+
489
+
490
+ def trim_audio_frame_t(
491
+ frame: av.AudioFrame,
492
+ start_time: float,
493
+ end_time: float,
494
+ time_base
495
+ ) -> av.AudioFrame:
496
+ """
497
+ Recorta un AudioFrame para quedarse solo con la parte entre [start_time, end_time] en segundos.
498
+ """
499
+ samples = frame.to_ndarray() # (channels, n_samples)
500
+ n_channels, n_samples = samples.shape
501
+ sr = frame.sample_rate
502
+
503
+ frame_start = float(frame.pts * time_base)
504
+ frame_end = frame_start + (n_samples / sr)
505
+
506
+ # calcular solapamiento en segundos
507
+ cut_start = max(frame_start, start_time)
508
+ cut_end = min(frame_end, end_time)
509
+
510
+ if cut_start >= cut_end:
511
+ return None # no hay solapamiento
512
+
513
+ # convertir a índices de samples
514
+ start_idx = int((cut_start - frame_start) * sr)
515
+ end_idx = int((cut_end - frame_start) * sr)
516
+
517
+ # print(f'cutting [{str(frame_start)}, {str(frame_end)}] to [{str(float(start_time))}, {str(float(end_time))}] from {str(start_idx)} to {str(end_idx)} of {str(int((frame_end - frame_start) * sr))}')
518
+ cut_samples = samples[:, start_idx:end_idx]
519
+
520
+ # crear nuevo AudioFrame
521
+ new_frame = av.AudioFrame.from_ndarray(cut_samples, format = frame.format, layout = frame.layout)
522
+ new_frame.sample_rate = sr
523
+
524
+ # ajustar PTS → corresponde al inicio real del recorte
525
+ new_pts = int(cut_start / time_base)
526
+ new_frame.pts = new_pts
527
+ new_frame.time_base = time_base
528
+
529
+ return new_frame
yta_video_opengl/t.py ADDED
@@ -0,0 +1,185 @@
1
+ from yta_validation.parameter import ParameterValidator
2
+ from yta_validation import PythonValidator
3
+ from yta_validation.number import NumberValidator
4
+ from quicktions import Fraction
5
+ from typing import Union
6
+
7
+
8
+ class T:
9
+ """
10
+ Class to simplify the way we work with a
11
+ 't' time moment but using the fractions
12
+ library to be precise and avoid any issue
13
+ related with commas.
14
+
15
+ This class must be used when trying to
16
+ apply a specific 't' time moment for a
17
+ video or audio frame, using the fps or
18
+ sample rate as time_base to be precise.
19
+ """
20
+
21
+ @property
22
+ def truncated(
23
+ self
24
+ ) -> Fraction:
25
+ """
26
+ The 't' but as a Fraction that is multiple
27
+ of the given 'time_base' and truncated.
28
+ """
29
+ return round_t(self._t, self.time_base)
30
+
31
+ @property
32
+ def rounded(
33
+ self
34
+ ) -> Fraction:
35
+ """
36
+ The 't' but as a Fraction that is multiple
37
+ of the given 'time_base' and rounded (the
38
+ value could be the same as truncated if it
39
+ is closer to the previou value).
40
+ """
41
+ return round_t(self._t, self.time_base, do_truncate = False)
42
+
43
+ @property
44
+ def truncated_pts(
45
+ self
46
+ ) -> int:
47
+ """
48
+ The 'truncated' value but as a pts, which
49
+ is the int value to be set in audio and
50
+ video frames in the pyav library to be
51
+ displayed in that moment.
52
+ """
53
+ return int(self.truncated / self.time_base)
54
+
55
+ @property
56
+ def rounded_pts(
57
+ self
58
+ ) -> int:
59
+ """
60
+ The 'rounded' value but as a pts, which
61
+ is the int value to be set in audio and
62
+ video frames in the pyav library to be
63
+ displayed in that moment.
64
+ """
65
+ return int(self.rounded / self.time_base)
66
+
67
+ def __init__(
68
+ self,
69
+ t: Union[int, float, Fraction],
70
+ time_base: Fraction
71
+ ):
72
+ ParameterValidator.validate_mandatory_instance_of('t', t, [int, float, 'Fraction'])
73
+ ParameterValidator.validate_mandatory_instance_of('time_base', time_base, 'Fraction')
74
+
75
+ self._t: Union[int, float, Fraction] = t
76
+ """
77
+ The 't' time moment as it was passed as
78
+ parameter.
79
+ """
80
+ self.time_base: Fraction = time_base
81
+ """
82
+ The time_base that will used to round the
83
+ values to be multiples of it.
84
+ """
85
+
86
+ def next(
87
+ self,
88
+ n: int = 1
89
+ ) -> 'T':
90
+ """
91
+ Get the value that is 'n' times ahead of
92
+ the 'truncated' property of this instance.
93
+
94
+ Useful when you need the next value for a
95
+ range in an iteration or similar.
96
+ """
97
+ return T(self.truncated + n * self.time_base, self.time_base)
98
+
99
+ # TODO: Maybe its better to make the '__init__'
100
+ # receive the fps and create the 'from_time_base'
101
+ # because I think we will provide the fps or the
102
+ # sample rate more often
103
+ @staticmethod
104
+ def from_fps(
105
+ t: Union[int, float, Fraction],
106
+ fps: Union[int, float, Fraction]
107
+ ):
108
+ """
109
+ Get the instance but providing the 'fps'
110
+ (or sample rate) value directly.
111
+ """
112
+ return T(t, fps_to_time_base(fps))
113
+
114
+ def get_ts(
115
+ start: Union[int, float, Fraction],
116
+ end: Union[int, float, Fraction],
117
+ fps: Fraction
118
+ ) -> list[Fraction]:
119
+ """
120
+ Get all the 't' time moments between the given
121
+ 'start' and the given 'end', using the provided
122
+ 'time_base' for precision.
123
+
124
+ The 'end' is not included, we return a range
125
+ [start, end) because the last frame is the
126
+ start of another time range.
127
+ """
128
+ start = T.from_fps(start, fps).truncated
129
+ end = T.from_fps(end, fps).truncated
130
+
131
+ time_base = fps_to_time_base(fps)
132
+ return [
133
+ start + i * time_base
134
+ for i in range((end - start) // time_base)
135
+ ]
136
+
137
+ def round_t(
138
+ t: Union[int, float, Fraction],
139
+ time_base = Fraction(1, 60),
140
+ do_truncate: bool = True
141
+ ):
142
+ """
143
+ Round the given 't' time moment to the most
144
+ near multiple of the given 'time_base' (or
145
+ the previous one if 'do_truncate' is True)
146
+ using fractions module to be precise.
147
+
148
+ This method is very useful to truncate 't'
149
+ time moments in order to get the frames or
150
+ samples for the specific and exact time
151
+ moments according to their fps or sample
152
+ rate (that should be passed as the
153
+ 'time_base' parameter).
154
+
155
+ Examples below, with `time_base = 1/5`:
156
+ - `t = 0.25` => `0.2` (truncated or rounded)
157
+ - `t = 0.35` => `0.2` (truncated)
158
+ - `t = 0.45` => `0.4` (truncated or rounded)
159
+ - `t = 0.55` => `0.6` (rounded)
160
+ """
161
+ t = Fraction(t).limit_denominator()
162
+ steps = t / time_base
163
+
164
+ snapped_steps = (
165
+ steps.numerator // steps.denominator
166
+ if do_truncate else
167
+ round(steps) # round(float(steps))
168
+ )
169
+
170
+ return snapped_steps * time_base
171
+
172
+ def fps_to_time_base(
173
+ fps: Union[int, float, Fraction]
174
+ ) -> Fraction:
175
+ """
176
+ Get the pyav time base from the given
177
+ 'fps'.
178
+ """
179
+ return (
180
+ Fraction(1, fps)
181
+ if NumberValidator.is_int(fps) else
182
+ Fraction(1, 1) / fps
183
+ if PythonValidator.is_instance_of(fps, 'Fraction') else
184
+ Fraction(1, 1) / Fraction.from_float(fps).limit_denominator(1000000) # if float
185
+ )
yta_video_opengl/tests.py CHANGED
@@ -586,10 +586,12 @@ def video_modified_stored():
586
586
 
587
587
  video = Video(VIDEO_PATH, 0.25, 0.75)
588
588
  timeline = Timeline()
589
- timeline.add_video(Video(VIDEO_PATH, 0.25, 0.75), 0.5)
589
+ timeline.add_video(Video(VIDEO_PATH, 0.25, 1.0), 0.5)
590
590
  # This is successfully raising an exception
591
591
  #timeline.add_video(Video(VIDEO_PATH, 0.25, 0.75), 0.6)
592
- timeline.add_video(Video(VIDEO_PATH, 0.25, 0.75), 1.5)
592
+ timeline.add_video(Video(VIDEO_PATH, 0.25, 0.75), 1.75)
593
+ timeline.add_video(Video('C:/Users/dania/Downloads/Y2meta.app-TOP 12 SIMPLE LIQUID TRANSITION _ GREEN SCREEN TRANSITION PACK-(1080p60).mp4', 4.0, 5.0), 3)
594
+ # timeline.add_video(Video('C:/Users/dania/Downloads/Y2meta.app-10 Smooth Transitions Green Screen Template For Kinemaster, Alight Motion, Filmora, premiere pro-(1080p).mp4', 2.25, 3.0), 3)
593
595
  timeline.render(OUTPUT_PATH)
594
596
 
595
597
  return