yta-video-frame-time 0.0.7__tar.gz → 0.0.13__tar.gz

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.

Potentially problematic release.


This version of yta-video-frame-time might be problematic. Click here for more details.

@@ -1,7 +1,8 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: yta-video-frame-time
3
- Version: 0.0.7
3
+ Version: 0.0.13
4
4
  Summary: Youtube Autonomous Video Frame Time Module
5
+ License-File: LICENSE
5
6
  Author: danialcala94
6
7
  Author-email: danielalcalavalera@gmail.com
7
8
  Requires-Python: ==3.9
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "yta-video-frame-time"
3
- version = "0.0.7"
3
+ version = "0.0.13"
4
4
  description = "Youtube Autonomous Video Frame Time Module"
5
5
  authors = [
6
6
  {name = "danialcala94",email = "danielalcalavalera@gmail.com"}
@@ -0,0 +1,569 @@
1
+ from yta_validation.parameter import ParameterValidator
2
+ from quicktions import Fraction
3
+ from typing import Union
4
+
5
+
6
+ """
7
+ Tengo un elemento con una duración concreta, por lo que
8
+ el rango es [0, duration):
9
+ - Recortar solo por el principio => 2 segmentos
10
+ - Recortar solo por el final => 2 segmentos
11
+ - Recortar en medio => 3 segmentos
12
+ """
13
+ Number = Union[int, float, Fraction]
14
+ """
15
+ Custom type to represent numbers.
16
+ """
17
+
18
+ """
19
+ TODO: Maybe we can add the possibility of having an
20
+ `fps` value when initializing it to be able to force
21
+ the time interval values to be multiple of `1/fps`.
22
+ But this, if implemented, should be `TimeIntervalFPS`
23
+ or similar, and inheritance from this one but forcing
24
+ the values to be transformed according to that `1/fps`.
25
+ """
26
+ class TimeInterval:
27
+ """
28
+ Class to represent a time interval, which is a tuple
29
+ of time moments representing the time range
30
+ `[start, end)`.
31
+ """
32
+
33
+ @property
34
+ def start_base(
35
+ self
36
+ ) -> float:
37
+ """
38
+ The `start` of the interval but always as 0.
39
+ """
40
+ return 0
41
+
42
+ @property
43
+ def end_base(
44
+ self
45
+ ) -> float:
46
+ """
47
+ The `end` of the interval but adapted to a `start=0`.
48
+ """
49
+ return self.end - self.start
50
+
51
+ @property
52
+ def duration(
53
+ self
54
+ ) -> float:
55
+ """
56
+ The `duration` of the time interval.
57
+ """
58
+ return self.end - self.start
59
+
60
+ @property
61
+ def copy(
62
+ self
63
+ ) -> 'TimeInterval':
64
+ """
65
+ A copy of this instance.
66
+ """
67
+ return TimeInterval(
68
+ start = self.start,
69
+ end = self.end
70
+ )
71
+
72
+ @property
73
+ def as_tuple(
74
+ self
75
+ ) -> tuple[float, float]:
76
+ """
77
+ The time interval but as a `(start, end)` tuple.
78
+ """
79
+ return (self.start, self.end)
80
+
81
+ def __init__(
82
+ self,
83
+ start: Number,
84
+ end: Number,
85
+ ):
86
+ """
87
+ Provide the interval as it actually is, with the `start`
88
+ and `end`. These values will be adjusted to an internal
89
+ interval starting on 0.
90
+
91
+ The `end` value must be greater than the `start` value.
92
+ """
93
+ if start > end:
94
+ raise Exception('The `start` value provided is greater than the `end` value provided.')
95
+
96
+ if start == end:
97
+ raise Exception('The `start` value provided is exactly the `end` value provided.')
98
+
99
+ self.start: float = start
100
+ """
101
+ The original `start` of the time segment.
102
+ """
103
+ self.end: float = end
104
+ """
105
+ The original `end` of the time segment.
106
+ """
107
+
108
+ def _validate_t(
109
+ self,
110
+ t: Number,
111
+ do_include_start: bool = False,
112
+ do_include_end: bool = False
113
+ ) -> None:
114
+ """
115
+ Validate that the provided `t` value is between the `start`
116
+ and the `end` parameters provided, including them or not
117
+ according to the boolean parameters provided.
118
+ """
119
+ ParameterValidator.validate_mandatory_number_between(
120
+ name = 't',
121
+ value = t,
122
+ lower_limit = self.start,
123
+ upper_limit = self.end,
124
+ do_include_lower_limit = do_include_start,
125
+ do_include_upper_limit = do_include_end
126
+ )
127
+
128
+ def _cut(
129
+ self,
130
+ start: Number,
131
+ end: Number
132
+ ) -> tuple[Union['TimeInterval', None], Union['TimeInterval', None], Union['TimeInterval', None]]:
133
+ """
134
+ *For internal use only*
135
+
136
+ Cut a segment with the given `start` and `end` time moments.
137
+
138
+ This method will return a tuple of 3 elements including the
139
+ segments created by cutting this time interval. The tuple
140
+ will include all the segments at the begining and the rest
141
+ will be None.
142
+
143
+ Examples below:
144
+ - A time interval of `[2, 5)` cut with `start=3` and `end=4`
145
+ will generate `((2, 3), (3, 4), (4, 5))`.
146
+ - A time interval of `[2, 5)` cut with `start=2` and `end=4`
147
+ will generate `((2, 4), (4, 5), None)`.
148
+ - A time interval of `[2, 5)` cut with `start=4` and `end=5`
149
+ will generate `((2, 4), (4, 5), None)`.
150
+ - A time interval of `[2, 5)` cut with `start=2` and `end=5`
151
+ will generate `((2, 5), None, None)`.
152
+
153
+ As you can see, the result could be the same in different
154
+ situations, but it's up to you (and the specific method in
155
+ which you are calling to this one) to choose the tuple you
156
+ want to return.
157
+
158
+ (!) This will not modify the original instance.
159
+ """
160
+ self._validate_t(start, do_include_start = True)
161
+ self._validate_t(end, do_include_end = True)
162
+
163
+ return (
164
+ (
165
+ self.copy,
166
+ None,
167
+ None
168
+ )
169
+ if (
170
+ start == self.start and
171
+ end == self.end
172
+ ) else
173
+ (
174
+ TimeInterval(
175
+ start = self.start,
176
+ end = end
177
+ ),
178
+ TimeInterval(
179
+ start = end,
180
+ end = self.end
181
+ ),
182
+ None
183
+ )
184
+ if start == self.start else
185
+ (
186
+ TimeInterval(
187
+ start = self.start,
188
+ end = start
189
+ ),
190
+ TimeInterval(
191
+ start = start,
192
+ end = self.end
193
+ ),
194
+ None
195
+ )
196
+ if end == self.end else
197
+ (
198
+ TimeInterval(
199
+ start = self.start,
200
+ end = start
201
+ ),
202
+ TimeInterval(
203
+ start = start,
204
+ end = end
205
+ ),
206
+ TimeInterval(
207
+ start = end,
208
+ end = self.end
209
+ )
210
+ )
211
+ )
212
+
213
+ def cut_from_start_to(
214
+ self,
215
+ t: Number,
216
+ do_get_cut: bool = False
217
+ ) -> Union['TimeInterval', None]:
218
+ """
219
+ Cut the interval from the start to the `t` value
220
+ provided. The return will be the segment cut if
221
+ `do_get_cut` is False, or the remaining part if
222
+ True.
223
+
224
+ (!) This will not modify the original instance.
225
+ """
226
+ intervals = self._cut(
227
+ start = self.start,
228
+ end = t
229
+ )
230
+
231
+ return (
232
+ intervals[0]
233
+ if (
234
+ len(intervals) == 1 or
235
+ do_get_cut
236
+ ) else
237
+ intervals[1]
238
+ )
239
+
240
+ def cut_to_end_from(
241
+ self,
242
+ t: Number,
243
+ do_get_cut: bool = False
244
+ ) -> Union['TimeInterval', None]:
245
+ """
246
+ Cut the interval from the start to the `t` value
247
+ provided. The return will be the segment cut if
248
+ `do_get_cut` is False, or the remaining part if
249
+ True.
250
+
251
+ (!) This will not modify the original instance.
252
+ """
253
+ intervals = self._cut(
254
+ start = t,
255
+ end = self.end
256
+ )
257
+
258
+ return (
259
+ intervals[0]
260
+ if (
261
+ len(intervals) == 1 or
262
+ not do_get_cut
263
+ ) else
264
+ intervals[1]
265
+ )
266
+
267
+ def cut_from_to(
268
+ self,
269
+ from_t: Number,
270
+ to_t: Number,
271
+ # do_get_cut: bool = False
272
+ ) -> Union['TimeInterval', None]:
273
+ """
274
+ Cut the interval from the `from_t` value provided
275
+ to the `to_t` value given. The return will be the
276
+ segment cut.
277
+
278
+ (!) This will not modify the original instance.
279
+
280
+ TODO: By now we are only getting the cut
281
+ """
282
+ intervals = self._cut(
283
+ start = from_t,
284
+ end = to_t
285
+ )
286
+
287
+ return (
288
+ intervals[0]
289
+ if (
290
+ len(intervals) == 1 or
291
+ (
292
+ len(intervals) == 2 and
293
+ from_t == self.start
294
+ )
295
+ ) else
296
+ intervals[1]
297
+ )
298
+
299
+ def is_t_included(
300
+ self,
301
+ t: float,
302
+ do_include_end: bool = False
303
+ ) -> bool:
304
+ """
305
+ Check if the `t` time moment provided is included in
306
+ this time interval, including the `end` only if the
307
+ `do_include_end` parameter is set as `True`.
308
+ """
309
+ return TimeIntervalUtils.a_includes_t(
310
+ t = t,
311
+ time_interval_a = self,
312
+ do_include_end = do_include_end
313
+ )
314
+
315
+ def is_adjacent_to(
316
+ self,
317
+ time_interval: 'TimeInterval'
318
+ ) -> bool:
319
+ """
320
+ Check if the `time_interval` provided is adjacent
321
+ to this time interval, which means that the `end`
322
+ of one interval is also the `start` of the other
323
+ one.
324
+
325
+ (!) Giving the time intervals inverted will
326
+ provide the same result.
327
+
328
+ Example below:
329
+ - `a=[2, 5)` and `b=[5, 7)` => `True`
330
+ - `a=[5, 7)` and `b=[2, 5)` => `True`
331
+ - `a=[2, 5)` and `b=[3, 4)` => `False`
332
+ - `a=[2, 5)` and `b=[6, 8)` => `False`
333
+ """
334
+ return TimeIntervalUtils.a_is_adjacent_to_b(
335
+ time_interval_a = self,
336
+ time_interval_b = time_interval
337
+ )
338
+
339
+ def do_contains_a(
340
+ self,
341
+ time_interval: 'TimeInterval'
342
+ ) -> bool:
343
+ """
344
+ Check if this time interval includes the `time_interval`
345
+ provided or not, which means that the `time_interval`
346
+ provided is fully contained (included) in this one.
347
+ """
348
+ return TimeIntervalUtils.a_contains_b(
349
+ time_interval_a = self,
350
+ time_interval_b = time_interval
351
+ )
352
+
353
+ def is_contained_in(
354
+ self,
355
+ time_interval: 'TimeInterval'
356
+ ) -> bool:
357
+ """
358
+ Check if this time interval is fully contained in
359
+ the `time_interval` provided, which is a synonim
360
+ of being fully overlapped by that `time_interval`.
361
+ """
362
+ return TimeIntervalUtils.a_is_contained_in_b(
363
+ time_interval_a = self,
364
+ time_interval_b = time_interval
365
+ )
366
+
367
+ def do_intersects_with(
368
+ self,
369
+ time_interval: 'TimeInterval'
370
+ ) -> bool:
371
+ """
372
+ Check if this time interval intersects with the one
373
+ provided as `time_interval`, which means that they
374
+ have at least a part in common.
375
+ """
376
+ return TimeIntervalUtils.a_intersects_with_b(
377
+ time_interval_a = self,
378
+ time_interval_b = time_interval
379
+ )
380
+
381
+ def get_intersection_with_a(
382
+ self,
383
+ time_interval: 'TimeInterval'
384
+ ) -> Union['TimeInterval', None]:
385
+ """
386
+ Get the time interval that intersects this one and the
387
+ one provided as `time_interval`. The result can be `None`
388
+ if there is no intersection in between both.
389
+ """
390
+ return TimeIntervalUtils.get_intersection_of_a_and_b(
391
+ time_interval_a = self,
392
+ time_interval_b = time_interval
393
+ )
394
+
395
+
396
+ class TimeIntervalUtils:
397
+ """
398
+ Static class to wrap the utils related to time intervals.
399
+ """
400
+
401
+ @staticmethod
402
+ def a_includes_t(
403
+ t: float,
404
+ time_interval_a: 'TimeInterval',
405
+ do_include_end: bool = False
406
+ ) -> bool:
407
+ """
408
+ Check if the `t` time moment provided is included in
409
+ the `time_interval_a` given. The `time_interval_a.end`
410
+ is excluded unless the `do_include_end` parameter is
411
+ set as `True`.
412
+
413
+ A time interval is `[start, end)`, thats why the end is
414
+ excluded by default.
415
+ """
416
+ return (
417
+ time_interval_a.start <= t <= time_interval_a.end
418
+ if do_include_end else
419
+ time_interval_a.start <= t < time_interval_a.end
420
+ )
421
+
422
+ @staticmethod
423
+ def a_is_adjacent_to_b(
424
+ time_interval_a: 'TimeInterval',
425
+ time_interval_b: 'TimeInterval',
426
+ ) -> bool:
427
+ """
428
+ Check if the `time_interval_a` provided and the
429
+ also given `time_interval_b` are adjacent, which
430
+ means that the `end` of one interval is also the
431
+ `start` of the other one.
432
+
433
+ (!) Giving the time intervals inverted will
434
+ provide the same result.
435
+
436
+ Examples below:
437
+ - `a=[2, 5)` and `b=[5, 7)` => `True`
438
+ - `a=[5, 7)` and `b=[2, 5)` => `True`
439
+ - `a=[2, 5)` and `b=[3, 4)` => `False`
440
+ - `a=[2, 5)` and `b=[6, 8)` => `False`
441
+ """
442
+ return (
443
+ TimeIntervalUtils.a_is_inmediately_before_b(time_interval_a, time_interval_b) or
444
+ TimeIntervalUtils.a_is_inmediately_after_b(time_interval_a, time_interval_b)
445
+ )
446
+
447
+ @staticmethod
448
+ def a_is_inmediately_before_b(
449
+ time_interval_a: 'TimeInterval',
450
+ time_interval_b: 'TimeInterval',
451
+ ) -> bool:
452
+ """
453
+ Check if the `time_interval_a` provided is inmediately
454
+ before the also given `time_interval_b`, which means
455
+ that the `end` of the first one is also the `start` of
456
+ the second one.
457
+
458
+ Examples below:
459
+ - `a=[2, 5)` and `b=[5, 7)` => `True`
460
+ - `a=[5, 7)` and `b=[2, 5)` => `False`
461
+ - `a=[2, 5)` and `b=[3, 4)` => `False`
462
+ - `a=[2, 5)` and `b=[6, 8)` => `False`
463
+ """
464
+ return time_interval_a.end == time_interval_b.start
465
+
466
+ @staticmethod
467
+ def a_is_inmediately_after_b(
468
+ time_interval_a: 'TimeInterval',
469
+ time_interval_b: 'TimeInterval',
470
+ ) -> bool:
471
+ """
472
+ Check if the `time_interval_a` provided is inmediately
473
+ after the also given `time_interval_b`, which means
474
+ that the `start` of the first one is also the `end` of
475
+ the second one.
476
+
477
+ Examples below:
478
+ - `a=[2, 5)` and `b=[5, 7)` => `False`
479
+ - `a=[5, 7)` and `b=[2, 5)` => `True`
480
+ - `a=[2, 5)` and `b=[3, 4)` => `False`
481
+ - `a=[2, 5)` and `b=[6, 8)` => `False`
482
+ """
483
+ return time_interval_a.start == time_interval_b.end
484
+
485
+ @staticmethod
486
+ def a_contains_b(
487
+ time_interval_a: 'TimeInterval',
488
+ time_interval_b: 'TimeInterval'
489
+ ) -> bool:
490
+ """
491
+ Check if the `time_interval_a` time interval provided
492
+ includes the `time_interval_b` or not, which means that
493
+ the `time_interval_b` is fully contained in the first
494
+ one.
495
+
496
+ Examples below:
497
+ - `a=[2, 5)` and `b=[3, 4)` => `True`
498
+ - `a=[2, 5)` and `b=[2, 4)` => `True`
499
+ - `a=[2, 5)` and `b=[3, 6)` => `False`
500
+ - `a=[2, 5)` and `b=[6, 8)` => `False`
501
+ """
502
+ return (
503
+ time_interval_a.start <= time_interval_b.start and
504
+ time_interval_a.end >= time_interval_b.end
505
+ )
506
+
507
+ @staticmethod
508
+ def a_is_contained_in_b(
509
+ time_interval_a: 'TimeInterval',
510
+ time_interval_b: 'TimeInterval',
511
+ ) -> bool:
512
+ """
513
+ Check if the `time_interval_a` provided is fully
514
+ contained into the also provided `time_interval_b`.
515
+
516
+ Examples below:
517
+ - `a=[2, 5)` and `b=[1, 6)` => `True`
518
+ - `a=[2, 5)` and `b=[0, 9)` => `True`
519
+ - `a=[2, 5)` and `b=[2, 4)` => `False`
520
+ - `a=[2, 5)` and `b=[4, 8)` => `False`
521
+ - `a=[2, 5)` and `b=[7, 8)` => `False`
522
+ """
523
+ return TimeIntervalUtils.a_contains_b(
524
+ time_interval_a = time_interval_b,
525
+ time_interval_b = time_interval_a
526
+ )
527
+
528
+ @staticmethod
529
+ def a_intersects_with_b(
530
+ time_interval_a: 'TimeInterval',
531
+ time_interval_b: 'TimeInterval',
532
+ ) -> bool:
533
+ """
534
+ Check if the `time_interval_a` and the `time_interval_b`
535
+ provided has at least a part in common.
536
+
537
+ Examples below:
538
+ - `a=[2, 5)` and `b=[4, 6)` => `True`
539
+ - `a=[2, 5)` and `b=[1, 3)` => `True`
540
+ - `a=[2, 5)` and `b=[5, 6)` => `False`
541
+ - `a=[2, 5)` and `b=[7, 8)` => `False`
542
+ - `a=[2, 5)` and `b=[1, 2)` => `False`
543
+ """
544
+ return (
545
+ time_interval_b.start < time_interval_a.end and
546
+ time_interval_a.start < time_interval_b.end
547
+ )
548
+
549
+ @staticmethod
550
+ def get_intersection_of_a_and_b(
551
+ time_interval_a: 'TimeInterval',
552
+ time_interval_b: 'TimeInterval'
553
+ ) -> Union['TimeInterval', None]:
554
+ """
555
+ Get the time interval that intersects the two time
556
+ intervals provided, that can be `None` if there is no
557
+ intersection in between both.
558
+ """
559
+ return (
560
+ None
561
+ if not TimeIntervalUtils.a_intersects_with_b(
562
+ time_interval_a = time_interval_a,
563
+ time_interval_b = time_interval_b
564
+ ) else
565
+ TimeInterval(
566
+ start = max(time_interval_a.start, time_interval_b.start),
567
+ end = min(time_interval_a.end, time_interval_b.end)
568
+ )
569
+ )
@@ -238,6 +238,8 @@ def round_pts(
238
238
  and 'time_base', but here is an easier
239
239
  example using the time moments.
240
240
 
241
+ (!) This is valid only for video.
242
+
241
243
  Examples below, with `time_base = 1/5`:
242
244
  - `t = 0.25` => `0.2` (truncated or rounded)
243
245
  - `t = 0.35` => `0.2` (truncated)
@@ -256,8 +258,6 @@ def round_pts(
256
258
 
257
259
  return int(frame_index * ticks_per_frame)
258
260
 
259
- # TODO: Create a 'round_pts'
260
-
261
261
  """
262
262
  When we are working with the 't' time
263
263
  moment we need to use the fps, and when
@@ -306,6 +306,18 @@ class _T:
306
306
  The formula:
307
307
  - `pts * time_base`
308
308
  """
309
+ t = Fraction(pts * self._t_handler.time_base)
310
+
311
+ return (
312
+ self._t_handler.t.truncated(t)
313
+ if do_truncate is True else
314
+ self._t_handler.t.rounded(t)
315
+ if do_truncate is False else
316
+ t # if None
317
+ )
318
+
319
+ # TODO: Remove this below in the next
320
+ # commit
309
321
  pts = (
310
322
  self._t_handler.pts.truncated(pts)
311
323
  if do_truncate is True else
@@ -342,7 +354,7 @@ class _T:
342
354
  variation.
343
355
 
344
356
  The formula:
345
- - `int(t * fps)`
357
+ - `int(round(t * fps))`
346
358
  """
347
359
  t = (
348
360
  self.truncated(t)
@@ -370,7 +382,7 @@ class _T:
370
382
  def truncated(
371
383
  self,
372
384
  t: Union[int, float, Fraction]
373
- ):
385
+ ) -> Fraction:
374
386
  """
375
387
  Get the 't' value provided but truncated.
376
388
 
@@ -380,10 +392,26 @@ class _T:
380
392
  """
381
393
  return round_t(t, Fraction(1, self._t_handler.fps), do_truncate = True)
382
394
 
395
+ def is_truncated(
396
+ self,
397
+ t: Union[int, float, Fraction]
398
+ ) -> bool:
399
+ """
400
+ Check if the `t` value provided is the truncated
401
+ value, which means that the `t` provided is the
402
+ `start` from the `[start, end)` range defined by
403
+ the fps.
404
+ """
405
+ return check_values_are_same(
406
+ value_a = t,
407
+ value_b = self.truncated(t),
408
+ tolerance = 0.000001
409
+ )
410
+
383
411
  def rounded(
384
412
  self,
385
413
  t: Union[int, float, Fraction]
386
- ):
414
+ ) -> Fraction:
387
415
  """
388
416
  Get the 't' value provided but rounded.
389
417
 
@@ -530,7 +558,7 @@ class _Pts:
530
558
  variation.
531
559
 
532
560
  The formula:
533
- - `int((pts * time_base) * fps)`
561
+ - `int(round((pts * time_base) * fps))`
534
562
  """
535
563
  return self._t_handler.t.to_index(
536
564
  self.to_t(pts, do_truncate = None),
@@ -552,12 +580,31 @@ class _Pts:
552
580
  t = self._t_handler.t.from_index(index),
553
581
  do_truncate = True
554
582
  )
583
+
584
+ """
585
+ These 2 methods below are here because they
586
+ seem to work for videos, but I think they
587
+ could work not if the video has dynamic frame
588
+ rate or in some other situations, thats why
589
+ this is here as a reminder.
590
+
591
+ I found one video that had audio_fps=44100
592
+ and time_base=256/11025, so it was impossible
593
+ to make a conversion using this formula with
594
+ the audio. With video seems to be ok, but...
595
+
596
+ Use these methods below at your own risk.
597
+ """
555
598
 
556
599
  def truncated(
557
600
  self,
558
601
  pts: int
559
602
  ):
560
603
  """
604
+ (!) This is valid only for video and/or
605
+ could work not properly. Use it at your
606
+ own risk.
607
+
561
608
  Get the 'pts' value provided but truncated.
562
609
 
563
610
  This means that if 't' is in a
@@ -576,6 +623,10 @@ class _Pts:
576
623
  pts: int
577
624
  ) -> int:
578
625
  """
626
+ (!) This is valid only for video and/or
627
+ could work not properly. Use it at your
628
+ own risk.
629
+
579
630
  Get the 'pts' value provided but rounded.
580
631
 
581
632
  This means that if 't' is in a
@@ -598,6 +649,10 @@ class _Pts:
598
649
  do_truncate: bool = True
599
650
  ) -> int:
600
651
  """
652
+ (!) This is valid only for video and/or
653
+ could work not properly. Use it at your
654
+ own risk.
655
+
601
656
  Get the value that is 'n' times ahead of
602
657
  the 'pts' value provided (truncated or
603
658
  rounded according to the 'do_truncate'
@@ -624,6 +679,10 @@ class _Pts:
624
679
  do_truncate: bool = True
625
680
  ) -> int:
626
681
  """
682
+ (!) This is valid only for video and/or
683
+ could work not properly. Use it at your
684
+ own risk.
685
+
627
686
  Get the value that is 'n' times before
628
687
  the 't' property of this instance
629
688
  (truncated or rounded according to the
@@ -647,7 +706,7 @@ class _Pts:
647
706
  )
648
707
 
649
708
  return pts - n * get_ticks_per_frame(self._t_handler.fps, self._t_handler.time_base)
650
-
709
+
651
710
  class THandler:
652
711
  """
653
712
  Class to simplify the way we work with
@@ -715,9 +774,9 @@ def frame_t_to_index(
715
774
  also provided 'fps'.
716
775
 
717
776
  The formula:
718
- - `int(t * fps)`
777
+ - `int(round(t * fps))`
719
778
  """
720
- return int(parse_fraction(t) * fps)
779
+ return int(round(t * fps))
721
780
 
722
781
  def frame_index_to_t(
723
782
  index: int,
@@ -745,6 +804,8 @@ def frame_t_to_pts(
745
804
  moment provided, based on the also provided
746
805
  'fps' and 'time_base'.
747
806
 
807
+ (!) This is valid only for videos.
808
+
748
809
  The formula:
749
810
  - `frame_index * ticks_per_frame`
750
811
  """
@@ -764,6 +825,23 @@ def frame_pts_to_t(
764
825
  - `pts * time_base`
765
826
  """
766
827
  return parse_fraction(pts * time_base)
828
+
829
+ def get_audio_frame_duration(
830
+ samples: int,
831
+ audio_fps: Fraction
832
+ ) -> Fraction:
833
+ """
834
+ Get the audio frame duration by giving the
835
+ number of '.samples' and also the rate (that
836
+ we call 'audio_fps').
837
+
838
+ This is useful when trying to guess the next
839
+ pts or t.
840
+
841
+ The formula:
842
+ - `samples / audio_fps`
843
+ """
844
+ return Fraction(samples / audio_fps)
767
845
 
768
846
  def get_ticks_per_frame(
769
847
  fps: Union[float, int, Fraction],
@@ -820,3 +898,17 @@ def parse_fraction(
820
898
 
821
899
  return fraction
822
900
 
901
+ def check_values_are_same(
902
+ value_a: Union[Fraction, float],
903
+ value_b: Union[Fraction, float],
904
+ tolerance: float = 1e-6
905
+ ) -> bool:
906
+ """
907
+ Check that the `value_a` and the `value_b` are the same
908
+ by applying the `tolerance` value, that is 0.000001 by
909
+ default.
910
+
911
+ For example, `0.016666666666666666` is the same value as
912
+ `1/60` with `tolerance=0.000001`.
913
+ """
914
+ return abs(float(value_a) - float(value_b)) < tolerance