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

Hey, just out of curiosity, this is probably a stupid question. When you set the scripts to auto-update, how does the auto-update work?

My updated Osprey scripts with the keep alive and bug fix got merged into master, but I noticed it's not pulling down. Is that something built into the image? I wasn't sure if it's built into the image or just like a curl request from GitHub.

They'll be in the next build.

1 Like

Oh cool, you are the best! :grinning:

Has anyone run into an issue where tuning has just kind of failed? I noticed if I have to reboot one of my encoders, tuning will fail the first time and then on the second try it will tune. But I can see in the LinkPi interface that it's actually trying to tune and it's tuning a channel successfully. It's just that AH4C just kind of doesn't send the stream and the script dies.

I have. I've actually started checking and usually find that the adb permission window popped up, but you need to be on the menu screen to see it. I've actually been thinking of changing that setting in my boxes so I can keep better track of the issue.

I also noticed that on a per box basis you must walk thru the favorites selection for the user profile your going to use. if you exit out of it, it will just continue popping up every few days.
this issue also goes away with a 2nd tune
also from my notes; My Favorites (if Animation is off in dev options, the UI may hang. press enter, exit, list, home, or guide to force reset 'dtv app')
i set my animation to .5 now and it actually seems faster than animations off.

1 Like

Yeah, I actually think it's a regression in the Osprey scripts. I'm testing right now, but Claude found something pretty quickly.

Edit: Fixed! prebmitune.sh

#!/bin/bash
#prebmitune.sh for osprey/dtvospreydeeplinks
# 2026.04.16

#Debug on if uncommented
set -x

streamerIP="$1"
streamerNoPort="${streamerIP%%:*}"
adbTarget="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

adbConnect() {
  adb connect $streamerIP

  local -i adbMaxRetries=3
  local -i adbCounter=0

  while true; do
    $adbTarget shell input keyevent KEYCODE_WAKEUP
    local adbEventSuccess=$?

    if [[ $adbEventSuccess -eq 0 ]]; then
      break
    fi

    if (($adbCounter > $adbMaxRetries)); then
      touch $streamerNoPort/adbCommunicationFail
      echo "Communication with $streamerIP failed after $adbMaxRetries retries"
      exit 2
    fi

    sleep 1
    ((adbCounter++))
  done
}

adbWake() {
  $adbTarget shell input keyevent KEYCODE_WAKEUP
  echo "Waking $streamerIP"
  touch $streamerNoPort/adbAppRunning
}

main() {
  adbConnect
  adbWake
}

main

been thinking the same thing, but never checked. I'm glad you know how to ask the machine questions right. I've never been good at that with even humans.

Try it out and let me know if it fixes it for you, but I just rebooted my LinkPi and tuned it and it worked right away.

This is for the deep links though, so I'm not sure if you're using that.
Regular Osprey prembitune.

#!/bin/bash
#prebmitune.sh for osprey/dtvosprey
# 2026.04.16

#Debug on if uncommented
set -x

streamerIP="$1"
streamerNoPort="${streamerIP%%:*}"
adbTarget="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

adbConnect() {
  adb connect $streamerIP

  local -i adbMaxRetries=3
  local -i adbCounter=0

  while true; do
    $adbTarget shell input keyevent KEYCODE_WAKEUP
    local adbEventSuccess=$?

    if [[ $adbEventSuccess -eq 0 ]]; then
      break
    fi

    if (($adbCounter > $adbMaxRetries)); then
      touch $streamerNoPort/adbCommunicationFail
      echo "Communication with $streamerIP failed after $adbMaxRetries retries"
      exit 2
    fi

    sleep 1
    ((adbCounter++))
  done
}

adbWake() {

    $adbTarget shell input keyevent KEYCODE_WAKEUP; sleep 2;
    echo "Waking $streamerIP"
    touch $streamerNoPort/adbAppRunning

}

main() {
  adbConnect
  adbWake
}

main

Edit: these scripts didn't fix my issue :frowning:

is that the denominator? the linkpi reboot? I hadn't isolated yet, but would like to re-create the issue before trying the fix. (I just know that when I rebooted everything I had the issue)

Yeah, I noticed it was whenever I rebooted my encoder. This is mirroring the Fire TV scripts actually.

actually I don't think this was my issue. I think it's the adb thing for me, I know somethings messed up in my setup. like how both ah4c stacks are using the same directory.

