|
- #!/bin/python
- """jsonclips.py - generate highlights based on JSON
-
- usage: jsonclips.py clips.json
-
- JSON file can specify:
- - output filename
- - source filepath
- - start time
- - duration
- - transition type [default: luma]
- - transition duration [default: 30]
- """
-
- TRANSITION_DURATION = 2 # seconds
-
- FFMPEG_COMMON_ARGS = ["ffmpeg", "-loglevel", "quiet", "-hide_banner"]
-
-
- def process_highlights(jsonfile):
- """Go through highlights, make clips, join to a video"""
- import tempfile
-
- def parse_time(timestring):
- from datetime import datetime
- """Custom time parsing function"""
- if timestring.count(":") == 0:
- # probably seconds
- formatstring = "%S"
- elif timestring.count(":") == 1:
- formatstring = "%M:%S"
- elif timestring.count(":") == 2:
- formatstring = "%H:%M:%S"
-
- if "." in timestring:
- formatstring += ".%f"
-
- return datetime.strptime(timestring, formatstring)
-
- def datetime_to_seconds(dt):
- """Take a datetime object and convert to seconds"""
- from datetime import datetime
-
- zero = datetime.strptime("0", "%S")
- return (dt - zero).seconds
-
- def make_clips(sources=None, outputdir=None):
- """Use ffmpeg to create output from processed JSON"""
- import subprocess
- import os
-
- clipnum = 0
-
- for source in sources:
- # decide on duration vs start+end
- if "duration" not in source:
- try:
- start = parse_time(source["start"])
- end = parse_time(source["end"])
- duration = (end - start).seconds
- start = datetime_to_seconds(start)
- except ValueError as e:
- print("Error: {}".format(e))
- else:
- start = source["start"]
- duration = source["duration"]
-
- print("Making {}s clip from {} at {}".format(
- duration, source["filepath"], start))
-
- ffmpeg_file_args = ["-ss", str(start),
- "-i", source["filepath"],
- "-t", str(duration),
- "-c", "copy",
- os.path.join(outputdir,
- "clip{}.mkv".format(clipnum))]
-
- ffmpeg_args = FFMPEG_COMMON_ARGS + ffmpeg_file_args
- print("Running ffmpeg with args:\n\n{}".
- format(ffmpeg_args))
- subprocess.run(ffmpeg_args)
-
- clipnum += 1
-
- def concat(workingdir=None, outputfile=None):
- """Use ffmpeg concat to join clips"""
- import subprocess
- import os
-
- joinfile = os.path.join(workingdir, "join.txt")
- ffmpeg_concat_args = ["-y", "-safe", "0",
- "-f", "concat",
- "-i", str(joinfile),
- "-c", "copy",
- str(outputfile)]
-
- print(FFMPEG_COMMON_ARGS + ffmpeg_concat_args)
- subprocess.run(FFMPEG_COMMON_ARGS + ffmpeg_concat_args)
-
- def parse_json(jsonfile=None):
- """Parse global / per-clip options from jsonfile
-
- Requires:
- - outputfile
- - list of sources
- """
- import json
- import sys
-
- if jsonfile:
- with open(jsonfile, "r") as fh:
- json_in = json.load(fh)
-
- if "outputfile" not in json_in:
- print("Please specify an outputfile to write to")
- sys.exit(1)
-
- if "sources" not in json_in:
- print("Please specify some sources to process")
- sys.exit(1)
-
- return json_in
-
- def split_first(videofile=None):
- """Split first videofile into videofileM + videofileO"""
- from basicclips import get_video_duration
- import subprocess
-
- video_length = get_video_duration(videofile)
-
- # main clip: full length minus half transition duration
- ffmpeg_video_args = ["-i", videofile,
- "-t", str(video_length - 1),
- "-c", "copy",
- "{}".format(videofile.replace(".mkv", "M.mkv"))]
-
- subprocess.run(FFMPEG_COMMON_ARGS + ffmpeg_video_args)
-
- # omega clip: half transition duration
- ffmpeg_video_args = ["-i", videofile,
- "-ss", str(video_length - 1),
- "-c:v", "libx264", "-crf", "20", "-c:a", "copy",
- "{}".format(videofile.replace(".mkv", "O.mkv"))]
-
- subprocess.run(FFMPEG_COMMON_ARGS + ffmpeg_video_args)
-
- def split_last(videofile=None):
- """Split last videofile into videofileA + videofileM"""
- import subprocess
-
- # alpha clip: half transition duration
- ffmpeg_video_args = ["-i", videofile,
- "-t", str(TRANSITION_DURATION/2),
- "-c:v", "libx264", "-crf", "20", "-c:a", "copy",
- "{}".format(videofile.replace(".mkv", "A.mkv"))]
-
- subprocess.run(FFMPEG_COMMON_ARGS + ffmpeg_video_args)
-
- # main clip: start at half transition duration, full length
- ffmpeg_video_args = ["-i", videofile,
- "-ss", str(TRANSITION_DURATION/2),
- "-c", "copy",
- "{}".format(videofile.replace(".mkv", "M.mkv"))]
-
- subprocess.run(FFMPEG_COMMON_ARGS + ffmpeg_video_args)
-
- def split_middle(videofile=None):
- """Split middle videofile into videofileA + videofileM + videofileO"""
- from basicclips import get_video_duration
- import subprocess
-
- video_length = get_video_duration(videofile)
-
- # alpha clip: half transition duration
- ffmpeg_video_args = ["-i", videofile,
- "-t", str(TRANSITION_DURATION/2),
- "-c:v", "libx264", "-crf", "20", "-c:a", "copy",
- "{}".format(videofile.replace(".mkv", "A.mkv"))]
-
- subprocess.run(FFMPEG_COMMON_ARGS + ffmpeg_video_args)
-
- # main clip: full length minus half transition duration
- ffmpeg_video_args = ["-i", videofile,
- "-ss", str(TRANSITION_DURATION/2),
- "-t", str(video_length - (TRANSITION_DURATION/2)),
- "-c", "copy",
- "{}".format(videofile.replace(".mkv", "M.mkv"))]
-
- subprocess.run(FFMPEG_COMMON_ARGS + ffmpeg_video_args)
-
- # omega clip: half transition duration
- ffmpeg_video_args = ["-i", videofile,
- "-ss",
- str(video_length - (TRANSITION_DURATION/2)),
- "-c:v", "libx264", "-crf", "20", "-c:a", "copy",
- "{}".format(videofile.replace(".mkv", "O.mkv"))]
-
- subprocess.run(FFMPEG_COMMON_ARGS + ffmpeg_video_args)
-
- def transition_join(video1=None, video2=None, workingdir=None, i=None):
- """Join videos 1 and 2 with a transition"""
- from moviepy.editor import VideoFileClip
- from moviepy.editor import CompositeVideoClip
-
- tr = TRANSITION_DURATION/2
-
- try:
- clip1 = VideoFileClip(os.path.join(workingdir, video1))
- except IOError as e:
- print("Error making joined clip: {}".format(e))
- import pdb
- pdb.set_trace()
-
- clip2 = VideoFileClip(os.path.join(workingdir, video2))
-
- final = CompositeVideoClip([clip1,
- clip2.set_start(clip1.end-tr)
- .crossfadein(tr)])
-
- outputfile = os.path.join(workingdir, "clip{}J.mkv".format(i))
- final.write_videofile(outputfile, codec="libx264", audio_codec="aac")
-
- return outputfile
-
- # Start
- import os
-
- if jsonfile:
- json_in = parse_json(jsonfile)
-
- # in temporary dir
- with tempfile.TemporaryDirectory() as tmpdir:
- # make main clips using codec copy
- # from clip0.mkv to clip[len(sources)-1]
- make_clips(sources=json_in["sources"], outputdir=tmpdir)
- # make subclips (main + transition subclips)
- # add to concat 'join.txt' as we go
- joinfile = os.path.join(tmpdir, "join.txt")
- with open(joinfile, "w") as fh:
- for i in range(len(json_in["sources"])):
- # First clip needs only main + end
- if i == 0:
- split_first(os.path.join(tmpdir, "clip0.mkv"))
- fh.write("file '{}'\n".format(
- os.path.join(tmpdir, "clip0M.mkv")))
- # End clip needs only start + main
- elif i == len(json_in["sources"]):
- split_last(os.path.join(tmpdir, "clip{}.mkv".format(i)))
- # Join previous clip omega + this clip alpha
- joined = transition_join("clip{}O.mkv".format(i-1),
- "clip{}A.mkv".format(i),
- tmpdir,
- i)
- fh.write("file '{}'\n".format(joined))
- fh.write("file '{}'\n".format(
- os.path.join(tmpdir, "clip{}M.mkv".format(i))))
- # Others need start + main + end
- else:
- split_middle(os.path.join(tmpdir, "clip{}.mkv".format(i)))
- # Join previous clip omega + this clip alpha
- joined = transition_join("clip{}O.mkv".format(i-1),
- "clip{}A.mkv".format(i),
- tmpdir,
- i)
- fh.write("file '{}'\n".format(joined))
- fh.write("file '{}'\n".format(
- os.path.join(tmpdir, "clip{}M.mkv".format(i))))
- # Done, join all of the above
- print("Making concatenated video: {}".format(json_in["outputfile"]))
- with open(os.path.join(tmpdir, "join.txt"), "r") as fh:
- for line in fh:
- print(line)
- concat(workingdir=tmpdir, outputfile=json_in["outputfile"])
-
-
- if __name__ == "__main__":
- import sys
-
- process_highlights(sys.argv[1])
|