simple_ffmpeg_batch_io

Reading and writing image and audio batches from/to video and audio files using an FFmpeg backend, with a simple Python API built on top of numpy.

Features

  • Read batches of audio and video frames from video and audio files into numpy arrays.
  • Write batches of frames from numpy arrays to video or audio files, even compressed
  • Uses static_ffmpeg to provide FFmpeg binaries in a portable way. Simple-ffmpeg-batch-io provide ffmpeg and ffprobe as commands within the virtual environment
  • Designed for machine learning and audio/video generation pipelines.

Installation of last version

pip install simple-ffmpeg-batch-io

Documentation

Automatically generated documentation is available online.

Examples

Handling video files (i.e. images from video files)

Read video file at its own frame rate and frame shape

# Open it with VideoIO old way (C++ style)
inputVideo = VideoIO()
inputVideo.open(video_filename) # video file is a str or a path

# or open it a more pythonish way
inputVideo = VideoIO.reader(video_filename) # video file is a str or a path

# Here, one can read inputVideo.width, inputVideo.height, inputVideo.fps
print(inputVideo.width, inputVideo.height, inputVideo.fps)

# read one frame
frame = inputVideo.read_frame() # Here frame is a numpy arrays of shape (Width,height,channels). Channel is 3 as we support only 3 channels for the moment.

# Process video frame by frame
for frame in inputVideo.iter_frames():
    # Process frame. Here frame is a numpy arrays of shape (Width,height,channels). Channel is 3 as we support only 3 channels for the moment.
    process( frame )

# or process video using batches
n = 10
for batch in inputVideo.iter_batches(n):
    # Process batch. Here batch is a numpy arrays of shape (n,Width,height,channels). Channel is 3 as we support only 3 channels for the moment.
    process( batch )

inputVideo.close()

# Read video frame by frame with associated timestamps using with context, no need to close after end of context, close is aotomùatically called.
with VideoIO.reader(video_filename) as inputVideo:
    for frame in inputVideo.iter_frames(with_timestamps = True):
        # Here frame is encapsulated within simple-ffmpeg-batch-io.FrameContainer object
        process( frame.data )    # numpy array of shape (Width,height,channels)
        print( frame.timestamps ) # python list of the timestamp associated to the frame (video here, but same for audio), here only one element as one frame is used

# OR
# Read video batch by batch with associated timestamps using with context, no need to close after end of context, close is aotomùatically called.
n = 10
with VideoIO.reader(video_filename) as inputVideo:
    for batch in inputVideo.iter_batches(n, with_timestamps = True):
        # Here batch is encapsulated within simple-ffmpeg-batch-io.FrameContainer object
        process( batch.data )    # numpy array of shape (n,Width,height,channels)
        print( batch.timestamps ) # python list of timestamps associated to each frame (video here, but same for audio), here only one element as one frame is used

Read video file with more parameters

from simple_ffmpeg_batch_io import VideoIO

# Read video changing width, height, and fps
inputVideo = VideoIO.reader(video_file, width=100, height=100, fps=1.0)

# Read modifying only some of them
with VideoIO.reader(video_file, width=100, fps=2.0) as inputVideo:
    ...

Read video and write video file with dedicated ffmpeg parameters

from simple_ffmpeg_batch_io import VideoIO

# open file using filter:
# resizing to width=320, adapting height to keep aspect ratio while keep height pair (for some codec like H264)
# pixelising it to 5x5 pixels
# important note: one must not use decodingParams for scalling. Indeed, VideoIO class use filter to scale video, use width/height parameters
with VideoIO.reader(video_in_filename, width=320, height=320, decodingParams="-vf pixelize=w=5:h=5") as inputVideo,
     VideoIO.writer(video_out_filename, width=inputVideo.width, height=inputVideo.height, fps=inputVideo.fps ) as outputVideo:  # possible to add encodingParams to add filters, ...
    # iter over batch of 10s and write it to the output file
    batch_size = int( 10*inputVideo.fps )
    for batch in inputVideo.iter_batches(batch_size):
        outputVideo.write_batch(batch)

Handling audio from video or audio files

Read audio from audio file

from simple_ffmpeg_batch_io import AudioIO

# Read audio converting it to one channel, 16000 Hz, frame size of 1s as parameter is a float, start reading file at 2.0s
# default mode is plannar, i.e. samples are not interleaved, they are separated by channel after reading
# frame_size for subsequent call to read_frame, iter_frames, read_batch or iter_batches is 1.0s (16000 samples) as the value is a float, thus times in seconds.
inputAudio = AudioIO.reader(audio_filename, sample_rate=16000, channels=1, frame_size = 1.0, start_time = 2.0 )
# Read batches of 10 audio frames, each frame has 16000 sanples (1.0 second)
for audio_batch in inputAudio.iter_batches(10):
    ... # audio_batch is a np.array

# OR

# If the frame_size value is an int, frame_size is considered as a number of samples, for instance to have 0.5 seconds at 16 Khz (8000 samples for each frame)
with AudioIO.reader(audio_filename, sample_rate=16000, channels=1, frame_size = 8000, start_time = 2.0 ) as inputAudio:
    # Read batches of 10 audio frames, each frame has 8000 sanples
    for audio_batch in inputAudio.iter_batches(10, with_timestamps = True):
        .... # audio_batch is encapsulated within a FrameContainer object with_timestamps = True

Copy audio stream(s) from an audio file or a video file with an audio stream to a wav file

from simple_ffmpeg_batch_io import AudioIO

# Read audio data in interleaved mode (plannard = False) to avoid useless conversion to plannar using default frame_size (1 second). Sample rate remains the same.
with AudioIO.reader(audio_or_video_filename, plannar = False) as inputAudio:
    # Copy sample_rate and channels to the created file, overwrite existing output filename if any.
    with AudioIO.writer(audio_filename, sample_rate=inputAudio.sample_rate, channels=inputAudio.channels, plannar = False, writeOverExistingFile = True) as outputAudio:
        # Read batches of 10 audio frames (10 seconds as frame_size was by default 1 second when opening video)
        for audio_batch in inputAudio.iter_batches(10):
            outputAudio.write_batch(audio_batch)

# no need to close AudioIO objects as 'with' context do it automatically.

Static utility functions

VideoIO

from simple_ffmpeg_batch_io import VideoIO

# get (width, height, fps) of a video using a static function
print( VideoIO.get_params(video_filename) )

# get length of an audio stream as float (seconds.milliseconds)
print( VideoIO.get_time_in_sec(video_filename) )

AudioIO

from simple_ffmpeg_batch_io import AudioIO

# get (channels,sample_rate) of a stream using astatic function
print( AudioIO.get_params(audio_or_video_filename) )

# get length of video stream as float (seconds.milliseconds)
print( AudioIO.get_time_in_sec(audio_or_video_filename) )

Submodules

 1"""
 2.. include:: ../../README.md
 3 :start-after: # simple-ffmpeg-batch-io
 4
 5# Submodules
 6"""
 7
 8__authors__ = ("Dominique Vaufreydaz")
 9
10# __init__.py
11from .VideoIO import VideoIO
12from .AudioIO import AudioIO
13from .PipeMode import PipeMode
14from .FrameCounter import FrameCounter
15from .FrameContainer import FrameContainer
16
17__all__ = [
18    "VideoIO",
19    "AudioIO",
20    "FrameCounter",
21    "FrameContainer",
22    "PipeMode",
23]
class VideoIO:
 32class VideoIO:
 33    # "static" variables to ffmpeg, ffprobe executables
 34    videoProgram, paramProgram = static_ffmpeg.run.get_or_fetch_platform_executables_else_raise()
 35
 36    class VideoIOException(Exception):
 37        """
 38        Dedicated exception class for VideoIO class.
 39        """
 40        def __init__(self, message="Error while reading/writing video occurs"):
 41            self.message = message
 42            super().__init__(self.message)
 43
 44    class PixelFormat(Enum):
 45        """
 46        Enum class for supported input video type: GBR 24 bits or RGB 24 bis.
 47        """
 48        GBR24 = 'bgr24' # default format
 49        RGB24 = 'rgb24'
 50
 51    @classmethod
 52    def reader(cls, filename, *, loglevel = 16, debug = False, **kwargs):
 53        """
 54        Create and open a VideoIO object in reader mode (read a video file)
 55
 56        See `VideoIO.open` for the full list
 57        of accepted parameters.
 58        """
 59        reader = cls(logLevel=loglevel, debug=debug)
 60        reader.open(filename,**kwargs)
 61        return reader
 62
 63    @classmethod
 64    def writer(cls, filename, width, height, fps, *, loglevel = 16, debug = False, **kwargs):
 65        """
 66        Create and open a VideoIO object in writer mode (write a video file)
 67
 68        See `VideoIO.create` for the full list
 69        of accepted parameters.
 70        """
 71        writer = cls(logLevel=loglevel, debug=debug)
 72        writer.create(filename, width, height, fps, **kwargs)
 73        return writer
 74
 75    # standard method
 76    def get_corresponding_writer(self, filename, **kwargs):
 77        """
 78        Method to get writer for a video file with same width, height, fps as the current one VideoIO object
 79
 80        See `VideoIO.create` for the full list
 81        of accepted parameters.
 82        """
 83        return VideoIO.writer(filename, self.width, self.height, self.fps, **kwargs)
 84
 85    # To use with context manager "with VideoIO.reader(...) as f:' for instance
 86    def __enter__(self):
 87        """
 88        Method call at initialisation of a context manager like "with VideoIO.reader(...) as f:' for instance
 89        """
 90        # simply return myself
 91        return self
 92
 93    def __exit__(self, exc_type, exc_val, exc_tb):
 94        """
 95        Method call when existing of a context manager like "with VideoIO.reader(...) as f:' for instance
 96        """
 97        # close VideoIO
 98        self.close()
 99        return False
100
101    @staticmethod
102    def get_time_in_sec(filename, *, debug=False, logLevel=16):
103        """
104        Static method to get length of a video file in seconds including milliseconds as decimal part.
105
106        Parameters
107        ----------
108        filename : str or path
109            Video file name.
110
111        debug : bool (default False)
112            Show debug info.
113
114        log_level: int (default 16)
115            Log level to pass to the underlying ffmpeg/ffprobe command.
116        
117        Returns
118        ----------
119        float
120            Length in seconds of video file (including milliseconds as decimal part)
121        """
122        
123        cmd = [VideoIO.paramProgram, # ffprobe
124                    '-hide_banner',
125                    '-loglevel', str(logLevel),
126                    '-show_entries', 'format=duration',
127                    '-of', 'default=noprint_wrappers=1:nokey=1',
128                    str(filename)
129                    ]
130
131        if debug == True:
132            print(' '.join(cmd))
133
134        # call ffprobe and get params in one single line
135        lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe
136        output = lpipe.stdout.readlines()
137        lpipe.terminate()
138        # transform Bytes output to one single string
139        output = ''.join( [element.decode('utf-8') for element in output])
140
141        try:
142            return float(output)
143        except (ValueError, TypeError):
144            return None
145
146    @staticmethod
147    def get_params(filename, *, debug=False, logLevel=16):
148        """
149        Static method to get params (width, height, fps) from a video file.
150
151        Parameters
152        ----------
153        filename: str or path
154            Video filename.
155
156        debug: bool (default (False)
157            Show debug info.
158
159        log_level: int (default 16)
160            Log level to pass to the underlying ffmpeg/ffprobe command.
161
162        Returns
163        ----------
164        tuple
165            Tuple containing (width, height, fps) of the video
166        """
167        cmd = [VideoIO.paramProgram, # ffprobe
168                    '-hide_banner',
169                    '-loglevel', str(logLevel),
170                    '-show_entries', 'stream=width,height,r_frame_rate',
171                    str(filename)
172                    ]
173
174        if debug == True:
175            print(' '.join(cmd))
176
177        # call ffprobe and get params in one single line
178        lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe
179        output = lpipe.stdout.readlines()
180        lpipe.terminate()
181        # transform Bytes output to one single string
182        output = ''.join( [element.decode('utf-8') for element in output])
183
184        pattern_width = r'width=(\d+)'
185        pattern_height = r'height=(\d+)'
186        pattern_fps = r'r_frame_rate=(\d+)/(\d+)'
187
188        # Search for values in the ffprobe output
189        match_width = re.search(pattern_width, output, flags=re.MULTILINE)
190        match_height = re.search(pattern_height, output, flags=re.MULTILINE)
191        match_fps = re.search(pattern_fps, output, flags=re.MULTILINE)
192
193        # Extraction des valeurs
194        if match_width:
195            width = int(match_width.group(1))
196        else:
197            raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'")
198
199        if match_height:
200            height = int(match_height.group(1))
201        else:
202            raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'")
203
204        if match_fps:
205            numerator = float(match_fps.group(1))
206            denominator = float(match_fps.group(2))
207            fps = numerator / denominator
208        else:
209            raise VideoIO.VideoIOException("Unable to get frame rate (fps) of '" + filename + "'")
210
211        return (width, height, fps)
212
213    # Attributes
214    mode: PipeMode
215    """ Pipemode of the current object (default PipeMode.UNK_MODE)"""
216
217    loglevel: int
218    """ loglevel of the underlying ffmpeg backend for this object (default 16)"""
219
220    debug: bool
221    """ debug flag for this object (print debut info, default False)"""
222
223    width: int
224    """ width of images (default -1) """
225
226    height: int
227    """ height of images (default -1) """
228
229    fps: float
230    """ fps of video (default -1.0) """
231
232    pipe: sp.Popen
233    """ pipe object to ffmpeg/ffprobe (default None)"""
234
235    shape: tuple
236    """ Shape of images (default (None, None, None))"""
237
238    imageSize: int
239    """ Weight in bytes of one image (default -1)"""
240
241    filename: str
242    """ Filename of the video file (default None)"""
243
244    frame_counter: FrameCounter
245    """ `Framecounter` object to count ellapsed time (default None)"""
246
247    def __init__(self, *, logLevel = 16, debug = False):
248        """
249        Create a VideoIO object giving ffmpeg/ffrobe loglevel and defining debug mode
250
251        Parameters
252        ----------
253        log_level: int (default 16)
254            Log level to pass to the underlying ffmpeg/ffprobe command.
255
256        debug: bool (default (False)
257            Show debug info. while processing video
258        """
259
260        self.mode = PipeMode.UNK_MODE
261        self.logLevel = logLevel
262        self.debug = debug
263
264        # Call init() method
265        self.init()
266
267    def init(self):
268        """
269        Init or reinit a VideoIO object.
270        """
271        self.width  = -1
272        self.height = -1
273        self.fps = -1.0
274        self.pipe = None
275        self.shape = (None, None, None)
276        self.imageSize = -1
277        self.filename = None
278        self.frame_counter = None
279
280    _repr_exclude = {"pipe"}
281    """ List of excluded attribute for string conversion. """
282
283    # converting the object to a string representation
284    def __repr__(self):
285        """
286        Convert object (excluding attributes in _repr_exclude) to string representation.
287        """
288        attrs = ", ".join(
289            f"{k}={v!r}"
290            for k, v in self.__dict__.items()
291            if k not in self._repr_exclude
292        )
293        return f"{self.__class__.__name__}({attrs})"
294
295    __str__ = __repr__
296    """ String representation """
297
298    def get_elapsed_time_as_str(self) -> str:
299        """
300        Method to get elapsed time (float value) as str from `frame_counter` attribute.
301
302        Returns
303        ----------
304        str or None
305            Elapsed time (float value) as str, "15.500" for instance for 15 secondes and 500 milliseconds
306            None if no frame counter are available.
307        """
308        if self.frame_counter is None:
309            return None
310        return self.frame_counter.get_elapsed_time_as_str()
311
312    def get_formated_elapsed_time_as_str(self,show_ms=True) -> str:
313        """
314        Method to get elapsed time (hour format) as str from `frame_counter` attribute.
315
316        Returns
317        ----------
318        str or None
319            Elapsed time (float value) as str, "00:00:15.500" for instance for 15 secondes and 500 milliseconds
320            None if no frame counter are available.
321        """
322        if self.frame_counter is None:
323            return None
324        return self.frame_counter.get_formated_elapsed_time_as_str()
325
326    def get_elapsed_time(self) -> float:
327        """
328        Method to get elapsed time as float value rounded to 3 decimals (millisecond) from `frame_counter` attribute.
329
330        Returns
331        ----------
332        float or None
333            Elapsed time (float value) as str, 15.500 for instance for 15 secondes and 500 milliseconds
334            None if no frame counter are available.
335        """
336        if self.frame_counter is None:
337            return None
338        return self.frame_counter.get_elapsed_time()
339
340    def is_opened(self) -> bool:
341        """
342        Method to get status of the underlying pipe to ffmpeg.
343
344        Returns
345        ----------
346        bool
347            True if pipe is opened (reading or writing mode), False if not.
348        """
349        # is the pipe opened?
350        if self.pipe is not None and self.pipe.poll() is None:
351            return True
352
353        return False
354
355    def close(self) -> None:
356        """
357        Method to close current pipe to ffmpeg (if any). Ffmpeg/ffprobe will be terminated. Object can be reused using open or create methods.
358        """
359        if self.pipe is not None:
360            if self.mode == PipeMode.WRITE_MODE:
361                # killing will make ffmpeg not finish properly the job, close the pipe
362                # to let it know that no more data are comming
363                self.pipe.stdin.close()
364            else: # self.mode == PipeMode.READ_MODE
365                # in read mode, no need to be nice, send SIGTERM on Linux,/Kill it on windows
366                self.pipe.kill()
367
368            # wait for subprocess to end
369            self.pipe.wait()
370
371        # reinit object for later use
372        self.init()
373
374    def create(self, filename, width, height, fps, *, writeOverExistingFile = False,
375                     inputEncoding = PixelFormat.GBR24, encodingParams = None ) -> bool:
376        """
377        Method to create a video using parametrized access through ffmpeg. Importante note: calling create
378        on a VideoIO will close any former open video.
379
380        Parameters
381        ----------
382        filename: str or path
383            filename of path to the file (mp4, avi, ...)
384
385        width: int
386            If defined as a positive value, width of output images will be set to this value.
387
388        height: int
389            If defined as a positive value, height of output images will be set to this value.
390
391        fps:
392            If defined as a positive value, fps of output video will be set to this value.
393
394        inputEncoding: PixelFormat optional (default PixelFormat.BGR24)
395            Define order of channels for channels. Possible values are PixelFormat.BGR24 or PixelFormat.RGB24.
396
397        encodingParams: str optional (default None)
398            Parameter to pass to ffmpeg to encode video like video filters.
399
400        Returns
401        ----------
402        bool
403            Was the creation successfull
404        """
405
406        # Close if already opened
407        self.close()
408
409        # Set geometry/fps of the video stream from params
410        self.width = int(width)
411        self.height = int(height)
412        self.fps = float(fps)
413
414        # Check params
415        if self.width <= 0 or self.height <= 0 or self.fps <= 0.0:
416            raise self.VideoIOException("Bad parameters: width={}, height={}, fps={:3f}".format(self.width,self.height,self.fps))
417
418        # Params are ok, set shape and image size
419        self.shape     = (self.height,self.width,3)
420        self.imageSize = self.height * self.width * 3
421
422        # Video params are set, open the video
423        cmd = [self.videoProgram] # ffmpeg
424
425        if writeOverExistingFile == True:
426            cmd.extend(['-y'])
427
428        cmd.extend(['-hide_banner',
429            '-nostats',
430            '-loglevel', str(self.logLevel),
431            '-f', 'rawvideo', '-vcodec', 'rawvideo', '-pix_fmt', inputEncoding.value,
432            '-video_size', f"{self.width}x{self.height}",
433            '-r', "{:.3f}".format(self.fps),
434            '-i', '-'])
435
436        if encodingParams is not None:
437            cmd.extend(encodingParams.split())
438
439        # Video filename converted to str (for Path values)
440        cmd.extend( ['-an', str(filename) ] )
441
442        if self.debug == True:
443            print( ' '.join(cmd), file=sys.stderr )
444
445        # store filename and set mode
446        self.filename = str(filename)
447        self.mode = PipeMode.WRITE_MODE
448
449        # Call ffmpeg in write mode
450        try:
451            self.pipe = sp.Popen(cmd, stdin=sp.PIPE)
452            self.frame_counter = FrameCounter(self.fps)
453        except Exception as e:
454            # if pipe failed, reinit object and raise exception
455            self.init()
456            raise
457
458        return True
459
460    def open( self, filename, *, width = -1, height = -1, fps = -1.0, outputEncoding = PixelFormat.GBR24,
461                    decodingParams = None, start_time = 0.0 ) -> bool:
462        """
463        Method to read video using parametrized access through ffmpeg. Importante note: calling open
464        on a VideoIO will close any former open video.
465
466        Parameters
467        ----------
468        filename: str or path
469            filename of path to the file (mp4, avi, ...)
470
471        width: int optional (default -1)
472            If defined as a positive value, width of input images will be converted to this value.
473
474        height: int optional (default -1)
475            If defined as a positive value, height of input images will be converted to this value.
476
477        fps: float optional (default -1.0)
478            If defined as a positive value, fps of input video will be converted to this value.
479
480        outputEncoding: PixelFormat optional (default PixelFormat.BGR24)
481            Define order of channels for channels. Possible values are PixelFormat.BGR24 or PixelFormat.RGB24.
482
483        decodingParams: str optional (default None)
484            Parameter to pass to ffmpeg to decode video like filters.
485
486        start_time: float optional (default 0.0)
487            Define the reading start time. If not set, reading at beginning of the video.
488
489        Returns
490        ----------
491        bool
492            Was the opening successfull
493        """
494
495        # Close if already opened
496        self.close()
497
498        # Force conversion of parameters
499        width = int(width)
500        height = int(height)
501        fps = float(fps)
502
503        # get parameters from video
504        self.width, self.height, self.fps = self.getVideoParams(str(filename))
505
506        # check if parameters ask to overide video parameters
507        # TODO: add support for negative value (automatic preservation of aspect ratio)
508        if width > 0:
509            self.width = width
510        if height > 0:
511            self.height = height
512        if fps > 0.0:
513            self.fps = fps
514
515        # Params are ok, set shape and image size
516        self.shape = (self.height,self.width,3)
517        self.imageSize = self.height * self.width * 3
518
519        # Video params are set, open the video
520        cmd = [self.videoProgram, # ffmpeg
521                    '-hide_banner',
522                    '-nostats',
523                    '-loglevel', str(self.logLevel)]
524
525        if start_time < 0.0:
526            pass
527        elif start_time > 0.0:
528            cmd.extend(["-ss", f"{start_time}"])    # set start time if any
529
530        cmd.extend( ['-i', str(filename)] )
531
532        video_filters = '' # empty
533        if decodingParams is not None:
534            decodingParams = decodingParams.split()
535            # walk over decodingParams for specific params
536            i = 0
537            while i < len(decodingParams):
538                if decodingParams[i] == '-vf':
539                    decodingParams.pop(i)  # remove '-vf'
540                    if i < len(decodingParams):
541                        video_filters += ','+decodingParams.pop(i)  # remove parameters from list too
542                    # to do : add support to other option like -y
543                else:
544                    i += 1
545        else:
546            decodingParams = []
547
548        cmd.extend( ['-vf', f'scale={self.width}:{self.height}{video_filters}', # rescale (or not if shape is original one), add specific video filters
549                    *(decodingParams),
550                    '-f', 'rawvideo', '-vcodec', 'rawvideo', '-pix_fmt', outputEncoding.value, # input expected coding
551                    '-an', # no audio
552                     '-r', f"{self.fps}",
553                     '-' # output to stdout
554                    ] )
555
556        if self.debug == True:
557            print( ' '.join(cmd) )
558
559        # store filename and set mode to READ_MODE
560        self.filename = str(filename)
561        self.mode = PipeMode.READ_MODE
562
563        # call ffmpeg in read mode
564        try:
565            self.pipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe
566            self.frame_counter = FrameCounter(self.fps)
567            if start_time > 0.0:
568                self.frame_counter += start_time # adding with float means adding time
569        except Exception as e:
570            # if pipe failed, reinit object and raise exception
571            self.init()
572            raise
573
574        return True
575
576    def read_frame(self, with_timestamps = False):
577        """
578        Read next frame from the video
579
580        Parameters
581        ----------
582        with_timestamps: bool optional (default False)
583            If set to True, the method returns a FrameContainer with the image and an array containing the associated timestamp(s)
584
585        Returns
586        ----------
587        nparray or FrameContainer
588            An image of shape (3,width,height). if with_timestamps is True, the return object is a FrameContainer with the image in ``FrameContainer.data`` and
589            the associated timestamp in ``FrameContainer.timestamps`` as an array (one element for one frame).
590        """
591
592        if self.pipe is None:
593            raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading a frame.".format(self.videoProgram))
594        # - pipe is in write mode
595        if self.mode != PipeMode.READ_MODE:
596            raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename))
597
598        if with_timestamps:
599            # get elapsed time in video, it is time of next frame(s)
600            current_elapsed_time = self.get_elapsed_time()
601
602        # read rgb image from pipe
603        buffer = self.pipe.stdout.read(self.imageSize)
604        if len(buffer) != self.imageSize:
605            # Incomplete image, ffmpeg have been stopped or killed, do not return an incomplete frame
606            # not considered as an error, no more frame, no exception
607            return None
608
609        # get numpy UINT8 array from buffer
610        rgbImage = np.frombuffer(buffer, dtype = np.uint8).reshape(self.shape)
611
612        # increase frame_counter
613        self.frame_counter.frame_count += 1
614
615        # say to gc that this buffer is no longer needed
616        del buffer
617
618        if with_timestamps:
619            return FrameContainer(1,rgbImage,self.fps,current_elapsed_time)
620
621        return rgbImage
622
623    def read_batch(self, number_of_frames, with_timestamps = False) ->  Union[np.array, FrameContainer]:
624        """
625        Read next batch of images from the video
626
627        Parameters
628        ----------
629        number_of_frames: int
630            Number of desired images within the batch. The last batch of the video may have less images.
631            
632        with_timestamps: bool optional (default False)
633            If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames
634
635        Returns
636        ----------
637        nparray or FrameContainer
638            A batch of images of shape (n,3,width,height). if with_timestamps is True, the return object is a FrameContainer with the batch in ``FrameContainer.data`` and
639            the associated timestamps in ``FrameContainer.timestamps`` as an array (one element for each frame).
640        """
641
642        if self.pipe is None:
643            raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading frames.".format(self.videoProgram))
644        # - pipe is in write mode
645        if self.mode != PipeMode.READ_MODE:
646            raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename))
647
648        if with_timestamps:
649            # get elapsed time in video, it is time of next frame(s)
650            current_elapsed_time = self.get_elapsed_time()
651
652        # try to read complete batch
653        buffer = self.pipe.stdout.read(self.imageSize*number_of_frames)
654
655        # check if we have at least 1 image
656        if len(buffer) < self.imageSize:
657            # ffmpag backend have been stopped or killed
658            # not considered as an error, no more frame, no exception
659            return None
660
661        # compute actual number of Frames
662        actualNbFrames = len(buffer)//self.imageSize
663
664        # get and reshape batch from buffer
665        batch = np.frombuffer(buffer, dtype = np.uint8).reshape((actualNbFrames, self.height, self.width, 3))
666
667        # increase frame_counter
668        self.frame_counter.frame_count += actualNbFrames
669        
670        # say to gc that this buffer is no longer needed
671        del buffer
672
673        if with_timestamps:
674            return FrameContainer(actualNbFrames, batch, self.fps, current_elapsed_time)
675
676        return batch
677
678    def write_frame(self, image) -> bool:
679        """
680        Write an image to the video
681
682        Parameters
683        ----------
684        image: nparray
685            The image of shape (3, width, height) to write to the video file in the PixelFormat provided when create was called.
686
687        Returns
688        ----------
689        bool
690            Writing was successful or not.
691        """
692        
693        # Check params
694        # - pipe exists
695        if self.pipe is None:
696            raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram))
697        # - pipe is in write mode
698        if self.mode != PipeMode.WRITE_MODE:
699            raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename))
700        # - shape of image is fine, thus we have pixels for a full compatible frame
701        if image.shape != self.shape:
702            raise self.VideoIOException("Wong image shape: {} expected {}.".format(image.shape,self.shape))
703        # - type of data is UINT8
704        if image.dtype != np.uint8:
705            raise self.VideoIOException("Wong pixel type: {} expected np.uint8.".format(image.dtype))
706
707        # write frame
708        buffer = image.tobytes()
709        if self.pipe.stdin.write( buffer ) < self.imageSize:
710            print( "Error writing frame to" )
711            return False
712
713        # increase frame_counter
714        self.frame_counter.frame_count += 1
715
716        # say to gc that this buffer is no longer needed 
717        del buffer
718
719        return True
720
721    def write_batch(self, batch) -> bool:
722        """
723        Write a batch of images to the video
724
725        Parameters
726        ----------
727        batch: nparray
728            A batch of images to write to the video file in the PixelFormat provided when create was called.
729
730        Returns
731        ----------
732        bool
733            Writing was successful or not.
734        """
735
736        # Check params
737        # - pipe exists
738        if self.pipe is None:
739            raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram))
740        # - pipe is in write mode
741        if self.mode != PipeMode.WRITE_MODE:
742            raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename))
743        # - shape of images in batch is fine
744        if batch.shape[-3:] != self.shape:
745            raise self.VideoIOException("Wrong image shape in batch: {} expected {}.".format(batch.shape[-3:], self.shape))
746        # - we have the right amount of pixels for the full batch
747        if batch.size != (batch.shape[0]*self.imageSize):
748            raise self.VideoIOException("Wrong number of pixels in batch: {} expected {}.".format(batch.shape[-3:], self.imageSize))
749
750        # write frame
751        buffer = batch.tobytes()
752        if self.pipe.stdin.write( buffer ) < batch.size:
753            # say to gc that this buffer is no longer needed
754            del buffer
755            raise self.VideoIOException("Error writing batch to '{}'.".format(self.filename))
756
757        # increase frame_counter
758        self.frame_counter.frame_count += batch.shape[0]       
759            
760        # say to gc that this buffer is no longer needed
761        del buffer
762
763        return True
764
765    def iter_frames(self, with_timestamps = False):
766        """
767        Method to iterate on video frames using VideoIO obj.
768        for frame in obj.iter_frames():
769            ....
770
771        Parameters
772        ----------
773        with_timestamps: bool optional (default False)
774            If set to True, the method returns a FrameContainer with the batch and an array containing the associated timestamps to frames
775        """
776
777        try:
778            if self.mode == PipeMode.READ_MODE:
779                while self.isOpened():
780                    frame = self.readFrame(with_timestamps)
781                    if frame is not None:
782                        yield frame
783        finally:
784            self.close()
785
786    def iter_batches(self, batch_size : int, with_timestamps = False):
787        """
788        Method to iterate on batch of frames using VideoIO obj.
789        for image_batch in obj.iter_batches():
790            ....
791
792        Parameters
793        ----------
794        with_timestamps: bool optional (default False)
795            If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames
796        """
797        try:
798            if self.mode == PipeMode.READ_MODE:
799                while self.isOpened():
800                    batch = self.readBatch(batch_size, with_timestamps)
801                    if batch is not None:
802                        yield batch
803        finally:
804            self.close()
805
806    # function aliases to be compliant with original C++ version
807    getVideoTimeInSec = get_time_in_sec
808    getVideoParams = get_params
809    get_video_time_in_sec = get_time_in_sec
810    get_video_params = get_params
811    isOpened = is_opened
812    readFrame = read_frame
813    readBatch = read_batch
814    writeFrame = write_frame
815    writeBatch = write_batch
VideoIO(*, logLevel=16, debug=False)
247    def __init__(self, *, logLevel = 16, debug = False):
248        """
249        Create a VideoIO object giving ffmpeg/ffrobe loglevel and defining debug mode
250
251        Parameters
252        ----------
253        log_level: int (default 16)
254            Log level to pass to the underlying ffmpeg/ffprobe command.
255
256        debug: bool (default (False)
257            Show debug info. while processing video
258        """
259
260        self.mode = PipeMode.UNK_MODE
261        self.logLevel = logLevel
262        self.debug = debug
263
264        # Call init() method
265        self.init()

