Odtwarzanie poklatkowe

31.08 ‘11



Problem

Naszym zadaniem było stworzenie rozwiązania, bazującego na obrazie płynnym (wideo), umożliwiającego modyfikację poszczególych klatek filmu przez użytkowników. Efekt zmiany miał być widoczny natychmiast, bez konieczności renderowania całej sekwencji od początku.

Projekt zakładał, że film zbudowany jest z około trzydziestosekundowej, powtarzalnej (zapetlonej), sekwencji. Dodatkowo wiedzieliśmy, że długość odtwarzanego materiału będzie się zmieniać w czasie, gdyż zależy od ilości zmodyfikowanych klatek filmu. Mieliśmy dać również możliwość wyświetlenia dowolnie wskazanego ujęcia (kardu). Ostatnim założeniem była możliwa zmiana szybkości odtwarzanego materiału, podczas jego oglądania.

Idea

Najbardziej oczywistym byłoby użycie poszczególnych kadrów w formie obrazów (plików graficznych).

Symulacja działania odtwarzania poklatkowego
(Sallie Gardner at a Gallop – Eadweard J. Muybridge)

Wydajność takiego rozwiązania jest jednak odpowiednia wyłącznie dla krótkich sekwencji, dyskwalifikując dłuższy obraz płynny.

Z uwagi na zmienną długość filmu (poszczególne kadry są generowane przez użytkowników) nie jest również możliwe osadzenie go bezpośrednio na listwie czasowej.

Rozwiązanie

Początkowo podjęliśmy próby generowania filmu po stronie serwera z poszczególnych klatek animacji.
Do testów użyliśmy takich rozwiązań jak Adobe Media Server, Red5, FFserver.
Wszystkie z nich miały swoje wady i zalety. Niemniej żadne z powyższych nie spełniło jednego z podstawowych założeń – nie dawało możliwości odtwarzania filmu poklatkowo. Najlepiej radził sobie FFserver, ale był dość niestabilny (co jakiś czas się zawieszał – na CentOS 5.7 i FFmpeg 0.6.1 z repozytorium ATRPMS-testing).

Postanowiliśmy więc z każdego rozwiązania wziąć to, co najlepsze i stworzyć własne, idealnie odpowiadające naszym założeniom.

Po stronie serwera generowaliśmy krótkie (50 klatek) filmy Adobe Flash (SWF) z zaszytymi wewnątrz klatkami zakodowanymi jako wideo (FLV).

Użyliśmy FFmpeg 0.6.1, programowanie w powłoce BASH, curl 7.19.7 oraz PHP 5.3.2 (cli).

Na początku następowała rezerwacja danej klatki filmu.

Plik „reserveFrame.sh

#!/bin/bash

HOST="http://host/reserveFrameComplete";
SHARED_DIR="/home/project/shared";
DURATION=643; # maksymalna ilość klatek filmu

if [ -n "$1" ]; then

FRAME_ID=$1;

LOOP_ID=$((FRAME_ID / DURATION));
LOOP_ID=${LOOP_ID/.*}; # Zaokrąglenie w dół (floor)

MODULO=$((FRAME_ID % DURATION));

cp $SHARED_DIR/frames/$MODULO.jpg $SHARED_DIR/drafts/$FRAME_ID.jpg;

curl --data "frameId=$FRAME_ID" $HOST > /dev/null 2>&1;

fi;

Wykonanie skryptu następowało z kontrolera w PHP:

shell_exec($shared_path."/reserveFrame.sh $frame_id");

Po ukończeniu tworzenia przez użytkownika określonej klatki z filmu, obraz był przesyłany do aplikacji. Następnie wywoływany był skrypt tworzący określoną sekwencję. Zdecydowaliśmy się na dzielenie filmu na krótkie, dwusekundowe ujęcia, które następnie były doczytywane w trakcje odtwarzania filmu, przy zachowaniu odpowiedniego bufora. Jeśli się zdarzyło że bufor jest pusty (np. na skutek wolnego łącza), następowało doładowywanie kolejnych sekwencji, analogicznie jak przy strumieniowym obrazie wideo.
Jeśli ktoś przerwał oglądanie w trakcie odtwarzania, kolejne sekwencje nie były ładowane. Rozwiązanie takie pozwala na oszczędzanie transferu, co jest ważne zwłaszcza przy dużej ilości odtworzeń.

