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

You can't use a Linux paths then. Make your HOST_DIR value something like C:/data then (forward slash is correct). Once you start the stack again, you should see that directory in your Windows file system.

BTW, if this is going to be a headless server, you can make your life significantly easier by using Debian Server or Ubuntu Server for your OS. It will will save you headaches in the long run, and is the very best way to run Docker/Portainer.

ok, this is where i've been getting confused. I guess most people install dd to run an actual linux distro.

edit; it seems to work properly now that ah4c has a persistent storage dir :slight_smile:

hence the planned purchase of a NAS. sadly the minipc needs to stay w11 for playon :frowning:

You can install a Linux distro for WSL2 right from the Microsoft Store:

Also, how did you install Portainer?

thanks, I'll probably play with docker and debian on my laptop now, but trying to keep the minipc software to a minimum

i don't remember

Yup, the Exvist is a POS in my experience. Not recommended.

@spammedeeper I noticed a bit of a discrepancy in the ah4c repo regarding how we're handling scripts/osprey. Currently it's script/osprey/directv, but that naming prevents dtvosprey.m3u from being used as the M3U file.

So, I'm going to rename to scripts/osprey/dtvosprey, so the correct M3U is used. Here are the current scripts and M3U from the repo, if you want to make any changes:

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

#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
    

    ((adbCounter++))
  done
}

adbWake() {

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

}

main() {
  adbConnect
  adbWake
}

main
#!/bin/bash
#bmitune.sh for osprey/dtvosprey
# 2025.09.10

#Debug on if uncommented
set -x

#Global
channelID=\""$1\""
specialID="$1"
streamerIP="$2"
streamerNoPort="${streamerIP%%:*}"
adbTarget="adb -s $streamerIP"
m3uName="${STREAMER_APP#*/*/}.m3u"


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

trap finish EXIT

#Set encoderURL based on the value of streamerIP
matchEncoderURL() {

  case "$streamerIP" in
    "$TUNER1_IP")
      encoderURL=$ENCODER1_URL
      ;;
    "$TUNER2_IP")
      encoderURL=$ENCODER2_URL
      ;;
    "$TUNER3_IP")
      encoderURL=$ENCODER3_URL
      ;;
    "$TUNER4_IP")
      encoderURL=$ENCODER4_URL
      ;;
    *)
      exit 1
      ;;
  esac
}

#Tuning is based on channel name values from $m3uName.
tuneChannel() {
  
  $adbTarget shell input text $channelID; 
}


main() {
  matchEncoderURL
  tuneChannel
}

main
#!/bin/bash
#stopbmitune.sh for osprey/dtvosprey
# 2025.09.10

#Debug on if uncommented
set -x

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

#Device sleep
adbSleep() {
  sleep="input keyevent KEYCODE_SLEEP"

  $adbTarget shell $sleep
  echo "Sleep initiated for $streamerIP"
  date +%s > $streamerNoPort/stream_stopped
  echo "$streamerNoPort/stream_stopped written with epoch stop time"
}

main() {
  adbSleep
}

main
#EXTM3U

#EXTINF:-1 channel-id="265" channel-number="265",A&E
http://{{ .IPADDRESS }}/play/tuner/265

#EXTINF:-1 channel-id="361" channel-number="361",AccuWeather
http://{{ .IPADDRESS }}/play/tuner/361

#EXTINF:-1 channel-id="254" channel-number="254",AMC
http://{{ .IPADDRESS }}/play/tuner/254

#EXTINF:-1 channel-id="282" channel-number="282",Animal Planet
http://{{ .IPADDRESS }}/play/tuner/282

#EXTINF:-1 channel-id="340" channel-number="340",AXS TV
http://{{ .IPADDRESS }}/play/tuner/340

#EXTINF:-1 channel-id="293" channel-number="293",BabyFirst HD
http://{{ .IPADDRESS }}/play/tuner/293

#EXTINF:-1 channel-id="264" channel-number="264",BBC America
http://{{ .IPADDRESS }}/play/tuner/264

#EXTINF:-1 channel-id="329" channel-number="329",BET
http://{{ .IPADDRESS }}/play/tuner/329

#EXTINF:-1 channel-id="353" channel-number="353",Bloomberg TV
http://{{ .IPADDRESS }}/play/tuner/353

#EXTINF:-1 channel-id="298" channel-number="298",Boomerang
http://{{ .IPADDRESS }}/play/tuner/298

#EXTINF:-1 channel-id="237" channel-number="237",Bravo
http://{{ .IPADDRESS }}/play/tuner/237

#EXTINF:-1 channel-id="296" channel-number="296",Cartoon Network East
http://{{ .IPADDRESS }}/play/tuner/296

#EXTINF:-1 channel-id="341" channel-number="341",CleoTV
http://{{ .IPADDRESS }}/play/tuner/341

#EXTINF:-1 channel-id="327" channel-number="327",CMT
http://{{ .IPADDRESS }}/play/tuner/327

#EXTINF:-1 channel-id="355" channel-number="355",CNBC
http://{{ .IPADDRESS }}/play/tuner/355

#EXTINF:-1 channel-id="202" channel-number="202",CNN
http://{{ .IPADDRESS }}/play/tuner/202

#EXTINF:-1 channel-id="419" channel-number="419",CNN En EspaƱol
http://{{ .IPADDRESS }}/play/tuner/419

#EXTINF:-1 channel-id="358" channel-number="358",CNNi
http://{{ .IPADDRESS }}/play/tuner/358

#EXTINF:-1 channel-id="249" channel-number="249",Comedy Central
http://{{ .IPADDRESS }}/play/tuner/249

#EXTINF:-1 channel-id="350" channel-number="350",C-SPAN
http://{{ .IPADDRESS }}/play/tuner/350

#EXTINF:-1 channel-id="351" channel-number="351",C-SPAN2
http://{{ .IPADDRESS }}/play/tuner/351

#EXTINF:-1 channel-id="369" channel-number="369",Daystar
http://{{ .IPADDRESS }}/play/tuner/369

#EXTINF:-1 channel-id="278" channel-number="278",Discovery
http://{{ .IPADDRESS }}/play/tuner/278

#EXTINF:-1 channel-id="290" channel-number="290",Disney Channel
http://{{ .IPADDRESS }}/play/tuner/290

#EXTINF:-1 channel-id="289" channel-number="289",Disney Junior
http://{{ .IPADDRESS }}/play/tuner/289

#EXTINF:-1 channel-id="292" channel-number="292",Disney XD
http://{{ .IPADDRESS }}/play/tuner/292

#EXTINF:-1 channel-id="236" channel-number="236",E!
http://{{ .IPADDRESS }}/play/tuner/236

#EXTINF:-1 channel-id="267" channel-number="267",EarthxTV
http://{{ .IPADDRESS }}/play/tuner/267

#EXTINF:-1 channel-id="206" channel-number="206",ESPN
http://{{ .IPADDRESS }}/play/tuner/206

#EXTINF:-1 channel-id="209" channel-number="209",ESPN2
http://{{ .IPADDRESS }}/play/tuner/209

#EXTINF:-1 channel-id="314" channel-number="314",Family Movie Classics (FMC)
http://{{ .IPADDRESS }}/play/tuner/314

#EXTINF:-1 channel-id="231" channel-number="231",Food Network
http://{{ .IPADDRESS }}/play/tuner/231

#EXTINF:-1 channel-id="359" channel-number="359",FOX Business Network
http://{{ .IPADDRESS }}/play/tuner/359

#EXTINF:-1 channel-id="360" channel-number="360",FOX News Channel
http://{{ .IPADDRESS }}/play/tuner/360

#EXTINF:-1 channel-id="219" channel-number="219",FOX Sports 1
http://{{ .IPADDRESS }}/play/tuner/219

#EXTINF:-1 channel-id="363" channel-number="363",FOX Weather
http://{{ .IPADDRESS }}/play/tuner/363

#EXTINF:-1 channel-id="311" channel-number="311",Freeform
http://{{ .IPADDRESS }}/play/tuner/311

#EXTINF:-1 channel-id="248" channel-number="248",FX
http://{{ .IPADDRESS }}/play/tuner/248

