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

Like I said, I don't think ESPN/ESPN+ is a good candidate, but if you want you mess around with it anyway you could set things up as though you were using DTV. You could use a STREAMER_APP value of scripts/firetv/dtvdeeplinks and a CDVR_M3U_NAME of dtvdeeplinks.m3u.

This would give you an idea of what the scripts would look like, and an M3U. You can find the Docker Compose, and some example env vars here:

2 posts were merged into an existing topic: ADBTuner: A "channel tuning" application for networked Google TV / Android TV devices

MovieSphere Gold got recently added to DTV/Stream. For those who need the streamlink, its

http://{{ .IPADDRESS }}/play/tuner/MVSGLD~6b272ceb-e01c-4c9a-b464-9e169f766fdb

and gracenote id is 185303

2 Likes

Thank you! MovieSphere Gold also has been added to FRNDLY's lineup.

@bnhf I've been trying out the pyatv version of ah4c for the last few days, and am trying to figure out if I can delay tuning while prebmitune and bmitune run so the stream seems more seamless (not showing app UI elements and button presses). Is there a way to run the above ffmpeg command simultaneous to prebmitune and bmitune? Right now, it seems like the tuning scripts run after CMD1.

I don't normally use this, but I just tested it a couple of times -- and it's working as intended for me.

For a definitive test, I set the -ss value to 20, which is also the number of seconds I wait before jumping to "live" in the current testing I'm doing with the ESPN app. Sure enough, I saw nothing of the tuning process, but when the video started it jumped to live immediately.

Your Docker Compose should look something like this:

services:
  ah4c: # This docker-compose typically requires no editing. Use the Environment variables section of Portainer to set your values.
    # 2025.09.13
    # GitHub home for this project: https://github.com/bnhf/ah4c.
    # Docker container home for this project with setup instructions: https://hub.docker.com/r/bnhf/ah4c.
    image: bnhf/ah4c:${TAG:-latest}
    container_name: ${CONTAINER_NAME:-ah4c}
    hostname: ${HOSTNAME:-ah4c}
    dns_search: ${DOMAIN:-localdomain} # Specify the name of your LAN's domain, usually local or localdomain
    #devices:
      #- /dev/dri:/dev/dri # Uncomment for Intel Quick Sync (GPU) access
    ports:
      - ${ADBS_PORT:-5037}:5037 # Port used by adb-server
      - ${HOST_PORT:-7654}:7654 # Port used by this ah4c proxy
      - ${SCRC_PORT:-7655}:8000 # Port used by ws-scrcpy
    environment:
      - IPADDRESS=${IPADDRESS} # Hostname or IP address of this ah4c extension to be used in M3U file (also add port number if not in M3U)
      - NUMBER_TUNERS=${NUMBER_TUNERS} # Number of tuners you'd like defined 1, 2, 3 or 4 supported
      - TUNER1_IP=${TUNER1_IP} # Streaming device #1 with adb port in the form hostname:port or ip:port
      - TUNER2_IP=${TUNER2_IP} # Streaming device #2 with adb port in the form hostname:port or ip:port
      - TUNER3_IP=${TUNER3_IP} # Streaming device #3 with adb port in the form hostname:port or ip:port
      - TUNER4_IP=${TUNER4_IP} # Streaming device #4 with adb port in the form hostname:port or ip:port
      - TUNER5_IP=${TUNER5_IP} # Streaming device #5 with adb port in the form hostname:port or ip:port
      - TUNER6_IP=${TUNER6_IP} # Streaming device #6 with adb port in the form hostname:port or ip:port
      - TUNER7_IP=${TUNER7_IP} # Streaming device #7 with adb port in the form hostname:port or ip:port
      - TUNER8_IP=${TUNER8_IP} # Streaming device #8 with adb port in the form hostname:port or ip:port
      - TUNER9_IP=${TUNER9_IP} # Streaming device #9 with adb port in the form hostname:port or ip:port
      - ENCODER1_URL=${ENCODER1_URL} # Full URL for tuner #1 in the form http://hostname/stream or http://ip/stream
      - ENCODER2_URL=${ENCODER2_URL} # Full URL for tuner #2 in the form http://hostname/stream or http://ip/stream
      - ENCODER3_URL=${ENCODER3_URL} # Full URL for tuner #3 in the form http://hostname/stream or http://ip/stream
      - ENCODER4_URL=${ENCODER4_URL} # Full URL for tuner #4 in the form http://hostname/stream or http://ip/stream
      - ENCODER5_URL=${ENCODER5_URL} # Full URL for tuner #5 in the form http://hostname/stream or http://ip/stream
      - ENCODER6_URL=${ENCODER6_URL} # Full URL for tuner #6 in the form http://hostname/stream or http://ip/stream
      - ENCODER7_URL=${ENCODER7_URL} # Full URL for tuner #7 in the form http://hostname/stream or http://ip/stream
      - ENCODER8_URL=${ENCODER8_URL} # Full URL for tuner #8 in the form http://hostname/stream or http://ip/stream
      - ENCODER9_URL=${ENCODER9_URL} # Full URL for tuner #9 in the form http://hostname/stream or http://ip/stream
      - CMD1=${CMD1} # Typically used for ffmpeg processing of a device's stream. ffmpeg -i ${ENCODER1_URL} -ss 8 -c:v copy -c:a copy -f mpegts -
      - CMD2=${CMD2} # Typically used for ffmpeg processing of a device's stream. ffmpeg -i ${ENCODER2_URL} -ss 8 -c:v copy -c:a copy -f mpegts -
      - STREAMER_APP=${STREAMER_APP} # Streaming device name and streaming app you're using in the form scripts/streamer/app (use lowercase with slashes between as shown)
      - CHANNELSIP=${CHANNELSIP} # Hostname or IP address of the Channels DVR server itself
      - ALERT_SMTP_SERVER=${ALERT_SMTP_SERVER} # The domainname:port of the SMTP server you'll be using like smtp.gmail.com:587. This is for sending ah4c alerts if tuning fails.
      - ALERT_AUTH_SERVER=${ALERT_AUTH_SERVER} # The auth server for the e-mail you'll be using like smtp.gmail.com
      - ALERT_EMAIL_FROM=${ALERT_EMAIL_FROM} # The e-mail address you'd like your ah4c failure alert e-mails to show as being from.
      - ALERT_EMAIL_PASS=${ALERT_EMAIL_PASS} # Gmail and Yahoo both support the creation of app-specific e-mail passwords, and this is the way to go! It's NOT recommended to use your everyday e-mail password.
      - ALERT_EMAIL_TO=${ALERT_EMAIL_TO} # The e-mail address you'd like your alert e-mails sent to.
      #- ALERT_WEBHOOK_URL=""
      - LIVETV_ATTEMPTS=${LIVETV_ATTEMPTS} # For FireTV Live Guide tuning only, set maximum number of attempts at finding the desired channel
      - CREATE_M3US=${CREATE_M3US:-false} # Set to true to create device-specific M3Us for use with Amazon Prime Premium channels -- requires a FireTV device
      - UPDATE_SCRIPTS=${UPDATE_SCRIPTS:-true} # Set to true if you'd like the sample scripts and STREAMER_APP scripts updated whether they exist of not
      - UPDATE_M3US=${UPDATE_M3US:-true} # Set to true if you'd like the sample m3us updated whether they exist of not
      - TZ=${TZ} # Your local timezone in Linux "tz" format
      - SPEED_MODE=${SPEED_MODE:-false} # Set to false if you'd like the target streaming app to be closed after each tuning cycle (limited script support).
      - KEEP_WATCHING=${KEEP_WATCHING:-235m} # In supported scripts, set the delay before resending a tuning deeplink to prevent "Are you still watching?" type messages. Examples: Use 4h for 4 hours or 240m for 240 minutes.
      - AUTOCROP_CHANNELS=${AUTOCROP_CHANNELS} # Space separated list of channels (by number) with black borders on 4 sides to autocrop while maintaining aspect ratio. Requires LinkPi Encoder! 
      - LINKPI_HOSTNAME=${LINKPI_HOSTNAME} # Hostname or IP address of your LinkPi encoder. For use with AUTOCROP_CHANNELS.
      - LINKPI_USERNAME=${LINKPI_USERNAME} # LinkPi username. For use with AUTOCROP_CHANNELS.
      - LINKPI_PASSWORD=${LINKPI_PASSWORD} # LinkPi password. For use with AUTOCROP_CHANNELS.
      - USER_SCRIPT=${USER_SCRIPT} # A custom user script to be run at container start. If placed in /HOST_DIR/olivetin, only the script name is required, like ./userscript.sh
    volumes:
      - ${HOST_DIR:-/data}/ah4c/scripts:/opt/scripts # pre/stop/bmitune.sh scripts will be stored in this bound host directory under streamer/app
      - ${HOST_DIR:-/data}/ah4c/m3u:/opt/m3u # m3u files will be stored here and hosted at http://<hostname or ip>:7654/m3u for use in Channels DVR - Custom Channels settings
      - ${HOST_DIR:-/data}/ah4c/adb:/root/.android # Persistent data directory for adb keys
    restart: unless-stopped

