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

Sure thing and thanks for looking at this :slight_smile:

Ok so iplayer has like 6 channels or so. So if i end up doing remote button presses, then i will need a seperate section for each stream correct? Logic would be i pass a var via the channel m3u. That var is matched to a section of the tuning script. That section will contain seperate key press sequences. Am i on the right track?

If that seems like the most efficient, reliable and reproducible, then sure. You could use a case statement for tuning with arguments like one, two, four and so on. Generally like this, though this example is not from a tuning function -- but you get the idea:

        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
1 Like

@slapman

In contrast, here's how tuning happens in scripts/firetv/directv. In this case, after checking for active audio, we use keyevents to get to the search box. From there, the channel argument passed by the M3U is searched for, followed by selecting it:

#Tuning is based on channel name values from $m3uName.
tuneChannel() {
  channelName=$(awk -F, '/channel-id='"$channelID"'/ {print $2}' m3u/$m3uName)
  channelName=$(echo $channelName | sed 's/^/"/;s/$/"/')
  numberOfBackspaces=25
  clearSearchBackspaces=$(for ((i=0; i<$numberOfBackspaces; i++)); do echo -n " KEYCODE_MEDIA_REWIND"; done)

  directvMenu="input keyevent KEYCODE_MENU; sleep 6"

  directvSearch="input keyevent KEYCODE_DPAD_LEFT; \
                 input keyevent KEYCODE_DPAD_UP; \
                 input keyevent KEYCODE_DPAD_CENTER; sleep 1; \
                 input keyevent KEYCODE_DPAD_CENTER; sleep 1"

  directvClearSearch="input keyevent$clearSearchBackspaces"

  directvTune="input keyevent KEYCODE_MEDIA_PLAY_PAUSE; sleep 1; \
               input keyevent KEYCODE_DPAD_DOWN; \
               input keyevent KEYCODE_DPAD_DOWN; \
               input keyevent KEYCODE_DPAD_DOWN; \
               input keyevent KEYCODE_DPAD_CENTER"

  $adbTarget shell $directvMenu
  $adbTarget shell $directvSearch
  $adbTarget shell $directvClearSearch
  $adbTarget shell input text "$channelName"
  $adbTarget shell $directvTune
}
1 Like

@slampman

And then a truly simple approach using deeplinks:

#Tuning is based on channel name values from dtvdeeplinks.m3u.
tuneChannel() {
  $adbTarget shell am start -n $packageName/$packageAction dtvnow://deeplink.directvnow.com/play/channel/$channelName/$channelID
}

Ok I think I am grasping this. I've done these commands via the shell and they work manually. Does the Tune BBC1 section look ok?

For the m3u would this be what I need?

#EXTM3U

#EXTINF:-1 channel-id="BBC1" tvc-guide-stationid="17153",BBC1 HD
http://{{ .IPADDRESS }}/play/tuner/17153

Beyond that I am not clear on how the proper set of scripts is called when the m3u is executed. Where is that defined?.

    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
          "17153")
            echo "Tune BBC1"
            $adbTarget shell am start uk.co.bbc.iplayer
            sleep 6
            $adbTarget shell input keyevent KEYCODE_DPAD_CENTER
            sleep 3
            $adbTarget shell input keyevent KEYCODE_DPAD_LEFT
            sleep 1
            $adbTarget shell input keyevent KEYCODE_DPAD_DOWN
            sleep 1
            $adbTarget shell input keyevent KEYCODE_DPAD_CENTER
            sleep 1
            $adbTarget shell input keyevent KEYCODE_DPAD_CENTER
            sleep 1
            $adbTarget shell input keyevent KEYCODE_DPAD_CENTER
			echo 0 > "$streamerNoPort/last_channel"
            exit 1

For fun i just tried this thinking that maybe I am not seeing some logic for the proper script selection and it may just work. I just tested this from a browser, the IP listed is the ah4c container

http://192.168.12.18:8000/play/tuner/17153

I got this response:

Cannot GET /play/tuner/17153

That's defined in the env vars for ah4c. Can you post ah4c Docker compose you're using, and the env vars so I can have a look?

