Now we can record segments of ad blocks and the playlists
authorKristian Kræmmer Nielsen <jkkn@jkkn.dk>
Tue, 14 Mar 2017 15:44:06 +0000 (16:44 +0100)
committerKristian Kræmmer Nielsen <jkkn@jkkn.dk>
Tue, 14 Mar 2017 15:44:44 +0000 (16:44 +0100)
.gitignore
monitor-dai.sh
start-monitors.sh

index 3fc2310f827b3e03300b1f1f082e9da06703e7e2..932811b85a2dc047cc2e6676e986754c3eae5cdb 100644 (file)
@@ -1,2 +1,4 @@
 config.sh
 logs/
+playlists/
+recordings/
index 9e6b4fdde7cbf2556cd0b4b100b50d6400f76cc7..af571f1b348ae7668a35e4820466bf92f17f2ef6 100755 (executable)
@@ -5,6 +5,7 @@
 # ================================================================
 #
 #  Author: Kristian Kræmmer Nielsen <jkkn@tv2.dk>
+#  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
 
index 8a3249577e361c83f9b9f9f0b87ec0f347b3a688..4d2306998d3b9fd6f995c7ac5010b109351e8aa9 100755 (executable)
@@ -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 <jkkn@tv2.dk>
 
@@ -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."