so without the sleep 1, using channel # tuning, even on an old c71kw-200, when I rewind to see the tuning process, I can see the osprey wake up a 1/2sec before ch# is inputed. :+1:
I'm actually ok with that sleep 1, but think I'll leave it for now, and keep a better eye on what my failure point is.

what's the best way to script the ch# input everytime even when channel isn't changing? this would be helpful in my constant playing with toys, and i'll only see it on the ending padding of reordings.

Honestly, channel ember tuning is really slow from what I've heard. You should really try deep links. It's so fast.

Give it a shot with an M3U and then revert back if you don't like it. You just switch to the deep link scripts and then you can always switch back if it's not for you.

But really, it's so much quicker than tuning by channel number. I find it to be near instantaneous.

ya lost me at honestly, but got me back with instantaneous. lol :laughing:
I've been gonna get into deeplinks with adbTuner, but everytime I try I can't get adbtuner to work. again likely my docker setup somehow. on the list to fix.

I will however play with deeplinks on 1 instance of ah4c. I've actually got some IR stuff in the works for an awesome setup as soon as I figure out 1 more piece. :shushing_face:

1 Like

So what I’ve seen every now and then, mostly after an internet loss, is that the authorization screen will pop up and it’ll tune to the last channel used. I can then pick another channel and the 2nd Osprey will show the same. Select another channel and the 1st Osprey will tune correctly. Change the channel again and the 2nd Osprey will tune correctly. I try to remember to perform a restart on my container each month which seems to prevent the problem. This is a very infrequent occurrence, maybe every 3-4 months. I used to run to get the remote but alas, I am not that fast.

yes, if you catch it when it happens you can use a remote/adb to tune correctly.
I've also experienced internet instability (high winds and cable lines) will pop up a screen after so long. a re-tune works, but ah4c doesn't re-tune if the channel is not changing.

I've actually meant to see if adding and 'exit' or 'back' (remote commands) before tuning, would fix these random occurances of adb auth

I can't say I've ever seen this happen. However, if you're using the deep link scripts, I did open a PR that quotes the URL because some URL slugs now have spaces in them like MSNOW for example. That was causing tuning to fail, I noticed.

I noticed that a while back. probably why I stuck with #'s

I've added this to my bmitune.

tuneChannel() {
  $adbTarget shell input keyevent KEYCODE_HOME; 
  $adbTarget shell input text $channelID; 
}

it might slow it down a tad, but I still have at least 1 box pestering me with the favorites screen. (I'm really leaning toward animation scale being off , never allowed the wizard to complete, leading to repetitive prompting.

This is exactly why I love open source. I fixed my tuning issue where when I would restart one of my encoders, tuning would initially fail.

It's a change to main.go Wrap encoder body in stall-tolerant reader (option 3) · mackid1993/ah4c@1bdc0f2 · GitHub

Essentially when you reboot your HDMI encoder (or any time bmitune.sh switches channels on a cold tuner), the encoder needs a few seconds to lock onto the new source. During that window it either stops sending TS data or closes the connection entirely. AH4C currently passes the encoder's stream straight through to Channels DVR, so when the encoder hiccups, Channels DVR sees the stream die and gives up on the tune. You end up having to start it again.

This patch puts a small buffering layer between the encoder and Channels DVR. AH4C reads from the encoder into a queue, and Channels DVR reads from the queue. If the encoder closes the connection or goes quiet for more than 5 seconds, AH4C reconnects to it behind the scenes and keeps trying until it works.

The part that makes this actually solve the problem: while AH4C is reconnecting (or the encoder is just paused), Channels DVR keeps getting MPEG-TS NULL packets, the standard "filler" packets every TS demuxer drops. From Channels DVR's side, bytes are still flowing, so it doesn't time out. As soon as the encoder is producing real data again, the NULLs stop and the real stream takes over seamlessly.

Normal warm tunes are unaffected, same speed as today. The recovery only kicks in when something actually breaks, and when it does, you don't have to do anything. The tune just works. @bnhf maybe you can take a look at this and see if maybe you can make it better as you always do but I will also open a PR to make things easier.

edit: (fix) Tuning fails after rebooting HDMI encoder by mackid1993 · Pull Request #9 · sullrich/ah4c · GitHub

If anyone wants to try this build, I published it at: ghcr.io/mackid1993/ah4c:latest. Hopefully it helps reliability for some other people too and can be considered for merging into main.

1 Like

(post deleted by author)

neat solution for your issue. I'm wondering thought if that essentially breaks the cdvr tuner rollover so it never tries a different tuner. ie adbtuner - ah4c