And in the Environment variables section of the Portainer-Stacks Editor:

TAG=latest
CONTAINER_NAME=ah4c
HOSTNAME=ah4c
DOMAIN=localdomain tailxxxxx.ts.net
ADBS_PORT=5037
HOST_PORT=7654
SCRC_PORT=7655
IPADDRESS=htpc6:7654
NUMBER_TUNERS=2
TUNER1_IP=firestick-rack1:5555
ENCODER1_URL=http://encoder_48007/0.ts
CMD1=ffmpeg -i ${ENCODER1_URL} -ss 20 -c:v copy -c:a copy -f mpegts -
TUNER2_IP=firestick-rack2:5555
ENCODER2_URL=http://encoder_48007/4.ts
CMD2=ffmpeg -i ${ENCODER2_URL} -ss 20 -c:v copy -c:a copy -f mpegts -
STREAMER_APP=scripts/firetv/espn
CHANNELSIP=media-server8
ALERT_SMTP_SERVER=smtp.gmail.com:587
ALERT_AUTH_SERVER=smtp.gmail.com
ALERT_EMAIL_FROM=[Redacted]
ALERT_EMAIL_PASS=[Redacted]
ALERT_EMAIL_TO=[Redacted]
LIVETV_ATTEMPTS=
CREATE_M3US=false
UPDATE_SCRIPTS=true
UPDATE_M3US=true
TZ=US/Mountain
SPEED_MODE=false
KEEP_WATCHING=235m
AUTOCROP_CHANNELS=
LINKPI_HOSTNAME=
LINKPI_USERNAME=
LINKPI_PASSWORD=
USER_SCRIPT=
HOST_DIR=/data
CDVR_M3U_NAME=espn_plus.m3u

Note that the env vars used in CMD1 and CMD2 are not placeholders -- those variable names should be used as shown. Adjust the number of seconds after -ss to suit.

Good call on testing with a much longer delay, CMD1 is indeed working as expected and I probably wasn't accounting for some inconsistent lag from my Apple TV responding to the tuning scripts. I'm not sure if it was using your slightly updated compose but everything seems to be a little snappier now. Thank you for taking some time to check it out for me.

@bnhf i see there is a Kodi favourites script on github but wanted to ask if it would be possible to tune channels on a Kodi box with JSON?

curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"Player.Open","params":{"item":{"channelid":**14**}}}' http://localhost:8080/jsonrpc

Example above to tune to channel 14

I was not very involved in those particular scripts. I'd suggest searching this thread for detail on what the person that wrote them was trying to accomplish.

Thanks but i dont want to use the Kodi script. What im really asking if its possible to adapt your basic script and tune channels with the curl command above and get ch no from m3u?

When Channels DVR wants to tune using ah4c, it passes two values to the ah4c scripts. The hostname or IP of the encoder-connected streaming stick to use (with port), and the value after the last slash in the M3U's URL.

That value after the last slash can be anything you want it to be, but it's typically something like a channel number or other unique identifier that allows for the channels to be tuned.

So in your example, if the part of the curl command that changes is the value currently represented by "14", then you'd use that after the last slash in the M3U's URL for that station "record".

You can even pass multiple values if needed. I usually separate multiple values with a tilde (~), and then parse them in the script. Like if you needed to pass a channel name and a deeplink, you could do that.

It's really only limited by your imagination. :slight_smile:

2 Likes

@bnhf
Need your thoughts on something. I currently am feeding three sources into ChanelsDVr via ah4c. My bmitune.sh looks like this and calls a central m3u file for tuning info:

#!/bin/bash

channelID=\""$1\""
specialID="$1"
TUNERIP="$2"
channelName=$(echo $1 | awk -F/ '{print $NF}')
providerDTV="dtv"
providerHBO="max"
providerYTV="ytv"
appendYTV="?onboard=1"

MAX_LAUNCH="com.wbd.stream"
MAX_NAME="com.wbd.beam.BeamActivity"
APP_NAME="com.google.android.apps.youtube.tvunplugged.activity.MainActivity"
APP_LAUNCH="com.google.android.youtube.tvunplugged"

echo $channelID
echo $TUNERIP
echo $specialID
echo $channelName
Realname="${channelName#???}"
echo $Realname
channelLabel=$(echo $Realname | awk -F~ '{print $1}')
echo $channelLabel
#contentID=$(echo $Realname | awk -F~ '{print $NF}')
contentID=$(echo $Realname | awk '{sub(/^[^:]*~/, ""); print}')
echo $contentID

if [[ "$channelName" == *"$providerDTV"* ]] ; then

     adb -s "$TUNERIP" shell "am start -a android.intent.action.VIEW -d dtvnow://deeplink.directvnow.com/play/channel/$channelLabel/"$contentID""

elif [[ "$channelName" == *"$providerHBO"* ]] ; then
     sh TuneMaxApp.sh  $TUNERIP $channelLabel
     #adb -s "$TUNERIP" shell "am start -a android.intent.action.VIEW -d https://play.max.com/channel/watch/"$contentID""
 
elif [[ "$channelName" == *"$providerYTV"* ]] ; then

     adb -s "$TUNERIP" shell "am start -a android.intent.action.VIEW -d  https://tv.youtube.com/watch/"$contentID"" -n $APP_LAUNCH/$APP_NAME


fi

Please excuse the unused variables and unnecessary echos for now. I know they look messy. That was from my limited coding skills as well as debugging and trying to get it the three sources tuning in one script. I found that with the hobo max app I had to go back to using a contentID txt file because using the entire length of the link to stream in my central m3u file would cause the hbo channels not to be read at all by ah4c.

The end of a hbo max live channel would have something like this if I were to tune it using the m3u and no content id.

a903ca8a-6d5e-559b-a027-f82997397694/452e5d78-1cca-59a9-8508-21b25d813874

It is much longer than the rest. I think you mentioned previously that the "/" in the middle of it all might be the problem.

Anyway, each tuner is called in ah4c with a script that looks like this:

Input1.sh

#!/bin/bash
sleep 15 && bash -c "magewell2ts -i 1 -s 100 -q 12 -c h264_qsv -m "

What I am trying to figure out is a way to vary the number of seconds to delay for each tuner based upon the source being tuned. What you see here is a default 15 seconds based on the amount of time it takes for the hbo max app to tune and get to video. Directv take around 8 or so and YoutubeTV takes less.

Keeping the default at 15 seconds therefore works for all of them, but I would still like to be able to vary things if possible.

I need a bit more detail on this. How and where is this script called?

The tuning script is called for each of the four tuners on the card this way:

If I send the tuning command from bmitune.sh and leave the CMD field empty would that mess how ah4c is able to switch from one tuner to the next?

Here is a sample of the m3u that bmitune.sh is calling to. There is one of each tuning source (Directv, YoutubeTV, and Hbo Max app):

#EXTM3U

#EXTINF:-1 channel-id="202" channel-number="202" tvc-guide-stationid="58646" tvg-group="" tvg-logo="",CNN HD
http://{{ .IPADDRESS }}/play/tuner/dtvCNNHD~d3603aea-f5d8-e789-786c-43c5e8799428
#EXTINF:-1 channel-id="#277" channel-number="277" tvc-guide-stationid="11871" tvg-group="" tvg-logo="",ACC
#http://{{ .IPADDRESS }}/play/tuner/ytv277~YaqNgmedD7U?onboard=1
#EXTINF:-1 channel-id="818" channel-number="818" tvc-guide-stationid="" tvg-group="" tvg-logo="",HBOCOM
http://{{ .IPADDRESS }}/play/tuner/max818

Like is said, I had to revert to the contentID method for the max app because either the length of the deep link or the "/" that appears in it does not allow it to be read from the m3u file.

Apologies, but I don't recognize this UI. Where is this from?

But I get the idea -- these are the CMD1, CMD2 environment variables. Let me ponder this briefly, and I'll get back to you.

The picture is from is the "Edit ENV Configuration & tuners" page from the ah4c web interface.

I am using the non docker version.

