AndroidHDMI for Channels (ah4c): A virtual channel tuner using HDMI Encoder(s) + streaming stick(s)

Also here's the complete log from an attempt to record a channel. It looks to me like it times out and shuts down. I didn't stop it in Channels DVR even when I saw it was recording only pre-roll:

2026/09/27 18:13:23 [PREROLL] tuner= channel=WPBAD-8037961156532819105 answered at once with the pre-roll
2026/09/27 18:13:23 Attempting network tune for device http://192.168.99.15:8090/stream0 192.168.99.35:5555 WPBAD-8037961156532819105
2026/09/27 18:13:23 [EXECUTE] Running [./scripts/firetv/xfinity/prebmitune.sh 192.168.99.35:5555 WPBAD-8037961156532819105]
2026/09/27 18:13:24 [EXECUTE] Stdout: 'already connected to 192.168.99.35:5555
prebmitune.sh is exiting for 192.168.99.35:5555 with exit code 0
'
2026/09/27 18:13:24 [EXECUTE] Stderr: '+ streamerIP=192.168.99.35:5555
+ streamerNoPort=192.168.99.35
+ adbTarget='adb -s 192.168.99.35:5555'
+ mkdir -p 192.168.99.35
+ trap finish EXIT
+ main
+ adbConnect
+ adb connect 192.168.99.35:5555
+ local -i adbMaxRetries=2
+ local -i adbCounter=0
+ true
+ adb -s 192.168.99.35:5555 shell input keyevent KEYCODE_WAKEUP
+ local adbEventSuccess=0
+ [[ 0 -eq 0 ]]
+ break
+ finish
+ echo 'prebmitune.sh is exiting for 192.168.99.35:5555 with exit code 0'
'
2026/09/27 18:13:24 [EXECUTE] Finished running ./scripts/firetv/xfinity/prebmitune.sh in 1.125366128s
2026/09/27 18:13:24 [HOLD] tuner=192.168.99.35:5555 hold until the app is seen playing, showing the pre-roll
2026/09/27 18:13:24 [PREROLL] tuner= channel=WPBAD-8037961156532819105 stream ready in 1.237s; pre-roll carries on under the hold
2026/09/27 18:13:24 [EXECUTE] Running [./scripts/firetv/xfinity/bmitune.sh WPBAD-8037961156532819105 192.168.99.35:5555]
2026/09/27 18:15:43 [IO] io.Copy: write tcp 172.23.0.2:7654->192.168.99.61:50162: write: broken pipe
2026/09/27 18:15:43 [IOINFO] Successfully copied 48390260 bytes
2026/09/27 18:15:43 [IOINFO] Transfer speed: 2.748693631911477 Mbits/second
2026/09/27 18:15:43 Performing Close() for 192.168.99.35:5555
2026/09/27 18:15:43 [EXECUTE] Running [./scripts/firetv/xfinity/stopbmitune.sh 192.168.99.35:5555 WPBAD-8037961156532819105]
2026/09/27 18:15:43 [EXECUTE] Stdout: 'Current PID for this script is 189
Not a special channel (exit nor reboot)
Starting: Intent { act=android.intent.action.VIEW dat=https://www.xfinity.com/... cmp=com.xfinity.cloudtvr.tenfoot/com.xfinity.common.view.LaunchActivity }
bmitune.sh is exiting for 192.168.99.35:5555 with exit code 0
'
2026/09/27 18:15:43 [EXECUTE] Stderr: '+ dvr=192.168.99.61:8089
+ channelNameID=WPBAD-8037961156532819105
++ echo WPBAD-8037961156532819105
++ awk -F- '{print $2}'
+ channelID=8037961156532819105
++ echo WPBAD-8037961156532819105
++ awk -F- '{print $1}'
+ channelName=WPBAD
+ specialID=WPBAD
+ streamerIP=192.168.99.35:5555
+ streamerNoPort=192.168.99.35
+ adbTarget='adb -s 192.168.99.35:5555'
+ packageName=com.xfinity.cloudtvr.tenfoot
+ packageAction=com.xfinity.common.view.LaunchActivity
+ m3uName=xfinity.m3u
++ grep -B1 /play/tuner/WPBAD-8037961156532819105 /opt/m3u/xfinity.m3u
++ awk -F 'channel-id="' 'NF>1 {split($2, a, "\""); print a[1]}'
+ m3uChannelID=816
++ curl -s http://192.168.99.61:8089/api/v1/channels
++ jq -r '.[] | select(.id == "816") | .number'
+ channelNumber=816
+ [[ true == '' ]]
+ speedMode=true
+ read -a autoCropChannels
+ trap finish EXIT
+ main
+ updateReferenceFiles
+ mkdir -p 192.168.99.35
+ [[ -f 192.168.99.35/stream_stopped ]]
+ echo 0
+ [[ -f 192.168.99.35/last_channel ]]
+ echo 0
+ echo 189
+ echo 'Current PID for this script is 189'
+ matchEncoderURL
+ case "$streamerIP" in
+ encoderURL=http://192.168.99.15:8090/stream0
+ encoderStreamNumber=stream0
+ encoderIP=192.168.99.15:8090/stream0
+ encoderIP=192.168.99.15
+ specialChannels
+ '[' WPBAD = exit ']'
+ '[' WPBAD = reboot ']'
+ [[ -f 192.168.99.35/adbCommunicationFail ]]
+ echo 'Not a special channel (exit nor reboot)'
+ tuneChannel
+ adb -s 192.168.99.35:5555 shell am start -n com.xfinity.cloudtvr.tenfoot/com.xfinity.common.view.LaunchActivity https://www.xfinity.com/stream/live/WPBAD/8037961156532819105/WPBAD
+ echo -e '#!/bin/bash\n\nwhile true; do sleep 235m; adb -s 192.168.99.35:5555 shell input keyevent KEYCODE_DPAD_DOWN; done'
+ chmod +x ./192.168.99.35/keep_watching.sh
+ [[ -n 235m ]]
+ nohup ./192.168.99.35/keep_watching.sh
+ printf '%s\n'
+ grep -qx 816
+ :
+ finish
+ echo 'bmitune.sh is exiting for 192.168.99.35:5555 with exit code 0'
Terminated
'
2026/09/27 18:15:43 [EXECUTE] Finished running ./scripts/firetv/xfinity/bmitune.sh in 2m19.617921793s
2026/09/27 18:15:44 [PLAYBACK] start on a moving keyframe after 2m20.261s
2026/09/27 18:15:49 [EXECUTE] Stdout: 'Streaming stopped for 192.168.99.35:5555
Sleep initiated for 192.168.99.35:5555
192.168.99.35/stream_stopped written with epoch stop time
'
2026/09/27 18:15:49 [EXECUTE] Stderr: '+ dvr=192.168.99.61:8089
+ streamerIP=192.168.99.35:5555
+ streamerNoPort=192.168.99.35
+ channelNameID=WPBAD-8037961156532819105
+ adbTarget='adb -s 192.168.99.35:5555'
+ packageName=com.xfinity.cloudtvr.tenfoot
+ m3uName=xfinity.m3u
++ awk -F 'channel-id="' 'NF>1 {split($2, a, "\""); print a[1]}'
++ grep -B1 /play/tuner/WPBAD-8037961156532819105 /opt/m3u/xfinity.m3u
+ m3uChannelID=816
++ curl -s http://192.168.99.61:8089/api/v1/channels
++ jq -r '.[] | select(.id == "816") | .number'
+ channelNumber=816
+ [[ true == '' ]]
+ speedMode=true
+ read -a autoCropChannels
+ printf '%s\n'
+ grep -qx 816
+ main
+ [[ -n croppedChannel ]]
+ matchEncoderURL
+ case "$streamerIP" in
+ encoderURL=http://192.168.99.15:8090/stream0
+ encoderStreamNumber=stream0
+ encoderIP=192.168.99.15:8090/stream0
+ encoderIP=192.168.99.15
+ bmituneDone
+ bmitunePID=189
++ pgrep -f 192.168.99.35/keep_watching.sh
+ keepWatchingPID=209
++ pgrep -P 209
+ keepWatchingCPID=210
+ [[ -n '' ]]
+ ps -p 189
+ [[ -n 235m ]]
+ kill 209 210
+ rm ./192.168.99.35/keep_watching.sh
+ [[ -n '' ]]
+ rm ./192.168.99.15/stream0.sh
rm: cannot remove './192.168.99.15/stream0.sh': No such file or directory
+ adbStop
+ [[ true == \t\r\u\e ]]
+ stop='input keyevent KEYCODE_BACK; input keyevent KEYCODE_HOME'
+ adb -s 192.168.99.35:5555 shell input keyevent 'KEYCODE_BACK;' input keyevent KEYCODE_HOME
+ sleep 2
+ echo 'Streaming stopped for 192.168.99.35:5555'
+ adbSleep
+ sleep='input keyevent KEYCODE_SLEEP'
+ adb -s 192.168.99.35:5555 shell input keyevent KEYCODE_SLEEP
+ echo 'Sleep initiated for 192.168.99.35:5555'
+ date +%s
+ echo '192.168.99.35/stream_stopped written with epoch stop time'
'
2026/09/27 18:15:49 [EXECUTE] Finished running ./scripts/firetv/xfinity/stopbmitune.sh in 5.857724056s
2026/09/27 18:15:49 [GIN-debug] Request: 192.168.99.61 GET /play/tuner/WPBAD-8037961156532819105, latency: 2m26.696604735s, status: 200

That firmware version I know to not be problematic.
So that doesn't look like it. The issue would be if you're encoding H.265 and you've got H.264 set.

This is what I have selected in the Link Pi, and h264 is in the .env if you didn't see that:

1 Like

Would you mind trying the new version and seeing if it helps, along with updating your compose file?
Not that it would help anything script-wise, but I'm wondering if I fixed a bug potentially that I'm just not remembering.

I know in between those two versions I did fix a bug with pre-roll.

Maybe set PLAYBACK_DETECTION=false for now, so you can see what's happening during the tuning process.

That did the trick, thanks! Now since it pulled the new version anyway, I need to update the compose as well.

For Xfinity, since deeplink tuning is slow, I would set PLAYBACK_DELAY=16. Then, add a pre-roll like one of the static images I posted a bit back in thread -- or add a video like "Snack Canyon" or similar.

You can use one of these. Just put it in the preroll directory:

2 Likes

Thanks, I was going to ask about the delay next since I timed mine at about 24 seconds. Here's the pre-roll I made:

1 Like

I think my recordings are working well enough now. I've put in some hours determining the correct channel numbers and guide data for my lineup. I have three main issues:

  1. Duplicate channels - Xfinity had up to 3 identical HD channels for some channels. ah4c couldn't handle that because it would request all at once and fail to tune. [RESOLVED]

  2. Channels with no URL - these channels work on the STB and can stream from the web page, but they don't change the browser URL when played: NEST, TVK, INTCH, FPTN, ATL29, ATLB, GETTV, GRIO. I've browsed these channels, and I'm most likely to want to watch the ATLB (Atlanta Braves) network, but even that's a low probability. I've run through 3 different AIs trying to capture the stream data from the browser console, and still have no data.[LOW PRIORITY]

  3. Incorrect Gracenote IDs. Using AI to help match resulted in a number of fabricated IDs with incorrect guide data. For now I'm scrolling through the Channels guide and making sure my top-used networks are correct. If I find an error, I start with http://[ChannelsDVR]:8089/tms/stations/[callsign] and also check TV Listings- Find Local TV Listings and Watch Full Episodes (the ID is in the logo url). I've tried having AI help me compare lists since manually scrolling through 205 channels can be tedious and prone to errors, but I keep getting mismatches or outright hallucinations. There's probably a better way, but eventually I'll get through the 205 stations. [IN PROGRESS]

If anyone would like me to share the Xfinity m3u, I can paste it here or upload it to github.

All this plus the 24-second tune time is making me consider dropping Xfinity and moving to DTV Stream. I have to teach my mom to use Channels as her TV watching app before I make any changes that get rid of her STB.

I hear you there. It's one thing if you need to keep Xfinity for one reason or another, but it comes with compromises. The FastChannels ah4c bridge for DTV Stream is definitely king at the moment, in terms of tuning times, price options and general usability. Your subscribed set of DTV channels is actually something we can capture (including Gracenote data), so you wouldn't have to repeat the Easter egg hunt for tuning and station IDs.

1 Like

I wish I could get Comcast/Xfinity to work with the FC Bridge, but they block me at every turn. Not looking promising.

We definitely appreciate the effort!

Since changing settings in my .env and re-deploying already upgraded me to v2026.09.16.1501, I want to upgrade the compose as well. I don't see any settings in the UI at present. Are they exposed only when they don't exist in the .env, or should I be looking elsewhere? I wanted to check it out before resetting my compose.yaml and .env to this version.

The current build is v2026.09.25.1259. Are you using the Re-pull & redeploy slider in the Portainer-Stacks Editor to pull the latest image?

Ok, so I'm not on the latest version. I saw a date newer than my compose file and made an incorrect assumption. I'm in the middle of a recording now but will update to the new version after it's done. FWIW I manage with Dockge which is essentially just a frontend to docker compose commands.

As an aside, this reminds me a lot of the early days of MythTV. Smart people like yourselves programming their way through issues while untrained but persistent users like me keep testing and asking for help, rinse and repeat. I think we'll get there eventually and Channels DVR can fade back into the "just works" category :slight_smile:

I would suggest setting PYATV=true, even though you're not using pyatv, as that should skip some/most adb commands.

Other than that, as I understand the Osprey/Gemini, there's no app to launch. So, as long as you keep your devices awake, I don't see why you couldn't write a bmitune.sh script that would send a curl command based the $1 (channelNumber) and $2 (streamerIP) values passed to the script.

prebmitune.sh and stopbmitune.sh need to exist, but can be "no-op" scripts.

I believe a posted a sample M3U file for you earlier that would have your channel numbers in it, which would pass the channelNumber value.

You should plan on finding an AI agent you like to help you with this. Also, as @mackid1993 said you'd really be best off using ADB -- and all evidence we know of suggests the problem you're having with your Geminis is something you should be able to resolve.

So our #1 recommendation is to get adb enabled, however this alternative option should work (though you'll be ah4c feature limited). It's also worth noting that you'll be an outlier in terms of how everyone else uses this (and other similar) tools, which of course, makes it more challenging for others to help out.

(post deleted by author)

Nevermind, part 2 I got ahead of myself -- the container didn't update when I changed the compose. I thought I ran a pull but forgot :grimacing:

New bnhf/ah4c:latest (aka bnhf/ah4c:2026.09.28) pushed this afternoon with support for the container's hostname to be used in the topline menu of the WebUI, and in browser tabs. This means, if you have multiple containers, that hostname will help distinguish between the two.

Typical hostnames are ah4c when you have a single container, and ah4c2 for a second. This is also the way multiple containers are handled when deploying them using Project One-Click. The value of AH4C_CONTAINER in Project One-Click is appended to ah4c.

2 Likes

This looks great!!

1 Like