2020-01-14 18:43:28 +01:00
|
|
|
import asyncio
|
|
|
|
import json
|
|
|
|
import logging
|
|
|
|
import os
|
|
|
|
import pathlib
|
|
|
|
import re
|
|
|
|
import shlex
|
|
|
|
import shutil
|
2020-03-06 19:45:34 +01:00
|
|
|
import platform
|
2020-03-06 19:31:17 +01:00
|
|
|
|
|
|
|
import lbry.utils
|
2020-01-14 18:43:28 +01:00
|
|
|
from lbry.conf import TranscodeConfig
|
|
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
2020-03-06 19:45:34 +01:00
|
|
|
DISABLED = platform.system() == "Windows"
|
2020-01-14 18:43:28 +01:00
|
|
|
|
|
|
|
|
|
|
|
class VideoFileAnalyzer:
|
|
|
|
|
2020-03-05 01:08:47 +01:00
|
|
|
def _replace_or_pop_env(self, variable):
|
|
|
|
if variable + '_ORIG' in self._env_copy:
|
|
|
|
self._env_copy[variable] = self._env_copy[variable + '_ORIG']
|
|
|
|
else:
|
|
|
|
self._env_copy.pop(variable, None)
|
|
|
|
|
2020-02-04 04:05:23 +01:00
|
|
|
def __init__(self, conf: TranscodeConfig):
|
|
|
|
self._conf = conf
|
|
|
|
self._available_encoders = ""
|
|
|
|
self._ffmpeg_installed = False
|
|
|
|
self._which = None
|
2020-02-20 22:39:22 +01:00
|
|
|
self._checked_ffmpeg = False
|
2020-03-05 01:08:47 +01:00
|
|
|
self._env_copy = dict(os.environ)
|
2020-03-06 19:31:17 +01:00
|
|
|
if lbry.utils.is_running_from_bundle():
|
|
|
|
# handle the situation where PyInstaller overrides our runtime environment:
|
|
|
|
self._replace_or_pop_env('LD_LIBRARY_PATH')
|
2020-02-04 04:05:23 +01:00
|
|
|
|
2020-01-14 18:43:28 +01:00
|
|
|
async def _execute(self, command, arguments):
|
2020-03-06 19:45:34 +01:00
|
|
|
if DISABLED:
|
|
|
|
return "Disabled on Windows", -1
|
2020-01-30 18:37:08 +01:00
|
|
|
args = shlex.split(arguments)
|
2020-03-03 02:34:54 +01:00
|
|
|
process = await asyncio.create_subprocess_exec(
|
|
|
|
os.path.join(self._conf.ffmpeg_folder, command), *args,
|
2020-03-05 01:08:47 +01:00
|
|
|
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, env=self._env_copy
|
2020-03-03 02:34:54 +01:00
|
|
|
)
|
2020-01-14 18:43:28 +01:00
|
|
|
stdout, stderr = await process.communicate() # returns when the streams are closed
|
2020-02-07 21:27:51 +01:00
|
|
|
return stdout.decode(errors='replace') + stderr.decode(errors='replace'), process.returncode
|
2020-01-14 18:43:28 +01:00
|
|
|
|
2020-01-17 00:20:01 +01:00
|
|
|
async def _verify_executable(self, name):
|
|
|
|
try:
|
|
|
|
version, code = await self._execute(name, "-version")
|
|
|
|
except Exception as e:
|
|
|
|
code = -1
|
2020-03-04 23:53:46 +01:00
|
|
|
version = str(e)
|
2020-01-17 00:20:01 +01:00
|
|
|
if code != 0 or not version.startswith(name):
|
2020-03-04 23:53:46 +01:00
|
|
|
log.warning("Unable to run %s, but it was requested. Code: %d; Message: %s", name, code, version)
|
2020-01-30 18:37:08 +01:00
|
|
|
raise FileNotFoundError(f"Unable to locate or run {name}. Please install FFmpeg "
|
|
|
|
f"and ensure that it is callable via PATH or conf.ffmpeg_folder")
|
2020-01-17 00:20:01 +01:00
|
|
|
return version
|
|
|
|
|
2020-01-14 18:43:28 +01:00
|
|
|
async def _verify_ffmpeg_installed(self):
|
|
|
|
if self._ffmpeg_installed:
|
|
|
|
return
|
2020-01-17 00:20:01 +01:00
|
|
|
await self._verify_executable("ffprobe")
|
|
|
|
version = await self._verify_executable("ffmpeg")
|
2020-03-04 01:27:07 +01:00
|
|
|
self._which = shutil.which(os.path.join(self._conf.ffmpeg_folder, "ffmpeg"))
|
2020-01-14 18:43:28 +01:00
|
|
|
self._ffmpeg_installed = True
|
2020-02-03 23:53:27 +01:00
|
|
|
log.debug("Using %s at %s", version.splitlines()[0].split(" Copyright")[0], self._which)
|
2020-01-14 18:43:28 +01:00
|
|
|
|
2020-02-20 22:39:22 +01:00
|
|
|
async def status(self, reset=False, recheck=False):
|
2020-02-03 23:53:27 +01:00
|
|
|
if reset:
|
|
|
|
self._available_encoders = ""
|
|
|
|
self._ffmpeg_installed = False
|
|
|
|
self._which = None
|
2020-02-20 22:39:22 +01:00
|
|
|
if self._checked_ffmpeg and not recheck:
|
|
|
|
installed = self._ffmpeg_installed
|
|
|
|
else:
|
|
|
|
installed = True
|
|
|
|
try:
|
|
|
|
await self._verify_ffmpeg_installed()
|
|
|
|
except FileNotFoundError:
|
|
|
|
installed = False
|
|
|
|
self._checked_ffmpeg = True
|
2020-02-03 23:53:27 +01:00
|
|
|
return {
|
|
|
|
"available": installed,
|
|
|
|
"which": self._which,
|
|
|
|
"analyze_audio_volume": int(self._conf.volume_analysis_time) > 0
|
|
|
|
}
|
2020-01-14 18:43:28 +01:00
|
|
|
|
2020-02-03 23:53:27 +01:00
|
|
|
@staticmethod
|
|
|
|
def _verify_container(scan_data: json):
|
2020-01-14 18:43:28 +01:00
|
|
|
container = scan_data["format"]["format_name"]
|
2020-01-30 18:37:08 +01:00
|
|
|
log.debug(" Detected container is %s", container)
|
2020-02-03 23:53:27 +01:00
|
|
|
if not {"webm", "mp4", "3gp", "ogg"}.intersection(container.split(",")):
|
2020-01-17 00:20:01 +01:00
|
|
|
return "Container format is not in the approved list of WebM, MP4. " \
|
|
|
|
f"Actual: {container} [{scan_data['format']['format_long_name']}]"
|
2020-01-14 18:43:28 +01:00
|
|
|
return ""
|
|
|
|
|
2020-02-03 23:53:27 +01:00
|
|
|
@staticmethod
|
|
|
|
def _verify_video_encoding(scan_data: json):
|
2020-01-14 18:43:28 +01:00
|
|
|
for stream in scan_data["streams"]:
|
|
|
|
if stream["codec_type"] != "video":
|
|
|
|
continue
|
|
|
|
codec = stream["codec_name"]
|
2020-01-30 18:37:08 +01:00
|
|
|
log.debug(" Detected video codec is %s, format is %s", codec, stream["pix_fmt"])
|
2020-02-03 23:53:27 +01:00
|
|
|
if not {"h264", "vp8", "vp9", "av1", "theora"}.intersection(codec.split(",")):
|
2020-01-17 00:20:01 +01:00
|
|
|
return "Video codec is not in the approved list of H264, VP8, VP9, AV1, Theora. " \
|
|
|
|
f"Actual: {codec} [{stream['codec_long_name']}]"
|
2020-01-14 18:43:28 +01:00
|
|
|
|
2020-02-03 23:53:27 +01:00
|
|
|
if "h264" in codec.split(",") and stream["pix_fmt"] != "yuv420p":
|
2020-01-17 00:20:01 +01:00
|
|
|
return "Video codec is H264, but its pixel format does not match the approved yuv420p. " \
|
|
|
|
f"Actual: {stream['pix_fmt']}"
|
2020-01-14 18:43:28 +01:00
|
|
|
|
|
|
|
return ""
|
|
|
|
|
2020-03-04 01:17:32 +01:00
|
|
|
def _verify_bitrate(self, scan_data: json, file_path):
|
|
|
|
bit_rate_max = float(self._conf.video_bitrate_maximum)
|
|
|
|
if bit_rate_max <= 0:
|
2020-01-14 18:43:28 +01:00
|
|
|
return ""
|
|
|
|
|
2020-03-04 01:17:32 +01:00
|
|
|
if "bit_rate" in scan_data["format"]:
|
|
|
|
bit_rate = float(scan_data["format"]["bit_rate"])
|
|
|
|
else:
|
|
|
|
bit_rate = os.stat(file_path).st_size / float(scan_data["format"]["duration"])
|
|
|
|
log.debug(" Detected bitrate is %s Mbps. Allowed is %s Mbps",
|
|
|
|
str(bit_rate / 1000000.0), str(bit_rate_max / 1000000.0))
|
2020-01-14 18:43:28 +01:00
|
|
|
|
2020-03-04 01:17:32 +01:00
|
|
|
if bit_rate > bit_rate_max:
|
|
|
|
return "The bit rate is above the configured maximum. Actual: " \
|
|
|
|
f"{bit_rate / 1000000.0} Mbps; Allowed: {bit_rate_max / 1000000.0} Mbps"
|
2020-01-14 18:43:28 +01:00
|
|
|
|
|
|
|
return ""
|
|
|
|
|
2020-01-17 00:20:01 +01:00
|
|
|
async def _verify_fast_start(self, scan_data: json, video_file):
|
2020-01-14 18:43:28 +01:00
|
|
|
container = scan_data["format"]["format_name"]
|
2020-02-03 23:53:27 +01:00
|
|
|
if {"webm", "ogg"}.intersection(container.split(",")):
|
2020-01-14 18:43:28 +01:00
|
|
|
return ""
|
|
|
|
|
2020-01-17 00:20:01 +01:00
|
|
|
result, _ = await self._execute("ffprobe", f'-v debug "{video_file}"')
|
2020-02-07 21:27:51 +01:00
|
|
|
match = re.search(r"Before avformat_find_stream_info.+?\s+seeks:(\d+)\s+", result)
|
|
|
|
if match and int(match.group(1)) != 0:
|
|
|
|
return "Video stream descriptors are not at the start of the file (the faststart flag was not used)."
|
2020-01-14 18:43:28 +01:00
|
|
|
return ""
|
|
|
|
|
2020-02-03 23:53:27 +01:00
|
|
|
@staticmethod
|
|
|
|
def _verify_audio_encoding(scan_data: json):
|
2020-01-14 18:43:28 +01:00
|
|
|
for stream in scan_data["streams"]:
|
|
|
|
if stream["codec_type"] != "audio":
|
|
|
|
continue
|
|
|
|
codec = stream["codec_name"]
|
2020-01-30 18:37:08 +01:00
|
|
|
log.debug(" Detected audio codec is %s", codec)
|
2020-02-03 23:53:27 +01:00
|
|
|
if not {"aac", "mp3", "flac", "vorbis", "opus"}.intersection(codec.split(",")):
|
2020-01-17 00:20:01 +01:00
|
|
|
return "Audio codec is not in the approved list of AAC, FLAC, MP3, Vorbis, and Opus. " \
|
|
|
|
f"Actual: {codec} [{stream['codec_long_name']}]"
|
2020-01-14 18:43:28 +01:00
|
|
|
|
|
|
|
return ""
|
|
|
|
|
2020-01-17 00:20:01 +01:00
|
|
|
async def _verify_audio_volume(self, seconds, video_file):
|
2020-01-14 18:43:28 +01:00
|
|
|
try:
|
|
|
|
validate_volume = int(seconds) > 0
|
|
|
|
except ValueError:
|
2020-02-03 23:53:27 +01:00
|
|
|
validate_volume = False
|
2020-01-14 18:43:28 +01:00
|
|
|
|
|
|
|
if not validate_volume:
|
|
|
|
return ""
|
|
|
|
|
2020-01-17 00:20:01 +01:00
|
|
|
result, _ = await self._execute("ffmpeg", f'-i "{video_file}" -t {seconds} '
|
|
|
|
f'-af volumedetect -vn -sn -dn -f null "{os.devnull}"')
|
2020-01-14 18:43:28 +01:00
|
|
|
try:
|
|
|
|
mean_volume = float(re.search(r"mean_volume:\s+([-+]?\d*\.\d+|\d+)", result).group(1))
|
|
|
|
max_volume = float(re.search(r"max_volume:\s+([-+]?\d*\.\d+|\d+)", result).group(1))
|
|
|
|
except Exception as e:
|
|
|
|
log.debug(" Failure in volume analysis. Message: %s", str(e))
|
|
|
|
return ""
|
|
|
|
|
|
|
|
if max_volume < -5.0 and mean_volume < -22.0:
|
2020-01-17 00:20:01 +01:00
|
|
|
return "Audio is at least five dB lower than prime. " \
|
|
|
|
f"Actual max: {max_volume}, mean: {mean_volume}"
|
2020-01-14 18:43:28 +01:00
|
|
|
|
2020-01-30 18:37:08 +01:00
|
|
|
log.debug(" Detected audio volume has mean, max of %f, %f dB", mean_volume, max_volume)
|
2020-01-14 18:43:28 +01:00
|
|
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _compute_crf(scan_data):
|
|
|
|
height = 240.0
|
|
|
|
for stream in scan_data["streams"]:
|
|
|
|
if stream["codec_type"] == "video":
|
|
|
|
height = max(height, float(stream["height"]))
|
|
|
|
|
|
|
|
# https://developers.google.com/media/vp9/settings/vod/
|
|
|
|
return int(-0.011 * height + 40)
|
|
|
|
|
2020-03-04 01:17:32 +01:00
|
|
|
def _get_video_scaler(self):
|
|
|
|
return self._conf.video_scaler
|
|
|
|
|
2020-01-14 18:43:28 +01:00
|
|
|
async def _get_video_encoder(self, scan_data):
|
|
|
|
# use what the user said if it's there:
|
|
|
|
# if it's not there, use h264 if we can because it's way faster than the others
|
|
|
|
# if we don't have h264 use vp9; it's fairly compatible even though it's slow
|
|
|
|
|
|
|
|
if not self._available_encoders:
|
|
|
|
self._available_encoders, _ = await self._execute("ffmpeg", "-encoders -v quiet")
|
|
|
|
|
|
|
|
encoder = self._conf.video_encoder.split(" ", 1)[0]
|
2020-01-17 00:20:01 +01:00
|
|
|
if re.search(fr"^\s*V..... {encoder} ", self._available_encoders, re.MULTILINE):
|
2020-01-14 18:43:28 +01:00
|
|
|
return self._conf.video_encoder
|
|
|
|
|
|
|
|
if re.search(r"^\s*V..... libx264 ", self._available_encoders, re.MULTILINE):
|
|
|
|
if encoder:
|
|
|
|
log.warning(" Using libx264 since the requested encoder was unavailable. Requested: %s", encoder)
|
2020-01-17 00:20:01 +01:00
|
|
|
return 'libx264 -crf 19 -vf "format=yuv420p"'
|
2020-01-14 18:43:28 +01:00
|
|
|
|
|
|
|
if not encoder:
|
|
|
|
encoder = "libx264"
|
|
|
|
|
|
|
|
if re.search(r"^\s*V..... libvpx-vp9 ", self._available_encoders, re.MULTILINE):
|
|
|
|
log.warning(" Using libvpx-vp9 since the requested encoder was unavailable. Requested: %s", encoder)
|
|
|
|
crf = self._compute_crf(scan_data)
|
2020-01-17 00:20:01 +01:00
|
|
|
return f"libvpx-vp9 -crf {crf} -b:v 0"
|
2020-01-14 18:43:28 +01:00
|
|
|
|
|
|
|
if re.search(r"^\s*V..... libtheora", self._available_encoders, re.MULTILINE):
|
|
|
|
log.warning(" Using libtheora since the requested encoder was unavailable. Requested: %s", encoder)
|
|
|
|
return "libtheora -q:v 7"
|
|
|
|
|
2020-01-17 00:20:01 +01:00
|
|
|
raise Exception(f"The video encoder is not available. Requested: {encoder}")
|
2020-01-14 18:43:28 +01:00
|
|
|
|
2020-01-17 00:20:01 +01:00
|
|
|
async def _get_audio_encoder(self, extension):
|
|
|
|
# if the video encoding is theora or av1/vp8/vp9 use opus (or fallback to vorbis)
|
|
|
|
# or we don't have a video encoding but we have an ogg or webm container use opus
|
|
|
|
# if we need to use opus/vorbis see if the conf file has it else use our own params
|
2020-01-14 18:43:28 +01:00
|
|
|
# else use the user-set value if it exists
|
|
|
|
# else use aac
|
|
|
|
|
2020-01-17 00:20:01 +01:00
|
|
|
wants_opus = extension != "mp4"
|
2020-01-14 18:43:28 +01:00
|
|
|
if not self._available_encoders:
|
|
|
|
self._available_encoders, _ = await self._execute("ffmpeg", "-encoders -v quiet")
|
|
|
|
|
2020-01-17 00:20:01 +01:00
|
|
|
encoder = self._conf.audio_encoder.split(" ", 1)[0]
|
|
|
|
if wants_opus and 'opus' in encoder:
|
|
|
|
return self._conf.audio_encoder
|
|
|
|
|
2020-01-14 18:43:28 +01:00
|
|
|
if wants_opus and re.search(r"^\s*A..... libopus ", self._available_encoders, re.MULTILINE):
|
|
|
|
return "libopus -b:a 160k"
|
|
|
|
|
2020-01-17 00:20:01 +01:00
|
|
|
if wants_opus and 'vorbis' in encoder:
|
|
|
|
return self._conf.audio_encoder
|
|
|
|
|
2020-01-14 18:43:28 +01:00
|
|
|
if wants_opus and re.search(r"^\s*A..... libvorbis ", self._available_encoders, re.MULTILINE):
|
|
|
|
return "libvorbis -q:a 6"
|
|
|
|
|
2020-01-17 00:20:01 +01:00
|
|
|
if re.search(fr"^\s*A..... {encoder} ", self._available_encoders, re.MULTILINE):
|
2020-01-14 18:43:28 +01:00
|
|
|
return self._conf.audio_encoder
|
|
|
|
|
|
|
|
if re.search(r"^\s*A..... aac ", self._available_encoders, re.MULTILINE):
|
|
|
|
return "aac -b:a 192k"
|
|
|
|
|
2020-01-17 00:20:01 +01:00
|
|
|
raise Exception(f"The audio encoder is not available. Requested: {encoder or 'aac'}")
|
2020-01-14 18:43:28 +01:00
|
|
|
|
2020-01-17 00:20:01 +01:00
|
|
|
async def _get_volume_filter(self):
|
2020-01-14 18:43:28 +01:00
|
|
|
return self._conf.volume_filter if self._conf.volume_filter else "-af loudnorm"
|
|
|
|
|
2020-02-03 23:53:27 +01:00
|
|
|
@staticmethod
|
|
|
|
def _get_best_container_extension(scan_data, video_encoder):
|
2020-01-14 18:43:28 +01:00
|
|
|
# the container is chosen by the video format
|
|
|
|
# if we are theora-encoded, we want ogg
|
|
|
|
# if we are vp8/vp9/av1 we want webm
|
|
|
|
# use mp4 for anything else
|
|
|
|
|
|
|
|
if not video_encoder: # not re-encoding video
|
|
|
|
for stream in scan_data["streams"]:
|
|
|
|
if stream["codec_type"] != "video":
|
|
|
|
continue
|
|
|
|
codec = stream["codec_name"].split(",")
|
2020-02-03 23:53:27 +01:00
|
|
|
if "theora" in codec:
|
2020-02-11 20:23:19 +01:00
|
|
|
return "ogv"
|
2020-02-03 23:53:27 +01:00
|
|
|
if {"vp8", "vp9", "av1"}.intersection(codec):
|
2020-01-14 18:43:28 +01:00
|
|
|
return "webm"
|
|
|
|
|
|
|
|
if "theora" in video_encoder:
|
2020-02-11 20:23:19 +01:00
|
|
|
return "ogv"
|
2020-01-17 00:20:01 +01:00
|
|
|
elif re.search(r"vp[89x]|av1", video_encoder.split(" ", 1)[0]):
|
2020-01-14 18:43:28 +01:00
|
|
|
return "webm"
|
|
|
|
return "mp4"
|
|
|
|
|
2020-01-17 00:20:01 +01:00
|
|
|
async def _get_scan_data(self, validate, file_path):
|
2020-01-14 18:43:28 +01:00
|
|
|
result, _ = await self._execute("ffprobe",
|
2020-01-17 00:20:01 +01:00
|
|
|
f'-v quiet -print_format json -show_format -show_streams "{file_path}"')
|
2020-01-14 18:43:28 +01:00
|
|
|
try:
|
|
|
|
scan_data = json.loads(result)
|
|
|
|
except Exception as e:
|
|
|
|
log.debug("Failure in JSON parsing ffprobe results. Message: %s", str(e))
|
2020-02-11 20:23:19 +01:00
|
|
|
raise ValueError(f'Absent or unreadable video file: {file_path}')
|
2020-01-14 18:43:28 +01:00
|
|
|
|
2020-02-11 20:23:19 +01:00
|
|
|
if "format" not in scan_data or "duration" not in scan_data["format"]:
|
|
|
|
log.debug("Format data is missing from ffprobe results for: %s", file_path)
|
|
|
|
raise ValueError(f'Media file does not appear to contain video content at: {file_path}')
|
|
|
|
|
|
|
|
if float(scan_data["format"]["duration"]) < 0.1:
|
|
|
|
log.debug("Media file appears to be an image: %s", file_path)
|
|
|
|
raise ValueError(f'Assuming image file at: {file_path}')
|
2020-01-14 18:43:28 +01:00
|
|
|
|
2020-01-17 00:20:01 +01:00
|
|
|
return scan_data
|
|
|
|
|
2020-03-04 01:27:07 +01:00
|
|
|
async def verify_or_repair(self, validate, repair, file_path, ignore_non_video=False):
|
2020-01-17 00:20:01 +01:00
|
|
|
if not validate and not repair:
|
|
|
|
return file_path
|
|
|
|
|
|
|
|
await self._verify_ffmpeg_installed()
|
2020-03-04 01:27:07 +01:00
|
|
|
try:
|
|
|
|
scan_data = await self._get_scan_data(validate, file_path)
|
|
|
|
except ValueError:
|
|
|
|
if ignore_non_video:
|
|
|
|
return file_path
|
|
|
|
raise
|
2020-01-17 00:20:01 +01:00
|
|
|
|
|
|
|
fast_start_msg = await self._verify_fast_start(scan_data, file_path)
|
2020-01-14 18:43:28 +01:00
|
|
|
log.debug("Analyzing %s:", file_path)
|
2020-01-17 00:20:01 +01:00
|
|
|
log.debug(" Detected faststart is %s", "false" if fast_start_msg else "true")
|
2020-01-14 18:43:28 +01:00
|
|
|
container_msg = self._verify_container(scan_data)
|
2020-03-04 01:17:32 +01:00
|
|
|
bitrate_msg = self._verify_bitrate(scan_data, file_path)
|
2020-01-14 18:43:28 +01:00
|
|
|
video_msg = self._verify_video_encoding(scan_data)
|
|
|
|
audio_msg = self._verify_audio_encoding(scan_data)
|
2020-01-17 00:20:01 +01:00
|
|
|
volume_msg = await self._verify_audio_volume(self._conf.volume_analysis_time, file_path)
|
|
|
|
messages = [container_msg, bitrate_msg, fast_start_msg, video_msg, audio_msg, volume_msg]
|
2020-01-14 18:43:28 +01:00
|
|
|
|
|
|
|
if not any(messages):
|
|
|
|
return file_path
|
|
|
|
|
|
|
|
if not repair:
|
2020-01-17 01:51:49 +01:00
|
|
|
errors = ["Streamability verification failed:"]
|
|
|
|
errors.extend(filter(None, messages))
|
|
|
|
raise Exception("\n ".join(errors))
|
2020-01-14 18:43:28 +01:00
|
|
|
|
|
|
|
# the plan for transcoding:
|
|
|
|
# we have to re-encode the video if it is in a nonstandard format
|
|
|
|
# we also re-encode if we are h264 but not yuv420p (both errors caught in video_msg)
|
|
|
|
# we also re-encode if our bitrate is too high
|
|
|
|
|
|
|
|
try:
|
2020-01-17 01:51:49 +01:00
|
|
|
transcode_command = [f'-i "{file_path}" -y -c:s copy -c:d copy -c:v']
|
2020-01-14 18:43:28 +01:00
|
|
|
|
|
|
|
video_encoder = ""
|
|
|
|
if video_msg or bitrate_msg:
|
|
|
|
video_encoder = await self._get_video_encoder(scan_data)
|
2020-01-17 01:51:49 +01:00
|
|
|
transcode_command.append(video_encoder)
|
2020-03-04 01:17:32 +01:00
|
|
|
# could do the scaling only if bitrate_msg, but if we're going to the effort to re-encode anyway...
|
|
|
|
transcode_command.append(self._get_video_scaler())
|
2020-01-14 18:43:28 +01:00
|
|
|
else:
|
2020-01-17 01:51:49 +01:00
|
|
|
transcode_command.append("copy")
|
2020-01-14 18:43:28 +01:00
|
|
|
|
2020-01-17 01:51:49 +01:00
|
|
|
transcode_command.append("-movflags +faststart -c:a")
|
2020-01-17 00:20:01 +01:00
|
|
|
path = pathlib.Path(file_path)
|
|
|
|
extension = self._get_best_container_extension(scan_data, video_encoder)
|
2020-01-14 18:43:28 +01:00
|
|
|
|
|
|
|
if audio_msg or volume_msg:
|
2020-01-17 00:20:01 +01:00
|
|
|
audio_encoder = await self._get_audio_encoder(extension)
|
2020-01-17 01:51:49 +01:00
|
|
|
transcode_command.append(audio_encoder)
|
2020-01-14 18:43:28 +01:00
|
|
|
if volume_msg:
|
2020-01-17 00:20:01 +01:00
|
|
|
volume_filter = await self._get_volume_filter()
|
2020-01-17 01:51:49 +01:00
|
|
|
transcode_command.append(volume_filter)
|
2020-01-14 18:43:28 +01:00
|
|
|
else:
|
2020-01-17 01:51:49 +01:00
|
|
|
transcode_command.append("copy")
|
2020-01-14 18:43:28 +01:00
|
|
|
|
|
|
|
# TODO: put it in a temp folder and delete it after we upload?
|
2020-01-17 00:20:01 +01:00
|
|
|
output = path.parent / f"{path.stem}_fixed.{extension}"
|
2020-01-17 01:51:49 +01:00
|
|
|
transcode_command.append(f'"{output}"')
|
2020-01-14 18:43:28 +01:00
|
|
|
|
2020-01-17 01:51:49 +01:00
|
|
|
ffmpeg_command = " ".join(transcode_command)
|
|
|
|
log.info("Proceeding on transcode via: ffmpeg %s", ffmpeg_command)
|
|
|
|
result, code = await self._execute("ffmpeg", ffmpeg_command)
|
2020-01-14 18:43:28 +01:00
|
|
|
if code != 0:
|
2020-01-17 00:20:01 +01:00
|
|
|
raise Exception(f"Failure to complete the transcode command. Output: {result}")
|
2020-01-14 18:43:28 +01:00
|
|
|
except Exception as e:
|
|
|
|
if validate:
|
|
|
|
raise
|
|
|
|
log.info("Unable to transcode %s . Message: %s", file_path, str(e))
|
|
|
|
# TODO: delete partial output file here if it exists?
|
|
|
|
return file_path
|
|
|
|
|
2020-01-31 23:27:32 +01:00
|
|
|
return str(output)
|