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

I too started having issues with the NBC app version 9.9.1 and ultimately updated to the latest version. I tried updating to the version that came right after 9.9.1 but unfortunately it seems every version beyond 9.9.1 has the dim screen and logo when it first loads.

HOWEVER, while no direct fix has yet to be implemented into ADBTuner (as far as I know), I figured out how to use Home Assistant to create automation that removes the dim screen and logo when the NBC app opens. To do this, you first need to create a script in Home Assistant (under the Automation and Scenes Category in Settings). Here is the YAML code:

alias: NBC App Dimming
sequence:
  - action: remote.send_command
    metadata: {}
    data:
      num_repeats: 2
      command: DPAD_UP
    target:
      device_id: #this is the ID number for your Android TV Remote Onn device
  - action: remote.send_command
    metadata: {}
    data:
      num_repeats: 2
      delay_secs: 1
      command: DPAD_CENTER
    target:
      device_id: #this is the ID number for your Android TV Remote Onn device
description: >-
  This script will remove the icons at the top of a channel that is playing
  through the NBC app and will also remove the dimness

Once that script is created, you then need to create an automation which can again be found under the Automation and Scenes category under Settings. Here is the YAML code for that:

alias: Run script when NBC app opens
description: ""
triggers:
  - entity_id: media_player.onn_streaming_device_4k_pro_2 #this is the ID Home Assistant gives to your Onn device
    attribute: app_id
    trigger: state
conditions:
  - condition: template
    value_template: "{{ trigger.to_state.attributes.app_id == 'com.nbcuni.nbc.androidtv' }}"
actions:
  - delay: "00:00:15"
  - target:
      entity_id: script.nbc_app_dimming #this is the ID of the first script created
    action: script.turn_on
mode: single

This has been working flawlessly for me. Let me know if you have any questions!

I'm pulling the beta version this morning and hope to have some time to dedicate to testing later this afternoon or evening.

@bnhf , thanks for this guide. I was able to get this setup on my Chromecast. Also note, I had to delete post_playback_start_commands and post_tune_commands for mine to play without going back to the homescreen after 20 seconds of playback.

I pushed an update to development (20251110-4) and beta (2.0-Beta-20251110-1) today.

@bnhf A basic rest api was added for administrative functions. Swagger docs are available at /api/docs .
Be careful with this, there isn't a ton of validation in place outside of enforcing schema.

If anyone runs into any issues, please let me know. Thanks!

Testing the BETA on both my setups, the Onn 4K Plus and my DirecTV Osprey Deep Link (DODL) stacks. Both working as expected.

I don't use the Onn 4K setup too often, Deep Link to the standard DirecTV App, as my Osprey production just works. Onn boxes are my backup. But when testing today the Deep Link would open to the guide, not the channel. This is not BETA related problem as I rolled back to last dev version, same issue. I had to adjust my custom config to get it back to normal. Below for those who want to test.

    "global_options": {
        "wait_for_video_playback_detection": true,
        "use_fixed_delay": true,
        "fixed_delay_seconds": 10,
        "check_for_and_clear_whos_watching_prompts": false,
        "wait_after_post_playback_start_commands_seconds": 6
    },
    "pre_tune_commands": [
        "input keyevent 3"
    ],
    "tune_commands": [
        "am start -W -a android.intent.action.VIEW -S -d '||TARGET_URL_OR_IDENTIFIER||' '||TARGET_PACKAGE_NAME||'"
    ],
    "post_playback_start_commands": [],
    "post_tune_commands": [
        "am force-stop '||TARGET_PACKAGE_NAME||'",
        "input keyevent KEYCODE_HOME"
    ]

As to my DODL setup with Osprey boxes, it always works and is faster to tune with Deep Links. The only quirk is I have to put in a fake target app, one that is installed, to allow the scripting to run. ADBT will not allow target app to be a system app. In the case of Osprey, system package is com.att.tv.openvideo to view channels. Once getting past target app validation on tune with dummy target, my custom config manually targets the the "openvideo" app. I do not use TARGET_PACKAGE_NAME variable in my custom config. Any way to skip the Target Package check for this setup? Of course, not urgent at all as it all works flawlessly, just an OCD thing.