#EXTINF:-1 channel-id="259" channel-number="259",FXX
http://{{ .IPADDRESS }}/play/tuner/259

#EXTINF:-1 channel-id="404" channel-number="404",Galavisión
http://{{ .IPADDRESS }}/play/tuner/404

#EXTINF:-1 channel-id="312" channel-number="312",Hallmark Channel
http://{{ .IPADDRESS }}/play/tuner/312

#EXTINF:-1 channel-id="565" channel-number="565",Hallmark Movies & Mysteries
http://{{ .IPADDRESS }}/play/tuner/565

#EXTINF:-1 channel-id="229" channel-number="229",HGTV
http://{{ .IPADDRESS }}/play/tuner/229

#EXTINF:-1 channel-id="204" channel-number="204",HLN
http://{{ .IPADDRESS }}/play/tuner/204

#EXTINF:-1 channel-id="240" channel-number="240",HSN
http://{{ .IPADDRESS }}/play/tuner/240

#EXTINF:-1 channel-id="333" channel-number="333",IFC
http://{{ .IPADDRESS }}/play/tuner/333

#EXTINF:-1 channel-id="285" channel-number="285",Investigation Discovery
http://{{ .IPADDRESS }}/play/tuner/285

#EXTINF:-1 channel-id="305" channel-number="305",ION
http://{{ .IPADDRESS }}/play/tuner/305

#EXTINF:-1 channel-id="252" channel-number="252",Lifetime
http://{{ .IPADDRESS }}/play/tuner/252

#EXTINF:-1 channel-id="281" channel-number="281",MotorTrend
http://{{ .IPADDRESS }}/play/tuner/281

#EXTINF:-1 channel-id="356" channel-number="356",MSNBC
http://{{ .IPADDRESS }}/play/tuner/356

#EXTINF:-1 channel-id="331" channel-number="331",MTV
http://{{ .IPADDRESS }}/play/tuner/331

#EXTINF:-1 channel-id="332" channel-number="332",MTV2
http://{{ .IPADDRESS }}/play/tuner/332

#EXTINF:-1 channel-id="276" channel-number="276",National Geographic Channel
http://{{ .IPADDRESS }}/play/tuner/276

#EXTINF:-1 channel-id="349" channel-number="349",Newsmax
http://{{ .IPADDRESS }}/play/tuner/349

#EXTINF:-1 channel-id="299" channel-number="299",Nickelodeon
http://{{ .IPADDRESS }}/play/tuner/299

#EXTINF:-1 channel-id="274" channel-number="274",Ovation
http://{{ .IPADDRESS }}/play/tuner/274

#EXTINF:-1 channel-id="241" channel-number="241",Paramount Network
http://{{ .IPADDRESS }}/play/tuner/241

#EXTINF:-1 channel-id="288" channel-number="288",PBS Kids
http://{{ .IPADDRESS }}/play/tuner/288

#EXTINF:-1 channel-id="275" channel-number="275",QVC
http://{{ .IPADDRESS }}/play/tuner/275

#EXTINF:-1 channel-id="315" channel-number="315",QVC2
http://{{ .IPADDRESS }}/play/tuner/315

#EXTINF:-1 channel-id="318" channel-number="318",QVC3
http://{{ .IPADDRESS }}/play/tuner/318

#EXTINF:-1 channel-id="238" channel-number="238",Reelz
http://{{ .IPADDRESS }}/play/tuner/238

#EXTINF:-1 channel-id="384" channel-number="384",Revolt
http://{{ .IPADDRESS }}/play/tuner/384

#EXTINF:-1 channel-id="345" channel-number="345",RFD-TV
http://{{ .IPADDRESS }}/play/tuner/345

#EXTINF:-1 channel-id="320" channel-number="320",Scientology Network
http://{{ .IPADDRESS }}/play/tuner/320

#EXTINF:-1 channel-id="226" channel-number="226",Shop LC
http://{{ .IPADDRESS }}/play/tuner/226

#EXTINF:-1 channel-id="239" channel-number="239",SundanceTV
http://{{ .IPADDRESS }}/play/tuner/239

#EXTINF:-1 channel-id="244" channel-number="244",Syfy
http://{{ .IPADDRESS }}/play/tuner/244

