Jump to content

Startup Buffering Fix using ffmpeg wrapper for LiveTV streams


Recommended Posts

Posted (edited)

@LukeTagging you here since creation of this header is something you could add to your custom builds of ffmpeg to add some resilience to stream startup.  Bonus points if you would consider sending a bit more of a buffer to a client when they try to join a live stream. 

I've been annoyed by buffering at the start of LiveTV feeds.  I use the force transcoding option in my playback settings for MPEGTS so that I can pause/skip backwards while watching live tv.  

When Emby uses ffmpeg to compose the segments and HLS manifest it doesn't contain an #EXT-X-START:TIME-OFFSET=0 header to tell the player to begin playback from the first segment in the file. Default behavior on the player side when this header is missing is to jump to the last segment in the list (verified in logs), which skips over all data in the tuner buffer and results in periodic pauses until the player falls back in time enough segments to be arriving ahead of the current playback position.  For example, ffmpeg generates roughly 8 segments of startup content in my current setup, but without this header being present, the player will start playing from segment 8 instead of segment 1.   Once the header is added, I can verify in the logs that the player always requests the first segment available in the m3u8 manifest.

I'm using the emby-server docker container and I've hacked a workaround in place for this by overriding the emby startup script to call an ffmpeg wrapper.  The wrapper redirects manifest output to a .m3u8.raw file, uses a loop to check it for updates and when a new version is found it creates the intended original target m3u8 file with the necessary header. 

CAVEAT: 
This works well for the first person that tunes into a live channel.  However, due to internal logic within Emby, additional sessions that join an in-progress live stream aren't given enough buffer data for this to have a meaningful impact. These add-on streams only receive enough data from the internal tuner cache to create 1-2 segments at startup which causes the emby client/player to pause a bit at startup while it waits for additional segments to be created and published. 


Docker compose bind mounts:

      - ./ffmpeg-wrapper-override.sh:/bin/ffmpeg-wrapper-override.sh:ro
      - ./emby-server-run-override.sh:/etc/services.d/emby-server/run:ro


ffmpeg-wrapper-override.sh:
 

#!/bin/sh

# Version 2.8.0 - HLS Segment Pinner (Universal)
REAL_FFMPEG="/bin/ffmpeg"

# Smart Patcher: Only updates when the manifest actually changes
maintain_proxy_smart() {
    OFFICIAL_M3U=$1
    SHADOW_M3U=$2
    FFMPEG_PID=$3
    LAST_MTIME=0
    
    until [ -f "$SHADOW_M3U" ]; do 
        kill -0 "$FFMPEG_PID" 2>/dev/null || exit
        sleep 0.1
    done

    while kill -0 "$FFMPEG_PID" 2>/dev/null; do
        if [ -f "$SHADOW_M3U" ]; then
            CURRENT_MTIME=$(stat -c %Y "$SHADOW_M3U" 2>/dev/null)
            if [ "$CURRENT_MTIME" != "$LAST_MTIME" ]; then
                # Inject the 'Start at 0' tag and commit atomically
                sed '2i#EXT-X-START:TIME-OFFSET=0' "$SHADOW_M3U" > "${OFFICIAL_M3U}.tmp"
                mv "${OFFICIAL_M3U}.tmp" "$OFFICIAL_M3U"
                LAST_MTIME=$CURRENT_MTIME
            fi
        fi
        sleep 0.1
    done
}

# TRIGGER: If Emby is generating an HLS manifest, we intercept.
TRIGGER=0
for arg in "$@"; do
    if [ "$arg" = "-segment_list" ]; then
        TRIGGER=1
        break
    fi
done

if [ "$TRIGGER" -eq 1 ]; then
    WRAPPER_PID=$$
    # Rebuild arguments to redirect the official manifest to a shadow file
    for arg do
        shift
        if [ "$PREV_ARG" = "-segment_list" ]; then
            REAL_PATH="$arg"
            SHADOW_PATH="${arg}.raw"
            set -- "$@" "$SHADOW_PATH"
        else
            set -- "$@" "$arg"
        fi
        PREV_ARG="$arg"
    done

    if [ -n "$REAL_PATH" ]; then
        maintain_proxy_smart "$REAL_PATH" "$SHADOW_PATH" "$WRAPPER_PID" &
    fi

    exec "$REAL_FFMPEG" "$@"
else
    # Fallback for library scans, probes, and non-HLS tasks
    exec "$REAL_FFMPEG" "$@"
fi

 

emby-server-run-override.sh: 

#!/usr/bin/with-contenv sh

# Maintain original config ownership logic
if [ "$(ls -nd /config | tr -s '[:space:]' | cut -d' ' -f3)" -ne "$UID" ] || [ "$(ls -nd /config | tr -s '[:space:]' | cut -d' ' -f4)" -ne "$GID" ]; then
  chown "$UID":"$GID" -R /config
fi

# Maintain GPU device discovery (Critical for Hardware Acceleration)
for d in $(find /dev/dri -type c 2>/dev/null); do
  gid=$(stat -c %g "${d}")
  [ -z "${GIDLIST}" ] && GIDLIST=${gid} || GIDLIST="${GIDLIST},${gid}"
done 

# Launch EmbyServer
# We change ONLY the -ffmpeg flag to point to your new wrapper path
if [ -n "$(uname -a | grep -q synology)" ] || [ "$IGNORE_VAAPI_ENABLED_FLAG" = "true" ]; then
  s6-applyuidgid -U /system/EmbyServer \
      -programdata /config \
      -ffdetect /bin/ffdetect \
      -ffmpeg /bin/ffmpeg-wrapper-override.sh \
      -ffprobe /bin/ffprobe \
      -ignore_vaapi_enabled_flag \
      -restartexitcode 3
else
  s6-applyuidgid -U /system/EmbyServer \
      -programdata /config \
      -ffdetect /bin/ffdetect \
      -ffmpeg /bin/ffmpeg-wrapper-override.sh \
      -ffprobe /bin/ffprobe \
      -restartexitcode 3
fi


UPDATES: 
Make sure you disable the bind mounts when doing updates so you can inspect the /etc/services.d/emby-server/run file for changes that you need to bring into your override. 

Edited by bruor

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...