Skip to content

Commit 3f8bbf2

Browse files
authored
Use custom ffmpeg class (#152)
* Add ffmpeg-dl Heavily borrowed from https://stackoverflow.com/a/77052858/3939155. Will fix #151 as we avoid socket.AF_UNIX and just read stdout * Address TODOs * Perform final refresh and close * Reorder imports
1 parent 29c5f89 commit 3f8bbf2

File tree

2 files changed

+98
-78
lines changed

2 files changed

+98
-78
lines changed

Diff for: nndownload/ffmpeg_dl.py

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import regex as re
2+
import subprocess
3+
import warnings
4+
from datetime import timedelta, datetime
5+
from typing import AnyStr, List
6+
7+
import ffmpeg
8+
from tqdm import TqdmExperimentalWarning
9+
from tqdm.rich import tqdm_rich
10+
11+
warnings.filterwarnings("ignore", category=TqdmExperimentalWarning)
12+
13+
14+
class FfmpegDLException(Exception):
15+
"""Raised when a download fails."""
16+
pass
17+
18+
19+
class FfmpegDL:
20+
"""Send input streams for download to an `ffmpeg` subprocess."""
21+
22+
FF_GLOBAL_ARGS = [
23+
"-progress",
24+
"-",
25+
"-nostats",
26+
"-y"
27+
]
28+
29+
REGEX_TIME_GROUP = "([0-9]{2}:[0-9]{2}:[0-9]{2}[.[0-9]*]?)"
30+
REGEX_OUT_TIME = re.compile(
31+
r"out_time=[ ]*" + REGEX_TIME_GROUP
32+
)
33+
34+
@classmethod
35+
def get_timedelta(cls, time_str: AnyStr, str_format: AnyStr = "%H:%M:%S.%f"):
36+
t = datetime.strptime(time_str, str_format)
37+
return timedelta(hours=t.hour, minutes=t.minute, seconds=t.second, microseconds=t.microsecond)
38+
39+
def __init__(self, streams: List, input_kwargs: List, output_path: AnyStr, output_kwargs: List, global_args: List = FF_GLOBAL_ARGS):
40+
inputs = []
41+
for stream in streams:
42+
input = ffmpeg.input(stream, **input_kwargs)
43+
inputs.append(input)
44+
stream_spec = ffmpeg.output(*inputs, output_path, **output_kwargs).global_args(*global_args)
45+
46+
self.proc_args = ffmpeg._run.compile(stream_spec=stream_spec)
47+
self.proc: subprocess.Popen = None
48+
49+
def load_subprocess(self):
50+
self.proc = subprocess.Popen(
51+
args=self.proc_args,
52+
stdin=subprocess.PIPE,
53+
stdout=subprocess.PIPE,
54+
stderr=subprocess.STDOUT,
55+
universal_newlines=False,
56+
)
57+
58+
def convert(self, name: AnyStr, duration: float):
59+
progress = tqdm_rich(desc=name, unit="seg", colour="green", total=duration)
60+
61+
self.load_subprocess()
62+
63+
stdout_line = None
64+
while True:
65+
if self.proc.stdout is None:
66+
continue
67+
prev_line = stdout_line
68+
stdout_line = self.proc.stdout.readline().decode("utf-8", errors="replace").strip()
69+
out_time_data = self.REGEX_OUT_TIME.search(stdout_line)
70+
if out_time_data is not None:
71+
out_time = self.get_timedelta(out_time_data.group(1))
72+
progress.update(out_time.total_seconds() - progress.n)
73+
continue
74+
if stdout_line == "" and self.proc.poll() is not None:
75+
progress.refresh()
76+
progress.close()
77+
exit_code = self.proc.poll()
78+
if exit_code:
79+
raise FfmpegDLException(prev_line)
80+
else:
81+
break

Diff for: nndownload/nndownload.py

+17-78
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,23 @@
1414
import os
1515
import re
1616
import shutil
17-
import socket
1817
import sys
1918
import tempfile
2019
import threading
2120
import time
22-
import warnings
2321
import xml.dom.minidom
2422
from typing import AnyStr, List, Match
2523

2624
import aiohttp
2725
import requests
28-
import ffmpeg
29-
import gevent.monkey
30-
gevent.monkey.patch_all(ssl=False,thread=False)
3126
from aiohttp_socks import ProxyConnector
3227
from bs4 import BeautifulSoup
3328
from mutagen.mp4 import MP4, MP4StreamInfoError
3429
from requests.adapters import HTTPAdapter
3530
from requests.utils import add_dict_to_cookiejar
3631
from urllib3.util import Retry
37-
from tqdm import tqdm, TqdmWarning
3832

33+
from ffmpeg_dl import FfmpegDL, FfmpegDLException
3934

4035
__version__ = "1.16"
4136
__author__ = "Alex Aplin"
@@ -1250,80 +1245,24 @@ def download_video_part(session: requests.Session, start, end, filename: AnyStr,
12501245
update_multithread_progress(len(block))
12511246

12521247

1253-
def capture_ffmpeg_progress(filename, sock, handler):
1254-
"""Capture and send ffmpeg events to the provided handler."""
1255-
1256-
connection, client_address = sock.accept()
1257-
data = b""
1258-
try:
1259-
while True:
1260-
more_data = connection.recv(16)
1261-
if not more_data:
1262-
break
1263-
data += more_data
1264-
lines = data.split(b"\n")
1265-
for line in lines[:-1]:
1266-
line = line.decode()
1267-
parts = line.split("=")
1268-
key = parts[0] if len(parts) > 0 else None
1269-
value = parts[1] if len(parts) > 1 else None
1270-
handler(key, value)
1271-
data = lines[-1]
1272-
finally:
1273-
connection.close()
1274-
1275-
1276-
@contextlib.contextmanager
1277-
def watch_ffmpeg_progress(handler):
1278-
"""Spawn a socket to capture ffmpeg events."""
1279-
1280-
with get_temp_dir() as temp_dir:
1281-
socket_filename = os.path.join(temp_dir, "sock")
1282-
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
1283-
with contextlib.closing(sock):
1284-
sock.bind(socket_filename)
1285-
sock.listen(1)
1286-
child = gevent.spawn(capture_ffmpeg_progress, socket_filename, sock, handler)
1287-
try:
1288-
yield socket_filename
1289-
except:
1290-
gevent.kill(child)
1291-
raise
1292-
1293-
1294-
@contextlib.contextmanager
1295-
def show_ffmpeg_progress(duration: float):
1296-
"""Render a tqdm progress bar relative to a specified stream duration."""
1297-
1298-
warnings.filterwarnings("ignore", category=TqdmWarning)
1299-
with tqdm(total=round(duration, 2)) as progress_bar:
1300-
def handler(key, value):
1301-
if key == "out_time_ms":
1302-
time = round(float(value) / 1000000., 2)
1303-
progress_bar.update(time - progress_bar.n)
1304-
elif key == "progress" and value == "end":
1305-
progress_bar.update(progress_bar.total - progress_bar.n)
1306-
with watch_ffmpeg_progress(handler) as socket_filename:
1307-
yield socket_filename
1308-
1309-
1310-
def perform_ffmpeg_dl(filename: AnyStr, duration: float, streams: List):
1248+
def perform_ffmpeg_dl(video_id: AnyStr, filename: AnyStr, duration: float, streams: List):
13111249
"""Send video and/or audio stream to ffmpeg for download."""
13121250

1313-
inputs = []
13141251
try:
1315-
with show_ffmpeg_progress(duration) as socket_filename:
1316-
for stream in streams:
1317-
input = ffmpeg.input(stream, protocol_whitelist="https,http,tls,tcp,file,crypto", allowed_extensions="ALL")
1318-
inputs.append(input)
1319-
output = ffmpeg.output(*inputs, filename, vcodec="copy", acodec="copy")
1320-
output = output.global_args("-progress", "unix://{}".format(socket_filename), "-y")
1321-
output.run(capture_stdout=True, capture_stderr=True)
1322-
return True
1323-
except ffmpeg.Error as error:
1324-
stderr = error.stderr.decode("utf8")
1325-
actual_error = stderr.splitlines()[-1]
1326-
raise FormatNotAvailableException(f"ffmpeg exited with an error: {actual_error}")
1252+
video_download = FfmpegDL(streams=streams,
1253+
input_kwargs={
1254+
"protocol_whitelist": "https,http,tls,tcp,file,crypto",
1255+
"allowed_extensions": "ALL",
1256+
},
1257+
output_path=filename,
1258+
output_kwargs={
1259+
"vcodec": "copy",
1260+
"acodec": "copy",
1261+
})
1262+
video_download.convert(name=video_id, duration=duration)
1263+
return True
1264+
except FfmpegDLException as error:
1265+
raise FormatNotAvailableException(f"ffmpeg failed to download the video or audio stream with the following error: \"{error}\"")
13271266
except Exception:
13281267
raise FormatNotAvailableException("Failed to download video or audio stream")
13291268

@@ -1366,7 +1305,7 @@ def download_video_media(session: requests.Session, filename: AnyStr, template_p
13661305
generic_dl_request(session, key_url, key_path, binary=True)
13671306
rewrite_file(m3u8_path, key_url, key_path)
13681307
m3u8_streams.append(m3u8_path)
1369-
continue_code = perform_ffmpeg_dl(filename, float(template_params["duration"]), m3u8_streams)
1308+
continue_code = perform_ffmpeg_dl(template_params["id"], filename, float(template_params["duration"]), m3u8_streams)
13701309
os.rename(filename, complete_filename)
13711310
return continue_code
13721311

0 commit comments

Comments
 (0)