Sure. I did find the streamer_app var and set it as you see in the stacks file but I got the same result

  ah4c:
    image: bnhf/ah4c:latest
    container_name: ah4c
    hostname: ah4c
    dns_search: ${DOMAIN} # Specify the name of your LAN's domain, usually local or localdomain
    ports:
      - ${ADBS_PORT}:5037 # Port used by adb-server
      - ${HOST_PORT}:7654 # Port used by this ah4c proxy
      - ${WSCR_PORT}:8000 # Port used by ws-scrcpy
    environment:
      - IPADDRESS=192.168.12.18 # Hostname or IP address of this ah4c extension to be used in M3U file (also add port number if not in M3U)
      - NUMBER_TUNERS=1 # Number of tuners you'd like defined 1, 2, 3 or 4 supported
      - TUNER1_IP=192.168.12.177:5555 # 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
      - ENCODER1_URL=http://192.168.12.16:8090/stream0 # 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
      - STREAMER_APP=scripts/firetv/bbc # Streaming device name and streaming app you're using in the form scripts/streamer/app (use lowercase with slashes between as shown)
      - CHANNELSIP=192.168.12.30 # 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=45 # For FireTV Live Guide tuning only, set maximum number of attempts at finding the desired channel
      - CREATE_M3US=false # Set to true to create device-specific M3Us for use with Amazon Prime Premium channels -- requires a FireTV device
      - UPDATE_SCRIPTS=false # Set to true if you'd like the sample scripts and STREAMER_APP scripts updated whether they exist or not
      - UPDATE_M3US=true # Set to true if you'd like the sample m3us updated whether they exist or not
      - TZ=America/New_York # Your local timezone in Linux "tz" format
    volumes:
      - ah4c_scripts:/opt/scripts # pre/stop/bmitune.sh scripts will be stored in this bound host directory under streamer/app
      - 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
      - ah4c_adb:/root/.android # Persistent data directory for adb keys
    restart: unless-stopped
    networks:
      IoT-enp10s0-macvlan-network:
        ipv4_address: 192.168.12.18

OK, so the idea with this style of Docker compose is to not hardcode your values into the compose, but rather to use Environment variables section of Portainer. That way, if the compose gets updated, or you decide to spin-up another ah4c container you can use the compose pretty much as-is.

Another advantage is makes it super-easy to see which values are specific to you. They way you have it setup currently, you don't have any of your ports defined. Here's the way I recommend you set it up:

I don't mind hard coding the values for now as doing env variables in a stacks file kind of sucks. I'll add the ports but I cant make out the screenshot unfortunately. I tried zooming in but I just couldn't make it any bigger.

Go into advanced mode in the Environment variables section, and you can copy and paste. It's actually easier than hard coding values.

version: '3.9'
services:
  ah4c:
    image: bnhf/ah4c:${TAG}
    container_name: ah4c
    hostname: ah4c
    dns_search: ${DOMAIN} # Specify the name of your LAN's domain, usually local or localdomain
    ports:
      - ${ADBS_PORT}:5037 # Port used by adb-server
      - ${HOST_PORT}:7654 # Port used by this ah4c proxy
      - ${WSCR_PORT}: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
      - 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
      - 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} # Set to true to create device-specific M3Us for use with Amazon Prime Premium channels -- requires a FireTV device
      - UPDATE_SCRIPTS=${UPDATE_SCRIPTS} # Set to true if you'd like the sample scripts and STREAMER_APP scripts updated whether they exist or not
      - UPDATE_M3US=${UPDATE_M3US} # Set to true if you'd like the sample m3us updated whether they exist or not
      - TZ=${TZ} # Your local timezone in Linux "tz" format
    volumes:
      - ${HOST_DIR}/ah4c/scripts:/opt/scripts # pre/stop/bmitune.sh scripts will be stored in this bound host directory under streamer/app
      - ${HOST_DIR}/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}/ah4c/adb:/root/.android # Persistent data directory for adb keys
    restart: unless-stopped
