Quick Hacks: A Script to Extract a Single Image/Frame From Video

Long ago, I posted the simple way to get a frame of a video using ffmpeg. I’ve been using that technique for a long time.

It can be a bit unwieldy for iteratively finding a specific frame, as when using a terminal you have to move the cursor to the time specification. So I wrote a very small wrapper script to put the time part at or towards the end:


#!/bin/bash
# f.sh - single frame

USAGE="f.sh infile timecode [outfile]"

if [ "$#" == "0" ]; then
        echo "$USAGE"
        exit 1
fi

if [ -e "$1" ]; then
        video="$1"
else
        echo "file not found: $1"
        exit 1
fi

if [ ! -z "$2" ]; then
        time="$2"
else
        echo "Need timecode!"
        exit 1
fi

# if we have a filename write to that, else imagemagick display

if [ ! -z "$3" ]; then
        echo "ffmpeg -i \"$video\" -ss $time  -vframes 1 -f image2 \"$3\""
        ffmpeg -loglevel quiet -hide_banner -ss $time -i "$video" -vframes 1 -f image2 "$3"
else
        echo "ffmpeg -i \"$video\" -ss $3  -vframes 1 -f image2 - | display"
        ffmpeg -hide_banner -loglevel quiet -ss $time  -i "$video" -vframes 1 -f image2 - | display
fi

Most of that is usage explanation, but broadly it has two modes:

  • display an image (f.sh video time)
  • write an image (f.sh video time image)

It’s more convenient to use it, hit ? and amend the time than to move the cursor into the depth of an ffmpeg command.

Rescuing a Truncated Recording Using ffmpeg

tl;dr: Well, it’s cheating because it’s not ‘rescuing’ what isn’t recorded, just retrieving and appending

Last night I played a few rounds of some cooperative gameplay with some good friends, and as usual I recorded the session for posterity.

Hans Volter's on fire and about to die
“Oh man, oh god!”, Hans is about to croak.

However, when looking over the file created, neither VLC nor ffmpeg could determine a duration. I used ffmpeg to create a new file (ffmpeg -i old.mkv -c copy new.mkv), and when it got to about the 1 hour 50 minute mark in the stream copy, it printed the following error:

invalid return value 0 for stream protocol
[matroska,webm @ 0x55eb60b609c0] Read error
Invalid return value 0 for stream protocol

It’s possible that I ran out of space during recording, so it behoves me to make sure my disks are more clear — hopefully the process of migrating to a combined fileserver will help there! — before I start recording.

Fortunately, I also stream to Twitch and so I could create a ‘highlight’ of the latter part of gameplay- about 45 minutes’ worth. The quality isn’t as good (max bitrate 3500kbit), but it’s better than nothing. So that it can be seamlessly appended, I decided to upscale the size from 720p to 1080p using ffmpeg:

$ ffmpeg -i ~/downloads/211006613-69819912-48eb80ad-009f-42d5-83aa-f68dda7b7612.mp4 -vf scale=1920x1080:flags=lanczos
-c:a copy -crf 20 -preset slow ~/mounts/storage/video/2017-12-19\ 22-04-59b.mkv

frame=105361 fps= 24 q=28.0 size= 2402802kB time=00:29:16.20 bitrate=11208.1kbits/s speed=0.408x

(Conversion in progress! Using -vf scale=1920x1080:flags=lanczos to scale and -crf 20 to ensure quality stays reasonable)

After that, it should be a simple matter of using the concat demuxer, assuming the files are similar enough (both are pixel format yuvj420p)– I previously had issues when trying to create an auto-highlighter/clip creator that minimised re-encoding.

For posterity!

Timesaver: import and combine GoPro Footage with FFmpeg

I’ve been taking my GoPro to Sunday Morning Football (as it is known) for a while now, so I figured I’d automate the process of importing the footage (moving it from microSD) and combining it into one file (GoPro splits recordings by default).

So I have the following script:


#!/bin/bash

GOPRO="/tmp/gopro"
DATE="$(date +%Y-%m-%d)"
VIDEO_BASE="/home/robert/mounts/storage/video/unsorted"
VIDEO_DEST="$VIDEO_BASE/$DATE"

if [ -e $GOPRO ]; then
        echo "Copying..."
        rsync -aP --info=progress2 --remove-source-files --include='*.MP4' $GOPRO/DCIM/100GOPRO/ $VIDEO_DEST/
        echo "Joining..."
        cd $VIDEO_DEST
        #cd $GOPRO/DCIM/100GOPRO/
        for file in `ls *.MP4`; do echo "file '$file'" >> stitch.txt; done
        #RECORD_DATE="$(ffprobe -v quiet `ls *.MP4 | head -n1` -show_entries stream=index,codec_type:stream_tags=creation_time:format_tags=creation_time | grep creation_time | head -n1| cut -d '=' -f 2| cut -d ' ' -f1)"
        # new format:
        RECORD_DATE="$(ffprobe -v quiet `ls *.MP4 | head -n1` -show_entries stream=index,codec_type:stream_tags=creation_time:format_tags=creation_time | grep creation_time | head -n1| cut -d '=' -f 2| cut -d ' ' -f1| cut -d 'T' -f1)"
        #echo "$RECORD_DATE"
        ffmpeg -y -f concat -i stitch.txt -c copy $RECORD_DATE.mp4
else
        echo "GoPro microSD not mounted?"
fi

Assumptions:

  • the microSD is already mounted before running (under /tmp/gopro) – I had considered automating this, but I figured running a script in response to insertion of removable media was a bad idea; I could add the mkdir and mount commands here, but since the latter requires root privileges I’d rather not and it is quickly recalled from bash history in any case
    • the $VIDEO_BASE directory is mounted and created (this is pretty stable)
    • the GoPro won’t number directories higher than 100GOPRO (eg 101GOPRO)- it possibly would if dealing with eg timelapse, but I am not covering that case
    • the GoPro will set creation time correctly; so far it has reset to the default date a few times, probably related to battery
    • I want to keep the source files around after creation (the script could remove them)

Given the above the script may seem a bit fragile – and it is definitely tightly coupled to my assumptions – but it’s done the job for a few weeks at least, and the commands it was based on have been pretty stable since I started recording football last year.

Extract A Single Image From A Video Using FFMPEG

Update: Still using this 8 years later, but in the form of a quick script, which is useful if you are doing it more than once

Dead handy, this:

ffmpeg -ss 0.5 -i inputfile.mp4 -t 1 -s 480x300 -f image2 imagefile.jpg

The various options:

  • -vframes 1: limit to 1 frame extracted
  • -ss 0.5: point of movie to extract from (ie seek to 0.5 seconds; you can also use HH:MM:SS.ZZZZ sexagesimal format)
  • -s 480x300: frame size of image to output (image resized to fit dimensions)
  • -f image2: forces format

I use this to generate preview stills for jwplayer to use. Dead handy!

Edit: Thanks to DieBagger who pointed out it is much faster to place the seek argument before the input file, and Matthias his point about the seek time.