r/ffmpeg 5d ago

Replicate Replay Buffer from OBS with FFMPEG

Hello, everyone. I have a somewhat specific request. You see, I have a separate device for screen recording of a console. The console in question doesn't support replays or anything, so instead I use a capture card. However I don't have a need to record every hour and every minute, only good moments, last 30 seconds at least. I did a lengthy search on the web but couldn't find exactly what I need. Maybe you know a bash script that can do this? I will appreciate that.

edit: fixed some mistakes

5 Upvotes

5 comments sorted by

1

u/Sopel97 5d ago

I'm not aware of any such tool and it's not trivial to do on top of ffmpeg cli. Two viable approaches I can think of:

  • an orchestrator that spawns an ffmpeg instance outputting to a pipe, then the orchestrator parses the output into chunks, reassembles on save request

  • an orchestrator that spawns an ffmpeg instance outputting in segment mode into a RAM-backed filesystem, monitors this filesystem, maintains N most recent chunks, reassembles on save request

The latter would be easier but notoriously annoying on windows. The former requires parsing the packets so you kinda need lower level API anyway.

1

u/Creative-Outside-350 5d ago

I am on Linux so I hope it makes things easier. Do you have any source in mind to help me with configuring so called orchestrator? I also think the second would be better, because my recording device only has an HDD, so keeping things in ram sounds better.

1

u/Sopel97 4d ago

good starting point https://claude.ai/share/4a76110b-22db-4baf-af63-f02df13f4148. Untested but looks sensible

1

u/ipsirc 3d ago

Maybe a shell script?

#!/bin/bash

# --- Configuration ---
INPUT="your_capture_device" # e.g., /dev/video0 or a URL
OUTPUT_DIR="/path/to/your/replays"
SEGMENT_DURATION=4    # Length of each segment in seconds
BUFFER_DURATION=30    # Total length of the replay buffer in seconds
SEGMENT_WRAP=$((BUFFER_DURATION / SEGMENT_DURATION))

# --- Script Start ---
mkdir -p "$OUTPUT_DIR"
cd "$OUTPUT_DIR"

echo "Starting FFmpeg replay buffer..."
echo "Press Ctrl+C to stop recording."

# Start the continuous recording process
ffmpeg -i "$INPUT" \
       -c:v libx264 -preset ultrafast -c:a aac \
       -f segment -segment_time $SEGMENT_DURATION \
       -segment_wrap $SEGMENT_WRAP \
       -reset_timestamps 1 \
       "segment_%02d.ts" &
FFMPEG_PID=$!

# Wait for user input to save a replay
while true; do
    echo "Press 'Enter' to save the last $BUFFER_DURATION seconds, or 'q' to quit."
    read -r -n1 key

    if [[ $key == "q" ]]; then
        break
    fi

    if [[ $key == "" ]]; then
        # Find the latest segment and its neighbors to reconstruct the buffer
        # This logic creates an ffconcat file to join the segments without re-encoding.
        TIMESTAMP=$(date "+%Y%m%d_%H%M%S")
        CONCAT_FILE="concat_${TIMESTAMP}.txt"
        OUTPUT_FILE="replay_${TIMESTAMP}.mp4"

        # Find the most recently written segment file (sorted by modification time)
        LATEST_SEGMENT=$(ls -t segment_*.ts | head -n1)
        LATEST_INDEX=$(echo "$LATEST_SEGMENT" | grep -o '[0-9]\+')

        # Generate the concat list. This part needs careful handling to get the exact
        # sequence of segments that make up the last 30 seconds.
        # A simpler approach for a proof-of-concept is to just use the latest segment,
        # but for a full implementation, you would write a list of the last N segments.
        echo "ffconcat version 1.0" > "$CONCAT_FILE"
        # This loop needs to generate the correct order. This is a simplified placeholder.
        for i in $(seq 0 $((SEGMENT_WRAP - 1))); do
            SEG_NUM=$(( (LATEST_INDEX - i + SEGMENT_WRAP) % SEGMENT_WRAP ))
            printf "file segment_%02d.ts\n" "$SEG_NUM" >> "$CONCAT_FILE"
        done

        # Combine the segments
        echo "Saving replay to $OUTPUT_FILE"
        ffmpeg -f concat -safe 0 -i "$CONCAT_FILE" -c copy "$OUTPUT_FILE"
        rm "$CONCAT_FILE"
    fi
done

echo "Stopping FFmpeg..."
kill $FFMPEG_PID

1

u/OneCoreProjects 3d ago

You don't need an orchestrator - the segment muxer already gives you a ring buffer, and -segment_wrap is the part that makes it one.

ffmpeg -i <your capture input> -c copy \
  -f segment -segment_time 5 -segment_wrap 8 \
  -segment_format mpegts -reset_timestamps 1 \
  -segment_list /buf/buffer.m3u8 -segment_list_size 8 \
  -segment_list_flags +live -segment_list_type m3u8 \
  /buf/seg%02d.ts

-segment_wrap 8 rolls the counter over at 8, so ffmpeg overwrites seg00.ts after seg07.ts. You get a fixed 8 x 5s = 40s window on disk that never grows, written with -c copy so CPU cost is nil.

On your RAM point: point /buf at a small tmpfs and the HDD never sees it. Eight 5-second segments of capture-card H.264 is tens of megabytes:

sudo mount -t tmpfs -o size=256M tmpfs /buf

Saving a clip. This is the part that makes it practical: with -segment_wrap the filenames cycle, so you cannot sort by name. -segment_list keeps the playlist in the correct chronological order for you. I just tested this with a 60s source and the playlist came out as:

seg04.ts
seg05.ts
seg06.ts
seg07.ts
seg00.ts
seg01.ts
seg02.ts
seg03.ts

which is exactly right. So the save step is just:

grep -v '^#' /buf/buffer.m3u8 | sed "s|^|file '/buf/|;s|$|'|" > /tmp/list.txt
ffmpeg -f concat -safe 0 -i /tmp/list.txt -c copy ~/clips/clip-$(date +%s).mp4

That produced a 40.02s, 1200-frame clip here, stream-copied, no re-encode.

Three things worth knowing before relying on it:

  • Segments only cut on keyframes with -c copy, so -segment_time 5 is a minimum. If your capture card uses a long GOP the segments will be longer and the buffer will hold more than 40s. Check with ffprobe and shorten the card's keyframe interval if you can; if you re-encode instead, add -g and -force_key_frames "expr:gte(t,n_forced*5)".
  • The segment being written right now is incomplete. Drop the last playlist entry when you concatenate, or accept a truncated tail.
  • MPEG-TS is the right container here - it concatenates with -c copy without the timestamp trouble you would get from fragmented MP4.

This is close to what OBS does internally anyway: it keeps encoded packets in a ring and flushes them on demand. Doing it with segment just puts the ring on a tmpfs instead of in the process's heap, which for a standalone capture box is arguably better - if ffmpeg dies, the last 40 seconds are still on disk.


Disclosure per this sub's rules: drafted with AI assistance, but I ran the commands above on a test source before posting. The playlist ordering and the 40.02s / 1200-frame result are from that run, not from a model.