TAG=latest
DOMAIN=localdomain
ADBS_PORT=5037
HOST_PORT=7654
WSCR_PORT=7655
IPADDRESS=htpc6:7654
NUMBER_TUNERS=2
TUNER1_IP=firestick-rack1:5555
ENCODER1_URL=http://encoder_48007/0.ts
TUNER2_IP=firestick-rack2:5555
ENCODER2_URL=http://encoder_48007/4.ts
STREAMER_APP=scripts/firetv/directv
CHANNELSIP=media-server6
ALERT_SMTP_SERVER=smtp.gmail.com:587
ALERT_AUTH_SERVER=smtp.gmail.com
[email protected]
ALERT_EMAIL_PASS=xxxxxxxxxxxxxxxx
[email protected]
LIVETV_ATTEMPTS=45
CREATE_M3US=false
UPDATE_SCRIPTS=true
UPDATE_M3US=true
TZ=US/Mountain
HOST_DIR=/data

looks like the only thing I was missing was the port on the end of the ah4c IP. I changed that and restarted the container with the same result. I tried it from curl on the Channels server and same failure.

seth@channelsdvr:~$ curl http://192.168.12.18:8000/play/tuner/17153
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot GET /play/tuner/17153</pre>
</body>
</html>

Unless you added env vars, you don't have any of your ports defined. As you know, the port number you're using on the docker host side is to the left of the colon.

Also, once you get your host side port assignments sorted out, port 8000 is the ws-scrcpy port. ah4c is on 7654.

So what would it look like then?

 ports:
      - 5037:5037 # Port used by adb-server
      - 7654:7654 # Port used by this ah4c proxy
      - 8000:8000 # Port used by ws-scrcpy

That would be correct if you're hard coding values.

same error. I dont want you to have to waste any more time. I seem to be the only one having issues getting this to work so the issue has to be me.

Are you sure you're using the right port with curl? Did you miss this post:

I just tried it on my system and I see:

root@htpc6:~# curl http://htpc6:7654/play/tuner/212
Warning: Binary output can mess up your terminal. Use "--output -" to tell 
Warning: curl to output it to your terminal anyway, or consider "--output 
Warning: <FILE>" to save to a file.

Which seems about right for output, though I haven't ever tried using curl on an ah4c port before.

@slampman

Can you post your Portainer log output from container spin-up until it shows as ready?

sure

