Transfer functions that use ffprobe into a dedicated module.
This commit is contained in:
@@ -0,0 +1,238 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#
|
||||||
|
# Copyright (C) 2026 Frédéric Tronel
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from math import floor
|
||||||
|
from datetime import timedelta
|
||||||
|
import os
|
||||||
|
from os import (
|
||||||
|
SEEK_SET,
|
||||||
|
lseek,
|
||||||
|
set_inheritable,
|
||||||
|
)
|
||||||
|
from typing import IO
|
||||||
|
from subprocess import PIPE, Popen
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from typeguard import typechecked
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@typechecked
|
||||||
|
def get_frame_rate(ffprobe_path:str, input_file: IO[bytes]) -> float|None:
|
||||||
|
"""
|
||||||
|
Retrieves the frame rate of a video file using the ffprobe tool.
|
||||||
|
|
||||||
|
This function runs the ffprobe binary with the specified input file and parses the output
|
||||||
|
to extract the frame rate.
|
||||||
|
It uses two methods to calculate the frame rate: one based on the timestamp of the frames
|
||||||
|
and another based on the duration of the frames.
|
||||||
|
If the two calculated frame rates are significantly different, the function returns an error
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ffprobe_path (str): The path to the ffprobe binary.
|
||||||
|
input_file (IO[bytes]): The input video file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float | None:
|
||||||
|
- The frame rate of the video file as a floating-point number
|
||||||
|
- None if an error occurs while running the ffprobe binary or if the calculated
|
||||||
|
frame rates are inconsistent
|
||||||
|
"""
|
||||||
|
infd = input_file.fileno()
|
||||||
|
lseek(infd, 0, SEEK_SET)
|
||||||
|
set_inheritable(infd, True)
|
||||||
|
|
||||||
|
mean_duration = 0.
|
||||||
|
nb_frames1 = 0
|
||||||
|
nb_frames2 = 0
|
||||||
|
min_ts = None
|
||||||
|
max_ts = None
|
||||||
|
interlaced = False
|
||||||
|
|
||||||
|
params = [ffprobe_path, '-loglevel', 'quiet', '-select_streams', 'v', '-show_frames',
|
||||||
|
'-read_intervals', '00%+30', '-of', 'json', f'/proc/self/fd/{infd:d}']
|
||||||
|
env = {**os.environ, 'LANG': 'C'}
|
||||||
|
with Popen(params, stdout=PIPE, close_fds=False, env=env) as ffprobe:
|
||||||
|
out, _ = ffprobe.communicate()
|
||||||
|
out = json.load(BytesIO(out))
|
||||||
|
if 'frames' in out:
|
||||||
|
for frame in out['frames']:
|
||||||
|
if 'interlaced_frame' in frame:
|
||||||
|
if frame['interlaced_frame'] == 1:
|
||||||
|
interlaced = True
|
||||||
|
if 'pts_time' in frame:
|
||||||
|
ts = float(frame['pts_time'])
|
||||||
|
if min_ts is None:
|
||||||
|
min_ts = ts
|
||||||
|
if max_ts is None:
|
||||||
|
max_ts = ts
|
||||||
|
min_ts = min(min_ts, ts)
|
||||||
|
max_ts = max(max_ts, ts)
|
||||||
|
nb_frames1+=1
|
||||||
|
if 'duration_time' in frame:
|
||||||
|
mean_duration+=float(frame['duration_time'])
|
||||||
|
nb_frames2+=1
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
ffprobe.wait()
|
||||||
|
|
||||||
|
if ffprobe.returncode != 0:
|
||||||
|
logger.error("ffprobe returns an error code: %d", ffprobe.returncode)
|
||||||
|
return None
|
||||||
|
|
||||||
|
frame_rate1 = nb_frames1/(max_ts-min_ts)
|
||||||
|
frame_rate2 = nb_frames2 / mean_duration
|
||||||
|
|
||||||
|
if abs(frame_rate1 - frame_rate2) > 0.2:
|
||||||
|
if not interlaced:
|
||||||
|
logger.error('Video is not interlaced and the disperancy between frame rates is too \
|
||||||
|
big: %f / %f', frame_rate1, frame_rate2)
|
||||||
|
return None
|
||||||
|
if abs(frame_rate1*2 - frame_rate2) < 0.2:
|
||||||
|
return frame_rate2/2
|
||||||
|
logger.error('Video is interlaced and the disperancy between frame rates is too big:\
|
||||||
|
%f / %f', frame_rate1, frame_rate2)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return frame_rate2
|
||||||
|
|
||||||
|
@typechecked
|
||||||
|
def get_subtitles_tracks(ffprobe_path:str, mkv_path: str) -> dict[str,str]|None:
|
||||||
|
tracks={}
|
||||||
|
|
||||||
|
with Popen([ffprobe_path, '-loglevel', 'quiet', '-select_streams', 's', '-show_entries',
|
||||||
|
'stream=index,codec_name:stream_tags=language', '-of', 'json', mkv_path],
|
||||||
|
stdout=PIPE) as ffprobe:
|
||||||
|
out, _ = ffprobe.communicate()
|
||||||
|
out = json.load(BytesIO(out))
|
||||||
|
if 'streams' in out:
|
||||||
|
for stream in out['streams']:
|
||||||
|
index = stream['index']
|
||||||
|
codec = stream['codec']
|
||||||
|
lang = stream['tags']['language']
|
||||||
|
if codec == 'dvd_subtitle':
|
||||||
|
if lang not in tracks:
|
||||||
|
tracks[lang] = [index]
|
||||||
|
else:
|
||||||
|
current_langs = tracks[lang]
|
||||||
|
current_langs.append(index)
|
||||||
|
tracks[lang] = current_langs
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
ffprobe.wait()
|
||||||
|
|
||||||
|
if ffprobe.returncode != 0:
|
||||||
|
logger.error("ffprobe returns an error code: %d", ffprobe.returncode)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return tracks
|
||||||
|
|
||||||
|
@typechecked
|
||||||
|
def get_format(ffprobe_path:str, input_file: IO[bytes]) -> dict|None:
|
||||||
|
infd = input_file.fileno()
|
||||||
|
lseek(infd, 0, SEEK_SET)
|
||||||
|
set_inheritable(infd, True)
|
||||||
|
with Popen([ffprobe_path, '-loglevel', 'quiet', '-show_format', '-of', 'json', '-i',
|
||||||
|
f'/proc/self/fd/{infd:d}'], stdout=PIPE, close_fds=False) as ffprobe:
|
||||||
|
out, _ = ffprobe.communicate()
|
||||||
|
out = json.load(BytesIO(out))
|
||||||
|
if 'format' in out:
|
||||||
|
return out['format']
|
||||||
|
logger.error('Impossible to retrieve format of file')
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@typechecked
|
||||||
|
def get_movie_duration(ffprobe_path:str, input_file: IO[bytes]) -> timedelta|None:
|
||||||
|
infd = input_file.fileno()
|
||||||
|
lseek(infd, 0, SEEK_SET)
|
||||||
|
set_inheritable(infd, True)
|
||||||
|
with Popen([ffprobe_path, '-loglevel', 'quiet', '-show_format', '-of', 'json', '-i',
|
||||||
|
f'/proc/self/fd/{infd:d}'], stdout=PIPE, close_fds=False) as ffprobe:
|
||||||
|
out, _ = ffprobe.communicate()
|
||||||
|
out = json.load(BytesIO(out))
|
||||||
|
if 'format' in out and 'duration' in out['format']:
|
||||||
|
duration = floor(float(out['format']['duration']))
|
||||||
|
ts = timedelta(seconds=duration)
|
||||||
|
return ts
|
||||||
|
logger.error('Impossible to retrieve duration of movie')
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ffprobe -loglevel quiet -select_streams v:0 -show_entries stream=width,height -of json sample.ts
|
||||||
|
@typechecked
|
||||||
|
def get_video_dimensions(ffprobe_path:str, input_file: IO[bytes]) -> tuple[int,int]|None:
|
||||||
|
infd = input_file.fileno()
|
||||||
|
lseek(infd, 0, SEEK_SET)
|
||||||
|
set_inheritable(infd, True)
|
||||||
|
with Popen([ffprobe_path, '-loglevel', 'quiet', '-select_streams', 'v:0', '-show_entries',\
|
||||||
|
'stream=width,height', '-of', 'json', '-i', f'/proc/self/fd/{infd:d}'],\
|
||||||
|
stdout=PIPE, close_fds=False) as ffprobe:
|
||||||
|
out, _ = ffprobe.communicate()
|
||||||
|
out = json.load(BytesIO(out))
|
||||||
|
if 'streams' in out:
|
||||||
|
video = out['streams'][0]
|
||||||
|
if ('width' in video) and ('height' in video):
|
||||||
|
return int(video['width']), int(video['height'])
|
||||||
|
|
||||||
|
logger.error('Impossible to retrieve dimensions of video')
|
||||||
|
return None
|
||||||
|
|
||||||
|
@typechecked
|
||||||
|
def get_streams(ffprobe_path:str, input_file: IO[bytes]) -> list|None:
|
||||||
|
infd = input_file.fileno()
|
||||||
|
lseek(infd, 0, SEEK_SET)
|
||||||
|
set_inheritable(infd, True)
|
||||||
|
with Popen([ffprobe_path, '-loglevel', 'quiet', '-show_streams', '-of', 'json', '-i',
|
||||||
|
f'/proc/self/fd/{infd:d}'], stdout=PIPE, close_fds=False) as ffprobe:
|
||||||
|
out, _ = ffprobe.communicate()
|
||||||
|
out = json.load(BytesIO(out))
|
||||||
|
if 'streams' in out:
|
||||||
|
return out['streams']
|
||||||
|
logger.error('Impossible to retrieve streams inside file')
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@typechecked
|
||||||
|
def with_subtitles(ffprobe_path:str, input_file: IO[bytes]) -> bool:
|
||||||
|
"""
|
||||||
|
Checks if a media file contains subtitles using the ffprobe tool.
|
||||||
|
|
||||||
|
This function runs the ffprobe binary with the specified input file and parses the output
|
||||||
|
to determine if the file contains subtitles.
|
||||||
|
It returns True if at least one subtitle stream is found, False otherwise.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ffprobe_path (str): The path to the ffprobe binary.
|
||||||
|
input_file (IO[bytes]): The input media file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool:
|
||||||
|
- True if the media file contains at least one subtitle stream
|
||||||
|
- False if:
|
||||||
|
- the media file does not contain any subtitle streams
|
||||||
|
- an error occurs while running the ffprobe binary
|
||||||
|
- the streams information cannot be retrieved from the media file
|
||||||
|
"""
|
||||||
|
infd = input_file.fileno()
|
||||||
|
lseek(infd, 0, SEEK_SET)
|
||||||
|
set_inheritable(infd, True)
|
||||||
|
with Popen([ffprobe_path, '-loglevel', 'quiet', '-show_streams', '-of', 'json', '-i',
|
||||||
|
f'/proc/self/fd/{infd:d}'], stdout=PIPE, close_fds=False) as ffprobe:
|
||||||
|
out, _ = ffprobe.communicate()
|
||||||
|
out = json.load(BytesIO(out))
|
||||||
|
if 'streams' in out:
|
||||||
|
streams = out['streams']
|
||||||
|
for stream in streams:
|
||||||
|
if 'codec_type' in stream and stream['codec_type'] == 'subtitle':
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error('Impossible to retrieve streams inside file')
|
||||||
|
|
||||||
|
return False
|
||||||
+10
-228
@@ -36,6 +36,16 @@ from typeguard import typechecked
|
|||||||
from tscut.h264.avc import (dump_codec_private_data,
|
from tscut.h264.avc import (dump_codec_private_data,
|
||||||
get_avc_config_from_h264,
|
get_avc_config_from_h264,
|
||||||
parse_codec_private)
|
parse_codec_private)
|
||||||
|
from tscut.matroska.ebml import change_ebml_element_size
|
||||||
|
from tscut.tools.ffprobe import (
|
||||||
|
get_format,
|
||||||
|
get_frame_rate,
|
||||||
|
get_video_dimensions,
|
||||||
|
get_movie_duration,
|
||||||
|
get_streams,
|
||||||
|
with_subtitles
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Useful SPS/PPS discussion.
|
# Useful SPS/PPS discussion.
|
||||||
# https://copyprogramming.com/howto/including-sps-and-pps-in-a-raw-h264-track
|
# https://copyprogramming.com/howto/including-sps-and-pps-in-a-raw-h264-track
|
||||||
@@ -142,120 +152,7 @@ def get_tesseract_supported_lang(tesseract_path:str) -> dict[Lang, str]|None:
|
|||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@typechecked
|
|
||||||
def get_frame_rate(ffprobe_path:str, input_file: IO[bytes]) -> float|None:
|
|
||||||
"""
|
|
||||||
Retrieves the frame rate of a video file using the ffprobe tool.
|
|
||||||
|
|
||||||
This function runs the ffprobe binary with the specified input file and parses the output
|
|
||||||
to extract the frame rate.
|
|
||||||
It uses two methods to calculate the frame rate: one based on the timestamp of the frames
|
|
||||||
and another based on the duration of the frames.
|
|
||||||
If the two calculated frame rates are significantly different, the function returns an error
|
|
||||||
|
|
||||||
Args:
|
|
||||||
ffprobe_path (str): The path to the ffprobe binary.
|
|
||||||
input_file (IO[bytes]): The input video file.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float | None:
|
|
||||||
- The frame rate of the video file as a floating-point number
|
|
||||||
- None if an error occurs while running the ffprobe binary or if the calculated
|
|
||||||
frame rates are inconsistent
|
|
||||||
"""
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
infd = input_file.fileno()
|
|
||||||
lseek(infd, 0, SEEK_SET)
|
|
||||||
set_inheritable(infd, True)
|
|
||||||
|
|
||||||
mean_duration = 0.
|
|
||||||
nb_frames1 = 0
|
|
||||||
nb_frames2 = 0
|
|
||||||
min_ts = None
|
|
||||||
max_ts = None
|
|
||||||
interlaced = False
|
|
||||||
|
|
||||||
params = [ffprobe_path, '-loglevel', 'quiet', '-select_streams', 'v', '-show_frames',
|
|
||||||
'-read_intervals', '00%+30', '-of', 'json', f'/proc/self/fd/{infd:d}']
|
|
||||||
env = {**os.environ, 'LANG': 'C'}
|
|
||||||
with Popen(params, stdout=PIPE, close_fds=False, env=env) as ffprobe:
|
|
||||||
out, _ = ffprobe.communicate()
|
|
||||||
out = json.load(BytesIO(out))
|
|
||||||
if 'frames' in out:
|
|
||||||
for frame in out['frames']:
|
|
||||||
if 'interlaced_frame' in frame:
|
|
||||||
if frame['interlaced_frame'] == 1:
|
|
||||||
interlaced = True
|
|
||||||
if 'pts_time' in frame:
|
|
||||||
ts = float(frame['pts_time'])
|
|
||||||
if min_ts is None:
|
|
||||||
min_ts = ts
|
|
||||||
if max_ts is None:
|
|
||||||
max_ts = ts
|
|
||||||
min_ts = min(min_ts, ts)
|
|
||||||
max_ts = max(max_ts, ts)
|
|
||||||
nb_frames1+=1
|
|
||||||
if 'duration_time' in frame:
|
|
||||||
mean_duration+=float(frame['duration_time'])
|
|
||||||
nb_frames2+=1
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
ffprobe.wait()
|
|
||||||
|
|
||||||
if ffprobe.returncode != 0:
|
|
||||||
logger.error("ffprobe returns an error code: %d", ffprobe.returncode)
|
|
||||||
return None
|
|
||||||
|
|
||||||
frame_rate1 = nb_frames1/(max_ts-min_ts)
|
|
||||||
frame_rate2 = nb_frames2 / mean_duration
|
|
||||||
|
|
||||||
if abs(frame_rate1 - frame_rate2) > 0.2:
|
|
||||||
if not interlaced:
|
|
||||||
logger.error('Video is not interlaced and the disperancy between frame rates is too \
|
|
||||||
big: %f / %f', frame_rate1, frame_rate2)
|
|
||||||
return None
|
|
||||||
if abs(frame_rate1*2 - frame_rate2) < 0.2:
|
|
||||||
return frame_rate2/2
|
|
||||||
logger.error('Video is interlaced and the disperancy between frame rates is too big:\
|
|
||||||
%f / %f', frame_rate1, frame_rate2)
|
|
||||||
return None
|
|
||||||
|
|
||||||
return frame_rate2
|
|
||||||
|
|
||||||
@typechecked
|
|
||||||
def get_subtitles_tracks(ffprobe_path:str, mkv_path: str) -> dict[str,str]|None:
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
tracks={}
|
|
||||||
|
|
||||||
with Popen([ffprobe_path, '-loglevel', 'quiet', '-select_streams', 's', '-show_entries',
|
|
||||||
'stream=index,codec_name:stream_tags=language', '-of', 'json', mkv_path],
|
|
||||||
stdout=PIPE) as ffprobe:
|
|
||||||
out, _ = ffprobe.communicate()
|
|
||||||
out = json.load(BytesIO(out))
|
|
||||||
if 'streams' in out:
|
|
||||||
for stream in out['streams']:
|
|
||||||
index = stream['index']
|
|
||||||
codec = stream['codec']
|
|
||||||
lang = stream['tags']['language']
|
|
||||||
if codec == 'dvd_subtitle':
|
|
||||||
if lang not in tracks:
|
|
||||||
tracks[lang] = [index]
|
|
||||||
else:
|
|
||||||
current_langs = tracks[lang]
|
|
||||||
current_langs.append(index)
|
|
||||||
tracks[lang] = current_langs
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
ffprobe.wait()
|
|
||||||
|
|
||||||
if ffprobe.returncode != 0:
|
|
||||||
logger.error("ffprobe returns an error code: %d", ffprobe.returncode)
|
|
||||||
return None
|
|
||||||
|
|
||||||
return tracks
|
|
||||||
|
|
||||||
@typechecked
|
@typechecked
|
||||||
def extract_srt(mkvextract:str, filename:str, subtitles:dict[str, list[int]],
|
def extract_srt(mkvextract:str, filename:str, subtitles:dict[str, list[int]],
|
||||||
@@ -569,122 +466,7 @@ def change_codec_private_data(mkvinfo_path:str, input_file: IO[bytes], codec_dat
|
|||||||
# been resized).
|
# been resized).
|
||||||
delta+=change_ebml_element_size(input_file, pos, delta)
|
delta+=change_ebml_element_size(input_file, pos, delta)
|
||||||
|
|
||||||
@typechecked
|
|
||||||
def get_format(ffprobe_path:str, input_file: IO[bytes]) -> dict|None:
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
infd = input_file.fileno()
|
|
||||||
lseek(infd, 0, SEEK_SET)
|
|
||||||
set_inheritable(infd, True)
|
|
||||||
with Popen([ffprobe_path, '-loglevel', 'quiet', '-show_format', '-of', 'json', '-i',
|
|
||||||
f'/proc/self/fd/{infd:d}'], stdout=PIPE, close_fds=False) as ffprobe:
|
|
||||||
out, _ = ffprobe.communicate()
|
|
||||||
out = json.load(BytesIO(out))
|
|
||||||
if 'format' in out:
|
|
||||||
return out['format']
|
|
||||||
else:
|
|
||||||
logger.error('Impossible to retrieve format of file')
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
@typechecked
|
|
||||||
def get_movie_duration(ffprobe_path:str, input_file: IO[bytes]) -> timedelta|None:
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
infd = input_file.fileno()
|
|
||||||
lseek(infd, 0, SEEK_SET)
|
|
||||||
set_inheritable(infd, True)
|
|
||||||
with Popen([ffprobe_path, '-loglevel', 'quiet', '-show_format', '-of', 'json', '-i',
|
|
||||||
f'/proc/self/fd/{infd:d}'], stdout=PIPE, close_fds=False) as ffprobe:
|
|
||||||
out, _ = ffprobe.communicate()
|
|
||||||
out = json.load(BytesIO(out))
|
|
||||||
if 'format' in out and 'duration' in out['format']:
|
|
||||||
duration = floor(float(out['format']['duration']))
|
|
||||||
ts = timedelta(seconds=duration)
|
|
||||||
return ts
|
|
||||||
else:
|
|
||||||
logger.error('Impossible to retrieve duration of movie')
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
# ffprobe -loglevel quiet -select_streams v:0 -show_entries stream=width,height -of json sample.ts
|
|
||||||
@typechecked
|
|
||||||
def get_video_dimensions(ffprobe_path:str, input_file: IO[bytes]) -> tuple[int,int]:
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
infd = input_file.fileno()
|
|
||||||
lseek(infd, 0, SEEK_SET)
|
|
||||||
set_inheritable(infd, True)
|
|
||||||
with Popen([ffprobe_path, '-loglevel', 'quiet', '-select_streams', 'v:0', '-show_entries',\
|
|
||||||
'stream=width,height', '-of', 'json', '-i', f'/proc/self/fd/{infd:d}'],\
|
|
||||||
stdout=PIPE, close_fds=False) as ffprobe:
|
|
||||||
out, _ = ffprobe.communicate()
|
|
||||||
out = json.load(BytesIO(out))
|
|
||||||
if 'streams' in out:
|
|
||||||
video = out['streams'][0]
|
|
||||||
if ('width' in video) and ('height' in video):
|
|
||||||
return int(video['width']), int(video['height'])
|
|
||||||
|
|
||||||
logger.error('Impossible to retrieve dimensions of video')
|
|
||||||
exit(-1)
|
|
||||||
|
|
||||||
@typechecked
|
|
||||||
def get_streams(ffprobe_path:str, input_file: IO[bytes]) -> list|None:
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
infd = input_file.fileno()
|
|
||||||
lseek(infd, 0, SEEK_SET)
|
|
||||||
set_inheritable(infd, True)
|
|
||||||
with Popen([ffprobe_path, '-loglevel', 'quiet', '-show_streams', '-of', 'json', '-i',
|
|
||||||
f'/proc/self/fd/{infd:d}'], stdout=PIPE, close_fds=False) as ffprobe:
|
|
||||||
out, _ = ffprobe.communicate()
|
|
||||||
out = json.load(BytesIO(out))
|
|
||||||
if 'streams' in out:
|
|
||||||
return out['streams']
|
|
||||||
else:
|
|
||||||
logger.error('Impossible to retrieve streams inside file')
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
@typechecked
|
|
||||||
def with_subtitles(ffprobe_path:str, input_file: IO[bytes]) -> bool:
|
|
||||||
"""
|
|
||||||
Checks if a media file contains subtitles using the ffprobe tool.
|
|
||||||
|
|
||||||
This function runs the ffprobe binary with the specified input file and parses the output
|
|
||||||
to determine if the file contains subtitles.
|
|
||||||
It returns True if at least one subtitle stream is found, False otherwise.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
ffprobe_path (str): The path to the ffprobe binary.
|
|
||||||
input_file (IO[bytes]): The input media file.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool:
|
|
||||||
- True if the media file contains at least one subtitle stream
|
|
||||||
- False if:
|
|
||||||
- the media file does not contain any subtitle streams
|
|
||||||
- an error occurs while running the ffprobe binary
|
|
||||||
- the streams information cannot be retrieved from the media file
|
|
||||||
"""
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
infd = input_file.fileno()
|
|
||||||
lseek(infd, 0, SEEK_SET)
|
|
||||||
set_inheritable(infd, True)
|
|
||||||
with Popen([ffprobe_path, '-loglevel', 'quiet', '-show_streams', '-of', 'json', '-i',
|
|
||||||
f'/proc/self/fd/{infd:d}'], stdout=PIPE, close_fds=False) as ffprobe:
|
|
||||||
out, _ = ffprobe.communicate()
|
|
||||||
out = json.load(BytesIO(out))
|
|
||||||
if 'streams' in out:
|
|
||||||
streams = out['streams']
|
|
||||||
for stream in streams:
|
|
||||||
if 'codec_type' in stream and stream['codec_type'] == 'subtitle':
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
logger.error('Impossible to retrieve streams inside file')
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
@typechecked
|
@typechecked
|
||||||
def parse_timestamp(ts:str) -> timedelta|None:
|
def parse_timestamp(ts:str) -> timedelta|None:
|
||||||
|
|||||||
Reference in New Issue
Block a user