react-native-tv-recommended-content
v1.1.4
Published
Publish content to the Android TV / Google TV home screen from your React Native app to the "Continue Watching" (Watch Next) row, and your own fully custom recommendation channels
Maintainers
Readme
react-native-tv-recommended-content
Publish content to the Android TV / Google TV home screen from your React Native app to the "Continue Watching" (Watch Next) row, and your own fully custom recommendation channels.
Built directly on top of androidx.tvprovider (WatchNextProgram, PreviewProgram, and Channel), fully typed, and safe to call from any platform. It no-ops gracefully on iOS, tvOS, and non-TV Android instead of crashing.
📋 Table of Contents
- Platform Support
- Installation
- Quick Start
- Core Concepts
- API Reference
- Types and Enums
- Watch Next Quality Guidelines
- Troubleshooting
- Testing
- Contributing
- License
- Support
🖥️ Platform Support
| Platform | Supported | Behavior |
|-------------------------|:---------:|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Android TV / Google TV | ✅ | Fully functional |
| Android (mobile/tablet) | 🚫 | All methods resolve to safe defaults (null or false or 0 or [] respectively). No-op, no crash. |
| iOS / tvOS | 🚫 | Same safe no-op / no-crash behavior. There is no tvOS equivalent of Watch Next/Channels (tvOS has its own separate, unrelated "Top Shelf" API), so no native implementation exists for this package at at this time. But could exist in the future. |
Every method is safe to call on any platform. Unsupported platforms are resolved gracefully.
📦 Installation
npm install react-native-tv-recommended-content
# or
yarn add react-native-tv-recommended-contentNo manual native linking or AndroidManifest.xml changes are required. Autolinking handles package registration, and Watch Next / Channels don't require any special Android permissions.
🔧 Quick Start
import TVRecommendedContent, { ProgramType } from 'react-native-tv-recommended-content';
// Add/update a movie in the Watch Next row (call this on pause/exit)
await TVRecommendedContent.addProgramToWatchNext({
contentId: 'movie-123',
title: 'The Great Adventure',
posterUrl: 'https://example.com/poster.jpg',
deepLinkUri: 'myapp://play/movie-123?resume=452000',
playbackPosition: 452000, // 7m32s in, in milliseconds
duration: 5400000, // 90 minutes total
type: ProgramType.MOVIE,
});
// Create a custom channel. The first one you ever create becomes your
// app's protected "default" channel automatically (see Core Concepts below)
const channelId = await TVRecommendedContent.createChannel({
displayName: 'New Releases',
appLinkUri: 'myapp://browse/new-releases',
});
// Add content into that channel
await TVRecommendedContent.addProgramToChannel(channelId, {
contentId: 'movie-456',
title: 'Another Great Film',
posterUrl: 'https://example.com/poster2.jpg',
deepLinkUri: 'myapp://play/movie-456',
type: ProgramType.MOVIE,
});You can also import individual functions instead of the default export:
import { getChannels, remove } from 'react-native-tv-recommended-content';
const channels = await getChannels();
const removedCount = await remove('movie-456'); // removes from everywhere at once📚️ Core Concepts
- Watch Next is a single, system-managed row ("Continue Watching") shared across all apps. You don't create it, you just publish/remove entries from it.
- Channels are rows you create and fully control (e.g. "New Releases", "Because You Watched X"). Each channel can hold many programs.
- The default channel: the very first channel your app creates is automatically treated by the system as "browsable". It appears on the home screen immediately, with no user approval needed. Every channel after that requires the user to explicitly approve adding it (the system shows a permission prompt when you call
createChannel). See more on "default channels" here. This package tracks which channel is yours to protect. ThedeleteChannel()api will refuse to delete the default channel unless you explicitly passforceDeleteIfDefault: true, since a deleted default channel can't automatically reappear. contentIdis your own stable unique identifier for a piece of content (e.g. your backend's movie/episode ID). Every method that looks up, updates, or removes a program does so by matching this field.
🔑️ API Reference
Watch Next
Publishes or updates an entry in the Watch Next row. If an entry with the sameaddProgramToWatchNext(programData: ProgramData):contentIdalready exists, it's updated in place (its progress and metadata refreshed) rather than duplicated. Resolves with the content provider URI string, ornullon unsupported platforms.
Removes one entry from Watch Next specifically. ResolvesremoveWatchNext(contentId: string):trueif something was found and removed,falseotherwise.
Removes every Watch Next entry belonging to your app. Resolves with the count removed.clearWatchNext():
Channel Management
Creates a new home screen channel. Resolves with the new channel's ID.createChannel(data: CreateChannelData):
Updates an existing channel. Only fields present inupdateChannel(channelId: string, data: UpdateChannelData):dataare changed. Everything else is preserved as-is.
Deletes a channel and, per the platform's own behavior, its associated programs. Rejects with error codedeleteChannel(options: DeleteChannelOptions):DEFAULT_CHANNEL_PROTECTEDif; the target is your app's default channel, andforceDeleteIfDefaultwasn't set totrue.
Lists every channel belonging to your app. Resolves with an array of found channels and their properties (getChannels():TvChannel[])
Returns how many channels your app currently has.getChannelsCount():
Programs Within a Channel
Publishes or updates a program inside a specific channel (same update-in-place-by-addProgramToChannel(channelId: string, programData: ProgramData):contentIdbehavior asaddProgramToWatchNext).
Removes one program from one specific channel.removeFromChannel(contentId: string, channelId: string):
Removes every program from a channel without deleting the channel itself. Resolves with the count removed.clearChannel(channelId: string):
Cross-cutting
APIs that can be used across both custom channels and the system-owned Watch Next channel.
Removes every instance of a givenremove(contentId: string):contentId, across all your channels, including Watch Next. Useful for a single "delete this title everywhere" action (e.g. when content is taken down entirely). Resolves with the total count removed across all locations.
Wipes everything (every program) your app has added across your own custom channels and Watch Next. This does not delete the channels themselves, only their contents. Resolves with the total count removed.clearAll():
⚙️️ Types and Enums
All types are exported from the package root: (import { ProgramType, GenreType, ... } from 'react-native-tv-recommended-content').
| Type | Import Type | Description |
|------------------------|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ProgramType | ENUM | MOVIE, TV_EPISODE, TV_SERIES, CLIP |
| ReviewRatingStyle | ENUM | STARS, THUMBS_UP_DOWN, PERCENTAGE |
| GenreType | ENUM | Canonical genre tags (COMEDY, DRAMA, SPORTS, etc.) |
| AspectRatio | ENUM | RATIO_16_9, RATIO_1_1, RATIO_2_3, RATIO_3_2, RATIO_3_4, RATIO_4_3, MOVIE_POSTER |
| Availability | ENUM | AVAILABILITY_AVAILABLE, AVAILABILITY_FREE, AVAILABILITY_FREE_WITH_ADS, AVAILABILITY_FREE_WITH_SUBSCRIPTION, AVAILABILITY_PAID_CONTENT, AVAILABILITY_PURCHASED |
| ContentRatingSystem | ENUM | Country-specific rating system identifiers (US_TV, US_MV, KR_TV, AU_TV, etc.) |
| ContentRating | TYPE | { ratingSystem, rating, subRatings?, domain? } |
| ProgramData | TYPE | Full metadata object accepted byaddProgramToWatchNext / addProgramToChannel - see inline JSDoc for every field, including TV-episode-specific (episodeTitle, seasonNumber, etc.) and clip/series-specific fields |
| CreateChannelData | TYPE | { displayName, appLinkUri, description?, appLinkText?, appLinkColor?, appLinkIconUri?, appLinkPosterArtUri?, displayOrder?, searchable? } |
| UpdateChannelData | TYPE | Partial<CreateChannelData> |
| DeleteChannelOptions | TYPE | { channelId, forceDeleteIfDefault? } |
| TvChannel | TYPE | Shape returned bygetChannels() |
🔔 Important field-naming note:
For
type: ProgramType.TV_EPISODE, when adding a program to a channel or the "Watch Next" row, thetitlefield must hold the series name (e.g. "Breaking Bad"), not the episode's own name. The episode's own name goes inepisodeTitle. This mirrors Android's own underlying data model and is easy to get backwards; getting it wrong doesn't throw an error, it just mislabels the card on screen.
📜 Watch Next Quality Guidelines
These aren't rules this library enforces for you. They're Google's own published guidelines for what belongs in Watch Next, worth building your calling logic around:
- Only add movies and TV episodes. Not clips, trailers, or short-form content.
- "Started" thresholds: A movie counts as started after 3% or 2 minutes watched (whichever comes first); a TV episode after 2 minutes.
- "Finished" means the end credits have started. Remove the entry at that point rather than leaving it stale. (An approximation like "under 3 minutes remaining" works if you don't have real credit-detection).
- Keep at most one Watch Next entry per TV series at a time.
- When an episode finishes, add the next episode in the series rather than just removing the finished one.
- Visit the official guidelines page for more information and details.
🔧 Troubleshooting
"Unsupported class file major version NN"during a Gradle build. This is a JDK-vs-Gradle version mismatch, unrelated to this package. Your system's active JDK is newer than the project's Gradle version supports. Point Gradle at an older JDK (17 is the safe default) viaorg.gradle.java.homeinandroid/gradle.properties, rather than changing your system-wideJAVA_HOME.Cannot add extension with name 'kotlin'/ aClassCastExceptioninvolvingBaseExtension. Both are Android Gradle Plugin 9.0+ migration issues (its new built-in Kotlin support and new DSL implementation), not issues with this package. Add to your app'sandroid/gradle.properties:android.builtInKotlin=false android.newDsl=falseNote these are temporary opt-outs Google has said will be removed in a future AGP major version. Treat as a stopgap, not a permanent fix.
My Watch Next / channel entries aren't appearing on the home screen at all.These can be one of these independent causes:- You're missing a required field. This package sets
lastEngagementTimeUtcMillisfor you automatically, but if entries still don't show, verify thatposterUrlis a real, reachable URL. - On genuine Google TV devices, the "Continue Watching" row specifically requires prior certification approval from Google, a server-side gate, separate from whether your code is correct. Plain AOSP Android TV (non-Google-TV-branded, leanback launcher) doesn't have this restriction. Custom channels you create yourself are not affected by this, only the system Watch Next row is.
- A newly created channel isn't showing up:
Every channel after your app's first one requires the user to explicitly approve it. This package triggers that system permission prompt automatically inside
createChannel, but the user still has to accept it. Only the very first channel your app ever creates is exempt from this. See here.
- You're missing a required field. This package sets
🧪 Testing
- Clone the repo and run
yarnat the root. yarn example androidruns the included example app.- Test against a real Android TV device or the Android TV emulator where possible. Some behavior (notably the Google TV certification gate mentioned earlier) can only be observed on genuine Google TV hardware/launcher, not a plain AOSP Android TV emulator image.
- There's no meaningful unit-test surface on the JS side by design. This package is a thin, deliberately logic-free pass-through to native code; the real behavior to verify is always on-device.
🤝 Contributing
See CONTRIBUTING.md and please follow the Code of Conduct.
🔒 License
Apache-2.0 © MadeByRaymond (Daniel Obiekwe)
❤️ Support
If this package saved you from writing raw androidx.tvprovider Kotlin yourself, consider buying me a coffee:
Issues and feature requests: GitHub Issues