#EXTINF:-1 channel-id="247" channel-number="247",TBS
http://{{ .IPADDRESS }}/play/tuner/247

#EXTINF:-1 channel-id="256" channel-number="256",TCM
http://{{ .IPADDRESS }}/play/tuner/256

#EXTINF:-1 channel-id="303" channel-number="303",TeenNick
http://{{ .IPADDRESS }}/play/tuner/303

#EXTINF:-1 channel-id="347" channel-number="347",The First
http://{{ .IPADDRESS }}/play/tuner/347

#EXTINF:-1 channel-id="269" channel-number="269",The HISTORY Channel
http://{{ .IPADDRESS }}/play/tuner/269

#EXTINF:-1 channel-id="342" channel-number="342",TheGrio
http://{{ .IPADDRESS }}/play/tuner/342

#EXTINF:-1 channel-id="280" channel-number="280",TLC
http://{{ .IPADDRESS }}/play/tuner/280

#EXTINF:-1 channel-id="245" channel-number="245",TNT
http://{{ .IPADDRESS }}/play/tuner/245

#EXTINF:-1 channel-id="246" channel-number="246",truTV
http://{{ .IPADDRESS }}/play/tuner/246

#EXTINF:-1 channel-id="304" channel-number="304",TV Land
http://{{ .IPADDRESS }}/play/tuner/304

#EXTINF:-1 channel-id="242" channel-number="242",USA Network
http://{{ .IPADDRESS }}/play/tuner/242

#EXTINF:-1 channel-id="335" channel-number="335",VH1
http://{{ .IPADDRESS }}/play/tuner/335

#EXTINF:-1 channel-id="271" channel-number="271",VICE
http://{{ .IPADDRESS }}/play/tuner/271

#EXTINF:-1 channel-id="260" channel-number="260",WE tv
http://{{ .IPADDRESS }}/play/tuner/260

Confused me a bit. I’m using the dtvosprey.m3u file in Channels currently. I’ve made some changes so I know it’s using it. Checked my source in channels and it points to the dtvosprey.m3u file. Anything I should be concerned with if I ever update the stack? My current file structure is as you described.

I’m using Portainer without Olive Tin and Project One Click.

Actually, no. This variable is defined in bmitune.sh, but I see it's not being used in the script. Some scripts use it, but in your case you should be OK:

The idea is for the structure to be consistent though, which means the M3U being used should have the same name as its immediate parent directory. My Xfinity stuff for example, does use the M3U to determine what's currently playing on a given tuned channel and for how long.

So, it's intended for STREAMER_APP=scripts/osprey/dtvosprey to use dtvosprey.m3u, though in many cases another name can be used without causing issues -- as in yours.

Have not been able to reply before now. I really appreciate the direction and will give this a shot when I find extra time. AH4C always worked so well for me.

1 Like

Setup a test AH4C with a single Osprey. Deep Links working!

Much like my last Osprey setup, the scripts are very slim. Don't really have to check app state as the app is always running on the Osprey.

I'm getting similar 10-13 second tune speeds, much like Osprey Deep Links in ADBTuner.

Right now, I'm seeing the channel start up screen on the Osprey when tuning in Channels. I have not looked at AH4C in a while, see what scripts to put back in to delay stream or wait for video/audio. But it's not bad seeing the channel tuning. At least I know it's the right channel!

Of course, seeing what happens when the box does the Doze thing (deep sleep). We will see.

I will keep you all posted.

3 Likes

Looking for some help.

I've installed ah4c using Portainer. I have it up and running. I get this screen when launching.

Where do I go from here? I'm hoping to launch the NESN 360 app to pull in both NESN and NESN Plus. Both apps will require multiple remote commands to "tune" to the channel. I'm lost on what to do from this point on.

Also I notice that when I click "Edit ENV configuration & tuners" AH4C crashes. When I clock "Show ENG variables and current loading config" the page is blank. I assume this is an issue. Any idea?

When you're setting up support for a new streaming app, it's up to you to create the scripts to make it happen. It's pretty straightforward Bash scripting using 3 scripts. The first (prebmitune.sh) is run before the virtual tuning process, the second (bmitune.sh) is the virtual tuning process, and the third (stopbmitune.sh) does any needed cleanup.

