# ================================================================
#
# Author: Kristian Kræmmer Nielsen <jkkn@tv2.dk>
+# License: GPL3
#
# Purpose:
# - Continuiously stream one HLS-stream
# - 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
# 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"
# Monitor stream
function monitorStream() {
local stream="$1"
+ local recordFolder="$2"
local nextSegmentLength=0
local nextSeq=-1
local curSeq=-1
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)
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"
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