Documentation Index
Fetch the complete documentation index at: https://mintlify.com/faraasaaay/innertube-v1/llms.txt
Use this file to discover all available pages before exploring further.
InnerTube resolves stream URLs by calling the YouTube /player endpoint with a specific client context. Because YouTube restricts which streams each client can access — and because certain clients require BotGuard proof tokens — the SDK provides ContentAwareFallbackStrategy to select an ordered list of clients suited to the content being played. If the primary client returns a 403 or an empty streamingData, the next client in the chain is tried automatically.
The player method
YouTube.player() is the core call that retrieves a PlayerResponse containing stream formats, video details, and playback metadata. Call it with a specific YouTubeClient from the companion object — the client determines which headers, user-agent, and context fields are sent to the /player endpoint.
import com.metrolist.innertube.YouTube
import com.metrolist.innertube.models.YouTubeClient
suspend fun fetchPlayerResponse(videoId: String) {
val result = YouTube.player(
videoId = videoId,
playlistId = null,
client = YouTubeClient.WEB_REMIX,
signatureTimestamp = null, // supply if client.useSignatureTimestamp is true
poToken = null, // supply if client.useWebPoTokens is true
)
result.onSuccess { playerResponse ->
val details = playerResponse.videoDetails
println("Title: ${details?.title}")
println("Duration: ${details?.lengthSeconds}s")
playerResponse.streamingData?.adaptiveFormats?.forEach { format ->
println("itag=${format.itag} mime=${format.mimeType} bitrate=${format.bitrate}")
}
}
}
Key parameters
| Parameter | Type | Description |
|---|
videoId | String | The YouTube video ID to load |
playlistId | String? | Optional playlist context (used for automix and radio queues) |
client | YouTubeClient | The client context sent with the request |
signatureTimestamp | Int? | Required when client.useSignatureTimestamp is true (obtained from the player JS) |
poToken | String? | BotGuard proof token; required when client.useWebPoTokens is true |
ContentAwareFallbackStrategy
ContentAwareFallbackStrategy.resolveClients(hints) returns an ordered List<YouTubeClient> tuned to the content you are about to play. Build a ContentHints value object that describes the track, then iterate the returned list — try each client in order until one yields a valid stream URL.
import com.metrolist.innertube.strategy.ContentAwareFallbackStrategy
import com.metrolist.innertube.strategy.ContentHints
val strategy = ContentAwareFallbackStrategy()
// Example: resolve clients for a regular (non-live, non-kids) track
val hints = ContentHints(
isExplicit = false,
isKidsContent = false,
isLive = false,
isUploaded = false,
)
val clients = strategy.resolveClients(hints)
// → [VISIONOS, ANDROID_VR_1_65_10, ANDROID_VR_1_43_32, WEB_REMIX, TVHTML5, TVHTML5_SIMPLY]
Client chains per content type
ContentHints flag | Client order |
|---|
| Default | VISIONOS → ANDROID_VR_1_65_10 → ANDROID_VR_1_43_32 → WEB_REMIX → TVHTML5 → TVHTML5_SIMPLY |
isUploaded = true | TVHTML5 → WEB_REMIX → WEB_CREATOR |
isLive = true | TVHTML5 → WEB_REMIX → WEB_CREATOR → TVHTML5_SIMPLY |
isKidsContent = true | TVHTML5 → WEB_REMIX → TVHTML5_SIMPLY → WEB_CREATOR |
isExplicit = true | VISIONOS → TVHTML5 → WEB_REMIX |
import com.metrolist.innertube.YouTube
import com.metrolist.innertube.strategy.ContentAwareFallbackStrategy
import com.metrolist.innertube.strategy.ContentHints
suspend fun resolveStreamUrl(videoId: String, isLive: Boolean): String? {
val strategy = ContentAwareFallbackStrategy()
val clients = strategy.resolveClients(ContentHints(isLive = isLive))
for (client in clients) {
val response = YouTube.player(
videoId = videoId,
playlistId = null,
client = client,
).getOrNull() ?: continue
val url = response.streamingData?.adaptiveFormats
?.maxByOrNull { it.bitrate ?: 0 }
?.url
if (url != null) return url
}
return null
}
The next endpoint
YouTube.next(endpoint) calls the InnerTube /next endpoint to retrieve the play queue, lyrics endpoint, and related tracks for a given video. This is the call your player makes after loading a track to populate the “Up next” list and wire the Lyrics and Related tabs.
import com.metrolist.innertube.YouTube
import com.metrolist.innertube.models.WatchEndpoint
suspend fun loadQueue(videoId: String) {
val result = YouTube.next(WatchEndpoint(videoId = videoId))
result.onSuccess { nextResult ->
println("Queue title: ${nextResult.title}")
println("Current track index: ${nextResult.currentIndex}")
nextResult.items.forEach { song ->
println(" ${song.title} — ${song.artists.joinToString { it.name }}")
}
// Endpoints for the Lyrics and Related tabs
val lyricsEndpoint = nextResult.lyricsEndpoint
val relatedEndpoint = nextResult.relatedEndpoint
// Load more queue items via continuation
if (nextResult.continuation != null) {
YouTube.next(nextResult.endpoint, continuation = nextResult.continuation)
}
}
}
NextResult fields
| Field | Type | Description |
|---|
title | String? | Queue or playlist title |
items | List<SongItem> | Ordered list of tracks in the play queue |
currentIndex | Int? | Index of the currently playing track within items |
lyricsEndpoint | BrowseEndpoint? | Browse endpoint to fetch lyrics via YouTube.lyrics() |
relatedEndpoint | BrowseEndpoint? | Browse endpoint to fetch related tracks via YouTube.related() |
continuation | String? | Token to fetch the next page of queue items |
endpoint | WatchEndpoint | The watch endpoint for the current or continuation position |
When YouTube.player() succeeds, PlayerResponse.streamingData contains two format lists:
adaptiveFormats — separate audio and video streams (preferred for audio-only playback)
formats — combined audio+video muxed streams
Each Format has the following fields relevant to playback:
| Field | Type | Description |
|---|
itag | Int | YouTube format identifier |
url | String? | Direct stream URL — null if the stream is cipher-protected |
mimeType | String? | MIME type and codec string, e.g. audio/webm; codecs="opus" |
bitrate | Int? | Nominal bitrate in bps |
contentLength | Long? | Total byte length of the stream |
import com.metrolist.innertube.YouTube
import com.metrolist.innertube.models.YouTubeClient
import com.metrolist.innertube.models.response.PlayerResponse
suspend fun getBestAudioUrl(videoId: String): String? {
val response = YouTube.player(
videoId = videoId,
playlistId = null,
client = YouTubeClient.ANDROID_VR_1_65_10,
).getOrNull() ?: return null
// Pick the highest-bitrate audio-only adaptive format
return response.streamingData
?.adaptiveFormats
?.filter { it.mimeType?.startsWith("audio/") == true }
?.maxByOrNull { it.bitrate ?: 0 }
?.url
}
PoToken requirement
Some clients embed BotGuard proof tokens in player requests via the serviceIntegrityDimensions body field. The flag is declared on YouTubeClient:
| Flag | Description |
|---|
useWebPoTokens = true | Client accepts a PoToken — pass playerRequestPoToken as poToken argument |
requirePoToken = true | Client requires a PoToken — requests without one will fail |
Clients that set useWebPoTokens = true include WEB_REMIX, WEB_CREATOR, TVHTML5, and TVHTML5_SIMPLY. Only TVHTML5_SIMPLY also sets requirePoToken = true.
For instructions on obtaining a valid PoToken, see the PoToken guide.
// Supplying a PoToken to a client that accepts one
val poTokenResult = poTokenGenerator.getWebClientPoToken(videoId, visitorData)
val response = YouTube.player(
videoId = videoId,
playlistId = null,
client = YouTubeClient.WEB_REMIX,
poToken = poTokenResult?.playerRequestPoToken,
)
Signature deobfuscation
When a format’s url field is null, the stream URL is encoded in a cipher and must be decoded before playback. The cipher module (YTPlayerUtils + CipherDeobfuscator) handles this transparently: it downloads the player JavaScript, extracts the deobfuscation function, and applies it to the cipher parameters embedded in each format.
Deobfuscation happens automatically inside YTPlayerUtils.playerResponseForPlayback(). If you call YouTube.player() directly, you must invoke the cipher module yourself on any format whose url is null.
Handling 403 stream failures
When a stream URL obtained from WEB_REMIX returns HTTP 403 during playback (detected by ExoPlayer’s error callback), you must call YTPlayerUtils.markWebRemixFailed(videoId) before retrying. This prevents the strategy from selecting WEB_REMIX again for the same video and allows the fallback chain to proceed to TVHTML5 or ANDROID_VR clients.// In your ExoPlayer error handler
if (error.errorCode == PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS) {
YTPlayerUtils.markWebRemixFailed(videoId)
// Re-trigger stream resolution with the same ContentHints
}
markWebRemixFailed() is scoped to the current process lifetime. Call YTPlayerUtils.clearWebRemixFailures() when the player JavaScript cipher is refreshed, because prior 403s may have been caused by a stale cipher rather than a permanent client restriction.