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.
- yta_video_opengl/complete/timeline.py +147 -59
- yta_video_opengl/complete/track.py +302 -27
- yta_video_opengl/complete/video_on_track.py +72 -9
- yta_video_opengl/reader/__init__.py +190 -89
- yta_video_opengl/reader/cache.py +258 -32
- yta_video_opengl/t.py +185 -0
- yta_video_opengl/tests.py +4 -2
- yta_video_opengl/utils.py +169 -8
- yta_video_opengl/video.py +85 -12
- yta_video_opengl/writer.py +23 -14
- {yta_video_opengl-0.0.11.dist-info → yta_video_opengl-0.0.13.dist-info}/METADATA +2 -1
- yta_video_opengl-0.0.13.dist-info/RECORD +21 -0
- yta_video_opengl-0.0.11.dist-info/RECORD +0 -20
- {yta_video_opengl-0.0.11.dist-info → yta_video_opengl-0.0.13.dist-info}/LICENSE +0 -0
- {yta_video_opengl-0.0.11.dist-info → yta_video_opengl-0.0.13.dist-info}/WHEEL +0 -0
yta_video_opengl/reader/cache.py
CHANGED
@@ -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
|
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
|
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.
|
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
|
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
|
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.
|
198
|
+
key_frame_pts = self._get_nearest_keyframe_pts(pts)
|
186
199
|
|
187
200
|
# Go to the key frame that includes it
|
188
|
-
|
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.
|
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.
|
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
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
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 =
|
317
|
+
start = T(start, self.time_base).truncated_pts
|
261
318
|
end = (
|
262
|
-
|
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.
|
323
|
+
key_frame_pts = self._get_nearest_keyframe_pts(start)
|
267
324
|
|
268
|
-
# Go to the
|
269
|
-
|
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
|
-
|
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
|
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
|
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.
|
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
|