Create a VideoIO object giving ffmpeg/ffrobe loglevel and defining debug mode

Parameters

log_level: int (default 16) Log level to pass to the underlying ffmpeg/ffprobe command.

debug: bool (default (False) Show debug info. while processing video

@classmethod
def reader(cls, filename, *, loglevel=16, debug=False, **kwargs):
51    @classmethod
52    def reader(cls, filename, *, loglevel = 16, debug = False, **kwargs):
53        """
54        Create and open a VideoIO object in reader mode (read a video file)
55
56        See `VideoIO.open` for the full list
57        of accepted parameters.
58        """
59        reader = cls(logLevel=loglevel, debug=debug)
60        reader.open(filename,**kwargs)
61        return reader

Create and open a VideoIO object in reader mode (read a video file)

See VideoIO.open for the full list of accepted parameters.

@classmethod
def writer( cls, filename, width, height, fps, *, loglevel=16, debug=False, **kwargs):
63    @classmethod
64    def writer(cls, filename, width, height, fps, *, loglevel = 16, debug = False, **kwargs):
65        """
66        Create and open a VideoIO object in writer mode (write a video file)
67
68        See `VideoIO.create` for the full list
69        of accepted parameters.
70        """
71        writer = cls(logLevel=loglevel, debug=debug)
72        writer.create(filename, width, height, fps, **kwargs)
73        return writer

Create and open a VideoIO object in writer mode (write a video file)

See VideoIO.create for the full list of accepted parameters.

def get_corresponding_writer(self, filename, **kwargs):
76    def get_corresponding_writer(self, filename, **kwargs):
77        """
78        Method to get writer for a video file with same width, height, fps as the current one VideoIO object
79
80        See `VideoIO.create` for the full list
81        of accepted parameters.
82        """
83        return VideoIO.writer(filename, self.width, self.height, self.fps, **kwargs)

Method to get writer for a video file with same width, height, fps as the current one VideoIO object

See VideoIO.create for the full list of accepted parameters.

@staticmethod
def get_time_in_sec(filename, *, debug=False, logLevel=16):
101    @staticmethod
102    def get_time_in_sec(filename, *, debug=False, logLevel=16):
103        """
104        Static method to get length of a video file in seconds including milliseconds as decimal part.
105
106        Parameters
107        ----------
108        filename : str or path
109            Video file name.
110
111        debug : bool (default False)
112            Show debug info.
113
114        log_level: int (default 16)
115            Log level to pass to the underlying ffmpeg/ffprobe command.
116        
117        Returns
118        ----------
119        float
120            Length in seconds of video file (including milliseconds as decimal part)
121        """
122        
123        cmd = [VideoIO.paramProgram, # ffprobe
124                    '-hide_banner',
125                    '-loglevel', str(logLevel),
126                    '-show_entries', 'format=duration',
127                    '-of', 'default=noprint_wrappers=1:nokey=1',
128                    str(filename)
129                    ]
130
131        if debug == True:
132            print(' '.join(cmd))
133
134        # call ffprobe and get params in one single line
135        lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe
136        output = lpipe.stdout.readlines()
137        lpipe.terminate()
138        # transform Bytes output to one single string
139        output = ''.join( [element.decode('utf-8') for element in output])
140
141        try:
142            return float(output)
143        except (ValueError, TypeError):
144            return None

Static method to get length of a video file in seconds including milliseconds as decimal part.

Parameters

filename : str or path Video file name.

debug : bool (default False) Show debug info.

log_level: int (default 16) Log level to pass to the underlying ffmpeg/ffprobe command.

Returns

float Length in seconds of video file (including milliseconds as decimal part)

@staticmethod
def get_params(filename, *, debug=False, logLevel=16):
146    @staticmethod
147    def get_params(filename, *, debug=False, logLevel=16):
148        """
149        Static method to get params (width, height, fps) from a video file.
150
151        Parameters
152        ----------
153        filename: str or path
154            Video filename.
155
156        debug: bool (default (False)
157            Show debug info.
158
159        log_level: int (default 16)
160            Log level to pass to the underlying ffmpeg/ffprobe command.
161
162        Returns
163        ----------
164        tuple
165            Tuple containing (width, height, fps) of the video
166        """
167        cmd = [VideoIO.paramProgram, # ffprobe
168                    '-hide_banner',
169                    '-loglevel', str(logLevel),
170                    '-show_entries', 'stream=width,height,r_frame_rate',
171                    str(filename)
172                    ]
173
174        if debug == True:
175            print(' '.join(cmd))
176
177        # call ffprobe and get params in one single line
178        lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe
179        output = lpipe.stdout.readlines()
180        lpipe.terminate()
181        # transform Bytes output to one single string
182        output = ''.join( [element.decode('utf-8') for element in output])
183
184        pattern_width = r'width=(\d+)'
185        pattern_height = r'height=(\d+)'
186        pattern_fps = r'r_frame_rate=(\d+)/(\d+)'
187
188        # Search for values in the ffprobe output
189        match_width = re.search(pattern_width, output, flags=re.MULTILINE)
190        match_height = re.search(pattern_height, output, flags=re.MULTILINE)
191        match_fps = re.search(pattern_fps, output, flags=re.MULTILINE)
192
193        # Extraction des valeurs
194        if match_width:
195            width = int(match_width.group(1))
196        else:
197            raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'")
198
199        if match_height:
200            height = int(match_height.group(1))
201        else:
202            raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'")
203
204        if match_fps:
205            numerator = float(match_fps.group(1))
206            denominator = float(match_fps.group(2))
207            fps = numerator / denominator
208        else:
209            raise VideoIO.VideoIOException("Unable to get frame rate (fps) of '" + filename + "'")
210
211        return (width, height, fps)

Static method to get params (width, height, fps) from a video file.

Parameters

filename: str or path Video filename.

debug: bool (default (False) Show debug info.

log_level: int (default 16) Log level to pass to the underlying ffmpeg/ffprobe command.

Returns

tuple Tuple containing (width, height, fps) of the video

mode: PipeMode

Pipemode of the current object (default PipeMode.UNK_MODE)

loglevel: int

loglevel of the underlying ffmpeg backend for this object (default 16)

debug: bool

debug flag for this object (print debut info, default False)

width: int

width of images (default -1)

height: int

height of images (default -1)

fps: float

fps of video (default -1.0)

pipe: pdoc.extract._PdocDefusedPopen

pipe object to ffmpeg/ffprobe (default None)

shape: tuple

Shape of images (default (None, None, None))

imageSize: int

Weight in bytes of one image (default -1)

filename: str

Filename of the video file (default None)

frame_counter: FrameCounter

Framecounter object to count ellapsed time (default None)

logLevel
def init(self):
267    def init(self):
268        """
269        Init or reinit a VideoIO object.
270        """
271        self.width  = -1
272        self.height = -1
273        self.fps = -1.0
274        self.pipe = None
275        self.shape = (None, None, None)
276        self.imageSize = -1
277        self.filename = None
278        self.frame_counter = None

Init or reinit a VideoIO object.

def get_elapsed_time_as_str(self) -> str:
298    def get_elapsed_time_as_str(self) -> str:
299        """
300        Method to get elapsed time (float value) as str from `frame_counter` attribute.
301
302        Returns
303        ----------
304        str or None
305            Elapsed time (float value) as str, "15.500" for instance for 15 secondes and 500 milliseconds
306            None if no frame counter are available.
307        """
308        if self.frame_counter is None:
309            return None
310        return self.frame_counter.get_elapsed_time_as_str()

Method to get elapsed time (float value) as str from frame_counter attribute.

Returns

str or None Elapsed time (float value) as str, "15.500" for instance for 15 secondes and 500 milliseconds None if no frame counter are available.

def get_formated_elapsed_time_as_str(self, show_ms=True) -> str:
312    def get_formated_elapsed_time_as_str(self,show_ms=True) -> str:
313        """
314        Method to get elapsed time (hour format) as str from `frame_counter` attribute.
315
316        Returns
317        ----------
318        str or None
319            Elapsed time (float value) as str, "00:00:15.500" for instance for 15 secondes and 500 milliseconds
320            None if no frame counter are available.
321        """
322        if self.frame_counter is None:
323            return None
324        return self.frame_counter.get_formated_elapsed_time_as_str()

Method to get elapsed time (hour format) as str from frame_counter attribute.

Returns

str or None Elapsed time (float value) as str, "00:00:15.500" for instance for 15 secondes and 500 milliseconds None if no frame counter are available.

def get_elapsed_time(self) -> float:
326    def get_elapsed_time(self) -> float:
327        """
328        Method to get elapsed time as float value rounded to 3 decimals (millisecond) from `frame_counter` attribute.
329
330        Returns
331        ----------
332        float or None
333            Elapsed time (float value) as str, 15.500 for instance for 15 secondes and 500 milliseconds
334            None if no frame counter are available.
335        """
336        if self.frame_counter is None:
337            return None
338        return self.frame_counter.get_elapsed_time()

Method to get elapsed time as float value rounded to 3 decimals (millisecond) from frame_counter attribute.

Returns

float or None Elapsed time (float value) as str, 15.500 for instance for 15 secondes and 500 milliseconds None if no frame counter are available.

def is_opened(self) -> bool:
340    def is_opened(self) -> bool:
341        """
342        Method to get status of the underlying pipe to ffmpeg.
343
344        Returns
345        ----------
346        bool
347            True if pipe is opened (reading or writing mode), False if not.
348        """
349        # is the pipe opened?
350        if self.pipe is not None and self.pipe.poll() is None:
351            return True
352
353        return False

Method to get status of the underlying pipe to ffmpeg.

Returns

bool True if pipe is opened (reading or writing mode), False if not.

def close(self) -> None:
355    def close(self) -> None:
356        """
357        Method to close current pipe to ffmpeg (if any). Ffmpeg/ffprobe will be terminated. Object can be reused using open or create methods.
358        """
359        if self.pipe is not None:
360            if self.mode == PipeMode.WRITE_MODE:
361                # killing will make ffmpeg not finish properly the job, close the pipe
362                # to let it know that no more data are comming
363                self.pipe.stdin.close()
364            else: # self.mode == PipeMode.READ_MODE
365                # in read mode, no need to be nice, send SIGTERM on Linux,/Kill it on windows
366                self.pipe.kill()
367
368            # wait for subprocess to end
369            self.pipe.wait()
370
371        # reinit object for later use
372        self.init()

Method to close current pipe to ffmpeg (if any). Ffmpeg/ffprobe will be terminated. Object can be reused using open or create methods.

def create( self, filename, width, height, fps, *, writeOverExistingFile=False, inputEncoding=<PixelFormat.GBR24: 'bgr24'>, encodingParams=None) -> bool:
374    def create(self, filename, width, height, fps, *, writeOverExistingFile = False,
375                     inputEncoding = PixelFormat.GBR24, encodingParams = None ) -> bool:
376        """
377        Method to create a video using parametrized access through ffmpeg. Importante note: calling create
378        on a VideoIO will close any former open video.
379
380        Parameters
381        ----------
382        filename: str or path
383            filename of path to the file (mp4, avi, ...)
384
385        width: int
386            If defined as a positive value, width of output images will be set to this value.
387
388        height: int
389            If defined as a positive value, height of output images will be set to this value.
390
391        fps:
392            If defined as a positive value, fps of output video will be set to this value.
393
394        inputEncoding: PixelFormat optional (default PixelFormat.BGR24)
395            Define order of channels for channels. Possible values are PixelFormat.BGR24 or PixelFormat.RGB24.
396
397        encodingParams: str optional (default None)
398            Parameter to pass to ffmpeg to encode video like video filters.
399
400        Returns
401        ----------
402        bool
403            Was the creation successfull
404        """
405
406        # Close if already opened
407        self.close()
408
409        # Set geometry/fps of the video stream from params
410        self.width = int(width)
411        self.height = int(height)
412        self.fps = float(fps)
413
414        # Check params
415        if self.width <= 0 or self.height <= 0 or self.fps <= 0.0:
416            raise self.VideoIOException("Bad parameters: width={}, height={}, fps={:3f}".format(self.width,self.height,self.fps))
417
418        # Params are ok, set shape and image size
419        self.shape     = (self.height,self.width,3)
420        self.imageSize = self.height * self.width * 3
421
422        # Video params are set, open the video
423        cmd = [self.videoProgram] # ffmpeg
424
425        if writeOverExistingFile == True:
426            cmd.extend(['-y'])
427
428        cmd.extend(['-hide_banner',
429            '-nostats',
430            '-loglevel', str(self.logLevel),
431            '-f', 'rawvideo', '-vcodec', 'rawvideo', '-pix_fmt', inputEncoding.value,
432            '-video_size', f"{self.width}x{self.height}",
433            '-r', "{:.3f}".format(self.fps),
434            '-i', '-'])
435
436        if encodingParams is not None:
437            cmd.extend(encodingParams.split())
438
439        # Video filename converted to str (for Path values)
440        cmd.extend( ['-an', str(filename) ] )
441
442        if self.debug == True:
443            print( ' '.join(cmd), file=sys.stderr )
444
445        # store filename and set mode
446        self.filename = str(filename)
447        self.mode = PipeMode.WRITE_MODE
448
449        # Call ffmpeg in write mode
450        try:
451            self.pipe = sp.Popen(cmd, stdin=sp.PIPE)
452            self.frame_counter = FrameCounter(self.fps)
453        except Exception as e:
454            # if pipe failed, reinit object and raise exception
455            self.init()
456            raise
457
458        return True

Method to create a video using parametrized access through ffmpeg. Importante note: calling create on a VideoIO will close any former open video.

Parameters

filename: str or path filename of path to the file (mp4, avi, ...)

width: int If defined as a positive value, width of output images will be set to this value.

height: int If defined as a positive value, height of output images will be set to this value.

fps: If defined as a positive value, fps of output video will be set to this value.

inputEncoding: PixelFormat optional (default PixelFormat.BGR24) Define order of channels for channels. Possible values are PixelFormat.BGR24 or PixelFormat.RGB24.

encodingParams: str optional (default None) Parameter to pass to ffmpeg to encode video like video filters.

Returns

bool Was the creation successfull

def open( self, filename, *, width=-1, height=-1, fps=-1.0, outputEncoding=<PixelFormat.GBR24: 'bgr24'>, decodingParams=None, start_time=0.0) -> bool:
460    def open( self, filename, *, width = -1, height = -1, fps = -1.0, outputEncoding = PixelFormat.GBR24,
461                    decodingParams = None, start_time = 0.0 ) -> bool:
462        """
463        Method to read video using parametrized access through ffmpeg. Importante note: calling open
464        on a VideoIO will close any former open video.
465
466        Parameters
467        ----------
468        filename: str or path
469            filename of path to the file (mp4, avi, ...)
470
471        width: int optional (default -1)
472            If defined as a positive value, width of input images will be converted to this value.
473
474        height: int optional (default -1)
475            If defined as a positive value, height of input images will be converted to this value.
476
477        fps: float optional (default -1.0)
478            If defined as a positive value, fps of input video will be converted to this value.
479
480        outputEncoding: PixelFormat optional (default PixelFormat.BGR24)
481            Define order of channels for channels. Possible values are PixelFormat.BGR24 or PixelFormat.RGB24.
482
483        decodingParams: str optional (default None)
484            Parameter to pass to ffmpeg to decode video like filters.
485
486        start_time: float optional (default 0.0)
487            Define the reading start time. If not set, reading at beginning of the video.
488
489        Returns
490        ----------
491        bool
492            Was the opening successfull
493        """
494
495        # Close if already opened
496        self.close()
497
498        # Force conversion of parameters
499        width = int(width)
500        height = int(height)
501        fps = float(fps)
502
503        # get parameters from video
504        self.width, self.height, self.fps = self.getVideoParams(str(filename))
505
506        # check if parameters ask to overide video parameters
507        # TODO: add support for negative value (automatic preservation of aspect ratio)
508        if width > 0:
509            self.width = width
510        if height > 0:
511            self.height = height
512        if fps > 0.0:
513            self.fps = fps
514
515        # Params are ok, set shape and image size
516        self.shape = (self.height,self.width,3)
517        self.imageSize = self.height * self.width * 3
518
519        # Video params are set, open the video
520        cmd = [self.videoProgram, # ffmpeg
521                    '-hide_banner',
522                    '-nostats',
523                    '-loglevel', str(self.logLevel)]
524
525        if start_time < 0.0:
526            pass
527        elif start_time > 0.0:
528            cmd.extend(["-ss", f"{start_time}"])    # set start time if any
529
530        cmd.extend( ['-i', str(filename)] )
531
532        video_filters = '' # empty
533        if decodingParams is not None:
534            decodingParams = decodingParams.split()
535            # walk over decodingParams for specific params
536            i = 0
537            while i < len(decodingParams):
538                if decodingParams[i] == '-vf':
539                    decodingParams.pop(i)  # remove '-vf'
540                    if i < len(decodingParams):
541                        video_filters += ','+decodingParams.pop(i)  # remove parameters from list too
542                    # to do : add support to other option like -y
543                else:
544                    i += 1
545        else:
546            decodingParams = []
547
548        cmd.extend( ['-vf', f'scale={self.width}:{self.height}{video_filters}', # rescale (or not if shape is original one), add specific video filters
549                    *(decodingParams),
550                    '-f', 'rawvideo', '-vcodec', 'rawvideo', '-pix_fmt', outputEncoding.value, # input expected coding
551                    '-an', # no audio
552                     '-r', f"{self.fps}",
553                     '-' # output to stdout
554                    ] )
555
556        if self.debug == True:
557            print( ' '.join(cmd) )
558
559        # store filename and set mode to READ_MODE
560        self.filename = str(filename)
561        self.mode = PipeMode.READ_MODE
562
563        # call ffmpeg in read mode
564        try:
565            self.pipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe
566            self.frame_counter = FrameCounter(self.fps)
567            if start_time > 0.0:
568                self.frame_counter += start_time # adding with float means adding time
569        except Exception as e:
570            # if pipe failed, reinit object and raise exception
571            self.init()
572            raise
573
574        return True

Method to read video using parametrized access through ffmpeg. Importante note: calling open on a VideoIO will close any former open video.

Parameters

filename: str or path filename of path to the file (mp4, avi, ...)

width: int optional (default -1) If defined as a positive value, width of input images will be converted to this value.

height: int optional (default -1) If defined as a positive value, height of input images will be converted to this value.

fps: float optional (default -1.0) If defined as a positive value, fps of input video will be converted to this value.

outputEncoding: PixelFormat optional (default PixelFormat.BGR24) Define order of channels for channels. Possible values are PixelFormat.BGR24 or PixelFormat.RGB24.

decodingParams: str optional (default None) Parameter to pass to ffmpeg to decode video like filters.

start_time: float optional (default 0.0) Define the reading start time. If not set, reading at beginning of the video.

Returns

bool Was the opening successfull

def read_frame(self, with_timestamps=False):
576    def read_frame(self, with_timestamps = False):
577        """
578        Read next frame from the video
579
580        Parameters
581        ----------
582        with_timestamps: bool optional (default False)
583            If set to True, the method returns a FrameContainer with the image and an array containing the associated timestamp(s)
584
585        Returns
586        ----------
587        nparray or FrameContainer
588            An image of shape (3,width,height). if with_timestamps is True, the return object is a FrameContainer with the image in ``FrameContainer.data`` and
589            the associated timestamp in ``FrameContainer.timestamps`` as an array (one element for one frame).
590        """
591
592        if self.pipe is None:
593            raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading a frame.".format(self.videoProgram))
594        # - pipe is in write mode
595        if self.mode != PipeMode.READ_MODE:
596            raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename))
597
598        if with_timestamps:
599            # get elapsed time in video, it is time of next frame(s)
600            current_elapsed_time = self.get_elapsed_time()
601
602        # read rgb image from pipe
603        buffer = self.pipe.stdout.read(self.imageSize)
604        if len(buffer) != self.imageSize:
605            # Incomplete image, ffmpeg have been stopped or killed, do not return an incomplete frame
606            # not considered as an error, no more frame, no exception
607            return None
608
609        # get numpy UINT8 array from buffer
610        rgbImage = np.frombuffer(buffer, dtype = np.uint8).reshape(self.shape)
611
612        # increase frame_counter
613        self.frame_counter.frame_count += 1
614
615        # say to gc that this buffer is no longer needed
616        del buffer
617
618        if with_timestamps:
619            return FrameContainer(1,rgbImage,self.fps,current_elapsed_time)
620
621        return rgbImage

