yta-video-opengl 0.0.8__tar.gz → 0.0.10__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.8
3
+ Version: 0.0.10
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.8"
3
+ version = "0.0.10"
4
4
  description = "Youtube Autonomous Video OpenGL Module"
5
5
  authors = [
6
6
  {name = "danialcala94",email = "danielalcalavalera@gmail.com"}
@@ -132,6 +132,7 @@ class _Uniforms:
132
132
  for key, value in self.uniforms.items():
133
133
  print(f'"{key}": {str(value)}')
134
134
 
135
+ # TODO: Moved to 'nodes.opengl.py'
135
136
  class BaseNode:
136
137
  """
137
138
  The basic class of a node to manipulate frames
@@ -0,0 +1,119 @@
1
+ from yta_validation.parameter import ParameterValidator
2
+ from typing import Union
3
+ from abc import ABC, abstractmethod
4
+
5
+ import av
6
+ import moderngl
7
+
8
+
9
+ class Node(ABC):
10
+ """
11
+ Base class to represent a node, which
12
+ is an entity that processes frames
13
+ individually.
14
+
15
+ This class must be inherited by any
16
+ video or audio node class.
17
+ """
18
+
19
+ # TODO: What about the types?
20
+ @abstractmethod
21
+ def process(
22
+ frame: Union[av.VideoFrame, av.AudioFrame, moderngl.Texture],
23
+ t: float
24
+ # TODO: Maybe we need 'fps' and 'number_of_frames'
25
+ # to calculate progressions or similar...
26
+ ) -> Union[av.VideoFrame, av.AudioFrame, moderngl.Texture]:
27
+ pass
28
+
29
+ class TimedNode:
30
+ """
31
+ Class to represent a Node wrapper to
32
+ be able to specify the time range in
33
+ which we want the node to be applied.
34
+
35
+ If the 't' time moment is not inside
36
+ this range, the frame will be returned
37
+ as it is, with no change.
38
+
39
+ A 't' time moment inside the range has
40
+ this condition:
41
+ - `start <= t < end`
42
+
43
+ We are not including the end because
44
+ the next TimedNode could start on that
45
+ specific value, and remember that the
46
+ first time moment is 0.
47
+
48
+ This is the class that has to be applied
49
+ when working with videos and not a Node
50
+ directly.
51
+
52
+ The 'start' and 'end' values by default
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ node: Node,
58
+ start: float = 0.0,
59
+ end: Union[float, None] = None
60
+ ):
61
+ ParameterValidator.validate_mandatory_positive_number('start', start, do_include_zero = True)
62
+ ParameterValidator.validate_positive_number('end', end, do_include_zero = False)
63
+
64
+ if (
65
+ end is not None and
66
+ end < start
67
+ ):
68
+ raise Exception('The "end" parameter provided must be greater or equal to the "start" parameter.')
69
+
70
+ self.node: Node = node
71
+ """
72
+ The node we are wrapping and we want to
73
+ apply as a modification of the frame in
74
+ which we are in a 't' time moment.
75
+ """
76
+ self.start: float = start
77
+ """
78
+ The 't' time moment in which the Node must
79
+ start being applied (including it).
80
+ """
81
+ self.end: Union[float, None] = end
82
+ """
83
+ The 't' time moment in which the Node must
84
+ stop being applied (excluding it).
85
+ """
86
+
87
+ def is_within_time(
88
+ self,
89
+ t: float
90
+ ) -> bool:
91
+ """
92
+ Flag to indicate if the 't' time moment provided
93
+ is in the range of this TimedNode instance,
94
+ which means that it fits this condition:
95
+ - `start <= t < end`
96
+ """
97
+ return (
98
+ self.start <= t < self.end
99
+ if self.end is not None else
100
+ self.start <= t
101
+ )
102
+
103
+ def process(
104
+ self,
105
+ frame: Union[av.VideoFrame, av.AudioFrame, moderngl.Texture],
106
+ t: float
107
+ # TODO: Maybe we need 'fps' and 'number_of_frames'
108
+ # to calculate progressions or similar...
109
+ ) -> Union['VideoFrame', 'AudioFrame', 'Texture']:
110
+ """
111
+ Process the frame if the provided 't' time
112
+ moment is in the range of this TimedNode
113
+ instance.
114
+ """
115
+ return (
116
+ self.node.process(frame, t)
117
+ if self.is_within_time(t) else
118
+ frame
119
+ )
@@ -0,0 +1,115 @@
1
+ """
2
+ When working with audio frames, we don't need
3
+ to use the GPU because audios are 1D and the
4
+ information can be processed perfectly with
5
+ a library like numpy.
6
+
7
+ If we need a very intense calculation for an
8
+ audio frame (FFT, convolution, etc.) we can
9
+ use CuPy or some DPS specific libraries, but
10
+ 90% is perfectly done with numpy.
11
+
12
+ If you want to modify huge amounts of audio
13
+ (some seconds at the same time), you can use
14
+ CuPy, that has the same API as numpy but
15
+ working in GPU. Doing this below most of the
16
+ changes would work:
17
+ - `import numpy as np` → `import cupy as np`
18
+ """
19
+ from yta_video_opengl.nodes import TimedNode
20
+ from abc import abstractmethod
21
+ from typing import Union
22
+
23
+ import numpy as np
24
+ import av
25
+
26
+
27
+ class AudioNode:
28
+ """
29
+ Base audio node class to implement a
30
+ change in an audio frame by using the
31
+ numpy library.
32
+ """
33
+
34
+ @abstractmethod
35
+ def process(
36
+ self,
37
+ frame: av.AudioFrame,
38
+ t: float
39
+ ):
40
+ """
41
+ Process the provided audio 'frame' that
42
+ is played on the given 't' time moment.
43
+ """
44
+ pass
45
+
46
+ """
47
+ Here you have an example. The 'private'
48
+ node class is the modifier, that we don't
49
+ want to expose, and the 'public' class is
50
+ the one that inherits from TimedNode and
51
+ wraps the 'private' class to build the
52
+ functionality.
53
+ """
54
+ class VolumeAudioNode(TimedNode):
55
+ """
56
+ TimedNode to set the audio volume of a video
57
+ in a specific frame.
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ factor_fn,
63
+ start: float = 0.0,
64
+ end: Union[float, None] = None
65
+ ):
66
+ super().__init__(
67
+ node = _SetVolumeAudioNode(factor_fn),
68
+ start = start,
69
+ end = end
70
+ )
71
+
72
+ class _SetVolumeAudioNode(AudioNode):
73
+ """
74
+ Audio node to change the volume of an
75
+ audio frame.
76
+ """
77
+
78
+ def __init__(
79
+ self,
80
+ factor_fn
81
+ ):
82
+ """
83
+ factor_fn: function (t, index) -> factor volumen
84
+ """
85
+ self.factor_fn = factor_fn
86
+
87
+ def process(
88
+ self,
89
+ frame: av.AudioFrame,
90
+ t: float,
91
+ ) -> av.AudioFrame:
92
+ # TODO: Why index (?) Maybe 'total_frames'
93
+ factor = self.factor_fn(t, 0)
94
+
95
+ samples = frame.to_ndarray().astype(np.float32)
96
+ samples *= factor
97
+
98
+ # Determine dtype according to format
99
+ samples = (
100
+ samples.astype(np.int16)
101
+ # 'fltp', 's16', 's16p'
102
+ if 's16' in frame.format.name else
103
+ samples.astype(np.float32)
104
+ )
105
+
106
+ new_frame = av.AudioFrame.from_ndarray(
107
+ samples,
108
+ format = frame.format.name,
109
+ layout = frame.layout.name
110
+ )
111
+ new_frame.sample_rate = frame.sample_rate
112
+ new_frame.pts = frame.pts
113
+ new_frame.time_base = frame.time_base
114
+
115
+ return new_frame
@@ -0,0 +1,5 @@
1
+ """
2
+ Working with video frames has to be done
3
+ with nodes that use OpenGL because this
4
+ way, by using the GPU, is the best way.
5
+ """
@@ -0,0 +1,309 @@
1
+ from yta_video_opengl.utils import frame_to_texture, get_fullscreen_quad_vao
2
+ from yta_video_opengl.nodes import Node
3
+ from yta_validation.parameter import ParameterValidator
4
+ from yta_validation import PythonValidator
5
+ from abc import abstractmethod
6
+ from typing import Union
7
+
8
+ import numpy as np
9
+ import moderngl
10
+
11
+
12
+ class _Uniforms:
13
+ """
14
+ Class to wrap the functionality related to
15
+ handling the opengl program uniforms.
16
+ """
17
+
18
+ @property
19
+ def uniforms(
20
+ self
21
+ ) -> dict:
22
+ """
23
+ The uniforms in the program, as a dict, in
24
+ the format `{key, value}`.
25
+ """
26
+ return {
27
+ key: self.program[key].value
28
+ for key in self.program
29
+ if PythonValidator.is_instance_of(self.program[key], moderngl.Uniform)
30
+ }
31
+
32
+ def __init__(
33
+ self,
34
+ program: moderngl.Program
35
+ ):
36
+ self.program: moderngl.Program = program
37
+ """
38
+ The program instance this handler class
39
+ belongs to.
40
+ """
41
+
42
+ def get(
43
+ self,
44
+ name: str
45
+ ) -> Union[any, None]:
46
+ """
47
+ Get the value of the uniform with the
48
+ given 'name'.
49
+ """
50
+ return self.uniforms.get(name, None)
51
+
52
+ # TODO: I need to refactor these method to
53
+ # accept a **kwargs maybe, or to auto-detect
54
+ # the type and add the uniform as it must be
55
+ # done
56
+ def set(
57
+ self,
58
+ name: str,
59
+ value
60
+ ) -> '_Uniforms':
61
+ """
62
+ Set the provided 'value' to the normal type
63
+ uniform with the given 'name'. Here you have
64
+ some examples of defined uniforms we can set
65
+ with this method:
66
+ - `uniform float name;`
67
+
68
+ TODO: Add more examples
69
+ """
70
+ if name in self.program:
71
+ self.program[name].value = value
72
+
73
+ return self
74
+
75
+ def set_vec(
76
+ self,
77
+ name: str,
78
+ values
79
+ ) -> '_Uniforms':
80
+ """
81
+ Set the provided 'value' to the normal type
82
+ uniform with the given 'name'. Here you have
83
+ some examples of defined uniforms we can set
84
+ with this method:
85
+ - `uniform vec2 name;`
86
+
87
+ TODO: Is this example ok? I didn't use it yet
88
+ """
89
+ if name in self.program:
90
+ self.program[name].write(np.array(values, dtype = 'f4').tobytes())
91
+
92
+ return self
93
+
94
+ def set_mat(
95
+ self,
96
+ name: str,
97
+ value
98
+ ) -> '_Uniforms':
99
+ """
100
+ Set the provided 'value' to a `matN` type
101
+ uniform with the given 'name'. The 'value'
102
+ must be a NxN matrix (maybe numpy array)
103
+ transformed to bytes ('.tobytes()').
104
+
105
+ This uniform must be defined in the vertex
106
+ like this:
107
+ - `uniform matN name;`
108
+
109
+ TODO: Maybe we can accept a NxN numpy
110
+ array and do the .tobytes() by ourselves...
111
+ """
112
+ if name in self.program:
113
+ self.program[name].write(value)
114
+
115
+ return self
116
+
117
+ def print(
118
+ self
119
+ ) -> '_Uniforms':
120
+ """
121
+ Print the defined uniforms in console.
122
+ """
123
+ for key, value in self.uniforms.items():
124
+ print(f'"{key}": {str(value)}')
125
+
126
+ class OpenglNode(Node):
127
+ """
128
+ The basic class of a node to manipulate frames
129
+ as opengl textures. This node will process the
130
+ frame as an input texture and will generate
131
+ also a texture as the output.
132
+
133
+ Nodes can be chained and the result from one
134
+ node can be applied on another node.
135
+ """
136
+
137
+ @property
138
+ @abstractmethod
139
+ def vertex_shader(
140
+ self
141
+ ) -> str:
142
+ """
143
+ The code of the vertex shader.
144
+ """
145
+ pass
146
+
147
+ @property
148
+ @abstractmethod
149
+ def fragment_shader(
150
+ self
151
+ ) -> str:
152
+ """
153
+ The code of the fragment shader.
154
+ """
155
+ pass
156
+
157
+ def __init__(
158
+ self,
159
+ context: moderngl.Context,
160
+ size: tuple[int, int],
161
+ **kwargs
162
+ ):
163
+ ParameterValidator.validate_mandatory_instance_of('context', context, moderngl.Context)
164
+ # TODO: Validate size
165
+
166
+ self.context: moderngl.Context = context
167
+ """
168
+ The context of the program.
169
+ """
170
+ self.size: tuple[int, int] = size
171
+ """
172
+ The size we want to use for the frame buffer
173
+ in a (width, height) format.
174
+ """
175
+ # Compile shaders within the program
176
+ self.program: moderngl.Program = self.context.program(
177
+ vertex_shader = self.vertex_shader,
178
+ fragment_shader = self.fragment_shader
179
+ )
180
+
181
+ # Create the fullscreen quad
182
+ self.quad = get_fullscreen_quad_vao(
183
+ context = self.context,
184
+ program = self.program
185
+ )
186
+
187
+ # Create the output fbo
188
+ self.output_tex = self.context.texture(self.size, 4)
189
+ self.output_tex.filter = (moderngl.LINEAR, moderngl.LINEAR)
190
+ self.fbo = self.context.framebuffer(color_attachments = [self.output_tex])
191
+
192
+ self.uniforms: _Uniforms = _Uniforms(self.program)
193
+ """
194
+ Shortcut to the uniforms functionality.
195
+ """
196
+ # Auto set uniforms dynamically if existing
197
+ for key, value in kwargs.items():
198
+ self.uniforms.set(key, value)
199
+
200
+ def process(
201
+ self,
202
+ input: Union[moderngl.Texture, 'VideoFrame', 'np.ndarray']
203
+ ) -> moderngl.Texture:
204
+ """
205
+ Apply the shader to the 'input', that
206
+ must be a frame or a texture, and return
207
+ the new resulting texture.
208
+
209
+ We use and return textures to maintain
210
+ the process in GPU and optimize it.
211
+ """
212
+ if PythonValidator.is_instance_of(input, ['VideoFrame', 'ndarray']):
213
+ # TODO: What about the numpy format (?)
214
+ input = frame_to_texture(input, self.context)
215
+
216
+ self.fbo.use()
217
+ self.context.clear(0.0, 0.0, 0.0, 0.0)
218
+
219
+ input.use(location = 0)
220
+
221
+ if 'texture' in self.program:
222
+ self.program['texture'] = 0
223
+
224
+ self.quad.render()
225
+
226
+ return self.output_tex
227
+
228
+ class WavingNode(OpenglNode):
229
+ """
230
+ Just an example, without the shaders code
231
+ actually, to indicate that we can use
232
+ custom parameters to make it work.
233
+ """
234
+
235
+ @property
236
+ def vertex_shader(
237
+ self
238
+ ) -> str:
239
+ return (
240
+ '''
241
+ #version 330
242
+ in vec2 in_vert;
243
+ in vec2 in_texcoord;
244
+ out vec2 v_uv;
245
+ void main() {
246
+ v_uv = in_texcoord;
247
+ gl_Position = vec4(in_vert, 0.0, 1.0);
248
+ }
249
+ '''
250
+ )
251
+
252
+ @property
253
+ def fragment_shader(
254
+ self
255
+ ) -> str:
256
+ return (
257
+ '''
258
+ #version 330
259
+ uniform sampler2D tex;
260
+ uniform float time;
261
+ uniform float amplitude;
262
+ uniform float frequency;
263
+ uniform float speed;
264
+ in vec2 v_uv;
265
+ out vec4 f_color;
266
+ void main() {
267
+ float wave = sin(v_uv.x * frequency + time * speed) * amplitude;
268
+ vec2 uv = vec2(v_uv.x, v_uv.y + wave);
269
+ f_color = texture(tex, uv);
270
+ }
271
+ '''
272
+ )
273
+
274
+ def __init__(
275
+ self,
276
+ context: moderngl.Context,
277
+ size: tuple[int, int],
278
+ amplitude: float = 0.05,
279
+ frequency: float = 10.0,
280
+ speed: float = 2.0
281
+ ):
282
+ super().__init__(
283
+ context = context,
284
+ size = size,
285
+ amplitude = amplitude,
286
+ frequency = frequency,
287
+ speed = speed
288
+ )
289
+
290
+ # This is just an example and we are not
291
+ # using the parameters actually, but we
292
+ # could set those specific uniforms to be
293
+ # processed by the code
294
+ def process(
295
+ self,
296
+ input: Union[moderngl.Texture, 'VideoFrame', 'np.ndarray'],
297
+ t: float = 0.0,
298
+ ) -> moderngl.Texture:
299
+ """
300
+ Apply the shader to the 'input', that
301
+ must be a frame or a texture, and return
302
+ the new resulting texture.
303
+
304
+ We use and return textures to maintain
305
+ the process in GPU and optimize it.
306
+ """
307
+ self.uniforms.set('time', t)
308
+
309
+ return super().process(input)
@@ -398,9 +398,14 @@ class VideoReader:
398
398
  """
399
399
  The stream that includes the audio.
400
400
  """
401
- self.cache: VideoFrameCache = None
401
+ self.video_cache: VideoFrameCache = None
402
402
  """
403
- The frame cache system to optimize
403
+ The video frame cache system to optimize
404
+ the way we access to the frames.
405
+ """
406
+ self.audio_cache: VideoFrameCache = None
407
+ """
408
+ The audio frame cache system to optimize
404
409
  the way we access to the frames.
405
410
  """
406
411
 
@@ -434,7 +439,8 @@ class VideoReader:
434
439
  self.video_stream.thread_type = 'AUTO'
435
440
  self.audio_stream = self.container.streams.audio[0]
436
441
  self.audio_stream.thread_type = 'AUTO'
437
- self.cache = VideoFrameCache(self)
442
+ self.video_cache = VideoFrameCache(self.container, self.video_stream)
443
+ self.audio_cache = VideoFrameCache(self.container, self.audio_stream)
438
444
 
439
445
  def seek(
440
446
  self,
@@ -564,10 +570,45 @@ class VideoReader:
564
570
  index: int
565
571
  ) -> 'VideoFrame':
566
572
  """
567
- Get the frame with the given 'index', using
568
- the cache system.
573
+ Get the video frame with the given 'index',
574
+ using the video cache system.
575
+ """
576
+ return self.video_cache.get_frame(index)
577
+
578
+ # TODO: Will we use this (?)
579
+ def get_audio_frame(
580
+ self,
581
+ index: int
582
+ ) -> 'VideoFrame':
583
+ """
584
+ Get the audio frame with the given 'index',
585
+ using the audio cache system.
586
+ """
587
+ return self.video_cache.get_frame(index)
588
+
589
+ def get_frames(
590
+ self,
591
+ start: float = 0.0,
592
+ end: Union[float, None] = None
593
+ ):
569
594
  """
