ts2mkv.py

changeset 15
82361ad7b3fe
parent 14
b398ae388328
child 16
ace8005f02cf
equal deleted inserted replaced
14:b398ae388328 15:82361ad7b3fe
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 """
56 self.msg_eit = "" 99 self.msg_eit = ""
57 self.msg_ffmpeg = "" 100 self.msg_ffmpeg = ""
58 self.command = None 101 self.command = None
59 self.filename = None 102 self.filename = None
60 self.outfilebase = None 103 self.outfilebase = None
104 self.fps = 0
105 self.frames_total = 0
106 self.overwrite = False
61 107
62 self.scaleto_720p = scaleto_720p 108 self.scaleto_720p = scaleto_720p
63 self.rename = rename 109 self.rename = rename
64 110
65 self.video_options = [ 111 self.video_options = [
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."
246 fn = "\"concat:" + \ 322 fn = "\"concat:" + \
247 "|".join(inputs)\ 323 "|".join(inputs)\
248 .replace('\ ', ' ')\ 324 .replace('\ ', ' ')\
249 .replace("\'", "'")\ 325 .replace("\'", "'")\
250 + "\"" 326 + "\""
327 # no ETA calculation possible since we have only the length of first file
328 # TODO: we COULD estimate by multiplying with factor generated by input file sizes
329 print "NO ETA POSSIBLE"
330 self.frames_total = 0
251 331
252 idx = 0 332 idx = 0
253 for tmp in inputs: 333 for tmp in inputs:
254 self.msg_prepare += "Input file #%i: %s\n" % ( 334 self.msg_prepare += "Input file #%i: %s\n" % (
255 idx, os.path.basename(tmp)) 335 idx, os.path.basename(tmp))
257 337
258 cmd = [ 338 cmd = [
259 "ffmpeg", "-hide_banner", 339 "ffmpeg", "-hide_banner",
260 "-i %s" % fn, 340 "-i %s" % fn,
261 ] 341 ]
342
343 if self.overwrite:
344 cmd.append("-y")
262 345
263 for tmp in submap: 346 for tmp in submap:
264 self.msg_prepare += "Subtitle Stream selected: Stream #%s\n" % tmp 347 self.msg_prepare += "Subtitle Stream selected: Stream #%s\n" % tmp
265 cmd.append("-map %s" % tmp) 348 cmd.append("-map %s" % tmp)
266 349
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

mercurial