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.
@@ -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
- ) -> float:
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(track.end for track in self.tracks)
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] = (1920, 1080),
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] = [Track(), 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 size and fps
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
- ) -> Union['VideoFrame', None]:
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
- frames = [
87
- frame
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
- ParameterValidator.validate_mandatory_positive_number('start', start, do_include_zero = True)
119
- ParameterValidator.validate_positive_number('end', end, do_include_zero = False)
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 = 60,
153
- size = (1920, 1080),
193
+ fps = self.fps,
194
+ size = self.size,
154
195
  pixel_format = 'yuv420p'
155
196
  )
156
197
 
157
- for t in generate_times(start, end, self.fps):
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
- 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
- )
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 = Fraction(1, int(self.fps))
171
- frame.pts = int(t / frame.time_base)
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()