The only way I can see out is to have a clunky script for each provider in the same way I have a TuneMaxApp.sh for hbo that takes care of the welcome screen.

I don't think that'll be necessary. I re-wrote your bmitune.sh script to clean it up, and add a new concept or two. It writes the desired sleep time to a file, with directories named by tuner IP.

I also re-worked your M3U, using another tilde(~) to better handle the data. Also, I URL encoded the slash(/) in your Max contentID value (using a %2F), which should allow it to be passed into the script. Then I URL decoded it to send via ADB.

bmitune.sh:

#!/bin/bash

provider=$(echo $1 | awk -F~ '{print $1}')
callSign=$(echo $1 | awk -F~ '{print $2}')
contentID=$(echo $1 | awk -F~ '{print $3}')
tunerIP="$2"
tunerNoPort="${tunerIP%%:*}"
    mkdir -p $tunerNoPort
adbTarget="adb -s $tunerIP shell am start -a android.intent.action.VIEW -d  "

# Adjust the desired sleep times by provider
declare -A sleepTime=(
  [dtv]=15
  [max]=15
  [yttv]=15
)

echo "${sleepTime[$provider]}" > $tunerNoPort/tune_sleep 

case "$provider" in
    "dtv")
        $adbTarget dtvnow://deeplink.directvnow.com/play/channel/$callSign/$contentID
        ;;
    "max")
        $adbTarget https://play.max.com/channel/watch/$(printf '%b\n' "${contentID//%/\\x}")
        ;;
    "yttv")
        $adbTarget https://tv.youtube.com/watch/$contentID?onboard=1 -n com.google.android.youtube.tvunplugged/com.google.android.apps.youtube.tvunplugged.activity.MainActivity
        ;;
    *)
        exit 1
        ;;
esac

magewell.m3u:

#EXTM3U

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

#EXTINF:-1 channel-id="277" channel-number="277" tvc-guide-stationid="11871" tvg-group="" tvg-logo="",ACC
http://{{ .IPADDRESS }}/play/tuner/yttv~~YaqNgmedD7U

#EXTINF:-1 channel-id="818" channel-number="818" tvc-guide-stationid="" tvg-group="" tvg-logo="",HBOCOM
http://{{ .IPADDRESS }}/play/tuner/max~~a903ca8a-6d5e-559b-a027-f82997397694%2F452e5d78-1cca-59a9-8508-21b25d813874

