ADBTuner: A "channel tuning" application for networked Google TV / Android TV devices

I have a quick question. Is there a video encoder that works with HDR since the uray video encoder doesn't do hdr? I have looked everywhere but I can't find a solution. Would a elgato capture card with obs work somehow (I have a good hdmi splitter)?

This is mainly for the atsc 3.0 boxes which forces hdr on nbc 5 and fox 32.

Not at the moment, but I will set something up as I do enjoy a cup of coffee. Thanks.

2 Likes

The development build (20260417-2) was updated with initial support for importing third-party configuration libraries. A bit of a prototype for now, but something to play with, maybe it helps.

I need to expose this to the API, add optional automatic updates, and deal with some edge cases. I should have time for that tomorrow.

An example repository layout is available here:
https://github.com/adbtuner/configuration-repository-example

If there is anything anyone is doing in configurations that seems tedious let me know. I can add better support for repeating commands, custom variables, etc.

2 Likes

Yeah...you may need to use ah4c for those boxes instead. Theres very little HDMI encoders out there that does HDR.

Ah4c CAN do tone-mapping and you can set it up to do that

I love ADBTuner and it's been working solidly for years.

This week I'm trying to get FruitDeepLinks going. I used OliveTin to install FDL and created custom channels for Prime, Paramount, Peacock, and ESPN.

So far I've had little luck getting events to open via ADBTuner and am thrashing...no idea what's going on and beginning to think I have multiple problems. The one app that will open events is Paramount but it gets stuck on the "who's watching?" screen. I've played around a bit with the custom configs but haven't made any progress. And now (to me) the strange part:

All of my testing has been opening programs from the web guide which for paramount lands on the who's watching page:

However I just tried opening the exact same program from the iOS client and it doesn't work at all: just gives a 404 error:

I checked the ADBTuner logs and for the iOS open there are no entries.

When opening via the guide I see the attempt to tune with some errors including a KeyError: 'check_for_and_clear_whos_watching_prompts' (the config is the One Click version for Paramount).

Do these differences in event openings point to a bigger underlying issue?

I include the log snippet below.