Read next frame from the video

Parameters

with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the image and an array containing the associated timestamp(s)

Returns

nparray or FrameContainer An image of shape (3,width,height). if with_timestamps is True, the return object is a FrameContainer with the image in FrameContainer.data and the associated timestamp in FrameContainer.timestamps as an array (one element for one frame).

def read_batch( self, number_of_frames, with_timestamps=False) -> Union[<built-in function array>, FrameContainer]:
623    def read_batch(self, number_of_frames, with_timestamps = False) ->  Union[np.array, FrameContainer]:
624        """
625        Read next batch of images from the video
626
627        Parameters
628        ----------
629        number_of_frames: int
630            Number of desired images within the batch. The last batch of the video may have less images.
631            
632        with_timestamps: bool optional (default False)
633            If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames
634
635        Returns
636        ----------
637        nparray or FrameContainer
638            A batch of images of shape (n,3,width,height). if with_timestamps is True, the return object is a FrameContainer with the batch in ``FrameContainer.data`` and
639            the associated timestamps in ``FrameContainer.timestamps`` as an array (one element for each frame).
640        """
641
642        if self.pipe is None:
643            raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading frames.".format(self.videoProgram))
644        # - pipe is in write mode
645        if self.mode != PipeMode.READ_MODE:
646            raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename))
647
648        if with_timestamps:
649            # get elapsed time in video, it is time of next frame(s)
650            current_elapsed_time = self.get_elapsed_time()
651
652        # try to read complete batch
653        buffer = self.pipe.stdout.read(self.imageSize*number_of_frames)
654
655        # check if we have at least 1 image
656        if len(buffer) < self.imageSize:
657            # ffmpag backend have been stopped or killed
658            # not considered as an error, no more frame, no exception
659            return None
660
661        # compute actual number of Frames
662        actualNbFrames = len(buffer)//self.imageSize
663
664        # get and reshape batch from buffer
665        batch = np.frombuffer(buffer, dtype = np.uint8).reshape((actualNbFrames, self.height, self.width, 3))
666
667        # increase frame_counter
668        self.frame_counter.frame_count += actualNbFrames
669        
670        # say to gc that this buffer is no longer needed
671        del buffer
672
673        if with_timestamps:
674            return FrameContainer(actualNbFrames, batch, self.fps, current_elapsed_time)
675
676        return batch

Read next batch of images from the video

Parameters

number_of_frames: int Number of desired images within the batch. The last batch of the video may have less images.

with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames

Returns

nparray or FrameContainer A batch of images of shape (n,3,width,height). if with_timestamps is True, the return object is a FrameContainer with the batch in FrameContainer.data and the associated timestamps in FrameContainer.timestamps as an array (one element for each frame).

def write_frame(self, image) -> bool:
678    def write_frame(self, image) -> bool:
679        """
680        Write an image to the video
681
682        Parameters
683        ----------
684        image: nparray
685            The image of shape (3, width, height) to write to the video file in the PixelFormat provided when create was called.
686
687        Returns
688        ----------
689        bool
690            Writing was successful or not.
691        """
692        
693        # Check params
694        # - pipe exists
695        if self.pipe is None:
696            raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram))
697        # - pipe is in write mode
698        if self.mode != PipeMode.WRITE_MODE:
699            raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename))
700        # - shape of image is fine, thus we have pixels for a full compatible frame
701        if image.shape != self.shape:
702            raise self.VideoIOException("Wong image shape: {} expected {}.".format(image.shape,self.shape))
703        # - type of data is UINT8
704        if image.dtype != np.uint8:
705            raise self.VideoIOException("Wong pixel type: {} expected np.uint8.".format(image.dtype))
706
707        # write frame
708        buffer = image.tobytes()
709        if self.pipe.stdin.write( buffer ) < self.imageSize:
710            print( "Error writing frame to" )
711            return False
712
713        # increase frame_counter
714        self.frame_counter.frame_count += 1
715
716        # say to gc that this buffer is no longer needed 
717        del buffer
718
719        return True

Write an image to the video

Parameters

image: nparray The image of shape (3, width, height) to write to the video file in the PixelFormat provided when create was called.

Returns

bool Writing was successful or not.

def write_batch(self, batch) -> bool:
721    def write_batch(self, batch) -> bool:
722        """
723        Write a batch of images to the video
724
725        Parameters
726        ----------
727        batch: nparray
728            A batch of images to write to the video file in the PixelFormat provided when create was called.
729
730        Returns
731        ----------
732        bool
733            Writing was successful or not.
734        """
735
736        # Check params
737        # - pipe exists
738        if self.pipe is None:
739            raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram))
740        # - pipe is in write mode
741        if self.mode != PipeMode.WRITE_MODE:
742            raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename))
743        # - shape of images in batch is fine
744        if batch.shape[-3:] != self.shape:
745            raise self.VideoIOException("Wrong image shape in batch: {} expected {}.".format(batch.shape[-3:], self.shape))
746        # - we have the right amount of pixels for the full batch
747        if batch.size != (batch.shape[0]*self.imageSize):
748            raise self.VideoIOException("Wrong number of pixels in batch: {} expected {}.".format(batch.shape[-3:], self.imageSize))
749
750        # write frame
751        buffer = batch.tobytes()
752        if self.pipe.stdin.write( buffer ) < batch.size:
753            # say to gc that this buffer is no longer needed
754            del buffer
755            raise self.VideoIOException("Error writing batch to '{}'.".format(self.filename))
756
757        # increase frame_counter
758        self.frame_counter.frame_count += batch.shape[0]       
759            
760        # say to gc that this buffer is no longer needed
761        del buffer
762
763        return True

Write a batch of images to the video

Parameters

batch: nparray A batch of images to write to the video file in the PixelFormat provided when create was called.

Returns

bool Writing was successful or not.

def iter_frames(self, with_timestamps=False):
765    def iter_frames(self, with_timestamps = False):
766        """
767        Method to iterate on video frames using VideoIO obj.
768        for frame in obj.iter_frames():
769            ....
770
771        Parameters
772        ----------
773        with_timestamps: bool optional (default False)
774            If set to True, the method returns a FrameContainer with the batch and an array containing the associated timestamps to frames
775        """
776
777        try:
778            if self.mode == PipeMode.READ_MODE:
779                while self.isOpened():
780                    frame = self.readFrame(with_timestamps)
781                    if frame is not None:
782                        yield frame
783        finally:
784            self.close()

Method to iterate on video frames using VideoIO obj. for frame in obj.iter_frames(): ....

Parameters

with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the batch and an array containing the associated timestamps to frames

def iter_batches(self, batch_size: int, with_timestamps=False):
786    def iter_batches(self, batch_size : int, with_timestamps = False):
787        """
788        Method to iterate on batch of frames using VideoIO obj.
789        for image_batch in obj.iter_batches():
790            ....
791
792        Parameters
793        ----------
794        with_timestamps: bool optional (default False)
795            If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames
796        """
797        try:
798            if self.mode == PipeMode.READ_MODE:
799                while self.isOpened():
800                    batch = self.readBatch(batch_size, with_timestamps)
801                    if batch is not None:
802                        yield batch
803        finally:
804            self.close()

Method to iterate on batch of frames using VideoIO obj. for image_batch in obj.iter_batches(): ....

Parameters

with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames

@staticmethod
def getVideoTimeInSec(filename, *, debug=False, logLevel=16):
101    @staticmethod
102    def get_time_in_sec(filename, *, debug=False, logLevel=16):
103        """
104        Static method to get length of a video file in seconds including milliseconds as decimal part.
105
106        Parameters
107        ----------
108        filename : str or path
109            Video file name.
110
111        debug : bool (default False)
112            Show debug info.
113
114        log_level: int (default 16)
115            Log level to pass to the underlying ffmpeg/ffprobe command.
116        
117        Returns
118        ----------
119        float
120            Length in seconds of video file (including milliseconds as decimal part)
121        """
122        
123        cmd = [VideoIO.paramProgram, # ffprobe
124                    '-hide_banner',
125                    '-loglevel', str(logLevel),
126                    '-show_entries', 'format=duration',
127                    '-of', 'default=noprint_wrappers=1:nokey=1',
128                    str(filename)
129                    ]
130
131        if debug == True:
132            print(' '.join(cmd))
133
134        # call ffprobe and get params in one single line
135        lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe
136        output = lpipe.stdout.readlines()
137        lpipe.terminate()
138        # transform Bytes output to one single string
139        output = ''.join( [element.decode('utf-8') for element in output])
140
141        try:
142            return float(output)
143        except (ValueError, TypeError):
144            return None

Static method to get length of a video file in seconds including milliseconds as decimal part.

Parameters

filename : str or path Video file name.

debug : bool (default False) Show debug info.

log_level: int (default 16) Log level to pass to the underlying ffmpeg/ffprobe command.

Returns

float Length in seconds of video file (including milliseconds as decimal part)

@staticmethod
def getVideoParams(filename, *, debug=False, logLevel=16):
146    @staticmethod
147    def get_params(filename, *, debug=False, logLevel=16):
148        """
149        Static method to get params (width, height, fps) from a video file.
150
151        Parameters
152        ----------
153        filename: str or path
154            Video filename.
155
156        debug: bool (default (False)
157            Show debug info.
158
159        log_level: int (default 16)
160            Log level to pass to the underlying ffmpeg/ffprobe command.
161
162        Returns
163        ----------
164        tuple
165            Tuple containing (width, height, fps) of the video
166        """
167        cmd = [VideoIO.paramProgram, # ffprobe
168                    '-hide_banner',
169                    '-loglevel', str(logLevel),
170                    '-show_entries', 'stream=width,height,r_frame_rate',
171                    str(filename)
172                    ]
173
174        if debug == True:
175            print(' '.join(cmd))
176
177        # call ffprobe and get params in one single line
178        lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe
179        output = lpipe.stdout.readlines()
180        lpipe.terminate()
181        # transform Bytes output to one single string
182        output = ''.join( [element.decode('utf-8') for element in output])
183
184        pattern_width = r'width=(\d+)'
185        pattern_height = r'height=(\d+)'
186        pattern_fps = r'r_frame_rate=(\d+)/(\d+)'
187
188        # Search for values in the ffprobe output
189        match_width = re.search(pattern_width, output, flags=re.MULTILINE)
190        match_height = re.search(pattern_height, output, flags=re.MULTILINE)
191        match_fps = re.search(pattern_fps, output, flags=re.MULTILINE)
192
193        # Extraction des valeurs
194        if match_width:
195            width = int(match_width.group(1))
196        else:
197            raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'")
198
199        if match_height:
200            height = int(match_height.group(1))
201        else:
202            raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'")
203
204        if match_fps:
205            numerator = float(match_fps.group(1))
206            denominator = float(match_fps.group(2))
207            fps = numerator / denominator
208        else:
209            raise VideoIO.VideoIOException("Unable to get frame rate (fps) of '" + filename + "'")
210
211        return (width, height, fps)

Static method to get params (width, height, fps) from a video file.

Parameters

filename: str or path Video filename.

