yta-video-opengl 0.0.6__tar.gz → 0.0.8__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: yta-video-opengl
3
- Version: 0.0.6
3
+ Version: 0.0.8
4
4
  Summary: Youtube Autonomous Video OpenGL Module
5
5
  Author: danialcala94
6
6
  Author-email: danielalcalavalera@gmail.com
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "yta-video-opengl"
3
- version = "0.0.6"
3
+ version = "0.0.8"
4
4
  description = "Youtube Autonomous Video OpenGL Module"
5
5
  authors = [
6
6
  {name = "danialcala94",email = "danielalcalavalera@gmail.com"}
@@ -8,7 +8,8 @@ so we use different triangles to build
8
8
  our shapes (quad normally).
9
9
  """
10
10
  from yta_validation.parameter import ParameterValidator
11
- from yta_video_opengl.utils import frame_to_texture
11
+ from yta_validation import PythonValidator
12
+ from yta_video_opengl.utils import frame_to_texture, get_fullscreen_quad_vao
12
13
  from abc import ABC, abstractmethod
13
14
  from typing import Union
14
15
 
@@ -24,29 +25,48 @@ class _Uniforms:
24
25
  """
25
26
 
26
27
  @property
27
- def program(
28
+ def uniforms(
28
29
  self
29
- ):
30
+ ) -> dict:
30
31
  """
31
- Shortcut to the FrameShader program.
32
+ The uniforms in the program, as a dict, in
33
+ the format `{key, value}`.
32
34
  """
33
- return self._shader_instance.program
35
+ return {
36
+ key: self.program[key].value
37
+ for key in self.program
38
+ if PythonValidator.is_instance_of(self.program[key], moderngl.Uniform)
39
+ }
34
40
 
35
41
  def __init__(
36
42
  self,
37
- shader_instance: 'FrameShaderBase'
43
+ program: moderngl.Program
38
44
  ):
39
- self._shader_instance: 'FrameShaderBase' = shader_instance
45
+ self.program: moderngl.Program = program
46
+ """
47
+ The program instance this handler class
48
+ belongs to.
49
+ """
50
+
51
+ def get(
52
+ self,
53
+ name: str
54
+ ) -> Union[any, None]:
40
55
  """
41
- The instance of the FrameShader class these
42
- uniforms belong to.
56
+ Get the value of the uniform with the
57
+ given 'name'.
43
58
  """
59
+ return self.uniforms.get(name, None)
44
60
 
61
+ # TODO: I need to refactor these method to
62
+ # accept a **kwargs maybe, or to auto-detect
63
+ # the type and add the uniform as it must be
64
+ # done
45
65
  def set(
46
66
  self,
47
67
  name: str,
48
68
  value
49
- ) -> 'FrameShaderBase':
69
+ ) -> '_Uniforms':
50
70
  """
51
71
  Set the provided 'value' to the normal type
52
72
  uniform with the given 'name'. Here you have
@@ -59,13 +79,13 @@ class _Uniforms:
59
79
  if name in self.program:
60
80
  self.program[name].value = value
61
81
 
62
- return self._shader_instance
82
+ return self
63
83
 
64
84
  def set_vec(
65
85
  self,
66
86
  name: str,
67
87
  values
68
- ) -> 'FrameShaderBase':
88
+ ) -> '_Uniforms':
69
89
  """
70
90
  Set the provided 'value' to the normal type
71
91
  uniform with the given 'name'. Here you have
@@ -78,11 +98,13 @@ class _Uniforms:
78
98
  if name in self.program:
79
99
  self.program[name].write(np.array(values, dtype = 'f4').tobytes())
80
100
 
101
+ return self
102
+
81
103
  def set_mat(
82
104
  self,
83
105
  name: str,
84
106
  value
85
- ) -> 'FrameShaderBase':
107
+ ) -> '_Uniforms':
86
108
  """
87
109
  Set the provided 'value' to a `matN` type
88
110
  uniform with the given 'name'. The 'value'
@@ -99,16 +121,26 @@ class _Uniforms:
99
121
  if name in self.program:
100
122
  self.program[name].write(value)
101
123
 
102
- return self._shader_instance
124
+ return self
125
+
126
+ def print(
127
+ self
128
+ ) -> '_Uniforms':
129
+ """
130
+ Print the defined uniforms in console.
131
+ """
132
+ for key, value in self.uniforms.items():
133
+ print(f'"{key}": {str(value)}')
103
134
 
104
- class FrameShaderBase(ABC):
135
+ class BaseNode:
105
136
  """
106
- Class to be inherited by any of our own
107
- custom opengl program classes.
137
+ The basic class of a node to manipulate frames
138
+ as opengl textures. This node will process the
139
+ frame as an input texture and will generate
140
+ also a texture as the output.
108
141
 
109
- This shader base class must be used by all
110
- the classes that are modifying the frames
111
- one by one.
142
+ Nodes can be chained and the result from one
143
+ node can be applied on another node.
112
144
  """
113
145
 
114
146
  @property
@@ -117,7 +149,7 @@ class FrameShaderBase(ABC):
117
149
  self
118
150
  ) -> str:
119
151
  """
120
- Source code of the vertex shader.
152
+ The code of the vertex shader.
121
153
  """
122
154
  pass
123
155
 
@@ -127,47 +159,211 @@ class FrameShaderBase(ABC):
127
159
  self
128
160
  ) -> str:
129
161
  """
