mirror of
https://github.com/yt-dlp/yt-dlp
synced 2025-02-13 20:48:33 +01: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
8 changed files with 473 additions and 120 deletions
|
@ -120,19 +120,22 @@ class TestFFmpegFD(unittest.TestCase):
|
||||||
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 ..minicurses import (
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
IDENTITY,
|
IDENTITY,
|
||||||
NO_DEFAULT,
|
NO_DEFAULT,
|
||||||
|
FormatProgressInfos,
|
||||||
LockingUnsupportedError,
|
LockingUnsupportedError,
|
||||||
Namespace,
|
Namespace,
|
||||||
RetryManager,
|
RetryManager,
|
||||||
|
@ -25,11 +26,9 @@ from ..utils import (
|
||||||
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 @@ class FileDownloader:
|
||||||
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 @@ class FileDownloader:
|
||||||
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 @@ class FileDownloader:
|
||||||
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 json
|
||||||
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 @@ import uuid
|
||||||
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 @@ class FFmpegFD(ExternalFD):
|
||||||
|
|
||||||
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 ..networking.exceptions import (
|
||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ContentTooShortError,
|
ContentTooShortError,
|
||||||
|
FormatProgressInfos,
|
||||||
RetryManager,
|
RetryManager,
|
||||||
ThrottledDownload,
|
ThrottledDownload,
|
||||||
XAttrMetadataError,
|
XAttrMetadataError,
|
||||||
|
@ -293,11 +294,11 @@ class HttpFD(FileDownloader):
|
||||||
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 @@ import time
|
||||||
|
|
||||||
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 @@ class RtmpFD(FileDownloader):
|
||||||
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 @@ class PostProcessor(metaclass=PostProcessorMetaClass):
|
||||||
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 @@ class PostProcessor(metaclass=PostProcessorMetaClass):
|
||||||
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 @@ class PostProcessor(metaclass=PostProcessorMetaClass):
|
||||||
# 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 json
|
||||||
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 @@ from ..utils import (
|
||||||
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 @@ class FFmpegPostProcessor(PostProcessor):
|
||||||
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 @@ class FFmpegPostProcessor(PostProcessor):
|
||||||
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 @@ class FFmpegPostProcessor(PostProcessor):
|
||||||
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 @@ class FFmpegPostProcessor(PostProcessor):
|
||||||
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 @@ class FFmpegExtractAudioPP(FFmpegPostProcessor):
|
||||||
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 @@ class FFmpegExtractAudioPP(FFmpegPostProcessor):
|
||||||
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 @@ class FFmpegVideoConvertorPP(FFmpegPostProcessor):
|
||||||
|
|
||||||
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 @@ class FFmpegMergerPP(FFmpegPostProcessor):
|
||||||
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 @@ class FFmpegSubtitlesConvertorPP(FFmpegPostProcessor):
|
||||||
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 @@ class FFmpegConcatPP(FFmpegPostProcessor):
|
||||||
'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 @@ class _YDLLogger:
|
||||||
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…
Add table
Reference in a new issue