debug: bool (default (False) Show debug info.

log_level: int (default 16) Log level to pass to the underlying ffmpeg/ffprobe command.

Returns

tuple Tuple containing (width, height, fps) of the video

@staticmethod
def get_video_time_in_sec(filename, *, debug=False, logLevel=16):
101    @staticmethod
102    def get_time_in_sec(filename, *, debug=False, logLevel=16):
103        """
104        Static method to get length of a video file in seconds including milliseconds as decimal part.
105
106        Parameters
107        ----------
108        filename : str or path
109            Video file name.
110
111        debug : bool (default False)
112            Show debug info.
113
114        log_level: int (default 16)
115            Log level to pass to the underlying ffmpeg/ffprobe command.
116        
117        Returns
118        ----------
119        float
120            Length in seconds of video file (including milliseconds as decimal part)
121        """
122        
123        cmd = [VideoIO.paramProgram, # ffprobe
124                    '-hide_banner',
125                    '-loglevel', str(logLevel),
126                    '-show_entries', 'format=duration',
127                    '-of', 'default=noprint_wrappers=1:nokey=1',
128                    str(filename)
129                    ]
130
131        if debug == True:
132            print(' '.join(cmd))
133
134        # call ffprobe and get params in one single line
135        lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe
136        output = lpipe.stdout.readlines()
137        lpipe.terminate()
138        # transform Bytes output to one single string
139        output = ''.join( [element.decode('utf-8') for element in output])
140
141        try:
142            return float(output)
143        except (ValueError, TypeError):
144            return None

Static method to get length of a video file in seconds including milliseconds as decimal part.

Parameters

filename : str or path Video file name.

debug : bool (default False) Show debug info.

log_level: int (default 16) Log level to pass to the underlying ffmpeg/ffprobe command.

Returns

float Length in seconds of video file (including milliseconds as decimal part)

@staticmethod
def get_video_params(filename, *, debug=False, logLevel=16):
146    @staticmethod
147    def get_params(filename, *, debug=False, logLevel=16):
148        """
149        Static method to get params (width, height, fps) from a video file.
150
151        Parameters
152        ----------
153        filename: str or path
154            Video filename.
155
156        debug: bool (default (False)
157            Show debug info.
158
159        log_level: int (default 16)
160            Log level to pass to the underlying ffmpeg/ffprobe command.
161
162        Returns
163        ----------
164        tuple
165            Tuple containing (width, height, fps) of the video
166        """
167        cmd = [VideoIO.paramProgram, # ffprobe
168                    '-hide_banner',
169                    '-loglevel', str(logLevel),
170                    '-show_entries', 'stream=width,height,r_frame_rate',
171                    str(filename)
172                    ]
173
174        if debug == True:
175            print(' '.join(cmd))
176
177        # call ffprobe and get params in one single line
178        lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe
179        output = lpipe.stdout.readlines()
180        lpipe.terminate()
181        # transform Bytes output to one single string
182        output = ''.join( [element.decode('utf-8') for element in output])
183
184        pattern_width = r'width=(\d+)'
185        pattern_height = r'height=(\d+)'
186        pattern_fps = r'r_frame_rate=(\d+)/(\d+)'
187
188        # Search for values in the ffprobe output
189        match_width = re.search(pattern_width, output, flags=re.MULTILINE)
190        match_height = re.search(pattern_height, output, flags=re.MULTILINE)
191        match_fps = re.search(pattern_fps, output, flags=re.MULTILINE)
192
193        # Extraction des valeurs
194        if match_width:
195            width = int(match_width.group(1))
196        else:
197            raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'")
198
199        if match_height:
200            height = int(match_height.group(1))
201        else:
202            raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'")
203
204        if match_fps:
205            numerator = float(match_fps.group(1))
206            denominator = float(match_fps.group(2))
207            fps = numerator / denominator
208        else:
209            raise VideoIO.VideoIOException("Unable to get frame rate (fps) of '" + filename + "'")
210
211        return (width, height, fps)

Static method to get params (width, height, fps) from a video file.

Parameters

filename: str or path Video filename.

debug: bool (default (False) Show debug info.

log_level: int (default 16) Log level to pass to the underlying ffmpeg/ffprobe command.

Returns

tuple Tuple containing (width, height, fps) of the video

def isOpened(self) -> bool:
340    def is_opened(self) -> bool:
341        """
342        Method to get status of the underlying pipe to ffmpeg.
343
344        Returns
345        ----------
346        bool
347            True if pipe is opened (reading or writing mode), False if not.
348        """
349        # is the pipe opened?
350        if self.pipe is not None and self.pipe.poll() is None:
351            return True
352
353        return False

Method to get status of the underlying pipe to ffmpeg.

Returns

bool True if pipe is opened (reading or writing mode), False if not.

def readFrame(self, with_timestamps=False):
576    def read_frame(self, with_timestamps = False):
577        """
578        Read next frame from the video
579
580        Parameters
581        ----------
582        with_timestamps: bool optional (default False)
583            If set to True, the method returns a FrameContainer with the image and an array containing the associated timestamp(s)
584
585        Returns
586        ----------
587        nparray or FrameContainer
588            An image of shape (3,width,height). if with_timestamps is True, the return object is a FrameContainer with the image in ``FrameContainer.data`` and
589            the associated timestamp in ``FrameContainer.timestamps`` as an array (one element for one frame).
590        """
591
592        if self.pipe is None:
593            raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading a frame.".format(self.videoProgram))
594        # - pipe is in write mode
595        if self.mode != PipeMode.READ_MODE:
596            raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename))
597
598        if with_timestamps:
599            # get elapsed time in video, it is time of next frame(s)
600            current_elapsed_time = self.get_elapsed_time()
601
602        # read rgb image from pipe
603        buffer = self.pipe.stdout.read(self.imageSize)
604        if len(buffer) != self.imageSize:
605            # Incomplete image, ffmpeg have been stopped or killed, do not return an incomplete frame
606            # not considered as an error, no more frame, no exception
607            return None
608
609        # get numpy UINT8 array from buffer
610        rgbImage = np.frombuffer(buffer, dtype = np.uint8).reshape(self.shape)
611
612        # increase frame_counter
613        self.frame_counter.frame_count += 1
614
615        # say to gc that this buffer is no longer needed
616        del buffer
617
618        if with_timestamps:
619            return FrameContainer(1,rgbImage,self.fps,current_elapsed_time)
620
621        return rgbImage

Read next frame from the video

Parameters

with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the image and an array containing the associated timestamp(s)

Returns

nparray or FrameContainer An image of shape (3,width,height). if with_timestamps is True, the return object is a FrameContainer with the image in FrameContainer.data and the associated timestamp in FrameContainer.timestamps as an array (one element for one frame).

def readBatch( self, number_of_frames, with_timestamps=False) -> Union[<built-in function array>, FrameContainer]:
623    def read_batch(self, number_of_frames, with_timestamps = False) ->  Union[np.array, FrameContainer]:
624        """
625        Read next batch of images from the video
626
627        Parameters
628        ----------
629        number_of_frames: int
630            Number of desired images within the batch. The last batch of the video may have less images.
631            
632        with_timestamps: bool optional (default False)
633            If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames
634
635        Returns
636        ----------
637        nparray or FrameContainer
638            A batch of images of shape (n,3,width,height). if with_timestamps is True, the return object is a FrameContainer with the batch in ``FrameContainer.data`` and
639            the associated timestamps in ``FrameContainer.timestamps`` as an array (one element for each frame).
640        """
641
642        if self.pipe is None:
643            raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading frames.".format(self.videoProgram))
644        # - pipe is in write mode
645        if self.mode != PipeMode.READ_MODE:
646            raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename))
647
648        if with_timestamps:
649            # get elapsed time in video, it is time of next frame(s)
650            current_elapsed_time = self.get_elapsed_time()
651
652        # try to read complete batch
653        buffer = self.pipe.stdout.read(self.imageSize*number_of_frames)
654
655        # check if we have at least 1 image
656        if len(buffer) < self.imageSize:
657            # ffmpag backend have been stopped or killed
658            # not considered as an error, no more frame, no exception
659            return None
660
661        # compute actual number of Frames
662        actualNbFrames = len(buffer)//self.imageSize
663
664        # get and reshape batch from buffer
665        batch = np.frombuffer(buffer, dtype = np.uint8).reshape((actualNbFrames, self.height, self.width, 3))
666
667        # increase frame_counter
668        self.frame_counter.frame_count += actualNbFrames
669        
670        # say to gc that this buffer is no longer needed
671        del buffer
672
673        if with_timestamps:
674            return FrameContainer(actualNbFrames, batch, self.fps, current_elapsed_time)
675
676        return batch

Read next batch of images from the video

Parameters

number_of_frames: int Number of desired images within the batch. The last batch of the video may have less images.

with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames

Returns

nparray or FrameContainer A batch of images of shape (n,3,width,height). if with_timestamps is True, the return object is a FrameContainer with the batch in FrameContainer.data and the associated timestamps in FrameContainer.timestamps as an array (one element for each frame).

def writeFrame(self, image) -> bool:
678    def write_frame(self, image) -> bool:
679        """
680        Write an image to the video
681
682        Parameters
683        ----------
684        image: nparray
685            The image of shape (3, width, height) to write to the video file in the PixelFormat provided when create was called.
686
687        Returns
688        ----------
689        bool
690            Writing was successful or not.
691        """
692        
693        # Check params
694        # - pipe exists
695        if self.pipe is None:
696            raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram))
697        # - pipe is in write mode
698        if self.mode != PipeMode.WRITE_MODE:
699            raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename))
700        # - shape of image is fine, thus we have pixels for a full compatible frame
701        if image.shape != self.shape:
702            raise self.VideoIOException("Wong image shape: {} expected {}.".format(image.shape,self.shape))
703        # - type of data is UINT8
704        if image.dtype != np.uint8:
705            raise self.VideoIOException("Wong pixel type: {} expected np.uint8.".format(image.dtype))
706
707        # write frame
708        buffer = image.tobytes()
709        if self.pipe.stdin.write( buffer ) < self.imageSize:
710            print( "Error writing frame to" )
711            return False
712
713        # increase frame_counter
714        self.frame_counter.frame_count += 1
715
716        # say to gc that this buffer is no longer needed 
717        del buffer
718
719        return True

Write an image to the video

Parameters

image: nparray The image of shape (3, width, height) to write to the video file in the PixelFormat provided when create was called.

Returns

bool Writing was successful or not.

def writeBatch(self, batch) -> bool:
721    def write_batch(self, batch) -> bool:
722        """
723        Write a batch of images to the video
724
725        Parameters
726        ----------
727        batch: nparray
728            A batch of images to write to the video file in the PixelFormat provided when create was called.
729
730        Returns
731        ----------
732        bool
733            Writing was successful or not.
734        """
735
736        # Check params
737        # - pipe exists
738        if self.pipe is None:
739            raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram))
740        # - pipe is in write mode
741        if self.mode != PipeMode.WRITE_MODE:
742            raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename))
743        # - shape of images in batch is fine
744        if batch.shape[-3:] != self.shape:
745            raise self.VideoIOException("Wrong image shape in batch: {} expected {}.".format(batch.shape[-3:], self.shape))
746        # - we have the right amount of pixels for the full batch
747        if batch.size != (batch.shape[0]*self.imageSize):
748            raise self.VideoIOException("Wrong number of pixels in batch: {} expected {}.".format(batch.shape[-3:], self.imageSize))
749
750        # write frame
751        buffer = batch.tobytes()
752        if self.pipe.stdin.write( buffer ) < batch.size:
753            # say to gc that this buffer is no longer needed
754            del buffer
755            raise self.VideoIOException("Error writing batch to '{}'.".format(self.filename))
756
757        # increase frame_counter
758        self.frame_counter.frame_count += batch.shape[0]       
759            
760        # say to gc that this buffer is no longer needed
761        del buffer
762
763        return True

Write a batch of images to the video

Parameters

batch: nparray A batch of images to write to the video file in the PixelFormat provided when create was called.

Returns

bool Writing was successful or not.

videoProgram = '/usr/local/lib/python3.12/site-packages/static_ffmpeg/bin/linux/ffmpeg'
paramProgram = '/usr/local/lib/python3.12/site-packages/static_ffmpeg/bin/linux/ffprobe'
class VideoIO.VideoIOException(builtins.Exception):
36    class VideoIOException(Exception):
37        """
38        Dedicated exception class for VideoIO class.
39        """
40        def __init__(self, message="Error while reading/writing video occurs"):
41            self.message = message
42            super().__init__(self.message)

Dedicated exception class for VideoIO class.

VideoIO.VideoIOException(message='Error while reading/writing video occurs')
40        def __init__(self, message="Error while reading/writing video occurs"):
41            self.message = message
42            super().__init__(self.message)
message
class VideoIO.PixelFormat(enum.Enum):
44    class PixelFormat(Enum):
45        """
46        Enum class for supported input video type: GBR 24 bits or RGB 24 bis.
47        """
48        GBR24 = 'bgr24' # default format
49        RGB24 = 'rgb24'

Enum class for supported input video type: GBR 24 bits or RGB 24 bis.

GBR24 = <PixelFormat.GBR24: 'bgr24'>
RGB24 = <PixelFormat.RGB24: 'rgb24'>
class AudioIO:
 34class AudioIO:
 35    # "static" variables  to ffmpeg, ffprobe executables
 36    audioProgram, paramProgram = static_ffmpeg.run.get_or_fetch_platform_executables_else_raise()
 37
 38    class AudioIOException(Exception):
 39        """
 40        Dedicated exception class for AudioIO class.
 41        """
 42        def __init__(self, message="Error while reading/writing video occurs"):
 43            self.message = message
 44            super().__init__(self.message)
 45
 46    class AudioFormat(Enum):
 47        """
 48        Enum class for supported input video type: 32-bit float is the only supported type for the moment.
 49        """
 50        PCM32LE = 'pcm_f32le' # default format (unique mode for the moment)
 51
 52    @classmethod
 53    def reader(cls, filename, *, loglevel = 16, debug = False, **kwargs):
 54        """
 55        Create and open an AudioIO object in reader mode
 56
 57        See ``AudioIO.open`` for the full list of accepted parameters.
 58        """
 59        reader = cls(logLevel=loglevel, debug=debug)
 60        reader.open(filename, **kwargs)
 61        return reader
 62
 63    @classmethod
 64    def writer(cls, filename, sample_rate, channels, *, loglevel = 16, debug = False, **kwargs):
 65        """
 66        Create and open an AudioIO object in writer mode
 67
 68        See ``AudioIO.create`` for the full list of accepted parameters.
 69        """
 70        writer = cls(logLevel=loglevel, debug=debug)
 71        writer.create(filename, sample_rate, channels, **kwargs)
 72        return writer
 73
 74    # standard method
 75    def get_corresponding_writer(self, filename, **kwargs):
 76        """
 77        Method to get writer for an audio file with same sample_rate, channels as the current one AudioIO object
 78
 79        See `AudioIO.create` for the full list
 80        of accepted parameters.
 81        """
 82        return AudioIO.writer(filename, self.sample_rate, self.channels, **kwargs)
 83
 84    # To use with context manager "with AudioIO.reader(...) as f:' for instance
 85    def __enter__(self):
 86        """
 87        Method call at initialisation of a context manager like "with AudioIO.reader/writer(...) as f:' for instance
 88        """
 89        # simply return myself
 90        return self
 91
 92    def __exit__(self, exc_type, exc_val, exc_tb):
 93        """
 94        Method call when existing of a context manager like "with AudioIO.reader/writer(...) as f:' for instance
 95        """
 96        # close AudioIO
 97        self.close()
 98        return False
 99