130
- Source code of the fragment shader.
162
+ The code of the fragment shader.
131
163
  """
132
164
  pass
133
165
 
166
+ def __init__(
167
+ self,
168
+ context: moderngl.Context,
169
+ size: tuple[int, int],
170
+ **kwargs
171
+ ):
172
+ ParameterValidator.validate_mandatory_instance_of('context', context, moderngl.Context)
173
+ # TODO: Validate size
174
+
175
+ self.context: moderngl.Context = context
176
+ """
177
+ The context of the program.
178
+ """
179
+ self.size: tuple[int, int] = size
180
+ """
181
+ The size we want to use for the frame buffer
182
+ in a (width, height) format.
183
+ """
184
+ # Compile shaders within the program
185
+ self.program: moderngl.Program = self.context.program(
186
+ vertex_shader = self.vertex_shader,
187
+ fragment_shader = self.fragment_shader
188
+ )
189
+
190
+ # Create the fullscreen quad
191
+ self.quad = get_fullscreen_quad_vao(
192
+ context = self.context,
193
+ program = self.program
194
+ )
195
+
196
+ # Create the output fbo
197
+ self.output_tex = self.context.texture(self.size, 4)
198
+ self.output_tex.filter = (moderngl.LINEAR, moderngl.LINEAR)
199
+ self.fbo = self.context.framebuffer(color_attachments = [self.output_tex])
200
+
201
+ self.uniforms: _Uniforms = _Uniforms(self.program)
202
+ """
203
+ Shortcut to the uniforms functionality.
204
+ """
205
+ # Auto set uniforms dynamically if existing
206
+ for key, value in kwargs.items():
207
+ self.uniforms.set(key, value)
208
+
209
+ def process(
210
+ self,
211
+ input: Union[moderngl.Texture, 'VideoFrame', 'np.ndarray']
212
+ ) -> moderngl.Texture:
213
+ """
214
+ Apply the shader to the 'input', that
215
+ must be a frame or a texture, and return
216
+ the new resulting texture.
217
+
218
+ We use and return textures to maintain
219
+ the process in GPU and optimize it.
220
+ """
221
+ # TODO: Maybe we can accept a VideoFrame
222
+ # or a numpy array and transform it here
223
+ # into a texture, ready to be used:
224
+ # frame_to_texture(
225
+ # # TODO: Do not use Pillow
226
+ # frame = np.array(Image.open("input.jpg").convert("RGBA")),
227
+ # context = self.context,
228
+ # numpy_format = 'rgba'
229
+ # )
230
+ if PythonValidator.is_instance_of(input, ['VideoFrame', 'ndarray']):
231
+ # TODO: What about the numpy format (?)
232
+ input = frame_to_texture(input, self.context)
233
+
234
+ self.fbo.use()
235
+ self.context.clear(0.0, 0.0, 0.0, 0.0)
236
+
237
+ input.use(location = 0)
238
+
239
+ if 'texture' in self.program:
240
+ self.program['texture'] = 0
241
+
242
+ self.quad.render()
243
+
244
+ return self.output_tex
245
+
246
+ class WavingNode(BaseNode):
247
+ """
248
+ Just an example, without the shaders code
249
+ actually, to indicate that we can use
250
+ custom parameters to make it work.
251
+ """
252
+
134
253
  @property
135
- def vertices(
254
+ def vertex_shader(
136
255
  self
137
- ) -> 'np.ndarray':
256
+ ) -> str:
257
+ return (
258
+ '''
259
+ #version 330
260
+ in vec2 in_vert;
261
+ in vec2 in_texcoord;
262
+ out vec2 v_uv;
263
+ void main() {
264
+ v_uv = in_texcoord;
265
+ gl_Position = vec4(in_vert, 0.0, 1.0);
266
+ }
267
+ '''
268
+ )
269
+
270
+ @property
271
+ def fragment_shader(
272
+ self
273
+ ) -> str:
274
+ return (
275
+ '''
276
+ #version 330
277
+ uniform sampler2D tex;
278
+ uniform float time;
279
+ uniform float amplitude;
280
+ uniform float frequency;
281
+ uniform float speed;
282
+ in vec2 v_uv;
283
+ out vec4 f_color;
284
+ void main() {
285
+ float wave = sin(v_uv.x * frequency + time * speed) * amplitude;
286
+ vec2 uv = vec2(v_uv.x, v_uv.y + wave);
287
+ f_color = texture(tex, uv);
288
+ }
289
+ '''
290
+ )
291
+
292
+ def __init__(
293
+ self,
294
+ context: moderngl.Context,
295
+ size: tuple[int, int],
296
+ amplitude: float = 0.05,
297
+ frequency: float = 10.0,
298
+ speed: float = 2.0
299
+ ):
300
+ super().__init__(
301
+ context = context,
302
+ size = size,
303
+ amplitude = amplitude,
304
+ frequency = frequency,
305
+ speed = speed
306
+ )
307
+
308
+ # This is just an example and we are not
309
+ # using the parameters actually, but we
310
+ # could set those specific uniforms to be
311
+ # processed by the code
312
+ def process(
313
+ self,
314
+ input: Union[moderngl.Texture, 'VideoFrame', 'np.ndarray'],
315
+ t: float = 0.0,
316
+ ) -> moderngl.Texture:
317
+ """
318
+ Apply the shader to the 'input', that
319
+ must be a frame or a texture, and return
320
+ the new resulting texture.
321
+
322
+ We use and return textures to maintain
323
+ the process in GPU and optimize it.
324
+ """
325
+ self.uniforms.set('time', t)
326
+
327
+ return super().process(input)
328
+
329
+
330
+
331
+ """
332
+ TODO: I should try to use the Node classes
333
+ to manipulate the frames because this is how
334
+ Davinci Resolve and other editors work.
335
+ """
336
+
337
+
338
+ class FrameShaderBase(ABC):
339
+ """
340
+ Class to be inherited by any of our own
341
+ custom opengl program classes.
342
+
343
+ This shader base class must be used by all
344
+ the classes that are modifying the frames
345
+ one by one.
346
+ """
347
+
348
+ @property
349
+ @abstractmethod
350
+ def vertex_shader(
351
+ self
352
+ ) -> str:
138
353
  """
