Video „streaming” self-hosted

Un client vrea să pună un video niște filme în hero, folosind good old video tag. Și mi-am dat seama că nu știu prea multe despre subiect :smiley:

Conștient sau nu, până acum m-am ferit de a lucra direct cu video în browser, deci nu sunt sigur că o apuc pe calea corectă.

Din ce am citit, MP4 + AVC reprezintă containerul + codec-ul cu cel mai bun suport; deși WEBM + VP9 este mai eficient, nu se bucură de același suport în browsere (mai ales pe iOS/Safari). Am convertit video în mp4 și webm, îl includ

<video muted autoplay loop playsinline>
   <source src="video.webm" type="video/webm">
   <source src="video.ogv" type="video/ogg">
   <source src="video.mp4" type="video/mp4">
</video>

Toate merg bine, doar că unele videouri au >40mb și durează câteva secunde bune (la mine) sau minute (la client) să înceapă să se miște ceva. Fiind un site foarte nișat cu un anumit target demografic, dimensiunea propriu-zisă nu este o problemă (dpdv al costului/traficului adică) ci doar durata de descărcare.

Restricții/cerințe:

  • este absolut musai să-l hostăm noi (deci nu YT, Vimeo sau alții)
  • nu am acces la a face config la server (sau mă rog, aș avea acces, dar prefer să fie cât mai neintruziv din punctul ăsta de vedere)
  • NU este nevoie de chestii ultra complexe, sunt maximum 6 filme de servit, nu este nevoie de statistici, controale sau alte giumbușlucuri. Deci nu caut SF-uri tehnologice, ci fix opusul :smiley:
  • play la video de calitate potrivită pentru ecran și conexiune la internet (aici m-am gândit să măsor cât durează descărcarea un chunk și în funcție de asta, următorul chunck va fi mai mic/mare)
  • playerul NU are vreun fel de control. Nu are audio, nu are scrubbing. Nu are tracking pentru stats, nu are share, nu are nimic. Are doar autoplay și loop (practic este markup de mai sus). Este un video care rulează ca background.

M-am gândit așa:

  • :white_check_mark: spart clipul în trei variante (1080, 720, 480). La 480, video-ul are sub 2mb, deci yay?
  • :white_check_mark: spart clipul în părți de 4-5s cu ffmpeg, astfel încât să am ~1mb fiecare chunk.
  • :white_check_mark: generat playlist pentru chunk-urile asetea.
  • :green_square: folosesc o bibliotecă js care lipește bucățile de video, „măsoară” viteza la internet și servește chunk-urile potrivite (am găsit video.js și hls, momentan investighez dacă chiar sunt ce am nevoie)

Iar la ultimul pas m-am oprit. Nu m-am blocat, m-am oprit, pentru că nu sunt sigur dacă merg în direcția corectă.

Fac ceva greșit? Sunt pe drumul bun?

Sugestii?

1 Like

Heh, a fost mai ușor decât am crezut:

În hls.js pot specifica un playlist de playlist-uri și se descurcă el mai departe să schimbe sursa în funcție de conexiune (am făcut probă cu throttle din chrome).

#EXT-X-STREAM-INF:BANDWIDTH=800000K
https://full-url/app/uploads/videos/hp-hero--480p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1400000K
https://full-url/app/uploads/videos/hp-hero--720p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2800000K
https://full-url/app/uploads/videos/hp-hero--1080p.m3u8

Apoi fiecare playlist are forma

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:5.760000,
https://full-url/app/uploads/videos/hp-hero--1080p_000.ts
#EXTINF:3.840000,
https://full-url/app/uploads/videos/hp-hero--1080p_001.ts
#EXTINF:3.840000,
https://full-url/app/uploads/videos/hp-hero--1080p_002.ts

