simple_ffmpeg_batch_io.VideoIO

Read/write video images or batches of images from video file using FFmpeg backend.

This module defines the main VideoIO class used to open videos, read images or batches of frames, and write processed outputs.

Authors

Dominique Vaufreydaz (From original C++ code: https://github.com/Vaufreyd/ReadWriteVideosWithOpenCV)

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

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

@classmethod
def reader(cls, filename, **kwargs):
51    @classmethod
52    def reader(cls, filename, **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()
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, **kwargs):
63    @classmethod
64    def writer(cls, filename, width, height, fps, **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()
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.

@staticmethod
def get_time_in_sec(filename, *, debug=False, logLevel=16):
 91    @staticmethod
 92    def get_time_in_sec(filename, *, debug=False, logLevel=16):
 93        """
 94        Static method to get length of a video file in seconds including milliseconds as decimal part.
 95
 96        Parameters
 97        ----------
 98        filename : str or path
 99            Video file name.
100
101        debug : bool (default False)
102            Show debug info.
103
104        log_level: int (default 16)
105            Log level to pass to the underlying ffmpeg/ffprobe command.
106        
107        Returns
108        ----------
109        float
110            Length in seconds of video file (including milliseconds as decimal part)
111        """
112        
113        cmd = [VideoIO.paramProgram, # ffprobe
114                    '-hide_banner',
115                    '-loglevel', str(logLevel),
116                    '-show_entries', 'format=duration',
117                    '-of', 'default=noprint_wrappers=1:nokey=1',
118                    filename
119                    ]
120
121        if debug == True:
122            print(' '.join(cmd))
123
124        # call ffprobe and get params in one single line
125        lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe
126        output = lpipe.stdout.readlines()
127        lpipe.terminate()
128        # transform Bytes output to one single string
129        output = ''.join( [element.decode('utf-8') for element in output])
130
131        try:
132            return float(output)
133        except (ValueError, TypeError):
134            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):
136    @staticmethod
137    def get_params(filename, *, debug=False, logLevel=16):
138        """
139        Static method to get params (width, height, fps) from a video file.
140
141        Parameters
142        ----------
143        filename: str or path
144            Video filename.
145
146        debug: bool (default (False)
147            Show debug info.
148
149        log_level: int (default 16)
150            Log level to pass to the underlying ffmpeg/ffprobe command.
151
152        Returns
153        ----------
154        tuple
155            Tuple containing (width, height, fps) of the video
156        """
157        cmd = [VideoIO.paramProgram, # ffprobe
158                    '-hide_banner',
159                    '-loglevel', str(logLevel),
160                    '-show_entries', 'stream=width,height,r_frame_rate',
161                    filename
162                    ]
163
164        if debug == True:
165            print(' '.join(cmd))
166
167        # call ffprobe and get params in one single line
168        lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe
169        output = lpipe.stdout.readlines()
170        lpipe.terminate()
171        # transform Bytes output to one single string
172        output = ''.join( [element.decode('utf-8') for element in output])
173
174        pattern_width = r'width=(\d+)'
175        pattern_height = r'height=(\d+)'
176        pattern_fps = r'r_frame_rate=(\d+)/(\d+)'
177
178        # Search for values in the ffprobe output
179        match_width = re.search(pattern_width, output, flags=re.MULTILINE)
180        match_height = re.search(pattern_height, output, flags=re.MULTILINE)
181        match_fps = re.search(pattern_fps, output, flags=re.MULTILINE)
182
183        # Extraction des valeurs
184        if match_width:
185            width = int(match_width.group(1))
186        else:
187            raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'")
188
189        if match_height:
190            height = int(match_height.group(1))
191        else:
192            raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'")
193
194        if match_fps:
195            numerator = float(match_fps.group(1))
196            denominator = float(match_fps.group(2))
197            fps = numerator / denominator
198        else:
199            raise VideoIO.VideoIOException("Unable to get frame rate (fps) of '" + filename + "'")
200
201        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

Pipemode of the current object (default PipeMode.UNK_MODE)

loglevel: int

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

debugModel: bool

debutMode 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)

Framecounter object to count ellapsed time (default None)

