Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Merge Files with Chapters
import datetime
import json
import os
import subprocess
import sys
#############
### USAGE ###
#############
if len(sys.argv) < 4:
print("Usage:")
print("{} [input file] [input file] [output file]".format(sys.argv[0]))
print("")
print("Both files are assumed to have their chapters")
print("entered correctly and completely.")
sys.exit(0)
########################
### Get Chapter List ###
########################
def getChapterList(videoFile):
# Get chapter list as JSON
result = subprocess.run(
["ffprobe", "-print_format", "json", "-show_chapters", videoFile],
capture_output=True,
text=True
)
# Load the JSON
fileJson = json.loads(result.stdout)['chapters']
# Map to Python object:
# {
# id: 1
# start: 123.456
# end: 789.012
# }
chapters = list(map(
lambda c: {
'index': c['id'],
'start': float(c['start_time']),
'end': float(c['end_time']),
'title': c['tags']['title']},
fileJson))
return list(chapters)
########################
### Video 1 Duration ###
########################
# Get the duration of the first video
result = subprocess.run(
["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", sys.argv[1]],
capture_output=True,
text=True
)
# Get the result and trim off the trailing newline.
file1duration = float(result.stdout.rstrip())
print("{} duration is {} seconds.".format(sys.argv[1], file1duration))
############################
### Video 2 Chapter List ###
############################
def chapterMigrator(chapter):
startTime = chapter['start']
endTime = chapter['end']
offsetStartTime = file1duration + startTime
offsetEndTime = file1duration + endTime
return {'index': chapter['index'], 'start': offsetStartTime, 'end': offsetEndTime, 'title': chapter['title']}
video2rawchapters = getChapterList(sys.argv[2])
# Migrate these chapters to be offset from the end of the first file
video2chapters = list(map(chapterMigrator, video2rawchapters))
print("{} has {} chapters.".format(sys.argv[2], len(video2chapters)))
###########################
### Get file 1 metadata ###
###########################
result = subprocess.run(
["ffmpeg", "-i", sys.argv[1], "-f", "ffmetadata", "-"],
capture_output=True,
text=True
)
metadata = result.stdout
##################################
### Append file 2 chapter list ###
##################################
metadataFileName = "metadata.txt"
# Note the timestamps are in milliseconds, and should be integers.
for c in video2chapters:
metadata += f"""
[CHAPTER]
TIMEBASE=1/1000
START={int(c['start'] * 1000)}
END={int(c['end'] * 1000)}
title={c['title']}"""
with open(metadataFileName, "w") as metadataFile:
metadataFile.write(metadata)
############################
### Join two video files ###
############################
fileListFileName = "files.txt"
fileList = f"""
file {sys.argv[1]}
file {sys.argv[2]}"""
with open(fileListFileName, "w") as fileListFile:
fileListFile.write(fileList)
if os.path.exists(sys.argv[3]):
os.remove(sys.argv[3])
print("Joining {} and {} into {}...".format(sys.argv[1], sys.argv[2], sys.argv[3]))
result = subprocess.run(
["ffmpeg", "-f", "concat", "-i", fileListFileName, "-i", metadataFileName, "-map_metadata", "1", "-c", "copy", sys.argv[3]],
capture_output=False
)
print("...file {} created.".format(sys.argv[3]))
# Clean up.
os.remove(metadataFileName)
os.remove(fileListFileName)
@eugenesvk

This comment has been minimized.

Copy link

@eugenesvk eugenesvk commented Feb 12, 2021

Just FYI ffmpeg does indeed seem to miss this concat feature as acknowledged in their bug tracker

@cliss

This comment has been minimized.

Copy link
Owner Author

@cliss cliss commented Feb 13, 2021

@eugenesvk Huh, thanks for pointing that out.

This most recent activity does not instill confidence, however:

Changed 2 years ago by dextro

@nathanchere

This comment has been minimized.

Copy link

@nathanchere nathanchere commented Apr 7, 2021

There were some minor quirks with file names and dropped audio streams but otherwise this worked a treat, thanks for sharing and saving me a lot of time and frustration!

By the way if you want to make sure it copies all audio streams, change line 128 like so:

    ["ffmpeg", "-f", "concat", "-i", fileListFileName,"-i", metadataFileName, "-map_metadata", "1",  "-map", "0", "-c", "copy", sys.argv[3]],

Basically adding -map 0 just before the copy command, more info here if you're interested how it works: https://trac.ffmpeg.org/wiki/Concatenate#samecodec

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment