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

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

Parameters

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

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

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

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

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

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

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

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

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

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

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

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

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

Parameters

filename : str or path Video file name.

debug : bool (default False) Show debug info.

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

Returns

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

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

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

Parameters

filename: str or path Video filename.

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

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

Returns

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

Pipemode of the current object (default PipeMode.UNK_MODE)

loglevel: int

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

debug: bool

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

width: int

width of images (default -1)

height: int

height of images (default -1)

fps: float

fps of video (default -1.0)

pipe: pdoc.extract._PdocDefusedPopen

pipe object to ffmpeg/ffprobe (default None)

shape: tuple

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

imageSize: int

Weight in bytes of one image (default -1)

filename: str

Filename of the video file (default None)

Framecounter object to count ellapsed time (default None)

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

Init or reinit a VideoIO object.

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

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

Returns

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

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

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

Returns

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

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

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

Returns

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

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

Method to get status of the underlying pipe to ffmpeg.

Returns

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

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

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

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

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

Parameters

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

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

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

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

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

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

Returns

bool Was the creation successfull

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

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

Parameters

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

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

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

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

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

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

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

Returns

bool Was the opening successfull

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

Read next frame from the video

Parameters

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

Returns

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

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

Read next batch of images from the video

Parameters

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

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

Returns

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

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

Write an image to the video

Parameters

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

Returns

bool Writing was successful or not.

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

Write a batch of images to the video

Parameters

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

Returns

bool Writing was successful or not.

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

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

Parameters

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

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

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

Parameters

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

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

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

Parameters

filename : str or path Video file name.

debug : bool (default False) Show debug info.

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

Returns

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

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

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

Parameters

filename: str or path Video filename.

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

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

Returns

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

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

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

Parameters

filename : str or path Video file name.

debug : bool (default False) Show debug info.

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

Returns

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

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

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

Parameters

filename: str or path Video filename.

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

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

Returns

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

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

Method to get status of the underlying pipe to ffmpeg.

Returns

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

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

Read next frame from the video

Parameters

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

Returns

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

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

Read next batch of images from the video

Parameters

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

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

Returns

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

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

Write an image to the video

Parameters

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

Returns

bool Writing was successful or not.

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

Write a batch of images to the video

Parameters

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

Returns

bool Writing was successful or not.

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

Dedicated exception class for VideoIO class.

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

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

GBR24 = <PixelFormat.GBR24: 'bgr24'>
RGB24 = <PixelFormat.RGB24: 'rgb24'>