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.
- yta_video_opengl/complete/timeline.py +143 -51
- yta_video_opengl/complete/track.py +286 -19
- yta_video_opengl/complete/video_on_track.py +50 -1
- yta_video_opengl/reader/__init__.py +180 -42
- yta_video_opengl/reader/cache.py +221 -17
- yta_video_opengl/utils.py +140 -1
- yta_video_opengl/writer.py +13 -3
- {yta_video_opengl-0.0.11.dist-info → yta_video_opengl-0.0.12.dist-info}/METADATA +1 -1
- yta_video_opengl-0.0.12.dist-info/RECORD +20 -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.12.dist-info}/LICENSE +0 -0
- {yta_video_opengl-0.0.11.dist-info → yta_video_opengl-0.0.12.dist-info}/WHEEL +0 -0
@@ -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(
|
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] = (
|
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] = [
|
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
|
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
|
-
) ->
|
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
|
-
|
87
|
-
|
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 =
|
153
|
-
size =
|
191
|
+
fps = self.fps,
|
192
|
+
size = self.size,
|
154
193
|
pixel_format = 'yuv420p'
|
155
194
|
)
|
156
195
|
|
157
|
-
|
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.
|
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
|
249
|
+
def _get_part_at_t(
|
64
250
|
self,
|
65
251
|
t: float
|
66
|
-
) ->
|
252
|
+
) -> _Part:
|
67
253
|
"""
|
68
|
-
Get the
|
69
|
-
|
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
|
72
|
-
if
|
73
|
-
return
|
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
|
-
) ->
|
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
|
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
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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.
|
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
|