Lastly, I'd be happy to help test and maintain custom configs and channel lists for DirecTV if someone sets up a repository.

Thanks for all the hard work and commitment to this community!

Without those commands, you're probably not going to the live point in any event, which you definitely want to do. Somebody else reported this, and as you'll see I'm suggesting trying multiple fast forwards instead:

Looks like this is going to be great!

Listing configurations worked:

But, listing tuners and channels both failed with 500 errors:

When you have a chance, can you share some docker logs from when this failed?

api/v1/tuners:

2025-11-11 12:19:03.080 - uvicorn.access - 100.98.232.107:0 - "GET /api/docs HTTP/1.1" 200
2025-11-11 12:19:03.589 - uvicorn.access - 100.98.232.107:0 - "GET /openapi.json HTTP/1.1" 200
2025-11-11 12:19:13.126 - uvicorn.access - 100.98.232.107:0 - "GET /api/v1/tuners HTTP/1.1" 500
ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "/usr/local/lib/python3.12/site-packages/uvicorn/protocols/http/httptools_impl.py", line 435, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__
    return await self.app(scope, receive, send)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/fastapi/applications.py", line 284, in __call__
    await super().__call__(scope, receive, send)
  File "/usr/local/lib/python3.12/site-packages/starlette/applications.py", line 122, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/usr/local/lib/python3.12/site-packages/starlette/middleware/errors.py", line 184, in __call__
    raise exc
  File "/usr/local/lib/python3.12/site-packages/starlette/middleware/errors.py", line 162, in __call__
    await self.app(scope, receive, _send)
  File "/usr/local/lib/python3.12/site-packages/starlette/middleware/sessions.py", line 86, in __call__
    await self.app(scope, receive, send_wrapper)
  File "/usr/local/lib/python3.12/site-packages/starlette/middleware/exceptions.py", line 79, in __call__
    raise exc
  File "/usr/local/lib/python3.12/site-packages/starlette/middleware/exceptions.py", line 68, in __call__
    await self.app(scope, receive, sender)
  File "/usr/local/lib/python3.12/site-packages/fastapi/middleware/asyncexitstack.py", line 20, in __call__
    raise e
  File "/usr/local/lib/python3.12/site-packages/fastapi/middleware/asyncexitstack.py", line 17, in __call__
    await self.app(scope, receive, send)
  File "/usr/local/lib/python3.12/site-packages/starlette/routing.py", line 718, in __call__
    await route.handle(scope, receive, send)
  File "/usr/local/lib/python3.12/site-packages/starlette/routing.py", line 276, in handle
    await self.app(scope, receive, send)
  File "/usr/local/lib/python3.12/site-packages/starlette/routing.py", line 66, in app
    response = await func(request)
               ^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/fastapi/routing.py", line 241, in app
    raw_response = await run_endpoint_function(
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/fastapi/routing.py", line 167, in run_endpoint_function
    return await dependant.call(**values)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/app/routers/rest.py", line 114, in list_tuners
    return [TunerSchema.from_orm(tuner) for tuner in tuners]
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "pydantic/main.py", line 585, in pydantic.main.BaseModel.from_orm
pydantic.error_wrappers.ValidationError: 1 validation error for TunerSchema
priority
  value is not a valid integer (type=type_error.integer)

api/v1/channels:

2025-11-11 12:35:38.816 - uvicorn.access - 100.98.232.107:0 - "GET /api/v1/channels HTTP/1.1" 500
ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "/usr/local/lib/python3.12/site-packages/uvicorn/protocols/http/httptools_impl.py", line 435, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__
    return await self.app(scope, receive, send)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/fastapi/applications.py", line 284, in __call__
    await super().__call__(scope, receive, send)
  File "/usr/local/lib/python3.12/site-packages/starlette/applications.py", line 122, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/usr/local/lib/python3.12/site-packages/starlette/middleware/errors.py", line 184, in __call__
    raise exc
  File "/usr/local/lib/python3.12/site-packages/starlette/middleware/errors.py", line 162, in __call__
    await self.app(scope, receive, _send)
  File "/usr/local/lib/python3.12/site-packages/starlette/middleware/sessions.py", line 86, in __call__
    await self.app(scope, receive, send_wrapper)
  File "/usr/local/lib/python3.12/site-packages/starlette/middleware/exceptions.py", line 79, in __call__
    raise exc
  File "/usr/local/lib/python3.12/site-packages/starlette/middleware/exceptions.py", line 68, in __call__
    await self.app(scope, receive, sender)
  File "/usr/local/lib/python3.12/site-packages/fastapi/middleware/asyncexitstack.py", line 20, in __call__
    raise e
  File "/usr/local/lib/python3.12/site-packages/fastapi/middleware/asyncexitstack.py", line 17, in __call__
    await self.app(scope, receive, send)
  File "/usr/local/lib/python3.12/site-packages/starlette/routing.py", line 718, in __call__
    await route.handle(scope, receive, send)
  File "/usr/local/lib/python3.12/site-packages/starlette/routing.py", line 276, in handle
    await self.app(scope, receive, send)
  File "/usr/local/lib/python3.12/site-packages/starlette/routing.py", line 66, in app
    response = await func(request)
               ^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/fastapi/routing.py", line 241, in app
    raw_response = await run_endpoint_function(
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/fastapi/routing.py", line 167, in run_endpoint_function
    return await dependant.call(**values)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/app/routers/rest.py", line 173, in list_channels
    return [ChannelSchema.from_orm(channel) for channel in channels]
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "pydantic/main.py", line 585, in pydantic.main.BaseModel.from_orm
pydantic.error_wrappers.ValidationError: 1 validation error for ChannelSchema
number
  none is not an allowed value (type=type_error.none.not_allowed)

This is fixed in beta (2.0-Beta-20251111-1) and development (20251111-1). The rest api will now set a default value when there are tuners with no priority defined or channels with no number defined.

3 Likes

Can we add some links to the different pages of the webUI? For instance, I know about /channels.m3u, /up, and /configurations. Are there any more pages than this?

It would be nice if there were buttons on the main UI page so that we can easily jump to the other pages. And, buttons on those pages to jump back home to the main page.

Beta (2.0-Beta-20251111-2) and Development (20251111-2) builds were updated to include a navigation menu at the upper right.

I had avoided adding this in the past because I didn't want users to think they had to configure anything beyond what was on the main screen. I may hide this menu behind a "show advanced settings" flag before the next major release.

2 Likes

Navigation menu works great. Agree, not necessary, but handy as I regularly test custom configs.

Also, I seems your dedicated proxy and playback detection changes are making a difference. My Osprey boxes have never tuned so fast, from cold start to channel on an average of 8-10 seconds. Bravo!

1 Like

Small Update:

Beta (2.0-Beta-20251112-2), Development (20251112-2):
Fixed admin UI layout bugs in mobile/tablet sized windows.
Added inverted logo image and used default colors so light/dark mode switching works as expected.

Edit: Additional change:
Beta (2.0-Beta-20251112-3), Development (20251112-3):
Configuration editor now automatically switches to a dark theme when browser is in dark mode.

FYI for everyone. Don't upgrade to docker 29 if you are using portainer. See this post for more information;

Is anyone having their streams buffer on Android TV? I have a Shield, everything is wired and I get random buffering with both of my encoders. The only thing that has fixed is to force transcoding on the DVR server (I'm running in Docker on Unraid with Intel QuickSync).

What settings are you using on your encoder? Encoding type, FPS, GOP, etc? I recently had to reduce the GOP on mine from 60 to 5 to get smooth playback in Multiview on an ATV 4K 1st gen.


This is how I have both of my encoders set! I've been pulling my hair out trying to dial this in right! Thank you for responding :slightly_smiling_face:!

I'd definitely try a GOP of 5, the theory being it reduces the decoding load on the client. You could also try CBR instead of VBR, and lower the Bitrate to 6000. Personally, I was surprised changing the GOP did the trick for me -- but it did, and makes no difference otherwise.

Keeping your bitrate and other settings high, and then turning around and transcoding to get things working right, is clearly not the answer. :slight_smile:

Oh I know it's not the answer!! I just could not figure out a solution. I'll test 5. Thanks so much!!!

1 Like