Skip to main content

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

ParameterTypeDescription
videoIdStringThe YouTube video ID to load
playlistIdString?Optional playlist context (used for automix and radio queues)
clientYouTubeClientThe client context sent with the request
signatureTimestampInt?Required when client.useSignatureTimestamp is true (obtained from the player JS)
poTokenString?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 flagClient order
DefaultVISIONOS → ANDROID_VR_1_65_10 → ANDROID_VR_1_43_32 → WEB_REMIX → TVHTML5 → TVHTML5_SIMPLY
isUploaded = trueTVHTML5 → WEB_REMIX → WEB_CREATOR
isLive = trueTVHTML5 → WEB_REMIX → WEB_CREATOR → TVHTML5_SIMPLY
isKidsContent = trueTVHTML5 → WEB_REMIX → TVHTML5_SIMPLY → WEB_CREATOR
isExplicit = trueVISIONOS → 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

FieldTypeDescription
titleString?Queue or playlist title
itemsList<SongItem>Ordered list of tracks in the play queue
currentIndexInt?Index of the currently playing track within items
lyricsEndpointBrowseEndpoint?Browse endpoint to fetch lyrics via YouTube.lyrics()
relatedEndpointBrowseEndpoint?Browse endpoint to fetch related tracks via YouTube.related()
continuationString?Token to fetch the next page of queue items
endpointWatchEndpointThe watch endpoint for the current or continuation position

Stream URL extraction

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:
FieldTypeDescription
itagIntYouTube format identifier
urlString?Direct stream URL — null if the stream is cipher-protected
mimeTypeString?MIME type and codec string, e.g. audio/webm; codecs="opus"
bitrateInt?Nominal bitrate in bps
contentLengthLong?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:
FlagDescription
useWebPoTokens = trueClient accepts a PoToken — pass playerRequestPoToken as poToken argument
requirePoToken = trueClient 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.

Build docs developers (and LLMs) love