yta-video-opengl 0.0.4__py3-none-any.whl → 0.0.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- yta_video_opengl/classes.py +1091 -0
- yta_video_opengl/{reader.py → reader/__init__.py} +61 -43
- yta_video_opengl/reader/cache.py +155 -0
- yta_video_opengl/tests.py +682 -58
- yta_video_opengl/utils.py +24 -0
- yta_video_opengl/writer.py +40 -1
- {yta_video_opengl-0.0.4.dist-info → yta_video_opengl-0.0.6.dist-info}/METADATA +1 -1
- yta_video_opengl-0.0.6.dist-info/RECORD +11 -0
- yta_video_opengl-0.0.4.dist-info/RECORD +0 -8
- {yta_video_opengl-0.0.4.dist-info → yta_video_opengl-0.0.6.dist-info}/LICENSE +0 -0
- {yta_video_opengl-0.0.4.dist-info → yta_video_opengl-0.0.6.dist-info}/WHEEL +0 -0
yta_video_opengl/tests.py
CHANGED
@@ -2,71 +2,683 @@
|
|
2
2
|
Manual tests that are working and are interesting
|
3
3
|
to learn about the code, refactor and build
|
4
4
|
classes.
|
5
|
+
"""
|
6
|
+
"""
|
7
|
+
Interesting information:
|
8
|
+
| Abrev. | Nombre completo | Uso principal |
|
9
|
+
| ------- | -------------------------- | -------------------------------------- |
|
10
|
+
| VAO | Vertex Array Object | Esquema de datos de vértices |
|
11
|
+
| VBO | Vertex Buffer Object | Datos crudos de vértices en GPU |
|
12
|
+
| FBO | Frame Buffer Object | Renderizar fuera de pantalla |
|
13
|
+
| UBO | Uniform Buffer Object | Variables `uniform` compartidas |
|
14
|
+
| EBO/IBO | Element / Index Buffer Obj | Índices para reutilizar vértices |
|
15
|
+
| PBO | Pixel Buffer Object | Transferencia rápida de imágenes |
|
16
|
+
| RBO | Render Buffer Object | Almacén intermedio (profundidad, etc.) |
|
17
|
+
|
5
18
|
"""
|
6
19
|
from yta_validation import PythonValidator
|
20
|
+
from yta_validation.parameter import ParameterValidator
|
7
21
|
from yta_video_opengl.reader import VideoReader
|
8
22
|
from yta_video_opengl.writer import VideoWriter
|
9
23
|
from yta_timer import Timer
|
10
24
|
from yta_video_frame_time import T
|
25
|
+
from abc import abstractmethod
|
26
|
+
from typing import Union
|
11
27
|
|
12
28
|
import av
|
13
29
|
import moderngl
|
14
30
|
import numpy as np
|
15
31
|
|
16
32
|
|
33
|
+
class OpenglVertexShader:
|
34
|
+
"""
|
35
|
+
The vertex shader, for opengl renders.
|
36
|
+
"""
|
37
|
+
|
38
|
+
def __init__(
|
39
|
+
self,
|
40
|
+
context: moderngl.Context,
|
41
|
+
code: str
|
42
|
+
):
|
43
|
+
self.context: moderngl.Context = context
|
44
|
+
"""
|
45
|
+
The context of the vertex.
|
46
|
+
"""
|
47
|
+
self.code: str = code
|
48
|
+
"""
|
49
|
+
The code that builts the vertex shader.
|
50
|
+
"""
|
51
|
+
|
52
|
+
class OpenglFragmentShader:
|
53
|
+
"""
|
54
|
+
The fragment shader, for opengl renders.
|
55
|
+
"""
|
56
|
+
|
57
|
+
def __init__(
|
58
|
+
self,
|
59
|
+
context: moderngl.Context,
|
60
|
+
code: str
|
61
|
+
):
|
62
|
+
self.context: moderngl.Context = context
|
63
|
+
"""
|
64
|
+
The context of the vertex.
|
65
|
+
"""
|
66
|
+
self.code: str = code
|
67
|
+
"""
|
68
|
+
The code that builts the vertex shader.
|
69
|
+
"""
|
70
|
+
|
71
|
+
class OpenglProgram:
|
72
|
+
"""
|
73
|
+
The program, for opengl renders.
|
74
|
+
"""
|
75
|
+
|
76
|
+
@property
|
77
|
+
def program(
|
78
|
+
self
|
79
|
+
) -> moderngl.Program:
|
80
|
+
"""
|
81
|
+
The opengl program to load variables and use to
|
82
|
+
modify the frams.
|
83
|
+
"""
|
84
|
+
if not hasattr(self, '_program'):
|
85
|
+
self._program = self.context.program(
|
86
|
+
vertex_shader = self.vertex_shader,
|
87
|
+
fragment_shader = self.fragment_shader
|
88
|
+
)
|
89
|
+
|
90
|
+
return self._program
|
91
|
+
|
92
|
+
@property
|
93
|
+
def vbo(
|
94
|
+
self
|
95
|
+
) -> moderngl.Buffer:
|
96
|
+
"""
|
97
|
+
The vertex buffer object.
|
98
|
+
|
99
|
+
Block of memory in the GPU in which we
|
100
|
+
store the information about the vertices
|
101
|
+
(positions, colors, texture coordinates,
|
102
|
+
etc.). The VAO points to one or more VBOs
|
103
|
+
to obtain the information.
|
104
|
+
"""
|
105
|
+
if not hasattr(self, '_vbo'):
|
106
|
+
self._vbo = self.create_vbo(self.vertices)
|
107
|
+
|
108
|
+
return self._vbo
|
109
|
+
|
110
|
+
@property
|
111
|
+
def vao(
|
112
|
+
self
|
113
|
+
):
|
114
|
+
"""
|
115
|
+
The vertex array object.
|
116
|
+
|
117
|
+
Store the state of how the vertices
|
118
|
+
information is organized.
|
119
|
+
"""
|
120
|
+
if not hasattr(self, '_vao'):
|
121
|
+
self._vao = self.create_vao(self.vbo)
|
122
|
+
|
123
|
+
return self._vao
|
124
|
+
|
125
|
+
def __init__(
|
126
|
+
self,
|
127
|
+
context: moderngl.Context,
|
128
|
+
vertex_shader: OpenglVertexShader,
|
129
|
+
fragment_shader: OpenglFragmentShader,
|
130
|
+
vertices: 'np.ndarray'
|
131
|
+
):
|
132
|
+
self.context: moderngl.Context = context
|
133
|
+
"""
|
134
|
+
The opengl context.
|
135
|
+
"""
|
136
|
+
self.vertex_shader: OpenglVertexShader = vertex_shader
|
137
|
+
"""
|
138
|
+
The vertex shader.
|
139
|
+
"""
|
140
|
+
self.fragment_shader: OpenglFragmentShader = fragment_shader
|
141
|
+
"""
|
142
|
+
The fragment shader.
|
143
|
+
"""
|
144
|
+
"""
|
145
|
+
TODO: A program will include the context, one
|
146
|
+
vertex shader, one fragment shader and one or
|
147
|
+
more vertices. So, this need to be refactor to
|
148
|
+
accept more than one vertices, and also to
|
149
|
+
provide one vbo for each vertices item.
|
150
|
+
"""
|
151
|
+
self.vertices: Union[list['np.ndarray'], 'np.ndarray'] = vertices
|
152
|
+
"""
|
153
|
+
The vertices we need to use.
|
154
|
+
"""
|
155
|
+
|
156
|
+
def set_value(
|
157
|
+
self,
|
158
|
+
name: str,
|
159
|
+
value: any
|
160
|
+
) -> 'OpenglProgram':
|
161
|
+
"""
|
162
|
+
Set the provided 'value' as the value of the
|
163
|
+
program property (uniform) with the name given
|
164
|
+
as 'name' parameter.
|
165
|
+
"""
|
166
|
+
self.program[name].value = value
|
167
|
+
|
168
|
+
return self
|
169
|
+
|
170
|
+
def create_vao(
|
171
|
+
self,
|
172
|
+
vbo: moderngl.Buffer
|
173
|
+
) -> moderngl.VertexArray:
|
174
|
+
"""
|
175
|
+
Create a vertex array with the given 'vbo'
|
176
|
+
(vertex array object) parameter.
|
177
|
+
"""
|
178
|
+
ParameterValidator.validate_mandatory_instance_of('vbo', vbo, moderngl.Buffer)
|
179
|
+
|
180
|
+
return self.context.vertex_array(self.program, vbo, 'in_pos', 'in_uv')
|
181
|
+
|
182
|
+
def create_vbo(
|
183
|
+
self,
|
184
|
+
vertices: np.ndarray
|
185
|
+
) -> moderngl.Buffer:
|
186
|
+
"""
|
187
|
+
Create a buffer with the given 'vertices'
|
188
|
+
parameter.
|
189
|
+
"""
|
190
|
+
ParameterValidator.validate_mandatory_numpy_array('vertices', vertices)
|
191
|
+
|
192
|
+
return self.context.buffer(vertices.tobytes())
|
193
|
+
|
194
|
+
class OpenglContext:
|
195
|
+
"""
|
196
|
+
Class to wrap an opengl context to handle
|
197
|
+
it properly and modify videos.
|
198
|
+
|
199
|
+
TODO: This is ready to apply only one
|
200
|
+
change per context because the shaders are
|
201
|
+
limited to one.
|
202
|
+
"""
|
203
|
+
|
204
|
+
def __init__(
|
205
|
+
self,
|
206
|
+
vertex_shader: OpenglVertexShader,
|
207
|
+
fragment_shader: OpenglFragmentShader,
|
208
|
+
vertices: 'np.ndarray'
|
209
|
+
):
|
210
|
+
self.context: moderngl.Context = moderngl.create_standalone_context()
|
211
|
+
"""
|
212
|
+
The headless context.
|
213
|
+
"""
|
214
|
+
self.program: OpenglProgram = OpenglProgram(
|
215
|
+
context = self.context,
|
216
|
+
vertex_shader = vertex_shader,
|
217
|
+
fragment_shader = fragment_shader,
|
218
|
+
vertices = vertices
|
219
|
+
)
|
220
|
+
"""
|
221
|
+
The program custom class instance that is
|
222
|
+
able to use the vao, vbo, etc.
|
223
|
+
"""
|
224
|
+
|
225
|
+
def fbo(
|
226
|
+
self,
|
227
|
+
frame_size: tuple[int, int]
|
228
|
+
) -> moderngl.Framebuffer:
|
229
|
+
"""
|
230
|
+
Get a frame buffer object (fbo) for the
|
231
|
+
given 'frame_size'.
|
232
|
+
|
233
|
+
A frame buffero bject is a virtual screen
|
234
|
+
in which you can render out of the screen
|
235
|
+
to lately do this:
|
236
|
+
- Save as a texture
|
237
|
+
- Apply post-processing methods
|
238
|
+
- Store to a file
|
239
|
+
"""
|
240
|
+
return self.context.simple_framebuffer(frame_size)
|
241
|
+
|
242
|
+
from dataclasses import dataclass
|
243
|
+
from abc import ABC
|
244
|
+
@dataclass
|
245
|
+
class OpenglEffectProgram(ABC):
|
246
|
+
"""
|
247
|
+
The abstract class to be inherited by any
|
248
|
+
of our opengl frame effect methods, that
|
249
|
+
are actually programs.
|
250
|
+
"""
|
251
|
+
|
252
|
+
@property
|
253
|
+
@abstractmethod
|
254
|
+
def vertex_shader(
|
255
|
+
self
|
256
|
+
) -> str:
|
257
|
+
pass
|
258
|
+
|
259
|
+
@property
|
260
|
+
@abstractmethod
|
261
|
+
def fragment_shader(
|
262
|
+
self
|
263
|
+
) -> str:
|
264
|
+
pass
|
265
|
+
|
266
|
+
@property
|
267
|
+
@abstractmethod
|
268
|
+
def vertices(
|
269
|
+
self
|
270
|
+
) -> 'np.ndarray':
|
271
|
+
pass
|
272
|
+
|
273
|
+
def __init__(
|
274
|
+
self
|
275
|
+
):
|
276
|
+
pass
|
277
|
+
|
278
|
+
@dataclass
|
279
|
+
class WavingEffectProgram(OpenglEffectProgram):
|
280
|
+
|
281
|
+
# TODO: I think this has to be a different
|
282
|
+
# thing...
|
283
|
+
@property
|
284
|
+
def vertex_shader(
|
285
|
+
self
|
286
|
+
) -> str:
|
287
|
+
return (
|
288
|
+
'''
|
289
|
+
#version 330
|
290
|
+
in vec2 in_pos;
|
291
|
+
in vec2 in_uv;
|
292
|
+
out vec2 v_uv;
|
293
|
+
void main() {
|
294
|
+
v_uv = in_uv;
|
295
|
+
gl_Position = vec4(in_pos, 0.0, 1.0);
|
296
|
+
}
|
297
|
+
'''
|
298
|
+
)
|
299
|
+
|
300
|
+
@property
|
301
|
+
def fragment_shader(
|
302
|
+
self
|
303
|
+
) -> str:
|
304
|
+
return (
|
305
|
+
'''
|
306
|
+
#version 330
|
307
|
+
uniform sampler2D tex;
|
308
|
+
uniform float time;
|
309
|
+
uniform float amp;
|
310
|
+
uniform float freq;
|
311
|
+
uniform float speed;
|
312
|
+
in vec2 v_uv;
|
313
|
+
out vec4 f_color;
|
314
|
+
void main() {
|
315
|
+
float wave = sin(v_uv.x * freq + time * speed) * amp;
|
316
|
+
vec2 uv = vec2(v_uv.x, v_uv.y + wave);
|
317
|
+
f_color = texture(tex, uv);
|
318
|
+
}
|
319
|
+
'''
|
320
|
+
)
|
321
|
+
|
322
|
+
@property
|
323
|
+
def vertices(
|
324
|
+
self
|
325
|
+
) -> 'np.ndarray':
|
326
|
+
# TODO: This should be an array of them because
|
327
|
+
# we can have more than one, but by now I leave
|
328
|
+
# it being only one
|
329
|
+
return np.array([
|
330
|
+
-1, -1, 0.0, 0.0,
|
331
|
+
1, -1, 1.0, 0.0,
|
332
|
+
-1, 1, 0.0, 1.0,
|
333
|
+
1, 1, 1.0, 1.0,
|
334
|
+
], dtype = 'f4')
|
335
|
+
|
336
|
+
NUMPY_FORMAT = 'rgb24'
|
337
|
+
|
338
|
+
# TODO: Maybe rename as ContextHandler (?)
|
339
|
+
class VideoProcessor:
|
340
|
+
"""
|
341
|
+
Class to read a video, process it (maybe
|
342
|
+
applying some effects) and writing the
|
343
|
+
results in a new video.
|
344
|
+
"""
|
345
|
+
|
346
|
+
@property
|
347
|
+
def fbo(
|
348
|
+
self
|
349
|
+
) -> moderngl.Framebuffer:
|
350
|
+
"""
|
351
|
+
The frame buffer object for the video frame
|
352
|
+
size.
|
353
|
+
"""
|
354
|
+
if not hasattr(self, '_fbo'):
|
355
|
+
self._fbo = self.context.fbo(self.reader.size)
|
356
|
+
|
357
|
+
return self._fbo
|
358
|
+
|
359
|
+
@property
|
360
|
+
def vao(
|
361
|
+
self
|
362
|
+
) -> moderngl.VertexArray:
|
363
|
+
"""
|
364
|
+
Shortcut to the program vao.
|
365
|
+
"""
|
366
|
+
return self.program.vao
|
367
|
+
|
368
|
+
@property
|
369
|
+
def first_frame(
|
370
|
+
self
|
371
|
+
) -> Union['VideoFrame', None]:
|
372
|
+
"""
|
373
|
+
The first frame of the video as a VideoFrame.
|
374
|
+
"""
|
375
|
+
if not hasattr(self, '_first_frame'):
|
376
|
+
# Framebuffer to render
|
377
|
+
self.fbo.use()
|
378
|
+
self._first_frame = self.reader.next_frame
|
379
|
+
# Reset the reader
|
380
|
+
self.reader.reset()
|
381
|
+
|
382
|
+
return self._first_frame
|
383
|
+
|
384
|
+
@property
|
385
|
+
def first_frame_as_texture(
|
386
|
+
self
|
387
|
+
) -> moderngl.Texture:
|
388
|
+
"""
|
389
|
+
The first frame of the video as a texture.
|
390
|
+
This is needed to start the process.
|
391
|
+
"""
|
392
|
+
if not hasattr(self, '_first_frame_as_texture'):
|
393
|
+
self._first_frame_as_texture = self.frame_to_texture(self.first_frame, NUMPY_FORMAT)
|
394
|
+
self._first_frame_as_texture.build_mipmaps()
|
395
|
+
|
396
|
+
return self._first_frame_as_texture
|
397
|
+
|
398
|
+
@property
|
399
|
+
def program(
|
400
|
+
self
|
401
|
+
) -> OpenglProgram:
|
402
|
+
"""
|
403
|
+
Shortcut to the context program custom class
|
404
|
+
instance.
|
405
|
+
"""
|
406
|
+
return self.context.program
|
407
|
+
|
408
|
+
def __init__(
|
409
|
+
self,
|
410
|
+
filename: str,
|
411
|
+
output_filename: str
|
412
|
+
):
|
413
|
+
self.filename: str = filename
|
414
|
+
"""
|
415
|
+
The filename of the video we want to read and
|
416
|
+
process.
|
417
|
+
"""
|
418
|
+
self.output_filename: str = output_filename
|
419
|
+
"""
|
420
|
+
The filename of the video we want to generate
|
421
|
+
and store once the original one has been
|
422
|
+
processed.
|
423
|
+
"""
|
424
|
+
# TODO: Hardcoded by now
|
425
|
+
effect = WavingEffectProgram()
|
426
|
+
self.context: OpenglContext = OpenglContext(
|
427
|
+
vertex_shader = effect.vertex_shader,
|
428
|
+
fragment_shader = effect.fragment_shader,
|
429
|
+
vertices = effect.vertices
|
430
|
+
)
|
431
|
+
"""
|
432
|
+
The headless context as a custom class instance.
|
433
|
+
"""
|
434
|
+
self.reader: VideoReader = VideoReader(self.filename)
|
435
|
+
"""
|
436
|
+
The video reader instance.
|
437
|
+
"""
|
438
|
+
# TODO: This has to be dynamic, but
|
439
|
+
# according to what (?)
|
440
|
+
|
441
|
+
# TODO: Where do we obtain this from (?)
|
442
|
+
VIDEO_CODEC_NAME = 'libx264'
|
443
|
+
# TODO: Where do we obtain this from (?)
|
444
|
+
PIXEL_FORMAT = 'yuv420p'
|
445
|
+
self.writer: VideoWriter = (
|
446
|
+
VideoWriter(output_filename)
|
447
|
+
.set_video_stream(VIDEO_CODEC_NAME, self.reader.fps, self.reader.size, PIXEL_FORMAT)
|
448
|
+
.set_audio_stream_from_template(self.reader.audio_stream)
|
449
|
+
)
|
450
|
+
"""
|
451
|
+
The video writer instance.
|
452
|
+
"""
|
453
|
+
|
454
|
+
# TODO: This should be a utils
|
455
|
+
def frame_to_texture(
|
456
|
+
self,
|
457
|
+
frame: 'VideoFrame',
|
458
|
+
numpy_format: str = 'rgb24'
|
459
|
+
):
|
460
|
+
"""
|
461
|
+
Transform the given 'frame' to an opengl
|
462
|
+
texture.
|
463
|
+
"""
|
464
|
+
# To numpy RGB inverted for OpenGL
|
465
|
+
# TODO: Maybe we can receive normal frames
|
466
|
+
# here, as np.ndarray, from other libraries
|
467
|
+
frame: np.ndarray = np.flipud(frame.to_ndarray(format = numpy_format))
|
468
|
+
|
469
|
+
return self.context.context.texture((frame.shape[1], frame.shape[0]), 3, frame.tobytes())
|
470
|
+
|
471
|
+
def process(
|
472
|
+
self
|
473
|
+
):
|
474
|
+
"""
|
475
|
+
Process the video and generate the new one.
|
476
|
+
|
477
|
+
TODO: Should I pass some effects to apply (?)
|
478
|
+
"""
|
479
|
+
# [ 1 ] Initialize fbo and texture mipmaps
|
480
|
+
self.first_frame_as_texture # This forces it in the code
|
481
|
+
|
482
|
+
# [ 2 ] Set general program uniforms
|
483
|
+
AMP = 0.05
|
484
|
+
FREQ = 10.0
|
485
|
+
SPEED = 2.0
|
486
|
+
(
|
487
|
+
self.context.program
|
488
|
+
.set_value('amp', AMP)
|
489
|
+
.set_value('freq', FREQ)
|
490
|
+
.set_value('speed', SPEED)
|
491
|
+
)
|
492
|
+
|
493
|
+
# [ 3 ] Process the frames
|
494
|
+
frame_index = 0
|
495
|
+
for frame_or_packet in self.reader.iterate_with_audio(
|
496
|
+
do_decode_video = True,
|
497
|
+
do_decode_audio = False
|
498
|
+
):
|
499
|
+
# This below is because of the parameters we
|
500
|
+
# passed to the method
|
501
|
+
is_video_frame = PythonValidator.is_instance_of(frame_or_packet, 'VideoReaderFrame')
|
502
|
+
is_audio_packet = PythonValidator.is_instance_of(frame_or_packet, 'VideoReaderPacket')
|
503
|
+
|
504
|
+
# To simplify the process
|
505
|
+
if frame_or_packet is not None:
|
506
|
+
frame_or_packet = frame_or_packet.data
|
507
|
+
if is_audio_packet:
|
508
|
+
self.writer.mux(frame_or_packet)
|
509
|
+
elif is_video_frame:
|
510
|
+
with Timer(is_silent_as_context = True) as timer:
|
511
|
+
# Check this link:
|
512
|
+
# https://stackoverflow.com/a/63153755
|
513
|
+
|
514
|
+
def process_frame(
|
515
|
+
frame: 'VideoFrame'
|
516
|
+
):
|
517
|
+
# [ 4 ] Add specific program uniforms
|
518
|
+
self.program.set_value('time', T.video_frame_index_to_video_frame_time(frame_index, float(self.reader.fps)))
|
519
|
+
|
520
|
+
# Create texture
|
521
|
+
texture = self.frame_to_texture(frame)
|
522
|
+
texture.use()
|
523
|
+
|
524
|
+
# Activate frame buffer
|
525
|
+
self.fbo.use()
|
526
|
+
|
527
|
+
# Render, captured by the fbo
|
528
|
+
self.vao.render(moderngl.TRIANGLE_STRIP)
|
529
|
+
|
530
|
+
# Processed GPU result (from fbo) to numpy
|
531
|
+
processed_data = np.frombuffer(
|
532
|
+
self.fbo.read(components = 3, alignment = 1), dtype = np.uint8
|
533
|
+
)
|
534
|
+
|
535
|
+
# Invert numpy to normal frame
|
536
|
+
processed_data = np.flipud(
|
537
|
+
processed_data.reshape((texture.size[1], texture.size[0], 3))
|
538
|
+
)
|
539
|
+
|
540
|
+
# To VideoFrame and to buffer
|
541
|
+
frame = av.VideoFrame.from_ndarray(processed_data, format = NUMPY_FORMAT)
|
542
|
+
# TODO: What is this for (?)
|
543
|
+
#out_frame.pict_type = 'NONE'
|
544
|
+
|
545
|
+
return frame
|
546
|
+
|
547
|
+
self.writer.mux_video_frame(process_frame(frame_or_packet))
|
548
|
+
|
549
|
+
print(f'Frame {str(frame_index)}: {timer.time_elapsed_str}s')
|
550
|
+
frame_index += 1
|
551
|
+
|
552
|
+
# While this code can be finished, the work in
|
553
|
+
# the muxer could be not finished and have some
|
554
|
+
# packets waiting to be written. Here we tell
|
555
|
+
# the muxer to process all those packets.
|
556
|
+
self.writer.mux_video_frame(None)
|
557
|
+
|
558
|
+
# TODO: Maybe move this to the '__del__' (?)
|
559
|
+
self.writer.output.close()
|
560
|
+
self.reader.container.close()
|
561
|
+
print(f'Saved as "{self.output_filename}".')
|
562
|
+
|
17
563
|
def video_modified_stored():
|
18
|
-
|
564
|
+
# This path below was trimmed in an online platform
|
565
|
+
# and seems to be bad codified and generates error
|
566
|
+
# when processing it, but it is readable in the
|
567
|
+
# file explorer...
|
568
|
+
#VIDEO_PATH = 'test_files/test_1_short_broken.mp4'
|
569
|
+
# This is short but is working well
|
570
|
+
VIDEO_PATH = "test_files/test_1_short_2.mp4"
|
571
|
+
# Long version below, comment to test faster
|
572
|
+
#VIDEO_PATH = "test_files/test_1.mp4"
|
19
573
|
OUTPUT_PATH = "test_files/output.mp4"
|
574
|
+
# TODO: This has to be dynamic, but
|
575
|
+
# according to what (?)
|
576
|
+
NUMPY_FORMAT = 'rgb24'
|
577
|
+
# TODO: Where do we obtain this from (?)
|
578
|
+
VIDEO_CODEC_NAME = 'libx264'
|
579
|
+
# TODO: Where do we obtain this from (?)
|
580
|
+
PIXEL_FORMAT = 'yuv420p'
|
581
|
+
|
582
|
+
from yta_video_opengl.classes import WavingFrame, BreathingFrame, HandheldFrame, OrbitingFrame, RotatingInCenterFrame, StrangeTvFrame, GlitchRgbFrame
|
583
|
+
|
584
|
+
video = VideoReader(VIDEO_PATH)
|
585
|
+
video_writer = (
|
586
|
+
VideoWriter(OUTPUT_PATH)
|
587
|
+
#.set_video_stream(VIDEO_CODEC_NAME, video.fps, video.size, PIXEL_FORMAT)
|
588
|
+
.set_video_stream_from_template(video.video_stream)
|
589
|
+
.set_audio_stream_from_template(video.audio_stream)
|
590
|
+
)
|
591
|
+
|
592
|
+
#effect = WavingFrame(size = video.size)
|
593
|
+
#effect = BreathingFrame(size = video.size)
|
594
|
+
#effect = HandheldFrame(size = video.size)
|
595
|
+
# effect = OrbitingFrame(
|
596
|
+
# size = video.size,
|
597
|
+
# first_frame = video.next_frame
|
598
|
+
# )
|
599
|
+
# effect = RotatingInCenterFrame(
|
600
|
+
# size = video.size,
|
601
|
+
# first_frame = video.next_frame
|
602
|
+
# )
|
603
|
+
effect = GlitchRgbFrame(
|
604
|
+
size = video.size,
|
605
|
+
first_frame = video.next_frame
|
606
|
+
)
|
607
|
+
# We need to reset it to being again pointing
|
608
|
+
# to the first frame...
|
609
|
+
# TODO: Improve this by, maybe, storing the first
|
610
|
+
# frame in memory so we can append it later, or
|
611
|
+
# using the '.seek(0)' even when it could be not
|
612
|
+
# accurate
|
613
|
+
video.reset()
|
614
|
+
|
615
|
+
frame_index = 0
|
616
|
+
for frame_or_packet in video.iterate_with_audio(
|
617
|
+
do_decode_video = True,
|
618
|
+
do_decode_audio = False
|
619
|
+
):
|
620
|
+
# This below is because of the parameters we
|
621
|
+
# passed to the method
|
622
|
+
is_video_frame = PythonValidator.is_instance_of(frame_or_packet, 'VideoReaderFrame')
|
623
|
+
is_audio_packet = PythonValidator.is_instance_of(frame_or_packet, 'VideoReaderPacket')
|
624
|
+
|
625
|
+
# To simplify the process
|
626
|
+
if frame_or_packet is not None:
|
627
|
+
frame_or_packet = frame_or_packet.data
|
628
|
+
|
629
|
+
if is_audio_packet:
|
630
|
+
video_writer.mux(frame_or_packet)
|
631
|
+
elif is_video_frame:
|
632
|
+
with Timer(is_silent_as_context = True) as timer:
|
633
|
+
t = T.video_frame_index_to_video_frame_time(frame_index, float(video.fps))
|
634
|
+
|
635
|
+
video_writer.mux_video_frame(
|
636
|
+
effect.process_frame(
|
637
|
+
frame = frame_or_packet,
|
638
|
+
t = t,
|
639
|
+
numpy_format = NUMPY_FORMAT
|
640
|
+
)
|
641
|
+
)
|
642
|
+
|
643
|
+
frame_index += 1
|
644
|
+
|
645
|
+
print(f'Frame {str(frame_index)}: {timer.time_elapsed_str}s')
|
646
|
+
|
647
|
+
video_writer.mux_video_frame(None)
|
648
|
+
|
649
|
+
# TODO: Maybe move this to the '__del__' (?)
|
650
|
+
video_writer.output.close()
|
651
|
+
video.container.close()
|
652
|
+
print(f'Saved as "{OUTPUT_PATH}".')
|
653
|
+
|
654
|
+
return
|
655
|
+
|
656
|
+
# # TODO: By now this is applying an effect
|
657
|
+
# # by default
|
658
|
+
# VideoProcessor(
|
659
|
+
# filename = VIDEO_PATH,
|
660
|
+
# output_filename = OUTPUT_PATH
|
661
|
+
# ).process()
|
662
|
+
|
663
|
+
# return
|
664
|
+
|
665
|
+
|
20
666
|
AMP = 0.05
|
21
667
|
FREQ = 10.0
|
22
668
|
SPEED = 2.0
|
23
669
|
|
670
|
+
# Get the information about the video
|
671
|
+
video = VideoReader(VIDEO_PATH)
|
672
|
+
|
24
673
|
# ModernGL context without window
|
25
674
|
context = moderngl.create_standalone_context()
|
26
675
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
#version 330
|
31
|
-
in vec2 in_pos;
|
32
|
-
in vec2 in_uv;
|
33
|
-
out vec2 v_uv;
|
34
|
-
void main() {
|
35
|
-
v_uv = in_uv;
|
36
|
-
gl_Position = vec4(in_pos, 0.0, 1.0);
|
37
|
-
}
|
38
|
-
''',
|
39
|
-
fragment_shader = '''
|
40
|
-
#version 330
|
41
|
-
uniform sampler2D tex;
|
42
|
-
uniform float time;
|
43
|
-
uniform float amp;
|
44
|
-
uniform float freq;
|
45
|
-
uniform float speed;
|
46
|
-
in vec2 v_uv;
|
47
|
-
out vec4 f_color;
|
48
|
-
void main() {
|
49
|
-
float wave = sin(v_uv.x * freq + time * speed) * amp;
|
50
|
-
vec2 uv = vec2(v_uv.x, v_uv.y + wave);
|
51
|
-
f_color = texture(tex, uv);
|
52
|
-
}
|
53
|
-
'''
|
676
|
+
waving_frame_effect = WavingFrame(
|
677
|
+
context = context,
|
678
|
+
frame_size = video.size
|
54
679
|
)
|
55
680
|
|
56
|
-
|
57
|
-
vertices = np.array([
|
58
|
-
-1, -1, 0.0, 0.0,
|
59
|
-
1, -1, 1.0, 0.0,
|
60
|
-
-1, 1, 0.0, 1.0,
|
61
|
-
1, 1, 1.0, 1.0,
|
62
|
-
], dtype = 'f4')
|
63
|
-
vbo = context.buffer(vertices.tobytes())
|
64
|
-
vao = context.simple_vertex_array(program, vbo, 'in_pos', 'in_uv')
|
65
|
-
|
66
|
-
video = VideoReader(VIDEO_PATH)
|
67
|
-
|
68
|
-
print(video.number_of_frames)
|
69
|
-
print(video.number_of_audio_frames)
|
681
|
+
vao = waving_frame_effect.vao
|
70
682
|
|
71
683
|
# TODO: This has to be dynamic, but
|
72
684
|
# according to what (?)
|
@@ -77,24 +689,38 @@ def video_modified_stored():
|
|
77
689
|
PIXEL_FORMAT = 'yuv420p'
|
78
690
|
|
79
691
|
# Framebuffer to render
|
80
|
-
fbo =
|
692
|
+
fbo = waving_frame_effect.fbo
|
81
693
|
fbo.use()
|
82
694
|
|
83
695
|
# Decode first frame and use as texture
|
84
696
|
first_frame = video.next_frame
|
697
|
+
# We need to reset it to being again pointing
|
698
|
+
# to the first frame...
|
699
|
+
# TODO: Improve this by, maybe, storing the first
|
700
|
+
# frame in memory so we can append it later, or
|
701
|
+
# using the '.seek(0)' even when it could be not
|
702
|
+
# accurate
|
703
|
+
video = VideoReader(VIDEO_PATH)
|
85
704
|
|
86
705
|
# Most of OpenGL textures expect origin in lower
|
87
706
|
# left corner
|
88
707
|
# TODO: What if alpha (?)
|
89
|
-
|
90
|
-
|
91
|
-
|
708
|
+
# TODO: Move this to the OpenglFrameEffect maybe (?)
|
709
|
+
|
710
|
+
texture: moderngl.Texture = frame_to_texture(first_frame, waving_frame_effect.context)
|
92
711
|
texture.build_mipmaps()
|
93
712
|
|
94
|
-
#
|
95
|
-
|
96
|
-
|
97
|
-
|
713
|
+
# These properties can be set before
|
714
|
+
# iterating the frames or maybe for
|
715
|
+
# each iteration... depending on the
|
716
|
+
# effect.
|
717
|
+
# Uniforms (properties)
|
718
|
+
(
|
719
|
+
waving_frame_effect
|
720
|
+
.set_value('amp', AMP)
|
721
|
+
.set_value('freq', FREQ)
|
722
|
+
.set_value('speed', SPEED)
|
723
|
+
)
|
98
724
|
|
99
725
|
# Writer with H.264 codec
|
100
726
|
video_writer = (
|
@@ -128,15 +754,10 @@ def video_modified_stored():
|
|
128
754
|
# Add some variables if we need, for the
|
129
755
|
# opengl change we are applying (check the
|
130
756
|
# program code)
|
131
|
-
|
757
|
+
waving_frame_effect.set_value('time', T.video_frame_index_to_video_frame_time(frame_index, float(video.fps)))
|
132
758
|
|
133
|
-
# To numpy RGB inverted for OpenGL
|
134
|
-
img_array = np.flipud(
|
135
|
-
frame.to_ndarray(format = NUMPY_FORMAT)
|
136
|
-
)
|
137
|
-
|
138
759
|
# Create texture
|
139
|
-
texture =
|
760
|
+
texture = frame_to_texture(frame, waving_frame_effect.context)
|
140
761
|
texture.use()
|
141
762
|
|
142
763
|
# Render with shader to frame buffer
|
@@ -147,9 +768,12 @@ def video_modified_stored():
|
|
147
768
|
processed_data = np.frombuffer(
|
148
769
|
fbo.read(components = 3, alignment = 1), dtype = np.uint8
|
149
770
|
)
|
771
|
+
|
150
772
|
# Invert numpy to normal frame
|
773
|
+
# TODO: Can I use the texture.size to fill
|
774
|
+
# these 'img_array.shape[0]' (?)
|
151
775
|
processed_data = np.flipud(
|
152
|
-
processed_data.reshape((
|
776
|
+
processed_data.reshape((texture.size[1], texture.size[0], 3))
|
153
777
|
)
|
154
778
|
|
155
779
|
# To VideoFrame and to buffer
|