I recently wanted to create animated GIFs from videos. The idea was to get video previews, in a very lightweight file. After a quick search online, I found FFMPEG, a fantastic multimedia framework to manipulate media. There is also a few wrappers that exists in different languages (ex: C#, JavaScript) but you still need to install FFMPEG locally, and I didn't want that. In fact, I wanted a simple solution that doesn't require any installation locally and something in the cloud. In this post, I want to share how I achieved the first one.
All the code and the container are available on Github and Docker Hub.
First Contact
The ffmpeg framework is very powerful and can do so many things; therefore it's normal that it has a ton of possible parameters and extensions. After time spent on the documentation and a few trials and errors, I found how to do exactly what I needed calling it this way:
ffmpeg -r 60 -i $INPUTFILE -loop 0 -vf scale=320:-1 -c:v gif -f gif -ss 00:00:00.500 -r 10 -t 5 - > $OUTPUTFILE
This will create a five second animated GIF from a video. It speeds up the video and lowers the framerate of the GIF to keep the output lightweight. Here is an example.
This is great, but this is not very friendly. How can someone who only creates a video once in a while be expected to remember all those parameters?! And even harder, when the video is vertical some parameters have different values. It was time to simplify, and here is how I did it. Note that I'm a Docker beginner and if you think there is a simpler or better way to do some steps, let me know, and let's learn together.
The Plan
The plan is simple: execute a simple Docker command like docker run fboucher/aciffmpeg -i NotInTheSky.mp4
and generate a video preview. To build our ephemeral container we will start with something lightweight like alpine,
install ffmpeg and add a script that would be executed as the container runs. That sounds like an excellent plan, let's do it!
Writing the Script
The script is simple, but I learned a few things writing it. This is why it's included in this post. The goal was simple: execute the ffmpeg command using some values from the parameters: file path, and if the video is vertical. Here is the script:
#!/bin/sh
while getopts ":i:v" opt; do
case $opt in
i) inputFile="$OPTARG"
;;
v) isVertical=true
;;
\?) echo "Invalid option -$OPTARG" >&2
exit 1
;;
esac
case $OPTARG in
-*) echo "Option $opt needs a valid argument"
exit 1
;;
esac
done
if [ -z "$isVertical" ]; then isVertical=false; fi
# used for bash
#IFS='.'
#read -a filePart <<< "$inputFile"
#outputFile="${filePart[0]}.gif"
# used for dash
filename=$(echo "$inputFile" | cut -d "." -f 1)
outputFile="$filename.gif"
if $isVertical
then
ffmpeg -r 60 -i $inputFile -loop 0 -vf scale=-1:320 -c:v gif -f gif -ss 00:00:00.500 -r 10 -t 5 - > $outputFile
else
ffmpeg -r 60 -i $inputFile -loop 0 -vf scale=320:-1 -c:v gif -f gif -ss 00:00:00.500 -r 10 -t 5 - > $outputFile
fi
Things I learned: Parameter without values
The script needs to be as friendly as possible, therefore any unnecessary information should be removed. Most videos will be horizontal, so let's make the parameter optional. However, I don't want users to have to specify the value script.sh -i myvideo.mp4 -v true
but instead script.sh -i myvideo.mp4 -v
. This is very simple to do, once you know it. On the first line of code when I get the parameters: getopts ":i:v"
notes that there is no ":" after the "v". This is to specify that we are not expecting any values.
Things I Learned: Bash and Dash
As mentioned earlier the container will be built from Alpine. And Alpine doesn't have bash but instead uses dash as a shell. It's mostly the same, but there are some differences. The first one will be the shebang (aka "#!/bin/sh" on the first line). And the second was the string manipulation. To generate a new file with the same name but a different extension of the script, split the file name at the ".". This can be done IFS ... read... <<<
command (commented in the script) on bash but this will give syntax error: unexpected redirection
and this is because there is no <<<
in bash. Instead, you need to use the command cut -d "." -f 1
(where -d specifies the CHAR to use as the delimiter, and -f return only this field).
Building the image
It's now time to connect all the dots in the dockerfile.
FROM alpine:3.13
LABEL Name=aciffmpeg Version=0.0.2
RUN apk add ffmpeg
COPY ./src/myscript.sh /
RUN chmod +x /myscript.sh
ENTRYPOINT ["/myscript.sh"]
The file is not extremely complex but let’s pass through it line by line.
- We start
FROM
Alpine version 3.13 and apply aLABEL
. RUN
Will execute the command to install ffmpeg. Theapk
is the default utility on Alpine to install apps just likeapt
on Ubuntu.COPY
Is copying the script from our local machine into the container at the root.- The second
RUN
command is to make sure the script is executable. - Finally,
ENTRYPOINT
will allow us to configure the container to run as an executable in this case as the script. All parameters passed to Docker will be passed to the script.
The only things left now are to build, tag, and push it on Docker Hub.
docker build -t fboucher/aciffmpeg .
docker tag 0f42a672d000 fboucher/aciffmpeg:2.0
docker push fboucher/aciffmpeg:2.0
The Simplified version
And now to create a preview of any video you just need to map a volume and specify the file path and optionally mention if the video is vertical.
On Linux/ WSL the command would look like this:
docker run -v /mnt/c/dev/test:/video fboucher/aciffmpeg -i /video/sample.mp4 -v
And on PowerShell like that:
docker run -v c/dev/test:/video fboucher/aciffmpeg -i /video/sample.mp4 -v
I learned a lot about Docker doing that project and now I have a very useful tool. What are the tools you built using containers that simplify your life or work?
Video Version
~frank