100    @staticmethod
101    def get_time_in_sec(filename, *, debug=False, logLevel=16):
102        """
103        Static method to get length of an audio file (or video file containing audio) in seconds including milliseconds as decimal part (3 decimals).
104
105        Parameters
106        ----------
107        filename : str or path. 
108            Raw audio waveform as a 1D array.
109
110        debug : bool (default False).
111            Show debug info.
112
113        logLevel: int (default 16).
114            Log level to pass to the underlying ffmpeg/ffprobe command.
115        
116        Returns
117        ----------
118        float
119            Length in seconds of video file (including milliseconds as decimal part with 3 decimals)
120        """
121        
122        cmd = [AudioIO.paramProgram, # ffprobe
123                    '-hide_banner',
124                    '-loglevel', str(logLevel),
125                    '-show_entries', 'format=duration',
126                    '-of', 'default=noprint_wrappers=1:nokey=1',
127                    str(filename)
128                    ]
129
130        if debug == True:
131            print(' '.join(cmd))
132
133        # call ffprobe and get params in one single line
134        lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg
135        output = lpipe.stdout.readlines()
136        lpipe.terminate()
137        # transform Bytes output to one single string
138        output = ''.join( [element.decode('utf-8') for element in output])
139
140        try:
141            return float(output)
142        except (ValueError, TypeError):
143            return None
144
145    @staticmethod
146    def get_params(filename, *, debug=False, logLevel=16):
147        """
148        Static method to get params (channels,sample_rate) of a (video containing) audio file in seconds.
149
150        Parameters
151        ----------
152        filename : str or path.
153            Raw audio waveform as a 1D array.
154
155        debug : bool (default (False).
156            Show debug info.
157
158        log_level: int (default 16).
159            Log level to pass to the underlying ffmpeg/ffprobe command.
160
161        Returns
162        ----------
163        tuple
164            Tuple containing (channels,sample_rate) of the file
165        """
166        cmd = [AudioIO.paramProgram, # ffprobe
167                    '-hide_banner',
168                    '-loglevel', str(logLevel),
169                    '-show_entries', 'stream=channels,sample_rate',
170                    str(filename)
171                    ]
172
173        if debug == True:
174            print(' '.join(cmd))
175
176        # call ffprobe and get params in one single line
177        lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg
178        output = lpipe.stdout.readlines()
179        lpipe.terminate()
180        # transform Bytes output to one single string
181        output = ''.join( [element.decode('utf-8') for element in output])
182
183        pattern_sample_rate = r'sample_rate=(\d+)'
184        pattern_channels = r'channels=(\d+)'
185
186        # Search for values in the ffprobe output
187        match_sample_rate = re.search(pattern_sample_rate, output, flags=re.MULTILINE)
188        match_channels = re.search(pattern_channels, output, flags=re.MULTILINE)
189
190        # Extraction des valeurs
191        if match_sample_rate:
192            sample_rate = int(match_sample_rate.group(1))
193        else:
194            raise AudioIO.AudioIOException("Unable to get audio sample_rate of '" + str(filename) + "'")
195
196        if match_channels:
197            channels = int(match_channels.group(1))
198        else:
199            raise AudioIO.AudioIOException("Unable to get audio channels of '" + str(filename) + "'")
200
201        return (channels,sample_rate)
202
203        # Attributes
204        mode: PipeMode
205        """ Pipemode of the current object (default PipeMode.UNK_MODE)"""
206
207        loglevel: int
208        """ loglevel of the underlying ffmpeg backend for this object (default 16)"""
209
210        debug: bool
211        """ debug flag for this object (print debut info, default False)"""
212
213        channels: int
214        """ Number of channels of images (default -1) """
215
216        sample_rate: int
217        """ sample_rate of images (default -1) """
218
219        plannar: bool
220        """ Read/write data as plannar, i.e. not interleaved (default True) """
221
222        pipe: sp.Popen
223        """ pipe object to ffmpeg/ffprobe (default None)"""
224
225        frame_size: int
226        """ Weight in bytes of one image (default -1)"""
227
228        filename: str
229        """ Filename of the file (default None)"""
230
231        frame_counter: FrameCounter
232        """ `Framecounter` object to count ellapsed time (default None)"""
233
234    def __init__(self, *, logLevel = 16, debug = False):
235        """
236        Create a AudioIO object giving ffmpeg/ffrobe loglevel and defining debug mode
237
238        Parameters
239        ----------
240        log_level: int (default 16)
241            Log level to pass to the underlying ffmpeg/ffprobe command.
242
243        debug: bool (default (False)
244            Show debug info. while processing video
245        """
246
247        self.mode = PipeMode.UNK_MODE
248        self.logLevel = logLevel
249        self.debug = debug
250
251        # Call init() method
252        self.init()
253
254    def init(self):
255        """
256        Init or reinit a AudioIO object.
257        """
258        self.channels  = -1
259        self.sample_rate = -1
260        self.plannar = True
261        self.pipe = None
262        self.frame_size = -1
263        self.filename = None
264        self.frame_counter = None
265        self.float_size = None
266
267    _repr_exclude = {"pipe"}
268    """ List of excluded attribute for string conversion. """
269
270    # converting the object to a string representation
271    def __repr__(self):
272        """
273        Convert object (excluding attributes in _repr_exclude) to string representation.
274        """
275        attrs = ", ".join(
276            f"{k}={v!r}"
277            for k, v in self.__dict__.items()
278            if k not in self._repr_exclude
279        )
280        return f"{self.__class__.__name__}({attrs})"
281
282    __str__ = __repr__
283    """ String representation """
284
285    def get_elapsed_time_as_str(self) -> str:
286        """
287        Method to get elapsed time (float value represented) as str.
288
289        Returns
290        ----------
291        str or None
292            Elapsed time (float value) as str, "15.500" for instance for 15 secondes and 500 milliseconds
293            None if no frame counter are available.
294        """
295        if self.frame_counter is None:
296            return None
297        return self.frame_counter.get_elapsed_time_as_str()
298
299    def get_formated_elapsed_time_as_str(self,show_ms=True) -> str:
300        """
301        Method to get elapsed time (hour format) as str.
302
303        Returns
304        ----------
305        str or None
306            Elapsed time (float value) as str, "00:00:15.500" for instance for 15 secondes and 500 milliseconds
307            None if no frame counter are available.
308        """
309        if self.frame_counter is None:
310            return None
311        return self.frame_counter.get_formated_elapsed_time_as_str()
312
313    def get_elapsed_time(self) -> float:
314        """
315        Method to get elapsed time as float value rounded to 3 decimals.
316
317        Returns
318        ----------
319        float or None
320            Elapsed time (float value) as str, 15.500 for instance for 15 secondes and 500 milliseconds
321            None if no frame counter are available.
322        """
323        if self.frame_counter is None:
324            return None
325        return self.frame_counter.get_elapsed_time()
326
327    def is_opened(self) -> bool:
328        """
329        Method to get status of the underlying pipe to ffmpeg.
330
331        Returns
332        ----------
333        bool
334            True if pipe is opened (reading or writing mode), False if not.
335        """
336        # is the pip opened?
337        if self.pipe is not None and self.pipe.poll() is None:
338            return True
339
340        return False
341
342    def close(self):
343        """
344        Method to close current pipe to ffmpeg (if any). Ffmpeg/ffprobe  will be terminated. Object can be reused using open or create methods.
345        """
346        if self.pipe is not None:
347            if self.mode == PipeMode.WRITE_MODE:
348                # killing will make ffmpeg not finish properly the job, close the pipe
349                # to let it know that no more data are comming
350                self.pipe.stdin.close()
351            else: # self.mode == PipeMode.READ_MODE
352                # in read mode, no need to be nice, send SIGTERM on Linux,/Kill it on windows
353                self.pipe.kill()
354
355            # wait for subprocess to end
356            self.pipe.wait()
357
358        # reinit object for later use
359        self.init()
360
361    def create( self, filename, sample_rate, channels, *, writeOverExistingFile = False,
362                outputEncoding = AudioFormat.PCM32LE, encodingParams = None, plannar = True ):
363        """
364        Method to create a audio file using parametrized access through ffmpeg. Importante note: calling create
365        on a AudioIO will close any former open video.
366
367        Parameters
368        ----------
369        filename: str or path
370            filename of path to the file (mp4, avi, ...)
371
372        sample_rate: int
373            If defined as a positive value, sample_rates of the output file will be set to this value.
374
375        channels: int
376            If defined as a positive value, number of channels of output file will be set to this value.
377
378        fps:
379            If defined as a positive value, fps of input video will be set to this value.
380
381        outputEncoding: AudioFormat optional (default AudioFormat.PCM32LE)
382            Define audio format for samples. Possible value is AudioFormat.PCM32LE.
383
384        encodingParams: str optional (default None)
385            Parameter to pass to ffmpeg to encode video like audio filters.
386
387        plannar : bool optionnal (default True)
388            Input data to write are grouped by channel if True, interleaved instead.
389
390        Returns
391        ----------
392        bool
393            Was the creation successfull
394        """
395
396        # Close if already opened
397        self.close()
398
399        # Set geometry/fps of the video stream from params
400        self.sample_rate = int(sample_rate)
401        self.channels = int(channels)
402        self.plannar = plannar
403
404        # Compute size of float 32, usefull if we want later to use other floating point convention
405        self.float_size = int(np.dtype(np.float32).itemsize)
406
407        # Check params
408        if self.sample_rate <= 0 or self.channels <= 0:
409            raise self.AudioIOException("Bad parameters: sample_rate={}, channels={}".format(self.sample_rate,self.channels))
410
411        # To write audio, we do not need to know in advance frame size, we will write x values of n bytes
412        self.frame_size = None
413
414        # Video params are set, open the video
415        cmd = [self.audioProgram] # ffmpeg
416
417        if writeOverExistingFile == True:
418            cmd.extend(['-y'])
419
420        cmd.extend(['-hide_banner',
421            '-nostats',
422            '-loglevel', str(self.logLevel),
423            '-f', 'f32le', '-acodec', outputEncoding.value, # input expected coding
424            '-ar', f"{self.sample_rate}",
425            '-ac', f"{self.channels}",
426            '-i', '-'])
427
428        if encodingParams is not None:
429            cmd.extend(encodingParams.split())
430
431        # Audio filename converted to str (for Path values)
432        cmd.extend( ['-vn', str(filename) ] )
433
434        if self.debug == True:
435            print( ' '.join(cmd), file=sys.stderr )
436
437        # store filename and set mode
438        self.filename = str(filename)
439        self.mode = PipeMode.WRITE_MODE
440
441        # call ffmpeg in write mode
442        try:
443            self.pipe = sp.Popen(cmd, stdin=sp.PIPE)
444            self.frame_counter = FrameCounter(self.sample_rate)
445        except Exception as e:
446            # if pipe failed, reinit object and raise exception
447            self.init()
448            raise
449
450        return True
451
452    def open( self, filename, *, sample_rate = -1, channels = -1, inputEncoding = AudioFormat.PCM32LE,
453                    decodingParams = None, frame_size = 1.0, plannar = True, start_time = 0.0 ):
454        """
455        Method to read (video file containing) audio using parametrized access through ffmpeg. Importante note: calling open
456        on a AudioIO will close any former open file.
457
458        Parameters
459        ----------
460        filename: str or path
461            filename of path to the file (mp4, avi, ...)
462
463        sample_rate: int optional (default -1)
464            If defined as a positive value, sample rate of the input audio will be converted to this value.
465
466        channels: int optional (default -1)
467            If defined as a positive value, number of channels of the input audio will converted to this value.
468
469        inputEncoding: AudioFormat optional (default AudioFormat.PCM32LE)
470            Define audio format for samples. Possible value is AudioFormat.PCM32LE.
471
472        decodingParams: str optional (default None)
473            Parameter to pass to ffmpeg to decode video like audio filters.
474
475        plannar: bool optionnal (default True)
476            Group audio samples per channel if True. Else, samples are interleaved.
477
478        frame_size: int or float (default 1.0)
479            If frame_size is an int, it is the number of expected samples in each frame, for instance 8000 for 8000 samples.
480            if frame_size is a float, it is considered as a time in seconds for each audio frame, for instance 1.0 for 1 second, 0.010 for 10 ms.
481            Number of samples in this case is computed using frame_size and sample_rate as int(frame_size * sample_rate)
482
483        start_time: float optional (default 0.0)
484            Define the reading start time. If not set, reading at beginning of the file.
485
486        Returns
487        ----------
488        bool
489            Was the opening successfull
490        """
491
492        # Close if already opened
493        self.close()
494
495        # Force conversion of parameters
496        channels = int(channels)
497        sample_rate = float(sample_rate)
498
499        self.plannar = plannar
500
501        # Compute size of float 32, usefull if we want later to use other floating point convention
502        self.float_size = int(np.dtype(np.float32).itemsize)
503
504        # get parameters from file if needed:
505        if sample_rate <= 0 or channels <= 0:
506            self.channels, self.sample_rate = self.getAudioParams(filename)
507
508        # check if parameters ask to overide video parameters
509        if channels > 0:
510            self.channels = channels
511        if sample_rate > 0:
512            self.sample_rate = sample_rate
513
514        # check parameters
515
516        if isinstance(frame_size,float):
517            # time in seconds
518            self.frame_size = int(frame_size*self.sample_rate)
519        elif isinstance(frame_size,int):
520            # number of samples
521            self.frame_size = frame_size
522        else:
523            # to do
524            pass
525
526        # Video params are set, open the video
527        cmd = [self.audioProgram, # ffmpeg
528                    '-hide_banner',
529                    '-nostats',
530                    '-loglevel', str(self.logLevel)]
531
532        if decodingParams is not None:
533            cmd.extend([decodingParams.split()])
534
535        if start_time < 0.0:
536            pass
537        elif start_time > 0.0:
538            cmd.extend(["-ss", f"{start_time}"])            
539
540        cmd.extend( ['-i', str(filename),
541                     '-f', 'f32le', '-acodec', inputEncoding.value, # input expected coding
542                     '-ar', f"{self.sample_rate}",
543                     '-ac', f"{self.channels}",
544                     '-' # output to stdout
545                    ]
546                )
547
548        if self.debug == True:
549            print( ' '.join(cmd) )
550
551        # store filename and set mode to READ_MODE
552        self.filename = str(filename)
553        self.mode = PipeMode.READ_MODE
554
555        # call ffmpeg in read mode
556        try:
557            self.pipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe
558            self.frame_counter = FrameCounter(self.sample_rate)
559            if start_time > 0.0:
560                self.frame_counter += start_time # adding with float means adding time
561        except Exception as e:
562            # if pipe failed, reinit object and raise exception
563            self.init()
564            raise
565
566        return True
567
568    def read_frame(self, with_timestamps = False):
569        """
570        Read next frame from the audio file
571
572        Parameters
573        ----------
574        with_timestamps: bool optional (default False)
575            If set to True, the method returns a ``FrameContainer`` with the audio and an array containing the associated timestamp(s)
576
577        Returns
578        ----------
579        nparray or FrameContainer
580            A frame of shape (self.channels,self.frame_size) as defined in the reader/open call if self.plannar is True. A frame
581            of shape (self.channels*self.frame_size) with interleaved data if self.plannar is False.
582            if with_timestamps is True, the return object is a FrameContainer with the audio data in ``FrameContainer.data`` and
583            the associated timestamp in ``FrameContainer.timestamps`` as an array (one element).
584        """
585
586        if self.pipe is None:
587            raise self.AudioIOException("No pipe opened to {}. Call open(...) before reading a frame.".format(self.audioProgram))
588        # - pipe is in write mode
589        if self.mode != PipeMode.READ_MODE:
590            raise self.AudioIOException("Pipe to {} for '{}' not opened in read mode.".format(self.audioProgram, self.filename))
591
592        if with_timestamps:
593            # get elapsed time in video, it is time of next frame(s)
594            current_elapsed_time = self.get_elapsed_time()
595
596        # read rgb image from pipe
597        toread = self.frame_size*self.float_size
598        buffer = self.pipe.stdout.read(toread)
599
600        if buffer == b"":
601            # not considered as an error, no more frame, no exception
602            return None
603
604        # get numpy UINT8 array from buffer
605        audio = np.frombuffer(buffer, dtype = np.float32).reshape(len(buffer)//self.float_size, self.channels)
606
607        # make it plannar (or not)
608        if self.plannar:
609            #transpose it
610            audio = audio.T
611
612        # increase frame_counter
613        self.frame_counter.frame_count += (self.frame_size * self.channels)
614
615        # say to gc that this buffer is no longer needed
616        del buffer
617
618        if with_timestamps:
619            return FrameContainer(1, audio, self.frame_size/self.sample_rate, current_elapsed_time)
620        
621        return audio
622
623    def read_batch(self, numberOfFrames, with_timestamps = False):
624        """
625        Read next batch of audio from the file
626
627        Parameters
628        ----------
629        number_of_frames: int
630            Number of desired images within the batch. The last batch from the file may have less images.
631            
632        with_timestamps: bool optional (default False)
633            If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames
634
635        Returns
636        ----------
637        nparray or FrameContainer
638            A batch of shape (n, self.channels,self.frame_size) as defined in the reader/open call if self.plannar is True. A batch
639            of shape (n, self.channels*self.frame_size) with interleaved data if self.plannar is False.
640            if with_timestamps is True, the return object is a FrameContainer with the audio batch in ``FrameContainer.data`` and
641            the associated timestamp in ``FrameContainer.timestamps`` as an array (one element for each audio frame).
642        """
643
644        if self.pipe is None:
645            raise self.AudioIOException("No pipe opened to {}. Call open(...) before reading frames.".format(self.audioProgram))
646        # - pipe is in write mode
647        if self.mode != PipeMode.READ_MODE:
648            raise self.AudioIOException("Pipe to {} for '{}' not opened in read mode.".format(self.audioProgram, self.filename))
649
650        if with_timestamps:
651            # get elapsed time in video, it is time of next frame(s)
652            current_elapsed_time = self.get_elapsed_time()
653
654        # try to read complete batch
655        toread = self.frame_size*self.float_size*self.channels*numberOfFrames
656        buffer = self.pipe.stdout.read(toread)
657
658        # check if we are at the end of the buffer
659        if buffer == b"":
660            # not considered as an error, no more frame, no exception
661            return None
662
663        # compute actual number of Frames
664        # do we have a full batch ? Computer standard division
665        actualNbFrames = len(buffer)/(self.frame_size*self.float_size*self.channels)
666
667        if actualNbFrames.is_integer():
668            actualNbFrames = int(actualNbFrames)
669            # get and reshape batch from buffer
670            batch = np.frombuffer(buffer, dtype = np.float32).reshape((actualNbFrames, self.frame_size, self.channels,))
671        else:
672            # We do not have a full batch, we are at end of the stream or ffmpeg has been stopped/killed
673            # Compute frame size in samples knowing len of buffer, self.float_size, self.channels and numberOfFrames
674            l_frame_size = len(buffer)//(numberOfFrames*self.float_size*self.channels)
675            if l_frame_size <= 0:
676                # not considered as an error, no more frame, no exception
677                return None
678            # get and reshape batch from buffer
679            batch = np.frombuffer(buffer, dtype = np.float32).reshape((numberOfFrames, l_frame_size, self.channels,))
680
681        if self.plannar:
682            batch = batch.transpose(0, 2, 1)
683
684        # increase frame_counter
685        self.frame_counter.frame_count += (actualNbFrames * self.frame_size * self.channels)
686        
687        # say to gc that this buffer is no longer needed
688        del buffer
689
690        if with_timestamps:
691            return FrameContainer( actualNbFrames, batch, self.frame_size/self.sample_rate, current_elapsed_time)
692        
693        return batch
694
695    def write_frame(self, audio) -> bool:
696        """
697        Write an audio frame to the file
698
699        Parameters
700        ----------
701        audio: nparray
702            The audio frame to write to the video file of shape (self.channels,nb_samples_per_channel) if plannar is True else (self.channels*nb_samples_per_channel).
703
704        Returns
705        ----------
706        bool
707            Writing was successful or not.
708        """
709        # Check params
710        # - pipe exists
711        if self.pipe is None:
712            raise self.AudioIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.audioProgram))
713        # - pipe is in write mode
714        if self.mode != PipeMode.WRITE_MODE:
715            raise self.AudioIOException("Pipe to {} for '{}' not opened in write mode.".format(self.audioProgram, self.filename))
716        # - shape of image is fine, thus we have pixels for a full compatible frame
717        if audio.shape[0] != self.channels:
718            raise self.AudioIOException("Wong audio shape: {} expected ({}, nb_samples_to_write).".format(audio.shape,self.channels))
719        # - type of data is Float32
720        if audio.dtype != np.float32:
721            raise self.AudioIOException("Wong audio type: {} expected np.float32.".format(audio.dtype))
722
723        # array must have a shape (channels, samples), reshape it it to (samples, channels) if plannar
724        if not self.plannar:
725            audio = audio.reshape(-1)
726
727        # print( audio.shape )
728
729        # garantee to have a C continuous array
730        if not audio.flags['C_CONTIGUOUS']:
731            a = np.ascontiguousarray(a) 
732
733        # write frame
734        buffer = audio.tobytes()
735        if self.pipe.stdin.write( buffer ) < len(buffer):
736            print( f"Error writing frame to {self.filename}" )
737            return False
738
739        # increase frame_counter
740        self.frame_counter.frame_count += (audio.shape[1] * self.channels)
741
742        # say to gc that this buffer is no longer needed 
743        del buffer
744
745        return True
746
747    def write_batch(self, batch):
748        """
749        Write a batch of audio frame to the file
750
751        Parameters
752        ----------
753        batch: nparray
754            The batch of audio frames to write to the video file of shape (n,self.channels,nb_samples_per_channel) if plannar is True else (n,self.channels*nb_samples_per_channel) of interleaved audio data.
755
756        Returns
757        ----------
758        bool
759            Writing was successful or not.
760        """
761        # Check params
762        # - pipe exists
763        if self.pipe is None:
764            raise self.AudioIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.audioProgram))
765        # - pipe is in write mode
766        if self.mode != PipeMode.WRITE_MODE:
767            raise self.AudioIOException("Pipe to {} for '{}' not opened in write mode.".format(self.audioProgram, self.filename))
768        # batch is 3D (n, channels, nb samples)
769        if batch.ndim !=3:
770            raise self.AudioIOException("Wrong batch shape: {} expected 3 dimensions (n, n_channels, n_samples_per_channel).".format(batch.shape))
771        # - shape of images in batch is fine
772        if batch.shape[2] != self.channels:
773            raise self.AudioIOException("Wrong audio channels in batch: {} expected {} {}.".format(batch.shape[2], self.channels, batch.shape))
774
775        # array must have a shape (n * n_channels * n_samples_per_channel) before writing them to pipe
776        # reshape it it to (n * n_channels * n_samples_per_channel) if plannar is False
777        if not self.plannar:
778            # goes from (n, n_channels, n_samples_per_channel) to (n * n_channels * n_samples_per_channel)
779            batch = batch.transpose(0, 2, 1) # first go to (n, n_samples_per_channel, n_channels)
780            batch = batch.reshape(-1) # then to 1D array (n * n_channels * n_samples_per_channel)
781
782        # garantee to have a C continuous array
783        if not batch.flags['C_CONTIGUOUS']:
784            batch = np.ascontiguousarray(batch)
785
786        # write frame
787        buffer = batch.tobytes()
788        if self.pipe.stdin.write( buffer ) < len(buffer):
789            # say to gc that this buffer is no longer needed
790            del buffer
791            raise self.AudioIOException("Error writing batch to '{}'.".format(self.filename))
792
793        # increase frame_counter
794        self.frame_counter.frame_count += int(batch.shape[0]/self.channels) # int conversion is mandatory to avoid confusion with time as float
795              
796        # say to gc that this buffer is no longer needed
797        del buffer
798
799        return True
800
801    def iter_frames(self, with_timestamps = False):
802        """
803        Method to iterate on audio frames using AudioIO obj.
804        for audio_frame in obj.iter_frames():
805            ....
806
807        Parameters
808        ----------
809        with_timestamps: bool optional (default False)
810            If set to True, the method returns a FrameContainer object with the batch and an array containing the associated timestamps to frames
811
812        Returns
813        ----------
814        nparray or FrameContainer
815            A batch of images of shape ()
816        """
817
818        try:
819            if self.mode == PipeMode.READ_MODE:
820                while self.isOpened():
821                    frame = self.readFrame(with_timestamps)
822                    if frame is not None:
823                        yield frame
824        finally:
825            self.close()
826
827    def iter_batches(self, batch_size : int, with_timestamps = False ):
828        """
829        Method to iterate on batch ofaudio  frames using AudioIO obj.
830        for audio_batch in obj.iter_batches():
831            ....
832
833        Parameters
834        ----------
835        with_timestamps: bool optional (default False)
836            If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames
837        """
838        try:
839            if self.mode == PipeMode.READ_MODE:
840                while self.isOpened():
841                    batch = self.readBatch(batch_size, with_timestamps)
842                    if batch is not None:
843                        yield batch
844        finally:
845            self.close()
846
847    # function aliases to be compliant with original C++ version
848    getAudioTimeInSec = get_time_in_sec
849    getAudioParams = get_params
850    get_audio_time_in_sec = get_time_in_sec
851    get_audio_params = get_params
852    isOpened = is_opened
853    readFrame = read_frame
854    readBatch = read_batch
855    writeFrame = write_frame
856    writeBatch = write_batch
AudioIO(*, logLevel=16, debug=False)
234    def __init__(self, *, logLevel = 16, debug = False):
235        """
236        Create a AudioIO object giving ffmpeg/ffrobe loglevel and defining debug mode
237
238        Parameters
239        ----------
240        log_level: int (default 16)
241            Log level to pass to the underlying ffmpeg/ffprobe command.
242
243        debug: bool (default (False)
244            Show debug info. while processing video
245        """
246
247        self.mode = PipeMode.UNK_MODE
248        self.logLevel = logLevel
249        self.debug = debug
250
251        # Call init() method
252        self.init()

Create a AudioIO object giving ffmpeg/ffrobe loglevel and defining debug mode

Parameters

log_level: int (default 16) Log level to pass to the underlying ffmpeg/ffprobe command.