Restul este easy peasy:

    const video = document.createElement('video');
    document.getElementById('vid-1-1').appendChild(video)
    const hls = new Hls();
    hls.loadSource('https://full-url/app/uploads/videos/hp-hero.m3u8')

    hls.attachMedia(video);
    hls.on(Hls.Events.MEDIA_ATTACHED,  () =>{
        video.muted = true;
        video.play();
    });

Revin cu scriptul de conversie imediat ce-mi dau seama cum să-l automatizez :sweat_smile:

Si in functie de ce alegi calitatea pe care o servesti? De obicei unde am vazut video de genul pus in BG, era pixelat, blurry, semn ca a fost redusa drastic calitatea si marimea. Am mai vazut care aveau un color layer pe ele, care banuiesc ca il face monocrom si ca atare mult mai mic. Nu am fost curios niciodata ce e dedesubt, zic doar ce am vazut in interactiunea utilizatorului.

Fix pentru usecase-ul meu? Este suficient doar bandwidth.

Dar investighez cum s-ar putea face și în funcție de rezoluția ecranului, că n-are rost să servesc video 4k pe ecran de 5 inch :sweat_smile:

si cum testezi bandwidth?

La hls.js exista capLevelToPlayerSize, in video.js cred ca se face asta automat.

2 Likes

Ideea inițială era să măsor cât durează descărcarea unui chunk. Am dimensiunea unui chunk, am timpul pentru acel chunk => bandwidth. Repet la fiecare chunk, ajustez dacă e nevoie.

Darrrrr se pare că hsl.js face asta automat, deci nu a mai fost nevoie de asta :person_shrugging:

Pune video-ul obligatoriu pe un CDN, nu il pune pe acelasi host ca si site-ul in sine.

1 Like

Și scriptul care generează video-urile, dacă e cineva interesat:

#!/bin/bash

IN=$1

OUT_BASE=$(basename "$1" | sed 's/^\(.*\)\.[a-zA-Z0-9]*$/\1/')
OUT="/app/converted/$OUT_BASE"
BASE_URL=$2

mkdir -p /app/converted/

CHUNK_DURATION=2

original_width=$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of csv=s=x:p=0 "$IN")
original_height=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of csv=s=x:p=0 "$IN")

scale1080_base=$((1080 * $original_width/$original_height))
scale1080="scale=$scale1080_base:1080"

scale720_base=$((720 * $original_width/$original_height))
scale720="scale=$scale720_base:720"

scale360_base=$((360 * $original_width/$original_height))
scale360="scale=$scale360_base:360"

ffmpeg -i $IN \
  -map 0:v -map 0:a? -map 0:s? -c:v:0 libx264 -c:a copy -c:s copy -b:v:0 5000k \
    -vf "$scale1080, pad=ceil(iw/2)*2:ceil(ih/2)*2" -preset fast -g 48 -sc_threshold 0 -keyint_min 48 -hls_time $CHUNK_DURATION -an \
    -hls_base_url "$BASE_URL" -hls_playlist_type vod -hls_segment_filename $OUT--1080p_%03d.ts $OUT--1080p.m3u8 \
\
  -map 0:v -map 0:a? -map 0:s? -c:v:1 libx264 -c:a copy -c:s copy -b:v:1 2800k \
    -vf "$scale720, pad=ceil(iw/2)*2:ceil(ih/2)*2" -preset fast -g 48 -sc_threshold 0 -keyint_min 48 -hls_time $CHUNK_DURATION -an \
    -hls_base_url "$BASE_URL" -hls_playlist_type vod -hls_segment_filename $OUT--720p_%03d.ts $OUT--720p.m3u8 \
\
  -map 0:v -map 0:a? -map 0:s? -c:v:2 libx264 -c:a copy -c:s copy -b:v:2 1400k \
    -vf "$scale360, pad=ceil(iw/2)*2:ceil(ih/2)*2" -preset fast -g 48 -sc_threshold 0 -keyint_min 48 -hls_time $CHUNK_DURATION -an \
    -hls_base_url "$BASE_URL" -hls_playlist_type vod -hls_segment_filename $OUT--360p_%03d.ts $OUT--360p.m3u8

