> For the complete documentation index, see [llms.txt](https://docs.webtonative.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.webtonative.com/javascript-apis/android-tv.md).

# Android TV

WebToNative's Smart TV Support

WebToNative's Support for TV add-on lets your website publish content to the Android TV home screen: a **Watch Next** row (the "Continue Watching" row Android TV shows near the top of the launcher, with a progress bar drawn from how far the user got into a video) and your own **recommendation channel** of programs the user can browse and launch straight into your app.

Your website calls these functions whenever it knows something worth surfacing — e.g. every time playback position updates, or once you've built a catalog of recommended content.

> You'll need to import the javascript file in your website before starting from this [link](https://docs.webtonative.com/javascript-apis/getting-started).

> **Platform support:** Android only.

***

## Setting Up Smart TV Support

1. Go to your **WebToNative dashboard** → **Add-ons** → **Smart TV Support** and enable it.
2. No further credentials are required.

{% hint style="info" %}
Every function below is a no-op (and reports `errorCode: "NOT_SUPPORTED"` — see [Callback Response Format](#callback-response-format)) unless the add-on is enabled **and** the app is running on a device the SDK detects as Android TV. Call [isSupported](#issupported) first if you want to confirm before building your Watch Next / channel calls into your site's playback logic.
{% endhint %}

***

## JavaScript API Reference

### isSupported

Checks whether Smart TV features are available right now (add-on enabled and running on an Android TV device).

{% tabs %}
{% tab title="Plain Javascript" %}

```javascript
window.WTN.SmartTV.isSupported({
  callback: function (response) {
    console.log(response.isSupported);
  },
});
```

{% endtab %}

{% tab title="npm" %}

```javascript
import { isSupported } from "webtonative/SmartTV";

isSupported({
  callback: (response) => {
    console.log(response.isSupported);
  },
});
```

{% endtab %}
{% endtabs %}

**Parameters:**

| Key        | Type       | Required | Description                                  |
| ---------- | ---------- | -------- | -------------------------------------------- |
| `callback` | `Function` | No       | Callback function invoked with the response. |

**Callback Response:**

| Key           | Type      | Description                                                           |
| ------------- | --------- | --------------------------------------------------------------------- |
| `type`        | `String`  | Always `"smartTvIsSupported"`.                                        |
| `success`     | `Boolean` | Always `true` for this call.                                          |
| `isSupported` | `Boolean` | `true` if Watch Next / channel calls will do anything on this device. |

***

### watchNextUpsert

Adds a new item to the Android TV **Watch Next** row, or updates one already there (matched by `contentId`). This is what drives the "Continue Watching" progress bar shown on the card — Android TV computes the progress fraction itself from `lastPlaybackPositionMs` / `durationMs`, there's no separate percentage field to send.

The common pattern is to call this periodically while the user watches (e.g. on your video element's `timeupdate` event, throttled), not just once:

{% tabs %}
{% tab title="Plain Javascript" %}

```javascript
const video = document.querySelector("video");

function syncWatchNext() {
  window.WTN.SmartTV.watchNextUpsert({
    contentId: "movie-123",
    title: "The Great Adventure",
    playbackUri: "https://example.com/watch/movie-123",
    description: "A thrilling journey across the mountains.",
    posterArtUri: "https://example.com/posters/movie-123.jpg",
    durationMs: Math.round(video.duration * 1000),
    lastPlaybackPositionMs: Math.round(video.currentTime * 1000),
    type: "MOVIE",
    callback: function (response) {
      if (!response.success) {
        console.warn("Watch Next update failed:", response.errorCode, response.message);
      }
    },
  });
}

let lastSyncMs = 0;
video.addEventListener("timeupdate", function () {
  const now = Date.now();
  if (now - lastSyncMs > 15000) { // throttle to every 15s
    lastSyncMs = now;
    syncWatchNext();
  }
});
```

{% endtab %}

{% tab title="npm" %}

```javascript
import { watchNextUpsert } from "webtonative/SmartTV";

watchNextUpsert({
  contentId: "movie-123",
  title: "The Great Adventure",
  playbackUri: "https://example.com/watch/movie-123",
  description: "A thrilling journey across the mountains.",
  posterArtUri: "https://example.com/posters/movie-123.jpg",
  durationMs: Math.round(video.duration * 1000),
  lastPlaybackPositionMs: Math.round(video.currentTime * 1000),
  type: "MOVIE",
  callback: (response) => {
    if (!response.success) {
      console.warn("Watch Next update failed:", response.errorCode, response.message);
    }
  },
});
```

{% endtab %}
{% endtabs %}

**Parameters:**

| Key                      | Type       | Required | Description                                                                                                                          |
| ------------------------ | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `contentId`              | `String`   | **Yes**  | Stable identifier for this content. Calling again with the same `contentId` updates the existing card instead of creating a new one. |
| `title`                  | `String`   | **Yes**  | Title shown on the card.                                                                                                             |
| `playbackUri`            | `String`   | **Yes**  | Deep link opened when the user selects the card. **Must be `https://`** — an `http://` URL is rejected.                              |
| `description`            | `String`   | No       | Description text shown for the card.                                                                                                 |
| `posterArtUri`           | `String`   | No       | Poster/thumbnail image URL. **Must be `https://`** if provided.                                                                      |
| `durationMs`             | `Number`   | No       | Total content duration in milliseconds. Defaults to `0`. Drives the progress bar together with `lastPlaybackPositionMs`.             |
| `lastPlaybackPositionMs` | `Number`   | No       | Current playback position in milliseconds. Defaults to `0`.                                                                          |
| `type`                   | `String`   | No       | One of `"MOVIE"`, `"TV_EPISODE"`, `"CLIP"`. Defaults to `"MOVIE"` if omitted or unrecognized.                                        |
| `callback`               | `Function` | No       | Callback function invoked with the response.                                                                                         |

**Callback Response:** see [Callback Response Format](#callback-response-format).

{% hint style="warning" %}
`contentId`, `title`, and `playbackUri` are required — the call fails with `errorCode: "INVALID_PAYLOAD"` if any is missing.
{% endhint %}

{% hint style="info" %}
There are two dashboard-configurable thresholds (default: 120,000ms minimum position, 95% completion) that affect this call automatically:

* An update where `lastPlaybackPositionMs` is still below the minimum-position threshold is rejected — Android TV doesn't show a card for content the user has barely started.
* An update where `lastPlaybackPositionMs` / `durationMs` reaches the completion threshold **removes** the item from Watch Next instead of updating it — matching Android TV's own "finished watching" behavior. You don't need to call [watchNextRemove](#watchnextremove) yourself when a video finishes.
  {% endhint %}

***

### watchNextRemove

Removes a single item from the Watch Next row.

{% tabs %}
{% tab title="Plain Javascript" %}

```javascript
window.WTN.SmartTV.watchNextRemove({
  contentId: "movie-123",
  callback: function (response) {
    console.log(response.success);
  },
});
```

{% endtab %}

{% tab title="npm" %}

```javascript
import { watchNextRemove } from "webtonative/SmartTV";

watchNextRemove({
  contentId: "movie-123",
  callback: (response) => {
    console.log(response.success);
  },
});
```

{% endtab %}
{% endtabs %}

**Parameters:**

| Key         | Type       | Required | Description                                  |
| ----------- | ---------- | -------- | -------------------------------------------- |
| `contentId` | `String`   | **Yes**  | The `contentId` of the item to remove.       |
| `callback`  | `Function` | No       | Callback function invoked with the response. |

**Callback Response:** see [Callback Response Format](#callback-response-format).

***

### watchNextClear

Removes every item from the Watch Next row.

{% tabs %}
{% tab title="Plain Javascript" %}

```javascript
window.WTN.SmartTV.watchNextClear({
  callback: function (response) {
    console.log(response.success);
  },
});
```

{% endtab %}

{% tab title="npm" %}

```javascript
import { watchNextClear } from "webtonative/SmartTV";

watchNextClear({
  callback: (response) => {
    console.log(response.success);
  },
});
```

{% endtab %}
{% endtabs %}

**Parameters:**

| Key        | Type       | Required | Description                                  |
| ---------- | ---------- | -------- | -------------------------------------------- |
| `callback` | `Function` | No       | Callback function invoked with the response. |

**Callback Response:** see [Callback Response Format](#callback-response-format). Use this on sign-out, for example, so a shared TV device doesn't keep showing the previous user's Watch Next items.

***

### channelPublish

Creates or updates one of your own recommendation channels on the Android TV home screen, with the list of programs it should show. Calling it again with the same `channelId` replaces the channel's programs with the ones you send.

{% tabs %}
{% tab title="Plain Javascript" %}

```javascript
window.WTN.SmartTV.channelPublish({
  channelId: "trending-now",
  name: "Trending Now",
  programs: [
    {
      programId: "movie-123",
      title: "The Great Adventure",
      playbackUri: "https://example.com/watch/movie-123",
      description: "A thrilling journey across the mountains.",
      posterArtUri: "https://example.com/posters/movie-123.jpg",
    },
    {
      programId: "movie-456",
      title: "City Lights",
      playbackUri: "https://example.com/watch/movie-456",
    },
  ],
  callback: function (response) {
    console.log(response.success);
  },
});
```

{% endtab %}

{% tab title="npm" %}

```javascript
import { channelPublish } from "webtonative/SmartTV";

channelPublish({
  channelId: "trending-now",
  name: "Trending Now",
  programs: [
    {
      programId: "movie-123",
      title: "The Great Adventure",
      playbackUri: "https://example.com/watch/movie-123",
      description: "A thrilling journey across the mountains.",
      posterArtUri: "https://example.com/posters/movie-123.jpg",
    },
    {
      programId: "movie-456",
      title: "City Lights",
      playbackUri: "https://example.com/watch/movie-456",
    },
  ],
  callback: (response) => {
    console.log(response.success);
  },
});
```

{% endtab %}
{% endtabs %}

**Parameters:**

| Key         | Type       | Required | Description                                                                                                 |
| ----------- | ---------- | -------- | ----------------------------------------------------------------------------------------------------------- |
| `channelId` | `String`   | **Yes**  | Stable identifier for this channel. Reusing it updates the channel in place.                                |
| `name`      | `String`   | **Yes**  | Channel name shown on the home screen.                                                                      |
| `logoUri`   | `String`   | No       | Channel logo image URL. **Currently not rendered** — accepted but has no visible effect yet. Safe to omit.  |
| `programs`  | `Array`    | No       | List of programs in the channel. Each entry uses the fields below. Defaults to an empty channel if omitted. |
| `callback`  | `Function` | No       | Callback function invoked with the response.                                                                |

**`programs[]` fields:**

| Key            | Type     | Required | Description                                                        |
| -------------- | -------- | -------- | ------------------------------------------------------------------ |
| `programId`    | `String` | **Yes**  | Stable identifier for this program.                                |
| `title`        | `String` | **Yes**  | Title shown for the program.                                       |
| `playbackUri`  | `String` | **Yes**  | Deep link opened when the user selects it. **Must be `https://`**. |
| `description`  | `String` | No       | Description text.                                                  |
| `posterArtUri` | `String` | No       | Poster/thumbnail image URL. **Must be `https://`** if provided.    |

**Callback Response:** see [Callback Response Format](#callback-response-format).

***

### channelRemove

Removes a previously published channel from the home screen.

{% tabs %}
{% tab title="Plain Javascript" %}

```javascript
window.WTN.SmartTV.channelRemove({
  channelId: "trending-now",
  callback: function (response) {
    console.log(response.success);
  },
});
```

{% endtab %}

{% tab title="npm" %}

```javascript
import { channelRemove } from "webtonative/SmartTV";

channelRemove({
  channelId: "trending-now",
  callback: (response) => {
    console.log(response.success);
  },
});
```

{% endtab %}
{% endtabs %}

**Parameters:**

| Key         | Type       | Required | Description                                  |
| ----------- | ---------- | -------- | -------------------------------------------- |
| `channelId` | `String`   | **Yes**  | The `channelId` of the channel to remove.    |
| `callback`  | `Function` | No       | Callback function invoked with the response. |

**Callback Response:** see [Callback Response Format](#callback-response-format).

***

## Callback Response Format

Every function above (except `isSupported`, documented separately) reports back in the same shape:

**Success:**

```json
{
  "type": "smartTvWatchNextUpsert",
  "success": true
}
```

**Failure:**

```json
{
  "type": "smartTvWatchNextUpsert",
  "success": false,
  "errorCode": "VALIDATION_FAILED",
  "message": "playbackUri must be https"
}
```

| `errorCode`         | Meaning                                                                                                                                                       |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NOT_SUPPORTED`     | The add-on is disabled, or the device isn't detected as Android TV.                                                                                           |
| `INVALID_PAYLOAD`   | A required field (`contentId`/`title`/`playbackUri`, or a channel/program's `channelId`/`name`/`programId`/`title`/`playbackUri`) was missing.                |
| `VALIDATION_FAILED` | A field was present but invalid — most commonly a `playbackUri` or `posterArtUri` that wasn't `https://`, or a position below the minimum-position threshold. |
| `UNKNOWN_ERROR`     | An unexpected native error occurred.                                                                                                                          |

***

## Implementation Checklist

### WebToNative Dashboard

* [ ] Smart TV Support add-on enabled

### Your Website

* [ ] Imported the [WebToNative JavaScript bridge](https://docs.webtonative.com/javascript-apis/getting-started)
* [ ] Every `playbackUri` / `posterArtUri` is `https://` — `http://` is rejected
* [ ] `watchNextUpsert` is called periodically during playback (not just once), so the progress bar stays accurate
* [ ] Not manually calling `watchNextRemove` when playback finishes — items near the completion threshold are removed automatically
* [ ] Called `watchNextClear` on sign-out for shared/family TV devices

***

## Frequently Asked Questions

<details>

<summary>Why doesn't a Watch Next card show up on my TV?</summary>

Check three things: the Smart TV Support add-on is enabled, the device is actually detected as Android TV (call `isSupported` to confirm), and `lastPlaybackPositionMs` is above the minimum-position threshold — very early playback positions are intentionally rejected so users don't see cards for content they barely started.

</details>

<details>

<summary>How do I control the progress bar shown on the card?</summary>

You can't set a percentage directly — send `durationMs` and `lastPlaybackPositionMs`, and Android TV computes the bar from those two values. Keep calling `watchNextUpsert` as playback progresses to keep it accurate.

</details>

<details>

<summary>Do I need to remove a Watch Next item once the user finishes it?</summary>

No. Once `lastPlaybackPositionMs` / `durationMs` crosses the completion threshold (95% by default), the item is automatically removed from Watch Next on your next `watchNextUpsert` call for it.

</details>

<details>

<summary>Why is my channel logo not showing?</summary>

Channel logo rendering isn't implemented yet — `logoUri` is accepted but currently has no visible effect. This will be documented as functional once it ships.

</details>

<details>

<summary>Is there a tvOS / Apple TV equivalent?</summary>

No. This feature is Android TV-only today.

</details>
