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 |
