232 lines
7.1 KiB
Python
232 lines
7.1 KiB
Python
"""
|
|
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"<Stream: #{self.index} [{self.codec_type}] {self.codec_long_name}, {self.framerate}, ({self.width}x{self.height})>"
|
|
|
|
if self.is_audio():
|
|
return f"<Stream: #{self.index} [{self.codec_type}] {self.codec_long_name}, channels: {self.channels} ({self.channel_layout}), " "{sample_rate}Hz> "
|
|
|
|
if self.is_subtitle() or self.is_attachment():
|
|
return f"<Stream: #{self.index} [{self.codec_type}] {self.codec_long_name}>"
|
|
|
|
return f"<Stream: #{self.index} [{self.codec_type}]>"
|
|
|
|
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 "<FFprobe: {metadata}, {video}, {audio}, {subtitle}, {attachment}>".format(**vars(self))
|
|
|