FFmpeg

Caoimhe

I have posted before about using FFmpeg to restore The Big O’s original intro and also my Jellyfin server. I am going to talk about the former again but first some more detail on the latter.

My Jellyfin server is actually just my desktop, which acts as server for everything on my home network. It has a 12TiB hard drive for storage which is divided up into a few partitions, one of which is my “library” partition of important files I want to keep, and which I make regular backups of, and one of which is my “media” partition that holds the files for my Jellyfin server. The media partition was running out of space (I may go a bit extreme with the bluray rips) and the library had several times more free space than used, so I decided to try to resize them and grow the media partition into some of the library partition’s empty space.

I just used KDE’s built-in partition manager for this, which successfully shrank the library partition but for some reason failed when trying to grow and move the media partition. I don’t know why this happened and am just hoping there’s no hardware problems. Nothing from the library partition was lost (and it was all backed up anyway) but the media partition was gone and so I’ve had to rebuild my Jellyfin library. This is not a huge deal it’s just been a little time-consuming but one of the things that was lost was, of course, my edited Big O episodes, which meant that I had to redo splicing the original intro in. But I had a fresh head again, free of the frustrations accumulated while trying to do this the first time, and I decided to do it better and actually get to grips with FFmpeg’s filter syntax. Here are the commands I ended up with:

mkdir -p tmp
mkdir -p out
for v in The\ Big\ O*.mkv
	set b (basename "$v" ".mkv")
	ffmpeg -i intro.webm -ss 00:01:12.02 -i "$v" -filter_complex "[0:v] scale=1424:1080,setsar=1:1 [intro]; [intro][0:a][0:a][1:v][1:a:0][1:a:1] concat=n=2:v=1:a=2 [outv][outa];" -shortest -map "[outv]" -map "[outa]" -metadata:s:a:0 language=eng -metadata:s:a:1 language=jpn "tmp/$v"

	set subs "$b.ass"
	ffmpeg -itsoffset -4.42 -i "$v" "tmp/$subs"
	ffmpeg -i "tmp/$v" -i "tmp/$subs" -shortest -map 0 -map 1 -c copy -metadata:s:s:0 language=eng "out/$v"
end

Let’s break down what’s happening here. First I create two directories, tmp and out. tmp is where temporary working files are going to be written to and out is for the final files when we’re finished processing.

Then loop over each episode matroška file with the file name for each one assigned to $v inside the loop. The file name without the file extension is set to the variable $b. I’m using a Fish shell here rather than Bash so the syntax is a little different to Bash.

Then the big command. We pass in the first input, intro.webm, which is the intro that I downloaded off of Youtube. Our second input is the episode, with the seek parameter -ss telling FFmpeg to skip to one and twelve point zero two seconds in when reading it. This is, unintuitively, set before you specify the input it applies to, not after.

Then the big -filter_complex. This takes a big string that takes filter definitions separated by semicolons. Each filter has input and output streams identified by labels in square brackets.

The first filter is [0:v] scale=1424:1080,setsar=1:1 [intro]. Its input is [0:v], the video stream from the first input1, i.e., intro.webm. It then resizes it to a resolution of 1,424×1080 pixels and sample aspect ratio of 1:1 and outputs it to a new stream labelled [intro]. The [intro] stream now has the same resolution as our episodes which will allow us to concatenate them in the next filter.

The second filter is [intro][0:a][0:a][1:v][1:a:0][1:a:1] concat=n=2:v=1:a=2 [outv][outa]. Let’s start in the middle here. concat=n=2:v=1:a=2 means that we are going to concatenate two segments (n=2) which each have one video stream (v=1) and two audio streams (a=2). Those two audio streams are going to be the English and Japanese dubs.

The inputs for this filter are [intro][0:a][0:a][1:v][1:a:0][1:a:1], which can be divided into our two segments—[intro][0:a][0:a] and [1:v][1:a:0][1:a:1]—which each have one video and two audio streams specified. The first segment has our resized intro video stream, [intro], and [a:0] is the audio from our first input (the intro again) specified twice because we are going to combine the same intro audio with both the English and Japanese episode audio. The second segment has the video and two audio streams from our second input file; the episode itself and its English and Japanese audio tracks.

The concatenation then has two output streams, [outv][outa], the video and audio.

Then the rest of the command: -shortest makes sure that the output of the command is equal to the shortest stream in the output, i.e. if your output has five minutes of video but only two minutes of audio then the output will be two minutes long rather than five minutes of video with three minutes of silence. I think that shouldn’t really be needed here but I was using it while testing and forgot to remove it and thought it would be dishonest to take it out for the post when it was what I actually ran.

-map "[outv]" -map "[outa]" defines what streams to include in the output, which here is simply the output streams of our concatination.

-metadata:s:a:0 language=eng -metadata:s:a:1 language=jpn labels the audio output streams as being English and Japanese, respectively, so that media players can display that information.

And then the last part of the command is ouputting the video to the tmp folder.

This gives us output files with the original intro with both dub tracks preserved, which is more than I had last time and with a lot less processing. But if I am going to include the Japanese audio I probably also want subtitles for that and unfortunately the -ss parameter does not seem to correctly offset the subtitles. If I want subtitles with correct timing I will have to fix them with another command.

First set a variable, $subs to the file name we want for the subtitles in the Advanced SubStation format.

