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