debug: bool (default (False) Show debug info. while processing video

@classmethod
def reader(cls, filename, *, loglevel=16, debug=False, **kwargs):
52    @classmethod
53    def reader(cls, filename, *, loglevel = 16, debug = False, **kwargs):
54        """
55        Create and open an AudioIO object in reader mode
56
57        See ``AudioIO.open`` for the full list of accepted parameters.
58        """
59        reader = cls(logLevel=loglevel, debug=debug)
60        reader.open(filename, **kwargs)
61        return reader

Create and open an AudioIO object in reader mode

See AudioIO.open for the full list of accepted parameters.

@classmethod
def writer( cls, filename, sample_rate, channels, *, loglevel=16, debug=False, **kwargs):
63    @classmethod
64    def writer(cls, filename, sample_rate, channels, *, loglevel = 16, debug = False, **kwargs):
65        """
66        Create and open an AudioIO object in writer mode
67
68        See ``AudioIO.create`` for the full list of accepted parameters.
69        """
70        writer = cls(logLevel=loglevel, debug=debug)
71        writer.create(filename, sample_rate, channels, **kwargs)
72        return writer

Create and open an AudioIO object in writer mode

See AudioIO.create for the full list of accepted parameters.

def get_corresponding_writer(self, filename, **kwargs):
75    def get_corresponding_writer(self, filename, **kwargs):
76        """
77        Method to get writer for an audio file with same sample_rate, channels as the current one AudioIO object
78
79        See `AudioIO.create` for the full list
80        of accepted parameters.
81        """
82        return AudioIO.writer(filename, self.sample_rate, self.channels, **kwargs)

Method to get writer for an audio file with same sample_rate, channels as the current one AudioIO object

See AudioIO.create for the full list of accepted parameters.

@staticmethod
def get_time_in_sec(filename, *, debug=False, logLevel=16):
100    @staticmethod
101    def get_time_in_sec(filename, *, debug=False, logLevel=16):
102        """
103        Static method to get length of an audio file (or video file containing audio) in seconds including milliseconds as decimal part (3 decimals).
104
105        Parameters
106        ----------
107        filename : str or path. 
108            Raw audio waveform as a 1D array.
109
110        debug : bool (default False).
111            Show debug info.
112
113        logLevel: int (default 16).
114            Log level to pass to the underlying ffmpeg/ffprobe command.
115        
116        Returns
117        ----------
118        float
119            Length in seconds of video file (including milliseconds as decimal part with 3 decimals)
120        """
121        
122        cmd = [AudioIO.paramProgram, # ffprobe
123                    '-hide_banner',
124                    '-loglevel', str(logLevel),
125                    '-show_entries', 'format=duration',
126                    '-of', 'default=noprint_wrappers=1:nokey=1',
127                    str(filename)
128                    ]
129
130        if debug == True:
131            print(' '.join(cmd))
132
133        # call ffprobe and get params in one single line
134        lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg
135        output = lpipe.stdout.readlines()
136        lpipe.terminate()
137        # transform Bytes output to one single string
138        output = ''.join( [element.decode('utf-8') for element in output])
139
140        try:
141            return float(output)
142        except (ValueError, TypeError):
143            return None

Static method to get length of an audio file (or video file containing audio) in seconds including milliseconds as decimal part (3 decimals).

Parameters

filename : str or path. Raw audio waveform as a 1D array.

debug : bool (default False). Show debug info.

logLevel: int (default 16). Log level to pass to the underlying ffmpeg/ffprobe command.

Returns

float Length in seconds of video file (including milliseconds as decimal part with 3 decimals)

@staticmethod
def get_params(filename, *, debug=False, logLevel=16):
145    @staticmethod
146    def get_params(filename, *, debug=False, logLevel=16):
147        """
148        Static method to get params (channels,sample_rate) of a (video containing) audio file in seconds.
149
150        Parameters
151        ----------
152        filename : str or path.
153            Raw audio waveform as a 1D array.
154
155        debug : bool (default (False).
156            Show debug info.
157
158        log_level: int (default 16).
159            Log level to pass to the underlying ffmpeg/ffprobe command.
160
161        Returns
162        ----------
163        tuple
164            Tuple containing (channels,sample_rate) of the file
165        """
166        cmd = [AudioIO.paramProgram, # ffprobe
167                    '-hide_banner',
168                    '-loglevel', str(logLevel),
169                    '-show_entries', 'stream=channels,sample_rate',
170                    str(filename)
171                    ]
172
173        if debug == True:
174            print(' '.join(cmd))
175
176        # call ffprobe and get params in one single line
177        lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg
178        output = lpipe.stdout.readlines()
179        lpipe.terminate()
180        # transform Bytes output to one single string
181        output = ''.join( [element.decode('utf-8') for element in output])
182
183        pattern_sample_rate = r'sample_rate=(\d+)'
184        pattern_channels = r'channels=(\d+)'
185
186        # Search for values in the ffprobe output
187        match_sample_rate = re.search(pattern_sample_rate, output, flags=re.MULTILINE)
188        match_channels = re.search(pattern_channels, output, flags=re.MULTILINE)
189
190        # Extraction des valeurs
191        if match_sample_rate:
192            sample_rate = int(match_sample_rate.group(1))
193        else:
194            raise AudioIO.AudioIOException("Unable to get audio sample_rate of '" + str(filename) + "'")
195
196        if match_channels:
197            channels = int(match_channels.group(1))
198        else:
199            raise AudioIO.AudioIOException("Unable to get audio channels of '" + str(filename) + "'")
200
201        return (channels,sample_rate)
202
203        # Attributes
204        mode: PipeMode
205        """ Pipemode of the current object (default PipeMode.UNK_MODE)"""
206
207        loglevel: int
208        """ loglevel of the underlying ffmpeg backend for this object (default 16)"""
209
210        debug: bool
211        """ debug flag for this object (print debut info, default False)"""
212
213        channels: int
214        """ Number of channels of images (default -1) """
215
216        sample_rate: int
217        """ sample_rate of images (default -1) """
218
219        plannar: bool
220        """ Read/write data as plannar, i.e. not interleaved (default True) """
221
222        pipe: sp.Popen
223        """ pipe object to ffmpeg/ffprobe (default None)"""
224
225        frame_size: int
226        """ Weight in bytes of one image (default -1)"""
227
228        filename: str
229        """ Filename of the file (default None)"""
230
231        frame_counter: FrameCounter
232        """ `Framecounter` object to count ellapsed time (default None)"""

Static method to get params (channels,sample_rate) of a (video containing) audio file in seconds.

Parameters

filename : str or path. Raw audio waveform as a 1D array.

debug : bool (default (False). Show debug info.

log_level: int (default 16). Log level to pass to the underlying ffmpeg/ffprobe command.

Returns

tuple Tuple containing (channels,sample_rate) of the file

mode
logLevel
debug
def init(self):
254    def init(self):
255        """
256        Init or reinit a AudioIO object.
257        """
258        self.channels  = -1
259        self.sample_rate = -1
260        self.plannar = True
261        self.pipe = None
262        self.frame_size = -1
263        self.filename = None
264        self.frame_counter = None
265        self.float_size = None

Init or reinit a AudioIO object.

def get_elapsed_time_as_str(self) -> str:
285    def get_elapsed_time_as_str(self) -> str:
286        """
287        Method to get elapsed time (float value represented) as str.
288
289        Returns
290        ----------
291        str or None
292            Elapsed time (float value) as str, "15.500" for instance for 15 secondes and 500 milliseconds
293            None if no frame counter are available.
294        """
295        if self.frame_counter is None:
296            return None
297        return self.frame_counter.get_elapsed_time_as_str()

Method to get elapsed time (float value represented) as str.

Returns

str or None Elapsed time (float value) as str, "15.500" for instance for 15 secondes and 500 milliseconds None if no frame counter are available.

def get_formated_elapsed_time_as_str(self, show_ms=True) -> str:
299    def get_formated_elapsed_time_as_str(self,show_ms=True) -> str:
300        """
301        Method to get elapsed time (hour format) as str.
302
303        Returns
304        ----------
305        str or None
306            Elapsed time (float value) as str, "00:00:15.500" for instance for 15 secondes and 500 milliseconds
307            None if no frame counter are available.
308        """
309        if self.frame_counter is None:
310            return None
311        return self.frame_counter.get_formated_elapsed_time_as_str()

Method to get elapsed time (hour format) as str.

Returns

str or None Elapsed time (float value) as str, "00:00:15.500" for instance for 15 secondes and 500 milliseconds None if no frame counter are available.

def get_elapsed_time(self) -> float:
313    def get_elapsed_time(self) -> float:
314        """
315        Method to get elapsed time as float value rounded to 3 decimals.
316
317        Returns
318        ----------
319        float or None
320            Elapsed time (float value) as str, 15.500 for instance for 15 secondes and 500 milliseconds
321            None if no frame counter are available.
322        """
323        if self.frame_counter is None:
324            return None
325        return self.frame_counter.get_elapsed_time()

Method to get elapsed time as float value rounded to 3 decimals.

Returns

float or None Elapsed time (float value) as str, 15.500 for instance for 15 secondes and 500 milliseconds None if no frame counter are available.

def is_opened(self) -> bool:
327    def is_opened(self) -> bool:
328        """
329        Method to get status of the underlying pipe to ffmpeg.
330
331        Returns
332        ----------
333        bool
334            True if pipe is opened (reading or writing mode), False if not.
335        """
336        # is the pip opened?
337        if self.pipe is not None and self.pipe.poll() is None:
338            return True
339
340        return False

Method to get status of the underlying pipe to ffmpeg.

Returns

bool True if pipe is opened (reading or writing mode), False if not.

def close(self):
342    def close(self):
343        """
344        Method to close current pipe to ffmpeg (if any). Ffmpeg/ffprobe  will be terminated. Object can be reused using open or create methods.
345        """
346        if self.pipe is not None:
347            if self.mode == PipeMode.WRITE_MODE:
348                # killing will make ffmpeg not finish properly the job, close the pipe
349                # to let it know that no more data are comming
350                self.pipe.stdin.close()
351            else: # self.mode == PipeMode.READ_MODE
352                # in read mode, no need to be nice, send SIGTERM on Linux,/Kill it on windows
353                self.pipe.kill()
354
355            # wait for subprocess to end
356            self.pipe.wait()
357
358        # reinit object for later use
359        self.init()

Method to close current pipe to ffmpeg (if any). Ffmpeg/ffprobe will be terminated. Object can be reused using open or create methods.

def create( self, filename, sample_rate, channels, *, writeOverExistingFile=False, outputEncoding=<AudioFormat.PCM32LE: 'pcm_f32le'>, encodingParams=None, plannar=True):
361    def create( self, filename, sample_rate, channels, *, writeOverExistingFile = False,
362                outputEncoding = AudioFormat.PCM32LE, encodingParams = None, plannar = True ):
363        """
364        Method to create a audio file using parametrized access through ffmpeg. Importante note: calling create
365        on a AudioIO will close any former open video.
366
367        Parameters
368        ----------
369        filename: str or path
370            filename of path to the file (mp4, avi, ...)
371
372        sample_rate: int
373            If defined as a positive value, sample_rates of the output file will be set to this value.
374
375        channels: int
376            If defined as a positive value, number of channels of output file will be set to this value.
377
378        fps:
379            If defined as a positive value, fps of input video will be set to this value.
380
381        outputEncoding: AudioFormat optional (default AudioFormat.PCM32LE)
382            Define audio format for samples. Possible value is AudioFormat.PCM32LE.
383
384        encodingParams: str optional (default None)
385            Parameter to pass to ffmpeg to encode video like audio filters.
386
387        plannar : bool optionnal (default True)
388            Input data to write are grouped by channel if True, interleaved instead.
389
390        Returns
391        ----------
392        bool
393            Was the creation successfull
394        """
395
396        # Close if already opened
397        self.close()
398
399        # Set geometry/fps of the video stream from params
400        self.sample_rate = int(sample_rate)
401        self.channels = int(channels)
402        self.plannar = plannar
403
404        # Compute size of float 32, usefull if we want later to use other floating point convention
405        self.float_size = int(np.dtype(np.float32).itemsize)
406
407        # Check params
408        if self.sample_rate <= 0 or self.channels <= 0:
409            raise self.AudioIOException("Bad parameters: sample_rate={}, channels={}".format(self.sample_rate,self.channels))
410
411        # To write audio, we do not need to know in advance frame size, we will write x values of n bytes
412        self.frame_size = None
413
414        # Video params are set, open the video
415        cmd = [self.audioProgram] # ffmpeg
416
417        if writeOverExistingFile == True:
418            cmd.extend(['-y'])
419
420        cmd.extend(['-hide_banner',
421            '-nostats',
422            '-loglevel', str(self.logLevel),
423            '-f', 'f32le', '-acodec', outputEncoding.value, # input expected coding
424            '-ar', f"{self.sample_rate}",
425            '-ac', f"{self.channels}",
426            '-i', '-'])
427
428        if encodingParams is not None:
429            cmd.extend(encodingParams.split())
430
431        # Audio filename converted to str (for Path values)
432        cmd.extend( ['-vn', str(filename) ] )
433
434        if self.debug == True:
435            print( ' '.join(cmd), file=sys.stderr )
436
437        # store filename and set mode
438        self.filename = str(filename)
439        self.mode = PipeMode.WRITE_MODE
440
441        # call ffmpeg in write mode
442        try:
443            self.pipe = sp.Popen(cmd, stdin=sp.PIPE)
444            self.frame_counter = FrameCounter(self.sample_rate)
445        except Exception as e:
446            # if pipe failed, reinit object and raise exception
447            self.init()
448            raise
449
450        return True

Method to create a audio file using parametrized access through ffmpeg. Importante note: calling create on a AudioIO will close any former open video.

Parameters

filename: str or path filename of path to the file (mp4, avi, ...)

sample_rate: int If defined as a positive value, sample_rates of the output file will be set to this value.

channels: int If defined as a positive value, number of channels of output file will be set to this value.

fps: If defined as a positive value, fps of input video will be set to this value.

outputEncoding: AudioFormat optional (default AudioFormat.PCM32LE) Define audio format for samples. Possible value is AudioFormat.PCM32LE.

encodingParams: str optional (default None) Parameter to pass to ffmpeg to encode video like audio filters.

plannar : bool optionnal (default True) Input data to write are grouped by channel if True, interleaved instead.

Returns

bool Was the creation successfull

def open( self, filename, *, sample_rate=-1, channels=-1, inputEncoding=<AudioFormat.PCM32LE: 'pcm_f32le'>, decodingParams=None, frame_size=1.0, plannar=True, start_time=0.0):
452    def open( self, filename, *, sample_rate = -1, channels = -1, inputEncoding = AudioFormat.PCM32LE,
453                    decodingParams = None, frame_size = 1.0, plannar = True, start_time = 0.0 ):
454        """
455        Method to read (video file containing) audio using parametrized access through ffmpeg. Importante note: calling open
456        on a AudioIO will close any former open file.
457
458        Parameters
459        ----------
460        filename: str or path
461            filename of path to the file (mp4, avi, ...)
462
463        sample_rate: int optional (default -1)
464            If defined as a positive value, sample rate of the input audio will be converted to this value.
465
466        channels: int optional (default -1)
467            If defined as a positive value, number of channels of the input audio will converted to this value.
468
469        inputEncoding: AudioFormat optional (default AudioFormat.PCM32LE)
470            Define audio format for samples. Possible value is AudioFormat.PCM32LE.
471
472        decodingParams: str optional (default None)
473            Parameter to pass to ffmpeg to decode video like audio filters.
474
475        plannar: bool optionnal (default True)
476            Group audio samples per channel if True. Else, samples are interleaved.
477
478        frame_size: int or float (default 1.0)
479            If frame_size is an int, it is the number of expected samples in each frame, for instance 8000 for 8000 samples.
480            if frame_size is a float, it is considered as a time in seconds for each audio frame, for instance 1.0 for 1 second, 0.010 for 10 ms.
481            Number of samples in this case is computed using frame_size and sample_rate as int(frame_size * sample_rate)
482
483        start_time: float optional (default 0.0)
484            Define the reading start time. If not set, reading at beginning of the file.
485
486        Returns
487        ----------
488        bool
489            Was the opening successfull
490        """
491
492        # Close if already opened
493        self.close()
494
495        # Force conversion of parameters
496        channels = int(channels)
497        sample_rate = float(sample_rate)
498
499        self.plannar = plannar
500
501        # Compute size of float 32, usefull if we want later to use other floating point convention
502        self.float_size = int(np.dtype(np.float32).itemsize)
503
504        # get parameters from file if needed:
505        if sample_rate <= 0 or channels <= 0:
506            self.channels, self.sample_rate = self.getAudioParams(filename)
507
508        # check if parameters ask to overide video parameters
509        if channels > 0:
510            self.channels = channels
511        if sample_rate > 0:
512            self.sample_rate = sample_rate
513
514        # check parameters
515
516        if isinstance(frame_size,float):
517            # time in seconds
518            self.frame_size = int(frame_size*self.sample_rate)
519        elif isinstance(frame_size,int):
520            # number of samples
521            self.frame_size = frame_size
522        else:
523            # to do
524            pass
525
526        # Video params are set, open the video
527        cmd = [self.audioProgram, # ffmpeg
528                    '-hide_banner',
529                    '-nostats',
530                    '-loglevel', str(self.logLevel)]
531
532        if decodingParams is not None:
533            cmd.extend([decodingParams.split()])
534
535        if start_time < 0.0:
536            pass
537        elif start_time > 0.0:
538            cmd.extend(["-ss", f"{start_time}"])            
539
540        cmd.extend( ['-i', str(filename),
541                     '-f', 'f32le', '-acodec', inputEncoding.value, # input expected coding
542                     '-ar', f"{self.sample_rate}",
543                     '-ac', f"{self.channels}",
544                     '-' # output to stdout
545                    ]
546                )
547
548        if self.debug == True:
549            print( ' '.join(cmd) )
550
551        # store filename and set mode to READ_MODE
552        self.filename = str(filename)
553        self.mode = PipeMode.READ_MODE
554
555        # call ffmpeg in read mode
556        try:
557            self.pipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe
558            self.frame_counter = FrameCounter(self.sample_rate)
559            if start_time > 0.0:
560                self.frame_counter += start_time # adding with float means adding time
561        except Exception as e:
562            # if pipe failed, reinit object and raise exception
563            self.init()
564            raise
565
566        return True

Method to read (video file containing) audio using parametrized access through ffmpeg. Importante note: calling open on a AudioIO will close any former open file.

Parameters

filename: str or path filename of path to the file (mp4, avi, ...)

sample_rate: int optional (default -1) If defined as a positive value, sample rate of the input audio will be converted to this value.

channels: int optional (default -1) If defined as a positive value, number of channels of the input audio will converted to this value.

inputEncoding: AudioFormat optional (default AudioFormat.PCM32LE) Define audio format for samples. Possible value is AudioFormat.PCM32LE.

decodingParams: str optional (default None) Parameter to pass to ffmpeg to decode video like audio filters.

plannar: bool optionnal (default True) Group audio samples per channel if True. Else, samples are interleaved.

frame_size: int or float (default 1.0) If frame_size is an int, it is the number of expected samples in each frame, for instance 8000 for 8000 samples. if frame_size is a float, it is considered as a time in seconds for each audio frame, for instance 1.0 for 1 second, 0.010 for 10 ms. Number of samples in this case is computed using frame_size and sample_rate as int(frame_size * sample_rate)

start_time: float optional (default 0.0) Define the reading start time. If not set, reading at beginning of the file.

Returns

bool Was the opening successfull

def read_frame(self, with_timestamps=False):
568    def read_frame(self, with_timestamps = False):
569        """
570        Read next frame from the audio file
571
572        Parameters
573        ----------
574        with_timestamps: bool optional (default False)
575            If set to True, the method returns a ``FrameContainer`` with the audio and an array containing the associated timestamp(s)
576
577        Returns
578        ----------
579        nparray or FrameContainer
580            A frame of shape (self.channels,self.frame_size) as defined in the reader/open call if self.plannar is True. A frame
581            of shape (self.channels*self.frame_size) with interleaved data if self.plannar is False.
582            if with_timestamps is True, the return object is a FrameContainer with the audio data in ``FrameContainer.data`` and
583            the associated timestamp in ``FrameContainer.timestamps`` as an array (one element).
584        """
585
586        if self.pipe is None:
587            raise self.AudioIOException("No pipe opened to {}. Call open(...) before reading a frame.".format(self.audioProgram))
588        # - pipe is in write mode
589        if self.mode != PipeMode.READ_MODE:
590            raise self.AudioIOException("Pipe to {} for '{}' not opened in read mode.".format(self.audioProgram, self.filename))
591
592        if with_timestamps:
593            # get elapsed time in video, it is time of next frame(s)
594            current_elapsed_time = self.get_elapsed_time()
595
596        # read rgb image from pipe
597        toread = self.frame_size*self.float_size
598        buffer = self.pipe.stdout.read(toread)
599
600        if buffer == b"":
601            # not considered as an error, no more frame, no exception
602            return None
603
604        # get numpy UINT8 array from buffer
605        audio = np.frombuffer(buffer, dtype = np.float32).reshape(len(buffer)//self.float_size, self.channels)
606
607        # make it plannar (or not)
608        if self.plannar:
609            #transpose it
610            audio = audio.T
611
612        # increase frame_counter
613        self.frame_counter.frame_count += (self.frame_size * self.channels)
614
615        # say to gc that this buffer is no longer needed
616        del buffer
617
618        if with_timestamps:
619            return FrameContainer(1, audio, self.frame_size/self.sample_rate, current_elapsed_time)
620        
621        return audio

Read next frame from the audio file

Parameters

with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the audio and an array containing the associated timestamp(s)

Returns

nparray or FrameContainer A frame of shape (self.channels,self.frame_size) as defined in the reader/open call if self.plannar is True. A frame of shape (self.channels*self.frame_size) with interleaved data if self.plannar is False. if with_timestamps is True, the return object is a FrameContainer with the audio data in FrameContainer.data and the associated timestamp in FrameContainer.timestamps as an array (one element).

def read_batch(self, numberOfFrames, with_timestamps=False):
623    def read_batch(self, numberOfFrames, with_timestamps = False):
624        """
625        Read next batch of audio from the file
626
627        Parameters
628        ----------
629        number_of_frames: int
630            Number of desired images within the batch. The last batch from the file may have less images.
631            
632        with_timestamps: bool optional (default False)
633            If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames
634
635        Returns
636        ----------
637        nparray or FrameContainer
638            A batch of shape (n, self.channels,self.frame_size) as defined in the reader/open call if self.plannar is True. A batch
639            of shape (n, self.channels*self.frame_size) with interleaved data if self.plannar is False.
640            if with_timestamps is True, the return object is a FrameContainer with the audio batch in ``FrameContainer.data`` and
641            the associated timestamp in ``FrameContainer.timestamps`` as an array (one element for each audio frame).
642        """
643
644        if self.pipe is None:
645            raise self.AudioIOException("No pipe opened to {}. Call open(...) before reading frames.".format(self.audioProgram))
646        # - pipe is in write mode
647        if self.mode != PipeMode.READ_MODE:
648            raise self.AudioIOException("Pipe to {} for '{}' not opened in read mode.".format(self.audioProgram, self.filename))
649
650        if with_timestamps:
651            # get elapsed time in video, it is time of next frame(s)
652            current_elapsed_time = self.get_elapsed_time()
653
654        # try to read complete batch
655        toread = self.frame_size*self.float_size*self.channels*numberOfFrames
656        buffer = self.pipe.stdout.read(toread)
657
658        # check if we are at the end of the buffer
659        if buffer == b"":
660            # not considered as an error, no more frame, no exception
661            return None
662
663        # compute actual number of Frames
664        # do we have a full batch ? Computer standard division
665        actualNbFrames = len(buffer)/(self.frame_size*self.float_size*self.channels)
666
667        if actualNbFrames.is_integer():
668            actualNbFrames = int(actualNbFrames)
669            # get and reshape batch from buffer
670            batch = np.frombuffer(buffer, dtype = np.float32).reshape((actualNbFrames, self.frame_size, self.channels,))
671        else:
672            # We do not have a full batch, we are at end of the stream or ffmpeg has been stopped/killed
673            # Compute frame size in samples knowing len of buffer, self.float_size, self.channels and numberOfFrames
674            l_frame_size = len(buffer)//(numberOfFrames*self.float_size*self.channels)
675            if l_frame_size <= 0:
676                # not considered as an error, no more frame, no exception
677                return None
678            # get and reshape batch from buffer
679            batch = np.frombuffer(buffer, dtype = np.float32).reshape((numberOfFrames, l_frame_size, self.channels,))
680
681        if self.plannar:
682            batch = batch.transpose(0, 2, 1)
683
684        # increase frame_counter
685        self.frame_counter.frame_count += (actualNbFrames * self.frame_size * self.channels)
686        
687        # say to gc that this buffer is no longer needed
688        del buffer
689
690        if with_timestamps:
691            return FrameContainer( actualNbFrames, batch, self.frame_size/self.sample_rate, current_elapsed_time)
692        
693        return batch

Read next batch of audio from the file

Parameters

number_of_frames: int Number of desired images within the batch. The last batch from the file may have less images.

with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames

Returns

nparray or FrameContainer A batch of shape (n, self.channels,self.frame_size) as defined in the reader/open call if self.plannar is True. A batch of shape (n, self.channels*self.frame_size) with interleaved data if self.plannar is False. if with_timestamps is True, the return object is a FrameContainer with the audio batch in FrameContainer.data and the associated timestamp in FrameContainer.timestamps as an array (one element for each audio frame).

def write_frame(self, audio) -> bool:
695    def write_frame(self, audio) -> bool:
696        """
697        Write an audio frame to the file
698
699        Parameters
700        ----------
701        audio: nparray
702            The audio frame to write to the video file of shape (self.channels,nb_samples_per_channel) if plannar is True else (self.channels*nb_samples_per_channel).
703
704        Returns
705        ----------
706        bool
707            Writing was successful or not.
708        """
709        # Check params
710        # - pipe exists
711        if self.pipe is None:
712            raise self.AudioIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.audioProgram))
713        # - pipe is in write mode
714        if self.mode != PipeMode.WRITE_MODE:
715            raise self.AudioIOException("Pipe to {} for '{}' not opened in write mode.".format(self.audioProgram, self.filename))
716        # - shape of image is fine, thus we have pixels for a full compatible frame
717        if audio.shape[0] != self.channels:
718            raise self.AudioIOException("Wong audio shape: {} expected ({}, nb_samples_to_write).".format(audio.shape,self.channels))
719        # - type of data is Float32
720        if audio.dtype != np.float32:
721            raise self.AudioIOException("Wong audio type: {} expected np.float32.".format(audio.dtype))
722
723        # array must have a shape (channels, samples), reshape it it to (samples, channels) if plannar
724        if not self.plannar:
725            audio = audio.reshape(-1)
726
727        # print( audio.shape )
728
729        # garantee to have a C continuous array
730        if not audio.flags['C_CONTIGUOUS']:
731            a = np.ascontiguousarray(a) 
732
733        # write frame
734        buffer = audio.tobytes()
735        if self.pipe.stdin.write( buffer ) < len(buffer):
736            print( f"Error writing frame to {self.filename}" )
737            return False
738
739        # increase frame_counter
740        self.frame_counter.frame_count += (audio.shape[1] * self.channels)
741
742        # say to gc that this buffer is no longer needed 
743        del buffer
744
745        return True

Write an audio frame to the file

Parameters

audio: nparray The audio frame to write to the video file of shape (self.channels,nb_samples_per_channel) if plannar is True else (self.channels*nb_samples_per_channel).

Returns

bool Writing was successful or not.

def write_batch(self, batch):
747    def write_batch(self, batch):
748        """
749        Write a batch of audio frame to the file
750
751        Parameters
752        ----------
753        batch: nparray
754            The batch of audio frames to write to the video file of shape (n,self.channels,nb_samples_per_channel) if plannar is True else (n,self.channels*nb_samples_per_channel) of interleaved audio data.
755
756        Returns
757        ----------
758        bool
759            Writing was successful or not.
760        """
761        # Check params
762        # - pipe exists
763        if self.pipe is None:
764            raise self.AudioIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.audioProgram))
765        # - pipe is in write mode
766        if self.mode != PipeMode.WRITE_MODE:
767            raise self.AudioIOException("Pipe to {} for '{}' not opened in write mode.".format(self.audioProgram, self.filename))
768        # batch is 3D (n, channels, nb samples)
769        if batch.ndim !=3:
770            raise self.AudioIOException("Wrong batch shape: {} expected 3 dimensions (n, n_channels, n_samples_per_channel).".format(batch.shape))
771        # - shape of images in batch is fine
772        if batch.shape[2] != self.channels:
773            raise self.AudioIOException("Wrong audio channels in batch: {} expected {} {}.".format(batch.shape[2], self.channels, batch.shape))
774
775        # array must have a shape (n * n_channels * n_samples_per_channel) before writing them to pipe
776        # reshape it it to (n * n_channels * n_samples_per_channel) if plannar is False
777        if not self.plannar:
778            # goes from (n, n_channels, n_samples_per_channel) to (n * n_channels * n_samples_per_channel)
779            batch = batch.transpose(0, 2, 1) # first go to (n, n_samples_per_channel, n_channels)
780            batch = batch.reshape(-1) # then to 1D array (n * n_channels * n_samples_per_channel)
781
782        # garantee to have a C continuous array
783        if not batch.flags['C_CONTIGUOUS']:
784            batch = np.ascontiguousarray(batch)
785
786        # write frame
787        buffer = batch.tobytes()
788        if self.pipe.stdin.write( buffer ) < len(buffer):
789            # say to gc that this buffer is no longer needed
790            del buffer
791            raise self.AudioIOException("Error writing batch to '{}'.".format(self.filename))
792
793        # increase frame_counter
794        self.frame_counter.frame_count += int(batch.shape[0]/self.channels) # int conversion is mandatory to avoid confusion with time as float
795              
796        # say to gc that this buffer is no longer needed
797        del buffer
798
799        return True