Then, rather than using an input1.sh script, I would use this directly in the CMD field (adjust by tuner #):

{ sleep $(cat 192.168.1.130/tune_sleep); bash -c "magewell2ts -i 1 -s 100 -q 12 -c h264_qsv -m "; }

This all needs to be tested of course!

Will try this out when I wake up later today and report back.

I get this for each channel on the three channels.

[START] ah4c is ready
[GIN-debug] Listening and serving HTTP on :7654
[GIN-debug] Request: 127.0.0.1 POST /configsave, latency: 158.507µs, status: 301
[GIN-debug] Request: 127.0.0.1 GET /config, latency: 1.040241ms, status: 200
Attempting network tune for device  192.168.1.130:5555 yttv~~YaqNgmedD7U 
[ERR] Failed to fetch source: Get "": unsupported protocol scheme ""
Attempting network tune for device  192.168.1.131:5555 yttv~~YaqNgmedD7U 
[ERR] Failed to fetch source: Get "": unsupported protocol scheme ""
[ERR] Failed to tune device(s) not available
[GIN-debug] Request: 192.168.1.188 GET /play/tuner/yttv~~YaqNgmedD7U, latency: 108.19µs, status: 500
Attempting network tune for device  192.168.1.130:5555 yttv~~YaqNgmedD7U 
[ERR] Failed to fetch source: Get "": unsupported protocol scheme ""
Attempting network tune for device  192.168.1.131:5555 yttv~~YaqNgmedD7U 
[ERR] Failed to fetch source: Get "": unsupported protocol scheme ""
[ERR] Failed to tune device(s) not available
[GIN-debug] Request: 192.168.1.188 GET /play/tuner/yttv~~YaqNgmedD7U, latency: 176.372µs, status: 500
[GIN-debug] Request: 192.168.1.188 GET /play/tuner/max~~a903ca8a-6d5e-559b-a027-f82997397694%2F452e5d78-1cca-59a9-8508-21b25d813874, latency: 430ns, status: 404
[GIN-debug] Request: 192.168.1.188 GET /play/tuner/max~~a903ca8a-6d5e-559b-a027-f82997397694%2F452e5d78-1cca-59a9-8508-21b25d813874, latency: 992ns, status: 404

Narrowed the issue down to the command for tuning.

{ sleep $(cat 192.168.1.130/tune_sleep); bash -c "magewell2ts -i 1 -s 100 -q 12 -c h264_qsv -m "; }

ah4c does not like this. It spits out this when I try to load it with that command in place:

christophe@christophe:~/ah4c$ ./androidhdmi-for-channels 
[START] ah4c is starting
[ENV] Loading env
[ENV] IPADDRESS                  
[ENV] ALERT_SMTP_SERVER          
[ENV] ALERT_AUTH_SERVER          
[ENV] ALERT_EMAIL_FROM           
[ENV] ALERT_EMAIL_PASS           
[ENV] ALERT_EMAIL_TO             
[ENV] ALERT_WEBHOOK_URL          
[ENV] ALLOW_DEBUG_VIDEO_PREVIEW  
panic: Could not find an environment variable named NUMBER_TUNERS

goroutine 1 [running]:
main.loadenv()
	/home/christophe/ah4c/main.go:967 +0xd0f
main.main()
	/home/christophe/ah4c/main.go:1008 +0x2b

I have resorted to simply this for debugging:

magewell2ts -i 1 -s 100 -q 12 -c h264_qsv -m

So far dtv and yttv works but hbo is still not picked up. I still get a "status : 404" when it tries to read the m3u:

[GIN-debug] Request: 192.168.1.188 GET /play/tuner/max~~a903ca8a-6d5e-559b-a027-f82997397694%2F452e5d78-1cca-59a9-8508-21b25d813874, latency: 594ns, status: 404
[GIN-debug] Request: 192.168.1.188 GET /play/tuner/max~~a903ca8a-6d5e-559b-a027-f82997397694%2F452e5d78-1cca-59a9-8508-21b25d813874, latency: 982ns, status: 404

I'll play around with putting multiple shell commands into CMD when I have a chance, but in the meantime putting them in a script is fine too.

The good news here is that the full content ID value is being passed, but the URL encoding/decoding isn't working.

I think it's safe to say that these are actually two values, given the presence of the slash (aka a separator), much like DTV requires two values.

So I'd propose replacing that slash with another tilde, and adjusting the script as follows:

#!/bin/bash

provider=$(echo $1 | awk -F~ '{print $1}')
callSign=$(echo $1 | awk -F~ '{print $2}')
contentID=$(echo $1 | awk -F~ '{print $3}')
contentID2=$(echo $1 | awk -F~ '{print $4}')
tunerIP="$2"
tunerNoPort="${tunerIP%%:*}"
    mkdir -p $tunerNoPort
adbTarget="adb -s $tunerIP shell am start -a android.intent.action.VIEW -d  "

# Adjust the desired sleep times by provider
declare -A sleepTime=(
  [dtv]=15
  [max]=15
  [yttv]=15
)

echo "${sleepTime[$provider]}" > $tunerNoPort/tune_sleep 

case "$provider" in
    "dtv")
        $adbTarget dtvnow://deeplink.directvnow.com/play/channel/$callSign/$contentID
        ;;
    "max")
        $adbTarget https://play.max.com/channel/watch/$contentID/$contentID2
        ;;
    "yttv")
        $adbTarget https://tv.youtube.com/watch/$contentID?onboard=1 -n com.google.android.youtube.tvunplugged/com.google.android.apps.youtube.tvunplugged.activity.MainActivity
        ;;
    *)
        exit 1
        ;;
esac

With the M3U adjusted to remove the URL encoded slash, and using a tilde instead:

#EXTM3U

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

#EXTINF:-1 channel-id="277" channel-number="277" tvc-guide-stationid="11871" tvg-group="" tvg-logo="",ACC
http://{{ .IPADDRESS }}/play/tuner/yttv~~YaqNgmedD7U

#EXTINF:-1 channel-id="818" channel-number="818" tvc-guide-stationid="" tvg-group="" tvg-logo="",HBOCOM
http://{{ .IPADDRESS }}/play/tuner/max~~a903ca8a-6d5e-559b-a027-f82997397694~452e5d78-1cca-59a9-8508-21b25d813874