From ce96faeead6e209a5b93f091e1b00066a78ba056 Mon Sep 17 00:00:00 2001 From: Csaba Henk Date: Mon, 1 Jan 2018 19:18:44 +0100 Subject: [PATCH 1/3] eliminate execution via shell --- ffmpeg-split.py | 62 +++++++++++++++++-------------------------------- 1 file changed, 21 insertions(+), 41 deletions(-) diff --git a/ffmpeg-split.py b/ffmpeg-split.py index 892af78..6b8ae9e 100644 --- a/ffmpeg-split.py +++ b/ffmpeg-split.py @@ -2,16 +2,13 @@ import csv import subprocess -import re import math import json import os +import shlex from optparse import OptionParser -length_regexp = 'Duration: (\d{2}):(\d{2}):(\d{2})\.\d+,' -re_length = re.compile(length_regexp) - def split_by_manifest(filename, manifest, vcodec="copy", acodec="copy", extra="", **kwargs): """ Split video into segments based on the given manifest file. @@ -39,18 +36,15 @@ def split_by_manifest(filename, manifest, vcodec="copy", acodec="copy", print "Format not supported. File must be a csv or json file" raise SystemExit - split_cmd = "ffmpeg -i '%s' -vcodec %s -acodec %s -y %s" % (filename, - vcodec, - acodec, - extra) - split_count = 1 - split_error = [] + split_cmd = ["ffmpeg", "-i", filename, "-vcodec", vcodec, + "-acodec", acodec, "-y"] + shlex.split(extra) try: fileext = filename.split(".")[-1] except IndexError as e: raise IndexError("No . in filename. Error: " + str(e)) for video_config in config: split_str = "" + split_args = [] try: split_start = video_config["start_time"] split_length = video_config.get("end_time", None) @@ -60,16 +54,12 @@ def split_by_manifest(filename, manifest, vcodec="copy", acodec="copy", if fileext in filebase: filebase = ".".join(filebase.split(".")[:-1]) - split_str += " -ss " + str(split_start) + " -t " + \ - str(split_length) + \ - " '"+ filebase + "." + fileext + \ - "'" + split_args += ["-ss", str(split_start), "-t", + str(split_length), filebase + "." + fileext] print "########################################################" - print "About to run: "+split_cmd+split_str + print "About to run: "+" ".join(split_cmd+split_args) print "########################################################" - output = subprocess.Popen(split_cmd+split_str, - shell = True, stdout = - subprocess.PIPE).stdout.read() + subprocess.check_output(split_cmd+split_args) except KeyError as e: print "############# Incorrect format ##############" if manifest_type == "json": @@ -82,7 +72,13 @@ def split_by_manifest(filename, manifest, vcodec="copy", acodec="copy", print e raise SystemExit +def get_video_length(filename): + output = subprocess.check_output(("ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", filename)).strip() + video_length = int(float(output)) + print "Video length in seconds: "+str(video_length) + + return video_length def split_by_seconds(filename, split_length, vcodec="copy", acodec="copy", extra="", **kwargs): @@ -90,45 +86,29 @@ def split_by_seconds(filename, split_length, vcodec="copy", acodec="copy", print "Split length can't be 0" raise SystemExit - output = subprocess.Popen("ffmpeg -i '"+filename+"' 2>&1 | grep 'Duration'", - shell = True, - stdout = subprocess.PIPE - ).stdout.read() - print output - matches = re_length.search(output) - if matches: - video_length = int(matches.group(1)) * 3600 + \ - int(matches.group(2)) * 60 + \ - int(matches.group(3)) - print "Video length in seconds: "+str(video_length) - else: - print "Can't determine video length." - raise SystemExit + video_length = get_video_length(filename) split_count = int(math.ceil(video_length/float(split_length))) if(split_count == 1): print "Video length is less then the target split length." raise SystemExit - split_cmd = "ffmpeg -i '%s' -vcodec %s -acodec %s %s" % (filename, vcodec, - acodec, extra) + split_cmd = ["ffmpeg", "-i", filename, "-vcodec", vcodec, "-acodec", acodec] + shlex.split(extra) try: filebase = ".".join(filename.split(".")[:-1]) fileext = filename.split(".")[-1] except IndexError as e: raise IndexError("No . in filename. Error: " + str(e)) for n in range(0, split_count): - split_str = "" + split_args = [] if n == 0: split_start = 0 else: split_start = split_length * n - split_str += " -ss "+str(split_start)+" -t "+str(split_length) + \ - " '"+filebase + "-" + str(n) + "." + fileext + \ - "'" - print "About to run: "+split_cmd+split_str - output = subprocess.Popen(split_cmd+split_str, shell = True, stdout = - subprocess.PIPE).stdout.read() + split_args += ["-ss", str(split_start), "-t", str(split_length), + filebase + "-" + str(n) + "." + fileext] + print "About to run: "+" ".join(split_cmd+split_args) + subprocess.check_output(split_cmd+split_args) def main(): From 6547271dfba0d47ff68eba7d9ab5d24f3c5bce3a Mon Sep 17 00:00:00 2001 From: Csaba Henk Date: Mon, 1 Jan 2018 19:20:42 +0100 Subject: [PATCH 2/3] enhance chunk naming Instead of chunk index (0 to n-1) we apply "chunk index (1 to n)-of-n". --- ffmpeg-split.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ffmpeg-split.py b/ffmpeg-split.py index 6b8ae9e..e4f8b76 100644 --- a/ffmpeg-split.py +++ b/ffmpeg-split.py @@ -106,7 +106,8 @@ def split_by_seconds(filename, split_length, vcodec="copy", acodec="copy", split_start = split_length * n split_args += ["-ss", str(split_start), "-t", str(split_length), - filebase + "-" + str(n) + "." + fileext] + filebase + "-" + str(n+1) + "-of-" + \ + str(split_count) + "." + fileext] print "About to run: "+" ".join(split_cmd+split_args) subprocess.check_output(split_cmd+split_args) From 979e0b9ed1eb4491966b28d97bbd476004b18d34 Mon Sep 17 00:00:00 2001 From: Csaba Henk Date: Mon, 1 Jan 2018 21:30:14 +0100 Subject: [PATCH 3/3] add some smart calculations Add the --split-{chunks,filesize} options to split to a number of chunks / split with chunks of capped size. For --split-filesize, also adding two auxiliary options: --chunk-strategy={eager,even} --filesize-factor= --- ffmpeg-split.py | 68 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/ffmpeg-split.py b/ffmpeg-split.py index e4f8b76..4f63c61 100644 --- a/ffmpeg-split.py +++ b/ffmpeg-split.py @@ -80,14 +80,18 @@ def get_video_length(filename): return video_length +def ceildiv(a, b): + return int(math.ceil(a / float(b))) + def split_by_seconds(filename, split_length, vcodec="copy", acodec="copy", - extra="", **kwargs): + extra="", video_length=None, **kwargs): if split_length and split_length <= 0: print "Split length can't be 0" raise SystemExit - video_length = get_video_length(filename) - split_count = int(math.ceil(video_length/float(split_length))) + if not video_length: + video_length = get_video_length(filename) + split_count = ceildiv(video_length, split_length) if(split_count == 1): print "Video length is less then the target split length." raise SystemExit @@ -127,6 +131,35 @@ def main(): type = "int", action = "store" ) + parser.add_option("-c", "--split-chunks", + dest = "split_chunks", + help = "Number of chunks to split to", + type = "int", + action = "store" + ) + parser.add_option("-S", "--split-filesize", + dest = "split_filesize", + help = "Split or chunk size in bytes (approximate)", + type = "int", + action = "store" + ) + parser.add_option("--filesize-factor", + dest = "filesize_factor", + help = "with --split-filesize, use this factor in time to" \ + " size heuristics [default: %default]", + type = "float", + action = "store", + default = 0.95 + ) + parser.add_option("--chunk-strategy", + dest = "chunk_strategy", + help = "with --split-filesize, allocate chunks according to" \ + " given strategy (eager or even)", + type = "choice", + action = "store", + choices = ['eager', 'even'], + default = 'eager' + ) parser.add_option("-m", "--manifest", dest = "manifest", help = "Split video based on a json manifest file. ", @@ -156,13 +189,32 @@ def main(): ) (options, args) = parser.parse_args() - if options.filename and options.manifest: - split_by_manifest(**(options.__dict__)) - elif options.filename and options.split_length: - split_by_seconds(**(options.__dict__)) - else: + def bailout(): parser.print_help() raise SystemExit + if not options.filename: + bailout() + + if options.manifest: + split_by_manifest(**(options.__dict__)) + else: + video_length = None + if not options.split_length: + video_length = get_video_length(options.filename) + file_size = os.stat(options.filename).st_size + split_filesize = None + if options.split_filesize: + split_filesize = int(options.split_filesize * options.filesize_factor) + if split_filesize and options.chunk_strategy == 'even': + options.split_chunks = ceildiv(file_size, split_filesize) + if options.split_chunks: + options.split_length = ceildiv(video_length, options.split_chunks) + if not options.split_length and split_filesize: + options.split_length = int(split_filesize / float(file_size) * video_length) + if not options.split_length: + bailout() + split_by_seconds(video_length=video_length, **(options.__dict__)) + if __name__ == '__main__': main()