Just confirmed trUe works LOL.
Haven't had any issue using your fork with YTTV. I had to modify the script since its long been updated (for those who need it)
bmitune.sh
#!/bin/bash
#bmitune.sh for android/yttv
ADB_CMD="adb -s $2 shell"
CHANNEL=\""$1\""
APP_LAUNCH="com.google.android.youtube.tvunplugged"
APP_NAME="com.google.android.apps.youtube.tvunplugged.activity.MainActivity"
#Send the command
if $ADB_CMD pm list packages | grep -q "^package:${APP_LAUNCH}$"; then
$ADB_CMD am start -a android.intent.action.VIEW \
-d "https://tv.youtube.com/watch/${CHANNEL}" \
-n "${APP_LAUNCH}/${APP_NAME}"
else
$ADB_CMD am start -a android.intent.action.VIEW \
-d "https://tv.youtube.com/watch/${CHANNEL}"
fi
stopbmitune.sh
#!/bin/bash
#stopbmitune.sh for android/yttv
IPADD="$1"
ADB_CMD="adb -s $1 shell"
HOME="input keyevent KEYCODE_HOME"
adb -s $IPADD shell input keyevent 86
$ADB_CMD $HOME
But yeah, I have success
So it just went right to the channel. You didn't have any issues with it displaying YouTube TV's really loud splash screen before you tuned? Also, I just want to make sure that would show up on your encoder, right? Like if you killed the app first?
No. If you don't kill the app, you rarely see it anyways. I used YTTV with apituner and adbtuner and you only see it once in a blue moon but channel tuning is relatively quick after the app launches for the first time (just don't kill, just send the device to the home screen)
Yeah, I just wanted to make sure if the app cold started, you wouldn't see it. That was my main concern.
Just because of the way this works, like it's looking for bitrate. And doing its best to detect actual movement versus like a static splash screen. And YouTube TV's splash screen is very active. So it's like the worst case scenario for it breaking through the gate. Also, thank you so much for testing.
33 posts were split to a new topic: Confused and Lost. Need help understanding how various community projects work, and are used with Channels DVR
I updated my pull request since I think the playback detection feature is working quite well. I also added a playback delay feature. This uses the standard ffmpeg -i <encoder url> -ss <seconds> -c:v copy -c:a copy -f mpegts and an env var PLAYBACK_DELAY which accepts a number. Unset or 5s will do nothing. It working between 2 and 30 seconds (due to keyframe and CDVR limitation respectfully) anything outside of this range will snap back to the nearest value in the range. I tried to implement this natively, but it was just not going well. I spent way too much time on it and realized the FFmpeg approach would be much more solid.
Claude also (possibly) found a bug but that fix should be reviewed more closely. I included it in a separate commit for due diligence.
When I get some energy, I'm considering looking into maybe modernizing the UI for ah4c. Considering the stance with ADBTuner and it not going to support scripting at all, whatsoever, I think ah4c is going to continue to have a place, and maybe getting a modern UI would help usability.
I was thinking of just maybe starting off by updating style sheets and that sort of thing. Times New Roman doesn't look so pretty, for example. 
Just don't want to go ahead and work on anything on a fork unless it's something that'll be considered. I don't want to waste time.
That's certainly an interesting idea. ah4c was originally built with no UI, and what's there is something of an afterthought. The other thing to consider is that the UI is entirely optional at this point -- and personally, I wouldn't want that to change.
BTW, I recently published some scripts under scripts/all/all that are designed to be proxies for anyone that wants to use multiple providers. I haven't documented it yet, but basically the format in the M3U for the tuning URL would be:
http://{{ .IPADDRESS }}/play/tuner/<device>~<provider>~KTVK~f8803ee2-28ec-4b62-9c22-8243f7d06ef9
Where <device> and <provider> would be the exact names used for the script directories. And, as you can see, for something like callsign~UUID we already use for DTV deeplinks -- that will continue to work fine.
So in short when someone uses scripts/all/all, their M3U will need to include device and provider, and the proxy scripts will hand off tuning to the appropriate scripts for that combo of device and provider.
This is totally untested, but should work in theory, and I'm mentioning it because it would/could affect any UI work -- especially the M3U Editor.
Just the fonts are a little dated. Really, just want to go in there and see what tuners are locked, what tuners are available, that sort of thing. I also think it can use some cleanup, like for new users, maybe the M3U editor could be actually a more useful thing that can be improved.
I also think the logs pile up really quickly, and on one of my machines, it actually kind of locks up the browser sometimes. Also adding a link to ws-scrcpy in the main web UI might be useful. Just thinking little enhancements and maybe like a styling update. AI makes making pretty user interfaces so easy these days with zero effort. 
Also let me know when you publish, I'll have to test those scripts out!
I made ah4c pretty if anyone wants to test: ghcr.io/mackid1993/ah4c:latest drops right in with no config changes!
New features:
- Control the device while also playing DRM content (built into video player and basic directional controls plus sleep, wake and reboot)
- Wake, Sleep, and Reboot all devices
- Clean modern UI
- Mobile responsive
- Log pruning so the log in the UI doesn't get crazy long
- ws-scrcpy can now be accessed from within ah4c making it less hidden for new users
- Just made everything pretty and 2026 looking throughout
- Both dark and light themes
Some screenshots to preview:
@bnhf really looking for your input! Code is here: GitHub - mackid1993/ah4c at ui-refactor · GitHub
Screenshots look very good! It'll likely be Saturday before I can take a closer look at this...
Thanks so much!
I've had a few things cooking and they've all landed together, so here's one post covering the lot instead of scattering it across the thread.
Two big pieces: a full redesign of the web UI, and native closed captions for HDMI encoders. Plus two new environment variables.
The web UI got a proper redesign
The old UI worked but it was showing its age. Everything now shares one stylesheet and one design, and every page has the same header bar and navigation so you're never guessing where you are.
What's new:
- Light and dark themes with a toggle on every page. It remembers your choice.
- Tuner cards on the Activity page — each tuner shows its device, its encoder, whether it's streaming, what channel it's on and how long it's been tuned.
- Device controls built in. Send remote keys straight from the tuner card, or reboot a device. There's a fan-out that hits every device at once, and it skips any tuner that's currently streaming so you can't kill a recording by accident.
- Live preview. Click a tuner and you get its actual HDMI output in a modal. Handy for seeing what state a Fire Stick has got itself into without walking over to the TV. DRM-protected apps won't show, that's expected.
- ws-scrcpy is proxied through the UI on the same origin, so there's no second port to open or configure. Just click Device Control.
- Release lock button on any tuner holding a stuck lock — runs the stop script and frees it.
- Log viewer rewritten so it appends new lines instead of rebuilding the whole log every second. It's colorized, capped, and doesn't hammer the browser anymore.
- Everything works on a phone. The nav collapses behind a menu button, the split log view stacks, and addresses stop wrapping in ugly places.
- Two stored XSS holes closed — one in the log viewer, one in the DVR activity feed. Log text and activity are escaped now.
- Polling pauses when the tab is hidden, so it isn't burning CPU in a background tab.
The two environment variables
PLAYBACK_DETECTION
Set it to TRUE and ah4c holds the stream until the device is actually playing — it checks the device reports media audio, confirms the picture is genuinely moving, and then starts on a keyframe.
The problem it solves: your recording starts on the app's loading screen or a spinner, and you lose the first few seconds of the show. This waits for real playback instead of guessing.
Needs adb access to the tuner. Network tuners only. Case-insensitive, so true/True/TRUE all work; anything else leaves it off.
PLAYBACK_DETECTION=TRUE
PLAYBACK_DELAY
The simpler version of the same idea, for when you don't have adb or don't want the detection. (pyatv for example) Give it a whole number of seconds and it skips that much off the front of every tune.
It runs through the bundled ffmpeg with -ss and a stream copy, so nothing is re-encoded. The skip lands on the next keyframe, so it can run slightly past the number you gave it.
The value is the total tune time including your scripts, not extra time on top. Range is 2 to 30 — the DVR only allows about 30 seconds for a tune, so anything outside gets clamped and logged. Ignored entirely if PLAYBACK_DETECTION is on. Network tuners only.
PLAYBACK_DELAY=8
Closed captions
This is the big one. HDMI encoders now get real closed captions.
Streaming apps hand the encoder a picture with the captions already stripped off, so everything downstream of ah4c has had nothing to display. Not anymore.
ah4c pulls the audio out of the encoder's transport stream, transcribes it on the CPU with an NVIDIA Parakeet model, and writes the text back into the video as CEA-608 caption data in ATSC A/53 user data. That is the exact same carriage an HDHomeRun uses for over-the-air captions, which is why the Channels DVR closed captions button just works on it.
To be clear about what this is not:
- Not burned into the picture. They're a real caption track you turn on and off.
- Not a sidecar .srt file. Nothing extra to manage.
- Not a re-encode. The compressed video is passed through byte for byte. Only a small caption message is inserted ahead of each picture. Quality, bitrate and tune time are all unchanged.
- Not a GPU job. It runs several times faster than real time on an ordinary CPU.
Captions land about a second and a half behind the audio, which is roughly where real broadcast captioning sits.
It's completely opt-in. With captions off, a tune takes exactly the path it always did.
Setup: one new volume
Everything the feature needs is downloaded on demand and has to live somewhere on your server. Add this to the ah4c service:
- ${HOST_DIR}/ah4c/captions:/opt/captions
That's the whole setup. It sits next to your existing scripts, m3u and adb folders, and stays completely empty unless you switch captions on. ah4c checks for it at startup, warns in the log, and puts a banner at the top of the Closed Captions page if it's missing, so nobody downloads a gigabyte and loses it on the next compose down.
No Dockerfile changes. No environment variables for any of the caption settings — it's all managed from the page and stored in a config.json in that folder.
Picking a model
Nothing is baked into the image. You pick a model on the page and press download, and the URL it's pulling from is printed right there so you can see exactly what it's fetching and from where.
| Model | Delay | Size | Languages | Runs well on |
|---|---|---|---|---|
| Nemotron 3.5 Streaming 0.6B (default) | ~1s | 938 MB | 25 | A normal multi-core CPU |
| Parakeet Realtime 120M | ~1s | 168 MB | English | A potato. NAS, mini PC, Pi |
| Parakeet TDT 0.6B v3 | 3–4s | 897 MB | 25 | A normal multi-core CPU |
| Parakeet TDT-CTC 110M | 3–4s | 170 MB | English | Modest hardware |
The two streaming models transcribe continuously as audio arrives. The other two wait for a whole phrase before starting, which is a bit more accurate but you feel the extra seconds.
Worth knowing: the 120M doesn't do punctuation at all — that's not a setting I missed, NVIDIA's model card says outright that it outputs none. So the default is the Nemotron streaming model, which is the only one that's both real-time and punctuated. You get proper sentences with periods and commas.
That Nemotron model covers 25 languages, so you can watch in one language and caption in another if you want.
Accuracy is around 2.4% word error rate, which is a couple of errors per eighty-odd words. Broadcast ASR captioning typically runs 5–10%.
Other caption settings
- Capitals, on by default. That's the broadcast standard and it's easier to read across a room. There's a setting for mixed case.
- Roll-up style — 2, 3 or 4 lines.
- Language — pin one or let it detect.
- Per-tuner — leave all unchecked to caption everything, or pick specific tuners.
- Extra delay if you want to nudge the sync.
GPU acceleration (completely optional)
You do not need this. On CPU a single stream uses a fraction of a core.
But if you're like me and have a stack of tuners that can all be going at once, ten streams transcribing continuously is real heat and real power draw, and at that point it's worth pushing onto a GPU.
There's an engine picker on the page. It greys out anything your container can't actually load, so you're not guessing, and it shows a green banner naming the device when acceleration is genuinely running.
Intel or AMD (Vulkan):
GPU_DEVICE=/dev/dri:/dev/dri
The Vulkan driver isn't in the image, but the page downloads it the same way it downloads a model — into your bind mount — and puts it back automatically after a rebuild so you don't think about it again.
NVIDIA (CUDA):
DOCKER_RUNTIME=nvidia
NVIDIA_VISIBLE_DEVICES=all
NVIDIA_DRIVER_CAPABILITIES=compute,utility
Nothing to install for this one — the CUDA build brings its own runtime and the NVIDIA container toolkit hands in the driver.
Both default to off, and I made sure that's genuinely safe. GPU_DEVICE defaults to /dev/null, which exists on every machine and does nothing, so if you don't have a GPU you change nothing and nothing breaks. No commented-out YAML to uncomment.
One thing that catches people: passing /dev/dri through is a separate step from installing the driver. The page tells you which one you're missing rather than just failing silently.
And Quick Sync doesn't help here, before anyone asks. I looked into it. Quick Sync is fixed-function video encode/decode silicon, not a general compute unit, so there's nothing for a speech model to run on. The VA-API packages already in the image are for video and are unrelated. Vulkan is the actual path for an Intel iGPU.
Example compose
services:
ah4c:
image: bnhf/ah4c:${TAG}
container_name: ah4c
hostname: ah4c
dns_search: ${DOMAIN}
ports:
- ${ADBS_PORT}:5037
- ${HOST_PORT}:7654
- ${WSCR_PORT}:8000
environment:
- IPADDRESS=${IPADDRESS}
- NUMBER_TUNERS=${NUMBER_TUNERS}
- TUNER1_IP=${TUNER1_IP}
- TUNER2_IP=${TUNER2_IP}
- TUNER3_IP=${TUNER3_IP}
- TUNER4_IP=${TUNER4_IP}
- TUNER5_IP=${TUNER5_IP}
- ENCODER1_URL=${ENCODER1_URL}
- ENCODER2_URL=${ENCODER2_URL}
- ENCODER3_URL=${ENCODER3_URL}
- ENCODER4_URL=${ENCODER4_URL}
- ENCODER5_URL=${ENCODER5_URL}
- STREAMER_APP=${STREAMER_APP}
- CHANNELSIP=${CHANNELSIP}
- ALERT_SMTP_SERVER=${ALERT_SMTP_SERVER}
- ALERT_AUTH_SERVER=${ALERT_AUTH_SERVER}
- ALERT_EMAIL_FROM=${ALERT_EMAIL_FROM}
- ALERT_EMAIL_PASS=${ALERT_EMAIL_PASS}
- ALERT_EMAIL_TO=${ALERT_EMAIL_TO}
- UPDATE_SCRIPTS=${UPDATE_SCRIPTS}
- UPDATE_M3US=${UPDATE_M3US}
- TZ=${TZ}
- SPEED_MODE=${SPEED_MODE}
- KEEP_WATCHING=${KEEP_WATCHING}
- NULL_FRAME_INSERTION=${NULL_FRAME_INSERTION}
- PLAYBACK_DETECTION=${PLAYBACK_DETECTION}
- PLAYBACK_DELAY=${PLAYBACK_DELAY}
- HEARTBEAT_INTERVAL=${HEARTBEAT_INTERVAL}
- NVIDIA_VISIBLE_DEVICES=${NVIDIA_VISIBLE_DEVICES}
- NVIDIA_DRIVER_CAPABILITIES=${NVIDIA_DRIVER_CAPABILITIES}
volumes:
- ${HOST_DIR}/ah4c/scripts:/opt/scripts
- ${HOST_DIR}/ah4c/m3u:/opt/m3u
- ${HOST_DIR}/ah4c/adb:/root/.android
- ${HOST_DIR}/ah4c/captions:/opt/captions
devices:
- ${GPU_DEVICE}
runtime: ${DOCKER_RUNTIME}
restart: unless-stopped
Example env
TAG=beta
DOMAIN=localdomain
ADBS_PORT=5037
HOST_PORT=7654
SCRC_PORT=7655
IPADDRESS=yourserver:7654
NUMBER_TUNERS=5
TUNER1_IP=firestick-rack1:5555
ENCODER1_URL=http://encoder_48007/0.ts
TUNER2_IP=firestick-rack2:5555
ENCODER2_URL=http://encoder_48007/4.ts
TUNER3_IP=firestick-rack3:5555
ENCODER3_URL=http://encoder_48007/8.ts
TUNER4_IP=firestick-rack4:5555
ENCODER4_URL=http://encoder_48007/12.ts
TUNER5_IP=firestick-travel2:5555
ENCODER5_URL=http://encoder_23393/0.ts
STREAMER_APP=scripts/firetv/dtvdeeplinks
CHANNELSIP=your-channels-server
ALERT_SMTP_SERVER=smtp.gmail.com:587
ALERT_AUTH_SERVER=smtp.gmail.com
[email protected]
ALERT_EMAIL_PASS=your-app-password
[email protected]
UPDATE_SCRIPTS=true
UPDATE_M3US=true
TZ=US/Mountain
SPEED_MODE=false
KEEP_WATCHING=4h
NULL_FRAME_INSERTION=FALSE
PLAYBACK_DETECTION=FALSE
PLAYBACK_DELAY=0
HEARTBEAT_INTERVAL=0
HOST_DIR=/data
# GPU acceleration for closed captions. Both default to off and do nothing.
GPU_DEVICE=/dev/null:/dev/null
DOCKER_RUNTIME=runc
NVIDIA_VISIBLE_DEVICES=
NVIDIA_DRIVER_CAPABILITIES=
A couple of notes
Infuse and Plex won't show the captions. That's not us — it's a known limitation where they don't read in-band captions from MPEG-TS at all. People recording off real HDHomeRuns hit exactly the same thing and there are open feature requests about it. Channels reads them fine, which is the point.
VLC needs you to pick the track. VLC finds the captions but never auto-enables them. Subtitle → Sub Track → Closed Captions 1.
Captions are still beta. The transport-stream side has had a hard going-over and I'm happy with it, but I'll keep iterating if people hit issues. Keep in mind STT models aren't perfect and there will be slight mistakes or typos.
- I'm more worried about stream corruption if anyone hits that ping me.
- Any weirdness beyond transcription errors. Especially in recordings.
Happy to answer questions on any of it.
How to get the new bits?
Pull bnhf/ah4c:beta
For captions don't forget to set the bind mount and GPU paths if desired. If you do have an iGPU I do suggest it to cut down on CPU usage and heat.
It would really cool to test some of these new features like the closed caption functionality with apple tv based tuners on apple silicon based machines.
@mackid1993 is your build , especially the closed caption functionality , capable of running on Apple Silicon based machines - in docker. The closed capture feature addition for encoder based streams is awesome.
@bnhf any chance of doing the merge you mentioned several weeks ago of the ah4c:apple TV branch into the main ah4c branch like you were thinking earlier?
Would this mean the underlying VM OS that @mackid1993 is using in his beta is the same as the one used for the ah4c apple tv folks? Or would the extra new env variable you mentioned preclude using these new beta features?
Happy to lend a hand testing.
The model should run on ARM64 under Linux/Docker. It just depends on whether @bnhf released an ARM64 build. I think this will probably stay on its own sort of branch for now, away from Apple TV until we get it 100% stable with Android TV.
That's up to the core maintainers though, not really me. I have an Apple TV to watch TV on, but I don't have an Apple TV to use as a tuner. I also don't know what you mean by VM OS. This is just a Docker container. There's no VM involved. You know, besides the Docker environment, I think there's a misunderstanding here.
In terms of the actual closed captions, that's more dependent on the encoder than the tuner. It's slipstreaming into the MPEG TS stream. It doesn't really have anything to do with what tuner is used.
Otherwise the only issue you're going to have running on an Apple device is going to be running on the CPU. Unless dev/dri works on Apple.
I would really suggest running this under Linux. I really am someone that doesn't recommend running Docker on macOS ever. But if you have to, I'm not 100% sure GPU acceleration certain will work.
Edit: There is an ARM64 image, so I would just give it a shot and see if it works.
I still want to do that. I'll see if I can build a combined image tomorrow under the beta tag.
Before you rebuild, I'm experimenting with some more models right now. I'm looking to see what else is out there on Hugging Face that might be more accurate. The ones we have are really lightweight and fast, but for those with like powerful systems, there might be better options.
I'm actually adding my personal favorite model that I'm actually talking into right now, Cohere Transcribe. It's crazy fast and just works super well.
Update:
Just fixing some bugs, but Cohere Transcribe is next level and runs great on my 12700K iGPU. I'm going to mark it as GPU only because I can't see how it could possibly run on a CPU without cooking the damn thing, but this is going to be a good one. It really looks like broadcast quality transcription.



