logLevel
debugMode
def init(self):
257    def init(self):
258        """
259        Init or reinit a VideoIO object.
260        """
261        self.width  = -1
262        self.height = -1
263        self.fps = -1.0
264        self.pipe = None
265        self.shape = (None, None, None)
266        self.imageSize = -1
267        self.filename = None
268        self.frame_counter = None

Init or reinit a VideoIO object.

def get_elapsed_time_as_str(self) -> str:
288    def get_elapsed_time_as_str(self) -> str:
289        """
290        Method to get elapsed time (float value) as str from `frame_counter` attribute.
291
292        Returns
293        ----------
294        str or None
295            Elapsed time (float value) as str, "15.500" for instance for 15 secondes and 500 milliseconds
296            None if no frame counter are available.
297        """
298        if self.frame_counter is None:
299            return None
300        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:
302    def get_formated_elapsed_time_as_str(self,show_ms=True) -> str:
303        """
304        Method to get elapsed time (hour format) as str from `frame_counter` attribute.
305
306        Returns
307        ----------
308        str or None
309            Elapsed time (float value) as str, "00:00:15.500" for instance for 15 secondes and 500 milliseconds
310            None if no frame counter are available.
311        """
312        if self.frame_counter is None:
313            return None
314        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:
316    def get_elapsed_time(self) -> float:
317        """
318        Method to get elapsed time as float value rounded to 3 decimals (millisecond) from `frame_counter` attribute.
319
320        Returns
321        ----------
322        float or None
323            Elapsed time (float value) as str, 15.500 for instance for 15 secondes and 500 milliseconds
324            None if no frame counter are available.
325        """
326        if self.frame_counter is None:
327            return None
328        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:
330    def is_opened(self) -> bool:
331        """
332        Method to get status of the underlying pipe to ffmpeg.
333
334        Returns
335        ----------
336        bool
337            True if pipe is opened (reading or writing mode), False if not.
338        """
339        # is the pipe opened?
340        if self.pipe is not None and self.pipe.poll() is None:
341            return True
342
343        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:
345    def close(self) -> None:
346        """
347        Method to close current pipe to ffmpeg (if any). Ffmpeg/ffprobe will be terminated. Object can be reused using open or create methods.
348        """
349        if self.pipe is not None:
350            if self.mode == PipeMode.WRITE_MODE:
351                # killing will make ffmpeg not finish properly the job, close the pipe
352                # to let it know that no more data are comming
353                self.pipe.stdin.close()
354            else: # self.mode == PipeMode.READ_MODE
355                # in read mode, no need to be nice, send SIGTERM on Linux,/Kill it on windows
356                self.pipe.kill()
357
358            # wait for subprocess to end
359            self.pipe.wait()
360
361        # reinit object for later use
362        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:
364    def create(self, filename, width, height, fps, *, writeOverExistingFile = False,
365                     inputEncoding = PixelFormat.GBR24, encodingParams = None ) -> bool:
366        """
367        Method to create a video using parametrized access through ffmpeg. Importante note: calling create
368        on a VideoIO will close any former open video.
369
370        Parameters
371        ----------
372        filename: str or path
373            filename of path to the file (mp4, avi, ...)
374
375        width: int
376            If defined as a positive value, width of output images will be set to this value.
377
378        height: int
379            If defined as a positive value, height of output images will be set to this value.
380
381        fps:
382            If defined as a positive value, fps of output video will be set to this value.
383
384        inputEncoding: PixelFormat optional (default PixelFormat.BGR24)
385            Define order of channels for channels. Possible values are PixelFormat.BGR24 or PixelFormat.RGB24.
386
387        encodingParams: str optional (default None)
388            Parameter to pass to ffmpeg to encode video like video filters.
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.width = int(width)
401        self.height = int(height)
402        self.fps = float(fps)
403
404        # Check params
405        if self.width <= 0 or self.height <= 0 or self.fps <= 0.0:
406            raise self.VideoIOException("Bad parameters: width={}, height={}, fps={:3f}".format(self.width,self.height,self.fps))
407
408        # Params are ok, set shape and image size
409        self.shape     = (self.height,self.width,3)
410        self.imageSize = self.height * self.width * 3
411
412        # Video params are set, open the video
413        cmd = [self.videoProgram] # ffmpeg
414
415        if writeOverExistingFile == True:
416            cmd.extend(['-y'])
417
418        cmd.extend(['-hide_banner',
419            '-nostats',
420            '-loglevel', str(self.logLevel),
421            '-f', 'rawvideo', '-vcodec', 'rawvideo', '-pix_fmt', inputEncoding.value,
422            '-video_size', f"{self.width}x{self.height}",
423            '-r', "{:.3f}".format(self.fps),
424            '-i', '-'])
425
426        if encodingParams is not None:
427            cmd.extend(encodingParams.split())
428
429        cmd.extend( ['-an', filename ] )
430
431        if self.debugMode == True:
432            print( ' '.join(cmd), file=sys.stderr )
433
434        # store filename and set mode
435        self.filename = filename
436        self.mode = PipeMode.WRITE_MODE
437
438        # Call ffmpeg in write mode
439        try:
440            self.pipe = sp.Popen(cmd, stdin=sp.PIPE)
441            self.frame_counter = FrameCounter(self.fps)
442        except Exception as e:
443            # if pipe failed, reinit object and raise exception
444            self.init()
445            raise
446
447        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:
449    def open( self, filename, *, width = -1, height = -1, fps = -1.0, outputEncoding = PixelFormat.GBR24,
450                    decodingParams = None, start_time = 0.0 ) -> bool:
451        """
452        Method to read video using parametrized access through ffmpeg. Importante note: calling open
453        on a VideoIO will close any former open video.
454
455        Parameters
456        ----------
457        filename: str or path
458            filename of path to the file (mp4, avi, ...)
459
460        width: int optional (default -1)
461            If defined as a positive value, width of input images will be converted to this value.
462
463        height: int optional (default -1)
464            If defined as a positive value, height of input images will be converted to this value.
465
466        fps: float optional (default -1.0)
467            If defined as a positive value, fps of input video will be converted to this value.
468
469        outputEncoding: PixelFormat optional (default PixelFormat.BGR24)
470            Define order of channels for channels. Possible values are PixelFormat.BGR24 or PixelFormat.RGB24.
471
472        decodingParams: str optional (default None)
473            Parameter to pass to ffmpeg to decode video like filters.
474
475        start_time: float optional (default 0.0)
476            Define the reading start time. If not set, reading at beginning of the video.
477
478        Returns
479        ----------
480        bool
481            Was the opening successfull
482        """
483
484        # Close if already opened
485        self.close()
486
487        # Force conversion of parameters
488        width = int(width)
489        height = int(height)
490        fps = float(fps)
491
492        # get parameters from video
493        self.width, self.height, self.fps = self.getVideoParams(filename)
494
495        # check if parameters ask to overide video parameters
496        # TODO: add support for negative value (automatic preservation of aspect ratio)
497        if width > 0:
498            self.width = width
499        if height > 0:
500            self.height = height
501        if fps > 0.0:
502            self.fps = fps
503
504        # Params are ok, set shape and image size
505        self.shape = (self.height,self.width,3)
506        self.imageSize = self.height * self.width * 3
507
508        # Video params are set, open the video
509        cmd = [self.videoProgram, # ffmpeg
510                    '-hide_banner',
511                    '-nostats',
512                    '-loglevel', str(self.logLevel)]
513
514        if start_time < 0.0:
515            pass
516        elif start_time > 0.0:
517            cmd.extend(["-ss", f"{start_time}"])    # set start time if any
518
519        cmd.extend( ['-i', filename] )
520
521        video_filters = '' # empty
522        if decodingParams is not None:
523            decodingParams = decodingParams.split()
524            # walk over decodingParams for specific params
525            i = 0
526            while i < len(decodingParams):
527                if decodingParams[i] == '-vf':
528                    decodingParams.pop(i)  # remove '-vf'
529                    if i < len(decodingParams):
530                        video_filters += ','+decodingParams.pop(i)  # remove parameters from list too
531                    # to do : add support to other option like -y
532                else:
533                    i += 1
534        else:
535            decodingParams = []
536
537        cmd.extend( ['-vf', f'scale={self.width}:{self.height}{video_filters}', # rescale (or not if shape is original one), add specific video filters
538                    *(decodingParams),
539                    '-f', 'rawvideo', '-vcodec', 'rawvideo', '-pix_fmt', outputEncoding.value, # input expected coding
540                    '-an', # no audio
541                     '-r', f"{self.fps}",
542                     '-' # output to stdout
543                    ] )
544
545        if self.debugMode == True:
546            print( ' '.join(cmd) )
547
548        # store filename and set mode to READ_MODE
549        self.filename = filename
550        self.mode = PipeMode.READ_MODE
551
552        # call ffmpeg in read mode
553        try:
554            self.pipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe
555            self.frame_counter = FrameCounter(self.fps)
556            if start_time > 0.0:
557                self.frame_counter += start_time # adding with float means adding time
558        except Exception as e:
559            # if pipe failed, reinit object and raise exception
560            self.init()
561            raise
562
563        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):
565    def read_frame(self, with_timestamps = False):
566        """
567        Read next frame from the video
568
569        Parameters
570        ----------
571        with_timestamps: bool optional (default False)
572            If set to True, the method returns a FrameContainer with the image and an array containing the associated timestamp(s)
573
574        Returns
575        ----------
576        nparray or FrameContainer
577            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
578            the associated timestamp in ``FrameContainer.timestamps`` as an array (one element for one frame).
579        """
580
581        if self.pipe is None:
582            raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading a frame.".format(self.videoProgram))
583        # - pipe is in write mode
584        if self.mode != PipeMode.READ_MODE:
585            raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename))
586
587        if with_timestamps:
588            # get elapsed time in video, it is time of next frame(s)
589            current_elapsed_time = self.get_elapsed_time()
590
591        # read rgb image from pipe
592        buffer = self.pipe.stdout.read(self.imageSize)
593        if len(buffer) != self.imageSize:
594            # not considered as an error, no more frame, no exception
595            return None
596
597        # get numpy UINT8 array from buffer
598        rgbImage = np.frombuffer(buffer, dtype = np.uint8).reshape(self.shape)
599
600        # increase frame_counter
601        self.frame_counter.frame_count += 1
602
603        # say to gc that this buffer is no longer needed
604        del buffer
605
606        if with_timestamps:
607            return FrameContainer(1,rgbImage,self.fps,current_elapsed_time)
608
609        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>, simple_ffmpeg_batch_io.FrameContainer]:
611    def read_batch(self, number_of_frames, with_timestamps = False) ->  Union[np.array, FrameContainer]:
612        """
613        Read next batch of images from the video
614
615        Parameters
616        ----------
617        number_of_frames: int
618            Number of desired images within the batch. The last batch of the video may have less images.
619            
620        with_timestamps: bool optional (default False)
621            If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames
622
623        Returns
624        ----------
625        nparray or FrameContainer
626            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
627            the associated timestamps in ``FrameContainer.timestamps`` as an array (one element for each frame).
628        """
629
630        if self.pipe is None:
631            raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading frames.".format(self.videoProgram))
632        # - pipe is in write mode
633        if self.mode != PipeMode.READ_MODE:
634            raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename))
635
636        if with_timestamps:
637            # get elapsed time in video, it is time of next frame(s)
638            current_elapsed_time = self.get_elapsed_time()
639
640        # try to read complete batch
641        buffer = self.pipe.stdout.read(self.imageSize*number_of_frames)
642
643        # check if we have at least 1 Frame
644        if len(buffer) < self.imageSize:
645            # not considered as an error, no more frame, no exception
646            return None
647
648        # compute actual number of Frames
649        actualNbFrames = len(buffer)//self.imageSize
650
651        # get and reshape batch from buffer
652        batch = np.frombuffer(buffer, dtype = np.uint8).reshape((actualNbFrames, self.height, self.width, 3))
653
654        # increase frame_counter
655        self.frame_counter.frame_count += actualNbFrames
656        
657        # say to gc that this buffer is no longer needed
658        del buffer
659
660        if with_timestamps:
661            return FrameContainer(actualNbFrames, batch, self.fps, current_elapsed_time)
662
663        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:
665    def write_frame(self, image) -> bool:
666        """
667        Write an image to the video
668
669        Parameters
670        ----------
671        image: nparray
672            The image of shape (3, width, height) to write to the video file in the PixelFormat provided when create was called.
673
674        Returns
675        ----------
676        bool
677            Writing was successful or not.
678        """
679        
680        # Check params
681        # - pipe exists
682        if self.pipe is None:
683            raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram))
684        # - pipe is in write mode
685        if self.mode != PipeMode.WRITE_MODE:
686            raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename))
687        # - shape of image is fine, thus we have pixels for a full compatible frame
688        if image.shape != self.shape:
689            raise self.VideoIOException("Wong image shape: {} expected {}.".format(image.shape,self.shape))
690        # - type of data is UINT8
691        if image.dtype != np.uint8:
692            raise self.VideoIOException("Wong pixel type: {} expected np.uint8.".format(image.dtype))
693
694        # write frame
695        buffer = image.tobytes()
696        if self.pipe.stdin.write( buffer ) < self.imageSize:
697            print( "Error writing frame to" )
698            return False
699
700        # increase frame_counter
701        self.frame_counter.frame_count += 1
702
703        # say to gc that this buffer is no longer needed 
704        del buffer
705
706        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:
708    def write_batch(self, batch) -> bool:
709        """
710        Write a batch of images to the video
711
712        Parameters
713        ----------
714        batch: nparray
715            A batch of images to write to the video file in the PixelFormat provided when create was called.
716
717        Returns
718        ----------
719        bool
720            Writing was successful or not.
721        """
722
723        # Check params
724        # - pipe exists
725        if self.pipe is None:
726            raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram))
727        # - pipe is in write mode
728        if self.mode != PipeMode.WRITE_MODE:
729            raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename))
730        # - shape of images in batch is fine
731        if batch.shape[-3:] != self.shape:
732            raise self.VideoIOException("Wrong image shape in batch: {} expected {}.".format(batch.shape[-3:], self.shape))
733        # - we have the right amount of pixels for the full batch
734        if batch.size != (batch.shape[0]*self.imageSize):
735            raise self.VideoIOException("Wrong number of pixels in batch: {} expected {}.".format(batch.shape[-3:], self.imageSize))
736
737        # write frame
738        buffer = batch.tobytes()
739        if self.pipe.stdin.write( buffer ) < batch.size:
740            # say to gc that this buffer is no longer needed
741            del buffer
742            raise self.VideoIOException("Error writing batch to '{}'.".format(self.filename))
743
744        # increase frame_counter
745        self.frame_counter.frame_count += batch.shape[0]       
746            
747        # say to gc that this buffer is no longer needed
748        del buffer
749
750        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):
752    def iter_frames(self, with_timestamps = False):
753        """
754        Method to iterate on video frames using VideoIO obj.
755        for frame in obj.iter_frames():
756            ....
757
758        Parameters
759        ----------
760        with_timestamps: bool optional (default False)
761            If set to True, the method returns a FrameContainer with the batch and an array containing the associated timestamps to frames
762        """
763
764        try:
765            if self.mode == PipeMode.READ_MODE:
766                while self.isOpened():
767                    frame = self.readFrame(with_timestamps)
768                    if frame is not None:
769                        yield frame
770        finally:
771            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):
773    def iter_batches(self, batch_size : int, with_timestamps = False):
774        """
775        Method to iterate on batch of frames using VideoIO obj.
776        for image_batch in obj.iter_batches():
777            ....
778
779        Parameters
780        ----------
781        with_timestamps: bool optional (default False)
782            If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames
783        """
784        try:
785            if self.mode == PipeMode.READ_MODE:
786                while self.isOpened():
787                    batch = self.readBatch(batch_size, with_timestamps)
788                    if batch is not None:
789                        yield batch
790        finally:
791            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):
 91    @staticmethod
 92    def get_time_in_sec(filename, *, debug=False, logLevel=16):
 93        """
 94        Static method to get length of a video file in seconds including milliseconds as decimal part.
 95
 96        Parameters
 97        ----------
 98        filename : str or path
 99            Video file name.