Plik „renderMovie.sh

#!/bin/bash

HOST="http://host/makeMovieComplete"
DRAFT_DIR="/home/project/shared/drafts"
STREAMS_DIR="/home/project/shared/streams"
KEYFRAME=50
DURATION=643

if [ -n "$3" ]
then

FRAME_HASH=$3

if [ -n "$1" ] && [ -n "$2" ]
then

FRAME_ID=$1
TOTAL_LOOPS=$2

LOOP_ID=$(( FRAME_ID / DURATION ))
LOOP_ID=${LOOP_ID/.*} # floor

LOOP_MODULO=$(( FRAME_ID % DURATION ))

KEYFRAME_ID=$(( FRAME_ID / KEYFRAME ))
KEYFRAME_ID=${KEYFRAME_ID/.*} # floor

KEYFRAME_MODULO=$(( FRAME_ID % KEYFRAME ))

START_FRAME=$(( DURATION + LOOP_MODULO - KEYFRAME_MODULO ))
START_FRAME=$(( START_FRAME % DURATION ))

if [ $KEYFRAME_ID -lt $TOTAL_LOOPS ]
then
KEYFRAME_LENGTH=$(( KEYFRAME - 1 ))
else
KEYFRAME_LENGTH=$KEYFRAME_MODULO
fi

START_FRAME=$(( DURATION + LOOP_MODULO - KEYFRAME_MODULO ))
START_FRAME=$(( START_FRAME % DURATION ))

TEMP_DIR=$(mktemp -d /tmp/ffmpeg_XXXXXXXXXXXXXXXXXXXXXXXX) || {
echo "Failed to create temp file"
exit 1
}

KEYFRAME_INDEX=0

while [ $KEYFRAME_INDEX -le $KEYFRAME_LENGTH ]
do
FILE=$(( START_FRAME + KEYFRAME_INDEX ))
FILE=$(( FILE % DURATION ))

if [ ! -f $DRAFT_DIR/$FILE.jpg ]
then
break
else
cp $DRAFT_DIR/$FILE.jpg $TEMP_DIR/$KEYFRAME_INDEX.jpg
fi

KEYFRAME_INDEX=$(( KEYFRAME_INDEX + 1 ))
done

#ffmpeg bug: fps 24 drop frames!
ffmpeg -y -i $TEMP_DIR/%d.jpg -r 25 -s 640x360 -b 1536k -an -vcodec flv -f avm2 $STREAMS_DIR/$KEYFRAME_ID.swf #1>/dev/null 2>/dev/null

rm -rf $TEMP_DIR

curl --data "frameHash=$FRAME_HASH" $HOST #> /dev/null 2>&1

fi

fi

Wywołania z PHP były odpalane „w tle”. Dodatkowo była napisana obsługa kolejkowania, która działała w taki sposób, że jeśli w tym samym czasie było wysłanych kilka, kilkanaście obrazów, generowane były tylko te, które się nie powtarzają w tej samej sekwencji.

exec($shared_path."/renderMovie.sh $frame_id $num_loops $pending_data > /dev/null 2>/dev/null &");

Czas wykonywania renderowania wynosił poniżej sekundy.
Użytkownik mógł zaraz po wysłaniu swojego kadru zobaczyć, jak jego praca prezentuje się w całej sekwencji.

Aplikacja była częścią szerszych działań promocyjnych organizowanych przez miasto stołeczne Warszawa wraz ze sponsorami globalnymi UEFA oraz PKP Polskie Koleje Państwowe.

Szymon Piotr Pepliński

Szymon Piotr
Pepliński

Head of Innovation