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

You are correct sir, and thanks for the reply.
I'm just trying to figure if there is a way, on this Directv box, do Ch Up/Down for only the Favorites "Collection" since this thread has some Directv box experts perhaps.
.

I'm on latest created 6/10/26 on Synology NAS- Deeplinks with Osprey's. I haven't seen this issue.

2026.06.29.0212

I would think if cdvr was doing it, it would show up in the support logs.

I think they only allow 2 streams via browser
edit; I just tested, had a stream running in firefox, chrome, and edge. I was thinking of this

so with ch4c it's possible the multiple sandboxed chromes are causing a hickup

1 Like

Thank you @boukmandutty and @bnhf. Nine months later and I'm finally getting around to setting up AH4C PyATV and a Magewell card "made into" an encoder using GitHub - jpoet/Magewell2TS: Reads audio and video from a Magewell PRO capture card and muxes them into a Transport Stream. · GitHub and GitHub - mackandall/BroadCastHub: Broadcast Hub is a self-hosted web application for managing video capture hardware · GitHub.
Nice explanation here: BroadcastHub For Magewell and Decklink.

The more I think of it, I should be able to run AH4C Apple TV version in a docker container and run everything else to use the Magewell card as an encoder as native. Also, I don't think I need to pass my Intel Quick Sync driver through to the container either then. Am I thinking about this correctly? As long as I'm not using ffmpeg commands within the .env files of AH4C, and instead letting Magewell2TS and BroadCastHub work together to do the encoding, this should work to have just AH4C PyATV in the container?

Sounds about right. The Broadcast Hub essentially operates the way a link pi box would. So for example one of my inputs from my magewell card broadcasts over the network as

http://192.168.1.188:6502/stream/0-1

This means that any program can see what is being streamed. The channels changing control goes to either adbtuner or ah4c. This means that the docker would work as easily as running it in non-docker mode.

1 Like

Great, thank you! Time to roll up sleeves and try it out. I'm going to start with a new box and load all from scratch as my original CDVR Ubuntu is pretty out of date. I know @boukmandutty tested Broadcast Hub with Ubuntu, but I'm thinking of trying it with latest Debian instead. Since I'm setting this up for Spectrum and using Apple TVs as my streamers, I'll be following some good input I found in the following thread: Advice Needed - ADBTuner or AH4C?. Just thought I would link that here for anyone trying to do the same. So, just for clarity, here is what I'll be setting up. I'm not completely literate with all these steps but will be reading and trying it out. I think I have a general direction now:

  • will leave existing CDVR on existing box (eventually may also move it over to repurpose existing box)
  • new Intel box (I think I have a 9th or 10th gen Xeon) to be used for AH4C:AppleTV / Magewell cards using Magewell2TS and Broadcast Hub
  • install Debian 13.5 or Ubuntu (may experiment with Debian first)
  • ensure driver is loaded for Intel Quick Sync (no need to pass through to docker) to handle video encoding
  • install docker compose
  • use one-click from @bnhf for portainer / olive tin
  • install AppleTV version of AH4C (running as docker)
  • install 2 Magewell Pro Capture dual HDMI cards (fanless) to have a total of 4 streams at once (will probably never use that many at once)
  • install Magewell2TS (native, no docker)
  • Install Broadcast Hub (native, no docker)
  • configure everything and cross fingers
    Thank you all!

You can already do that (more-or-less) using "CMDx" and ffmpeg. There have been posts about it in the past, and I've used it myself. You're basically just letting ffmpeg deliver a black screen for x seconds to absorb whatever part of the typical tuning process you'd prefer to not see.

If a fixed delay isn't to your liking, you can also use ffmpeg to test for active video. I've written functions to do that in the past for active audio, using ffmpeg -- and I'm fairly sure you can do that with active video as well.

The parameters might be a bit trickier, but I believe you could differentiate between menu navigation and fullscreen video using ffmpeg. If you can do it this way, using a "CMDx" tee, anyone using any set of scripts could take advantage of it.

Fire up your bots and set them to the task! :slight_smile:

1 Like

where did you land with the audio check? I like the idea of audio check since that will catch a froze tune or other lockup. i've found that most of the time when I have a failed tune, a new attempt from ah4c (2nd tune) usually works.

1 Like

Once you have the ffmpeg command figured out, a bot can update your Docker Compose accordingly with the appropriate CMDx variants -- which should make it pretty simple.

so, wouldn't it make sense to use the audio to trigger that the channel has tuned so you don't see the channel changing splash screen

The function looks like this:

