From 565f056b68e149a9b1f400f7b7f30c77bc8893aa Mon Sep 17 00:00:00 2001 From: =?utf8?q?Kristian=20Kr=C3=A6mmer=20Nielsen?= Date: Tue, 14 Mar 2017 16:44:06 +0100 Subject: [PATCH] Now we can record segments of ad blocks and the playlists --- .gitignore | 2 + monitor-dai.sh | 215 +++++++++++++++++++++++++++++++++------------- start-monitors.sh | 44 +++++++--- 3 files changed, 187 insertions(+), 74 deletions(-) diff --git a/.gitignore b/.gitignore index 3fc2310..932811b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ config.sh logs/ +playlists/ +recordings/ diff --git a/monitor-dai.sh b/monitor-dai.sh index 9e6b4fd..af571f1 100755 --- a/monitor-dai.sh +++ b/monitor-dai.sh @@ -5,6 +5,7 @@ # ================================================================ # # Author: Kristian Kræmmer Nielsen +# License: GPL3 # # Purpose: # - Continuiously stream one HLS-stream @@ -14,6 +15,8 @@ # - Warn-log differencies over reasonable margin (TODO: reasonable margin) # - Error-log if seeing duplicate CUE-OUT or CUE-IN markers # - Optionally warn-log if seeing breaks over 12 minutes a hour (TODO) +# - Records first seen ad-block including one segment before and after +# - very useful for synchronization # # Errors goes to stderr # Warnings and info goes to stdout @@ -40,6 +43,9 @@ TARGET_DURATION=10 # Time screw (expected delay in stream in seconds) STREAM_TIME_SCREW=105 +# No adblocks to record +RECORD_AD_BLOCKS=3 + # Very limited function to resolve relative url function resolveRelativeURL() { local relative="$1" @@ -96,6 +102,7 @@ function resolveFirstStream() { # Monitor stream function monitorStream() { local stream="$1" + local recordFolder="$2" local nextSegmentLength=0 local nextSeq=-1 local curSeq=-1 @@ -104,13 +111,17 @@ function monitorStream() { local curLength=0 local failed=0 local target_duration=$TARGET_DURATION + local firstheader=0 + local tags="" + local lastUsedSegment="" lastUsedStreamTime=0 # Stored for use of recording previous segment when seeing ad block + local doRecord=0 doStopRecord=0 real_time=$(date +%s) stream_time=$(($real_time - $STREAM_TIME_SCREW)) pretty_time=$($DATE${stream_time%.*} +"%Y-%m-%d %H:%M:%S") - echo "NOTICE: Using time-offset of -$STREAM_TIME_SCREW seconds so time is $pretty_time, real time is $(date ${realtime} +"%Y-%m-%d %H:%M:%S")" - echo "$pretty_time: INFO: Started monitoring using: $stream" + echo >&2 "NOTICE: Using time-offset of -$STREAM_TIME_SCREW seconds so time is $pretty_time, real time is $(date ${realtime} +"%Y-%m-%d %H:%M:%S")" + echo >&2 "$pretty_time: INFO: Started monitoring using: $stream" while true; do starttime=$(date +%s) @@ -124,72 +135,108 @@ function monitorStream() { if [ "$line" == "#EXTM3U" ]; then # Correct playlist warn_not_a_playlist=0 + if [ "$firstheader" = "0" ]; then + echo $line + firstheader=1 + fi + continue fi # Look for segment - re="^[^#].*" - if [[ "$line" =~ $re ]]; then - # Got segment - if [[ $curSeq -ne -1 ]]; then # we know media sequence - if [[ $curSeq -ge $nextSeq ]]; then # later sequence than we need - curLength=$(echo "$nextSegmentLength + $curLength" | bc) - stream_time=$(echo "$stream_time + $nextSegmentLength" | bc) - pretty_time=$($DATE${stream_time%.*} +"%Y-%m-%d %H:%M:%S") - if [[ $curSeq -ne $nextSeq ]]; then - echo >&2 "$pretty_time: WARN: Lost segments ($curSeq > $nextSeq)" + if [ "$warn_not_a_playlist" == "0" ]; then + tags="$tags$line"$'\n' + re="^[^#].*" + if [[ "$line" =~ $re ]]; then + # Got segment + if [[ $curSeq -ne -1 ]]; then # we know media sequence + if [[ $curSeq -ge $nextSeq ]]; then # later sequence than we need + if [[ $doRecord -eq 1 ]]; then + saveStreamBit "$stream" "$tags" "$stream_time" "$recordFolder" + fi + lastUsedSegment="$tags" + lastUsedStreamTime="$stream_time" + # forward stream time: + curLength=$(echo "$nextSegmentLength + $curLength" | bc) + stream_time=$(echo "$stream_time + $nextSegmentLength" | bc) + pretty_time=$($DATE${stream_time%.*} +"%Y-%m-%d %H:%M:%S") + if [[ $curSeq -ne $nextSeq ]]; then + echo >&2 "$pretty_time: WARN: Lost segments ($curSeq > $nextSeq)" + fi + #echo >&2 "$pretty_time: Current mode length: $curLength" + nextSeq=$(($curSeq + 1)) + echo -n "$tags" + # Log stopped recording (after pretty_time moved forward) + if [ "$doStopRecord" == "1" ]; then + doRecord=0 + echo >&2 "$pretty_time: INFO: Stopped recording." + fi fi - #echo >&2 "$pretty_time: Current mode length: $curLength" - nextSeq=$(($curSeq + 1)) + curSeq=$(($curSeq + 1)) fi - curSeq=$(($curSeq + 1)) - fi - fi + tags="" + fi - # Look for tags - re="^#([^:]+):?([0-9.]*)" - if [[ "$line" =~ $re ]]; then - tag="${BASH_REMATCH[1]}" - value="${BASH_REMATCH[2]}" - if [ "$tag" == "EXT-X-TARGETDURATION" ]; then - if [ "$value" != "$target_duration" ]; then - echo "$pretty_time: INFO: Changing target duration to: $value" - target_duration="$value" - fi - elif [ "$tag" == "EXTINF" ]; then - nextSegmentLength=$value - elif [ "$tag" == "EXT-X-MEDIA-SEQUENCE" ]; then - #echo "$pretty_time: Got sequence no: $value" - curSeq=$value - if [ $nextSeq -le 0 ]; then - nextSeq=$value - fi - fi - if [[ $curSeq -ge $nextSeq ]]; then # we are interested in this tag - if [ "$tag" == "EXT-X-CUE-OUT" ]; then - # Begin ad block - echo "$pretty_time: INFO: Seeing CUE-OUT (Ad block start) of duration: $value" - if [ "$mode" == "ADBLOCK" ]; then - echo >&2 "$pretty_time: ERROR: Already in ad block - extra cue-out after $curLength" - else - echo "$pretty_time: INFO: Ad-block started after $curLength seconds" + # Look for tags + re="^#([^:]+):?([0-9.]*)" + if [[ "$line" =~ $re ]]; then + tag="${BASH_REMATCH[1]}" + value="${BASH_REMATCH[2]}" + if [ "$tag" == "EXT-X-TARGETDURATION" ]; then + if [ "$value" != "$target_duration" ]; then + echo >&2 "$pretty_time: INFO: Changing target duration to: $value" + target_duration="$value" + fi + elif [ "$tag" == "EXTINF" ]; then + nextSegmentLength=$value + elif [ "$tag" == "EXT-X-MEDIA-SEQUENCE" ]; then + #echo >&2 "$pretty_time: Got sequence no: $value" + curSeq=$value + if [ $nextSeq -le 0 ]; then + nextSeq=$value fi - mode="ADBLOCK" - expectedAdLength=$value - curLength=0 - elif [ "$tag" == "EXT-X-CUE-IN" ]; then - # End ad block - echo "$pretty_time: INFO: Seeing CUE-IN (Ad block end) after duration: $curLength" - if [ "$mode" == "ADBLOCK" ]; then - if [ "$expectedAdLength" != "$curLength" ]; then - echo >&2 "$pretty_time: WARN: Block was not the length expected ($curLength <> $expectedAdLength)" + fi + if [[ $curSeq -ge $nextSeq ]]; then # we are interested in this tag + if [ "$tag" == "EXT-X-CUE-OUT" ]; then + # Begin ad block + echo >&2 "$pretty_time: INFO: Seeing CUE-OUT (Ad block start) of duration: $value" + if [ "$mode" == "ADBLOCK" ]; then + echo >&2 "$pretty_time: ERROR: Already in ad block - extra cue-out after $curLength" + #else + # echo >&2 "$pretty_time: INFO: Ad-block started after $curLength seconds" + fi + mode="ADBLOCK" + expectedAdLength=$value + curLength=0 + if [ "$RECORD_AD_BLOCKS" -gt 0 ]; then + if [ -z "$lastUsedSegment" ]; then + echo >&2 "$pretty_time: INFO: Skipping recording since we did not see start of it." + else + RECORD_AD_BLOCKS=$(($RECORD_AD_BLOCKS - 1)) + echo >&2 "$pretty_time: INFO: Recording this adblock ($RECORD_AD_BLOCKS left)." + saveStreamBit "$stream" "$lastUsedSegment" "$lastUsedStreamTime" "$recordFolder" + doRecord=1 + doStopRecord=0 + fi + fi + elif [ "$tag" == "EXT-X-CUE-IN" ]; then + # End ad block + echo >&2 "$pretty_time: INFO: Seeing CUE-IN (Ad block end) after duration: $curLength" + if [ "$mode" == "ADBLOCK" ]; then + if [ "$expectedAdLength" != "$curLength" ]; then + echo >&2 "$pretty_time: WARN: Block was not the length expected ($curLength <> $expectedAdLength)" + fi + elif [ "$mode" == "LIVE" ]; then + echo >&2 "$pretty_time: ERROR: Extra CUE-IN outside Ad-block after $curLength" fi - elif [ "$mode" == "LIVE" ]; then - echo >&2 "$pretty_time: ERROR: Extra CUE-IN outside Ad-block after $curLength" + mode="LIVE" + curLength=0 + if [ "$doRecord" == "1" ]; then + echo >&2 "$pretty_time: INFO: Stopping recording." + doStopRecord=1 + fi fi - mode="LIVE" - curLength=0 fi - fi + fi fi done <"$TMPFILE" @@ -219,13 +266,57 @@ function monitorStream() { done } +## Record stream +# +# Saves a bit of the stream and deencrypts on the fly +function saveStreamBit() { + local streamURI="$1" tags="$2" ts="$3" recordFolder="$4" + local key="" keyType="NONE" keyURI="" keyIV=0 + if [ -z "$recordFolder" ]; then + return + fi + if [[ ! -d "$recordFolder" || ! -w "$recordFolder" ]]; then + mkdir -p "$recordFolder" || return + fi + while IFS="" read line; do + re="^[^#].*" + if [[ "$line" =~ $re ]]; then + # Output filename + output="$recordFolder/"$($DATE${ts%.*} +"%Y%m%d-%H-%M-%S")".ts" + uri=$(resolveRelativeURL "$line" "$streamURI") + + # Fetch key if needed + if [[ -n "$keyURI" ]]; then + keyURI=$(resolveRelativeURL "$keyURI" "$streamURI") + key=$(curl $BASIC_CURL_PARAMS "$keyURI" | hexdump -v -e '/1 "%02X"') + # Decrypt on the fly + curl $BASIC_CURL_PARAMS "$uri" | \ + openssl aes-128-cbc -d -K "$key" -iv "$keyIV" -nosalt -out "$output" + else + # Fetch stream to file + curl $BASIC_CURL_PARAMS -o "$output" "$uri" + fi + else + re="^#EXT-X-KEY:METHOD=(AES-128|NONE)(,URI=\"([^\"]*)\",IV=0x([0-9A-F]+))?" + if [[ "$line" =~ $re ]]; then + keyType="${BASH_REMATCH[1]}" + keyURI="${BASH_REMATCH[3]}" + keyIV="${BASH_REMATCH[4]}" + fi + fi + done <<< $(echo -n "$tags") +} + + +#### MAIN + while true; do streamurl=$(resolveFirstStream "$1") - if [ -e "$streamurl" ]; then - echo >&2 "ERROR: Invalid HLS-stream: $1" + if [ -z "$streamurl" ]; then + echo >&2 "INFO: (will retry in 10 seconds)" sleep 10 + else + monitorStream "$streamurl" "$2" fi - - monitorStream "$streamurl" done diff --git a/start-monitors.sh b/start-monitors.sh index 8a32495..4d23069 100755 --- a/start-monitors.sh +++ b/start-monitors.sh @@ -4,6 +4,12 @@ # Starts up monitor script per stream # ================================================================ # +# +# Usage: +# ./start-monitors.sh +# +# (will fork into a background daemon) +# # Author: Kristian Kræmmer Nielsen @@ -27,24 +33,38 @@ function fatal() { cd $(dirname $0) source "config.sh" || fatal "Missing config.sh" -trap "stop_jobs" EXIT - if [ ! -d "$log_dir" ]; then mkdir -p "$log_dir" || exit 1 fi -for stream in "${streams[@]}"; do +if [ ! -d "$playlist_dir" ]; then + mkdir -p "$playlist_dir" || exit 1 +fi - NAME="" URL="" - declare ${stream} - echo "Starting monitor for $NAME ..." - ./monitor-dai.sh "$URL" >>"logs/$NAME.log" 2>&1 & - myjobs+=($!) +if [ ! -d "$recordings_dir" ]; then + mkdir -p "$recordings_dir" || exit 1 +fi -done + exec 0>&- 2>&- +( -echo "All started." + trap "stop_jobs" EXIT -wait + for stream in "${streams[@]}"; do + + NAME="" URL="" + declare ${stream} + echo "Starting monitor for $NAME ..." + ./monitor-dai.sh "$URL" >"$playlist_dir/$NAME.m3u8" 2>"$log_dir/$NAME.log" "$recordings_dir/$NAME/" & + myjobs+=($!) + + done + + echo "All started." + echo "(To shutdown all run: kill $BASHPID)" + + exec 1>&- + + wait +) & -echo "Stopped unexpected." -- 2.52.0