Created
February 11, 2018 06:02
-
-
Save rgov/97a77ebd23c8de031783f57c2fe306b1 to your computer and use it in GitHub Desktop.
A script for merging and transcoding sequences of videos produced by the Apeman action camera
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
''' | |
My Apeman-branded action camera for some reason splits videos across many | |
different MP4 files, each lasting only a few minutes. This script handles | |
merging these videos back together. It can automatically separate multiple | |
sequences in the same args.directory. | |
Requires FFmpeg (with the ffprobe tool). If FFmpeg is built with H.265 support, | |
this script can also transcode the video with the --hevc option. | |
''' | |
import argparse | |
import datetime | |
import json | |
import os | |
import re | |
import shutil | |
import subprocess | |
import tempfile | |
parser = argparse.ArgumentParser(description='Merge and convert Apeman videos') | |
parser.add_argument('dir', help='args.directory of videos') | |
parser.add_argument('--hevc', action='store_true', help='transcode to H.265') | |
args = parser.parse_args() | |
videos = [] | |
class Video(object): | |
def __init__(self): | |
self.predecessor = None | |
self.successor = None | |
# Enumerate video files in the args.directory | |
fpattern = re.compile( | |
r'([0-9]{4}_[0-9]{2}[0-9]{2}_[0-9]{2}[0-9]{2}[0-9]{2})_([0-9]{3})[.]mp4') | |
for f in os.listdir(args.dir): | |
v = Video() | |
v.path = os.path.join(args.dir, f) | |
m = fpattern.match(f) | |
if not m: continue | |
date, sequence = m.groups() | |
v.date = datetime.datetime.strptime(date, '%Y_%m%d_%H%M%S') | |
v.sequence = int(sequence) | |
videos.append(v) | |
# Get extended information about each video | |
for v in videos: | |
info = subprocess.check_output(['ffprobe', '-v', 'quiet', '-print_format', | |
'json', '-show_format', v.path]) | |
info = json.loads(info) | |
v.duration = datetime.timedelta(seconds=float(info['format']['duration'])) | |
# Match up each video with its successor | |
for v in videos: | |
for vv in videos: | |
if v is vv: continue | |
if vv.sequence != v.sequence + 1: | |
continue | |
if vv.date < v.date: | |
continue | |
if vv.date - v.date <= v.duration + datetime.timedelta(seconds=3.0): | |
v.successor = vv | |
vv.predecessor = v | |
break | |
# Split the linked lists into separate groups | |
groups = [] | |
for v in videos: | |
if v.successor != None: continue | |
group = [] | |
groups.append(group) | |
vv = v | |
while vv is not None: | |
group.insert(0, vv) | |
vv = vv.predecessor | |
# Sanity check that we grouped every video | |
assert len(videos) == sum(len(g) for g in groups) | |
# Print out what's going to be merged | |
for i, group in enumerate(groups): | |
print('Group', i) | |
for v in group: | |
print(' ', v.sequence, v.date, v.duration) | |
print() | |
def merge_videos(group): | |
listfile = tempfile.NamedTemporaryFile(mode='w', suffix='.txt') | |
for v in group: | |
listfile.write('file \'' + v.path + '\'\n') | |
listfile.flush() | |
mergedfile = tempfile.NamedTemporaryFile(suffix='.mov') | |
subprocess.check_call(['ffmpeg', '-y', '-f', 'concat', '-safe', '0', '-i', | |
listfile.name, '-c', 'copy', mergedfile.name]) | |
return mergedfile | |
def transcode_to_hevc(video): | |
# FIXME: This tries to convert the audio stream whether or not it exists, | |
# hopefully FFmpeg doesn't care. | |
hevcfile = tempfile.NamedTemporaryFile(suffix='.mov') | |
subprocess.check_call(['ffmpeg', '-y', '-i', video.name, | |
'-c:v', 'libx265', '-preset', 'medium', '-crf', '28', '-vtag', 'hvc1', | |
'-c:a', 'aac', '-b:a', '128k', hevcfile.name]) | |
return hevcfile | |
# Run the pipeline | |
for i, group in enumerate(groups): | |
print('Merging group', i) | |
merged = merge_videos(group) | |
product = merged | |
if args.hevc: | |
print('Transcoding group', i, 'to HEVC') | |
product = transcode_to_hevc(merged) | |
print('Creating output file video_%i.mov' % i) | |
shutil.copyfile(product.name, os.path.join(args.dir, 'video_%i.mov' % i)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment