7 Automatic audio stream selection (deu/eng) |
7 Automatic audio stream selection (deu/eng) |
8 Automatic crop detection to remove cinematic bars |
8 Automatic crop detection to remove cinematic bars |
9 """ |
9 """ |
10 |
10 |
11 import subprocess |
11 import subprocess |
|
12 import pexpect |
12 from eit import readeit, eitinfo |
13 from eit import readeit, eitinfo |
13 import os, shlex |
14 import os, shlex, sys, time |
14 |
15 |
15 def filter_lines(data, search): |
16 def filter_lines(data, search): |
16 """ |
17 """ |
17 input: data = \n separated string |
18 input: data = \n separated string |
18 output: tuple containing all lines where search is found |
19 output: tuple containing all lines where search is found |
25 return "\n".join(ret) |
26 return "\n".join(ret) |
26 |
27 |
27 def run_command(command): |
28 def run_command(command): |
28 """ |
29 """ |
29 run command as blocking subprocess, returns exit code |
30 run command as blocking subprocess, returns exit code |
|
31 if total_frames > 0 parse ffmpeg status line and insert ETA at line start before output |
30 """ |
32 """ |
31 process = subprocess.Popen(shlex.split(command), \ |
33 process = subprocess.Popen(shlex.split(command), \ |
32 stdout=subprocess.PIPE) |
34 stdout=subprocess.PIPE) |
33 while True: |
35 while True: |
34 output = process.stdout.readline() |
36 output = process.stdout.readline() |
37 if output: |
39 if output: |
38 print output.strip() |
40 print output.strip() |
39 rc = process.poll() |
41 rc = process.poll() |
40 return rc |
42 return rc |
41 |
43 |
|
44 def run_ffmpeg_watch(command, frames_total = 0): |
|
45 """ |
|
46 run command as blocking subprocess, returns exit code |
|
47 if total_frames > 0 parse ffmpeg status line and insert ETA at line start before output |
|
48 """ |
|
49 thread = pexpect.spawn(command) |
|
50 cpl = thread.compile_pattern_list([ |
|
51 pexpect.EOF, |
|
52 "frame= *(\d+)", |
|
53 '(.+)' |
|
54 ]) |
|
55 percent = 0 |
|
56 eta = 0 |
|
57 time_start = time.time() - 0.1 # start in the past |
|
58 while True: |
|
59 i = thread.expect_list(cpl, timeout=None) |
|
60 if i == 0: # EOF |
|
61 print "the sub process exited" |
|
62 break |
|
63 elif i == 1: |
|
64 try: |
|
65 frame_number = int(thread.match.group(1)) |
|
66 if frames_total > 0: |
|
67 percent = frame_number * 100.00 / frames_total |
|
68 eta = frame_number / (time.time() - time_start) |
|
69 # eta is frames per second so far |
|
70 eta = (frames_total - frame_number) / eta / 60 |
|
71 sys.stdout.write("\rFrame %i of %i, %.1f%% done, ETA %.0f minutes, " % ( |
|
72 frame_number, frames_total, percent, eta |
|
73 )) |
|
74 except: |
|
75 sys.stdout.write(thread.match.group(0)) |
|
76 sys.stdout.flush() |
|
77 thread.close |
|
78 elif i == 2: |
|
79 unknown_line = thread.match.group(0) |
|
80 sys.stdout.write(unknown_line) |
|
81 sys.stdout.flush() |
|
82 pass |
|
83 |
42 def ffmpeg_filename(filename): |
84 def ffmpeg_filename(filename): |
43 """ |
85 """ |
44 Escape filename path contents for ffmpeg shell command |
86 Escape filename path contents for ffmpeg shell command |
45 """ |
87 """ |
46 fn = "\\'".join(p for p in filename.split("'")) |
88 #fn = "\\'".join(p for p in filename.split("'")) |
|
89 fn = filename.replace("'", "\\'") |
47 fn = fn.replace(" ", "\\ ") |
90 fn = fn.replace(" ", "\\ ") |
48 return fn |
91 return fn |
49 |
92 |
50 class ts2mkv(object): |
93 class ts2mkv(object): |
51 """ |
94 """ |
129 parse the ffmpeg analyze output cropdetect lines |
175 parse the ffmpeg analyze output cropdetect lines |
130 returns None or valid crop string for ffmpeg video filter |
176 returns None or valid crop string for ffmpeg video filter |
131 """ |
177 """ |
132 lines = filter_lines(self.msg_ffmpeg, "[Parsed_cropdetect").split("\n") |
178 lines = filter_lines(self.msg_ffmpeg, "[Parsed_cropdetect").split("\n") |
133 option = None |
179 option = None |
|
180 failcount = 0 |
134 for line in lines: |
181 for line in lines: |
135 tmp = line[line.find(" crop="):].strip() |
182 tmp = line[line.find(" crop="):].strip() |
136 #print "DEBUG: " + tmp |
183 #print "DEBUG: " + tmp |
137 if not option: |
184 if not option: |
138 option = tmp |
185 option = tmp |
139 else: |
186 else: |
140 if option != tmp: |
187 if option != tmp: |
141 self.msg_prepare += "WARNING: cropdetect inconsistent over scan time, disabling autocrop\n" |
188 failcount += 1 |
142 return None |
189 if failcount > 12: |
|
190 print "!!! Crop detect is inconsistent" |
|
191 self.msg_prepare += "WARNING: cropdetect >50% inconsistent over scan time, disabling autocrop\n" |
|
192 return None |
143 self.msg_prepare += "Crop detected: %s\n" % option |
193 self.msg_prepare += "Crop detected: %s\n" % option |
144 return option |
194 return option |
145 |
195 |
146 def __get_audiomap(self, info): |
196 def __get_audiomap(self, info): |
147 """ |
197 """ |
167 audiomap.append(aidx) |
217 audiomap.append(aidx) |
168 |
218 |
169 audio = filter_lines(audioall, "(eng):") |
219 audio = filter_lines(audioall, "(eng):") |
170 aidx = self.get_stream_index( |
220 aidx = self.get_stream_index( |
171 filter_lines(audio, "ac3")) |
221 filter_lines(audio, "ac3")) |
172 if aidx != "": |
222 if aidx != "" and filter_lines(audio, "ac3").find(" 0 channels ") < 1: |
173 # append english audio too! |
223 # append english audio too! |
174 print "Selecting english ac3 stream." |
224 print "Selecting english ac3 stream." |
175 audiomap.append(aidx) |
225 audiomap.append(aidx) |
176 return audiomap |
226 return audiomap |
177 |
227 |
187 commands = [] |
237 commands = [] |
188 fn = ffmpeg_filename(self.filename) |
238 fn = ffmpeg_filename(self.filename) |
189 outfn = self.outfilebase + ".mkv" |
239 outfn = self.outfilebase + ".mkv" |
190 # double-check: pull the kill switch and exit if outfile exists already! |
240 # double-check: pull the kill switch and exit if outfile exists already! |
191 # we do not want to overwrite files in accident (caused by automatic file naming) |
241 # we do not want to overwrite files in accident (caused by automatic file naming) |
192 if len(glob.glob(outfn)) > 0: |
242 if not self.overwrite and len(glob.glob(outfn)) > 0: |
193 print "Output file exists: %s" % outfn |
243 print "Output file exists: %s" % outfn |
194 print "NOT overwriting it!" |
244 print "NOT overwriting it!" |
195 return None |
245 return None |
196 outfn = ffmpeg_filename(outfn) |
246 outfn = ffmpeg_filename(outfn) |
197 |
247 |
198 cmd = [ |
248 cmd = [ |
199 "ffmpeg", "-hide_banner", |
249 "ffmpeg", "-hide_banner", |
200 "-ss 00:05:00", "-t 1", # search to 5 minutes, analyze 1 second |
250 "-ss 00:05:00", "-t 2", # search to 5 minutes, analyze 2 seconds |
201 "-i %s" % fn, |
251 "-i %s" % fn, |
202 "-vf \"cropdetect=24:2:0\"", # detect black bar crop on top and bottom |
252 "-vf \"cropdetect=24:2:0\"", # detect black bar crop on top and bottom |
203 "-f null", "-" # no output file |
253 "-f null", "-" # no output file |
204 ] |
254 ] |
205 p = subprocess.Popen(shlex.split(" ".join(cmd)), \ |
255 p = subprocess.Popen(shlex.split(" ".join(cmd)), \ |
215 filter_lines(info, "Video:")) |
265 filter_lines(info, "Video:")) |
216 if v == "": |
266 if v == "": |
217 print "No video stream found" |
267 print "No video stream found" |
218 return None |
268 return None |
219 |
269 |
220 |
270 # get total duration and fps from input stream |
221 # TODO: copy ALL subtitle streams if present! |
271 # Input #0, mpegts, from '/srv/storage0/DREAMBOX/Action/Transporter/20101201 0630 - Sky Action HD - Transporter 3.ts': |
|
272 # Duration: 01:39:59.88, start: 93674.825111, bitrate: 9365 kb/s |
|
273 # Stream #0:1[0x1ff]: Video: h264 (High) ([27][0][0][0] / 0x001B), yuv420p(tv, bt709), 1920x1080 [SAR 1:1 DAR 16:9], 25 fps, 50 tbr, 90k tbn, 50 tbc |
|
274 self.frames_total = filter_lines(self.msg_ffmpeg, "Duration:").strip()[10:] |
|
275 self.frames_total = self.frames_total[0:self.frames_total.find(",")].strip() |
|
276 print "Input duration: %s" % self.frames_total |
|
277 try: |
|
278 self.frames_total = int(self.frames_total[0:2]) * 3600 + \ |
|
279 int(self.frames_total[3:5]) * 60 + int(self.frames_total[6:8]) |
|
280 except ValueError: |
|
281 self.frames_total = 0 |
|
282 |
|
283 tmp = filter_lines(info, "Video:").split(",") |
|
284 for fps in tmp: |
|
285 if fps.strip().endswith('fps'): |
|
286 try: |
|
287 self.fps = float(fps.strip().split(' ')[0]) |
|
288 except ValueError: |
|
289 self.fps = 0 |
|
290 break |
|
291 self.frames_total = round(self.frames_total * self.fps, 0) |
|
292 print "Input framerate: %f fps" % self.fps |
|
293 print "Total frames of input file: %i" % (self.frames_total) |
|
294 |
|
295 # copy ALL subtitle streams if present! |
222 # Stream #0:0[0x20](deu): Subtitle: dvb_teletext ([6][0][0][0] / 0x0006), 492x250 |
296 # Stream #0:0[0x20](deu): Subtitle: dvb_teletext ([6][0][0][0] / 0x0006), 492x250 |
223 submap = [] |
297 submap = [] |
224 for tmp in filter_lines(info, "Subtitle: dvb_teletext").split("\n"): |
298 for tmp in filter_lines(info, "Subtitle: dvb_teletext").split("\n"): |
225 if self.get_stream_index(tmp): |
299 if self.get_stream_index(tmp): |
226 submap.append(self.get_stream_index(tmp)) |
300 submap.append(self.get_stream_index(tmp)) |
|
301 # Subtitles disabled, that doesnt work as expected, dreambox crashes on copied subtitle stream |
|
302 submap = [] |
227 |
303 |
228 # select audio streams |
304 # select audio streams |
229 audiomap = self.__get_audiomap(info) |
305 audiomap = self.__get_audiomap(info) |
230 if len(audiomap) == 0: |
306 if len(audiomap) == 0: |
231 print "No suitable audio stream found, aborting." |
307 print "No suitable audio stream found, aborting." |
295 First step: setup, analyze & prepare for conversion |
378 First step: setup, analyze & prepare for conversion |
296 """ |
379 """ |
297 self.msg_prepare = "" |
380 self.msg_prepare = "" |
298 self.msg_eit = "" |
381 self.msg_eit = "" |
299 self.msg_ffmpeg = "" |
382 self.msg_ffmpeg = "" |
|
383 self.fps = 0 |
|
384 self.frames_total = 0 |
300 |
385 |
301 self.filename = filename |
386 self.filename = filename |
302 self.outfilebase = os.path.splitext(filename)[0] |
387 self.outfilebase = os.path.splitext(filename)[0] |
303 self.get_movie_description() |
388 self.get_movie_description() |
304 self.command = self.get_ffmpeg_command() |
389 self.command = self.get_ffmpeg_command() |
319 fd.close() |
404 fd.close() |
320 #print self.msg_ffmpeg |
405 #print self.msg_ffmpeg |
321 |
406 |
322 for cmd in self.command: |
407 for cmd in self.command: |
323 print "Executing ffmpeg:\n%s\n" % cmd |
408 print "Executing ffmpeg:\n%s\n" % cmd |
324 return run_command(cmd) |
409 #return run_command(cmd, self.total_frames) |
|
410 return run_ffmpeg_watch(cmd, frames_total=self.frames_total) |
325 |
411 |
326 |
412 |
327 |
413 |
328 if __name__ == "__main__": |
414 if __name__ == "__main__": |
329 # parse command line options |
415 # parse command line options |
338 help='no rescaling (default is scale to 720p)') |
424 help='no rescaling (default is scale to 720p)') |
339 parser.add_argument('--rename', action='store_true', default=False, \ |
425 parser.add_argument('--rename', action='store_true', default=False, \ |
340 help='rename file basename to name and genre from EIT file if present') |
426 help='rename file basename to name and genre from EIT file if present') |
341 parser.add_argument('input', metavar='input', nargs='+', \ |
427 parser.add_argument('input', metavar='input', nargs='+', \ |
342 help='one or more files, glob style syntax') |
428 help='one or more files, glob style syntax') |
|
429 parser.add_argument('-f', action='store_true', default=False, \ |
|
430 help='force overwrite of existing file') |
343 |
431 |
344 args = parser.parse_args() |
432 args = parser.parse_args() |
345 processor = ts2mkv(crf=args.crf, tune=args.tune, scaleto_720p=(not args.ns), \ |
433 processor = ts2mkv(crf=args.crf, tune=args.tune, scaleto_720p=(not args.ns), \ |
346 rename=args.rename) |
434 rename=args.rename) |
|
435 processor.overwrite = args.f |
347 |
436 |
348 for srcstr in args.input: |
437 for srcstr in args.input: |
349 src = glob.glob(srcstr) |
438 src = glob.glob(srcstr) |
350 for srcfile in src: |
439 for srcfile in src: |
351 print "Processing: %s" % srcfile |
440 print "Processing: %s" % srcfile |