Skip to content

Instantly share code, notes, and snippets.

@rgov
Created February 11, 2018 06:02
Show Gist options
  • Save rgov/97a77ebd23c8de031783f57c2fe306b1 to your computer and use it in GitHub Desktop.
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
#!/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