570
- return self.cache.get_frame(index)
595
+ Iterator to get the video frames in between
596
+ the provided 'start' and 'end' time moments.
597
+ """
598
+ for frame in self.video_cache.get_frames(start, end):
599
+ yield frame
600
+
601
+ def get_audio_frames(
602
+ self,
603
+ start: float = 0.0,
604
+ end: Union[float, None] = None
605
+ ):
606
+ """
607
+ Iterator to get the audio frames in between
608
+ the provided 'start' and 'end' time moments.
609
+ """
610
+ for frame in self.audio_cache.get_frames(start, end):
611
+ yield frame
571
612
 
572
613
  def close(
573
614
  self
@@ -0,0 +1,233 @@
1
+ """
2
+ The pyav container stores the information based
3
+ on the packets timestamps (called 'pts'). Some
4
+ of the packets are considered key_frames because
5
+ they include those key frames.
6
+
7
+ Also, this library uses those key frames to start
8
+ decodifying from there to the next one, obtaining
9
+ all the frames in between able to be read and
10
+ modified.
11
+
12
+ This cache system will look for the range of
13
+ frames that belong to the key frame related to the
14
+ frame we are requesting in the moment, keeping in
15
+ memory all those frames to be handled fast. It
16
+ will remove the old frames if needed to use only
17
+ the 'size' we set when creating it.
18
+ """
19
+ from yta_video_opengl.utils import t_to_pts, pts_to_t, pts_to_index
20
+ from av.container import InputContainer
21
+ from av.video.stream import VideoStream
22
+ from av.audio.stream import AudioStream
23
+ from av.video.frame import VideoFrame
24
+ from av.audio.frame import AudioFrame
25
+ from yta_validation.parameter import ParameterValidator
26
+ from fractions import Fraction
27
+ from collections import OrderedDict
28
+ from typing import Union
29
+
30
+
31
+ class VideoFrameCache:
32
+ """
33
+ Class to manage the frames cache of a video
34
+ within a video reader instance.
35
+ """
36
+
37
+ @property
38
+ def fps(
39
+ self
40
+ ) -> float:
41
+ """
42
+ The frames per second as a float.
43
+ """
44
+ return (
45
+ float(self.stream.average_rate)
46
+ if self.stream.type == 'video' else
47
+ float(self.stream.rate)
48
+ )
49
+
50
+ @property
51
+ def time_base(
52
+ self
53
+ ) -> Union[Fraction, None]:
54
+ """
55
+ The time base of the stream.
56
+ """
57
+ return self.stream.time_base
58
+
59
+ def __init__(
60
+ self,
61
+ container: InputContainer,
62
+ stream: Union[VideoStream, AudioStream],
63
+ size: int = 50
64
+ ):
65
+ ParameterValidator.validate_mandatory_instance_of('container', container, InputContainer)
66
+ ParameterValidator.validate_mandatory_instance_of('stream', stream, [VideoStream, AudioStream])
67
+ ParameterValidator.validate_mandatory_positive_int('size', size)
68
+
69
+ self.container: InputContainer = container
70
+ """
71
+ The pyav container.
72
+ """
73
+ self.stream: Union[VideoStream, AudioStream] = stream
74
+ """
75
+ The pyav stream.
76
+ """
77
+ self.cache: OrderedDict = OrderedDict()
78
+ """
79
+ The cache ordered dictionary.
80
+ """
81
+ self.size = size
82
+ """
83
+ The size (in number of frames) of the cache.
84
+ """
85
+ self.key_frames_pts: list[int] = []
86
+ """
87
+ The list that contains the timestamps of the
88
+ key frame packets, ordered from begining to
89
+ end.
90
+ """
91
+
92
+ self._prepare()
93
+
94
+ def _prepare(
95
+ self
96
+ ):
97
+ # Index key frames
98
+ for packet in self.container.demux(self.stream):
99
+ if packet.is_keyframe:
100
+ self.key_frames_pts.append(packet.pts)
101
+
102
+ self.container.seek(0)
103
+
104
+ def _get_nearest_keyframe_fps(
105
+ self,
106
+ pts: int
107
+ ):
108
+ """
109
+ Get the fps of the keyframe that is the
110
+ nearest to the provided 'pts'. Useful to
111
+ seek and start decoding frames from that
112
+ keyframe.
113
+ """
114
+ return max([
115
+ key_frame_pts
116
+ for key_frame_pts in self.key_frames_pts
117
+ if key_frame_pts <= pts
118
+ ])
119
+
120
+ def _get_frame_by_pts(
121
+ self,
122
+ pts: int
123
+ ):
124
+ """
125
+ Get the frame that has the provided 'pts'.
126
+
127
+ This method will start decoding frames from the
128
+ most near key frame (the one with the nearer
129
+ pts) until the one requested is found. All those
130
+ frames will be stored in cache.
131
+
132
+ This method must be called when the frame
133
+ requested is not stored in the caché.
134
+ """
135
+ # Look for the most near key frame
136
+ key_frame_pts = self._get_nearest_keyframe_fps(pts)
137
+
138
+ # Go to the key frame that includes it
139
+ self.container.seek(key_frame_pts, stream = self.stream)
140
+
141
+ decoded = None
142
+ for frame in self.container.decode(self.stream):
143
+ # TODO: Could 'frame' be None (?)
144
+ if frame.pts is None:
145
+ continue
146
+
147
+ # Store in cache if needed
148
+ if frame.pts not in self.cache:
149
+ # TODO: The 'format' must be dynamic
150
+ self.cache[frame.pts] = frame.to_ndarray(format = "rgb24")
151
+
152
+ # Clean cache if full
153
+ if len(self.cache) > self.size:
154
+ self.cache.popitem(last = False)
155
+
156
+ if frame.pts >= pts:
157
+ decoded = self.cache[frame.pts]
158
+ break
159
+
160
+ return decoded
161
+
162
+ def get_frame(
163
+ self,
164
+ index: int
165
+ ) -> Union[VideoFrame, AudioFrame]:
166
+ """
167
+ Get the frame with the given 'index' from
168
+ the cache.
169
+ """
170
+ # TODO: Maybe we can accept 't' and 'pts' also
171
+ target_pts = int(index / self.fps / self.time_base)
172
+
173
+ return (
174
+ self.cache[target_pts]
175
+ if target_pts in self.cache else
176
+ self._get_frame_by_pts(target_pts)
177
+ )
178
+
179
+ def get_frames(
180
+ self,
181
+ start: float = 0,
182
+ end: Union[float, None] = None
183
+ ):
184
+ """
185
+ Get all the frames in the range between
186
+ the provided 'start' and 'end' time in
187
+ seconds.
188
+ """
189
+ # TODO: I create this method by default using
190
+ # the cache. Think about how to implement it
191
+ # and apply it here, please.
192
+ # Go to the nearest key frame
193
+ start = t_to_pts(start, self.time_base)
194
+ end = (
195
+ t_to_pts(end, self.time_base)
196
+ if end is not None else
197
+ None
198
+ )
199
+ key_frame_pts = self._get_nearest_keyframe_fps(start)
200
+
201
+ # Go to the nearest key frame to start decoding
202
+ self.container.seek(key_frame_pts, stream = self.stream)
203
+
204
+ for packet in self.container.demux(self.stream):
205
+ for frame in packet.decode():
206
+ if frame.pts is None:
207
+ continue
208
+
209
+ if frame.pts < start:
210
+ continue
211
+
212
+ if (
213
+ end is not None and
214
+ frame.pts > end
215
+ ):
216
+ return
217
+
218
+ # TODO: Maybe send a @dataclass instead (?)
219
+ yield (
220
+ frame,
221
+ pts_to_t(frame.pts, self.time_base),
222
+ pts_to_index(frame.pts, self.time_base, self.fps)
223
+ )
224
+
225
+ def clear(
226
+ self
227
+ ) -> 'VideoFrameCache':
228
+ """
229
+ Clear the cache by removing all the items.
230
+ """
231
+ self.cache.clear()
232
+
233
+ return self
@@ -583,7 +583,7 @@ def video_modified_stored():
583
583
  from yta_video_opengl.utils import texture_to_frame, frame_to_texture
584
584
  from yta_video_opengl.video import Video
585
585
 
586
- Video(VIDEO_PATH, 0, 0.5).save_as(OUTPUT_PATH)
586
+ Video(VIDEO_PATH, 0.25, 0.75).save_as(OUTPUT_PATH)
587
587
 
588
588
  return
589
589
 
@@ -22,6 +22,9 @@ class Video:
22
22
  """