139
- The UV coordinates to build the quad we
140
- will use to represent the frame by
141
- applying it as a texture.
354
+ Source code of the vertex shader.
142
355
  """
143
- return np.array([
144
- # vertex 0 - bottom left
145
- -1.0, -1.0, 0.0, 0.0,
146
- # vertex 1 - bottom right
147
- 1.0, -1.0, 1.0, 0.0,
148
- # vertex 2 - top left
149
- -1.0, 1.0, 0.0, 1.0,
150
- # vertex 3 - top right
151
- 1.0, 1.0, 1.0, 1.0
152
- ], dtype = 'f4')
356
+ pass
153
357
 
154
358
  @property
155
- def indexes(
359
+ @abstractmethod
360
+ def fragment_shader(
156
361
  self
157
- ) -> 'np.ndarray':
362
+ ) -> str:
158
363
  """
159
- The indexes of the vertices (see 'vertices'
160
- property) to build the 2 opengl triangles
161
- that will represent the quad we need for
162
- the frame.
364
+ Source code of the fragment shader.
163
365
  """
164
- return np.array(
165
- [
166
- 0, 1, 2,
167
- 2, 1, 3
168
- ],
169
- dtype = 'i4'
170
- )
366
+ pass
171
367
 
172
368
  def __init__(
173
369
  self,
@@ -203,14 +399,6 @@ class FrameShaderBase(ABC):
203
399
  """
204
400
  The frame buffer object.
205
401
  """
206
- self.vbo: moderngl.Buffer = None
207
- """
208
- The vertices buffer object.
209
- """
210
- self.ibo: moderngl.Buffer = None
211
- """
212
- The indexes buffer object.
213
- """
214
402
  self.uniforms: _Uniforms = None
215
403
  """
216
404
  Shortcut to the uniforms functionality.
@@ -221,6 +409,12 @@ class FrameShaderBase(ABC):
221
409
  def _initialize_program(
222
410
  self
223
411
  ):
412
+ """
413
+ This method is to allow the effects to
414
+ change their '__init__' method to be able
415
+ to provide parameters that will be set as
416
+ uniforms.
417
+ """
224
418
  # Compile shaders within the program
225
419
  self.program: moderngl.Program = self.context.program(
226
420
  vertex_shader = self.vertex_shader,
@@ -229,18 +423,9 @@ class FrameShaderBase(ABC):
229
423
 
230
424
  # Create frame buffer
231
425
  self.fbo = self.context.simple_framebuffer(self.size)
232
-
233
- # Create buffers
234
- # TODO: I could have more than 1 vertices,
235
- # so more than 1 vbo and more than 1 vao...
236
- self.vbo: moderngl.Buffer = self.context.buffer(self.vertices.tobytes())
237
- self.ibo: moderngl.Buffer = self.context.buffer(self.indexes.tobytes())
238
- vao_content = [
239
- (self.vbo, "2f 2f", "in_vert", "in_texcoord")
240
- ]
241
- self.vao: moderngl.VertexArray = self.context.vertex_array(self.program, vao_content, self.ibo)
242
-
243
- self.uniforms: _Uniforms = _Uniforms(self)
426
+ # Create quad vertex array
427
+ self.vao: moderngl.VertexArray = get_fullscreen_quad_vao(self.context, self.program)
428
+ self.uniforms: _Uniforms = _Uniforms(self.program)
244
429
 
245
430
  # TODO: How do I manage these textures (?)
246
431
  self.textures = {}
@@ -270,8 +455,7 @@ class FrameShaderBase(ABC):
270
455
  tex = self.context.texture((image.shape[1], image.shape[0]), 4, image.tobytes())
271
456
  tex.use(texture_unit)
272
457
  self.textures[uniform_name] = tex
273
- if uniform_name in self.program:
274
- self.program[uniform_name].value = texture_unit
458
+ self.uniforms.set(uniform_name, texture_unit)
275
459
 
276
460
  @abstractmethod
277
461
  def _prepare_frame(
@@ -3,6 +3,7 @@ A video reader using the PyAv (av) library
3
3
  that, using ffmpeg, detects the video.
4
4
  """
5
5
  from yta_video_opengl.reader.cache import VideoFrameCache
6
+ from yta_video_opengl.utils import iterate_stream_frames_demuxing
6
7
  from yta_validation import PythonValidator
7
8
  from av.video.frame import VideoFrame
8
9
  from av.audio.frame import AudioFrame
@@ -32,7 +33,7 @@ class VideoReaderFrame:
32
33
  Flag to indicate if the instance is a video
33
34
  frame.
34
35
  """
35
- return PythonValidator.is_instance_of(self.data, VideoFrame)
36
+ return PythonValidator.is_instance_of(self.value, VideoFrame)
36
37
 
37
38
  @property
38
39
  def is_audio(
@@ -42,18 +43,37 @@ class VideoReaderFrame:
42
43
  Flag to indicate if the instance is an audio
43
44
  frame.
44
45
  """
45
- return PythonValidator.is_instance_of(self.data, AudioFrame)
46
+ return PythonValidator.is_instance_of(self.value, AudioFrame)
47
+
48
+ @property
49
+ def as_numpy(
50
+ self
51
+ ):
52
+ """
53
+ The frame as a numpy array.
54
+ """
55
+ return self.value.to_ndarray(format = self.pixel_format)
46
56
 
47
57
  def __init__(
48
58
  self,
49
59
  # TODO: Add the type, please
50
- data: any
60
+ frame: any,
61
+ t: float = None,
62
+ pixel_format: str = 'rgb24'
51
63
  ):
52
- self.data: Union[AudioFrame, VideoFrame] = data
64
+ self.value: Union[AudioFrame, VideoFrame] = frame
53
65
  """
54
66
  The frame content, that can be audio or video
55
67
  frame.
56
68
  """
69
+ self.t: float = t
70
+ """
71
+ The 't' time moment of the frame.
72
+ """
73
+ self.pixel_format: str = pixel_format
74
+ """
75
+ The pixel format of the frame.
76
+ """
57
77
 
58
78
  @dataclass
59
79
  class VideoReaderPacket:
@@ -71,7 +91,7 @@ class VideoReaderPacket:
71
91
  Flag to indicate if the packet includes video
72
92
  frames or not.
73
93
  """
74
- return self.data.stream.type == 'video'
94
+ return self.value.stream.type == 'video'
75
95
 
76
96
  @property
77
97
  def is_audio(
@@ -81,13 +101,13 @@ class VideoReaderPacket:
81
101
  Flag to indicate if the packet includes audio
82
102
  frames or not.
83
103
  """
84
- return self.data.stream.type == 'audio'
104
+ return self.value.stream.type == 'audio'
85
105
 
86
106
  def __init__(
87
107
  self,
88
- data: Packet
108
+ packet: Packet
89
109
  ):
90
- self.data: Packet = data
110
+ self.value: Packet = packet
91
111
  """
92
112
  The packet, that can include video or audio
93
113
  frames and can be decoded.
@@ -100,7 +120,7 @@ class VideoReaderPacket:
100
120
  Get the frames but decoded, perfect to make
101
121
  modifications and encode to save them again.
102
122
  """
103
- return self.data.decode()
123
+ return self.value.decode()
104
124
 
105
125
 
106
126
  class VideoReader:
@@ -266,7 +286,55 @@ class VideoReader:
266
286
  The fps of the audio.
267
287
  """
268
288
  # TODO: What if no audio (?)
269
- return self.audio_stream.average_rate
289
+ return self.audio_stream.rate
290
+
291
+ @property
292
+ def time_base(
293
+ self
294
+ ) -> Fraction:
295
+ """
296
+ The time base of the video.
297
+ """
298
+ return self.video_stream.time_base
299
+
300
+ @property
301
+ def audio_time_base(
302
+ self
303
+ ) -> Fraction:
304
+ """
305
+ The time base of the audio.
306
+ """
307
+ # TODO: What if no audio (?)
308
+ return self.audio_stream.time_base
309
+
310
+ @property
311
+ def duration(
312
+ self
313
+ ) -> Union[float, None]:
314
+ """
315
+ The duration of the video.
316
+ """
317
+ return (
318
+ float(self.video_stream.duration * self.video_stream.time_base)
319
+ if self.video_stream.duration else
320
+ # TODO: What to do in this case (?)
321
+ None
322
+ )
323
+
324
+ @property
325
+ def audio_duration(
326
+ self
327
+ ) -> Union[float, None]:
328
+ """
329
+ The duration of the audio.
330
+ """
331
+ # TODO: What if no audio (?)
332
+ return (
333
+ float(self.audio_stream.duration * self.audio_stream.time_base)
334
+ if self.audio_stream.duration else
335
+ # TODO: What to do in this case (?)
336
+ None
337
+ )
270
338
 
271
339
  @property
272
340
  def size(
@@ -303,12 +371,18 @@ class VideoReader:
303
371
 
304
372
  def __init__(
305
373
  self,
306
- filename: str
374
+ filename: str,
375
+ # Use 'rgba' if alpha channel
376
+ pixel_format: str = 'rgb24'
307
377
  ):
308
378
  self.filename: str = filename
309
379
  """
310
380
  The filename of the video source.
311
381
  """
382
+ self.pixel_format: str = pixel_format
383
+ """
384
+ The pixel format.
385
+ """
312
386
  self.container: InputContainer = None
313
387
  """
314
388
  The av input general container of the
@@ -362,6 +436,26 @@ class VideoReader:
362
436
  self.audio_stream.thread_type = 'AUTO'
363
437
  self.cache = VideoFrameCache(self)
364
438
 
439
+ def seek(
440
+ self,
441
+ pts,
442
+ stream = None
443
+ ) -> 'VideoReader':
444
+ """
445
+ Call the container '.seek()' method with
446
+ the given 'pts' packet time stamp.
447
+ """
448
+ stream = (
449
+ self.video_stream
450
+ if stream is None else
451
+ stream
452
+ )
453
+
454
+ # TODO: Is 'offset' actually a 'pts' (?)
455
+ self.container.seek(pts, stream = stream)
456
+
457
+ return self
458
+
365
459
  def iterate(
366
460
  self
367
461
  ) -> 'Iterator[Union[VideoFrame, AudioFrame]]':
@@ -370,7 +464,11 @@ class VideoReader:
370
464
  (already decoded).
371
465
  """
372
466
  for frame in self.frame_iterator:
373
- yield VideoReaderFrame(frame)
467
+ yield VideoReaderFrame(
468
+ frame = frame,
469
+ t = float(frame.pts * self.time_base),
470
+ pixel_format = self.pixel_format
471
+ )
374
472
 
375
473
  def iterate_with_audio(
376
474
  self,
@@ -407,7 +505,58 @@ class VideoReader:
407
505
  yield VideoReaderFrame(frame)
408
506
  else:
409
507
  # Return the packet as it is
410
- yield VideoReaderPacket(packet)
508
+ yield VideoReaderPacket(packet)
509
+
510
+ # These methods below are using the demux
511
+ def iterate_video_frames(
512
+ self,
513
+ start_pts: int = 0,
514
+ end_pts: Union[int, None] = None
515
+ ):
516
+ """
517
+ Iterate over the video stream packets and
518
+ decode only the ones in the expected range,
519
+ so only those frames are decoded (which is
520
+ an expensive process).
521
+
522
+ This method returns a tuple of 3 elements:
523
+ - `frame` as a `VideoFrame` instance
524
+ - `t` as the frame time moment
525
+ - `index` as the frame index
526
+ """
527
+ for frame in iterate_stream_frames_demuxing(
528
+ container = self.container,
529
+ video_stream = self.video_stream,
530
+ audio_stream = None,
531
+ start_pts = start_pts,
532
+ end_pts = end_pts
533
+ ):
534
+ yield frame
535
+
536
+ def iterate_audio_frames(
537
+ self,
538
+ start_pts: int = 0,
539
+ end_pts: Union[int, None] = None
540
+ ):
541
+ """
542
+ Iterate over the audio stream packets and
543
+ decode only the ones in the expected range,
544
+ so only those frames are decoded (which is
545
+ an expensive process).
546
+
547
+ This method returns a tuple of 3 elements:
548
+ - `frame` as a `AudioFrame` instance
549
+ - `t` as the frame time moment
550
+ - `index` as the frame index
551
+ """
552
+ for frame in iterate_stream_frames_demuxing(
553
+ container = self.container,
554
+ video_stream = None,
555
+ audio_stream = self.audio_stream,
556
+ start_pts = start_pts,
557
+ end_pts = end_pts
558
+ ):
559
+ yield frame
411
560
 
412
561
  # TODO: Will we use this (?)
413
562
  def get_frame(
@@ -419,6 +568,14 @@ class VideoReader:
419
568
  the cache system.
420
569
  """
421
570
  return self.cache.get_frame(index)
571
+
572
+ def close(
573
+ self
574
+ ) -> None:
575
+ """
576
+ Close the container to free it.
577
+ """
578
+ self.container.close()
422
579
 
423
580
 
424
581
 
@@ -579,7 +579,13 @@ def video_modified_stored():
579
579
  # TODO: Where do we obtain this from (?)
580
580
  PIXEL_FORMAT = 'yuv420p'
581
581
 
582
- from yta_video_opengl.classes import WavingFrame, BreathingFrame, HandheldFrame, OrbitingFrame, RotatingInCenterFrame, StrangeTvFrame, GlitchRgbFrame
582
+ from yta_video_opengl.classes import WavingFrame, BreathingFrame, HandheldFrame, OrbitingFrame, RotatingInCenterFrame, StrangeTvFrame, GlitchRgbFrame, WavingNode
583
+ from yta_video_opengl.utils import texture_to_frame, frame_to_texture
584
+ from yta_video_opengl.video import Video
585
+
586
+ Video(VIDEO_PATH, 0, 0.5).save_as(OUTPUT_PATH)
587
+
588
+ return
583
589
 
584
590
  video = VideoReader(VIDEO_PATH)
585
591
  video_writer = (
@@ -604,6 +610,10 @@ def video_modified_stored():
604
610
  size = video.size,
605
611
  first_frame = video.next_frame
606
612
  )
613
+ context = moderngl.create_context(standalone = True)
614
+
615
+ # New way, with nodes
616
+ node = WavingNode(context, video.size, amplitude = 0.2, frequency = 9, speed = 3)
607
617
  # We need to reset it to being again pointing
608
618
  # to the first frame...
609
619
  # TODO: Improve this by, maybe, storing the first
@@ -624,21 +634,32 @@ def video_modified_stored():
624
634
 
625
635
  # To simplify the process
626
636
  if frame_or_packet is not None:
627
- frame_or_packet = frame_or_packet.data
637
+ frame_or_packet = frame_or_packet.value
628
638
 
629
639
  if is_audio_packet:
630
640
  video_writer.mux(frame_or_packet)
631
641
  elif is_video_frame:
632
642
  with Timer(is_silent_as_context = True) as timer:
633
643
  t = T.video_frame_index_to_video_frame_time(frame_index, float(video.fps))
634
-
644
+ # This is another way of getting 't'
645
+ #t = float(frame_or_packet.pts * video.time_base)
646
+
635
647
  video_writer.mux_video_frame(
636
- effect.process_frame(
637
- frame = frame_or_packet,
638
- t = t,
639
- numpy_format = NUMPY_FORMAT
648
+ frame = texture_to_frame(
649
+ texture = node.process(
650
+ input = frame_or_packet,
651
+ t = t
652
+ )
640
653
  )
641
654
  )
655
+
656
+ # video_writer.mux_video_frame(
657
+ # effect.process_frame(
658
+ # frame = frame_or_packet,
659
+ # t = t,
660
+ # numpy_format = NUMPY_FORMAT
661
+ # )
662
+ # )
642
663
 
643
664
  frame_index += 1
644
665
 
@@ -0,0 +1,343 @@
1
+ from yta_validation import PythonValidator
2
+ from av.container import InputContainer
3
+ from av.video.stream import VideoStream
4
+ from av.audio.stream import AudioStream
5
+ from av.video.frame import VideoFrame
6
+ from typing import Union
7
+
8
+ import av
9
+ import numpy as np
10
+ import moderngl
11
+
12
+
13
+ def frame_to_texture(
14
+ frame: Union['VideoFrame', 'np.ndarray'],
15
+ context: moderngl.Context,
16
+ numpy_format: str = 'rgb24'
17
+ ):
18
+ """
19
+ Transform the given 'frame' to an opengl
20
+ texture. The frame can be a VideoFrame
21
+ instance (from pyav library) or a numpy
22
+ array.
23
+ """
24
+ # To numpy RGB inverted for opengl
25
+ frame: np.ndarray = (
26
+ np.flipud(frame.to_ndarray(format = numpy_format))
27
+ if PythonValidator.is_instance_of(frame, 'VideoFrame') else
28
+ np.flipud(frame)
29
+ )
30
+
31
+ return context.texture(
32
+ size = (frame.shape[1], frame.shape[0]),
33
+ components = frame.shape[2],
34
+ data = frame.tobytes()
35
+ )
36
+
37
+ # TODO: I should make different methods to
38
+ # obtain a VideoFrame or a numpy array frame
39
+ def texture_to_frame(
40
+ texture: moderngl.Texture
41
+ ) -> 'VideoFrame':
42
+ """
43
+ Transform an opengl texture into a pyav
44
+ VideoFrame instance.
45
+ """
46
+ # RGBA8
47
+ data = texture.read(alignment = 1)
48
+ frame = np.frombuffer(data, dtype = np.uint8).reshape((texture.size[1], texture.size[0], 4))
49
+ # Opengl gives it with the y inverted
50
+ frame = np.flipud(frame)
51
+ # TODO: This can be returned as a numpy frame
52
+
53
+ # This is if we need an 'av' VideoFrame (to
54
+ # export through the demuxer, for example)
55
+ frame = av.VideoFrame.from_ndarray(frame, format = 'rgba')
56
+ # TODO: Make this customizable
57
+ frame = frame.reformat(format = 'yuv420p')
58
+
59
+ return frame
60
+
61
+ def get_fullscreen_quad_vao(
62
+ context: moderngl.Context,
63
+ program: moderngl.Program
64
+ ) -> moderngl.VertexArray:
65
+ """
66
+ Get the vertex array object of a quad, by
67
+ using the vertices, the indexes, the vbo,
68
+ the ibo and the vao content.
69
+ """
70
+ # Quad vertices in NDC (-1..1) with texture
71
+ # coords (0..1)
72
+ """
73
+ The UV coordinates to build the quad we
74
+ will use to represent the frame by
75
+ applying it as a texture.
76
+ """
77
+ vertices = np.array([
78
+ # pos.x, pos.y, tex.u, tex.v
79
+ -1.0, -1.0, 0.0, 0.0, # vertex 0 - bottom left
80
+ 1.0, -1.0, 1.0, 0.0, # vertex 1 - bottom right
81
+ -1.0, 1.0, 0.0, 1.0, # vertex 2 - top left
82
+ 1.0, 1.0, 1.0, 1.0, # vertex 3 - top right
83
+ ], dtype = 'f4')
84
+
85
+ """
86
+ The indexes of the vertices (see 'vertices'
87
+ property) to build the 2 opengl triangles
88
+ that will represent the quad we need for
89
+ the frame.
90
+ """
91
+ indices = np.array([
92
+ 0, 1, 2,
93
+ 2, 1, 3
94
+ ], dtype = 'i4')
95
+
96
+ vbo = context.buffer(vertices.tobytes())
97
+ ibo = context.buffer(indices.tobytes())
98
+
99
+ vao_content = [
100
+ # 2 floats position, 2 floats texcoords
101
+ (vbo, '2f 2f', 'in_vert', 'in_texcoord'),
102
+ ]
103
+
104
+ return context.vertex_array(program, vao_content, ibo)
105
+
106
+ def iterate_streams_packets(
107
+ container: 'InputContainer',
108
+ video_stream: 'VideoStream',
109
+ audio_stream: 'AudioStream',
110
+ video_start_pts: int = 0,
111
+ video_end_pts: Union[int, None] = None,
112
+ audio_start_pts: int = 0,
113
+ audio_end_pts: Union[int, None] = None
114
+ ):
115
+ """
116
+ Iterate over the provided 'stream' packets
117
+ and yield the ones in the expected range.
118
+ This is nice when trying to copy a stream
119
+ without modifications.
120
+ """
121
+ # 'video_start_pts' and 'audio_start_pts' must
122
+ # be 0 or a positive tps
123
+
124
+ if (
125
+ video_stream is None and
126
+ audio_stream is None
127
+ ):
128
+ raise Exception('No streams provided.')
129
+
130
+ # We only need to seek on video
131
+ if video_stream is not None:
132
+ container.seek(video_start_pts, stream = video_stream)
133
+ if audio_stream is not None:
134
+ container.seek(audio_start_pts, stream = audio_stream)
135
+
136
+ stream = [
137
+ stream
138
+ for stream in (video_stream, audio_stream)
139
+ if stream
140
+ ]
141
+
142
+ """
143
+ Apparently, if we ignore some packets based
144
+ on the 'pts', we can be ignoring information
145
+ that is needed for the next frames to be
146
+ decoded, so we need to decode them all...
147
+
148
+ If we can find some strategy to seek not for
149
+ the inmediate but some before and read from
150
+ that one to avoid reading all of the packets
151
+ we could save some time, but at what cost?
152
+ We cannot skip any crucial frame so we need
153
+ to know how many we can skip, and that sounds
154
+ a bit difficult depending on the codec.
155
+ """
156
+ stream_finished: str = ''
157
+ for packet in container.demux(stream):
158
+ if packet.pts is None:
159
+ continue
160
+
161
+ # TODO: We cannot skip like this, we need to
162
+ # look for the nearest keyframe to be able
163
+ # to decode the frames later. Take a look at
164
+ # the VideoFrameCache class and use it.
165
+
166
+ # start_pts = (
167
+ # video_start_pts
168
+ # if packet.stream.type == 'video' else
169
+ # audio_start_pts
170
+ # )
171
+ # end_pts = (
172
+ # video_end_pts
173
+ # if packet.stream.type == 'video' else
174
+ # audio_end_pts
175
+ # )
176
+
177
+ # if packet.pts < start_pts:
178
+ # continue
179
+
180
+ # if (
181
+ # end_pts is not None and
182
+ # packet.pts > end_pts
183
+ # ):
184
+ # if (
185
+ # stream_finished != '' and
186
+ # (
187
+ # # Finish if only one stream
188
+ # stream_finished != packet.stream.type or
189
+ # video_stream is None or
190
+ # audio_stream is None
191
+ # )
192
+ # ):
193
+ # # We have yielded all the frames in the
194
+ # # expected range, no more needed
195
+ # return
196
+
197
+ # stream_finished = packet.stream.type
198
+ # continue
199
+
200
+ yield packet
201
+
202
+ def iterate_stream_frames_demuxing(
203
+ container: 'InputContainer',
204
+ video_stream: 'VideoStream',
205
+ audio_stream: 'AudioStream',
206
+ video_start_pts : int = 0,
207
+ video_end_pts: Union[int, None] = None,
208
+ audio_start_pts: int = 0,
209
+ audio_end_pts: Union[int, None] = None
210
+ ):
211
+ """
212
+ Iterate over the provided 'stream' packets
213
+ and decode only the ones in the expected
214
+ range, so only those frames are decoded
215
+ (which is an expensive process).
216
+
217
+ This method returns a tuple of 3 elements:
218
+ - `frame` as a `VideoFrame` instance
219
+ - `t` as the frame time moment
220
+ - `index` as the frame index
221
+
222
+ You can easy transform the frame received
223
+ to a numpy array by using this:
224
+ - `frame.to_ndarray(format = format)`
225
+ """
226
+ # 'start_pts' must be 0 or a positive tps
227
+ # 'end_pts' must be None or a positive tps
228
+
229
+ # We cannot skip packets or we will lose
230
+ # information needed to build the video
231
+ for packet in iterate_streams_packets(
232
+ container = container,
233
+ video_stream = video_stream,
234
+ audio_stream = audio_stream,
235
+ video_start_pts = video_start_pts,
236
+ video_end_pts = video_end_pts,
237
+ audio_start_pts = audio_start_pts,
238
+ audio_end_pts = audio_end_pts
239
+ ):
240
+ # Only valid and in range packets here
241
+ # Here only the accepted ones
242
+ stream_finished: str = ''
243
+ for frame in packet.decode():
244
+ if frame.pts is None:
245
+ continue
246
+
247
+ time_base = (
248
+ video_stream.time_base
249
+ if PythonValidator.is_instance_of(frame, VideoFrame) else
250
+ audio_stream.time_base
251
+ )
252
+
253
+ average_rate = (
254
+ video_stream.average_rate
255
+ if PythonValidator.is_instance_of(frame, VideoFrame) else
256
+ audio_stream.rate
257
+ )
258
+
259
+ start_pts = (
260
+ video_start_pts
261
+ if packet.stream.type == 'video' else
262
+ audio_start_pts
263
+ )
264
+
265
+ end_pts = (
266
+ video_end_pts
267
+ if packet.stream.type == 'video' else
268
+ audio_end_pts
269
+ )
270
+
271
+ if frame.pts < start_pts:
272
+ continue
273
+
274
+ if (
275
+ end_pts is not None and
276
+ frame.pts > end_pts
277
+ ):
278
+ if (
279
+ stream_finished != '' and
280
+ (
281
+ # Finish if only one stream
282
+ stream_finished != packet.stream.type or
283
+ video_stream is None or
284
+ audio_stream is None
285
+ )
286
+ ):
287
+ # We have yielded all the frames in the
288
+ # expected range, no more needed
289
+ return
290
+
291
+ stream_finished = packet.stream.type
292
+ continue
293
+
294
+ time_base = (
295
+ video_stream.time_base
296
+ if PythonValidator.is_instance_of(frame, VideoFrame) else
297
+ audio_stream.time_base
298
+ )
299
+
300
+ average_rate = (
301
+ video_stream.average_rate
302
+ if PythonValidator.is_instance_of(frame, VideoFrame) else
303
+ audio_stream.rate
304
+ )
305
+
306
+ # TODO: Maybe send a @dataclass instead (?)
307
+ yield (
308
+ frame,
309
+ pts_to_t(frame.pts, time_base),
310
+ pts_to_index(frame.pts, time_base, average_rate)
311
+ )
312
+
313
+ def t_to_pts(
314
+ t: float,
315
+ stream_time_base: 'Fraction'
316
+ ) -> int:
317
+ """
318
+ Transform a 't' time moment (in seconds) to
319
+ a packet timestamp (pts) understandable by
320
+ the pyav library.
321
+ """
322
+ return int((t + 0.000001) / stream_time_base)
323
+
324
+ def pts_to_index(
325
+ pts: int,
326
+ stream_time_base: 'Fraction',
327
+ fps: float
328
+ ) -> int:
329
+ """
330
+ Transform a 'pts' packet timestamp to a
331
+ frame index.
332
+ """
333
+ return int(round(pts_to_t(pts, stream_time_base) * fps))
334
+
335
+ def pts_to_t(
336
+ pts: int,
337
+ stream_time_base: 'Fraction'
338
+ ) -> float:
339
+ """
340
+ Transform a 'pts' packet timestamp to a 't'
341
+ time moment.
342
+ """
343
+ return pts * stream_time_base
@@ -0,0 +1,164 @@
1
+ from yta_video_opengl.reader import VideoReader
2
+ from yta_video_opengl.writer import VideoWriter
3
+ from yta_video_opengl.utils import iterate_stream_frames_demuxing
4
+ from yta_validation import PythonValidator
5
+ from typing import Union
6
+
7
+
8
+ # TODO: Where can I obtain this dynamically (?)
9
+ PIXEL_FORMAT = 'yuv420p'
10
+
11
+ # TODO: Maybe rename to 'Media' (?)
12
+ class Video:
13
+ """
14
+ Class to wrap the functionality related to
15
+ handling and modifying a video.
16
+ """
17
+
18
+ @property
19
+ def start_pts(
20
+ self
21
+ ) -> int:
22
+ """
23
+ The start packet time stamp (pts), needed
24
+ to optimize the packet iteration process.
25
+ """
26
+ return int(self.start / self.reader.time_base)
27
+
28
+ @property
29
+ def end_pts(
30
+ self
31
+ ) -> Union[int, None]:
32
+ """
33
+ The end packet time stamp (pts), needed to
34
+ optimize the packet iteration process.
35
+ """
36
+ return (
37
+ int(self.end / self.reader.time_base)
38
+ # TODO: What do we do if no duration (?)
39
+ if self.duration is not None else
40
+ None
41
+ )
42
+
43
+ @property
44
+ def audio_start_pts(
45
+ self
46
+ ) -> int:
47
+ """
48
+ The start packet time stamp (pts), needed
49
+ to optimize the packet iteration process.
50
+ """
51
+ return int(self.start / self.reader.audio_time_base)
52
+
53
+ @property
54
+ def audio_end_pts(
55
+ self
56
+ ) -> Union[int, None]:
57
+ """
58
+ The end packet time stamp (pts), needed to
59
+ optimize the packet iteration process.
60
+ """
61
+ return (
62
+ int(self.end / self.reader.audio_time_base)
63
+ # TODO: What do we do if no duration (?)
64
+ if self.duration is not None else
65
+ None
66
+ )
67
+
68
+ @property
69
+ def duration(
70
+ self
71
+ ):
72
+ """
73
+ The duration of the video.
74
+ """
75
+ return self.end - self.start
76
+
77
+ @property
78
+ def frames(
79
+ self
80
+ ):
81
+ """
82
+ Iterator to yield all the frames, one by
83
+ one, within the range defined by the
84
+ 'start' and 'end' parameters provided when
85
+ instantiating it.
86
+
87
+ This method returns a tuple of 3 elements:
88
+ - `frame` as a `VideoFrame` instance
89
+ - `t` as the frame time moment
90
+ - `index` as the frame index
91
+ """
92
+ for frame in iterate_stream_frames_demuxing(
93
+ container = self.reader.container,
94
+ video_stream = self.reader.video_stream,
95
+ audio_stream = self.reader.audio_stream,
96
+ video_start_pts = self.start_pts,
97
+ video_end_pts = self.end_pts,
98
+ audio_start_pts = self.audio_start_pts,
99
+ audio_end_pts = self.audio_end_pts
100
+ ):
101
+ yield frame
102
+
103
+ def __init__(
104
+ self,
105
+ filename: str,
106
+ start: float = 0.0,
107
+ end: Union[float, None] = None
108
+ ):
109
+ self.filename: str = filename
110
+ """
111
+ The filename of the original video.
112
+ """
113
+ # TODO: Detect the 'pixel_format' from the
114
+ # extension (?)
115
+ self.reader: VideoReader = VideoReader(self.filename)
116
+ """
117
+ The pyav video reader.
118
+ """
119
+ self.start: float = start
120
+ """
121
+ The time moment 't' in which the video
122
+ should start.
123
+ """
124
+ self.end: Union[float, None] = (
125
+ # TODO: Is this 'end' ok (?)
126
+ self.reader.duration
127
+ if end is None else
128
+ end
129
+ )
130
+ """
131
+ The time moment 't' in which the video
132
+ should end.
133
+ """
134
+
135
+ def save_as(
136
+ self,
137
+ filename: str
138
+ ) -> 'Video':
139
+ writer = VideoWriter(filename)
140
+ #writer.set_video_stream(self.reader.video_stream.codec.name, self.reader.fps, self.reader.size, PIXEL_FORMAT)
141
+ writer.set_video_stream_from_template(self.reader.video_stream)
142
+ writer.set_audio_stream_from_template(self.reader.audio_stream)
143
+
144
+ # TODO: I need to process the audio also, so
145
+ # build a method that do the same but for
146
+ # both streams at the same time
147
+ for frame, t, index in self.frames:
148
+ if PythonValidator.is_instance_of(frame, 'VideoFrame'):
149
+ print(f'Saving video frame {str(index)}, with t = {str(t)}')
150
+ writer.mux_video_frame(
151
+ frame = frame
152
+ )
153
+ else:
154
+ print(f'Saving audio frame {str(index)} ({str(round(float(t * self.reader.fps), 2))}), with t = {str(t)}')
155
+ writer.mux_audio_frame(
156
+ frame = frame
157
+ )
158
+
159
+ writer.mux_audio_frame(None)
160
+ writer.mux_video_frame(None)
161
+
162
+ # TODO: Maybe move this to the '__del__' (?)
163
+ writer.output.close()
164
+ self.reader.container.close()
@@ -1,4 +1,3 @@
1
- from yta_validation import PythonValidator
2
1
  from yta_validation.parameter import ParameterValidator
3
2
  from av.stream import Stream
4
3
  from av.packet import Packet
@@ -115,9 +114,23 @@ class VideoWriter:
115
114
  You can pass the audio stream as it was
116
115
  obtained from the reader.
117
116
  """
117
+ self.audio_stream: AudioStream = self.output.add_stream(
118
+ codec_name = template.codec_context.name,
119
+ rate = template.codec_context.rate
120
+ )
121
+ self.audio_stream.codec_context.format = template.codec_context.format
122
+ self.audio_stream.codec_context.layout = template.codec_context.layout
123
+ self.audio_stream.time_base = Fraction(1, template.codec_context.rate)
124
+
125
+ return self
126
+
127
+ # This below is not working
118
128
  self.audio_stream: AudioStream = self.output.add_stream_from_template(
119
129
  template
120
130
  )
131
+ # TODO: Is this actually needed (?)
132
+ # Force this 'rate'
133
+ self.audio_stream.time_base = Fraction(1, template.codec_context.rate)
121
134
 
122
135
  return self
123
136
 
@@ -1,24 +0,0 @@
1
- from yta_validation import PythonValidator
2
- from typing import Union
3
-
4
- import numpy as np
5
- import moderngl
6
-
7
-
8
- def frame_to_texture(
9
- frame: Union['VideoFrame', 'np.ndarray'],
10
- context: moderngl.Context,
11
- numpy_format: str = 'rgb24'
12
- ):
13
- """
14
- Transform the given 'frame' to an opengl
15
- texture.
16
- """
17
- # To numpy RGB inverted for opengl
18
- frame: np.ndarray = (
19
- np.flipud(frame.to_ndarray(format = numpy_format))
20
- if PythonValidator.is_instance_of(frame, 'VideoFrame') else
21
- np.flipud(frame)
22
- )
23
-
24
- return context.texture((frame.shape[1], frame.shape[0]), 3, frame.tobytes())