cat <<'EOF' > $OUT.m3u8
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
EOF

echo "#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360" >> $OUT.m3u8
echo $BASE_URL$OUT_BASE--360p.m3u8 >> $OUT.m3u8

echo "#EXT-X-STREAM-INF:BANDWIDTH=1400000,RESOLUTION=1280x720" >> $OUT.m3u8
echo "$BASE_URL$OUT_BASE--720p.m3u8" >> $OUT.m3u8

echo "#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=1920x1080" >> $OUT.m3u8
echo "$BASE_URL$OUT_BASE--1080p.m3u8" >> $OUT.m3u8
1 Like

Cred ca poti sa folosesti optiunea de master_pl_name (nu am link direct e pe undeva pe la optiunile pt muxerul de hls) pentru a lasa ffmpeg sa genereze un manifest care le contine pe celalalte (aka master), fara sa mai fie nevoie sa mai faci partea de la final.

1 Like

Cum sa eviti descarcarea completa a unui MP4 inainte de a putea fi redat

3 Likes

Asta-l foloseam eu acum vreo 3-4 ani, cu dash.js:

#!/bin/bash

# can't remember where I found this..
#
# THIS SCRIPT CONVERTS EVERY MP4 (IN THE CURRENT FOLDER AND SUBFOLDER) TO A MULTI-BITRATE VIDEO IN MP4-DASH
# For each file "videoname.mp4" it creates a folder "dash_videoname" containing a dash manifest file "stream.mpd" and subfolders containing video segments.
# Explanation:


# Validation tool:
# https://conformance.dashif.org/

# MDN reference:
# https://developer.mozilla.org/en-US/Apps/Fundamentals/Audio_and_video_delivery/Setting_up_adaptive_streaming_media_sources

# Add the following mime-types (uncommented) to .htaccess:
# AddType video/mp4 m4s
# AddType application/dash+xml mpd

# Use type="application/dash+xml"
# in html when using mp4 as fallback:
#                <video data-dashjs-player loop="true" >
#                    <source src="/walking/walking.mpd" type="application/dash+xml">
#                    <source src="/walking/walking.mp4" type="video/mp4">
#                </video>

# DASH.js
# https://github.com/Dash-Industry-Forum/dash.js

#MYDIR=$(dirname $(readlink -f ${BASH_SOURCE[0]}))
#SAVEDIR=$(pwd)

# Check programs
if [ -z "$(which ffmpeg)" ]; then
    echo "Error: ffmpeg is not installed"
    exit 1
fi

if [ -z "$(which MP4Box)" ]; then
    echo "Error: MP4Box is not installed"
    exit 1
fi