2026-04-17 21:33:55.302 - stream - [Tune (GcXUh7UDvfjgxtWfcCT64X)] Using channel configuration: FruitDeepLinks - Parmount+ v4.0 (FireTV and AndroidTV) (62a01c95-94f8-41b7-b56b-4838859ab42d)
2026-04-17 21:33:55.524 - lib.adb - [Tune (GcXUh7UDvfjgxtWfcCT64X)] ADB: 192.168.30.23 - pidof com.cbs.ott
2026-04-17 21:33:55.566 - stream - [Tune (GcXUh7UDvfjgxtWfcCT64X)] Resolving dynamic URL (http://10.167.168.6:6655/api/adb/lanes/pplus/5/deeplink?format=json) for channel.
2026-04-17 21:33:55.606 - stream - [Tune (GcXUh7UDvfjgxtWfcCT64X)] Retrieved dynamic URL data: {'channel_id': 'pplus05', 'channel_name': 'Paramount+ Sports US EBS', 'deeplink': 'pplus://www.paramountplus.com/live-tv/stream/serie-a/811b80b0-6b79-42fb-bb82-5b3f6ec051e1?searchReferral=appleatv&source=spotlight', 'deeplink_format': 'scheme', 'event_end_utc': '2026-04-17T22:25:00+00:00', 'event_start_utc': '2026-04-17T18:45:00+00:00', 'lane_number': 5, 'provider_code': 'pplus', 'start_utc': '2026-04-17T18:45:00+00:00', 'status': 'success', 'stop_utc': '2026-04-17T22:25:00+00:00', 'title': 'Serie A: Internazionale Milano vs. Cagliari Calcio'}
2026-04-17 21:33:55.606 - stream - [Tune (GcXUh7UDvfjgxtWfcCT64X)] Using pplus://www.paramountplus.com/live-tv/stream/serie-a/811b80b0-6b79-42fb-bb82-5b3f6ec051e1?searchReferral=appleatv&source=spotlight to load channel.
2026-04-17 21:33:55.606 - lib.adb - [Tune (GcXUh7UDvfjgxtWfcCT64X)] ADB: 192.168.30.23 - input keyevent KEYCODE_MEDIA_STOP
2026-04-17 21:33:56.603 - lib.adb - [Tune (GcXUh7UDvfjgxtWfcCT64X)] ADB: 192.168.30.23 - am force-stop com.cbs.ott
2026-04-17 21:33:56.689 - lib.adb - [Tune (GcXUh7UDvfjgxtWfcCT64X)] ADB: 192.168.30.23 - am start -W -a android.intent.action.VIEW -d 'pplus://www.paramountplus.com/live-tv/stream/serie-a/811b80b0-6b79-42fb-bb82-5b3f6ec051e1?searchReferral=appleatv&source=spotlight' 'com.cbs.ott'
Exception in thread Thread-37 (background_tune):
Traceback (most recent call last):
  File "/usr/local/lib/python3.12/threading.py", line 1075, in _bootstrap_inner
    self.run()
  File "/usr/local/lib/python3.12/threading.py", line 1012, in run
    self._target(*self._args, **self._kwargs)
  File "/app/app/routers/stream.py", line 193, in background_tune
    if channel_configuration["global_options"][
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
KeyError: 'check_for_and_clear_whos_watching_prompts'
2026-04-17 21:34:00.363 - stream - [Tune GcXUh7UDvfjgxtWfcCT64X] Redirecting to stream after 5.06 seconds (fixed delay of 5 seconds exceeded). Tuning is still in progress.
2026-04-17 21:34:00.363 - uvicorn.access - 138.68.32.225:0 - "GET /stream/210 HTTP/1.1" 307
2026/04/17 21:34:00 [PROXY] 138.68.32.225 -> GET "/proxy/1?requestKey=GcXUh7UDvfjgxtWfcCT64X" -> "http://192.168.30.20/8.ts"
2026-04-17 21:34:18.646 - server - 

--------------------------------------------------
Tuner "FireTV23-TV3" is currently in use and locked.
Tune ID: N/A
Channel: Paramount+ 5
Lock Obtained: 2026-04-17 21:33:55 (0.39 minutes ago)
Last Seen: N/A
Bytes Transferred: None
Remote User Agent:
N/A
--------------------------------------------------
2026-04-17 21:34:48.675 - server - 

--------------------------------------------------
Tuner "FireTV23-TV3" is currently in use and locked.
Tune ID: N/A
Channel: Paramount+ 5
Lock Obtained: 2026-04-17 21:33:55 (0.89 minutes ago)
Last Seen: N/A
Bytes Transferred: None
Remote User Agent:
N/A
--------------------------------------------------
2026-04-17 21:34:56.556 - tuner_management - Releasing tuner 1 as it is untracked and has been locked for over a minute.
2026-04-17 21:34:56.556 - tuner_management - Released tuner (1).

Can you try the default configuration for those Paramount channels? It looks like the configuration you are using might be out of date or corrupt as it's missing a field

As do I. I'm actually a bit of an aficionado, but if you do, please ping me. I would like to donate.

1 Like

I'm actually using the default Paramount config created by OliveTin. Have recreated (and chosen to remove old one) a few times. Here's the config as shown in ADB editor:

{
    "name": "FruitDeepLinks - Parmount+ v4.0 (FireTV and AndroidTV)",
    "author": "bnhf",
    "version": "4.0",
    "description": "Paramount+ for FruitDeepLinks. Compatible with FireTV and AndroidTV devices.",
    "uuid": "62a01c95-94f8-41b7-b56b-4838859ab42d",
    "global_options": {
        "wait_for_video_playback_detection": false,
        "use_fixed_delay": true,
        "fixed_delay_seconds": 5,
        "wait_after_post_playback_start_commands_seconds": 0
    },
    "pre_tune_commands": [
        "input keyevent KEYCODE_MEDIA_STOP",
        "am force-stop ||TARGET_PACKAGE_NAME||"
    ],
    "tune_commands": [
        "am start -W -a android.intent.action.VIEW -d '||TARGET_URL_OR_IDENTIFIER||' '||TARGET_PACKAGE_NAME||'"
    ],
    "tune_match_text_commands": [
        {
            "match_text": [
                "who's watching",
                "edit profiles"
            ],
            "commands": [
                "input keyevent KEYCODE_DPAD_CENTER"
            ],
            "check_after_seconds": 5
        }
    ],
    "post_playback_start_commands": [],
    "post_tune_commands": [
        "input keyevent KEYCODE_HOME",
        "am force-stop ||TARGET_PACKAGE_NAME||"
    ],
    "timed_keep_active_commands": []
}

In case it matters: I have two different ADBTuner instances running atm.

  • The original one which is on a Synology and everything still works fine.
  • The new one running in Docker/Portainer on the Mac with bare metal DVR

This doesn't seem like it should cause an issue but wanted to point it out just in case

1 Like

I'm not in a position to confirm this atm, but I believe you still need to use the version of ADBTuner with the development tag for the custom configs to work.

1 Like

Pulled the development version and now everything opens.

Yes: Development version is still needed!

Do you know what tag you were using prior to switching to development? The current stable version (20251228-2) (turtletank99/adbtuner:stable) should also work fine with that configuration.

Pretty sure i was using latest and that it was set by OliveTin (I was using the automation as much as possible and it was awesome!)

I found an issue where when I would reboot my encoder, if not using wait_for_video_playback_detection and just letting the stream kind of start, which is sort of the correct way to do it, I find with Osprey boxes because they just go right into a stream, the initial attempt will fail.

I observed this with ah4c and also ADBTuner. I opened a PR for ah4c that I confirm does fix it and make tuning so much more reliable and allows me to tune within maybe 3 to 4 seconds from a sleeping Osprey box with no hiccups. I just wanted to highlight it here for @turtletank since ADBTuner has the same race condition. Essentially, the mental logic is if video data isn't flowing pretty much immediately, we send TS NULL packets until video data starts flowing. If video data is flowing immediately, we just send the video data. This keeps Channels DVR warm and waiting for video data to flow through and allows the encoder to spin up. It's effectively a no-op if your encoder is already ready to go, and if it's not, it adds maybe another second instead of a failed tune.

I observed this with all four of my LinkPi units, so I know it's not just me. Everything's wired in my setup throughout my apartment, so I know it's not a networking issue or anything like that. I just think it's a weird race condition that probably would only surface with an Osprey setup where one can tune really, really quickly.

This is obviously implemented in Go and not Python, but hopefully it is helpful as something that can maybe be ported.

The reproduction steps are basically set your delay to absolutely nothing in your config, so streaming starts immediately. Reboot a LinkPi encoder and attempt to stream, and it will fail immediately, and then you try to stream again, and it succeeds.

I understand and sympathize with what it can be like! ADBTuner is my preferred capture method, too, because I find it so intuitive and clean. I'm going to add you to a PM offline to discuss what we're up to, but to be clear, I'm perfectly happy working within whatever confines you or ADBTuner might have.

1 Like

The development build (20260418-5) was updated to add api endpoints for managing remote repositories, optional automatic repository updates, improved duplicate configuration UUID handling, and a new dialog for selecting configurations (the dropdown was getting cumbersome with a large list of configurations).

1 Like

Just in case it's helpful to anyone, I updated my PR for ah4c to add a 15-second timeout with basically 3 retries to prevent an infinite loop. I'm hoping that this fix can be adapted into ADBTuner because I would prefer to use ADBTuner long term, but obviously I have to use what's going to work best. So if I have to fork ah4c or hopefully they merge my PR or ADBTuner implements this solution. Just figured I'd mention it here just to share the implementation in Go to see how it can possibly be potentially ported to Python.

With my shiny new ADBTuner setup fed by FDL I was able to setup a recording of the Warriors/Suns game last night. When I watched it this morning it was 3 hours of screen saver.

The tuner got me to the game but the custom config for Prime (FruitDeepLinks - Prime Video v3.0) didn't click the "Watch" button. I've pasted screenshot of recording and custom config below.

(Just realized I have a second issue which is the screenshot is for the prior game...the FDL start time was 30 minutes earlier than the actual game)

Anyway...I'm new to these configs and wondering how to tweak the custom config so it gets past the watch screen. In this case is it as simple as adding a new text snipped "watch" to the "match_text" blob?

{
    "name": "FruitDeepLinks - Prime Video v3.0 (FireTV and AndroidTV)",
    "author": "bnhf",
    "version": "3.0",
    "description": "Prime Video for FruitDeepLinks. Compatible with FireTV and AndroidTV devices.",
    "uuid": "a176643f-b3b6-47fb-8527-47908d089695",
    "global_options": {
        "wait_for_video_playback_detection": false,
        "use_fixed_delay": true,
        "fixed_delay_seconds": 5,
        "wait_after_post_playback_start_commands_seconds": 0
    },
    "pre_tune_commands": [
        "input keyevent KEYCODE_MEDIA_STOP",
        "input keyevent KEYCODE_HOME"
    ],
    "tune_commands": [
        "am start -n $(dumpsys package '||TARGET_PACKAGE_NAME||' 2>/dev/null | awk '/https:/{getline; print $2; exit}') -d '||TARGET_URL_OR_IDENTIFIER||'"
    ],
    "tune_match_text_commands": [
        {
            "match_text": [
                "who's watching",
                "watch live",
                "new",
                "guest"
            ],
            "commands": [
                "input keyevent KEYCODE_DPAD_CENTER"
            ],
            "check_after_seconds": 5
        }
    ],
    "post_playback_start_commands": [],
    "post_tune_commands": [
        "input keyevent KEYCODE_MEDIA_STOP",
        "input keyevent KEYCODE_HOME"
    ],
    "timed_keep_active_commands": []
}

Getting some data here, after a reboot, my LinkPi ENC5V2 starts streaming video at the ~20 second mark. However, that video drops out at ~40 seconds and doesn't return until ~80 seconds. So even if we do wait until the stream is available that just gets us ~20 seconds of video before it drops out and the tuning session is torn down. Does that match what you are seeing?

Edit: so you are suggesting serving null data during that downtime in hopes it comes back up and the stream can resume?

So what will happen in my case after a reboot, my stream will basically die immediately because it's waking my Osprey box. Then I'll go and press play to continue, and it'll stream just fine.

The idea is to have a config similar to this, because the Osprey box just wakes right to video. There's no tuning process to watch. It just wakes and tunes right away.

{
    "name": "Osprey",
    "author": "David B.",
    "version": "1.0.0",
    "description": "Tuning of Osprey Hardware.",
    "uuid": "39734252-cf01-443e-97ac-e53d5220f40f",
    "global_options": {
        "wait_for_video_playback_detection": false,
        "use_fixed_delay": true,
        "fixed_delay_seconds": 0,
        "check_for_and_clear_whos_watching_prompts": false
    },
    "pre_tune_commands": [
        "input keyevent 224"
    ],
    "tune_commands": [
        "am start -a android.intent.action.VIEW -d '||TARGET_URL_OR_IDENTIFIER||' ||TARGET_PACKAGE_NAME||"
    ],
    "post_tune_commands": [
        "input keyevent 26"
    ],
    "timed_keep_active_commands": [
        {
            "run_every_minutes": 60,
            "commands": [
                "input keyevent KEYCODE_MEDIA_PLAY"
            ]
        }
    ]
}

What my solution was with ah4c was to insert null packets so Channels DVR doesn't think that there's no video data coming in allowing a retry for recovery. But I'm not sure if you have a better solution for ADBTuner.

So the way the tuning process with ah4c works is it just wakes, tunes, and that's it. There's no delay or anything like that with the Osprey boxes. It's really nice because you have video up in 4 seconds. The problem is if you had just rebooted your encoder, it's not going to play. It'll error out and you have to play it again. So my solution was to insult that null data while it's coming up essentially. My implementation in Go worked perfectly. Not sure if that can be used as sort of a reference for ADBTuner. I know this is kind of like an edge case, but it does definitely happen when you are basically tuning with zero delay.

So essentially I'll get the "connection to the tuner was lost, press play to try again." That's basically the error that comes up.