Then read the original episode file into FFmpeg again with a negative offset of 4.42 seconds (-itsoffset -4.42) and write the subtitle data to a file in the tmp folder.

The last command is taking in out output video and the subtitle file and recombining them, using another metadata command to label the subtitle track as English, setting the codec mode (-c) to copy so that the audio and video do not get re-encoded and writing the finished file to the out folder.

I didn’t bother fixing the chapters this time.

  1. FFmpeg indexes from 0, so [0:v] refers to the video stream from the first input, [1:a:0] refers to the first audio stream from the second input, etc. 


Caoimhe

I have an updated version of these commands in a new bog post.

I have been rewatching The Big O with my partner from bluray rips and as nice as it is to watch it in so much higher quality than when I saw it as a kid but the bluray release lacks the original iconic intro, which is presumably related to the fact that it’s basically Flash by Queen over the visuals of the intro for Ultraseven.

I know enough FFmpeg to be a danger to myself so I decided to spend far too much time banging my head against my keyboard until I managed to splice the original intro into all the episodes. There was a good bit of trial and error and fixing things and adjusting commands but I decided to document a cleaned up version of the steps mostly as a reference material for myself if I decide to do something like this in the future but if it helps anyone else then that’s cool.

What I have below is definitely not the best way to do this. I ended up reprocessing the same videos multiple times which is inherently going to result in a loss in quality and I lost information like subtitles and the Japanese audio track by converting from Matroška files to plain MPEG-4s but I wasn’t using those anyway.

1. Prepare the intro

I used yt-dlp to download the intro from Youtube as it wasn’t included in the bluray files and then blew it up to the same resolution as the bluray rips, 1424×1080.

yt-dlp 'https://www.youtube.com/watch?v=s7_Od9CmTu0'
ffmpeg -i The\ Big\ O\ Opening⧸Intro\ Theme\ \[720p\]\ \[s7_Od9CmTu0\].webm -vf "scale=1424:1080,setsar=1:1" intro.mp4

2. Preparing files

I copied all the episodes that had the intro I wanted to replace into a folder. Episodes one and two of the first series and episodes one, eight and thirteen of the second series have special intros so no processing needed to be done on them. For step 4 it turned out that spaces in the filenames broke the command and I couldn’t figure out how to properly escape them so while preparing the files also remove any spaces or other characters that might cause problems from the filenames.

3. Strip the existing intro

I loaded episodes up in Kdenlive just to check the exact length of the existing intro on the episodes and found it to be 1′12″ and so wrote a command to iterate over all the files and write out an MP4 version with the English audio track (the second audio track in the file, but mapped as 1 in the command as FFmpeg indexes from 0) with that much time cut from the start.

I use a Fish shell rather than Bash. If you use Bash or a different shell you will need to adjust the commands.

for v in *.mkv
	set b (basename "$v" ".mkv")
	ffmpeg -i "$v" -ss 00:01:12.01 -map 0:v -map 0:a:1 "$b-nointro.mp4"
end

There were a couple of episodes where it turned out that there was still one frame of the old intro left at the start so those had to be reprocessed with the start time set to 00:01:12.02.

4. Splice in the original intro

I moved the downloaded intro file into the same folder as the episodes and spliced it into the files. This broke when there were spaces in the filenames and I wasn’t able to escape it properly so I ended up just stripping spaces out and renaming the files back with KRename afterwards.

for v in *-nointro.mp4
	set b (basename "$v" "-nointro.mp4")
	ffmpeg -i intro.mp4 -i "$v" -filter_complex "movie=intro.mp4, scale=1424:1080 [v1] ; amovie=intro.mp4 [a1] ; movie=$v, scale=1424:1080 [v2] ; amovie=$v [a2] ; [v1] [v2] concat [outv] ; [a1] [a2] concat=v=0:a=1 [outa]" -map "[outv]" -map "[outa]" "$b-intro.mp4"
end

5. Fixing chapter metadata

The episodes had some chapter metadata dividing up parts of the episode, with the first one covering just the episode intros, which cutting out that part of the video removed. I decided to fix that with the versions with the restored intro, even though obviously I am never actually going to skip it in practise. The first step of this was outputting the metadata to a text file.

for v in *-intro.mp4
	set b (basename "$v" "-intro.mp4")
	ffmpeg -i "$v" -f ffmetadata "$b.txt"
end

I then manually adjusted the metadata entry to restore the Chapter 01 entry, putting it above the existing chapters in the file. Most of them had their Chapter 01 entry wiped out so I just added it in above the Chapter 02 entry, though some of them still had a Chapter 01 lasting just a few milliseconds so for those I just modified the END time for it. The new intro was 1′7.61″ which meant a 67,610ms timestamp. I also changed the START entry for every Chapter 02 to 67610 to match.

[CHAPTER]
TIMEBASE=1/1000
START=0
END=67610
title=Chapter 01

Once they were all updated I applied the modified chapter metadata back to the files

for v in *-intro.mp4
	set b (basename "$v" "-intro.mp4")
	ffmpeg -i "$v" -i "$b.txt" -map_chapters 1 -codec copy "$b.mp4"
end

6. Done!

Then I deleted all the intermediary files and dropped the processed files into my Jellyfin server along with the episodes that didn’t need fixing.