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.
- {yta_video_opengl-0.0.6 → yta_video_opengl-0.0.8}/PKG-INFO +1 -1
- {yta_video_opengl-0.0.6 → yta_video_opengl-0.0.8}/pyproject.toml +1 -1
- {yta_video_opengl-0.0.6 → yta_video_opengl-0.0.8}/src/yta_video_opengl/classes.py +256 -72
- {yta_video_opengl-0.0.6 → yta_video_opengl-0.0.8}/src/yta_video_opengl/reader/__init__.py +170 -13
- {yta_video_opengl-0.0.6 → yta_video_opengl-0.0.8}/src/yta_video_opengl/tests.py +28 -7
- yta_video_opengl-0.0.8/src/yta_video_opengl/utils.py +343 -0
- yta_video_opengl-0.0.8/src/yta_video_opengl/video.py +164 -0
- {yta_video_opengl-0.0.6 → yta_video_opengl-0.0.8}/src/yta_video_opengl/writer.py +14 -1
- yta_video_opengl-0.0.6/src/yta_video_opengl/utils.py +0 -24
- {yta_video_opengl-0.0.6 → yta_video_opengl-0.0.8}/LICENSE +0 -0
- {yta_video_opengl-0.0.6 → yta_video_opengl-0.0.8}/README.md +0 -0
- {yta_video_opengl-0.0.6 → yta_video_opengl-0.0.8}/src/yta_video_opengl/__init__.py +0 -0
- {yta_video_opengl-0.0.6 → yta_video_opengl-0.0.8}/src/yta_video_opengl/reader/cache.py +0 -0
@@ -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
|
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
|
28
|
+
def uniforms(
|
28
29
|
self
|
29
|
-
):
|
30
|
+
) -> dict:
|
30
31
|
"""
|
31
|
-
|
32
|
+
The uniforms in the program, as a dict, in
|
33
|
+
the format `{key, value}`.
|
32
34
|
"""
|
33
|
-
return
|
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
|
-
|
43
|
+
program: moderngl.Program
|
38
44
|
):
|
39
|
-
self.
|
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
|
-
|
42
|
-
|
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
|
-
) -> '
|
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
|
82
|
+
return self
|
63
83
|
|
64
84
|
def set_vec(
|
65
85
|
self,
|
66
86
|
name: str,
|
67
87
|
values
|
68
|
-
) -> '
|
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
|
-
) -> '
|
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
|
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
|
135
|
+
class BaseNode:
|
105
136
|
"""
|
106
|
-
|
107
|
-
|
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
|
-
|
110
|
-
|
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
|
-
|
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
|
-
|
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
|
254
|
+
def vertex_shader(
|
136
255
|
self
|
137
|
-
) ->
|
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
|
-
|
140
|
-
will use to represent the frame by
|
141
|
-
applying it as a texture.
|
354
|
+
Source code of the vertex shader.
|
142
355
|
"""
|
143
|
-
|
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
|
-
|
359
|
+
@abstractmethod
|
360
|
+
def fragment_shader(
|
156
361
|
self
|
157
|
-
) ->
|
362
|
+
) -> str:
|
158
363
|
"""
|
159
|
-
|
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
|
-
|
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
|
-
|
234
|
-
|
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
|
-
|
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.
|
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.
|
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
|
-
|
60
|
+
frame: any,
|
61
|
+
t: float = None,
|
62
|
+
pixel_format: str = 'rgb24'
|
51
63
|
):
|
52
|
-
self.
|
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.
|
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.
|
104
|
+
return self.value.stream.type == 'audio'
|
85
105
|
|
86
106
|
def __init__(
|
87
107
|
self,
|
88
|
-
|
108
|
+
packet: Packet
|
89
109
|
):
|
90
|
-
self.
|
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.
|
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.
|
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(
|
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.
|
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
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
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())
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|