mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2024-12-04 20:36:44 +00:00
Add ffmpeg progress tracking to FFmpegFD
Add ffmpeg progress tracking to FFmpegPostProcessor Apply changes from the code review Fix a bug where the subprocess didn't capture any output thus an empty stdout and stderr were sent back Add missing hooks Revert "Add missing hooks" This reverts commit a359c5ea10bb35b965e80801e736f43cdbcf3294. Add support of -ss=132 timestamp format Infer filename from ffmpeg args instead of info_dic Remove redundant parenthesis and switch from to_stodout to to_screen Add info kwarg with multiple files and ffmpeg to track progress Moved format progress function to util Moved format progress function to util Add progress tracking to postprocessing operations Fix typing error Handle self._downloader is None at __init__ Move format progress functions to utils Move format progress functions to utils Handle case where ydl passed is None Handle case where ydl passed is None Handle case where _multiline isn't initialized Handle case where _multiline isn't initialized Fix streams incorrectly returned Fix case where ydl is nested in the downloader Add progress_hook attribute Fix bug after merge Fix import bugs after merge Catch up with upstream Fix merge errors #1 Adapt tests and implementatation for ffmpeg progress tracking args
This commit is contained in:
parent
4a601c9eff
commit
bcec568ea7
|
@ -120,19 +120,22 @@ def test_make_cmd(self):
|
||||||
downloader._call_downloader('test', {**TEST_INFO, 'ext': 'mp4'})
|
downloader._call_downloader('test', {**TEST_INFO, 'ext': 'mp4'})
|
||||||
self.assertEqual(self._args, [
|
self.assertEqual(self._args, [
|
||||||
'ffmpeg', '-y', '-hide_banner', '-i', 'http://www.example.com/',
|
'ffmpeg', '-y', '-hide_banner', '-i', 'http://www.example.com/',
|
||||||
'-c', 'copy', '-f', 'mp4', 'file:test'])
|
'-c', 'copy', '-f', 'mp4', 'file:test', '-progress', 'pipe:1'])
|
||||||
|
|
||||||
# Test cookies arg is added
|
# Test cookies arg is added
|
||||||
ydl.cookiejar.set_cookie(http.cookiejar.Cookie(**TEST_COOKIE))
|
ydl.cookiejar.set_cookie(http.cookiejar.Cookie(**TEST_COOKIE))
|
||||||
downloader._call_downloader('test', {**TEST_INFO, 'ext': 'mp4'})
|
downloader._call_downloader('test', {**TEST_INFO, 'ext': 'mp4'})
|
||||||
self.assertEqual(self._args, [
|
self.assertEqual(self._args, [
|
||||||
'ffmpeg', '-y', '-hide_banner', '-cookies', 'test=ytdlp; path=/; domain=.example.com;\r\n',
|
'ffmpeg', '-y', '-hide_banner', '-cookies',
|
||||||
'-i', 'http://www.example.com/', '-c', 'copy', '-f', 'mp4', 'file:test'])
|
'test=ytdlp; path=/; domain=.example.com;\r\n', '-i',
|
||||||
|
'http://www.example.com/', '-c', 'copy', '-f', 'mp4',
|
||||||
|
'file:test', '-progress', 'pipe:1'])
|
||||||
|
|
||||||
# Test with non-url input (ffmpeg reads from stdin '-' for websockets)
|
# Test with non-url input (ffmpeg reads from stdin '-' for websockets)
|
||||||
downloader._call_downloader('test', {'url': 'x', 'ext': 'mp4'})
|
downloader._call_downloader('test', {'url': 'x', 'ext': 'mp4'})
|
||||||
self.assertEqual(self._args, [
|
self.assertEqual(self._args, [
|
||||||
'ffmpeg', '-y', '-hide_banner', '-i', 'x', '-c', 'copy', '-f', 'mp4', 'file:test'])
|
'ffmpeg', '-y', '-hide_banner', '-i', 'x', '-c', 'copy', '-f',
|
||||||
|
'mp4', 'file:test', '-progress', 'pipe:1'])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
IDENTITY,
|
IDENTITY,
|
||||||
NO_DEFAULT,
|
NO_DEFAULT,
|
||||||
|
FormatProgressInfos,
|
||||||
LockingUnsupportedError,
|
LockingUnsupportedError,
|
||||||
Namespace,
|
Namespace,
|
||||||
RetryManager,
|
RetryManager,
|
||||||
|
@ -25,11 +26,9 @@
|
||||||
format_bytes,
|
format_bytes,
|
||||||
join_nonempty,
|
join_nonempty,
|
||||||
parse_bytes,
|
parse_bytes,
|
||||||
remove_start,
|
|
||||||
sanitize_open,
|
sanitize_open,
|
||||||
shell_quote,
|
shell_quote,
|
||||||
timeconvert,
|
timeconvert,
|
||||||
timetuple_from_msec,
|
|
||||||
try_call,
|
try_call,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -115,56 +114,6 @@ def to_screen(self, *args, **kargs):
|
||||||
def FD_NAME(cls):
|
def FD_NAME(cls):
|
||||||
return re.sub(r'(?<=[a-z])(?=[A-Z])', '_', cls.__name__[:-2]).lower()
|
return re.sub(r'(?<=[a-z])(?=[A-Z])', '_', cls.__name__[:-2]).lower()
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def format_seconds(seconds):
|
|
||||||
if seconds is None:
|
|
||||||
return ' Unknown'
|
|
||||||
time = timetuple_from_msec(seconds * 1000)
|
|
||||||
if time.hours > 99:
|
|
||||||
return '--:--:--'
|
|
||||||
return '%02d:%02d:%02d' % time[:-1]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def format_eta(cls, seconds):
|
|
||||||
return f'{remove_start(cls.format_seconds(seconds), "00:"):>8s}'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def calc_percent(byte_counter, data_len):
|
|
||||||
if data_len is None:
|
|
||||||
return None
|
|
||||||
return float(byte_counter) / float(data_len) * 100.0
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def format_percent(percent):
|
|
||||||
return ' N/A%' if percent is None else f'{percent:>5.1f}%'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def calc_eta(cls, start_or_rate, now_or_remaining, total=NO_DEFAULT, current=NO_DEFAULT):
|
|
||||||
if total is NO_DEFAULT:
|
|
||||||
rate, remaining = start_or_rate, now_or_remaining
|
|
||||||
if None in (rate, remaining):
|
|
||||||
return None
|
|
||||||
return int(float(remaining) / rate)
|
|
||||||
|
|
||||||
start, now = start_or_rate, now_or_remaining
|
|
||||||
if total is None:
|
|
||||||
return None
|
|
||||||
if now is None:
|
|
||||||
now = time.time()
|
|
||||||
rate = cls.calc_speed(start, now, current)
|
|
||||||
return rate and int((float(total) - float(current)) / rate)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def calc_speed(start, now, bytes):
|
|
||||||
dif = now - start
|
|
||||||
if bytes == 0 or dif < 0.001: # One millisecond
|
|
||||||
return None
|
|
||||||
return float(bytes) / dif
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def format_speed(speed):
|
|
||||||
return ' Unknown B/s' if speed is None else f'{format_bytes(speed):>10s}/s'
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format_retries(retries):
|
def format_retries(retries):
|
||||||
return 'inf' if retries == float('inf') else int(retries)
|
return 'inf' if retries == float('inf') else int(retries)
|
||||||
|
@ -343,18 +292,16 @@ def with_fields(*tups, default=''):
|
||||||
return tmpl
|
return tmpl
|
||||||
return default
|
return default
|
||||||
|
|
||||||
_format_bytes = lambda k: f'{format_bytes(s.get(k)):>10s}'
|
|
||||||
|
|
||||||
if s['status'] == 'finished':
|
if s['status'] == 'finished':
|
||||||
if self.params.get('noprogress'):
|
if self.params.get('noprogress'):
|
||||||
self.to_screen('[download] Download completed')
|
self.to_screen('[download] Download completed')
|
||||||
speed = try_call(lambda: s['total_bytes'] / s['elapsed'])
|
speed = try_call(lambda: s['total_bytes'] / s['elapsed'])
|
||||||
s.update({
|
s.update({
|
||||||
'speed': speed,
|
'speed': speed,
|
||||||
'_speed_str': self.format_speed(speed).strip(),
|
'_speed_str': FormatProgressInfos.format_speed(speed).strip(),
|
||||||
'_total_bytes_str': _format_bytes('total_bytes'),
|
'_total_bytes_str': format_bytes(s.get('total_bytes')),
|
||||||
'_elapsed_str': self.format_seconds(s.get('elapsed')),
|
'_elapsed_str': FormatProgressInfos.format_seconds(s.get('elapsed')),
|
||||||
'_percent_str': self.format_percent(100),
|
'_percent_str': FormatProgressInfos.format_percent(100),
|
||||||
})
|
})
|
||||||
self._report_progress_status(s, join_nonempty(
|
self._report_progress_status(s, join_nonempty(
|
||||||
'100%%',
|
'100%%',
|
||||||
|
@ -367,16 +314,16 @@ def with_fields(*tups, default=''):
|
||||||
return
|
return
|
||||||
|
|
||||||
s.update({
|
s.update({
|
||||||
'_eta_str': self.format_eta(s.get('eta')).strip(),
|
'_eta_str': FormatProgressInfos.format_eta(s.get('eta')),
|
||||||
'_speed_str': self.format_speed(s.get('speed')),
|
'_speed_str': FormatProgressInfos.format_speed(s.get('speed')),
|
||||||
'_percent_str': self.format_percent(try_call(
|
'_percent_str': FormatProgressInfos.format_percent(try_call(
|
||||||
lambda: 100 * s['downloaded_bytes'] / s['total_bytes'],
|
lambda: 100 * s['downloaded_bytes'] / s['total_bytes'],
|
||||||
lambda: 100 * s['downloaded_bytes'] / s['total_bytes_estimate'],
|
lambda: 100 * s['downloaded_bytes'] / s['total_bytes_estimate'],
|
||||||
lambda: s['downloaded_bytes'] == 0 and 0)),
|
lambda: s['downloaded_bytes'] == 0 and 0)),
|
||||||
'_total_bytes_str': _format_bytes('total_bytes'),
|
'_total_bytes_str': format_bytes(s.get('total_bytes')),
|
||||||
'_total_bytes_estimate_str': _format_bytes('total_bytes_estimate'),
|
'_total_bytes_estimate_str': format_bytes(s.get('total_bytes_estimate')),
|
||||||
'_downloaded_bytes_str': _format_bytes('downloaded_bytes'),
|
'_downloaded_bytes_str': format_bytes(s.get('downloaded_bytes')),
|
||||||
'_elapsed_str': self.format_seconds(s.get('elapsed')),
|
'_elapsed_str': FormatProgressInfos.format_seconds(s.get('elapsed')),
|
||||||
})
|
})
|
||||||
|
|
||||||
msg_template = with_fields(
|
msg_template = with_fields(
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
@ -11,7 +10,11 @@
|
||||||
from .fragment import FragmentFD
|
from .fragment import FragmentFD
|
||||||
from ..compat import functools
|
from ..compat import functools
|
||||||
from ..networking import Request
|
from ..networking import Request
|
||||||
from ..postprocessor.ffmpeg import EXT_TO_OUT_FORMATS, FFmpegPostProcessor
|
from ..postprocessor.ffmpeg import (
|
||||||
|
EXT_TO_OUT_FORMATS,
|
||||||
|
FFmpegPostProcessor,
|
||||||
|
FFmpegProgressTracker,
|
||||||
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
Popen,
|
Popen,
|
||||||
RetryManager,
|
RetryManager,
|
||||||
|
@ -619,26 +622,23 @@ def _call_downloader(self, tmpfilename, info_dict):
|
||||||
|
|
||||||
args = [encodeArgument(opt) for opt in args]
|
args = [encodeArgument(opt) for opt in args]
|
||||||
args.append(encodeFilename(ffpp._ffmpeg_filename_argument(tmpfilename), True))
|
args.append(encodeFilename(ffpp._ffmpeg_filename_argument(tmpfilename), True))
|
||||||
|
args += ['-progress', 'pipe:1']
|
||||||
self._debug_cmd(args)
|
self._debug_cmd(args)
|
||||||
|
|
||||||
piped = any(fmt['url'] in ('-', 'pipe:') for fmt in selected_formats)
|
piped = any(fmt['url'] in ('-', 'pipe:') for fmt in selected_formats)
|
||||||
with Popen(args, stdin=subprocess.PIPE, env=env) as proc:
|
self._debug_cmd(args)
|
||||||
|
ffmpeg_progress_tracker = FFmpegProgressTracker(info_dict, args, self._ffmpeg_hook)
|
||||||
|
proc = ffmpeg_progress_tracker.ffmpeg_proc
|
||||||
if piped:
|
if piped:
|
||||||
self.on_process_started(proc, proc.stdin)
|
self.on_process_started(proc, proc.stdin)
|
||||||
try:
|
_, _, return_code = ffmpeg_progress_tracker.run_ffmpeg_subprocess()
|
||||||
retval = proc.wait()
|
return return_code
|
||||||
except BaseException as e:
|
|
||||||
# subprocces.run would send the SIGKILL signal to ffmpeg and the
|
def _ffmpeg_hook(self, status, info_dict):
|
||||||
# mp4 file couldn't be played, but if we ask ffmpeg to quit it
|
status['downloaded_bytes'] = status.get('outputted', 0)
|
||||||
# produces a file that is playable (this is mostly useful for live
|
if status.get('status') == 'ffmpeg_running':
|
||||||
# streams). Note that Windows is not affected and produces playable
|
status['status'] = 'downloading'
|
||||||
# files (see https://github.com/ytdl-org/youtube-dl/issues/8300).
|
self._hook_progress(status, info_dict)
|
||||||
if isinstance(e, KeyboardInterrupt) and sys.platform != 'win32' and not piped:
|
|
||||||
proc.communicate_or_kill(b'q')
|
|
||||||
else:
|
|
||||||
proc.kill(timeout=None)
|
|
||||||
raise
|
|
||||||
return retval
|
|
||||||
|
|
||||||
|
|
||||||
class AVconvFD(FFmpegFD):
|
class AVconvFD(FFmpegFD):
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ContentTooShortError,
|
ContentTooShortError,
|
||||||
|
FormatProgressInfos,
|
||||||
RetryManager,
|
RetryManager,
|
||||||
ThrottledDownload,
|
ThrottledDownload,
|
||||||
XAttrMetadataError,
|
XAttrMetadataError,
|
||||||
|
@ -293,11 +294,11 @@ def retry(e):
|
||||||
before = after
|
before = after
|
||||||
|
|
||||||
# Progress message
|
# Progress message
|
||||||
speed = self.calc_speed(start, now, byte_counter - ctx.resume_len)
|
speed = FormatProgressInfos.calc_speed(start, now, byte_counter - ctx.resume_len)
|
||||||
if ctx.data_len is None:
|
if ctx.data_len is None:
|
||||||
eta = None
|
eta = None
|
||||||
else:
|
else:
|
||||||
eta = self.calc_eta(start, time.time(), ctx.data_len - ctx.resume_len, byte_counter - ctx.resume_len)
|
eta = FormatProgressInfos.calc_eta(start, time.time(), ctx.data_len - ctx.resume_len, byte_counter - ctx.resume_len)
|
||||||
|
|
||||||
self._hook_progress({
|
self._hook_progress({
|
||||||
'status': 'downloading',
|
'status': 'downloading',
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
from .common import FileDownloader
|
from .common import FileDownloader
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
FormatProgressInfos,
|
||||||
Popen,
|
Popen,
|
||||||
check_executable,
|
check_executable,
|
||||||
encodeArgument,
|
encodeArgument,
|
||||||
|
@ -50,8 +51,8 @@ def run_rtmpdump(args):
|
||||||
resume_percent = percent
|
resume_percent = percent
|
||||||
resume_downloaded_data_len = downloaded_data_len
|
resume_downloaded_data_len = downloaded_data_len
|
||||||
time_now = time.time()
|
time_now = time.time()
|
||||||
eta = self.calc_eta(start, time_now, 100 - resume_percent, percent - resume_percent)
|
eta = FormatProgressInfos.calc_eta(start, time_now, 100 - resume_percent, percent - resume_percent)
|
||||||
speed = self.calc_speed(start, time_now, downloaded_data_len - resume_downloaded_data_len)
|
speed = FormatProgressInfos.calc_speed(start, time_now, downloaded_data_len - resume_downloaded_data_len)
|
||||||
data_len = None
|
data_len = None
|
||||||
if percent > 0:
|
if percent > 0:
|
||||||
data_len = int(downloaded_data_len * 100 / percent)
|
data_len = int(downloaded_data_len * 100 / percent)
|
||||||
|
|
|
@ -1,15 +1,27 @@
|
||||||
import functools
|
import functools
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from ..minicurses import (
|
||||||
|
BreaklineStatusPrinter,
|
||||||
|
MultilineLogger,
|
||||||
|
MultilinePrinter,
|
||||||
|
QuietMultilinePrinter,
|
||||||
|
)
|
||||||
from ..networking import Request
|
from ..networking import Request
|
||||||
from ..networking.exceptions import HTTPError, network_exceptions
|
from ..networking.exceptions import HTTPError, network_exceptions
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
FormatProgressInfos,
|
||||||
|
Namespace,
|
||||||
PostProcessingError,
|
PostProcessingError,
|
||||||
RetryManager,
|
RetryManager,
|
||||||
_configuration_args,
|
_configuration_args,
|
||||||
deprecation_warning,
|
deprecation_warning,
|
||||||
encodeFilename,
|
encodeFilename,
|
||||||
|
format_bytes,
|
||||||
|
join_nonempty,
|
||||||
|
try_call,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -56,7 +68,9 @@ def __init__(self, downloader=None):
|
||||||
self._progress_hooks = []
|
self._progress_hooks = []
|
||||||
self.add_progress_hook(self.report_progress)
|
self.add_progress_hook(self.report_progress)
|
||||||
self.set_downloader(downloader)
|
self.set_downloader(downloader)
|
||||||
|
self._out_files = self.set_out_files()
|
||||||
self.PP_NAME = self.pp_key()
|
self.PP_NAME = self.pp_key()
|
||||||
|
self._prepare_multiline_status()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def pp_key(cls):
|
def pp_key(cls):
|
||||||
|
@ -102,6 +116,11 @@ def get_param(self, name, default=None, *args, **kwargs):
|
||||||
return self._downloader.params.get(name, default, *args, **kwargs)
|
return self._downloader.params.get(name, default, *args, **kwargs)
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
def set_out_files(self):
|
||||||
|
if not self._downloader:
|
||||||
|
return None
|
||||||
|
return getattr(self._downloader, '_out_files', None) or self._downloader.ydl._out_files
|
||||||
|
|
||||||
def set_downloader(self, downloader):
|
def set_downloader(self, downloader):
|
||||||
"""Sets the downloader for this PP."""
|
"""Sets the downloader for this PP."""
|
||||||
self._downloader = downloader
|
self._downloader = downloader
|
||||||
|
@ -173,25 +192,116 @@ def add_progress_hook(self, ph):
|
||||||
# See YoutubeDl.py (search for postprocessor_hooks) for a description of this interface
|
# See YoutubeDl.py (search for postprocessor_hooks) for a description of this interface
|
||||||
self._progress_hooks.append(ph)
|
self._progress_hooks.append(ph)
|
||||||
|
|
||||||
def report_progress(self, s):
|
def report_destination(self, filename):
|
||||||
s['_default_template'] = '%(postprocessor)s %(status)s' % s
|
"""Report destination filename."""
|
||||||
if not self._downloader:
|
self.to_screen('[processing] Destination: ' + filename)
|
||||||
return
|
|
||||||
|
def _prepare_multiline_status(self, lines=1):
|
||||||
|
if self._downloader:
|
||||||
|
if self._downloader.params.get('noprogress'):
|
||||||
|
self._multiline = QuietMultilinePrinter()
|
||||||
|
elif self._downloader.params.get('logger'):
|
||||||
|
self._multiline = MultilineLogger(self._downloader.params['logger'], lines)
|
||||||
|
elif self._downloader.params.get('progress_with_newline'):
|
||||||
|
self._multiline = BreaklineStatusPrinter(self._downloader._out_files.out, lines)
|
||||||
|
elif hasattr(self._downloader, "_out_files"):
|
||||||
|
self._multiline = MultilinePrinter(self._downloader._out_files.out, lines, not self._downloader.params.get('quiet'))
|
||||||
|
else:
|
||||||
|
self._multiline = MultilinePrinter(sys.stdout, lines, not self._downloader.params.get('quiet'))
|
||||||
|
self._multiline.allow_colors = self._multiline._HAVE_FULLCAP and not self._downloader.params.get('no_color')
|
||||||
|
else:
|
||||||
|
self._multiline = MultilinePrinter(sys.stdout, lines, True)
|
||||||
|
self._multiline.allow_colors = self._multiline._HAVE_FULLCAP
|
||||||
|
|
||||||
|
def _finish_multiline_status(self):
|
||||||
|
self._multiline.end()
|
||||||
|
|
||||||
|
ProgressStyles = Namespace(
|
||||||
|
processed_bytes='light blue',
|
||||||
|
percent='light blue',
|
||||||
|
eta='yellow',
|
||||||
|
speed='green',
|
||||||
|
elapsed='bold white',
|
||||||
|
total_bytes='',
|
||||||
|
total_bytes_estimate='',
|
||||||
|
)
|
||||||
|
|
||||||
|
def _report_progress_status(self, s, default_template):
|
||||||
|
for name, style in self.ProgressStyles.items_:
|
||||||
|
name = f'_{name}_str'
|
||||||
|
if name not in s:
|
||||||
|
continue
|
||||||
|
s[name] = self._format_progress(s[name], style)
|
||||||
|
s['_default_template'] = default_template % s
|
||||||
|
|
||||||
progress_dict = s.copy()
|
progress_dict = s.copy()
|
||||||
progress_dict.pop('info_dict')
|
progress_dict.pop('info_dict')
|
||||||
progress_dict = {'info': s['info_dict'], 'progress': progress_dict}
|
progress_dict = {'info': s['info_dict'], 'progress': progress_dict}
|
||||||
|
|
||||||
progress_template = self.get_param('progress_template', {})
|
progress_template = self._downloader.params.get('progress_template', {})
|
||||||
tmpl = progress_template.get('postprocess')
|
self._multiline.print_at_line(self._downloader.evaluate_outtmpl(
|
||||||
if tmpl:
|
progress_template.get('process') or '[processing] %(progress._default_template)s',
|
||||||
self._downloader.to_screen(
|
progress_dict), s.get('progress_idx') or 0)
|
||||||
self._downloader.evaluate_outtmpl(tmpl, progress_dict), quiet=False)
|
|
||||||
|
|
||||||
self._downloader.to_console_title(self._downloader.evaluate_outtmpl(
|
self._downloader.to_console_title(self._downloader.evaluate_outtmpl(
|
||||||
progress_template.get('postprocess-title') or 'yt-dlp %(progress._default_template)s',
|
progress_template.get('download-title') or 'yt-dlp %(progress._default_template)s',
|
||||||
progress_dict))
|
progress_dict))
|
||||||
|
|
||||||
|
def _format_progress(self, *args, **kwargs):
|
||||||
|
return self._downloader._format_text(
|
||||||
|
self._multiline.stream, self._multiline.allow_colors, *args, **kwargs)
|
||||||
|
|
||||||
|
def report_progress(self, s):
|
||||||
|
def with_fields(*tups, default=''):
|
||||||
|
for *fields, tmpl in tups:
|
||||||
|
if all(s.get(f) is not None for f in fields):
|
||||||
|
return tmpl
|
||||||
|
return default
|
||||||
|
|
||||||
|
if not self._downloader:
|
||||||
|
return
|
||||||
|
|
||||||
|
if s['status'] == 'finished':
|
||||||
|
if self._downloader.params.get('noprogress'):
|
||||||
|
self.to_screen('[processing] Download completed')
|
||||||
|
speed = try_call(lambda: s['total_bytes'] / s['elapsed'])
|
||||||
|
s.update({
|
||||||
|
'speed': speed,
|
||||||
|
'_speed_str': FormatProgressInfos.format_speed(speed).strip(),
|
||||||
|
'_total_bytes_str': format_bytes(s.get('total_bytes')),
|
||||||
|
'_elapsed_str': FormatProgressInfos.format_seconds(s.get('elapsed')),
|
||||||
|
'_percent_str': FormatProgressInfos.format_percent(100),
|
||||||
|
})
|
||||||
|
self._report_progress_status(s, join_nonempty(
|
||||||
|
'100%%',
|
||||||
|
with_fields(('total_bytes', 'of %(_total_bytes_str)s')),
|
||||||
|
with_fields(('elapsed', 'in %(_elapsed_str)s')),
|
||||||
|
with_fields(('speed', 'at %(_speed_str)s')),
|
||||||
|
delim=' '))
|
||||||
|
|
||||||
|
if s['status'] != 'processing':
|
||||||
|
return
|
||||||
|
|
||||||
|
s.update({
|
||||||
|
'_eta_str': FormatProgressInfos.format_eta(s.get('eta')),
|
||||||
|
'_speed_str': FormatProgressInfos.format_speed(s.get('speed')),
|
||||||
|
'_percent_str': FormatProgressInfos.format_percent(try_call(
|
||||||
|
lambda: 100 * s['processed_bytes'] / s['total_bytes'],
|
||||||
|
lambda: 100 * s['processed_bytes'] / s['total_bytes_estimate'],
|
||||||
|
lambda: s['processed_bytes'] == 0 and 0)),
|
||||||
|
'_total_bytes_str': format_bytes(s.get('total_bytes')),
|
||||||
|
'_total_bytes_estimate_str': format_bytes(s.get('total_bytes_estimate')),
|
||||||
|
'_processed_bytes_str': format_bytes(s.get('processed_bytes')),
|
||||||
|
'_elapsed_str': FormatProgressInfos.format_seconds(s.get('elapsed')),
|
||||||
|
})
|
||||||
|
|
||||||
|
msg_template = with_fields(
|
||||||
|
('total_bytes', '%(_percent_str)s of %(_total_bytes_str)s at %(_speed_str)s ETA %(_eta_str)s'),
|
||||||
|
('processed_bytes', 'elapsed', '%(_processed_bytes_str)s at %(_speed_str)s (%(_elapsed_str)s)'),
|
||||||
|
('processed_bytes', '%(_processed_bytes_str)s at %(_speed_str)s'),
|
||||||
|
default='%(_percent_str)s at %(_speed_str)s ETA %(_eta_str)s')
|
||||||
|
|
||||||
|
self._report_progress_status(s, msg_template)
|
||||||
|
|
||||||
def _retry_download(self, err, count, retries):
|
def _retry_download(self, err, count, retries):
|
||||||
# While this is not an extractor, it behaves similar to one and
|
# While this is not an extractor, it behaves similar to one and
|
||||||
# so obey extractor_retries and "--retry-sleep extractor"
|
# so obey extractor_retries and "--retry-sleep extractor"
|
||||||
|
|
|
@ -5,7 +5,10 @@
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
|
from queue import Queue
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
from .common import PostProcessor
|
from .common import PostProcessor
|
||||||
from ..compat import functools, imghdr
|
from ..compat import functools, imghdr
|
||||||
|
@ -23,6 +26,7 @@
|
||||||
encodeFilename,
|
encodeFilename,
|
||||||
filter_dict,
|
filter_dict,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
|
int_or_none,
|
||||||
is_outdated_version,
|
is_outdated_version,
|
||||||
orderedSet,
|
orderedSet,
|
||||||
prepend_extension,
|
prepend_extension,
|
||||||
|
@ -326,11 +330,9 @@ def _duration_mismatch(self, d1, d2, tolerance=2):
|
||||||
return abs(d1 - d2) > tolerance
|
return abs(d1 - d2) > tolerance
|
||||||
|
|
||||||
def run_ffmpeg_multiple_files(self, input_paths, out_path, opts, **kwargs):
|
def run_ffmpeg_multiple_files(self, input_paths, out_path, opts, **kwargs):
|
||||||
return self.real_run_ffmpeg(
|
return self.real_run_ffmpeg([(path, []) for path in input_paths], [(out_path, opts)], **kwargs)
|
||||||
[(path, []) for path in input_paths],
|
|
||||||
[(out_path, opts)], **kwargs)
|
|
||||||
|
|
||||||
def real_run_ffmpeg(self, input_path_opts, output_path_opts, *, expected_retcodes=(0,)):
|
def real_run_ffmpeg(self, input_path_opts, output_path_opts, *, expected_retcodes=(0,), info_dict=None):
|
||||||
self.check_version()
|
self.check_version()
|
||||||
|
|
||||||
oldest_mtime = min(
|
oldest_mtime = min(
|
||||||
|
@ -350,19 +352,20 @@ def make_args(file, args, name, number):
|
||||||
args += self._configuration_args(self.basename, keys)
|
args += self._configuration_args(self.basename, keys)
|
||||||
if name == 'i':
|
if name == 'i':
|
||||||
args.append('-i')
|
args.append('-i')
|
||||||
return (
|
return [encodeArgument(arg) for arg in args] + [encodeFilename(self._ffmpeg_filename_argument(file), True)]
|
||||||
[encodeArgument(arg) for arg in args]
|
|
||||||
+ [encodeFilename(self._ffmpeg_filename_argument(file), True)])
|
|
||||||
|
|
||||||
for arg_type, path_opts in (('i', input_path_opts), ('o', output_path_opts)):
|
for arg_type, path_opts in (('i', input_path_opts), ('o', output_path_opts)):
|
||||||
cmd += itertools.chain.from_iterable(
|
cmd += itertools.chain.from_iterable(
|
||||||
make_args(path, list(opts), arg_type, i + 1)
|
make_args(path, list(opts), arg_type, i + 1)
|
||||||
for i, (path, opts) in enumerate(path_opts) if path)
|
for i, (path, opts) in enumerate(path_opts) if path)
|
||||||
|
|
||||||
|
cmd += ['-progress', 'pipe:1']
|
||||||
self.write_debug('ffmpeg command line: %s' % shell_quote(cmd))
|
self.write_debug('ffmpeg command line: %s' % shell_quote(cmd))
|
||||||
_, stderr, returncode = Popen.run(
|
|
||||||
cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
|
ffmpeg_progress_tracker = FFmpegProgressTracker(info_dict, cmd, self._ffmpeg_hook, self._downloader)
|
||||||
if returncode not in variadic(expected_retcodes):
|
_, stderr, return_code = ffmpeg_progress_tracker.run_ffmpeg_subprocess()
|
||||||
|
if return_code not in variadic(expected_retcodes):
|
||||||
|
stderr = stderr.strip()
|
||||||
self.write_debug(stderr)
|
self.write_debug(stderr)
|
||||||
raise FFmpegPostProcessorError(stderr.strip().splitlines()[-1])
|
raise FFmpegPostProcessorError(stderr.strip().splitlines()[-1])
|
||||||
for out_path, _ in output_path_opts:
|
for out_path, _ in output_path_opts:
|
||||||
|
@ -370,7 +373,7 @@ def make_args(file, args, name, number):
|
||||||
self.try_utime(out_path, oldest_mtime, oldest_mtime)
|
self.try_utime(out_path, oldest_mtime, oldest_mtime)
|
||||||
return stderr
|
return stderr
|
||||||
|
|
||||||
def run_ffmpeg(self, path, out_path, opts, **kwargs):
|
def run_ffmpeg(self, path, out_path, opts, informations=None, **kwargs):
|
||||||
return self.run_ffmpeg_multiple_files([path], out_path, opts, **kwargs)
|
return self.run_ffmpeg_multiple_files([path], out_path, opts, **kwargs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -435,6 +438,12 @@ def _concat_spec(cls, in_files, concat_opts=None):
|
||||||
if directive in opts:
|
if directive in opts:
|
||||||
yield f'{directive} {opts[directive]}\n'
|
yield f'{directive} {opts[directive]}\n'
|
||||||
|
|
||||||
|
def _ffmpeg_hook(self, status, info_dict):
|
||||||
|
status['processed_bytes'] = status.get('outputted', 0)
|
||||||
|
if status.get('status') == 'ffmpeg_running':
|
||||||
|
status['status'] = 'processing'
|
||||||
|
self._hook_progress(status, info_dict)
|
||||||
|
|
||||||
|
|
||||||
class FFmpegExtractAudioPP(FFmpegPostProcessor):
|
class FFmpegExtractAudioPP(FFmpegPostProcessor):
|
||||||
COMMON_AUDIO_EXTS = MEDIA_EXTENSIONS.common_audio + ('wma', )
|
COMMON_AUDIO_EXTS = MEDIA_EXTENSIONS.common_audio + ('wma', )
|
||||||
|
@ -469,14 +478,14 @@ def _quality_args(self, codec):
|
||||||
return ['-vbr', f'{int(q)}']
|
return ['-vbr', f'{int(q)}']
|
||||||
return ['-q:a', f'{q}']
|
return ['-q:a', f'{q}']
|
||||||
|
|
||||||
def run_ffmpeg(self, path, out_path, codec, more_opts):
|
def run_ffmpeg(self, path, out_path, codec, more_opts, informations=None):
|
||||||
if codec is None:
|
if codec is None:
|
||||||
acodec_opts = []
|
acodec_opts = []
|
||||||
else:
|
else:
|
||||||
acodec_opts = ['-acodec', codec]
|
acodec_opts = ['-acodec', codec]
|
||||||
opts = ['-vn'] + acodec_opts + more_opts
|
opts = ['-vn'] + acodec_opts + more_opts
|
||||||
try:
|
try:
|
||||||
FFmpegPostProcessor.run_ffmpeg(self, path, out_path, opts)
|
FFmpegPostProcessor.run_ffmpeg(self, path, out_path, opts, informations)
|
||||||
except FFmpegPostProcessorError as err:
|
except FFmpegPostProcessorError as err:
|
||||||
raise PostProcessingError(f'audio conversion failed: {err.msg}')
|
raise PostProcessingError(f'audio conversion failed: {err.msg}')
|
||||||
|
|
||||||
|
@ -527,7 +536,7 @@ def run(self, information):
|
||||||
return [], information
|
return [], information
|
||||||
|
|
||||||
self.to_screen(f'Destination: {new_path}')
|
self.to_screen(f'Destination: {new_path}')
|
||||||
self.run_ffmpeg(path, temp_path, acodec, more_opts)
|
self.run_ffmpeg(path, temp_path, acodec, more_opts, information)
|
||||||
|
|
||||||
os.replace(path, orig_path)
|
os.replace(path, orig_path)
|
||||||
os.replace(temp_path, new_path)
|
os.replace(temp_path, new_path)
|
||||||
|
@ -570,7 +579,7 @@ def run(self, info):
|
||||||
|
|
||||||
outpath = replace_extension(filename, target_ext, source_ext)
|
outpath = replace_extension(filename, target_ext, source_ext)
|
||||||
self.to_screen(f'{self._ACTION.title()} video from {source_ext} to {target_ext}; Destination: {outpath}')
|
self.to_screen(f'{self._ACTION.title()} video from {source_ext} to {target_ext}; Destination: {outpath}')
|
||||||
self.run_ffmpeg(filename, outpath, self._options(target_ext))
|
self.run_ffmpeg(filename, outpath, self._options(target_ext), info)
|
||||||
|
|
||||||
info['filepath'] = outpath
|
info['filepath'] = outpath
|
||||||
info['format'] = info['ext'] = target_ext
|
info['format'] = info['ext'] = target_ext
|
||||||
|
@ -834,7 +843,7 @@ def run(self, info):
|
||||||
if fmt.get('vcodec') != 'none':
|
if fmt.get('vcodec') != 'none':
|
||||||
args.extend(['-map', '%u:v:0' % (i)])
|
args.extend(['-map', '%u:v:0' % (i)])
|
||||||
self.to_screen('Merging formats into "%s"' % filename)
|
self.to_screen('Merging formats into "%s"' % filename)
|
||||||
self.run_ffmpeg_multiple_files(info['__files_to_merge'], temp_filename, args)
|
self.run_ffmpeg_multiple_files(info['__files_to_merge'], temp_filename, args, info_dict=info)
|
||||||
os.rename(encodeFilename(temp_filename), encodeFilename(filename))
|
os.rename(encodeFilename(temp_filename), encodeFilename(filename))
|
||||||
return info['__files_to_merge'], info
|
return info['__files_to_merge'], info
|
||||||
|
|
||||||
|
@ -1005,7 +1014,7 @@ def run(self, info):
|
||||||
else:
|
else:
|
||||||
sub_filenames.append(srt_file)
|
sub_filenames.append(srt_file)
|
||||||
|
|
||||||
self.run_ffmpeg(old_file, new_file, ['-f', new_format])
|
self.run_ffmpeg(old_file, new_file, ['-f', new_format], info)
|
||||||
|
|
||||||
with open(new_file, encoding='utf-8') as f:
|
with open(new_file, encoding='utf-8') as f:
|
||||||
subs[lang] = {
|
subs[lang] = {
|
||||||
|
@ -1188,3 +1197,237 @@ def run(self, info):
|
||||||
'ext': ie_copy['ext'],
|
'ext': ie_copy['ext'],
|
||||||
}]
|
}]
|
||||||
return files_to_delete, info
|
return files_to_delete, info
|
||||||
|
|
||||||
|
|
||||||
|
class FFmpegProgressTracker:
|
||||||
|
def __init__(self, info_dict, ffmpeg_args, hook_progress, ydl=None):
|
||||||
|
self.ydl = ydl
|
||||||
|
self._info_dict = info_dict
|
||||||
|
self._ffmpeg_args = ffmpeg_args
|
||||||
|
self._hook_progress = hook_progress
|
||||||
|
self._stdout_queue, self._stderr_queue = Queue(), Queue()
|
||||||
|
self._streams, self._stderr_buffer, self._stdout_buffer = ['', ''], '', ''
|
||||||
|
self._progress_pattern = re.compile(r'''(?x)
|
||||||
|
(?:
|
||||||
|
frame=\s(?P<frame>\S+)\n
|
||||||
|
fps=\s(?P<fps>\S+)\n
|
||||||
|
stream\d+_\d+_q=\s(?P<stream_d_d_q>\S+)\n
|
||||||
|
)?
|
||||||
|
bitrate=\s(?P<bitrate>\S+)\n
|
||||||
|
total_size=\s(?P<total_size>\S+)\n
|
||||||
|
out_time_us=\s(?P<out_time_us>\S+)\n
|
||||||
|
out_time_ms=\s(?P<out_time_ms>\S+)\n
|
||||||
|
out_time=\s(?P<out_time>\S+)\n
|
||||||
|
dup_frames=\s(?P<dup_frames>\S+)\n
|
||||||
|
drop_frames=\s(?P<drop_frames>\S+)\n
|
||||||
|
speed=\s(?P<speed>\S+)\n
|
||||||
|
progress=\s(?P<progress>\S+)
|
||||||
|
''')
|
||||||
|
|
||||||
|
if self.ydl:
|
||||||
|
self.ydl.write_debug(f'ffmpeg command line: {shell_quote(self._ffmpeg_args)}')
|
||||||
|
self.ffmpeg_proc = Popen(self._ffmpeg_args, universal_newlines=True,
|
||||||
|
encoding='utf8', stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
self._start_time = time.time()
|
||||||
|
|
||||||
|
def trigger_progress_hook(self, dct):
|
||||||
|
self._status.update(dct)
|
||||||
|
self._hook_progress(self._status, self._info_dict)
|
||||||
|
|
||||||
|
def run_ffmpeg_subprocess(self):
|
||||||
|
if self._info_dict and self.ydl:
|
||||||
|
return self._track_ffmpeg_progress()
|
||||||
|
return self._run_ffmpeg_without_progress_tracking()
|
||||||
|
|
||||||
|
def _run_ffmpeg_without_progress_tracking(self):
|
||||||
|
"""Simply run ffmpeg and only care about the last stderr, stdout and the retcode"""
|
||||||
|
stdout, stderr = self.ffmpeg_proc.communicate_or_kill()
|
||||||
|
retcode = self.ffmpeg_proc.returncode
|
||||||
|
return stdout, stderr, retcode
|
||||||
|
|
||||||
|
def _track_ffmpeg_progress(self):
|
||||||
|
""" Track ffmpeg progress in a non blocking way using queues"""
|
||||||
|
self._start_time = time.time()
|
||||||
|
# args needed to track ffmpeg progress from stdout
|
||||||
|
self._duration_to_track, self._total_duration = self._compute_duration_to_track()
|
||||||
|
self._total_filesize = self._compute_total_filesize(self._duration_to_track, self._total_duration)
|
||||||
|
self._status = {
|
||||||
|
'filename': self._ffmpeg_args[-3].split(":")[-1],
|
||||||
|
'status': 'ffmpeg_running',
|
||||||
|
'total_bytes': self._total_filesize,
|
||||||
|
'elapsed': 0,
|
||||||
|
'outputted': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
out_listener = Thread(
|
||||||
|
target=self._enqueue_lines,
|
||||||
|
args=(self.ffmpeg_proc.stdout, self._stdout_queue),
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
err_listener = Thread(
|
||||||
|
target=self._enqueue_lines,
|
||||||
|
args=(self.ffmpeg_proc.stderr, self._stderr_queue),
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
out_listener.start()
|
||||||
|
err_listener.start()
|
||||||
|
|
||||||
|
retcode = self._wait_for_ffmpeg()
|
||||||
|
|
||||||
|
self._status.update({
|
||||||
|
'status': 'finished',
|
||||||
|
'outputted': self._total_filesize
|
||||||
|
})
|
||||||
|
time.sleep(.5) # Needed if ffmpeg didn't release the file in time for yt-dlp to change its name
|
||||||
|
return self._streams[0], self._streams[1], retcode
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _enqueue_lines(out, queue):
|
||||||
|
for line in iter(out.readline, ''):
|
||||||
|
queue.put(line.rstrip())
|
||||||
|
out.close()
|
||||||
|
|
||||||
|
def _save_stream(self, lines, to_stderr=False):
|
||||||
|
if not lines:
|
||||||
|
return
|
||||||
|
self._streams[to_stderr] += lines
|
||||||
|
|
||||||
|
self.ydl.to_screen('\r', skip_eol=True)
|
||||||
|
for msg in lines.splitlines():
|
||||||
|
if msg.strip():
|
||||||
|
self.ydl.write_debug(f'ffmpeg: {msg}')
|
||||||
|
|
||||||
|
def _handle_lines(self):
|
||||||
|
if not self._stdout_queue.empty():
|
||||||
|
stdout_line = self._stdout_queue.get_nowait()
|
||||||
|
self._stdout_buffer += stdout_line + '\n'
|
||||||
|
self._parse_ffmpeg_output()
|
||||||
|
|
||||||
|
if not self._stderr_queue.empty():
|
||||||
|
stderr_line = self._stderr_queue.get_nowait()
|
||||||
|
self._stderr_buffer += stderr_line
|
||||||
|
|
||||||
|
def _wait_for_ffmpeg(self):
|
||||||
|
retcode = self.ffmpeg_proc.poll()
|
||||||
|
while retcode is None:
|
||||||
|
time.sleep(.01)
|
||||||
|
self._handle_lines()
|
||||||
|
self._status.update({
|
||||||
|
'elapsed': time.time() - self._start_time
|
||||||
|
})
|
||||||
|
self._hook_progress(self._status, self._info_dict)
|
||||||
|
retcode = self.ffmpeg_proc.poll()
|
||||||
|
return retcode
|
||||||
|
|
||||||
|
def _parse_ffmpeg_output(self):
|
||||||
|
ffmpeg_prog_infos = re.match(self._progress_pattern, self._stdout_buffer)
|
||||||
|
if not ffmpeg_prog_infos:
|
||||||
|
return
|
||||||
|
eta_seconds = self._compute_eta(ffmpeg_prog_infos, self._duration_to_track)
|
||||||
|
bitrate_int = self._compute_bitrate(ffmpeg_prog_infos.group('bitrate'))
|
||||||
|
# Not using ffmpeg 'total_size' value as it's imprecise and gives progress percentage over 100
|
||||||
|
out_time_second = int_or_none(ffmpeg_prog_infos.group('out_time_us')) // 1_000_000
|
||||||
|
try:
|
||||||
|
outputted_bytes_int = int_or_none(out_time_second / self._duration_to_track * self._total_filesize)
|
||||||
|
except ZeroDivisionError:
|
||||||
|
outputted_bytes_int = 0
|
||||||
|
self._status.update({
|
||||||
|
'outputted': outputted_bytes_int,
|
||||||
|
'speed': bitrate_int,
|
||||||
|
'eta': eta_seconds,
|
||||||
|
})
|
||||||
|
self._hook_progress(self._status, self._info_dict)
|
||||||
|
self._stderr_buffer = re.sub(r'=\s+', '=', self._stderr_buffer)
|
||||||
|
print(self._stdout_buffer, file=sys.stdout, end='')
|
||||||
|
print(self._stderr_buffer, file=sys.stderr)
|
||||||
|
self._stdout_buffer = ''
|
||||||
|
self._stderr_buffer = ''
|
||||||
|
|
||||||
|
def _compute_total_filesize(self, duration_to_track, total_duration):
|
||||||
|
if not total_duration:
|
||||||
|
return 0
|
||||||
|
filesize = self._info_dict.get('filesize')
|
||||||
|
if not filesize:
|
||||||
|
filesize = self._info_dict.get('filesize_approx', 0)
|
||||||
|
total_filesize = filesize * duration_to_track // total_duration
|
||||||
|
return total_filesize
|
||||||
|
|
||||||
|
def _compute_duration_to_track(self):
|
||||||
|
duration = self._info_dict.get('duration')
|
||||||
|
if not duration:
|
||||||
|
return 0, 0
|
||||||
|
|
||||||
|
start_time, end_time = 0, duration
|
||||||
|
for i, arg in enumerate(self._ffmpeg_args[:-1]):
|
||||||
|
next_arg_is_a_timestamp = re.match(r'(?P<at>(-ss|-sseof|-to))', arg)
|
||||||
|
this_arg_is_a_timestamp = re.match(r'(?P<at>(-ss|-sseof|-to))=(?P<timestamp>\d+)', arg)
|
||||||
|
if not (next_arg_is_a_timestamp or this_arg_is_a_timestamp):
|
||||||
|
continue
|
||||||
|
elif next_arg_is_a_timestamp:
|
||||||
|
timestamp_seconds = self.ffmpeg_time_string_to_seconds(self._ffmpeg_args[i + 1])
|
||||||
|
else:
|
||||||
|
timestamp_seconds = self.ffmpeg_time_string_to_seconds(this_arg_is_a_timestamp.group('timestamp'))
|
||||||
|
if next_arg_is_a_timestamp.group('at') == '-ss':
|
||||||
|
start_time = timestamp_seconds
|
||||||
|
elif next_arg_is_a_timestamp.group('at') == '-sseof':
|
||||||
|
start_time = end_time - timestamp_seconds
|
||||||
|
elif next_arg_is_a_timestamp.group('at') == '-to':
|
||||||
|
end_time = timestamp_seconds
|
||||||
|
|
||||||
|
duration_to_track = end_time - start_time
|
||||||
|
if duration_to_track >= 0:
|
||||||
|
return duration_to_track, duration
|
||||||
|
return 0, duration
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _compute_eta(ffmpeg_prog_infos, duration_to_track):
|
||||||
|
try:
|
||||||
|
speed = float_or_none(ffmpeg_prog_infos.group('speed')[:-1])
|
||||||
|
out_time_second = int_or_none(ffmpeg_prog_infos.group('out_time_us')) // 1_000_000
|
||||||
|
eta_seconds = (duration_to_track - out_time_second) // speed
|
||||||
|
except (TypeError, ZeroDivisionError):
|
||||||
|
eta_seconds = 0
|
||||||
|
return eta_seconds
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def ffmpeg_time_string_to_seconds(time_string):
|
||||||
|
ffmpeg_time_seconds = 0
|
||||||
|
hms_parsed = re.match(r"((?P<Hour>\d+):)?((?P<Minute>\d+):)?(?P<Second>\d+)(\.(?P<float>\d+))?", time_string)
|
||||||
|
smu_parse = re.match(r"(?P<Time>\d+)(?P<Unit>[mu]?s)", time_string)
|
||||||
|
if hms_parsed:
|
||||||
|
if hms_parsed.group('Hour'):
|
||||||
|
ffmpeg_time_seconds += 3600 * int_or_none(hms_parsed.group('Hour'))
|
||||||
|
if hms_parsed.group('Minute'):
|
||||||
|
ffmpeg_time_seconds += 60 * int_or_none(hms_parsed.group('Minute'))
|
||||||
|
ffmpeg_time_seconds += int_or_none(hms_parsed.group('Second'))
|
||||||
|
if hms_parsed.group('float'):
|
||||||
|
float_part = hms_parsed.group('float')
|
||||||
|
ffmpeg_time_seconds += int_or_none(float_part) / (10 ** len(float_part))
|
||||||
|
elif smu_parse:
|
||||||
|
ffmpeg_time_seconds = int_or_none(smu_parse.group('Time'))
|
||||||
|
prefix_and_unit = smu_parse.group('Unit')
|
||||||
|
if prefix_and_unit == 'ms':
|
||||||
|
ffmpeg_time_seconds /= 1_000
|
||||||
|
elif prefix_and_unit == 'us':
|
||||||
|
ffmpeg_time_seconds /= 1_000_000
|
||||||
|
return ffmpeg_time_seconds
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _compute_bitrate(bitrate):
|
||||||
|
bitrate_str = re.match(r"(?P<Integer>\d+)(\.(?P<float>\d+))?(?P<Prefix>[gmk])?bits/s", bitrate)
|
||||||
|
try:
|
||||||
|
no_prefix_bitrate = int_or_none(bitrate_str.group('Integer'))
|
||||||
|
if bitrate_str.group('float'):
|
||||||
|
float_part = bitrate_str.group('float')
|
||||||
|
no_prefix_bitrate += int_or_none(float_part) / (10 ** len(float_part))
|
||||||
|
if bitrate_str.group('Prefix'):
|
||||||
|
unit_prefix = bitrate_str.group('Prefix')
|
||||||
|
if unit_prefix == 'g':
|
||||||
|
no_prefix_bitrate *= 1_000_000_000
|
||||||
|
elif unit_prefix == 'm':
|
||||||
|
no_prefix_bitrate *= 1_000_000
|
||||||
|
elif unit_prefix == 'k':
|
||||||
|
no_prefix_bitrate *= 1_000
|
||||||
|
except (TypeError, AttributeError):
|
||||||
|
return 0
|
||||||
|
return no_prefix_bitrate
|
||||||
|
|
|
@ -5500,3 +5500,51 @@ def stdout(self, message):
|
||||||
def stderr(self, message):
|
def stderr(self, message):
|
||||||
if self._ydl:
|
if self._ydl:
|
||||||
self._ydl.to_stderr(message)
|
self._ydl.to_stderr(message)
|
||||||
|
|
||||||
|
|
||||||
|
class FormatProgressInfos:
|
||||||
|
@staticmethod
|
||||||
|
def format_seconds(seconds):
|
||||||
|
if seconds is None:
|
||||||
|
return ' Unknown'
|
||||||
|
time = timetuple_from_msec(seconds * 1000)
|
||||||
|
if time.hours > 99:
|
||||||
|
return '--:--:--'
|
||||||
|
if not time.hours:
|
||||||
|
return '%02d:%02d' % time[1:-1]
|
||||||
|
return '%02d:%02d:%02d' % time[:-1]
|
||||||
|
|
||||||
|
format_eta = format_seconds
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def calc_percent(byte_counter, data_len):
|
||||||
|
if data_len is None:
|
||||||
|
return None
|
||||||
|
return float(byte_counter) / float(data_len) * 100.0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_percent(percent):
|
||||||
|
return ' N/A%' if percent is None else f'{percent:>5.1f}%'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def calc_eta(start, now, total, current):
|
||||||
|
if total is None:
|
||||||
|
return None
|
||||||
|
if now is None:
|
||||||
|
now = time.time()
|
||||||
|
dif = now - start
|
||||||
|
if current == 0 or dif < 0.001: # One millisecond
|
||||||
|
return None
|
||||||
|
rate = float(current) / dif
|
||||||
|
return int((float(total) - float(current)) / rate)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def calc_speed(start, now, bytes):
|
||||||
|
dif = now - start
|
||||||
|
if bytes == 0 or dif < 0.001: # One millisecond
|
||||||
|
return None
|
||||||
|
return float(bytes) / dif
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_speed(speed):
|
||||||
|
return ' Unknown B/s' if speed is None else f'{format_bytes(speed):>10s}/s'
|
||||||
|
|
Loading…
Reference in a new issue