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
@@ -1,11 +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
|
15
|
+
from yta_video_opengl.t import get_ts, fps_to_time_base, T
|
3
16
|
from yta_validation.parameter import ParameterValidator
|
17
|
+
from quicktions import Fraction
|
4
18
|
from typing import Union
|
5
|
-
from fractions import Fraction
|
6
|
-
|
7
|
-
import numpy as np
|
8
|
-
import av
|
9
19
|
|
10
20
|
|
11
21
|
class Timeline:
|
@@ -18,38 +28,63 @@ class Timeline:
|
|
18
28
|
@property
|
19
29
|
def end(
|
20
30
|
self
|
21
|
-
) ->
|
31
|
+
) -> Fraction:
|
22
32
|
"""
|
23
33
|
The end of the last video of the track
|
24
34
|
that lasts longer. This is the last time
|
25
35
|
moment that has to be rendered.
|
26
36
|
"""
|
27
|
-
return max(
|
37
|
+
return max(
|
38
|
+
track.end
|
39
|
+
for track in self.tracks
|
40
|
+
)
|
28
41
|
|
29
42
|
def __init__(
|
30
43
|
self,
|
31
|
-
size: tuple[int, int] = (
|
32
|
-
fps: float = 60.0
|
44
|
+
size: tuple[int, int] = (1_920, 1_080),
|
45
|
+
fps: Union[int, float, Fraction] = 60.0,
|
46
|
+
audio_fps: Union[int, Fraction] = 44_100.0, # 48_000.0 for aac
|
47
|
+
# TODO: I don't like this name
|
48
|
+
# TODO: Where does this come from (?)
|
49
|
+
audio_samples_per_frame: int = 1024
|
33
50
|
):
|
34
51
|
# TODO: By now we are using just two video
|
35
52
|
# tracks to test the composition
|
36
53
|
# TODO: We need to be careful with the
|
37
54
|
# priority, by now its defined by its
|
38
55
|
# position in the array
|
39
|
-
self.tracks: list[Track] = [
|
56
|
+
self.tracks: list[Track] = [
|
57
|
+
Track(
|
58
|
+
size = size,
|
59
|
+
fps = fps,
|
60
|
+
audio_fps = audio_fps,
|
61
|
+
# TODO: I need more info about the audio
|
62
|
+
# I think
|
63
|
+
audio_samples_per_frame = audio_samples_per_frame
|
64
|
+
),
|
65
|
+
Track(
|
66
|
+
size = size,
|
67
|
+
fps = fps,
|
68
|
+
audio_fps = audio_fps,
|
69
|
+
# TODO: I need more info about the audio
|
70
|
+
# I think
|
71
|
+
audio_samples_per_frame = audio_samples_per_frame
|
72
|
+
)
|
73
|
+
]
|
40
74
|
"""
|
41
75
|
All the video tracks we are handling.
|
42
76
|
"""
|
43
|
-
# TODO: Handle
|
77
|
+
# TODO: Handle the other properties
|
44
78
|
self.size = size
|
45
79
|
self.fps = fps
|
80
|
+
self.audio_fps = audio_fps
|
46
81
|
|
47
82
|
# TODO: Create 'add_track' method, but by now
|
48
83
|
# we hare handling only one
|
49
84
|
def add_video(
|
50
85
|
self,
|
51
86
|
video: Video,
|
52
|
-
t: float,
|
87
|
+
t: Union[int, float, Fraction],
|
53
88
|
# TODO: This is for testing, it has to
|
54
89
|
# disappear
|
55
90
|
do_use_second_track: bool = False
|
@@ -61,19 +96,23 @@ class Timeline:
|
|
61
96
|
TODO: The 'do_use_second_track' parameter
|
62
97
|
is temporary.
|
63
98
|
"""
|
99
|
+
# TODO: This is temporary logic by now
|
100
|
+
# just to be able to test mixing frames
|
101
|
+
# from 2 different tracks at the same
|
102
|
+
# time
|
64
103
|
index = 1 * do_use_second_track
|
65
104
|
|
66
105
|
self.tracks[index].add_video(video, t)
|
67
106
|
|
68
107
|
return self
|
69
|
-
|
108
|
+
|
70
109
|
# TODO: This method is not for the Track but
|
71
110
|
# for the timeline, as one track can only
|
72
111
|
# have consecutive elements
|
73
112
|
def get_frame_at(
|
74
113
|
self,
|
75
|
-
t: float
|
76
|
-
) ->
|
114
|
+
t: Union[int, float, Fraction]
|
115
|
+
) -> 'VideoFrame':
|
77
116
|
"""
|
78
117
|
Get all the frames that are played at the
|
79
118
|
't' time provided, but combined in one.
|
@@ -82,29 +121,43 @@ class Timeline:
|
|
82
121
|
track.get_frame_at(t)
|
83
122
|
for track in self.tracks
|
84
123
|
)
|
124
|
+
# TODO: Here I receive black frames because
|
125
|
+
# it was empty, but I don't have a way to
|
126
|
+
# detect those black empty frames because
|
127
|
+
# they are just VideoFrame instances... I
|
128
|
+
# need a way to know so I can skip them if
|
129
|
+
# other frame in other track, or to know if
|
130
|
+
# I want them as transparent or something
|
85
131
|
|
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
|
-
)
|
132
|
+
# TODO: Combinate them, I send first by now
|
133
|
+
return next(frames)
|
102
134
|
|
135
|
+
def get_audio_frames_at(
|
136
|
+
self,
|
137
|
+
t: float
|
138
|
+
):
|
139
|
+
# TODO: What if the different audio streams
|
140
|
+
# have also different fps (?)
|
141
|
+
frames = []
|
142
|
+
for track in self.tracks:
|
143
|
+
# TODO: Make this work properly
|
144
|
+
audio_frames = track.get_audio_frames_at(t)
|
145
|
+
|
146
|
+
# TODO: Combine them
|
147
|
+
if audio_frames is not None:
|
148
|
+
frames = audio_frames
|
149
|
+
break
|
150
|
+
|
151
|
+
#from yta_video_opengl.utils import get_silent_audio_frame
|
152
|
+
#make_silent_audio_frame()
|
153
|
+
for frame in frames:
|
154
|
+
yield frame
|
155
|
+
|
103
156
|
def render(
|
104
157
|
self,
|
105
158
|
filename: str,
|
106
|
-
start: float = 0.0,
|
107
|
-
end: Union[float, None] = None
|
159
|
+
start: Union[int, float, Fraction] = 0.0,
|
160
|
+
end: Union[int, float, Fraction, None] = None
|
108
161
|
) -> 'Timeline':
|
109
162
|
"""
|
110
163
|
Render the time range in between the given
|
@@ -115,8 +168,10 @@ class Timeline:
|
|
115
168
|
project will be rendered.
|
116
169
|
"""
|
117
170
|
ParameterValidator.validate_mandatory_string('filename', filename, do_accept_empty = False)
|
118
|
-
|
119
|
-
ParameterValidator.
|
171
|
+
# TODO: We need to accept Fraction as number
|
172
|
+
#ParameterValidator.validate_mandatory_positive_number('start', start, do_include_zero = True)
|
173
|
+
# TODO: We need to accept Fraction as number
|
174
|
+
#ParameterValidator.validate_positive_number('end', end, do_include_zero = False)
|
120
175
|
|
121
176
|
# TODO: Limitate 'end' a bit...
|
122
177
|
end = (
|
@@ -127,20 +182,6 @@ class Timeline:
|
|
127
182
|
|
128
183
|
if start >= end:
|
129
184
|
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
185
|
|
145
186
|
from yta_video_opengl.writer import VideoWriter
|
146
187
|
|
@@ -149,31 +190,78 @@ class Timeline:
|
|
149
190
|
# video we are writing
|
150
191
|
writer.set_video_stream(
|
151
192
|
codec_name = 'h264',
|
152
|
-
fps =
|
153
|
-
size =
|
193
|
+
fps = self.fps,
|
194
|
+
size = self.size,
|
154
195
|
pixel_format = 'yuv420p'
|
155
196
|
)
|
156
197
|
|
157
|
-
|
198
|
+
writer.set_audio_stream(
|
199
|
+
codec_name = 'aac',
|
200
|
+
fps = self.audio_fps
|
201
|
+
)
|
202
|
+
|
203
|
+
time_base = fps_to_time_base(self.fps)
|
204
|
+
audio_time_base = fps_to_time_base(self.audio_fps)
|
205
|
+
|
206
|
+
"""
|
207
|
+
We are trying to render this:
|
208
|
+
-----------------------------
|
209
|
+
[0 a 0.5) => Frames negros
|
210
|
+
[0.5 a 1.25) => [0.25 a 1.0) de Video1
|
211
|
+
[1.25 a 1.75) => Frames negros
|
212
|
+
[1.75 a 2.25) => [0.25 a 0.75) de Video1
|
213
|
+
[2.25 a 3.0) => Frames negros
|
214
|
+
[3.0 a 3.75) => [2.25 a 3.0) de Video2
|
215
|
+
"""
|
216
|
+
|
217
|
+
audio_pts = 0
|
218
|
+
for t in get_ts(start, end, self.fps):
|
158
219
|
frame = self.get_frame_at(t)
|
159
220
|
|
160
|
-
|
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
|
-
)
|
221
|
+
#print(frame)
|
166
222
|
|
167
223
|
# We need to adjust our output elements to be
|
168
224
|
# consecutive and with the right values
|
169
225
|
# TODO: We are using int() for fps but its float...
|
170
|
-
frame.time_base =
|
171
|
-
frame.pts = int(
|
226
|
+
frame.time_base = time_base
|
227
|
+
#frame.pts = int(video_frame_index / frame.time_base)
|
228
|
+
frame.pts = T(t, time_base).truncated_pts
|
172
229
|
|
173
230
|
# TODO: We need to handle the audio
|
174
231
|
writer.mux_video_frame(
|
175
232
|
frame = frame
|
176
233
|
)
|
177
234
|
|
235
|
+
#print(f' [VIDEO] Here in t:{str(t)} -> pts:{str(frame.pts)} - dts:{str(frame.dts)}')
|
236
|
+
|
237
|
+
# TODO: Uncomment all this below for the audio
|
238
|
+
num_of_audio_frames = 0
|
239
|
+
for audio_frame in self.get_audio_frames_at(t):
|
240
|
+
# TODO: The track gives us empty (black)
|
241
|
+
# frames by default but maybe we need a
|
242
|
+
# @dataclass in the middle to handle if
|
243
|
+
# we want transparent frames or not and/or
|
244
|
+
# to detect them here because, if not,
|
245
|
+
# they are just simple VideoFrames and we
|
246
|
+
# don't know they are 'empty' frames
|
247
|
+
|
248
|
+
# We need to adjust our output elements to be
|
249
|
+
# consecutive and with the right values
|
250
|
+
# TODO: We are using int() for fps but its float...
|
251
|
+
audio_frame.time_base = audio_time_base
|
252
|
+
#audio_frame.pts = int(audio_frame_index / audio_frame.time_base)
|
253
|
+
audio_frame.pts = audio_pts
|
254
|
+
# We increment for the next iteration
|
255
|
+
audio_pts += audio_frame.samples
|
256
|
+
#audio_frame.pts = int(t + (audio_frame_index * audio_frame.time_base) / audio_frame.time_base)
|
257
|
+
|
258
|
+
#print(f'[AUDIO] Here in t:{str(t)} -> pts:{str(audio_frame.pts)} - dts:{str(audio_frame.dts)}')
|
259
|
+
|
260
|
+
#num_of_audio_frames += 1
|
261
|
+
#print(audio_frame)
|
262
|
+
writer.mux_audio_frame(audio_frame)
|
263
|
+
#print(f'Num of audio frames: {str(num_of_audio_frames)}')
|
264
|
+
|
178
265
|
writer.mux_video_frame(None)
|
266
|
+
writer.mux_audio_frame(None)
|
179
267
|
writer.output.close()
|