""" Python wrapper for ffprobe command line tool. ffprobe must exist in the path. """ import functools import json import operator import os import pipes import subprocess class FFProbeError(Exception): pass class FFStream: """ An object representation of an individual stream in a multimedia file. """ def __init__(self, json_data: dict): for key, val in json_data.items(): setattr(self, key, val) try: self.__dict__['framerate'] = round( functools.reduce( operator.truediv, map(int, self.__dict__.get('avg_frame_rate', '').split('/')) ) ) except ValueError: self.__dict__['framerate'] = None except ZeroDivisionError: self.__dict__['framerate'] = 0 def __repr__(self): if not 'codec_long_name' in self.__dict__: self.codec_long_name = self.__dict__.get('codec_name', '') if self.is_video(): return f"" if self.is_audio(): return f" " if self.is_subtitle() or self.is_attachment(): return f"" return f"" def is_audio(self): """ Is this stream labelled as an audio stream? """ return self.__dict__.get('codec_type', None) == 'audio' def is_video(self): """ Is the stream labelled as a video stream. """ return self.__dict__.get('codec_type', None) == 'video' def is_subtitle(self): """ Is the stream labelled as a subtitle stream. """ return self.__dict__.get('codec_type', None) == 'subtitle' def is_attachment(self): """ Is the stream labelled as a attachment stream. """ return self.__dict__.get('codec_type', None) == 'attachment' def frame_size(self): """ Returns the pixel frame size as an integer tuple (width,height) if the stream is a video stream. Returns None if it is not a video stream. """ size = None if self.is_video(): width = self.__dict__['width'] height = self.__dict__['height'] if width and height: try: size = (int(width), int(height)) except ValueError: raise FFProbeError("None integer size {}:{}".format(width, height)) else: return None return size def pixel_format(self): """ Returns a string representing the pixel format of the video stream. e.g. yuv420p. Returns none is it is not a video stream. """ return self.__dict__.get('pix_fmt', None) def frames(self): """ Returns the length of a video stream in frames. Returns 0 if not a video stream. """ if self.is_video() or self.is_audio(): try: frame_count = int(self.__dict__.get('nb_frames', '')) except ValueError: raise FFProbeError('None integer frame count') else: frame_count = 0 return frame_count def duration_seconds(self): """ Returns the runtime duration of the video stream as a floating point number of seconds. Returns 0.0 if not a video stream. """ if self.is_video() or self.is_audio(): try: duration = float(self.__dict__.get('duration', '')) except ValueError: raise FFProbeError('None numeric duration') else: duration = 0.0 return duration def language(self): """ Returns language tag of stream. e.g. eng """ return self.__dict__.get('TAG:language', None) def codec(self): """ Returns a string representation of the stream codec. """ return self.__dict__.get('codec_name', None) def codec_description(self): """ Returns a long representation of the stream codec. """ return self.__dict__.get('codec_long_name', None) def codec_tag(self): """ Returns a short representative tag of the stream codec. """ return self.__dict__.get('codec_tag_string', None) def bit_rate(self): """ Returns bit_rate as an integer in bps """ try: return int(self.__dict__.get('bit_rate', '')) except ValueError: raise FFProbeError('None integer bit_rate') class FFProbe: """ FFProbe wraps the ffprobe command and pulls the data into an object form:: metadata=FFProbe('multimedia-file.mov') """ audio: list[FFStream] = [] attachment: list[FFStream] = [] streams: list[FFStream] = [] subtitle: list[FFStream] = [] video: list[FFStream] = [] metadata: dict = {} path_to_video: str = '' def __init__(self, path_to_video): self.path_to_video = path_to_video try: with open(os.devnull, 'w') as tempf: subprocess.check_call(["ffprobe", "-h"], stdout=tempf, stderr=tempf) except FileNotFoundError: raise IOError('ffprobe not found.') if os.path.isfile(self.path_to_video): cmd: list = [ "ffprobe -v quiet -of json -show_format -show_streams " + pipes.quote(self.path_to_video) ] p = subprocess.Popen( args=cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True ) p.wait() data, err = p.communicate() if p.returncode == 0: parsed: dict = json.loads(data.decode('utf-8')) else: raise FFProbeError(f"FFProbe error: {err}") stream = False self.streams = [] self.video = [] self.audio = [] self.subtitle = [] self.attachment = [] for stream in parsed['streams'] if 'streams' in parsed else []: self.streams.append(FFStream(stream)) self.metadata = parsed['format'] if 'format' in parsed else {} for stream in self.streams: if stream.is_audio(): self.audio.append(stream) elif stream.is_video(): self.video.append(stream) elif stream.is_subtitle(): self.subtitle.append(stream) elif stream.is_attachment(): self.attachment.append(stream) else: raise IOError('No such media file ' + self.path_to_video) def __repr__(self): return "".format(**vars(self))