-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathdvd2webm.py
360 lines (331 loc) · 11.3 KB
/
dvd2webm.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
#!/usr/bin/env python
# file: dvd2webm.py
# vim:fileencoding=utf-8:ft=python
#
# Copyright © 2016-2018 R.F. Smith <[email protected]>.
# SPDX-License-Identifier: MIT
# Created: 2016-02-11T19:02:34+01:00
# Last modified: 2023-03-03T20:34:56+0100
"""
Convert an mpeg stream from a DVD to a webm file, using constrained rate VP9
encoding for video and libvorbis for audio.
It uses the first video stream and the first audio stream, unless otherwise
indicated.
Optionally it can include a subtitle in the form of an SRT file in the output.
If the subtitle is a dvdsub track number, it gets overlayed on the video track
because the webm format only allows webVTT subtitle tracks.
"""
from collections import Counter
from datetime import datetime
import argparse
import logging
import math
import os
import re
import subprocess as sp
import sys
__version__ = "2022.12.11"
def main():
"""Entry point for dvd2webm.py."""
args = setup()
logging.info(f"processing '{args.fn}'.")
starttime = datetime.now()
startstr = str(starttime)[:-7]
logging.info(f"started at {startstr}.")
logging.info(f"using audio stream {args.audio}.")
tc = 1
if not args.crop and args.detect:
logging.info("looking for cropping.")
args.crop = findcrop(args.fn)
width, height, _, _ = args.crop.split(":")
if width in ["720", "704"] and height == "576":
logging.info("standard format, no cropping necessary.")
args.crop = None
tc = tile_cols(width)
else:
width, _, _, _ = args.crop.split(":")
tc = tile_cols(width)
if args.crop:
logging.info("using cropping " + args.crop)
subtrack, srtfile = None, None
if args.subtitle:
try:
subtrack = str(int(args.subtitle))
logging.info("using subtitle track " + subtrack)
except ValueError:
srtfile = args.subtitle
logging.info("using subtitle file " + srtfile)
a1 = mkargs(
args.fn,
1,
tc,
crop=args.crop,
start=args.start,
subf=srtfile,
subt=subtrack,
atrack=args.audio,
)
a2 = mkargs(
args.fn,
2,
tc,
crop=args.crop,
start=args.start,
subf=srtfile,
subt=subtrack,
atrack=args.audio,
)
if not args.dummy:
origbytes, newbytes = encode(a1, a2)
else:
logging.basicConfig(level="INFO")
logging.info("first pass: " + " ".join(a1))
logging.info("second pass: " + " ".join(a2))
return
stoptime = datetime.now()
stopstr = str(stoptime)[:-7]
logging.info(f"ended at {stopstr}.")
runtime = stoptime - starttime
runstr = str(runtime)[:-7]
logging.info(f"total running time {runstr}.")
encspeed = origbytes / (runtime.seconds * 1000)
logging.info(f"average input encoding speed {encspeed:.2f} kB/s.")
def setup():
"""Process command-line arguments and configure the program."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--log",
default="info",
choices=["debug", "info", "warning", "error"],
help="logging level (defaults to 'info')",
)
parser.add_argument("-v", "--version", action="version", version=__version__)
parser.add_argument(
"-s",
"--start",
type=str,
default=None,
help="time (hh:mm:ss) at which to start encoding",
)
parser.add_argument("-c", "--crop", type=str, help="crop (w:h:x:y) to use")
parser.add_argument(
"-d", "--dummy", action="store_true", help="print commands but do not run them"
)
parser.add_argument(
"-e", "--detect", action="store_true", help="detect cropping automatically"
)
parser.add_argument(
"-t",
"--subtitle",
type=str,
help="srt file or dvdsub track number (default: no subtitle)",
)
ahelp = "number of the audio track to use (default: 0; first audio track)"
parser.add_argument("-a", "--audio", type=int, default=0, help=ahelp)
parser.add_argument("fn", metavar="filename", help="MPEG file to process")
args = parser.parse_args(sys.argv[1:])
logging.basicConfig(
level=getattr(logging, args.log.upper(), None),
format="%(levelname)s: %(message)s",
)
logging.debug(f"command line arguments = {sys.argv}")
logging.debug(f"parsed arguments = {args}")
if not check_ffmpeg():
sys.exit(1)
return args
def tile_cols(width):
return math.floor(math.log2(math.ceil(float(width) / 64.0)))
def check_ffmpeg():
"""Check the minumum version requirement of ffmpeg, and that it is built with
the needed drivers enabled."""
args = ["ffmpeg", "-version"]
try:
proc = sp.run(args, text=True, stdout=sp.PIPE, stderr=sp.DEVNULL)
except FileNotFoundError:
logging.error("ffmpeg not found")
return False
verre = r"ffmpeg version (\d+)\.(\d+)(?:\.(\d+))? Copyright"
major, minor, patch, *rest = re.findall(verre, proc.stdout)[0]
logging.info(f"found ffmpeg {major}.{minor}.{patch}")
if int(major) < 3 and int(minor) < 3:
logging.error(f"ffmpeg 3.3 is required; found {major}.{minor}.{patch}")
return False
if not re.search(r"enable-libvpx", proc.stdout):
logging.error("ffmpeg is not built with VP9 video support.")
return False
if not re.search(r"enable-libvorbis", proc.stdout):
logging.error("ffmpeg is not built with Vorbis audio support.")
return False
return True
def findcrop(path, start="00:10:00", duration="00:00:01"):
"""
Find the cropping of the video file.
Arguments:
path: location of the file to query.
start: A string that defines where in the movie to start scanning.
Defaults to 10 minutes from the start. Format HH:MM:SS.
duration: A string defining how much of the movie to scan. Defaults to
one second. Format HH:MM:SS.
Returns:
A string containing the cropping to use with ffmpeg.
"""
args = [
"ffmpeg",
"-hide_banner",
"-ss",
start, # Start at 10 minutes in.
"-t",
duration, # Parse for one second.
"-i",
path, # Path to the input file.
"-vf",
"cropdetect", # Use the crop detect filter.
"-an", # Disable audio output.
"-y", # Overwrite output without asking.
"-f",
"rawvideo", # write raw video output.
"/dev/null", # Write output to /dev/null
]
proc = sp.run(args, universal_newlines=True, stdout=sp.DEVNULL, stderr=sp.PIPE)
rv = Counter(re.findall(r"crop=(\d+:\d+:\d+:\d+)", proc.stderr))
return rv.most_common(1)[0][0]
def reporttime(p, start, end):
"""
Report the amount of time passed between start and end.
Arguments:
p: number of the pass.
start: datetime.datetime instance.
end: datetime.datetime instance.
"""
dt = str(end - start)[:-7]
logging.info(f"pass {p} took {dt}.")
def mkargs(
fn, npass, tile_columns, crop=None, start=None, subf=None, subt=None, atrack=0
):
"""Create argument list for constrained quality VP9/vorbis encoding.
Arguments:
fn: String containing the path of the input file
npass: Number of the pass. Must be 1 or 2.
tile_columns: number of tile columns.
crop: Optional string containing the cropping to use. Must be in the
format W:H:X:Y, where W, H, X and Y are numbers.
start: Optional string containing the start time for the conversion.
Must be in the format HH:MM:SS, where H, M and S are digits.
subf: Optional string containing the name of the SRT file to use.
subt: Optional string containing the index of the dvdsub stream to use.
atrack: Optional number of the audio track to use. Defaults to 0.
Returns:
A list of strings suitable for calling a subprocess.
"""
if npass not in (1, 2):
raise ValueError("npass must be 1 or 2")
if crop and not re.search(r"\d+:\d+:\d+:\d+", crop):
raise ValueError("cropping must be in the format W:H:X:Y")
if start and not re.search(r"\d{2}:\d{2}:\d{2}", start):
raise ValueError("starting time must be in the format HH:MM:SS")
numthreads = str(os.cpu_count())
basename = fn.rsplit(".", 1)[0]
args = [
"ffmpeg",
"-loglevel",
"quiet",
"-probesize",
"1G",
"-analyzeduration",
"1G",
]
if start:
args += ["-ss", start]
args += ["-i", fn, "-passlogfile", basename]
speed = "2"
if npass == 1:
logging.info(f"using {numthreads} threads")
logging.info(f"using {tile_columns} tile columns")
speed = "4"
args += [
"-c:v",
"libvpx-vp9",
"-row-mt",
"1",
"-threads",
numthreads,
"-pass",
str(npass),
"-b:v",
"1400k",
"-crf",
"33",
"-g",
"250",
"-speed",
speed,
"-tile-columns",
str(tile_columns),
]
if npass == 2:
args += ["-auto-alt-ref", "1", "-lag-in-frames", "25"]
args += ["-sn"]
if npass == 1:
args += ["-an"]
elif npass == 2:
args += ["-c:a", "libvorbis", "-q:a", "3"]
args += ["-f", "webm"]
if not subt: # SRT file
args += ["-map", "0:v", "-map", f"0:a:{atrack}"]
vf = []
if subf:
vf = [f"subtitles={subf}"]
if crop:
vf.append(f"crop={crop}")
if vf:
args += ["-vf", ",".join(vf)]
else:
fc = f"[0:v][0:s:{subt}]overlay"
if crop:
fc += f",crop={crop}[v]"
else:
fc += "[v]"
args += ["-filter_complex", fc, "-map", "[v]", "-map", f"0:a:{atrack}"]
if npass == 1:
outname = "/dev/null"
else:
outname = basename + ".webm"
args += ["-y", outname]
return args
def encode(args1, args2):
"""
Run the encoding subprocesses.
Arguments:
args1: Commands to run the first encoding step as a subprocess.
args2: Commands to run the second encoding step as a subprocess.
Return values:
A 2-tuple of the original movie size in bytes and the encoded movie size in bytes.
"""
oidx = args2.index("-i") + 1
origsize = os.path.getsize(args2[oidx])
logging.info("running pass 1...")
logging.debug("pass 1: {}".format(" ".join(args1)))
start = datetime.utcnow()
proc = sp.run(args1, stdout=sp.DEVNULL, stderr=sp.DEVNULL)
end = datetime.utcnow()
if proc.returncode:
logging.error(f"pass 1 returned {proc.returncode}.")
return origsize, 0
else:
reporttime(1, start, end)
logging.info("running pass 2...")
logging.debug("pass 2: {}".format(" ".join(args2)))
start = datetime.utcnow()
proc = sp.run(args2, stdout=sp.DEVNULL, stderr=sp.DEVNULL)
end = datetime.utcnow()
if proc.returncode:
logging.error(f"pass 2 returned {proc.returncode}.")
else:
reporttime(2, start, end)
newsize = os.path.getsize(args2[-1])
percentage = int(100 * newsize / origsize)
ifn, ofn = args2[oidx], args2[-1]
logging.info(f"the size of '{ofn}' is {percentage}% of the size of '{ifn}'.")
return origsize, newsize # both in bytes.
if __name__ == "__main__":
main()