connected to 192.168.12.177:5555
Existing ./scripts/firetv/directv/prebmitune.sh found, and will be preserved
Existing ./scripts/firetv/directv/bmitune.sh found, and will be preserved
Existing ./scripts/firetv/directv/stopbmitune.sh found, and will be preserved
No existing directv.m3u found or UPDATE_M3US set to true
No existing dtvdeeplinks.m3u found or UPDATE_M3US set to true
No existing dtvosprey.m3u found or UPDATE_M3US set to true
No existing dtvstream.m3u found or UPDATE_M3US set to true
No existing dtvstreamdeeplinks.m3u found or UPDATE_M3US set to true
No existing foo-fighters.m3u found or UPDATE_M3US set to true
No existing fubo.m3u found or UPDATE_M3US set to true
No existing hulu.m3u found or UPDATE_M3US set to true
No existing livetv.m3u found or UPDATE_M3US set to true
No existing nbc.m3u found or UPDATE_M3US set to true
No existing npo.m3u found or UPDATE_M3US set to true
No existing pbs-seatac.m3u found or UPDATE_M3US set to true
No existing pbs-worcester.m3u found or UPDATE_M3US set to true
No existing silicondust.m3u found or UPDATE_M3US set to true
No existing sling.m3u found or UPDATE_M3US set to true
No existing spectrum.m3u found or UPDATE_M3US set to true
No existing xfinity.m3u found or UPDATE_M3US set to true
No existing youtubetv_shield.m3u found or UPDATE_M3US set to true
No existing youtubetv.m3u found or UPDATE_M3US set to true
[START] ah4c is starting
[ENV] Not loading env
[ENV] IPADDRESS                  192.168.12.18:7654
[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  
[ENV] Creating tuner             1
[ENV] ENCODER1_URL               http://192.168.12.16:8090/stream0
[ENV] TUNER1_IP                  192.168.12.177:5555
[ENV] CMD1                       
[ENV] TEECMD1                    
[ENV] PRE SCRIPT                 ./scripts/firetv/bbc/prebmitune.sh
[ENV] START SCRIPT               ./scripts/firetv/bbc/bmitune.sh
[ENV] STOP SCRIPT                ./scripts/firetv/bbc/stopbmitune.sh
[ENV] REBOOT SCRIPT              ./scripts/firetv/bbc/reboot.sh
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:	export GIN_MODE=release
 - using code:	gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET    /favicon.ico              --> github.com/gin-gonic/gin.(*RouterGroup).StaticFile.func1 (2 handlers)
[GIN-debug] HEAD   /favicon.ico              --> github.com/gin-gonic/gin.(*RouterGroup).StaticFile.func1 (2 handlers)
[GIN-debug] Loaded HTML Templates (11): 
	- index.html
	- routes.html
	- status.html
	- stream.html
	- 
      
	- edit.html
      
	- logs.html
      
	- m3us.html
	- status_and_logs.html
	- config.html
	- editm3u.html
[GIN-debug] GET    /static/*filepath         --> github.com/gin-gonic/gin.(*RouterGroup).createStaticHandler.func1 (2 handlers)
[GIN-debug] HEAD   /static/*filepath         --> github.com/gin-gonic/gin.(*RouterGroup).createStaticHandler.func1 (2 handlers)
[GIN-debug] GET    /                         --> main.run.func1 (2 handlers)
[GIN-debug] GET    /routes                   --> main.run.func2 (2 handlers)
[GIN-debug] GET    /play/tuner:tuner/:channel --> main.run.func3 (2 handlers)
[GIN-debug] GET    /m3u/:channel             --> main.run.func4 (2 handlers)
[GIN-debug] GET    /env                      --> main.run.func5 (2 handlers)
[GIN-debug] GET    /logs/text                --> main.run.func6 (2 handlers)
[GIN-debug] GET    /logs                     --> main.run.func7 (2 handlers)
[GIN-debug] GET    /status/andlogs           --> main.run.func8 (2 handlers)
[GIN-debug] GET    /logs/json                --> main.run.func9 (2 handlers)
[GIN-debug] GET    /video                    --> main.run.func10 (2 handlers)
[GIN-debug] GET    /status                   --> main.statusPageHandler (2 handlers)
[GIN-debug] GET    /api/status               --> main.apiStatusHandler (2 handlers)
[GIN-debug] GET    /stream                   --> main.run.func11 (2 handlers)
[GIN-debug] GET    /test/webhook             --> main.run.func12 (2 handlers)
[GIN-debug] GET    /test/email               --> main.run.func13 (2 handlers)
[GIN-debug] GET    /status/channelsactivity  --> main.run.func14 (2 handlers)
[GIN-debug] GET    /edit                     --> main.run.func15 (2 handlers)
[GIN-debug] POST   /save                     --> main.run.func16 (2 handlers)
[GIN-debug] POST   /m3usave/:file            --> main.run.func17 (2 handlers)
[GIN-debug] GET    /m3us                     --> main.run.func18 (2 handlers)
[GIN-debug] GET    /editm3u/:file            --> main.run.func19 (2 handlers)
[GIN-debug] GET    /config                   --> main.run.func20 (2 handlers)
[GIN-debug] POST   /configsave               --> main.run.func21 (2 handlers)
[START] ah4c is ready
[GIN-debug] Listening and serving HTTP on :7654
> [email protected] start
> node ./index.js
Listening on:
	http://ah4c:8000 http://localhost:8000
	http://127.0.0.1:8000 http://192.168.12.18:8000
	http://[::1]:8000
[GIN-debug] Request: 172.16.1.11 GET /status/andlogs, latency: 4.11206ms, status: 200
[GIN-debug] Request: 172.16.1.11 GET /favicon.ico, latency: 824.386µs, status: 200
[GIN-debug] Request: 172.16.1.11 GET /logs?notime, latency: 2.421034ms, status: 200
[GIN-debug] Request: 172.16.1.11 GET /status, latency: 4.255339ms, status: 200