Write a batch of audio frame to the file

Parameters

batch: nparray The batch of audio frames to write to the video file of shape (n,self.channels,nb_samples_per_channel) if plannar is True else (n,self.channels*nb_samples_per_channel) of interleaved audio data.

Returns

bool Writing was successful or not.

def iter_frames(self, with_timestamps=False):
801    def iter_frames(self, with_timestamps = False):
802        """
803        Method to iterate on audio frames using AudioIO obj.
804        for audio_frame in obj.iter_frames():
805            ....
806
807        Parameters
808        ----------
809        with_timestamps: bool optional (default False)
810            If set to True, the method returns a FrameContainer object with the batch and an array containing the associated timestamps to frames
811
812        Returns
813        ----------
814        nparray or FrameContainer
815            A batch of images of shape ()
816        """
817
818        try:
819            if self.mode == PipeMode.READ_MODE:
820                while self.isOpened():
821                    frame = self.readFrame(with_timestamps)
822                    if frame is not None:
823                        yield frame
824        finally:
825            self.close()

Method to iterate on audio frames using AudioIO obj. for audio_frame in obj.iter_frames(): ....

Parameters

with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer object with the batch and an array containing the associated timestamps to frames

Returns

nparray or FrameContainer A batch of images of shape ()

def iter_batches(self, batch_size: int, with_timestamps=False):
827    def iter_batches(self, batch_size : int, with_timestamps = False ):
828        """
829        Method to iterate on batch ofaudio  frames using AudioIO obj.
830        for audio_batch in obj.iter_batches():
831            ....
832
833        Parameters
834        ----------
835        with_timestamps: bool optional (default False)
836            If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames
837        """
838        try:
839            if self.mode == PipeMode.READ_MODE:
840                while self.isOpened():
841                    batch = self.readBatch(batch_size, with_timestamps)
842                    if batch is not None:
843                        yield batch
844        finally:
845            self.close()

Method to iterate on batch ofaudio frames using AudioIO obj. for audio_batch in obj.iter_batches(): ....

Parameters

with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames

@staticmethod
def getAudioTimeInSec(filename, *, debug=False, logLevel=16):
100    @staticmethod
101    def get_time_in_sec(filename, *, debug=False, logLevel=16):
102        """
103        Static method to get length of an audio file (or video file containing audio) in seconds including milliseconds as decimal part (3 decimals).
104
105        Parameters
106        ----------
107        filename : str or path. 
108            Raw audio waveform as a 1D array.
109
110        debug : bool (default False).
111            Show debug info.
112
113        logLevel: int (default 16).
114            Log level to pass to the underlying ffmpeg/ffprobe command.
115        
116        Returns
117        ----------
118        float
119            Length in seconds of video file (including milliseconds as decimal part with 3 decimals)
120        """
121        
122        cmd = [AudioIO.paramProgram, # ffprobe
123                    '-hide_banner',
124                    '-loglevel', str(logLevel),
125                    '-show_entries', 'format=duration',
126                    '-of', 'default=noprint_wrappers=1:nokey=1',
127                    str(filename)
128                    ]
129
130        if debug == True:
131            print(' '.join(cmd))
132
133        # call ffprobe and get params in one single line
134        lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg
135        output = lpipe.stdout.readlines()
136        lpipe.terminate()
137        # transform Bytes output to one single string
138        output = ''.join( [element.decode('utf-8') for element in output])
139
140        try:
141            return float(output)
142        except (ValueError, TypeError):
143            return None

Static method to get length of an audio file (or video file containing audio) in seconds including milliseconds as decimal part (3 decimals).

Parameters

filename : str or path. Raw audio waveform as a 1D array.

debug : bool (default False). Show debug info.

logLevel: int (default 16). Log level to pass to the underlying ffmpeg/ffprobe command.

Returns

float Length in seconds of video file (including milliseconds as decimal part with 3 decimals)

@staticmethod
def getAudioParams(filename, *, debug=False, logLevel=16):
145    @staticmethod
146    def get_params(filename, *, debug=False, logLevel=16):
147        """
148        Static method to get params (channels,sample_rate) of a (video containing) audio file in seconds.
149
150        Parameters
151        ----------
152        filename : str or path.
153            Raw audio waveform as a 1D array.
154
155        debug : bool (default (False).
156            Show debug info.
157
158        log_level: int (default 16).
159            Log level to pass to the underlying ffmpeg/ffprobe command.
160
161        Returns
162        ----------
163        tuple
164            Tuple containing (channels,sample_rate) of the file
165        """
166        cmd = [AudioIO.paramProgram, # ffprobe
167                    '-hide_banner',
168                    '-loglevel', str(logLevel),
169                    '-show_entries', 'stream=channels,sample_rate',
170                    str(filename)
171                    ]
172
173        if debug == True:
174            print(' '.join(cmd))
175
176        # call ffprobe and get params in one single line
177        lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg
178        output = lpipe.stdout.readlines()
179        lpipe.terminate()
180        # transform Bytes output to one single string
181        output = ''.join( [element.decode('utf-8') for element in output])
182
183        pattern_sample_rate = r'sample_rate=(\d+)'
184        pattern_channels = r'channels=(\d+)'
185
186        # Search for values in the ffprobe output
187        match_sample_rate = re.search(pattern_sample_rate, output, flags=re.MULTILINE)
188        match_channels = re.search(pattern_channels, output, flags=re.MULTILINE)
189
190        # Extraction des valeurs
191        if match_sample_rate:
192            sample_rate = int(match_sample_rate.group(1))
193        else:
194            raise AudioIO.AudioIOException("Unable to get audio sample_rate of '" + str(filename) + "'")
195
196        if match_channels:
197            channels = int(match_channels.group(1))
198        else:
199            raise AudioIO.AudioIOException("Unable to get audio channels of '" + str(filename) + "'")
200
201        return (channels,sample_rate)
202
203        # Attributes
204        mode: PipeMode
205        """ Pipemode of the current object (default PipeMode.UNK_MODE)"""
206
207        loglevel: int
208        """ loglevel of the underlying ffmpeg backend for this object (default 16)"""
209
210        debug: bool
211        """ debug flag for this object (print debut info, default False)"""
212
213        channels: int
214        """ Number of channels of images (default -1) """
215
216        sample_rate: int
217        """ sample_rate of images (default -1) """
218
219        plannar: bool
220        """ Read/write data as plannar, i.e. not interleaved (default True) """
221
222        pipe: sp.Popen
223        """ pipe object to ffmpeg/ffprobe (default None)"""
224
225        frame_size: int
226        """ Weight in bytes of one image (default -1)"""
227
228        filename: str
229        """ Filename of the file (default None)"""
230
231        frame_counter: FrameCounter
232        """ `Framecounter` object to count ellapsed time (default None)"""

Static method to get params (channels,sample_rate) of a (video containing) audio file in seconds.

Parameters

filename : str or path. Raw audio waveform as a 1D array.

debug : bool (default (False). Show debug info.

log_level: int (default 16). Log level to pass to the underlying ffmpeg/ffprobe command.

Returns

tuple Tuple containing (channels,sample_rate) of the file

@staticmethod
def get_audio_time_in_sec(filename, *, debug=False, logLevel=16):
100    @staticmethod
101    def get_time_in_sec(filename, *, debug=False, logLevel=16):
102        """
103        Static method to get length of an audio file (or video file containing audio) in seconds including milliseconds as decimal part (3 decimals).
104
105        Parameters
106        ----------
107        filename : str or path. 
108            Raw audio waveform as a 1D array.
109
110        debug : bool (default False).
111            Show debug info.
112
113        logLevel: int (default 16).
114            Log level to pass to the underlying ffmpeg/ffprobe command.
115        
116        Returns
117        ----------
118        float
119            Length in seconds of video file (including milliseconds as decimal part with 3 decimals)
120        """
121        
122        cmd = [AudioIO.paramProgram, # ffprobe
123                    '-hide_banner',
124                    '-loglevel', str(logLevel),
125                    '-show_entries', 'format=duration',
126                    '-of', 'default=noprint_wrappers=1:nokey=1',
127                    str(filename)
128                    ]
129
130        if debug == True:
131            print(' '.join(cmd))
132
133        # call ffprobe and get params in one single line
134        lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg
135        output = lpipe.stdout.readlines()
136        lpipe.terminate()
137        # transform Bytes output to one single string
138        output = ''.join( [element.decode('utf-8') for element in output])
139
140        try:
141            return float(output)
142        except (ValueError, TypeError):
143            return None

Static method to get length of an audio file (or video file containing audio) in seconds including milliseconds as decimal part (3 decimals).

Parameters

filename : str or path. Raw audio waveform as a 1D array.

debug : bool (default False). Show debug info.

logLevel: int (default 16). Log level to pass to the underlying ffmpeg/ffprobe command.

Returns

float Length in seconds of video file (including milliseconds as decimal part with 3 decimals)

@staticmethod
def get_audio_params(filename, *, debug=False, logLevel=16):
145    @staticmethod
146    def get_params(filename, *, debug=False, logLevel=16):
147        """
148        Static method to get params (channels,sample_rate) of a (video containing) audio file in seconds.
149
150        Parameters
151        ----------
152        filename : str or path.
153            Raw audio waveform as a 1D array.
154
155        debug : bool (default (False).
156            Show debug info.
157
158        log_level: int (default 16).
159            Log level to pass to the underlying ffmpeg/ffprobe command.
160
161        Returns
162        ----------
163        tuple
164            Tuple containing (channels,sample_rate) of the file
165        """
166        cmd = [AudioIO.paramProgram, # ffprobe
167                    '-hide_banner',
168                    '-loglevel', str(logLevel),
169                    '-show_entries', 'stream=channels,sample_rate',
170                    str(filename)
171                    ]
172
173        if debug == True:
174            print(' '.join(cmd))
175
176        # call ffprobe and get params in one single line
177        lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg
178        output = lpipe.stdout.readlines()
179        lpipe.terminate()
180        # transform Bytes output to one single string
181        output = ''.join( [element.decode('utf-8') for element in output])
182
183        pattern_sample_rate = r'sample_rate=(\d+)'
184        pattern_channels = r'channels=(\d+)'
185
186        # Search for values in the ffprobe output
187        match_sample_rate = re.search(pattern_sample_rate, output, flags=re.MULTILINE)
188        match_channels = re.search(pattern_channels, output, flags=re.MULTILINE)
189
190        # Extraction des valeurs
191        if match_sample_rate:
192            sample_rate = int(match_sample_rate.group(1))
193        else:
194            raise AudioIO.AudioIOException("Unable to get audio sample_rate of '" + str(filename) + "'")
195
196        if match_channels:
197            channels = int(match_channels.group(1))
198        else:
199            raise AudioIO.AudioIOException("Unable to get audio channels of '" + str(filename) + "'")
200
201        return (channels,sample_rate)
202
203        # Attributes
204        mode: PipeMode
205        """ Pipemode of the current object (default PipeMode.UNK_MODE)"""
206
207        loglevel: int
208        """ loglevel of the underlying ffmpeg backend for this object (default 16)"""
209
210        debug: bool
211        """ debug flag for this object (print debut info, default False)"""
212
213        channels: int
214        """ Number of channels of images (default -1) """
215
216        sample_rate: int
217        """ sample_rate of images (default -1) """
218
219        plannar: bool
220        """ Read/write data as plannar, i.e. not interleaved (default True) """
221
222        pipe: sp.Popen
223        """ pipe object to ffmpeg/ffprobe (default None)"""
224
225        frame_size: int
226        """ Weight in bytes of one image (default -1)"""
227
228        filename: str
229        """ Filename of the file (default None)"""
230
231        frame_counter: FrameCounter
232        """ `Framecounter` object to count ellapsed time (default None)"""

Static method to get params (channels,sample_rate) of a (video containing) audio file in seconds.

Parameters

filename : str or path. Raw audio waveform as a 1D array.

debug : bool (default (False). Show debug info.

log_level: int (default 16). Log level to pass to the underlying ffmpeg/ffprobe command.

Returns

tuple Tuple containing (channels,sample_rate) of the file

def isOpened(self) -> bool:
327    def is_opened(self) -> bool:
328        """
329        Method to get status of the underlying pipe to ffmpeg.
330
331        Returns
332        ----------
333        bool
334            True if pipe is opened (reading or writing mode), False if not.
335        """
336        # is the pip opened?
337        if self.pipe is not None and self.pipe.poll() is None:
338            return True
339
340        return False

Method to get status of the underlying pipe to ffmpeg.

Returns

bool True if pipe is opened (reading or writing mode), False if not.

def readFrame(self, with_timestamps=False):
568    def read_frame(self, with_timestamps = False):
569        """
570        Read next frame from the audio file
571
572        Parameters
573        ----------
574        with_timestamps: bool optional (default False)
575            If set to True, the method returns a ``FrameContainer`` with the audio and an array containing the associated timestamp(s)
576
577        Returns
578        ----------
579        nparray or FrameContainer
580            A frame of shape (self.channels,self.frame_size) as defined in the reader/open call if self.plannar is True. A frame
581            of shape (self.channels*self.frame_size) with interleaved data if self.plannar is False.
582            if with_timestamps is True, the return object is a FrameContainer with the audio data in ``FrameContainer.data`` and
583            the associated timestamp in ``FrameContainer.timestamps`` as an array (one element).
584        """
585
586        if self.pipe is None:
587            raise self.AudioIOException("No pipe opened to {}. Call open(...) before reading a frame.".format(self.audioProgram))
588        # - pipe is in write mode
589        if self.mode != PipeMode.READ_MODE:
590            raise self.AudioIOException("Pipe to {} for '{}' not opened in read mode.".format(self.audioProgram, self.filename))
591
592        if with_timestamps:
593            # get elapsed time in video, it is time of next frame(s)
594            current_elapsed_time = self.get_elapsed_time()
595
596        # read rgb image from pipe
597        toread = self.frame_size*self.float_size
598        buffer = self.pipe.stdout.read(toread)
599
600        if buffer == b"":
601            # not considered as an error, no more frame, no exception
602            return None
603
604        # get numpy UINT8 array from buffer
605        audio = np.frombuffer(buffer, dtype = np.float32).reshape(len(buffer)//self.float_size, self.channels)
606
607        # make it plannar (or not)
608        if self.plannar:
609            #transpose it
610            audio = audio.T
611
612        # increase frame_counter
613        self.frame_counter.frame_count += (self.frame_size * self.channels)
614
615        # say to gc that this buffer is no longer needed
616        del buffer
617
618        if with_timestamps:
619            return FrameContainer(1, audio, self.frame_size/self.sample_rate, current_elapsed_time)
620        
621        return audio

Read next frame from the audio file

Parameters

with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the audio and an array containing the associated timestamp(s)

Returns

nparray or FrameContainer A frame of shape (self.channels,self.frame_size) as defined in the reader/open call if self.plannar is True. A frame of shape (self.channels*self.frame_size) with interleaved data if self.plannar is False. if with_timestamps is True, the return object is a FrameContainer with the audio data in FrameContainer.data and the associated timestamp in FrameContainer.timestamps as an array (one element).

def readBatch(self, numberOfFrames, with_timestamps=False):
623    def read_batch(self, numberOfFrames, with_timestamps = False):
624        """
625        Read next batch of audio from the file
626
627        Parameters
628        ----------
629        number_of_frames: int
630            Number of desired images within the batch. The last batch from the file may have less images.
631            
632        with_timestamps: bool optional (default False)
633            If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames
634
635        Returns
636        ----------
637        nparray or FrameContainer
638            A batch of shape (n, self.channels,self.frame_size) as defined in the reader/open call if self.plannar is True. A batch
639            of shape (n, self.channels*self.frame_size) with interleaved data if self.plannar is False.
640            if with_timestamps is True, the return object is a FrameContainer with the audio batch in ``FrameContainer.data`` and
641            the associated timestamp in ``FrameContainer.timestamps`` as an array (one element for each audio frame).
642        """
643
644        if self.pipe is None:
645            raise self.AudioIOException("No pipe opened to {}. Call open(...) before reading frames.".format(self.audioProgram))
646        # - pipe is in write mode
647        if self.mode != PipeMode.READ_MODE:
648            raise self.AudioIOException("Pipe to {} for '{}' not opened in read mode.".format(self.audioProgram, self.filename))
649
650        if with_timestamps:
651            # get elapsed time in video, it is time of next frame(s)
652            current_elapsed_time = self.get_elapsed_time()
653
654        # try to read complete batch
655        toread = self.frame_size*self.float_size*self.channels*numberOfFrames
656        buffer = self.pipe.stdout.read(toread)
657
658        # check if we are at the end of the buffer
659        if buffer == b"":
660            # not considered as an error, no more frame, no exception
661            return None
662
663        # compute actual number of Frames
664        # do we have a full batch ? Computer standard division
665        actualNbFrames = len(buffer)/(self.frame_size*self.float_size*self.channels)
666
667        if actualNbFrames.is_integer():
668            actualNbFrames = int(actualNbFrames)
669            # get and reshape batch from buffer
670            batch = np.frombuffer(buffer, dtype = np.float32).reshape((actualNbFrames, self.frame_size, self.channels,))
671        else:
672            # We do not have a full batch, we are at end of the stream or ffmpeg has been stopped/killed
673            # Compute frame size in samples knowing len of buffer, self.float_size, self.channels and numberOfFrames
674            l_frame_size = len(buffer)//(numberOfFrames*self.float_size*self.channels)
675            if l_frame_size <= 0:
676                # not considered as an error, no more frame, no exception
677                return None
678            # get and reshape batch from buffer
679            batch = np.frombuffer(buffer, dtype = np.float32).reshape((numberOfFrames, l_frame_size, self.channels,))
680
681        if self.plannar:
682            batch = batch.transpose(0, 2, 1)
683
684        # increase frame_counter
685        self.frame_counter.frame_count += (actualNbFrames * self.frame_size * self.channels)
686        
687        # say to gc that this buffer is no longer needed
688        del buffer
689
690        if with_timestamps:
691            return FrameContainer( actualNbFrames, batch, self.frame_size/self.sample_rate, current_elapsed_time)
692        
693        return batch

Read next batch of audio from the file

Parameters

number_of_frames: int Number of desired images within the batch. The last batch from the file may have less images.

with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames

Returns

nparray or FrameContainer A batch of shape (n, self.channels,self.frame_size) as defined in the reader/open call if self.plannar is True. A batch of shape (n, self.channels*self.frame_size) with interleaved data if self.plannar is False. if with_timestamps is True, the return object is a FrameContainer with the audio batch in FrameContainer.data and the associated timestamp in FrameContainer.timestamps as an array (one element for each audio frame).

def writeFrame(self, audio) -> bool:
695    def write_frame(self, audio) -> bool:
696        """
697        Write an audio frame to the file
698
699        Parameters
700        ----------
701        audio: nparray
702            The audio frame to write to the video file of shape (self.channels,nb_samples_per_channel) if plannar is True else (self.channels*nb_samples_per_channel).
703
704        Returns
705        ----------
706        bool
707            Writing was successful or not.
708        """
709        # Check params
710        # - pipe exists
711        if self.pipe is None:
712            raise self.AudioIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.audioProgram))
713        # - pipe is in write mode
714        if self.mode != PipeMode.WRITE_MODE:
715            raise self.AudioIOException("Pipe to {} for '{}' not opened in write mode.".format(self.audioProgram, self.filename))
716        # - shape of image is fine, thus we have pixels for a full compatible frame
717        if audio.shape[0] != self.channels:
718            raise self.AudioIOException("Wong audio shape: {} expected ({}, nb_samples_to_write).".format(audio.shape,self.channels))
719        # - type of data is Float32
720        if audio.dtype != np.float32:
721            raise self.AudioIOException("Wong audio type: {} expected np.float32.".format(audio.dtype))
722
723        # array must have a shape (channels, samples), reshape it it to (samples, channels) if plannar
724        if not self.plannar:
725            audio = audio.reshape(-1)
726
727        # print( audio.shape )
728
729        # garantee to have a C continuous array
730        if not audio.flags['C_CONTIGUOUS']:
731            a = np.ascontiguousarray(a) 
732
733        # write frame
734        buffer = audio.tobytes()
735        if self.pipe.stdin.write( buffer ) < len(buffer):
736            print( f"Error writing frame to {self.filename}" )
737            return False
738
739        # increase frame_counter
740        self.frame_counter.frame_count += (audio.shape[1] * self.channels)
741
742        # say to gc that this buffer is no longer needed 
743        del buffer
744
745        return True

Write an audio frame to the file

Parameters

audio: nparray The audio frame to write to the video file of shape (self.channels,nb_samples_per_channel) if plannar is True else (self.channels*nb_samples_per_channel).

Returns

bool Writing was successful or not.

def writeBatch(self, batch):
747    def write_batch(self, batch):
748        """
749        Write a batch of audio frame to the file
750
751        Parameters
752        ----------
753        batch: nparray
754            The batch of audio frames to write to the video file of shape (n,self.channels,nb_samples_per_channel) if plannar is True else (n,self.channels*nb_samples_per_channel) of interleaved audio data.
755
756        Returns
757        ----------
758        bool
759            Writing was successful or not.
760        """
761        # Check params
762        # - pipe exists
763        if self.pipe is None:
764            raise self.AudioIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.audioProgram))
765        # - pipe is in write mode
766        if self.mode != PipeMode.WRITE_MODE:
767            raise self.AudioIOException("Pipe to {} for '{}' not opened in write mode.".format(self.audioProgram, self.filename))
768        # batch is 3D (n, channels, nb samples)
769        if batch.ndim !=3:
770            raise self.AudioIOException("Wrong batch shape: {} expected 3 dimensions (n, n_channels, n_samples_per_channel).".format(batch.shape))
771        # - shape of images in batch is fine
772        if batch.shape[2] != self.channels:
773            raise self.AudioIOException("Wrong audio channels in batch: {} expected {} {}.".format(batch.shape[2], self.channels, batch.shape))
774
775        # array must have a shape (n * n_channels * n_samples_per_channel) before writing them to pipe
776        # reshape it it to (n * n_channels * n_samples_per_channel) if plannar is False
777        if not self.plannar:
778            # goes from (n, n_channels, n_samples_per_channel) to (n * n_channels * n_samples_per_channel)
779            batch = batch.transpose(0, 2, 1) # first go to (n, n_samples_per_channel, n_channels)
780            batch = batch.reshape(-1) # then to 1D array (n * n_channels * n_samples_per_channel)
781
782        # garantee to have a C continuous array
783        if not batch.flags['C_CONTIGUOUS']:
784            batch = np.ascontiguousarray(batch)
785
786        # write frame
787        buffer = batch.tobytes()
788        if self.pipe.stdin.write( buffer ) < len(buffer):
789            # say to gc that this buffer is no longer needed
790            del buffer
791            raise self.AudioIOException("Error writing batch to '{}'.".format(self.filename))
792
793        # increase frame_counter
794        self.frame_counter.frame_count += int(batch.shape[0]/self.channels) # int conversion is mandatory to avoid confusion with time as float
795              
796        # say to gc that this buffer is no longer needed
797        del buffer
798
799        return True