#Check for active audio stream with maxDuration, preTuneAudioCheck, sleepBeforeAudioCheck and sleepAfterAudioCheck as arguments
activeAudioCheck() {
  local startTime=$(date +%s)
  local maxDuration=$1
  local minimumLoudness=-50
  local sleepBeforeAudioCheck=$3
  local sleepAfterAudioCheck=$4
  local preTuneAudioCheck=$2
  
  while true; do
    sleep $sleepBeforeAudioCheck
    checkLoudness=$(ffmpeg -t 1 -i $encoderURL -filter:a ebur128 -map 0:a -f null -hide_banner - 2>&1 | awk '/I:        /{print $2}')

    if (( $(date +%s) - $startTime > $maxDuration )); then
      echo "Active audio stream not detected in $maxDuration seconds."
      if [ $preTuneAudioCheck = "false" ]; then
        echo "Active audio stream not detected after tuning completed"
        case "$specialID" in
          "212")
            echo "Possible sports event blackout on NFL Network, so bumping channel up"
            $adbTarget shell input keyevent KEYCODE_DPAD_LEFT
            echo 0 > "$streamerNoPort/last_channel"
            exit 1
            ;; 
          "213")
            echo "Possible sports event blackout on MLB Network, so bumping channel down"
            $adbTarget shell input keyevent KEYCODE_DPAD_RIGHT
            echo 0 > "$streamerNoPort/last_channel"
            exit 1
            ;; 
          *)
            echo "Possible sports event blackout, so bumping channel down"
            $adbTarget shell input keyevent KEYCODE_DPAD_RIGHT
            echo 0 > "$streamerNoPort/last_channel"
            exit 1
            ;;
        esac
      else
        exit 1
      fi
    fi

    if (( $(echo "$checkLoudness > $minimumLoudness" | bc -l) )); then
      echo "Active audio stream detected with $checkLoudness LUF."
      break
    fi

    echo "Active audio stream not yet detected -- loudness is $checkLoudness LUF. Continuing..."
    sleep $sleepAfterAudioCheck
  done
}

You can look at bmitune.sh in scripts/firetv/directv to see it in-full.

EDIT: That was pretty crucial back a few years ago, when the only option for DTV app tuning was remote control emulation.

can't you use it also for the aforementioned gate

on my boxes changing the channel brings up a splash screen for the new channel, and cuts out the audio. usually about 2 seconds. might be enough window to re-detect audio.

ie, wake, wait for audio, tune, wait 1 sec, wait for audio, send stream.

My 2 cents:

When I tune to an Osprey channel using deeplinks, I will see the channel splash screen just prior to the channel going live. Prior to deeplinks, I would see the channel numbers. I really like that behavior because it gives me visual proof that everything is working as expected. I have had instances where the box has missed the adb authorization and it just goes back to last viewed channel. The adb authorization screen never shows on the tv, only on the link pi for 1-2 seconds. Retuning to another channel has always fixed it. When it occurs on 1, the others will follow suit so I know to fix them.

ah4c (appletv tag): tuner held permanently when encoder input goes silent — io.Copy blocked in Read with no deadline

In a nutshell, I'd periodically find both of my ah4c tuners showing in use when they are not being used.

I've been working through this one with Claude for a little bit, and this is the summary of what it's helped me find:

On bnhf/ah4c:appletv using the Spectrum scripts, a tuner can be marked Active forever with no error logged. It is only recoverable by restarting the container.

The trigger is reproducible on demand: put the streaming Apple TV to sleep while a stream is in progress. The HDMI encoder stops emitting bytes but holds the TCP connection open. ah4c blocks in net.(*conn).Read with no deadline and never returns, so Close() and the stop script never run and the tuner is never released.

I've got a much longer doc outlining what was done and the logs captured showing everything, which I threw into a google doc to avoid a long post here.

Can you post the Spectrum scripts you're using please, along with the M3U file?

This is what I've got:

{
echo "### `prebmitune.sh`"
echo 'bash' cat scripts/atv/spectrum/prebmitune.sh echo ''
echo
echo "### `bmitune.sh`"
echo 'bash' cat scripts/atv/spectrum/bmitune.sh echo ''
echo
echo "### `stopbmitune.sh`"
echo 'bash' cat scripts/atv/spectrum/stopbmitune.sh echo ''
echo
echo "### `spectrum.m3u`"
echo '' cat m3u/spectrum.m3u echo ''
} > /home/peter/ah4c_watchdog/scripts-and-m3u.md
cat /home/peter/ah4c_watchdog/scripts-and-m3u.md

prebmitune.sh

#!/bin/bash
streamerIP="$1"

# 1. Wake the Apple TV
/usr/local/bin/atvremote --storage-filename /root/.android/.pyatv.conf -s $streamerIP home
sleep 2

# 2. Gracefully open the Spectrum app to its default startup screen
/usr/local/bin/atvremote --storage-filename /root/.android/.pyatv.conf -s $streamerIP launch_app=spectrumTV://

# 3. Give the app 7 seconds to load your profile and system preferences
sleep 7

bmitune.sh

