Guide Index Quick Links

Faster Cinepak Encoding with GNU Parallel

Created on January 18, 2023

This is not so much of a guide as it is a template for you to go off of to help speed up the encoding of videos in the Cinepak format. It's intended to supplement the actual guide on encoding in formats supported by old machines which I first wrote a couple years back. If you've already tried encoding a video to Cinepak, you'll probably know that FFmpeg's implementation of it is incredibly slow. I've developed a workaround against the slowness that uses all the CPU cores available in a system.

This is a Bash script, so you'll need some form of Linux or Unix at the very least in order to use it. It assumes you have FFmpeg and GNU Parallel installed already. If you don't, you should install the ffmpeg and parallel packages or get these programs installed in some other way.

You can copy and paste the following text into a new shell script file. One thing to be mindful of is the tmpdir variable. It's set to . by default to represent the current working directory, but if you have a tmpfs directory or other sort of RAM disk handy, you should change the variable to point there to reduce strain on your disk. Do not append a trailing slash to the path.

#!/usr/bin/env bash

# It's recommended that you set this to a tmpfs
# directory so the encodes are written to RAM
# prior to being combined into a single clip.
# Default is "." meaning current directory

# Generate a random hash some files will use
# to avoid conflicts with anything else
rando=$(echo $RANDOM | md5sum | head -c 20)

# In case the user exits prematurely,
# clean up leftover files
abort () {
	rm "$tmpdir"/.$rando* 2> /dev/null
trap abort SIGINT

# Returns the number of cores or threads
cores=$(parallel --number-of-cores)

# Error handling
if [ -z "$1" ]; then
	echo "A filename must be specified!"
	exit 1
elif [ ! -f "$1" ]; then
	echo "The file does not exist!"
	exit 2
elif [ $cores -eq 0 ]; then
	# Avoiding (possibly improbable) division
	# by zero error
	echo "Zero cores reported, exiting"
	exit 3

# Get precise duration of clip
duration=$(ffprobe -hide_banner -i "$1" -v quiet \
	-show_entries format=duration \
	-of default=noprint_wrappers=1:nokey=1)

# Default to Cinepak codec if not specified
[ -z "$2" ] && codec="cinepak"

# Replace file extension with .avi

# Arguments are written to a named pipe.
# For this to work correctly, they must
# be combined into one gob of output
# before writing the whole thing into it.
mkfifo "$tmpdir/.$rando"
# bc is better for division.
segdur=$(echo "scale=1; $duration / $cores" \
	| bc | awk '{printf "%f", $0}')
# If at the first segment, do not add -ss,
# and do not add -t at the last one.
# Output file is (random_hash)_(segment).avi
for (( i=1; i<=$cores; ++i)); do
	# Buffer arguments for a segment
	echo $([ ! $i -eq 1 ] && echo -ss $start) \
		$([ $i -lt $cores ] && echo -t $segdur) \
	# Advance the starting point for the next segment
	start=$(echo "scale=1; $start + $segdur" \
		| bc | awk '{printf "%f", $0}')
} > "$tmpdir/.$rando" &

# Encode multiple fragments of the same clip at once.
# Adjust some of the ffmpeg parameters as needed.
parallel --bar -j$cores "ffmpeg -loglevel quiet \
	-i \"$1\" \
	-c:v \"$codec\" \
	-vf \"scale=-4:'min(240,ih)'\" \
	-r 15 \
	-c:a pcm_u8 -ar 8000 -ac 1 {=uq=}" \
	:::: "$tmpdir/.$rando"

# Now, concatenate them back into one clip
printf "file '$tmpdir/.${rando}_%s.avi'\n" \
	$(seq $cores) > "$tmpdir/.$rando" &
echo "Finished, combining into $outfile..."
ffmpeg -loglevel quiet -f concat -safe 0 \
	-i "$tmpdir/.$rando" -c copy "$outfile"

# Remove temporary files as well as the named pipe
rm "$tmpdir"/.$rando*

What this script basically does is take the source video and encode it to many split fragments, with each immediately following the previous. The number of fragments it creates is determined by how many cores GNU Parallel reports are available. The temporary output files are sequentially numbered, and when all of them are finished, an additional execution of FFmpeg is run to concatenate all of the fragments back into one clip at the working directory.

The required arguments for start/cutoff times and the output filenames are written to a named pipe, which works a little bit differently from a plain old file on the surface. Named pipes have other applications, but here it's just being used as a temporary buffer for text. The concatenation list is written to the same pipe as well using a single printf command in combination with seq.

As soon as it is done with everything (or the user presses <CTRL> + C to abort the script), it cleans up all of the temporary files it created, including the named pipe and the clip fragments.

Note the use of bc for performing certain calculations, particularly for division. The scale=1 argument is passed to it so that the each fragment's duration has one-tenth precision. This should be enough to avoid repeating fragments on systems with high core counts working with very short clips, as well as potential loss of audio synchronization when trying to use higher precision.

The actual ffmpeg command being run uses ideal settings for a Cinepak AVI - proportionally downscaling a video to a height of 240 pixels if it is larger, setting it to 15 FPS, and using the pcm_u8 audio codec at 8000Hz in mono. You can change these if you want, but setting them higher could result in more space being used up, as well them possibly being harder to play back on slower systems.

The --bar argument is used with Parallel to display a progress bar to show how many fragments have been fully encoded so far. Initially, this bar will not move very much, but when it does, it should be an indicator that the clip is almost ready.

To run this script, first enable the execute bit for the owner:

chmod u+x cinepak_fast.sh

Then run it with a source file as the argument:

./cinepak_fast.sh mcdonals.mp4

The resulting output should be a new video labeled mcdonals.avi.

In case you want to use the Microsoft Video 1 codec for reduced size at the cost of quality, you can pass msvideo1 as a second argument:

./cinepak_fast.sh mcdonals.mp4 msvideo1

You can also prepend this with time if you want to compare how long this script takes against running a standalone ffmpeg command writing to one file, being constrained by the single-threaded Cinepak encoder. I ran a test on one of my 30 second videos, and with the script using a 16 core CPU, it took only 10 seconds, while using one thread took a full minute and 33 seconds!


No comments for this page.

Leave a Comment

Name: (required)

Website: (optional)

Maximum comment length is 1000 characters.
First time? Read the guidelines

Enter the text shown in the image: