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

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.

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

My recordings have been going great the first two days. Closed captions are the final piece in the WAF puzzle. I'm finding 4 seconds behind with Cohere is significantly different from the broadcast channels with embedded captions. I'm lowering it to 2 seconds to see if that's better.

I've been having good success with Purfview Faster Whisper XXL model largev2 using Subtitle Edit on Windows. My Channels VM doesn't have access to the iGPU, but my media VM (where ah4c sits) does. I'm thinking of installing something there to generate captions after recording. My two biggest issues are what to install and how to run it automatically after Channels commercial detection is complete. We almost never watch live TV, and captioning after the fact would be much more synchronized. I know this is going outside the subject of the thread and can take it elsewhere.

I feel like every few threads it's important to thank the developers for writing this and making it so easy to implement. I like to say "it's just TV," but when TV dies a lot of voices are suddenly clamoring for the TV they lost. It's nice to be able to bring it back.

You should probably take a look at this project:

Last I looked at it, it seemed a bit challenging to get started, but that may have changed.

Hi @tanderson92 can you take a look at GitHub - handy-computer/transcribe.cpp: ggml speech-to-text inference for 16+ model families · GitHub
It might be time to bump and pin and maybe add another model. If you can find one that works well enough for you let me know and I can look into adding it to the model picker. LLM's evolve so quickly I had to stay on this with the captions.

Will do. I'm recording one with a 2-second delay right now. If that passes the wife test I won't need to look any further.

2 Likes

If you have the HW for it have you tried turning on realtime? Several models support it. Just uses more memory and GPU for processing. It's usually fine if you don't have 10 tuners like yours truly.

1 Like

oh @tanderson92 unless you have CUDA backing and a nice GPU I'd avoid Cohere. It's a great model but not great for closed captions unless you really have fast graphics to run it.

I have an iGPU and NVIDIA card on my Proxmox server. The NVIDIA card is passed to one of my VMs and the iGPU to my media VM because it was recommended for Plex/Jellyfin transcoding. I have ah4c on the media (iGPU) VM right now. According to ah4c it ran Cohere 4 second delay at 4x or higher; it's currently running Cohere 2 second delay at 2.9x. I'll try Parakeet real time and Nemotron real time to see if those are any better if Cohere 2 seconds doesn't work for us.

How much memory do you have.

Proxmox is a subject that's very near-and-dear to my heart. I run almost nothing bare metal anymore, everything is virtualized on Proxmox, and it works incredibly well for me. There's really nothing better for the tinkerer. :slight_smile:

One suggestion I would make. VMs are great for something like Windows, but LXCs are the answer for just about everything else. It keeps virtualizations lightweight, and allows for the sharing of resources like an iGPU across those virtualizations.

In fact, Channels DVR itself makes an excellent LXC:

Then you can run Docker in another LXC, with ah4c and other projects. Moving LXCs like this from one Proxmox host to another is super easy with a Proxmox Backup Server, to balance loads or to take a host down for an upgrade or other maintenance.

64GB

I'm still new at Proxmox and built my system based on the best recommendations I could find during my preparations. I have a dedicated Channels VM and a separate Media VM that has Plex, Jellyfin, ah4c and anything else I want to access the iGPU while also seamlessly accessing each other.

I'm looking at spinning up a 2nd CDVR server to be my Mom's (using the main CDVR as the channel source) because she seemed surprisingly open to switching how she watches TV. If I do, I'll make that one an LXC and see how I like it. If that works maybe I'll spin up an LXC for my main server and migrate the settings there. One thing at a time, as she absolutely requires stable captions as opposed to my wife who just prefers watching that way.

I have a very fast (4-5 second wake to playback) set of Osprey deeplinks scripts to PR but at the same time I wonder if any bugs have been fixed with com.att.tv and I want to see if any lessons I learned from debugging my Ospreys can apply to the native client. The FC bridge is good but having more then one option is ideal and I see the current scripts/firetv/directv are going on 3 years without a refactor.

I also plan on updating the channel# Osprey scripts a bit also. The readiness gate is a win for reliability on those but the heartbeat is 100 percent no longer needed. They fixed that in com.att.tv.openvideo with a recent patch. That Japanese IME key code I found however is perfect to prevent keep watching prompts so those just need a slight tweak!

Those are the old remote control emulation scripts we used before the deeplinks were uncovered. There's another set of scripts that use deeplinks.

1 Like

I see. That makes more sense. I wanted to poke around with them just to see if maybe I know there were some issues with deep linking into them, especially if you do am kill—that will like produce a failed deep link pretty easily. I want to see it maybe my readiness gate that I developed for the Osprey scripts could work around that. That's what I was going to experiment with.

Experiment away! We have two relevant sets of scripts firetv/dtvdeeplinks and firetv/dtvstreamdeeplinks. We needed two back before the "merge" of DTV and DTV Stream. Not really necessary now. I'd suggest working with firetv/dtvstreamdeeplinks as a starting point.

On a grander scale, I think a number of scripts could transition from device specific to a device of all. The differences are so minimal anymore that I believe we could move most to all/<provider>, since other than package name, most scripts are close to identical.