#!/bin/bash
channelID="$1"
streamerIP="$2"

/usr/local/bin/atvremote --storage-filename /root/.android/.pyatv.conf -s $streamerIP launch_app=spectrumTV://watch.spectrum.net/livetv/$channelID

stopbmitune.sh

#!/bin/bash
streamerIP="$1"

# 1. Exit the stream and drop to the home screen
/usr/local/bin/atvremote --storage-filename /root/.android/.pyatv.conf -s $streamerIP home
sleep 2

# 2. Open the App Switcher
/usr/local/bin/atvremote --storage-filename /root/.android/.pyatv.conf -s $streamerIP home home
sleep 3

# 3. Navigate left to focus on the Spectrum app
/usr/local/bin/atvremote --storage-filename /root/.android/.pyatv.conf -s $streamerIP left
sleep 2

# 4. Perform the "Double-Click UP" force-quit maneuver
# We send two 'up' commands in immediate succession
/usr/local/bin/atvremote --storage-filename /root/.android/.pyatv.conf -s $streamerIP up up
sleep 2

# 5. Return to Home Screen
/usr/local/bin/atvremote --storage-filename /root/.android/.pyatv.conf -s $streamerIP home

spectrum.m3u

#EXTM3U

#EXTINF:-1 channel-id="31507" channel-number="" tvc-guide-stationid="31507" tvg-group="" tvg-logo="",WGRZ
http://{{ .IPADDRESS }}/play/tuner/31507

#EXTINF:-1 channel-id="24052" channel-number="" tvc-guide-stationid="24052" tvg-group="" tvg-logo="",WKBW
http://{{ .IPADDRESS }}/play/tuner/24052

#EXTINF:-1 channel-id="24051" channel-number="" tvc-guide-stationid="24051" tvg-group="" tvg-logo="",WIVB
http://{{ .IPADDRESS }}/play/tuner/24051

#EXTINF:-1 channel-id="10091" channel-number="" tvc-guide-stationid="10091" tvg-group="" tvg-logo="",CBLT
http://{{ .IPADDRESS }}/play/tuner/10091

#EXTINF:-1 channel-id="65437" channel-number="" tvc-guide-stationid="65437" tvg-group="" tvg-logo="",MSG Sports Buffalo
http://{{ .IPADDRESS }}/play/tuner/65437

#EXTINF:-1 channel-id="43432" channel-number="" tvc-guide-stationid="43432" tvg-group="" tvg-logo="",Rewind TV
http://{{ .IPADDRESS }}/play/tuner/43432

#EXTINF:-1 channel-id="123810" channel-number="" tvc-guide-stationid="123810" tvg-group="" tvg-logo="",Bay News 9 Tampa
http://{{ .IPADDRESS }}/play/tuner/123810

#EXTINF:-1 channel-id="68904" channel-number="" tvc-guide-stationid="68904" tvg-group="" tvg-logo="",Spectrum News 1 Buffalo
http://{{ .IPADDRESS }}/play/tuner/68904

#EXTINF:-1 channel-id="63992" channel-number="" tvc-guide-stationid="63992" tvg-group="" tvg-logo="",WBBZ MeTv Buffalo
http://{{ .IPADDRESS }}/play/tuner/63992

#EXTINF:-1 channel-id="123830" channel-number="" tvc-guide-stationid="123830" tvg-group="" tvg-logo="",Spectrum News 13 Central Florida
http://{{ .IPADDRESS }}/play/tuner/123830

#EXTINF:-1 channel-id="34915" channel-number="" tvc-guide-stationid="34915" tvg-group="" tvg-logo="",WNED 
http://{{ .IPADDRESS }}/play/tuner/34915

#EXTINF:-1 channel-id="43430" channel-number="" tvc-guide-stationid="43430" tvg-group="" tvg-logo="",WNLO
http://{{ .IPADDRESS }}/play/tuner/43430

#EXTINF:-1 channel-id="44668" channel-number="" tvc-guide-stationid="44668" tvg-group="" tvg-logo="",WNYO
http://{{ .IPADDRESS }}/play/tuner/44668

#EXTINF:-1 channel-id="42945" channel-number="" tvc-guide-stationid="42945" tvg-group="" tvg-logo="",WUTV
http://{{ .IPADDRESS }}/play/tuner/42945

#EXTINF:-1 channel-id="47796" channel-number="" tvc-guide-stationid="47796" tvg-group="" tvg-logo="",Antenna TV
http://{{ .IPADDRESS }}/play/tuner/47796

These channels are coming in over a VPN so I adjusted the stop script to force quit the spectrum app to minimize vpn traffic when we aren't streaming these channels. I also added the bit where it opens the Spectrum app and waits a few seconds for it to load before tuning to a channel, without this the app sometimes would launch with subtitles turned on, even though all settings had them disabled.