23
23
  The start packet time stamp (pts), needed
24
24
  to optimize the packet iteration process.
25
+
26
+ This timestamp is used to read the video
27
+ file source.
25
28
  """
26
29
  return int(self.start / self.reader.time_base)
27
30
 
@@ -32,6 +35,9 @@ class Video:
32
35
  """
33
36
  The end packet time stamp (pts), needed to
34
37
  optimize the packet iteration process.
38
+
39
+ This timestamp is used to read the video
40
+ file source.
35
41
  """
36
42
  return (
37
43
  int(self.end / self.reader.time_base)
@@ -73,6 +79,15 @@ class Video:
73
79
  The duration of the video.
74
80
  """
75
81
  return self.end - self.start
82
+
83
+ @property
84
+ def number_of_frames(
85
+ self
86
+ ):
87
+ """
88
+ The number of frames of the video.
89
+ """
90
+ return self.reader.number_of_frames
76
91
 
77
92
  @property
78
93
  def frames(
@@ -84,20 +99,19 @@ class Video:
84
99
  'start' and 'end' parameters provided when
85
100
  instantiating it.
86
101
 
102
+ The iterator will iterate first over the
103
+ video frames, and once finished over the
104
+ audio frames.
105
+
87
106
  This method returns a tuple of 3 elements:
88
107
  - `frame` as a `VideoFrame` instance
89
108
  - `t` as the frame time moment
90
109
  - `index` as the frame index
91
110
  """
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
- ):
111
+ for frame in self.reader.get_frames(self.start, self.end):
112
+ yield frame
113
+
114
+ for frame in self.reader.get_audio_frames(self.start, self.end):
101
115
  yield frame
102
116
 
103
117
  def __init__(
@@ -137,25 +151,46 @@ class Video:
137
151
  filename: str
138
152
  ) -> 'Video':
139
153
  writer = VideoWriter(filename)
140
- #writer.set_video_stream(self.reader.video_stream.codec.name, self.reader.fps, self.reader.size, PIXEL_FORMAT)
141
154
  writer.set_video_stream_from_template(self.reader.video_stream)
142
155
  writer.set_audio_stream_from_template(self.reader.audio_stream)
143
156
 
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
157
+ from yta_video_opengl.nodes.audio import VolumeAudioNode
158
+ # Audio from 0 to 1
159
+ # TODO: This effect 'fn' is shitty
160
+ def fade_in_fn(t, index, start=0.5, end=1.0):
161
+ if t < start or t > end:
162
+ # fuera de la franja: no tocar nada → volumen original (1.0)
163
+ progress = 1.0
164
+ else:
165
+ # dentro de la franja: interpolar linealmente entre 0 → 1
166
+ progress = (t - start) / (end - start)
167
+
168
+ return progress
169
+
170
+ #fade_in = SetVolumeAudioNode(lambda t, i: min(1, t / self.duration))
171
+ fade_in = VolumeAudioNode(lambda t, i: fade_in_fn(t, i, 0.5, 1.0))
172
+
147
173
  for frame, t, index in self.frames:
148
174
  if PythonValidator.is_instance_of(frame, 'VideoFrame'):
149
175
  print(f'Saving video frame {str(index)}, with t = {str(t)}')
176
+
177
+ # TODO: Process any video frame change
178
+
150
179
  writer.mux_video_frame(
151
180
  frame = frame
152
181
  )
153
182
  else:
154
183
  print(f'Saving audio frame {str(index)} ({str(round(float(t * self.reader.fps), 2))}), with t = {str(t)}')
184
+
185
+ # TODO: Process any audio frame change
186
+ # Test setting audio
187
+ frame = fade_in.process(frame, t)
188
+
155
189
  writer.mux_audio_frame(
156
190
  frame = frame
157
191
  )
158
192
 
193
+ # Flush the remaining frames to write
159
194
  writer.mux_audio_frame(None)
160
195
  writer.mux_video_frame(None)
161
196
 
@@ -1,155 +0,0 @@
1
- """
2
- The pyav container stores the information based
3
- on the packets timestamps (called 'pts'). Some
4
- of the packets are considered key_frames because
5
- they include those key frames.
6
-
7
- Also, this library uses those key frames to start
8
- decodifying from there to the next one, obtaining
9
- all the frames in between able to be read and
10
- modified.
11
-
12
- This cache system will look for the range of
13
- frames that belong to the key frame related to the
14
- frame we are requesting in the moment, keeping in
15
- memory all those frames to be handled fast. It
16
- will remove the old frames if needed to use only
17
- the 'size' we set when creating it.
18
- """
19
- from collections import OrderedDict
20
-
21
-
22
- class VideoFrameCache:
23
- """
24
- Class to manage the frames cache of a video
25
- within a video reader instance.
26
- """
27
-
28
- @property
29
- def container(
30
- self
31
- ) -> 'InputContainer':
32
- """
33
- Shortcut to the video reader instance container.
34
- """
35
- return self.reader_instance.container
36
-
37
- @property
38
- def stream(
39
- self
40
- ) -> 'VideoStream':
41
- """
42
- Shortcut to the video reader instance video
43
- stream.
44
- """
45
- return self.reader_instance.video_stream
46
-
47
- def __init__(
48
- self,
49
- reader: 'VideoReader',
50
- size: int = 50
51
- ):
52
- self.reader_instance: 'VideoReader' = reader
53
- """
54
- The video reader instance this cache belongs
55
- to.
56
- """
57
- self.cache: OrderedDict = OrderedDict()
58
- """
59
- The cache ordered dictionary.
60
- """
61
- self.size = size
62
- """
63
- The size (in number of frames) of the cache.
64
- """
65
- self.key_frames_pts: list[int] = []
66
- """
67
- The list that contains the timestamps of the
68
- key frame packets, ordered from begining to
69
- end.
70
- """
71
-
72
- # Index key frames
73
- for packet in self.container.demux(self.stream):
74
- if packet.is_keyframe:
75
- self.key_frames_pts.append(packet.pts)
76
-
77
- self.container.seek(0)
78
- # TODO: Maybe this is better (?)
79
- #self.reader_instance.reset()
80
-
81
- def _get_frame_by_pts(
82
- self,
83
- target_pts
84
- ):
85
- """
86
- Get the frame that has the provided 'target_pts'.
87
-
88
- This method will start decoding frames from the
89
- most near key frame (the one with the nearer
90
- pts) until the one requested is found. All those
91
- frames will be stored in cache.
92
-
93
- This method must be called when the frame
94
- requested is not stored in the caché.
95
- """
96
- # Look for the most near key frame
97
- key_frame_pts = max([
98
- key_frame_pts
99
- for key_frame_pts in self.key_frames_pts
100
- if key_frame_pts <= target_pts
101
- ])
102
-
103
- # Go to the key frame that includes it
104
- self.container.seek(key_frame_pts, stream = self.stream)
105
-
106
- decoded = None
107
- for frame in self.container.decode(self.stream):
108
- # TODO: Could 'frame' be None (?)
109
- pts = frame.pts
110
- if pts is None:
111
- continue
112
-
113
- # Store in cache if needed
114
- if pts not in self.cache:
115
- # TODO: The 'format' must be dynamic
116
- self.cache[pts] = frame.to_ndarray(format = "rgb24")
117
-
118
- # Clean cache if full
119
- if len(self.cache) > self.size:
120
- self.cache.popitem(last = False)
121
-
122
- if pts >= target_pts:
123
- decoded = self.cache[pts]
124
- break
125
-
126
- return decoded
127
-
128
- def get_frame(
129
- self,
130
- index: int
131
- ) -> 'VideoFrame':
132
- """
133
- Get the frame with the given 'index' from
134
- the cache.
135
- """
136
- # convertir frame_number a PTS (timestamps internos)
137
- time_base = self.stream.time_base
138
- fps = float(self.stream.average_rate)
139
- target_pts = int(index / fps / time_base)
140
-
141
- return (
142
- self.cache[target_pts]
143
- if target_pts in self.cache else
144
- self._get_frame_by_pts(target_pts)
145
- )
146
-
147
- def clear(
148
- self
149
- ) -> 'VideoFrameCache':
150
- """
151
- Clear the cache by removing all the items.
152
- """
153
- self.cache.clear()
154
-
155
- return self