I think I got something solid. Going home then running am kill com.att.tv would usually throw it into a bad state. I managed to sort that. On the warm path I'm seeing 5 second or so tunes!!

prebmitune

#!/bin/bash

# prebmitune.sh for osprey/dtvospreydeeplinks
# 2026.09.12

#Debug on if uncommented
set -x

streamerIP="$1"
streamerNoPort="${streamerIP%%:*}"
adbTarget="timeout 15 adb -s $streamerIP"

mkdir -p "$streamerNoPort"

#Trap end of script run
finish() {
  echo "prebmitune.sh is exiting for $streamerIP with exit code $?"
}

trap finish EXIT

adbWake() {
  $adbTarget shell input keyevent KEYCODE_WAKEUP
}

adbConnect() {
  adb connect "$streamerIP"
  if adbWake; then
    echo "Waking $streamerIP" > /proc/1/fd/1
    touch "$streamerNoPort/adbAppRunning"
    return 0
  fi
  touch "$streamerNoPort/adbCommunicationFail"
  echo "Communication with $streamerIP failed" > /proc/1/fd/1
  exit 2
}

main() {
  adbConnect
}

main

bmitune

#!/bin/bash
# bmitune.sh for firetv/dtvstreamdeeplinks
# 2026.09.30

#Debug on if uncommented
set -x

#Global
channelID=$(echo $1 | awk -F~ '{print $2}')
channelName=$(echo $1 | awk -F~ '{print $1}')
streamerIP="$2"
streamerNoPort="${streamerIP%%:*}"
adbTarget="adb -s $streamerIP"
keepWatchingInterval="$KEEP_WATCHING"
keepWatchingKeycode=KEYCODE_ZENKAKU_HANKAKU

mkdir -p $streamerNoPort
echo $$ > "$streamerNoPort/bmitune_pid"

#Trap end of script run
finish() {
  echo "bmitune.sh is exiting for $streamerIP with exit code $?"
}

trap finish EXIT

tuneChannel() {
  $adbTarget shell "
    if pidof com.att.tv >/dev/null; then
      am start -a android.intent.action.VIEW -d 'dtvnow://deeplink.directvnow.com/play/channel/$channelName/$channelID' com.att.tv
    else
      am start --activity-clear-task -a android.intent.action.VIEW -d 'dtvnow://deeplink.directvnow.com/play/channel/$channelName/$channelID' com.att.tv
    fi"
}

startKeepWatching() {
  if [[ -f "$streamerNoPort/keepwatching_pid" ]]; then
    kill -- -"$(<"$streamerNoPort/keepwatching_pid")" 2>/dev/null
  fi
  cat > ./$streamerNoPort/keep_watching.sh <<HBEOF
#!/bin/bash
trap 'echo "keep watching pid killed" > /proc/1/fd/1; exit 0' TERM INT
echo "keep watching started for $streamerIP -- keyevent $keepWatchingKeycode every ${keepWatchingInterval}s" > /proc/1/fd/1
while true; do
  sleep $keepWatchingInterval
  $adbTarget shell input keyevent $keepWatchingKeycode
done
HBEOF
  chmod +x ./$streamerNoPort/keep_watching.sh
  setsid ./$streamerNoPort/keep_watching.sh >/dev/null 2>&1 &
  echo $! > "$streamerNoPort/keepwatching_pid"
}

main() {
  tuneChannel
  if [[ -n "$keepWatchingInterval" && "$keepWatchingInterval" != 0 ]]; then
    startKeepWatching
  fi
}

main

stopbmitune

#!/bin/bash
# stopbmitune.sh for firetv/dtvstreamdeeplinks
# 2026.09.29

#Debug on if uncommented
set -x

streamerIP="$1"
streamerNoPort="${streamerIP%%:*}"
adbTarget="adb -s $streamerIP"

#Check if bmitune.sh is done running, then kill the heartbeat before the device is slept
bmituneDone() {
  bmitunePID=$(<"$streamerNoPort/bmitune_pid")

  while ps -p $bmitunePID > /dev/null; do
    echo "Waiting for bmitune.sh to complete..."
    sleep 2
  done

  if [[ -f "$streamerNoPort/heartbeat_pid" ]]; then
    heartbeatPID=$(<"$streamerNoPort/heartbeat_pid")
    kill -- -"$heartbeatPID" 2>/dev/null
    rm -f "./$streamerNoPort/heartbeat_pid"
  fi
  rm -f "./$streamerNoPort/heartbeat.sh"
}

#Stop DTV's playback so there's nothing to reload on the next wake, then sleep
adbSleep() {
  $adbTarget shell "input keyevent KEYCODE_MEDIA_STOP; input keyevent KEYCODE_SLEEP"
  echo "Sleep initiated for $streamerIP"
  date +%s > $streamerNoPort/stream_stopped
  echo "$streamerNoPort/stream_stopped written with epoch stop time"
}

main() {
  bmituneDone
  adbSleep
}

main

Looking for some testing and feedback before I open a PR. Please try and break them! This works on firetv and google tv.

1 Like