Files

197 lines
6.6 KiB
Python

import json
import logging
import os
import subprocess
# Abstraction of the source file from which we are producing our encodes
# We are prefetching properties of the source file audio/video streams to help determine encoding argument values
class SourceFile:
format_args = [
"ffprobe",
"-v",
"quiet",
"-print_format",
"json",
"-show_streams",
"-show_format",
"-show_chapters",
]
def __init__(
self,
file,
file_format,
selected_video_stream,
selected_audio_stream,
video_format,
audio_format,
):
self.file = file
self.file_format = file_format
self.selected_video_stream = selected_video_stream
self.selected_audio_stream = selected_audio_stream
self.video_format = video_format
self.audio_format = audio_format
@classmethod
def from_file(cls, file, encoding_config):
video_file = None
audio_file = None
try:
file_format = SourceFile.get_file_format(file)
selected_video_stream = SourceFile.get_default_stream(
file_format, "video", encoding_config
)
if selected_video_stream is None:
selected_video_stream = SourceFile.get_selected_stream(
file_format, "video"
)
selected_audio_stream = SourceFile.get_default_stream(
file_format, "audio", encoding_config
)
if selected_audio_stream is None:
selected_audio_stream = SourceFile.get_selected_stream(
file_format, "audio"
)
logging.info("Retrieving extracted audio/video stream/format data...")
video_file = "[Video]" + file
logging.debug(f"[SourceFile.from_file] video_file: '{video_file}'")
audio_file = "[Audio]" + file
logging.debug(f"[SourceFile.from_file] audio_file: '{audio_file}'")
demux_args = [
"ffmpeg",
"-i",
file,
"-v",
"quiet",
"-y",
"-sn",
"-dn",
"-map",
f"0:v:{selected_video_stream}",
"-vcodec",
"copy",
video_file,
"-map",
f"0:a:{selected_audio_stream}",
"-acodec",
"copy",
audio_file,
]
subprocess.call(demux_args)
video_args = SourceFile.format_args + [video_file]
video_format = subprocess.check_output(video_args).decode("utf-8")
video_format = json.loads(video_format)
os.remove(video_file)
logging.debug(
f"[SourceFile.from_file] video_file deleted: {not os.path.isfile(video_file)}"
)
audio_args = SourceFile.format_args + [audio_file]
audio_format = subprocess.check_output(audio_args).decode("utf-8")
audio_format = json.loads(audio_format)
os.remove(audio_file)
logging.debug(
f"[SourceFile.from_file] audio_file deleted: {not os.path.isfile(audio_file)}"
)
return cls(
file,
file_format,
selected_video_stream,
selected_audio_stream,
video_format,
audio_format,
)
except KeyboardInterrupt:
logging.info("Attempting to delete temp files after keyboard interrupt")
if os.path.isfile(video_file):
os.remove(video_file)
if os.path.isfile(audio_file):
os.remove(audio_file)
raise
# Source file streams/formats
@staticmethod
def get_file_format(file):
logging.info("Retrieving source file stream/format data...")
format_args = SourceFile.format_args + [file]
file_format = subprocess.check_output(format_args).decode("utf-8")
return json.loads(file_format)
# Get the number of streams of the codec type (audio/video)
@staticmethod
def get_stream_count(file_format, target_codec_type) -> int:
count = 0
for stream in file_format["streams"]:
if stream["codec_type"] == target_codec_type:
count += 1
logging.debug(
f"[SourceFile.get_stream_count] target_codec_type: '{target_codec_type}', count: '{count}'"
)
return count
# Validate default stream selection before prompting the user to specify which stream to use
@staticmethod
def get_default_stream(file_format, stream_type, encoding_config) -> int | None:
# Exit early if default stream is not set
default_stream = encoding_config.get_default_stream(stream_type)
if not default_stream:
return None
stream_count = SourceFile.get_stream_count(file_format, stream_type)
try:
default_stream = int(default_stream)
if default_stream in range(stream_count):
return default_stream
logging.error(f"Default stream selection '{default_stream}' is invalid")
except ValueError:
logging.error(
f"Default stream selection '{default_stream}' must be an integer"
)
return None
# If there exists more than one stream for a codec type (audio/video),
# we want the user to specify which stream to use
@staticmethod
def get_selected_stream(file_format, stream_type) -> int:
stream_count = SourceFile.get_stream_count(file_format, stream_type)
if stream_count <= 1:
return 0
streams = range(stream_count)
prompt_text = f"Select {stream_type} stream [0-{stream_count - 1}]: "
while True:
try:
selected_stream = int(input(prompt_text))
if selected_stream in streams:
return selected_stream
logging.error("Stream selection is invalid")
except ValueError:
logging.error("Stream selection must be an integer")
# If our source file audio stream is not a 2-channel stereo layout, we need to resample it before normalization
def apply_audio_resampling(self, audio_filters) -> None:
channels = int(self.audio_format["streams"][0].get("channels", 2))
channel_layout = self.audio_format["streams"][0].get("channel_layout", "stereo")
if channels != 2 or channel_layout != "stereo":
audio_filters.append("aresample=ochl=stereo")