65 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/ah4c.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | # 2024.10.30
3 | # GitHub home for this project with setup instructions: https://github.com/sullrich/ah4c
4 | # Docker Hub home for this project: https://hub.docker.com/repository/docker/bnhf/ah4c
5 | ah4c:
6 | image: bnhf/ah4c:${TAG}
7 | container_name: ah4c
8 | hostname: ah4c
9 | dns_search: ${DOMAIN} # Specify the name of your LAN's domain, usually local or localdomain
10 | ports:
11 | - ${ADBS_PORT}:5037 # Port used by adb-server
12 | - ${HOST_PORT}:7654 # Port used by this ah4c proxy
13 | - ${WSCR_PORT}:8000 # Port used by ws-scrcpy
14 | environment:
15 | - IPADDRESS=${IPADDRESS} # Hostname or IP address of this ah4c extension to be used in M3U file (also add port number if not in M3U)
16 | - NUMBER_TUNERS=${NUMBER_TUNERS} # Number of tuners you'd like defined 1, 2, 3, 4, 5, 6, 7, 8 or 9 supported
17 | - TUNER1_IP=${TUNER1_IP} # Streaming device #1 with adb port in the form hostname:port or ip:port
18 | - TUNER2_IP=${TUNER2_IP} # Streaming device #2 with adb port in the form hostname:port or ip:port
19 | - TUNER3_IP=${TUNER3_IP} # Streaming device #3 with adb port in the form hostname:port or ip:port
20 | - TUNER4_IP=${TUNER4_IP} # Streaming device #4 with adb port in the form hostname:port or ip:port
21 | - TUNER5_IP=${TUNER5_IP} # Streaming device #5 with adb port in the form hostname:port or ip:port
22 | - TUNER6_IP=${TUNER6_IP} # Streaming device #6 with adb port in the form hostname:port or ip:port
23 | - TUNER7_IP=${TUNER7_IP} # Streaming device #7 with adb port in the form hostname:port or ip:port
24 | - TUNER8_IP=${TUNER8_IP} # Streaming device #8 with adb port in the form hostname:port or ip:port
25 | - TUNER9_IP=${TUNER9_IP} # Streaming device #9 with adb port in the form hostname:port or ip:port
26 | - ENCODER1_URL=${ENCODER1_URL} # Full URL for tuner #1 in the form http://hostname/stream or http://ip/stream
27 | - ENCODER2_URL=${ENCODER2_URL} # Full URL for tuner #2 in the form http://hostname/stream or http://ip/stream
28 | - ENCODER3_URL=${ENCODER3_URL} # Full URL for tuner #3 in the form http://hostname/stream or http://ip/stream
29 | - ENCODER4_URL=${ENCODER4_URL} # Full URL for tuner #4 in the form http://hostname/stream or http://ip/stream
30 | - ENCODER5_URL=${ENCODER5_URL} # Full URL for tuner #5 in the form http://hostname/stream or http://ip/stream
31 | - ENCODER6_URL=${ENCODER6_URL} # Full URL for tuner #6 in the form http://hostname/stream or http://ip/stream
32 | - ENCODER7_URL=${ENCODER7_URL} # Full URL for tuner #7 in the form http://hostname/stream or http://ip/stream
33 | - ENCODER8_URL=${ENCODER8_URL} # Full URL for tuner #8 in the form http://hostname/stream or http://ip/stream
34 | - ENCODER9_URL=${ENCODER9_URL} # Full URL for tuner #9 in the form http://hostname/stream or http://ip/stream
35 | - STREAMER_APP=${STREAMER_APP} # Streaming device name and streaming app you're using in the form scripts/streamer/app (use lowercase with slashes between as shown)
36 | - CHANNELSIP=${CHANNELSIP} # Hostname or IP address of the Channels DVR server itself
37 | - ALERT_SMTP_SERVER=${ALERT_SMTP_SERVER} # The domainname:port of the SMTP server you'll be using like smtp.gmail.com:587. This is for sending ah4c alerts if tuning fails.
38 | - ALERT_AUTH_SERVER=${ALERT_AUTH_SERVER} # The auth server for the e-mail you'll be using like smtp.gmail.com
39 | - ALERT_EMAIL_FROM=${ALERT_EMAIL_FROM} # The e-mail address you'd like your ah4c failure alert e-mails to show as being from.
40 | - ALERT_EMAIL_PASS=${ALERT_EMAIL_PASS} # Gmail and Yahoo both support the creation of app-specific e-mail passwords, and this is the way to go! It's NOT recommended to use your everyday e-mail password.
41 | - ALERT_EMAIL_TO=${ALERT_EMAIL_TO} # The e-mail address you'd like your alert e-mails sent to.
42 | #- ALERT_WEBHOOK_URL=""
43 | - LIVETV_ATTEMPTS=${LIVETV_ATTEMPTS} # For FireTV Live Guide tuning only, set maximum number of attempts at finding the desired channel
44 | - CREATE_M3US=${CREATE_M3US} # Set to true to create device-specific M3Us for use with Amazon Prime Premium channels -- requires a FireTV device
45 | - UPDATE_SCRIPTS=${UPDATE_SCRIPTS} # Set to true if you'd like the sample scripts and STREAMER_APP scripts updated whether they exist or not
46 | - UPDATE_M3US=${UPDATE_M3US} # Set to true if you'd like the sample m3us updated whether they exist or not
47 | - TZ=${TZ} # Your local timezone in Linux "tz" format
48 | - SPEED_MODE=${SPEED_MODE} # Set to false if you'd like the target streaming app to be closed after each tuning cycle (limited script support).
49 | - KEEP_WATCHING=${KEEP_WATCHING} # In supported scripts, set the delay before resending a tuning deeplink to prevent "Are you still watching?" type messages. Examples: Use 4h for 4 hours or 240m for 240 minutes.
50 | volumes:
51 | - ${HOST_DIR}/ah4c/scripts:/opt/scripts # pre/stop/bmitune.sh scripts will be stored in this bound host directory under streamer/app
52 | - ${HOST_DIR}/ah4c/m3u:/opt/m3u # m3u files will be stored here and hosted at http://:7654/m3u for use in Channels DVR - Custom Channels settings
53 | - ${HOST_DIR}/ah4c/adb:/root/.android # Persistent data directory for adb keys
54 | restart: unless-stopped
--------------------------------------------------------------------------------
/scripts/firetv/livetv/createm3u.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | #createm3u.sh for firetv/livetv
3 |
4 | #Debug on if uncommented
5 | #set -x
6 |
7 | #Global
8 | streamerIP="$1"
9 | streamerNoPort="${streamerIP%%:*}"
10 | adbTarget="adb -s $streamerIP"
11 | m3uName="$streamerNoPort.m3u"
12 |
13 | initializeDevice() {
14 | redEcho "Waking $streamerNoPort..."
15 |
16 | $adbTarget shell input keyevent KEYCODE_WAKEUP; sleep 2
17 | $adbTarget shell input keyevent KEYCODE_HOME; sleep 2
18 | $adbTarget logcat -c; sleep 2
19 | $adbTarget shell input keyevent KEYCODE_LIVE_TV; sleep 2
20 | channelID=$($adbTarget shell "input keyevent KEYCODE_LIVE_TV && logcat -d" | grep GuideManager | tail -n 1 | awk -F? '{print$3}')
21 |
22 | while [ -z $channelID ]; do
23 | channelID=$($adbTarget shell "input keyevent KEYCODE_DPAD_UP; input keyevent KEYCODE_DPAD_DOWN; logcat -d" | grep GuideManager | tail -n 1 | awk -F? '{print$3}')
24 | done
25 |
26 | startingChannel=$channelID
27 | }
28 |
29 | sortM3U() {
30 | redEcho "Sorting livetv.m3u alphabetically to match FireTV LiveTV Guide..."
31 |
32 | cp m3u/livetv.m3u m3u/$m3uName
33 | sed -i ':a;N;$!ba;s/\nhttp/ ,http/g' m3u/$m3uName
34 | [ -z "$SORT_M3US" ] || [ "$SORT_M3US" == "true" ] \
35 | && sort -t',' -k2 -f -o m3u/$m3uName m3u/$m3uName
36 | sed -i '/^$/d' m3u/$m3uName
37 | sed -i 's/ \& / AND /g' m3u/$m3uName
38 | }
39 |
40 | updateM3U() {
41 | redEcho "Reading $m3uName and updating it with device specific channelID..."
42 | echo "Starting channelID is $channelID"
43 |
44 | while IFS= read -r currentLineM3U; do
45 | if [ "$currentLineM3U" != "#EXTM3U" ]; then
46 | echo "$currentLineM3U" | awk -F, '{print "assigned to M3U channel name: " $2 "\n"}'
47 | newLineM3U=$(echo "$currentLineM3U" | sed 's|tuner/.*|tuner/'"$channelID"'|')
48 | sed -i 's|'"$currentLineM3U"'|'"$newLineM3U"'|' m3u/$m3uName
49 | channelID=$($adbTarget "$streamerNoPort/stream_stopped"
27 | [[ -f "$streamerNoPort/last_channel" ]] || echo 0 > "$streamerNoPort/last_channel"
28 |
29 | # Write PID for this script to bmitune_pid for use in stopbmitune.sh
30 | echo $$ > "$streamerNoPort/bmitune_pid"
31 | echo "Current PID for this script is $$"
32 | }
33 |
34 | #Set encoderURL based on the value of streamerIP
35 | matchEncoderURL() {
36 |
37 | case "$streamerIP" in
38 | "$TUNER1_IP")
39 | encoderURL=$ENCODER1_URL
40 | ;;
41 | "$TUNER2_IP")
42 | encoderURL=$ENCODER2_URL
43 | ;;
44 | "$TUNER3_IP")
45 | encoderURL=$ENCODER3_URL
46 | ;;
47 | "$TUNER4_IP")
48 | encoderURL=$ENCODER4_URL
49 | ;;
50 | *)
51 | exit 1
52 | ;;
53 | esac
54 | }
55 |
56 | #Check for active audio stream
57 | activeAudioCheck() {
58 | local startTime=$(date +%s)
59 | local maxDuration=60
60 | local minimumLoudness=-50
61 | local sleepDuration=0.5
62 |
63 | while true; do
64 | checkLoudness=$(ffmpeg -t 1 -i $encoderURL -filter:a ebur128 -map 0:a -f null -hide_banner - 2>&1 | awk '/I: /{print $2}')
65 |
66 | if (( $(date +%s) - $startTime > $maxDuration )); then
67 | echo "Active audio stream not detected in $maxDuration seconds."
68 | exit 1
69 | fi
70 |
71 | if (( $(echo "$checkLoudness > $minimumLoudness" | bc -l) )); then
72 | echo "Active audio stream detected with $checkLoudness LUF."
73 | break
74 | fi
75 |
76 | if appFocusCheck 0; then
77 | echo "Active audio stream not yet detected -- loudness is $checkLoudness LUF. Continuing..."
78 | sleep $sleepDuration
79 | else
80 | echo "No active audio stream detected and app is not in focus after $(($(date +%s) - $startTime)) seconds -- attempting to tune again..."
81 | tuneChannel
82 | fi
83 |
84 | done
85 | }
86 |
87 | appFocusCheck() {
88 | appFocus=$($adbTarget shell dumpsys window windows | grep -E 'mCurrentFocus' | cut -d '/' -f1 | sed 's/.* //g')
89 |
90 | if [[ $appFocus == $packageName ]]; then
91 | return 0
92 | else
93 | return 1
94 | fi
95 | }
96 |
97 | #Special channels to kill DirecTV app or reboot FireStick
98 | specialChannels() {
99 |
100 | if [ $specialID = "exit" ]; then
101 | echo "Exit $packageName requested on $streamerIP"
102 | rm $streamerNoPort/last_channel $streamerNoPort/adbAppRunning
103 | $adbTarget shell am force-stop $packageName
104 | exit 0
105 | elif [ $specialID = "reboot" ]; then
106 | echo "Reboot $streamerIP requested"
107 | rm $streamerNoPort/last_channel $streamerNoPort/adbAppRunning
108 | $adbTarget reboot
109 | exit 0
110 | elif [[ -f $streamerNoPort/adbCommunicationFail ]]; then
111 | rm $streamerNoPort/adbCommunicationFail
112 | exit 1
113 | else
114 | echo "Not a special channel (exit nor reboot)"
115 | #if appFocusCheck; then
116 | #echo "$packageName is the app in focus, OK to tune"
117 | #fi
118 | fi
119 | }
120 |
121 | #Variable delay based on whether app was running or needed to be launched
122 | #and whether less than maxTime seconds (maxTime/3600 for hours) has passed while sleeping
123 | launchDelay() {
124 | local lastChannel
125 | local lastAwake
126 | local timeNow
127 | local timeElapsed
128 | local maxTime=14400
129 |
130 | lastChannel=$(<"$streamerNoPort/last_channel")
131 | lastAwake=$(<"$streamerNoPort/stream_stopped")
132 | timeNow=$(date +%s)
133 | timeElapsed=$(($timeNow - $lastAwake))
134 |
135 | if (( $lastChannel == $specialID )) && (( $timeElapsed < $maxTime )); then
136 | echo "Last channel selected on this tuner, no channel change required"
137 | exit 0
138 | elif [ -f $streamerNoPort/adbAppRunning ] && (( $timeElapsed < $maxTime )); then
139 | activeAudioCheck
140 | #sleep 14
141 | rm $streamerNoPort/adbAppRunning
142 | echo $specialID > "$streamerNoPort/last_channel"
143 | else
144 | activeAudioCheck
145 | #sleep 32
146 | echo $specialID > "$streamerNoPort/last_channel"
147 | fi
148 | }
149 |
150 | #Tuning is based on channel name values from sling.m3u.
151 | tuneChannel() {
152 | #channelName=$(awk '/channel-id='"$channelID"'/ {getline; print}' m3u/sling.m3u | cut -d'/' -f6)
153 | #channelName=$(echo $channelName | sed 's/^/"/;s/$/"/')
154 |
155 | #livetvMenu="input keyevent KEYCODE_HOME"
156 |
157 | #livetvGuide="input keyevent KEYCODE_LIVE_TV"
158 |
159 | #livetvTune="input keyevent KEYCODE_DPAD_DOWN"
160 |
161 | #$adbTarget shell $livetvMenu
162 | #$adbTarget shell $livetvGuide
163 | #$adbTarget shell $livetvGuide
164 | #$adbTarget shell input text $channelName
165 | #$adbTarget shell $livetvTune
166 | #livetvTune
167 | $adbTarget shell am start -a android.intent.action.VIEW -d https://watch.sling.com/1/channel/$channelID/watch
168 | }
169 |
170 | main() {
171 | updateReferenceFiles
172 | matchEncoderURL
173 | specialChannels
174 | #launchDelay
175 | tuneChannel
176 | activeAudioCheck
177 | }
178 |
179 | main
180 |
--------------------------------------------------------------------------------
/scripts/firetv/fubo/bmitune.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #bmitune.sh for firetv/fubo
3 |
4 | #Debug on if uncommented
5 | set -x
6 |
7 | #Global
8 | channelID="$1"
9 | specialID="$1"
10 | streamerIP="$2"
11 | streamerNoPort="${streamerIP%%:*}"
12 | adbTarget="adb -s $streamerIP"
13 | packageName=com.fubo.firetv.screen
14 |
15 | #Trap end of script run
16 | finish() {
17 | echo "bmitune.sh is exiting for $streamerIP with exit code $?"
18 | }
19 |
20 | trap finish EXIT
21 |
22 | updateReferenceFiles() {
23 |
24 | # Handle cases where stream_stopped or last_channel don't exist
25 | mkdir -p $streamerNoPort
26 | [[ -f "$streamerNoPort/stream_stopped" ]] || echo 0 > "$streamerNoPort/stream_stopped"
27 | [[ -f "$streamerNoPort/last_channel" ]] || echo 0 > "$streamerNoPort/last_channel"
28 |
29 | # Write PID for this script to bmitune_pid for use in stopbmitune.sh
30 | echo $$ > "$streamerNoPort/bmitune_pid"
31 | echo "Current PID for this script is $$"
32 | }
33 |
34 | #Set encoderURL based on the value of streamerIP
35 | matchEncoderURL() {
36 |
37 | case "$streamerIP" in
38 | "$TUNER1_IP")
39 | encoderURL=$ENCODER1_URL
40 | ;;
41 | "$TUNER2_IP")
42 | encoderURL=$ENCODER2_URL
43 | ;;
44 | "$TUNER3_IP")
45 | encoderURL=$ENCODER3_URL
46 | ;;
47 | "$TUNER4_IP")
48 | encoderURL=$ENCODER4_URL
49 | ;;
50 | *)
51 | exit 1
52 | ;;
53 | esac
54 | }
55 |
56 | #Check for active audio stream
57 | activeAudioCheck() {
58 | local startTime=$(date +%s)
59 | local maxDuration=60
60 | local minimumLoudness=-50
61 | local sleepDuration=0.5
62 |
63 | while true; do
64 | checkLoudness=$(ffmpeg -t 1 -i $encoderURL -filter:a ebur128 -map 0:a -f null -hide_banner - 2>&1 | awk '/I: /{print $2}')
65 |
66 | if (( $(date +%s) - $startTime > $maxDuration )); then
67 | echo "Active audio stream not detected in $maxDuration seconds."
68 | exit 1
69 | fi
70 |
71 | if (( $(echo "$checkLoudness > $minimumLoudness" | bc -l) )); then
72 | echo "Active audio stream detected with $checkLoudness LUF."
73 | break
74 | fi
75 |
76 | if appFocusCheck 0; then
77 | echo "Active audio stream not yet detected -- loudness is $checkLoudness LUF. Continuing..."
78 | sleep $sleepDuration
79 | else
80 | echo "No active audio stream detected and app is not in focus after $(($(date +%s) - $startTime)) seconds -- attempting to tune again..."
81 | #tuneChannel
82 | $adbTarget shell input keyevent KEYCODE_CENTER
83 | fi
84 |
85 | done
86 | }
87 |
88 | appFocusCheck() {
89 | appFocus=$($adbTarget shell dumpsys window windows | grep -E 'mCurrentFocus' | cut -d '/' -f1 | sed 's/.* //g')
90 |
91 | if [[ $appFocus == $packageName ]]; then
92 | return 0
93 | else
94 | return 1
95 | fi
96 | }
97 |
98 | #Special channels to kill DirecTV app or reboot FireStick
99 | specialChannels() {
100 |
101 | if [ $specialID = "exit" ]; then
102 | echo "Exit $packageName requested on $streamerIP"
103 | rm $streamerNoPort/last_channel $streamerNoPort/adbAppRunning
104 | $adbTarget shell am force-stop $packageName
105 | exit 0
106 | elif [ $specialID = "reboot" ]; then
107 | echo "Reboot $streamerIP requested"
108 | rm $streamerNoPort/last_channel $streamerNoPort/adbAppRunning
109 | $adbTarget reboot
110 | exit 0
111 | elif [[ -f $streamerNoPort/adbCommunicationFail ]]; then
112 | rm $streamerNoPort/adbCommunicationFail
113 | exit 1
114 | else
115 | echo "Not a special channel (exit nor reboot)"
116 | #if appFocusCheck; then
117 | #echo "$packageName is the app in focus, OK to tune"
118 | #fi
119 | fi
120 | }
121 |
122 | #Variable delay based on whether app was running or needed to be launched
123 | #and whether less than maxTime seconds (maxTime/3600 for hours) has passed while sleeping
124 | launchDelay() {
125 | local lastChannel
126 | local lastAwake
127 | local timeNow
128 | local timeElapsed
129 | local maxTime=14400
130 |
131 | lastChannel=$(<"$streamerNoPort/last_channel")
132 | lastAwake=$(<"$streamerNoPort/stream_stopped")
133 | timeNow=$(date +%s)
134 | timeElapsed=$(($timeNow - $lastAwake))
135 |
136 | if (( $lastChannel == $specialID )) && (( $timeElapsed < $maxTime )); then
137 | echo "Last channel selected on this tuner, no channel change required"
138 | exit 0
139 | elif [ -f $streamerNoPort/adbAppRunning ] && (( $timeElapsed < $maxTime )); then
140 | activeAudioCheck
141 | #sleep 14
142 | rm $streamerNoPort/adbAppRunning
143 | echo $specialID > "$streamerNoPort/last_channel"
144 | else
145 | activeAudioCheck
146 | #sleep 32
147 | echo $specialID > "$streamerNoPort/last_channel"
148 | fi
149 | }
150 |
151 | #Tuning is based on channel name values from fubo.m3u.
152 | tuneChannel() {
153 | #channelName=$(awk '/channel-id='"$channelID"'/ {getline; print}' m3u/fubo.m3u | cut -d'/' -f6)
154 | #channelName=$(echo $channelName | sed 's/^/"/;s/$/"/')
155 |
156 | #livetvMenu="input keyevent KEYCODE_HOME"
157 |
158 | #livetvGuide="input keyevent KEYCODE_LIVE_TV"
159 |
160 | #livetvTune="input keyevent KEYCODE_DPAD_DOWN"
161 |
162 | #$adbTarget shell $livetvMenu
163 | #$adbTarget shell $livetvGuide
164 | #$adbTarget shell $livetvGuide
165 | #$adbTarget shell input text $channelName
166 | #$adbTarget shell $livetvTune
167 | #livetvTune
168 | $adbTarget shell am start -a android.intent.action.VIEW -d https://link.fubo.tv/al1%3Fv%3D1%26a%3Dplay%26t%3Dchannel%26channel_id%3D$channelID
169 | }
170 |
171 | main() {
172 | updateReferenceFiles
173 | matchEncoderURL
174 | specialChannels
175 | #launchDelay
176 | tuneChannel
177 | activeAudioCheck
178 | }
179 |
180 | main
181 |
--------------------------------------------------------------------------------
/docker-start-pyatv.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # docker-start-pyatv.sh
3 | # 2025.01.22
4 |
5 | #androids=( $TUNER1_IP $TUNER2_IP $TUNER3_IP $TUNER4_IP )
6 |
7 | # Make tuner hostnames without local domain name resolvable in Alpine containers by adding each to /etc/hosts
8 | fixTunerDNS() {
9 |
10 | local androids=($@)
11 | local resolvFile=/etc/resolv.conf
12 | local hostsFile=/etc/hosts
13 | local localDomain=$(awk '/search/ {print $2}' $resolvFile)
14 | local ipv4Pattern='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
15 | local hostnamePattern='^[a-zA-Z0-9_-]+$'
16 |
17 | for android in "${androids[@]}"
18 | do
19 | local tunerNoPort="${android%%:*}"
20 |
21 | if [[ -n $$android ]]; then
22 | if [[ $tunerNoPort =~ $ipv4Pattern ]]; then
23 | break
24 | elif [[ $tunerNoPort =~ $hostnamePattern ]]; then
25 | tunerIP=$(dig +short $tunerNoPort.$localDomain)
26 | echo "$tunerIP $tunerNoPort" >> $hostsFile
27 | fi
28 | fi
29 | done
30 | }
31 |
32 | # Make encoder hostnames without local domain name resolvable in Alpine containers by adding each to /etc/hosts
33 | fixEncoderDNS() {
34 |
35 | local encoders=($@)
36 | local resolvFile=/etc/resolv.conf
37 | local hostsFile=/etc/hosts
38 | local localDomain=$(awk '/search/ {print $2}' $resolvFile)
39 | local ipv4Pattern='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
40 | local hostnamePattern='^[a-zA-Z0-9_-]+$'
41 |
42 | for encoder in "${encoders[@]}"
43 | do
44 | local encoderNoURL=$(echo "$encoder" | sed -n 's|^.*://\([^/]*\)/.*|\1|p')
45 |
46 | if [[ -n $encoder ]]; then
47 | if [[ $encoderNoURL =~ $ipv4Pattern ]]; then
48 | break
49 | elif [[ $encoderNoURL =~ $hostnamePattern ]]; then
50 | encoderIP=$(dig +short $encoderNoURL.$localDomain)
51 | echo "$encoderIP $encoderNoURL" >> $hostsFile
52 | fi
53 | fi
54 | done
55 |
56 | awk '!a[$0]++' $hostsFile
57 | }
58 |
59 | # List currently connected adb devices and then connect to each indivdually
60 | adbConnections() {
61 |
62 | local androids=($@)
63 | adb devices
64 |
65 | for android in "${androids[@]}"
66 | do
67 | if [[ -n $android ]]; then
68 | adb connect $android
69 | fi
70 | done
71 | }
72 |
73 | # List currently connected atv devices and then connect to each indivdually
74 | atvConnections() {
75 |
76 | local atvs=($@)
77 |
78 | for atv in "${atvs[@]}"
79 | do
80 | if [[ -n $atv ]]; then
81 | atvremote --scan-hosts $atv scan
82 | #atvremote -s $atv --protocol airplay pair
83 | #atvremote -s $atv --protocol companion pair
84 | #atvremote -s $atv --protocol raop pair
85 | fi
86 | done
87 | }
88 |
89 | # Check if a given script is already present in the appropriate scripts directory, and if not, copy it
90 | checkScripts() {
91 |
92 | local scripts=($@)
93 | mkdir -p ./scripts/firetv/directv ./$STREAMER_APP
94 | #scripts=( prebmitune.sh bmitune.sh stopbmitune.sh isconnected.sh keep_alive.sh reboot.sh )
95 |
96 | for script in "${scripts[@]}"
97 | do
98 | if [ ! -f /opt/scripts/firetv/directv/$script ] && [ -f /tmp/scripts/firetv/directv/$script ] || [[ $UPDATE_SCRIPTS == "true" ]]; then
99 | cp /tmp/scripts/firetv/directv/$script ./scripts/firetv/directv 2>/dev/null \
100 | && chmod +x ./scripts/firetv/directv/$script \
101 | && echo "No existing ./scripts/firetv/directv/$script found or UPDATE_SCRIPTS set to true"
102 | else
103 | if [ -f /tmp/scripts/firetv/directv/$script ]; then
104 | echo "Existing ./scripts/firetv/directv/$script found, and will be preserved"
105 | fi
106 | fi
107 |
108 | if [ ! -f /opt/$STREAMER_APP/$script ] && [ -f /tmp/$STREAMER_APP/$script ] || [[ $UPDATE_SCRIPTS == "true" ]]; then
109 | cp /tmp/$STREAMER_APP/$script ./$STREAMER_APP 2>/dev/null \
110 | && chmod +x ./$STREAMER_APP/$script \
111 | && echo "No existing ./$STREAMER_APP/$script found or UPDATE_SCRIPTS set to true"
112 | else
113 | if [ -f /tmp/$STREAMER_APP/$script ]; then
114 | echo "Existing ./$STREAMER_APP/$script found, and will be preserved"
115 | fi
116 | fi
117 | done
118 | }
119 |
120 | # Check if a given M3U file is already present in the M3U directory, and if not, copy it
121 | checkM3Us() {
122 |
123 | local m3us=($@)
124 | mkdir -p ./m3u
125 | #m3us=( directv.m3u foo-fighters.m3u hulu.m3u youtubetv.m3u )
126 |
127 | for m3u in "${m3us[@]}"
128 | do
129 | if [ ! -f /opt/m3u/$m3u ] || [[ $UPDATE_M3US == "true" ]]; then
130 | cp /tmp/m3u/$m3u ./m3u \
131 | && echo "No existing $m3u found or UPDATE_M3US set to true"
132 | else
133 | echo "Existing $m3u found, and will be preserved"
134 | fi
135 | done
136 | }
137 |
138 | # Create device specific M3Us for use with firetv/livetv channels
139 | createM3Us() {
140 | local androids=($@)
141 |
142 | for android in "${androids[@]}"
143 | do
144 | if [[ -n $android ]] && [[ $CREATE_M3US == "true" ]]; then
145 | adb -s $android shell input keyevent KEYCODE_WAKEUP; sleep 5
146 | adb -s $android shell reboot; sleep 45
147 | $STREAMER_APP/createm3u.sh $android
148 | fi
149 | done
150 | }
151 |
152 | # Fix hostanme resolution, connect adb devices, copy scripts and M3U files as needed, start ws-scrcpy and ah4c
153 | main() {
154 |
155 | fixTunerDNS $TUNER1_IP $TUNER2_IP $TUNER3_IP $TUNER4_IP
156 | fixEncoderDNS $ENCODER1_URL $ENCODER2_URL $ENCODER3_URL $ENCODER4_URL
157 | atvConnections $TUNER1_IP $TUNER2_IP $TUNER3_IP $TUNER4_IP
158 | checkScripts prebmitune.sh bmitune.sh stopbmitune.sh isconnected.sh keep_alive.sh reboot.sh createm3u.sh atvpair.sh
159 | checkM3Us directv.m3u dtvosprey.m3u dtvstream.m3u foo-fighters.m3u fubo.m3u hulu.m3u livetv.m3u npo.m3u silicondust.m3u sling.m3u spectrum.m3u youtubetv_shield.m3u youtubetv.m3u
160 | #createM3Us $TUNER1_IP $TUNER2_IP $TUNER3_IP $TUNER4_IP
161 | [[ -n $USER_SCRIPT ]] && { ./"$USER_SCRIPT" & } || echo "No user-defined custom script to run"
162 | ./ah4c
163 | }
164 |
165 | main
166 |
--------------------------------------------------------------------------------
/docker-start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # docker-start.sh
3 | # 2025.09.23
4 |
5 | # Ensure render group can access GPU device
6 | [[ -c /dev/dri/renderD128 ]] && chgrp render /dev/dri/renderD128
7 |
8 | #androids=( $TUNER1_IP $TUNER2_IP $TUNER3_IP $TUNER4_IP )
9 | #[[ "$STREAMER_APP" == *"/atv/"* ]] && appleTV=true
10 |
11 | # Make tuner hostnames without local domain name resolvable in Alpine containers by adding each to /etc/hosts
12 | fixTunerDNS() {
13 |
14 | local androids=($@)
15 | local resolvFile=/etc/resolv.conf
16 | local hostsFile=/etc/hosts
17 | local localDomain=$(awk '/search/ {print $2}' $resolvFile)
18 | local ipv4Pattern='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
19 | local hostnamePattern='^[a-zA-Z0-9_-]+$'
20 |
21 | for android in "${androids[@]}"
22 | do
23 | local tunerNoPort="${android%%:*}"
24 |
25 | if [[ -n $$android ]]; then
26 | if [[ $tunerNoPort =~ $ipv4Pattern ]]; then
27 | break
28 | elif [[ $tunerNoPort =~ $hostnamePattern ]]; then
29 | tunerIP=$(dig +short $tunerNoPort.$localDomain)
30 | echo "$tunerIP $tunerNoPort" >> $hostsFile
31 | fi
32 | fi
33 | done
34 | }
35 |
36 | # Make encoder hostnames without local domain name resolvable in Alpine containers by adding each to /etc/hosts
37 | fixEncoderDNS() {
38 |
39 | local encoders=($@)
40 | local resolvFile=/etc/resolv.conf
41 | local hostsFile=/etc/hosts
42 | local localDomain=$(awk '/search/ {print $2}' $resolvFile)
43 | local ipv4Pattern='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
44 | local hostnamePattern='^[a-zA-Z0-9_-]+$'
45 |
46 | for encoder in "${encoders[@]}"
47 | do
48 | local encoderNoURL=$(echo "$encoder" | sed -n 's|^.*://\([^/]*\)/.*|\1|p')
49 |
50 | if [[ -n $encoder ]]; then
51 | if [[ $encoderNoURL =~ $ipv4Pattern ]]; then
52 | break
53 | elif [[ $encoderNoURL =~ $hostnamePattern ]]; then
54 | encoderIP=$(dig +short $encoderNoURL.$localDomain)
55 | echo "$encoderIP $encoderNoURL" >> $hostsFile
56 | fi
57 | fi
58 | done
59 |
60 | awk '!a[$0]++' $hostsFile
61 | }
62 |
63 | # List currently connected adb devices and then connect to each indivdually
64 | adbConnections() {
65 |
66 | local androids=($@)
67 | adb devices
68 |
69 | for android in "${androids[@]}"
70 | do
71 | if [[ -n $android ]]; then
72 | adb connect $android
73 | fi
74 | done
75 | }
76 |
77 | # List currently connected atv devices and then connect to each indivdually
78 | atvConnections() {
79 |
80 | local atvs=($@)
81 |
82 | for atv in "${atvs[@]}"
83 | do
84 | if [[ -n $atv ]]; then
85 | atvremote --scan-hosts $atv scan
86 | #atvremote -s $atv --protocol airplay pair
87 | #atvremote -s $atv --protocol companion pair
88 | #atvremote -s $atv --protocol raop pair
89 | fi
90 | done
91 | }
92 |
93 | # Check if a given script is already present in the appropriate scripts directory, and if not, copy it
94 | checkScripts() {
95 |
96 | local scripts=($@)
97 | mkdir -p ./scripts/firetv/directv ./$STREAMER_APP
98 | #scripts=( prebmitune.sh bmitune.sh stopbmitune.sh isconnected.sh keep_alive.sh reboot.sh )
99 |
100 | for script in "${scripts[@]}"
101 | do
102 | if [ ! -f /opt/scripts/firetv/directv/$script ] && [ -f /tmp/scripts/firetv/directv/$script ] || [[ $UPDATE_SCRIPTS == "true" ]]; then
103 | cp /tmp/scripts/firetv/directv/$script ./scripts/firetv/directv 2>/dev/null \
104 | && chmod +x ./scripts/firetv/directv/$script \
105 | && echo "No existing ./scripts/firetv/directv/$script found or UPDATE_SCRIPTS set to true"
106 | else
107 | if [ -f /tmp/scripts/firetv/directv/$script ]; then
108 | echo "Existing ./scripts/firetv/directv/$script found, and will be preserved"
109 | fi
110 | fi
111 |
112 | if [ ! -f /opt/$STREAMER_APP/$script ] && [ -f /tmp/$STREAMER_APP/$script ] || [[ $UPDATE_SCRIPTS == "true" ]]; then
113 | cp /tmp/$STREAMER_APP/$script ./$STREAMER_APP 2>/dev/null \
114 | && chmod +x ./$STREAMER_APP/$script \
115 | && echo "No existing ./$STREAMER_APP/$script found or UPDATE_SCRIPTS set to true"
116 | else
117 | if [ -f /tmp/$STREAMER_APP/$script ]; then
118 | echo "Existing ./$STREAMER_APP/$script found, and will be preserved"
119 | fi
120 | fi
121 | done
122 | }
123 |
124 | # Check if a given M3U file is already present in the M3U directory, and if not, copy it
125 | checkM3Us() {
126 |
127 | local m3us=($@)
128 | mkdir -p ./m3u
129 | #m3us=( directv.m3u foo-fighters.m3u hulu.m3u youtubetv.m3u )
130 |
131 | for m3u in "${m3us[@]}"
132 | do
133 | if [ ! -f /opt/m3u/$m3u ] || [[ $UPDATE_M3US == "true" ]]; then
134 | cp /tmp/m3u/$m3u ./m3u \
135 | && echo "No existing $m3u found or UPDATE_M3US set to true"
136 | else
137 | echo "Existing $m3u found, and will be preserved"
138 | fi
139 | done
140 | }
141 |
142 | # Create device specific M3Us for use with firetv/livetv channels
143 | createM3Us() {
144 | local androids=($@)
145 |
146 | for android in "${androids[@]}"
147 | do
148 | if [[ -n $android ]] && [[ $CREATE_M3US == "true" ]]; then
149 | adb -s $android shell input keyevent KEYCODE_WAKEUP; sleep 5
150 | adb -s $android shell reboot; sleep 45
151 | $STREAMER_APP/createm3u.sh $android
152 | fi
153 | done
154 | }
155 |
156 | # Fix hostanme resolution, connect adb devices, copy scripts and M3U files as needed, start ws-scrcpy and ah4c
157 | main() {
158 |
159 | fixTunerDNS $TUNER1_IP $TUNER2_IP $TUNER3_IP $TUNER4_IP $TUNER5_IP $TUNER6_IP $TUNER7_IP $TUNER8_IP $TUNER9_IP
160 | fixEncoderDNS $ENCODER1_URL $ENCODER2_URL $ENCODER3_URL $ENCODER4_URL $ENCODER5_URL $ENCODER6_URL $ENCODER7_URL $ENCODER8_URL $ENCODER9_URL
161 | adbConnections $TUNER1_IP $TUNER2_IP $TUNER3_IP $TUNER4_IP $TUNER5_IP $TUNER6_IP $TUNER7_IP $TUNER8_IP $TUNER9_IP
162 | checkScripts prebmitune.sh bmitune.sh stopbmitune.sh isconnected.sh keep_alive.sh reboot.sh createm3u.sh common.sh
163 | checkM3Us allente.m3u channels.m3u coachella.m3u directv.m3u dtvdeeplinks.m3u dtvosprey.m3u dtvstream.m3u dtvstreamdeeplinks.m3u edc.m3u foo-fighters.m3u fubo.m3u hulu.m3u kodifaves-pbs-seatac.m3u livetv.m3u nbc.m3u npo.m3u pbs-seatac.m3u pbs-worcester.m3u silicondust.m3u sling.m3u spectrum.m3u xfinity.m3u youtubetv_shield.m3u youtubetv.m3u zinwell.m3u
164 | createM3Us $TUNER1_IP $TUNER2_IP $TUNER3_IP $TUNER4_IP $TUNER5_IP $TUNER6_IP $TUNER7_IP $TUNER8_IP $TUNER9_IP
165 | [[ -n $USER_SCRIPT ]] && { ./"$USER_SCRIPT" & } || echo "No user-defined custom script to run"
166 | npm start --prefix ws-scrcpy &
167 | ./ah4c
168 | }
169 |
170 | main
171 |
--------------------------------------------------------------------------------
/scripts/firetv/directv/bmitune.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #bmitune.sh for firetv/directv
3 |
4 | #Debug on if uncommented
5 | set -x
6 |
7 | #Global
8 | channelID=\""$1\""
9 | specialID="$1"
10 | streamerIP="$2"
11 | streamerNoPort="${streamerIP%%:*}"
12 | adbTarget="adb -s $streamerIP"
13 | packageName=com.att.tv
14 | m3uName="${STREAMER_APP#*/*/}.m3u"
15 |
16 | #Trap end of script run
17 | finish() {
18 | echo "bmitune.sh is exiting for $streamerIP with exit code $?"
19 | }
20 |
21 | trap finish EXIT
22 |
23 | updateReferenceFiles() {
24 |
25 | # Handle cases where stream_stopped or last_channel don't exist
26 | mkdir -p $streamerNoPort
27 | [[ -f "$streamerNoPort/stream_stopped" ]] || echo 0 > "$streamerNoPort/stream_stopped"
28 | [[ -f "$streamerNoPort/last_channel" ]] || echo 0 > "$streamerNoPort/last_channel"
29 |
30 | # Write PID for this script to bmitune_pid for use in stopbmitune.sh
31 | echo $$ > "$streamerNoPort/bmitune_pid"
32 | echo "Current PID for this script is $$"
33 | }
34 |
35 | #Set encoderURL based on the value of streamerIP
36 | matchEncoderURL() {
37 |
38 | case "$streamerIP" in
39 | "$TUNER1_IP")
40 | encoderURL=$ENCODER1_URL
41 | ;;
42 | "$TUNER2_IP")
43 | encoderURL=$ENCODER2_URL
44 | ;;
45 | "$TUNER3_IP")
46 | encoderURL=$ENCODER3_URL
47 | ;;
48 | "$TUNER4_IP")
49 | encoderURL=$ENCODER4_URL
50 | ;;
51 | *)
52 | exit 1
53 | ;;
54 | esac
55 | }
56 |
57 | #Check for active audio stream with maxDuration, preTuneAudioCheck, sleepBeforeAudioCheck and sleepAfterAudioCheck as arguments
58 | activeAudioCheck() {
59 | local startTime=$(date +%s)
60 | local maxDuration=$1
61 | local minimumLoudness=-50
62 | local sleepBeforeAudioCheck=$3
63 | local sleepAfterAudioCheck=$4
64 | local preTuneAudioCheck=$2
65 |
66 | while true; do
67 | sleep $sleepBeforeAudioCheck
68 | checkLoudness=$(ffmpeg -t 1 -i $encoderURL -filter:a ebur128 -map 0:a -f null -hide_banner - 2>&1 | awk '/I: /{print $2}')
69 |
70 | if (( $(date +%s) - $startTime > $maxDuration )); then
71 | echo "Active audio stream not detected in $maxDuration seconds."
72 | if [ $preTuneAudioCheck = "false" ]; then
73 | echo "Active audio stream not detected after tuning completed"
74 | case "$specialID" in
75 | "212")
76 | echo "Possible sports event blackout on NFL Network, so bumping channel up"
77 | $adbTarget shell input keyevent KEYCODE_DPAD_LEFT
78 | echo 0 > "$streamerNoPort/last_channel"
79 | exit 1
80 | ;;
81 | "213")
82 | echo "Possible sports event blackout on MLB Network, so bumping channel down"
83 | $adbTarget shell input keyevent KEYCODE_DPAD_RIGHT
84 | echo 0 > "$streamerNoPort/last_channel"
85 | exit 1
86 | ;;
87 | *)
88 | echo "Possible sports event blackout, so bumping channel down"
89 | $adbTarget shell input keyevent KEYCODE_DPAD_RIGHT
90 | echo 0 > "$streamerNoPort/last_channel"
91 | exit 1
92 | ;;
93 | esac
94 | else
95 | exit 1
96 | fi
97 | fi
98 |
99 | if (( $(echo "$checkLoudness > $minimumLoudness" | bc -l) )); then
100 | echo "Active audio stream detected with $checkLoudness LUF."
101 | break
102 | fi
103 |
104 | echo "Active audio stream not yet detected -- loudness is $checkLoudness LUF. Continuing..."
105 | sleep $sleepAfterAudioCheck
106 | done
107 | }
108 |
109 | #Special channels to kill DirecTV app or reboot FireStick
110 | specialChannels() {
111 |
112 | if [ $specialID = "exit" ]; then
113 | echo "Exit $packageName requested on $streamerIP"
114 | rm $streamerNoPort/last_channel $streamerNoPort/adbAppRunning
115 | $adbTarget shell am force-stop $packageName
116 | exit 0
117 | elif [ $specialID = "reboot" ]; then
118 | echo "Reboot $streamerIP requested"
119 | rm $streamerNoPort/last_channel $streamerNoPort/adbAppRunning
120 | $adbTarget reboot
121 | exit 0
122 | elif [[ -f $streamerNoPort/adbCommunicationFail ]]; then
123 | rm $streamerNoPort/adbCommunicationFail
124 | exit 1
125 | else
126 | echo "Not a special channel (exit nor reboot)"
127 | appFocus=$($adbTarget shell dumpsys window windows | grep -E 'mCurrentFocus' | cut -d '/' -f1 | sed 's/.* //g')
128 | echo "Current app in focus is $appFocus"
129 | fi
130 | }
131 |
132 | #Variable delay based on whether app was running or needed to be launched
133 | #and whether less than maxTime seconds (maxTime/3600 for hours) has passed while sleeping
134 | launchDelay() {
135 | local lastChannel
136 | local lastAwake
137 | local timeNow
138 | local timeElapsed
139 | local maxTime=14400
140 |
141 | lastChannel=$(<"$streamerNoPort/last_channel")
142 | lastAwake=$(<"$streamerNoPort/stream_stopped")
143 | timeNow=$(date +%s)
144 | timeElapsed=$(($timeNow - $lastAwake))
145 |
146 | if (( $lastChannel == $specialID )) && (( $timeElapsed < $maxTime )); then
147 | echo "Last channel selected on this tuner, no channel change required"
148 | exit 0
149 | elif [ -f $streamerNoPort/adbAppRunning ] && (( $timeElapsed < $maxTime )); then
150 | activeAudioCheck 42 true 0 1 # (maxDuration, preTuneAudioCheck, sleepBeforeAudioCheck, sleepAfterAudioCheck)
151 | #sleep 14
152 | rm $streamerNoPort/adbAppRunning
153 | echo $specialID > "$streamerNoPort/last_channel"
154 | else
155 | activeAudioCheck 42 true 0 1 # (maxDuration, preTuneAudioCheck, sleepBeforeAudioCheck, sleepAfterAudioCheck)
156 | #sleep 32
157 | echo $specialID > "$streamerNoPort/last_channel"
158 | fi
159 | }
160 |
161 | #Tuning is based on channel name values from $m3uName.
162 | tuneChannel() {
163 | channelName=$(awk -F, '/channel-id='"$channelID"'/ {print $2}' m3u/$m3uName)
164 | channelName=$(echo $channelName | sed 's/^/"/;s/$/"/')
165 |
166 | directvMenu="input keyevent KEYCODE_MENU; \
167 | input keyevent KEYCODE_MENU; \
168 | input keyevent KEYCODE_MENU; \
169 | input keyevent KEYCODE_MENU"
170 |
171 | directvSearch="input keyevent KEYCODE_DPAD_RIGHT; \
172 | input keyevent KEYCODE_DPAD_RIGHT; \
173 | input keyevent KEYCODE_DPAD_RIGHT; \
174 | input keyevent KEYCODE_DPAD_RIGHT; \
175 | input keyevent KEYCODE_DPAD_DOWN; sleep 3; \
176 | input keyevent KEYCODE_DPAD_CENTER; sleep 3"
177 |
178 | directvTune="input keyevent KEYCODE_MEDIA_PLAY_PAUSE; sleep 3; \
179 | input keyevent KEYCODE_DPAD_DOWN; \
180 | input keyevent KEYCODE_DPAD_DOWN; \
181 | input keyevent KEYCODE_DPAD_DOWN; \
182 | input keyevent KEYCODE_DPAD_LEFT; \
183 | input keyevent KEYCODE_DPAD_CENTER"
184 |
185 | $adbTarget shell $directvMenu
186 | $adbTarget shell $directvSearch
187 | $adbTarget shell input text $channelName
188 | $adbTarget shell $directvTune
189 | }
190 |
191 | main() {
192 | updateReferenceFiles
193 | matchEncoderURL
194 | specialChannels
195 | launchDelay
196 | tuneChannel
197 | activeAudioCheck 24 false 5 1 # (maxDuration, preTuneAudioCheck, sleepBeforeAudioCheck, sleepAfterAudioCheck)
198 | }
199 |
200 | main
201 |
--------------------------------------------------------------------------------
/scripts/firetv/dtvstream/bmitune.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #bmitune.sh for firetv/directv
3 |
4 | #Debug on if uncommented
5 | set -x
6 |
7 | #Global
8 | channelID=\""$1\""
9 | specialID="$1"
10 | streamerIP="$2"
11 | streamerNoPort="${streamerIP%%:*}"
12 | adbTarget="adb -s $streamerIP"
13 | packageName=com.att.tv
14 | m3uName="${STREAMER_APP#*/*/}.m3u"
15 |
16 | #Trap end of script run
17 | finish() {
18 | echo "bmitune.sh is exiting for $streamerIP with exit code $?"
19 | }
20 |
21 | trap finish EXIT
22 |
23 | updateReferenceFiles() {
24 |
25 | # Handle cases where stream_stopped or last_channel don't exist
26 | mkdir -p $streamerNoPort
27 | [[ -f "$streamerNoPort/stream_stopped" ]] || echo 0 > "$streamerNoPort/stream_stopped"
28 | [[ -f "$streamerNoPort/last_channel" ]] || echo 0 > "$streamerNoPort/last_channel"
29 |
30 | # Write PID for this script to bmitune_pid for use in stopbmitune.sh
31 | echo $$ > "$streamerNoPort/bmitune_pid"
32 | echo "Current PID for this script is $$"
33 | }
34 |
35 | #Set encoderURL based on the value of streamerIP
36 | matchEncoderURL() {
37 |
38 | case "$streamerIP" in
39 | "$TUNER1_IP")
40 | encoderURL=$ENCODER1_URL
41 | ;;
42 | "$TUNER2_IP")
43 | encoderURL=$ENCODER2_URL
44 | ;;
45 | "$TUNER3_IP")
46 | encoderURL=$ENCODER3_URL
47 | ;;
48 | "$TUNER4_IP")
49 | encoderURL=$ENCODER4_URL
50 | ;;
51 | *)
52 | exit 1
53 | ;;
54 | esac
55 | }
56 |
57 | #Check for active audio stream with maxDuration, preTuneAudioCheck, sleepBeforeAudioCheck and sleepAfterAudioCheck as arguments
58 | activeAudioCheck() {
59 | local startTime=$(date +%s)
60 | local maxDuration=$1
61 | local minimumLoudness=-50
62 | local sleepBeforeAudioCheck=$3
63 | local sleepAfterAudioCheck=$4
64 | local preTuneAudioCheck=$2
65 |
66 | while true; do
67 | sleep $sleepBeforeAudioCheck
68 | checkLoudness=$(ffmpeg -t 1 -i $encoderURL -filter:a ebur128 -map 0:a -f null -hide_banner - 2>&1 | awk '/I: /{print $2}')
69 |
70 | if (( $(date +%s) - $startTime > $maxDuration )); then
71 | echo "Active audio stream not detected in $maxDuration seconds."
72 | if [ $preTuneAudioCheck = "false" ]; then
73 | echo "Active audio stream not detected after tuning completed"
74 | case "$specialID" in
75 | "212")
76 | echo "Possible sports event blackout on NFL Network, so bumping channel up"
77 | $adbTarget shell input keyevent KEYCODE_DPAD_LEFT
78 | echo 0 > "$streamerNoPort/last_channel"
79 | exit 1
80 | ;;
81 | "213")
82 | echo "Possible sports event blackout on MLB Network, so bumping channel down"
83 | $adbTarget shell input keyevent KEYCODE_DPAD_RIGHT
84 | echo 0 > "$streamerNoPort/last_channel"
85 | exit 1
86 | ;;
87 | *)
88 | echo "Possible sports event blackout, so bumping channel down"
89 | $adbTarget shell input keyevent KEYCODE_DPAD_RIGHT
90 | echo 0 > "$streamerNoPort/last_channel"
91 | exit 1
92 | ;;
93 | esac
94 | else
95 | exit 1
96 | fi
97 | fi
98 |
99 | if (( $(echo "$checkLoudness > $minimumLoudness" | bc -l) )); then
100 | echo "Active audio stream detected with $checkLoudness LUF."
101 | break
102 | fi
103 |
104 | echo "Active audio stream not yet detected -- loudness is $checkLoudness LUF. Continuing..."
105 | sleep $sleepAfterAudioCheck
106 | done
107 | }
108 |
109 | #Special channels to kill DirecTV app or reboot FireStick
110 | specialChannels() {
111 |
112 | if [ $specialID = "exit" ]; then
113 | echo "Exit $packageName requested on $streamerIP"
114 | rm $streamerNoPort/last_channel $streamerNoPort/adbAppRunning
115 | $adbTarget shell am force-stop $packageName
116 | exit 0
117 | elif [ $specialID = "reboot" ]; then
118 | echo "Reboot $streamerIP requested"
119 | rm $streamerNoPort/last_channel $streamerNoPort/adbAppRunning
120 | $adbTarget reboot
121 | exit 0
122 | elif [[ -f $streamerNoPort/adbCommunicationFail ]]; then
123 | rm $streamerNoPort/adbCommunicationFail
124 | exit 1
125 | else
126 | echo "Not a special channel (exit nor reboot)"
127 | appFocus=$($adbTarget shell dumpsys window windows | grep -E 'mCurrentFocus' | cut -d '/' -f1 | sed 's/.* //g')
128 | echo "Current app in focus is $appFocus"
129 | fi
130 | }
131 |
132 | #Variable delay based on whether app was running or needed to be launched
133 | #and whether less than maxTime seconds (maxTime/3600 for hours) has passed while sleeping
134 | launchDelay() {
135 | local lastChannel
136 | local lastAwake
137 | local timeNow
138 | local timeElapsed
139 | local maxTime=14400
140 |
141 | lastChannel=$(<"$streamerNoPort/last_channel")
142 | lastAwake=$(<"$streamerNoPort/stream_stopped")
143 | timeNow=$(date +%s)
144 | timeElapsed=$(($timeNow - $lastAwake))
145 |
146 | if (( $lastChannel == $specialID )) && (( $timeElapsed < $maxTime )); then
147 | echo "Last channel selected on this tuner, no channel change required"
148 | exit 0
149 | elif [ -f $streamerNoPort/adbAppRunning ] && (( $timeElapsed < $maxTime )); then
150 | activeAudioCheck 42 true 0 1 # (maxDuration, preTuneAudioCheck, sleepBeforeAudioCheck, sleepAfterAudioCheck)
151 | #sleep 14
152 | rm $streamerNoPort/adbAppRunning
153 | echo $specialID > "$streamerNoPort/last_channel"
154 | else
155 | activeAudioCheck 42 true 0 1 # (maxDuration, preTuneAudioCheck, sleepBeforeAudioCheck, sleepAfterAudioCheck)
156 | #sleep 32
157 | echo $specialID > "$streamerNoPort/last_channel"
158 | fi
159 | }
160 |
161 | #Tuning is based on channel name values from $m3uName.
162 | tuneChannel() {
163 | channelName=$(awk -F, '/channel-id='"$channelID"'/ {print $2}' m3u/$m3uName)
164 | channelName=$(echo $channelName | sed 's/^/"/;s/$/"/')
165 | numberOfBackspaces=25
166 | clearSearchBackspaces=$(for ((i=0; i<$numberOfBackspaces; i++)); do echo -n " KEYCODE_MEDIA_REWIND"; done)
167 |
168 | directvMenu="input keyevent KEYCODE_MENU; sleep 6"
169 |
170 | directvSearch="input keyevent KEYCODE_DPAD_LEFT; \
171 | input keyevent KEYCODE_DPAD_UP; \
172 | input keyevent KEYCODE_DPAD_CENTER; sleep 1; \
173 | input keyevent KEYCODE_DPAD_CENTER; sleep 1"
174 |
175 | directvClearSearch="input keyevent$clearSearchBackspaces"
176 |
177 | directvTune="input keyevent KEYCODE_MEDIA_PLAY_PAUSE; sleep 1; \
178 | input keyevent KEYCODE_DPAD_DOWN; \
179 | input keyevent KEYCODE_DPAD_DOWN; \
180 | input keyevent KEYCODE_DPAD_DOWN; \
181 | input keyevent KEYCODE_DPAD_CENTER"
182 |
183 | $adbTarget shell $directvMenu
184 | $adbTarget shell $directvSearch
185 | $adbTarget shell $directvClearSearch
186 | $adbTarget shell input text "$channelName"
187 | $adbTarget shell $directvTune
188 | }
189 |
190 | main() {
191 | updateReferenceFiles
192 | matchEncoderURL
193 | specialChannels
194 | launchDelay
195 | tuneChannel
196 | activeAudioCheck 24 false 5 1 # (maxDuration, preTuneAudioCheck, sleepBeforeAudioCheck, sleepAfterAudioCheck)
197 | }
198 |
199 | main
200 |
--------------------------------------------------------------------------------
/html/editm3u.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Edit M3U File
5 |
6 |
7 |
57 |
58 |
59 |
Edit M3U File: {{.filename}}
60 |
61 |
62 |
63 |
Id
64 |
Station Id
65 |
Channel Number
66 |
Channel Name
67 |
Stream Location
68 |
Group
69 |
Logo
70 |
Action
71 |
Toggle
72 |
73 |
74 |
75 | {{range .entries}}
76 |
77 |
{{ .Id }}
78 |
{{ .StationId }}
79 |
{{ .ChannelNumber }}
80 |
{{ .ChannelName }}
81 |
{{ .StreamURL }}
82 |
{{ .Group }}
83 |
{{ .Logo }}
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | {{end}}
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
Total Channels: {{ len .entries }}
104 |
105 |
106 |
107 |
174 |
175 |
176 |
--------------------------------------------------------------------------------
/scripts/chromecast/pbs/README.txt:
--------------------------------------------------------------------------------
1 | Tunes live TV in the PBS app.
2 |
3 | * PBS STREAMING INFRASTRUCTURE
4 |
5 | Most PBS affiliates use the streaming platform operated by the
6 | national PBS umbrella organization, and that's what the PBS app
7 | is for. I think there are a few PBS affiliates that use some
8 | different infrastructure for streaming, and you won't be able
9 | to use the PBS app and these scripts for those affiliates.
10 |
11 | The scripts here are aimed at navigating to a PBS affiliate's
12 | live stream. They don't know anything about on-demand programming,
13 | even though it is available in the same PBS app.
14 |
15 | * SCRIPTS STRUCTURE AND CONFIGURATION
16 |
17 | Our caller, ah4c, expects there to exist 3 scripts: "prebmitune.sh",
18 | "bmitune.sh", and "stopbmitune.sh". For this set of scripts, there are
19 | several common definitions, functions, etc. Each of the expected
20 | scripts is a trivial 3-liner that sources "common.sh" and then calls
21 | the applicable function defined within "common.sh". The intent of that
22 | arrangement is to achive more consistent naming, simpler editing, and
23 | so on.
24 |
25 | At the top of "common.sh" is a collection of variables whose names
26 | start with "CONFIG_". As you might guess, those are things that
27 | conditionally control aspects of the scripts behaviors. If you are
28 | happy with the default values defined in "common.sh", then that's all
29 | you need to know. If you want to change any of them, you can, of
30 | course, just modify "common.sh". A better way is to create a file in
31 | this same directory called "config-local.sh" and provide modified
32 | values for just the items of interest. Copy/paste/modify is the most
33 | reliable way of doing that. The advantage of using "config-local.sh"
34 | is that your changes would not be overwritten by any updates to this
35 | set of scripts. If you don't want to modify any config values, it is
36 | not necessary to create "config-local.sh" at all.
37 |
38 | * TUNING AND STATION SELECTION
39 |
40 | The channel tuning info (the final part of the URLs in the m3u) can be
41 | one of two forms:
42 |
43 | "Waaa"
44 | or
45 | "Waaa_12345_2".
46 |
47 | In both cases, the tag "Waaa" (or whatever) is ignored. It's just
48 | documentation for you, our dear user. Station call sign or marketing
49 | names are good choices. For the second form, the separator character
50 | is underscore, so don't include any underscores in the tag. For URL
51 | reasons, don't include any slashes or spaces or other URL unsafe
52 | characters in any part of the channel tuning info.
53 |
54 | For the first form, live TV is selected with whatever local PBS
55 | channel you have last configured in the app. This is most useful for
56 | the majority of places which are only served by a single PBS affiliate
57 | (or if you only ever watch a single affiliate).
58 |
59 | The second form has a ZIP code used to populate the app's search box
60 | for stations. The last number is a one-based position in the results
61 | list. I'm hoping the results always come back in the same order, but I
62 | have no way to verify that. This is most useful for places served by 2
63 | or more PBS affiliates. There are many places with 2 and a few places
64 | with 3. I don't know if there are any places with more than 3.
65 |
66 | For example, in the Seattle and Tacoma, Washington, area, there are 2
67 | PBS affiliates. If you search for a Seattle area ZIP code, you get 2
68 | results back. So far, I've always seen them come back as KCTS (the
69 | Seattle affiliate) first and KBTC (the Tacoma affiliate) second. You
70 | select which one you want with either "_1" or "_2". You can see this
71 | in the sample M3U pbs-seatac.m3u. There's another example M3U,
72 | pbs-worcester.m3u, for a ZIP code in Worcester, Massachusetts. That
73 | ZIP code search offers a choice of 3 PBS affiliates.
74 |
75 | The tuning script remembers the last station it tuned, so it only goes
76 | through the station search dialog if it needs to change to a different
77 | PBS affiliate (or if a lot of time has passed).
78 |
79 | NOTE: Although the search for affiliates is based on the ZIP code
80 | entered in the search box, the PBS streaming platform does geographic
81 | restrictions based on the source IP address it sees. That's why you
82 | can't watch out-of-area PBS programming. The PBS app will let you
83 | search for any ZIP code and will display the results. Selecting an
84 | out-of-area PBS affiliate in the search results will give an error
85 | message. The code in these scripts doesn't have a way of knowing that
86 | and assumes the station selection went correctly. There's also a small
87 | chance that your ISP's location will be different from your location
88 | for the purposes of PBS restrctions. If you can select it manually in
89 | the PBS app, the scripts can select it.
90 |
91 | * DELAYS
92 |
93 | The scripts work mostly by doing what's called "remote emulation".
94 | That is, they use adb commands to simulate button presses on a remote
95 | control. When a human operates the remote, it's obvious when they can
96 | press more buttons. The scripts mostly don't know when a reaction has
97 | happened on the screen, so they resort to a bunch of fixed delays to
98 | wait until the right time for the next thing. Those delays are a bit
99 | fiddly and depend on your specific equipment, what your equipment is
100 | doing at the moment, and, in some cases, the response time to network
101 | requests over the internet. Phase of the moon might be in there
102 | somewhere.
103 |
104 | Delays are expressed in seconds and need not be limited to whole
105 | numbers. Decimal fraction amounts are acceptable, e.g., "1.345".
106 |
107 | Instead of calling sleep directly, the scripts use the "settle"
108 | function (defined in "common.sh") which scales how much they
109 | sleep. When called, that function alters the requested sleep duration
110 | according to CONFIG_DELAY_SCALING and CONFIG_DELAY_OFFSET. If the
111 | scaling is 1 and the offset is 0 (the defaults), you get exactly the
112 | sleep durations coded into the scripts. If you need proportionally
113 | longer delays, increase the scaling. If you want proportionally
114 | shorter delays, decrease the scaling. If you want to add (or
115 | subtract) a fixed amount to each delay, set the offset to that amount.
116 | If you configure the scaling and offset values to 0, sleep becomes a
117 | no-op (which probably will not be a good choice).
118 |
119 | There are CONFIG_ items for each individual delay. Instead of scaling
120 | or offsetting them across the board, you can adjust them individually.
121 |
122 | The advantage of long delays is that you can be sure your device has
123 | finished reacting to inputs before the script moves on to the next
124 | thing.
125 |
126 | The disadvantage of having overly long delays is that you risk losing
127 | the beginning of real programming. For example, if you are recording a
128 | 30 minute program and you have 1 minute worth of delays, then the
129 | first minute of the recording will be showing this tuning noise
130 | instead of the first minute of the real program. For PBS shows, that
131 | usually doesn't matter since they tend to thank sponsors at the
132 | beginning and sometimes show a preview of the show. However, it's best
133 | to not rely on that and have the delays as short as you can and still
134 | have them tune reliably. Timing is trickiest if you have multiple PBS
135 | affiliates and need to change your local station before recording.
136 |
137 | In my environment, with the default delays, it takes about 30 seconds
138 | to tune when a station change is required. It takes about 12 seconds
139 | when a channel change is not required. Tested on Google Chromecast HD,
140 | Tivo Stream 4k, and Onn 2k Stick. I also tested against a fairly old
141 | Amazon Fire TV stick, though I had to scale the delays a bit for that
142 | to work.
143 |
--------------------------------------------------------------------------------