You can take a look in the repo:

Under scripts/<streaming stick>/<streaming app> to get an idea of what these scripts look like. Some are very simple, some are a bit more "advanced", but none are complicated if you have some familiarity with Bash.

It's not an issue. Those two options don't apply to Docker installations.

Hmm... This might be the first Channels DVR add on that could me out of my league. I'll dive more into it and reach out for help if needed. At first glance I have no idea where these three scripts are located in order to edit them.

The three scripts don't exist yet, you'll be creating them. Once you look at some of the scripts in the repo, you should have a better idea what's required.

The env var STREAMER_APP defines where you'll place the scripts, for example if you're using FireSticks as your streaming sticks and NESN is the app, it'd be scripts/firetv/nesn. Under the parent directory you specified for your HOST_DIR value, you'll find a few example scripts -- but the repo has them all.

Thanks again for sharing that you rock. I always knew there must be SOMETHING to get those to work! Do you happen to have any deep link scripts for the ospreys yet? If not no worries I'll just experiment this week and tinker with it. Huge step for people with this setup. Will help when sometimes the channel numbers don't patch through (or other behavior, where it will very very rarely inputs the channel number and then nothing happens). This is leagues ahead more reliable.

I actually have a working setup. Deep Link tuning works well with AH4C and Doze did not seem to bother it. Tuning is faster than channel input too. I will share next time at a computer. The scripts will need more testing.

No rush. I was looking at the logic of the scripts last night and see a couple of ways to throw the deep links into a miscellaneous field within the M3U + changing the adb tuning command in bmitune.sh to reference the deep link. Deeplinks on osprey boxes also helpfully solves the "different channels with same channel number" problem; no more silly macro to automate channel up/ down. Off topic slightly; do you get a "start from beginning prompt" (lasts a few seconds) when tuning live now? Couldn't find a feature to turn it off, and the notification isn't apart of the android notification system but likely within the directv app itself.

The start from beginning seems to be new, only noticed it recently. From my time testing of late, mostly multi view, I’ve just let it time out and clear in its own.

No need to re-invent the wheel, there's already a well-functioning scheme for handling DTV and other deeplinks that require passing a channel ID and a channel name.

In the global variables section:

channelID=$(echo $1 | awk -F~ '{print $2}')
channelName=$(echo $1 | awk -F~ '{print $1}')

In the tuneChannel() function:

  $adbTarget shell am start -n $packageName/$packageAction dtvnow://deeplink.directvnow.com/play/channel/$channelName/$channelID

And a sample record from an M3U:

#EXTINF:-1 channel-id="202" channel-number="202" tvc-guide-stationid="58646",CNNHD
http://{{ .IPADDRESS }}/play/tuner/CNNHD~d3603aea-f5d8-e789-786c-43c5e8799428

Hello I pasted the exact docker compose in Portainer Stacks that you show in your sample, and I have t I get the following error when I try to deploy my stack, (my environment variables are copied and pasted all the way at the bottom, can you help me please?:

"Deployment error

Failed to deploy a stack: compose up operation failed: Error response from daemon: pull access denied for bnhf/ah4c, repository does not exist or may require 'docker login'"

I
TAG=latest
DOMAIN=attlocal.net
ADBS_PORT=5037
HOST_PORT=7654
SCRC_PORT=7655
IPADDRESS=192.168.1.112:7654
NUMBER_TUNERS=1
TUNER1_IP=192.168.1.64:5555
ENCODER1_URL=http://192.168.1.13:8000/chn3
STREAMER_APP=scripts/firetv/fubo
CHANNELSIP=192.168.1.112:8089
ALERT_SMTP_SERVER=smtp.gmail.com:587
ALERT_AUTH_SERVER=smtp.gmail.com
ALERT_EMAIL_FROM=nguev4841@@gmail.com
ALERT_EMAIL_PASS=Nephtaliguevara001@4842
[email protected]
UPDATE_SCRIPTS=true
UPDATE_M3US=true
TZ=America/New_York
SPEED_MODE=false
KEEP_WATCHING=4h
HOST_DIR=/data