Mercurial > hg > config
comparison python/ffslice.py @ 741:9681a0bd74d6
add utility for slicing video files
author | Jeff Hammel <k0scist@gmail.com> |
---|---|
date | Sun, 28 Jun 2015 11:56:13 -0700 |
parents | |
children | f011ec45b8e8 |
comparison
equal
deleted
inserted
replaced
739:8a9fe2f20bcb | 741:9681a0bd74d6 |
---|---|
1 #!/usr/bin/env python | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 """ | |
5 slice a clip with `ffmpeg`: | |
6 | |
7 ffmpeg -ss <00:00:00> -t <sec> -i <input_file> -q 1 <output_file> # | |
8 | |
9 --ss = start time ; | |
10 | |
11 -t = length of video to create from the start time in seconds. | |
12 """ | |
13 | |
14 # imports | |
15 import argparse | |
16 import os | |
17 import subprocess | |
18 import sys | |
19 | |
20 # module globals | |
21 __all__ = ['main', 'Parser'] | |
22 here = os.path.dirname(os.path.realpath(__file__)) | |
23 string = (str, unicode) | |
24 | |
25 | |
26 def ensure_dir(directory): | |
27 """ensure a directory exists""" | |
28 if os.path.exists(directory): | |
29 assert os.path.isdir(directory) | |
30 return directory | |
31 os.makedirs(directory) | |
32 return directory | |
33 | |
34 | |
35 def convert_seconds_to_hhmmss(time_sec): | |
36 """converts `time_sec` to (hh,mm,ss)""" | |
37 | |
38 hh = int(time_sec / 3600) | |
39 mm = int((time_sec % 3600) / 60) | |
40 ss = int((time_sec % 3600) % 60) | |
41 return (hh,mm,ss) | |
42 | |
43 | |
44 def format_seconds_to_hhmmss(time_sec, separator=':'): | |
45 """converts `time_sec` to string 'hh:mm:ss'""" | |
46 return separator.join(['{:0>2d}'.format(i) | |
47 for i in convert_seconds_to_hhmmss(time_sec)]) | |
48 | |
49 | |
50 def duration(clip, ffprobe='ffprobe'): | |
51 """get the duration in seconds using `ffprobe` from ffmpeg""" | |
52 | |
53 command = [ffprobe, clip] | |
54 assert os.path.exists(clip), "Missing clip, duration not available" | |
55 | |
56 # probe the file | |
57 try: | |
58 output = subprocess.check_output(command, stderr=subprocess.STDOUT) | |
59 # (for some reason, ffprobe sends output to stderr) | |
60 except subprocess.CalledProcessError as e: | |
61 print (e.output) | |
62 raise | |
63 | |
64 duration = None | |
65 for line in output.splitlines(): | |
66 # Parse the output of `ffprobe` | |
67 # look for a line like: | |
68 # Duration: 00:00:59.73, start: 0.000000, bitrate: 7783 kb/s | |
69 | |
70 line = line.strip() | |
71 if line.startswith("Duration:"): | |
72 if duration: | |
73 raise AssertionError("Duplicate duration - already found: {}".format(duration)) | |
74 line = line.split(',')[0] | |
75 duration = line.split(':', 1)[1].strip() | |
76 | |
77 if duration: | |
78 hh, mm, ss = [float(i) for i in duration.split(':')] | |
79 duration = 3600*hh + 60*mm + ss | |
80 | |
81 return duration | |
82 | |
83 | |
84 class FFSliceParser(argparse.ArgumentParser): | |
85 """fflice CLI option parser""" | |
86 | |
87 default_slice_time = 300. | |
88 | |
89 def __init__(self): | |
90 argparse.ArgumentParser.__init__(self, description=__doc__) | |
91 self.add_argument('clips', metavar='clip', nargs='+', help="clips to slice") | |
92 self.add_argument('-d', '--directory', dest='directory', | |
93 default=os.getcwd(), | |
94 help="output directory [DEFAULT: %(default)s]") | |
95 self.add_argument('--durations', '--print-durations', dest='print_durations', | |
96 action='store_true', default=False, | |
97 help="print durations and exit") | |
98 self.add_argument('-n', dest='number', type=int, | |
99 help="number of slices") | |
100 self.add_argument('-t', '--time', dest='slice_time', | |
101 type=float, | |
102 help="time of each slice [DEFAULT: {}]".format(self.default_slice_time)) | |
103 self.add_argument('--dry-run', dest='dry_run', | |
104 action='store_true', default=False, | |
105 help="print out what will be done") | |
106 self.add_argument('--hhmmss', dest='hhmmss', | |
107 action='store_true', default=False, | |
108 help="display times in 'hh:mm:ss' format; thedefault is in seconds") | |
109 | |
110 self.add_argument('-v', '--verbose', dest='verbose', | |
111 action='store_true', default=False, | |
112 help="be verbose") | |
113 self.options = None | |
114 | |
115 def parse_args(self, *args, **kw): | |
116 options = argparse.ArgumentParser.parse_args(self, *args, **kw) | |
117 self.validate(options) | |
118 self.options = options | |
119 return options | |
120 | |
121 def validate(self, options): | |
122 """validate options""" | |
123 | |
124 missing = [clip for clip in options.clips | |
125 if not os.path.exists(clip)] | |
126 if missing: | |
127 self.error("Not found: {}".format(', '.join(missing))) | |
128 | |
129 if options.slice_time and options.number: | |
130 self.error("Cannot specify slice time and number of slices") | |
131 # TODO: allow specification of both | |
132 elif options.slice_time is None and options.number is None: | |
133 options.slice_time = self.default_slice_time | |
134 | |
135 ensure_dir(options.directory) | |
136 | |
137 def format_seconds(self, seconds): | |
138 """format seconds to string""" | |
139 if self.options.hhmmss: | |
140 return format_seconds_to_hhmmss(seconds) | |
141 return '{:.2}'.format(seconds) | |
142 | |
143 | |
144 def main(args=sys.argv[1:]): | |
145 """CLI""" | |
146 | |
147 # parse command line options | |
148 parser = FFSliceParser() | |
149 options = parser.parse_args(args) | |
150 | |
151 # compute durations | |
152 durations = {clip: duration(clip) for clip in options.clips} | |
153 if options.print_durations: | |
154 returncode = 0 | |
155 total = 0. | |
156 for clip in options.clips: | |
157 _duration = durations[clip] | |
158 if _duration is None: | |
159 print ("Duration not found: '{}'".format(clip)) | |
160 returncode = 1 | |
161 else: | |
162 print ('{} : {}'.format(clip, | |
163 parser.format_seconds(_duration))) | |
164 sys.exit(returncode) | |
165 | |
166 if __name__ == '__main__': | |
167 main() | |
168 |