TARGET_FILES=$(find ./ -maxdepth 1 -type f \( -name "*.mov" -or -name "*.mp4" \))
for f in $TARGET_FILES
do
  fe=$(basename "$f") # fullname of the file
  f="${fe%.*}" # name without extension

  if [ ! -d "${f}" ]; then #if directory does not exist, convert
    echo "Converting \"$f\" to multi-bitrate video in MPEG-DASH"

    mkdir "${f}"

    ffmpeg -y -i "${fe}" -c:a aac -b:a 192k -vn "${f}_audio.m4a"

    ffmpeg -y -i "${fe}" -preset slow -tune film -vsync passthrough -write_tmcd 0 -an -c:v libx264 -x264opts 'keyint=25:min-keyint=25:no-scenecut' -crf 22 -maxrate 5000k -bufsize 12000k -pix_fmt yuv420p -f mp4 "${f}_5000.mp4"
    ffmpeg -y -i "${fe}" -preset slow -tune film -vsync passthrough -write_tmcd 0 -an -c:v libx264 -x264opts 'keyint=25:min-keyint=25:no-scenecut' -crf 23 -maxrate 3000k -bufsize 6000k -pix_fmt yuv420p -f mp4  "${f}_3000.mp4"
    ffmpeg -y -i "${fe}" -preset slow -tune film -vsync passthrough -write_tmcd 0 -an -c:v libx264 -x264opts 'keyint=25:min-keyint=25:no-scenecut' -crf 23 -maxrate 1500k -bufsize 3000k -pix_fmt yuv420p -f mp4   "${f}_1500.mp4"
    ffmpeg -y -i "${fe}" -preset slow -tune film -vsync passthrough -write_tmcd 0 -an -c:v libx264 -x264opts 'keyint=25:min-keyint=25:no-scenecut' -crf 23 -maxrate 800k -bufsize 2000k -pix_fmt yuv420p -vf "scale=-2:720" -f mp4  "${f}_800.mp4"
    ffmpeg -y -i "${fe}" -preset slow -tune film -vsync passthrough -write_tmcd 0 -an -c:v libx264 -x264opts 'keyint=25:min-keyint=25:no-scenecut' -crf 23 -maxrate 400k -bufsize 1000k -pix_fmt yuv420p -vf "scale=-2:540" -f mp4  "${f}_400.mp4"
    # static file for ios and old browsers, you may choose to not do this do this..
    ffmpeg -y -i "${fe}" -preset slow -tune film -vsync passthrough -write_tmcd 0 -c:a aac -b:a 160k -c:v libx264  -crf 23 -maxrate 2000k -bufsize 4000k -pix_fmt yuv420p -f mp4 "${f}/${f}.mp4"


    rm -f ffmpeg*log*
    # if audio stream does not exist, ignore it
    if [ -e "${f}_audio.m4a" ]; then
        MP4Box -dash-strict 2000 -rap -frag-rap  -bs-switching no -profile "dashavc264:live" "${f}_5000.mp4" "${f}_3000.mp4" "${f}_1500.mp4" "${f}_800.mp4" "${f}_400.mp4" "${f}_audio.m4a" -out "${f}/${f}.mpd"
        rm "${f}_5000.mp4" "${f}_3000.mp4" "${f}_1500.mp4" "${f}_800.mp4" "${f}_400.mp4" "${f}_audio.m4a"
    else
        MP4Box -dash-strict 2000 -rap -frag-rap  -bs-switching no -profile "dashavc264:live" "${f}_5000.mp4" "${f}_3000.mp4" "${f}_1500.mp4" "${f}_800.mp4" "${f}_400.mp4" -out "${f}/${f}.mpd"
        rm "${f}_5000.mp4" "${f}_3000.mp4" "${f}_1500.mp4" "${f}_800.mp4" "${f}_400.mp4"
    fi
    # create a jpg for poster. Use imagemagick or just save the frame directly from ffmpeg is you don't have cjpeg installed.
    #ffmpeg -i "${fe}" -ss 00:00:00 -vframes 1  -qscale:v 10 -n -f image2 - | cjpeg -progressive -quality 75 -outfile "${f}"/"${f}".jpg
    ffmpeg -v quiet -i "${fe}" -vframes 1 -an -f image2 -y "${f}"/"${f}".jpg

    fi
done
2 Likes

și aș mai avea niște recomandări:

  1. pune un jpg să fie afișat înainte să se încarce primul mp4; se cheamă poster;
  2. poate fi un blurred version base64 encoded;
  3. asigură-te că faci acel loading doar când e pe cale să îți intre <video> în viewport
  4. cum zicea și cineva mai sus, pune fișierele pe un CDN
  5. include toate versiunile de fișiere mp4, webm etc. și lasă browserul să decidă ce anume încarcă; o să facă fallback la mp4 dacă nu merge webm; dar pentru 90% din browserele care suportă webm, o să meargă și aici avantaj tu
  6. inspiră-te de la companii mari cum fac treaba asta; sunt multe site-uri care sunt video heavy și care totuși se mișcă impecabil
3 Likes