Write a batch of audio frame to the file

Parameters

batch: nparray The batch of audio frames to write to the video file of shape (n,self.channels,nb_samples_per_channel) if plannar is True else (n,self.channels*nb_samples_per_channel) of interleaved audio data.

Returns

bool Writing was successful or not.

audioProgram = '/usr/local/lib/python3.12/site-packages/static_ffmpeg/bin/linux/ffmpeg'
paramProgram = '/usr/local/lib/python3.12/site-packages/static_ffmpeg/bin/linux/ffprobe'
class AudioIO.AudioIOException(builtins.Exception):
38    class AudioIOException(Exception):
39        """
40        Dedicated exception class for AudioIO class.
41        """
42        def __init__(self, message="Error while reading/writing video occurs"):
43            self.message = message
44            super().__init__(self.message)

Dedicated exception class for AudioIO class.

AudioIO.AudioIOException(message='Error while reading/writing video occurs')
42        def __init__(self, message="Error while reading/writing video occurs"):
43            self.message = message
44            super().__init__(self.message)
message
class AudioIO.AudioFormat(enum.Enum):
46    class AudioFormat(Enum):
47        """
48        Enum class for supported input video type: 32-bit float is the only supported type for the moment.
49        """
50        PCM32LE = 'pcm_f32le' # default format (unique mode for the moment)

Enum class for supported input video type: 32-bit float is the only supported type for the moment.

PCM32LE = <AudioFormat.PCM32LE: 'pcm_f32le'>
class FrameCounter:
 16class FrameCounter:
 17    """
 18    Create a ``FrameCounter`` to follow elapsed time in audio/video file in read or write mode. Static utility functions allow to format elapsed time.
 19    """
 20
 21    class FrameCounterException(Exception):
 22        """
 23        Dedicated exception class for FrameCounter class.
 24        """
 25        def __init__(self, message="Error while setting FrameCounter parameters."):
 26            self.message = message
 27            super().__init__(self.message)
 28
 29    _fps: float # (private)
 30    """ Fps of current stream """
 31
 32    _frame_count: int # (private)
 33    """ Frame count in the current stream """
 34
 35    def __init__(self, fps: Union[int, float]):
 36        """
 37        Create a VideoIO object giving ffmpeg/ffrobe loglevel and defining debug mode
 38
 39        Parameters
 40        ----------
 41        fps: int or float.
 42            Frames per second of the associated stream.
 43        """
 44        # check init fps value
 45        self._fps = float(fps)
 46        if self._fps <= 0.0:
 47            raise FrameCounterException("fps must be > 0.0.")
 48
 49        # 2 modes
 50        self._frame_count = 0       # at 00:00:00.000
 51
 52    # support +=
 53    def __iadd__(self, other: Union[int, float]):
 54        """
 55        Support += operator for FrameCounter. 
 56
 57        Parameters
 58        ----------
 59        other: int or float.
 60            If other is a float, add the number of frame to add 'other' seconds (thus other * self._fps samples).
 61            If other is an int, add the value as a number of samples in the stream.
 62        """        
 63        if isinstance(other,float):
 64            # float means adding time
 65            self._frame_count += int(other * self._fps) # number of second * Nb of elements per seconds
 66        else:
 67            # for int, add number of element
 68            self._frame_count += other
 69        return self
 70    
 71    @property
 72    def frame_count(self):
 73        """
 74        Property to get underlying self._frame_count. Idea is to control setter to valid setting values.
 75        """   
 76        return self._frame_count
 77
 78    @frame_count.setter
 79    def frame_count(self, value: int):
 80        """
 81        Setter for underlying self._frame_count controlling setting value.
 82        """   
 83        if value < 0:
 84            raise FrameCounterException("frame_count must be >= 0")
 85        self._frame_count = value
 86
 87    @property
 88    def fps(self):
 89        """
 90        Property to get underlying self._fps.
 91        """
 92        return self._fps
 93
 94    @staticmethod
 95    def format_time(nb_frames: int, fps: float, show_ms : bool = True, show_days : bool = False) -> str:
 96        """
 97        Static function to format time given by a number of frames and an fps. show_ms value defines if we show milliseconds,
 98        show_days to show days instead of cummulative hour count.
 99
100        Parameters
101        ----------
102        nb_frames: int.
103            Number of samples already present in the stream.
104            
105        fps: float.
106            Fps of the associated stream.
107            
108        show_ms: bool.
109            Flag to say if we want to show milliseconds in the output str.
110            
111        show_days: bool.
112            Flag to say if we want to show says instead of cumulative hours in the output str.
113
114        Returns
115        -------
116            str representing corresponding time. Either 26:15:00 (show_ms=False, show_days=False), 26:15:00.500 (show_ms=True, show_days=False)
117            or 1 day(s) 02:15:00.500 (show_ms=True, show_days=True)
118        """  
119        # exact time in seconds (float)
120        exact_seconds = nb_frames / fps
121
122        # integer part for days/hours/minutes/seconds
123        total_seconds = int(exact_seconds)
124
125        # milliseconds = decimal part * 1000
126        millis = int(round((exact_seconds - total_seconds) * 1000))
127
128        # handle the case where rounding results in 1000 ms
129        if millis == 1000:
130            millis = 0
131            total_seconds += 1
132
133        if show_days:
134            # compute number of days, hours, minutes, seconds
135            days, mod = divmod(total_seconds, 24 * 3600)
136            hours, mod = divmod(mod, 3600)
137            minutes, seconds = divmod(mod, 60)
138
139            if days > 0:
140                days = f"{days} days(s) "
141            else:
142                days = ""
143        else:
144            days = "" # no day as show_days = False
145            hours, mod = divmod(total_seconds, 3600)
146            minutes, seconds = divmod(mod, 60)
147
148        if show_ms == True:
149            millis = f".{millis:03d}"
150        else:
151            millis = ""
152
153        return f"{days}{hours:02d}:{minutes:02d}:{seconds:02d}{millis}"
154
155    def get_elapsed_time_as_str(self) -> str:
156        """
157        Get elapsed time as string representing a float value rounded to 3 decimals.
158
159        Returns
160        -------
161            str representing corresponding time in float format rounded to 3 decimals.
162        """
163        return f"{float(self._frame_count)/self._fps:.3f}"
164
165    def get_formated_elapsed_time_as_str(self, show_ms : bool = True, show_days : bool = False) -> str:
166        """
167        Get elapsed time as string representing time with different mode (see ``FrameCounter.format_time`` for parameter explanation).
168        Returns
169        -------
170            str representing corresponding time in float format rounded to 3 decimals.
171        """
172        # frame count to time correction is done in format_time
173        return FrameCounter.format_time(self._frame_count, self._fps, show_ms, show_days)
174
175    def get_elapsed_time(self) -> float:
176        """
177        Get elapsed time as float value rounded to 3 decimals.
178
179        Returns
180        -------
181            str representing corresponding time in float format rounded to 3 decimals.
182        """
183        return round(float(self._frame_count)/self._fps,3)

Create a FrameCounter to follow elapsed time in audio/video file in read or write mode. Static utility functions allow to format elapsed time.

FrameCounter(fps: Union[int, float])
35    def __init__(self, fps: Union[int, float]):
36        """
37        Create a VideoIO object giving ffmpeg/ffrobe loglevel and defining debug mode
38
39        Parameters
40        ----------
41        fps: int or float.
42            Frames per second of the associated stream.
43        """
44        # check init fps value
45        self._fps = float(fps)
46        if self._fps <= 0.0:
47            raise FrameCounterException("fps must be > 0.0.")
48
49        # 2 modes
50        self._frame_count = 0       # at 00:00:00.000

Create a VideoIO object giving ffmpeg/ffrobe loglevel and defining debug mode

Parameters

fps: int or float. Frames per second of the associated stream.

frame_count
71    @property
72    def frame_count(self):
73        """
74        Property to get underlying self._frame_count. Idea is to control setter to valid setting values.
75        """   
76        return self._frame_count

Property to get underlying self._frame_count. Idea is to control setter to valid setting values.

fps
87    @property
88    def fps(self):
89        """
90        Property to get underlying self._fps.
91        """
92        return self._fps

Property to get underlying self._fps.

@staticmethod
def format_time( nb_frames: int, fps: float, show_ms: bool = True, show_days: bool = False) -> str:
 94    @staticmethod
 95    def format_time(nb_frames: int, fps: float, show_ms : bool = True, show_days : bool = False) -> str:
 96        """
 97        Static function to format time given by a number of frames and an fps. show_ms value defines if we show milliseconds,
 98        show_days to show days instead of cummulative hour count.
 99
100        Parameters
101        ----------
102        nb_frames: int.
103            Number of samples already present in the stream.
104            
105        fps: float.
106            Fps of the associated stream.
107            
108        show_ms: bool.
109            Flag to say if we want to show milliseconds in the output str.
110            
111        show_days: bool.
112            Flag to say if we want to show says instead of cumulative hours in the output str.
113
114        Returns
115        -------
116            str representing corresponding time. Either 26:15:00 (show_ms=False, show_days=False), 26:15:00.500 (show_ms=True, show_days=False)
117            or 1 day(s) 02:15:00.500 (show_ms=True, show_days=True)
118        """  
119        # exact time in seconds (float)
120        exact_seconds = nb_frames / fps
121
122        # integer part for days/hours/minutes/seconds
123        total_seconds = int(exact_seconds)
124
125        # milliseconds = decimal part * 1000
126        millis = int(round((exact_seconds - total_seconds) * 1000))
127
128        # handle the case where rounding results in 1000 ms
129        if millis == 1000:
130            millis = 0
131            total_seconds += 1
132
133        if show_days:
134            # compute number of days, hours, minutes, seconds
135            days, mod = divmod(total_seconds, 24 * 3600)
136            hours, mod = divmod(mod, 3600)
137            minutes, seconds = divmod(mod, 60)
138
139            if days > 0:
140                days = f"{days} days(s) "
141            else:
142                days = ""
143        else:
144            days = "" # no day as show_days = False
145            hours, mod = divmod(total_seconds, 3600)
146            minutes, seconds = divmod(mod, 60)
147
148        if show_ms == True:
149            millis = f".{millis:03d}"
150        else:
151            millis = ""
152
153        return f"{days}{hours:02d}:{minutes:02d}:{seconds:02d}{millis}"

Static function to format time given by a number of frames and an fps. show_ms value defines if we show milliseconds, show_days to show days instead of cummulative hour count.

Parameters

nb_frames: int. Number of samples already present in the stream.

fps: float. Fps of the associated stream.

show_ms: bool. Flag to say if we want to show milliseconds in the output str.

show_days: bool. Flag to say if we want to show says instead of cumulative hours in the output str.

Returns

str representing corresponding time. Either 26:15:00 (show_ms=False, show_days=False), 26:15:00.500 (show_ms=True, show_days=False)
or 1 day(s) 02:15:00.500 (show_ms=True, show_days=True)
def get_elapsed_time_as_str(self) -> str:
155    def get_elapsed_time_as_str(self) -> str:
156        """
157        Get elapsed time as string representing a float value rounded to 3 decimals.
158
159        Returns
160        -------
161            str representing corresponding time in float format rounded to 3 decimals.
162        """
163        return f"{float(self._frame_count)/self._fps:.3f}"

Get elapsed time as string representing a float value rounded to 3 decimals.

Returns

str representing corresponding time in float format rounded to 3 decimals.
def get_formated_elapsed_time_as_str(self, show_ms: bool = True, show_days: bool = False) -> str:
165    def get_formated_elapsed_time_as_str(self, show_ms : bool = True, show_days : bool = False) -> str:
166        """
167        Get elapsed time as string representing time with different mode (see ``FrameCounter.format_time`` for parameter explanation).
168        Returns
169        -------
170            str representing corresponding time in float format rounded to 3 decimals.
171        """
172        # frame count to time correction is done in format_time
173        return FrameCounter.format_time(self._frame_count, self._fps, show_ms, show_days)

Get elapsed time as string representing time with different mode (see FrameCounter.format_time for parameter explanation).

Returns

str representing corresponding time in float format rounded to 3 decimals.
def get_elapsed_time(self) -> float:
175    def get_elapsed_time(self) -> float:
176        """
177        Get elapsed time as float value rounded to 3 decimals.
178
179        Returns
180        -------
181            str representing corresponding time in float format rounded to 3 decimals.
182        """
183        return round(float(self._frame_count)/self._fps,3)

Get elapsed time as float value rounded to 3 decimals.

Returns

str representing corresponding time in float format rounded to 3 decimals.
class FrameCounter.FrameCounterException(builtins.Exception):
21    class FrameCounterException(Exception):
22        """
23        Dedicated exception class for FrameCounter class.
24        """
25        def __init__(self, message="Error while setting FrameCounter parameters."):
26            self.message = message
27            super().__init__(self.message)

Dedicated exception class for FrameCounter class.

FrameCounter.FrameCounterException(message='Error while setting FrameCounter parameters.')
25        def __init__(self, message="Error while setting FrameCounter parameters."):
26            self.message = message
27            super().__init__(self.message)
message
class FrameContainer:
16class FrameContainer:
17    """
18    Create a Container with audio or image data and associated timestamp(s).
19    """
20
21    class FrameContainerException(Exception):
22        """
23        Dedicated exception class for FrameContainer class.
24        """
25        def __init__(self, message="Error while setting FrameCounter parameters."):
26            self.message = message
27            super().__init__(self.message)
28
29    nb_frames: int
30    """ number of frames in the frame container. 1 for audio frame or images, n for a batch. """
31
32    data: np.array
33    """ Audio frame or image, or batch of images or audio frames. """
34
35    timestamps: np.array
36    """ an np.array of timestamp(s) associated to the data frame(s). """
37
38    def __init__(self, nb_frames : int, frames : np.array, fps : float, start_time : float = 0.0 ):
39        """
40        Create a FrameContainer object with data and associated timestamps
41
42        Parameters
43        ----------
44        nb_frames: int.
45            Number of frame(s) in the frame container. Either 1, either number of element in the batch.
46
47        frames:  np.array
48            Data for image, audio frame or batch of them.
49
50        fps : float.
51            fps of the read stream.
52            
53        start_time: float (default = 0.0).
54            Start time of the current FrameContainer, i.e. time of the (first) frame.
55        """
56        
57        # check params
58        if nb_frames <= 0:
59            raise self.FrameContainerException("nb_frames must be > 0.")
60        if nb_frames > 1 and frames.shape[0] != nb_frames:
61            raise self.FrameContainerException("nb_frames and batch size (frames.shape[0]) differs.")
62        if fps <= 0.0:
63            raise self.FrameContainerException("fps must be > 0.0.")
64        if start_time < 0.0:
65            raise self.FrameContainerException("start_time must be >= 0.0.")
66
67        self.nb_frames = nb_frames
68        self.data = frames
69        # first frame is at time start_time
70        self.timestamps = np.linspace(start_time, start_time+(self.nb_frames-1)*fps, self.nb_frames).tolist()
71
72    def totorch(self):
73        """
74        Convert the numy array into a pytorch tensor.
75
76        Returns
77        ----------
78            pytorch tensor.
79        """
80        # import torch late as it is not in the standard requirements for simple-ffmpeg-batch-io
81        import torch
82        return torch.from_numpy(self.data)

Create a Container with audio or image data and associated timestamp(s).

FrameContainer( nb_frames: int, frames: <built-in function array>, fps: float, start_time: float = 0.0)
38    def __init__(self, nb_frames : int, frames : np.array, fps : float, start_time : float = 0.0 ):
39        """
40        Create a FrameContainer object with data and associated timestamps
41
42        Parameters
43        ----------
44        nb_frames: int.
45            Number of frame(s) in the frame container. Either 1, either number of element in the batch.
46
47        frames:  np.array
48            Data for image, audio frame or batch of them.
49
50        fps : float.
51            fps of the read stream.
52            
53        start_time: float (default = 0.0).
54            Start time of the current FrameContainer, i.e. time of the (first) frame.
55        """
56        
57        # check params
58        if nb_frames <= 0:
59            raise self.FrameContainerException("nb_frames must be > 0.")
60        if nb_frames > 1 and frames.shape[0] != nb_frames:
61            raise self.FrameContainerException("nb_frames and batch size (frames.shape[0]) differs.")
62        if fps <= 0.0:
63            raise self.FrameContainerException("fps must be > 0.0.")
64        if start_time < 0.0:
65            raise self.FrameContainerException("start_time must be >= 0.0.")
66
67        self.nb_frames = nb_frames
68        self.data = frames
69        # first frame is at time start_time
70        self.timestamps = np.linspace(start_time, start_time+(self.nb_frames-1)*fps, self.nb_frames).tolist()

Create a FrameContainer object with data and associated timestamps

Parameters

nb_frames: int. Number of frame(s) in the frame container. Either 1, either number of element in the batch.

frames: np.array Data for image, audio frame or batch of them.

fps : float. fps of the read stream.

start_time: float (default = 0.0). Start time of the current FrameContainer, i.e. time of the (first) frame.

nb_frames: int

number of frames in the frame container. 1 for audio frame or images, n for a batch.

data: <built-in function array>

Audio frame or image, or batch of images or audio frames.

timestamps: <built-in function array>

an np.array of timestamp(s) associated to the data frame(s).

def totorch(self):
72    def totorch(self):
73        """
74        Convert the numy array into a pytorch tensor.
75
76        Returns
77        ----------
78            pytorch tensor.
79        """
80        # import torch late as it is not in the standard requirements for simple-ffmpeg-batch-io
81        import torch
82        return torch.from_numpy(self.data)

Convert the numy array into a pytorch tensor.

Returns

pytorch tensor.
class FrameContainer.FrameContainerException(builtins.Exception):
21    class FrameContainerException(Exception):
22        """
23        Dedicated exception class for FrameContainer class.
24        """
25        def __init__(self, message="Error while setting FrameCounter parameters."):
26            self.message = message
27            super().__init__(self.message)

Dedicated exception class for FrameContainer class.

FrameContainer.FrameContainerException(message='Error while setting FrameCounter parameters.')
25        def __init__(self, message="Error while setting FrameCounter parameters."):
26            self.message = message
27            super().__init__(self.message)
message
class PipeMode(enum.Enum):
15class PipeMode(Enum):
16    """
17    Enum class for pipe opening type.
18    """
19    UNK_MODE = 0
20    """ No pipe mode defined """
21
22    READ_MODE = 1  
23    """ Read mode (get data from pipe) """
24
25    WRITE_MODE = 2 
26    """ Write mode (write data to pipe) """

Enum class for pipe opening type.

UNK_MODE = <PipeMode.UNK_MODE: 0>

No pipe mode defined

READ_MODE = <PipeMode.READ_MODE: 1>

Read mode (get data from pipe)

WRITE_MODE = <PipeMode.WRITE_MODE: 2>

Write mode (write data to pipe)