100
101        debug : bool (default False)
102            Show debug info.
103
104        log_level: int (default 16)
105            Log level to pass to the underlying ffmpeg/ffprobe command.
106        
107        Returns
108        ----------
109        float
110            Length in seconds of video file (including milliseconds as decimal part)
111        """
112        
113        cmd = [VideoIO.paramProgram, # ffprobe
114                    '-hide_banner',
115                    '-loglevel', str(logLevel),
116                    '-show_entries', 'format=duration',
117                    '-of', 'default=noprint_wrappers=1:nokey=1',
118                    filename
119                    ]
120
121        if debug == True:
122            print(' '.join(cmd))
123
124        # call ffprobe and get params in one single line
125        lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe
126        output = lpipe.stdout.readlines()
127        lpipe.terminate()
128        # transform Bytes output to one single string
129        output = ''.join( [element.decode('utf-8') for element in output])
130
131        try:
132            return float(output)
133        except (ValueError, TypeError):
134            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):
136    @staticmethod
137    def get_params(filename, *, debug=False, logLevel=16):
138        """
139        Static method to get params (width, height, fps) from a video file.
140
141        Parameters
142        ----------
143        filename: str or path
144            Video filename.
145
146        debug: bool (default (False)
147            Show debug info.
148
149        log_level: int (default 16)
150            Log level to pass to the underlying ffmpeg/ffprobe command.
151
152        Returns
153        ----------
154        tuple
155            Tuple containing (width, height, fps) of the video
156        """
157        cmd = [VideoIO.paramProgram, # ffprobe
158                    '-hide_banner',
159                    '-loglevel', str(logLevel),
160                    '-show_entries', 'stream=width,height,r_frame_rate',
161                    filename
162                    ]
163
164        if debug == True:
165            print(' '.join(cmd))
166
167        # call ffprobe and get params in one single line
168        lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe
169        output = lpipe.stdout.readlines()
170        lpipe.terminate()
171        # transform Bytes output to one single string
172        output = ''.join( [element.decode('utf-8') for element in output])
173
174        pattern_width = r'width=(\d+)'
175        pattern_height = r'height=(\d+)'
176        pattern_fps = r'r_frame_rate=(\d+)/(\d+)'
177
178        # Search for values in the ffprobe output
179        match_width = re.search(pattern_width, output, flags=re.MULTILINE)
180        match_height = re.search(pattern_height, output, flags=re.MULTILINE)
181        match_fps = re.search(pattern_fps, output, flags=re.MULTILINE)
182
183        # Extraction des valeurs
184        if match_width:
185            width = int(match_width.group(1))
186        else:
187            raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'")
188
189        if match_height:
190            height = int(match_height.group(1))
191        else:
192            raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'")
193
194        if match_fps:
195            numerator = float(match_fps.group(1))
196            denominator = float(match_fps.group(2))
197            fps = numerator / denominator
198        else:
199            raise VideoIO.VideoIOException("Unable to get frame rate (fps) of '" + filename + "'")
200
201        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):
 91    @staticmethod
 92    def get_time_in_sec(filename, *, debug=False, logLevel=16):
 93        """
 94        Static method to get length of a video file in seconds including milliseconds as decimal part.
 95
 96        Parameters
 97        ----------
 98        filename : str or path
 99            Video file name.
100
101        debug : bool (default False)
102            Show debug info.
103
104        log_level: int (default 16)
105            Log level to pass to the underlying ffmpeg/ffprobe command.
106        
107        Returns
108        ----------
109        float
110            Length in seconds of video file (including milliseconds as decimal part)
111        """
112        
113        cmd = [VideoIO.paramProgram, # ffprobe
114                    '-hide_banner',
115                    '-loglevel', str(logLevel),
116                    '-show_entries', 'format=duration',
117                    '-of', 'default=noprint_wrappers=1:nokey=1',
118                    filename
119                    ]
120
121        if debug == True:
122            print(' '.join(cmd))
123
124        # call ffprobe and get params in one single line
125        lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe
126        output = lpipe.stdout.readlines()
127        lpipe.terminate()
128        # transform Bytes output to one single string
129        output = ''.join( [element.decode('utf-8') for element in output])
130
131        try:
132            return float(output)
133        except (ValueError, TypeError):
134            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):
136    @staticmethod
137    def get_params(filename, *, debug=False, logLevel=16):
138        """
139        Static method to get params (width, height, fps) from a video file.
140
141        Parameters
142        ----------
143        filename: str or path
144            Video filename.
145
146        debug: bool (default (False)
147            Show debug info.
148
149        log_level: int (default 16)
150            Log level to pass to the underlying ffmpeg/ffprobe command.
151
152        Returns
153        ----------
154        tuple
155            Tuple containing (width, height, fps) of the video
156        """
157        cmd = [VideoIO.paramProgram, # ffprobe
158                    '-hide_banner',
159                    '-loglevel', str(logLevel),
160                    '-show_entries', 'stream=width,height,r_frame_rate',
161                    filename
162                    ]
163
164        if debug == True:
165            print(' '.join(cmd))
166
167        # call ffprobe and get params in one single line
168        lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe
169        output = lpipe.stdout.readlines()
170        lpipe.terminate()
171        # transform Bytes output to one single string
172        output = ''.join( [element.decode('utf-8') for element in output])
173
174        pattern_width = r'width=(\d+)'
175        pattern_height = r'height=(\d+)'
176        pattern_fps = r'r_frame_rate=(\d+)/(\d+)'
177
178        # Search for values in the ffprobe output
179        match_width = re.search(pattern_width, output, flags=re.MULTILINE)
180        match_height = re.search(pattern_height, output, flags=re.MULTILINE)
181        match_fps = re.search(pattern_fps, output, flags=re.MULTILINE)
182
183        # Extraction des valeurs
184        if match_width:
185            width = int(match_width.group(1))
186        else:
187            raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'")
188
189        if match_height:
190            height = int(match_height.group(1))
191        else:
192            raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'")
193
194        if match_fps:
195            numerator = float(match_fps.group(1))
196            denominator = float(match_fps.group(2))
197            fps = numerator / denominator
198        else:
199            raise VideoIO.VideoIOException("Unable to get frame rate (fps) of '" + filename + "'")
200
201        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:
330    def is_opened(self) -> bool:
331        """
332        Method to get status of the underlying pipe to ffmpeg.
333
334        Returns
335        ----------
336        bool
337            True if pipe is opened (reading or writing mode), False if not.
338        """
339        # is the pipe opened?
340        if self.pipe is not None and self.pipe.poll() is None:
341            return True
342
343        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):
565    def read_frame(self, with_timestamps = False):
566        """
567        Read next frame from the video
568
569        Parameters
570        ----------
571        with_timestamps: bool optional (default False)
572            If set to True, the method returns a FrameContainer with the image and an array containing the associated timestamp(s)
573
574        Returns
575        ----------
576        nparray or FrameContainer
577            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
578            the associated timestamp in ``FrameContainer.timestamps`` as an array (one element for one frame).
579        """
580
581        if self.pipe is None:
582            raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading a frame.".format(self.videoProgram))
583        # - pipe is in write mode
584        if self.mode != PipeMode.READ_MODE:
585            raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename))
586
587        if with_timestamps:
588            # get elapsed time in video, it is time of next frame(s)
589            current_elapsed_time = self.get_elapsed_time()
590
591        # read rgb image from pipe
592        buffer = self.pipe.stdout.read(self.imageSize)
593        if len(buffer) != self.imageSize:
594            # not considered as an error, no more frame, no exception
595            return None
596
597        # get numpy UINT8 array from buffer
598        rgbImage = np.frombuffer(buffer, dtype = np.uint8).reshape(self.shape)
599
600        # increase frame_counter
601        self.frame_counter.frame_count += 1
602
603        # say to gc that this buffer is no longer needed
604        del buffer
605
606        if with_timestamps:
607            return FrameContainer(1,rgbImage,self.fps,current_elapsed_time)
608
609        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>, simple_ffmpeg_batch_io.FrameContainer]:
611    def read_batch(self, number_of_frames, with_timestamps = False) ->  Union[np.array, FrameContainer]:
612        """
613        Read next batch of images from the video
614
615        Parameters
616        ----------
617        number_of_frames: int
618            Number of desired images within the batch. The last batch of the video may have less images.
619            
620        with_timestamps: bool optional (default False)
621            If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames
622
623        Returns
624        ----------
625        nparray or FrameContainer
626            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
627            the associated timestamps in ``FrameContainer.timestamps`` as an array (one element for each frame).
628        """
629
630        if self.pipe is None:
631            raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading frames.".format(self.videoProgram))
632        # - pipe is in write mode
633        if self.mode != PipeMode.READ_MODE:
634            raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename))
635
636        if with_timestamps:
637            # get elapsed time in video, it is time of next frame(s)
638            current_elapsed_time = self.get_elapsed_time()
639
640        # try to read complete batch
641        buffer = self.pipe.stdout.read(self.imageSize*number_of_frames)
642
643        # check if we have at least 1 Frame
644        if len(buffer) < self.imageSize:
645            # not considered as an error, no more frame, no exception
646            return None
647
648        # compute actual number of Frames
649        actualNbFrames = len(buffer)//self.imageSize
650
651        # get and reshape batch from buffer
652        batch = np.frombuffer(buffer, dtype = np.uint8).reshape((actualNbFrames, self.height, self.width, 3))
653
654        # increase frame_counter
655        self.frame_counter.frame_count += actualNbFrames
656        
657        # say to gc that this buffer is no longer needed
658        del buffer
659
660        if with_timestamps:
661            return FrameContainer(actualNbFrames, batch, self.fps, current_elapsed_time)
662
663        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:
665    def write_frame(self, image) -> bool:
666        """
667        Write an image to the video
668
669        Parameters
670        ----------
671        image: nparray
672            The image of shape (3, width, height) to write to the video file in the PixelFormat provided when create was called.
673
674        Returns
675        ----------
676        bool
677            Writing was successful or not.
678        """
679        
680        # Check params
681        # - pipe exists
682        if self.pipe is None:
683            raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram))
684        # - pipe is in write mode
685        if self.mode != PipeMode.WRITE_MODE:
686            raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename))
687        # - shape of image is fine, thus we have pixels for a full compatible frame
688        if image.shape != self.shape:
689            raise self.VideoIOException("Wong image shape: {} expected {}.".format(image.shape,self.shape))
690        # - type of data is UINT8
691        if image.dtype != np.uint8:
692            raise self.VideoIOException("Wong pixel type: {} expected np.uint8.".format(image.dtype))
693
694        # write frame
695        buffer = image.tobytes()
696        if self.pipe.stdin.write( buffer ) < self.imageSize:
697            print( "Error writing frame to" )
698            return False
699
700        # increase frame_counter
701        self.frame_counter.frame_count += 1
702
703        # say to gc that this buffer is no longer needed 
704        del buffer
705
706        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:
708    def write_batch(self, batch) -> bool:
709        """
710        Write a batch of images to the video
711
712        Parameters
713        ----------
714        batch: nparray
715            A batch of images to write to the video file in the PixelFormat provided when create was called.
716
717        Returns
718        ----------
719        bool
720            Writing was successful or not.
721        """
722
723        # Check params
724        # - pipe exists
725        if self.pipe is None:
726            raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram))
727        # - pipe is in write mode
728        if self.mode != PipeMode.WRITE_MODE:
729            raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename))
730        # - shape of images in batch is fine
731        if batch.shape[-3:] != self.shape:
732            raise self.VideoIOException("Wrong image shape in batch: {} expected {}.".format(batch.shape[-3:], self.shape))
733        # - we have the right amount of pixels for the full batch
734        if batch.size != (batch.shape[0]*self.imageSize):
735            raise self.VideoIOException("Wrong number of pixels in batch: {} expected {}.".format(batch.shape[-3:], self.imageSize))
736
737        # write frame
738        buffer = batch.tobytes()
739        if self.pipe.stdin.write( buffer ) < batch.size:
740            # say to gc that this buffer is no longer needed
741            del buffer
742            raise self.VideoIOException("Error writing batch to '{}'.".format(self.filename))
743
744        # increase frame_counter
745        self.frame_counter.frame_count += batch.shape[0]       
746            
747        # say to gc that this buffer is no longer needed
748        del buffer
749
750        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'>