Skip to content

Secure playback

Playing content from libraries with token authentication or hotlink protection enabled.

Token authentication

When "Token authentication" is on for your library (Bunny dashboard > Stream > your library > Security), every playback needs a signed token and an expiry timestamp. Both players accept them:

// VOD
player.playVideo(videoId, token = token, expires = expires)

// Live
BunnyLiveStreamPlayer(libraryId, streamId, token = token, expires = expires)

expires is a unix timestamp (seconds). The token is a SHA-256 based signature over your library's token authentication key, the video id and the expiry.

Forgetting the token looks like nothing failing. The management API does not need it, so the call for play data succeeds and the player comes up with a working timeline and controls — the CDN just serves no media, and the picture stays black. There is no error to catch. The same happens with a token signed using a different library's key. If playback is black on a library that works in the Bunny dashboard, check this first.

Sign tokens on your server. The signature requires the library's token authentication key; shipping that key inside the app makes the protection pointless, since anyone can extract it and mint their own tokens. The usual setup: your backend exposes an endpoint that returns {token, expires} for a video id, your app calls it before starting playback.

The demo app signs tokens on the device purely so it can run standalone; treat that as a demo convenience, not a pattern to copy.

When "Block direct URL file access" is on, Bunny's CDN only serves requests carrying the expected Referer header. The SDK sends it for everything it loads itself: playback, seek thumbnails, posters inside the players.

You only need to act when you load Bunny-hosted images with your own image loader (thumbnails in your video list, for example). Send the SDK's Referer constant:

// Coil
val request = ImageRequest.Builder(context)
    .data(thumbnailUrl)
    .httpHeaders(NetworkHeaders.Builder().set("Referer", BunnyCdn.REFERER).build())
    .build()

// Glide
val glideUrl = GlideUrl(thumbnailUrl) { mapOf("Referer" to BunnyCdn.REFERER) }

Without the header those requests come back as 403.

Enterprise DRM (Widevine)

The player follows the library's Enterprise DRM settings on its own; there is nothing to configure in the app. What viewers see depends on the Widevine security level of their device. Emulators and many cheaper phones only have L3 (software), most current phones have L1 (hardware).

  • SD only for L3: the license server only gives L3 devices the keys for the SD renditions. On an L3 device the player stays at or below the library's maximum software resolution (480p unless the library says otherwise), and the quality menu only lists those renditions. L1 devices get the full ladder.
  • Minimum security level: a device below the library's minimum gets no license at all. The player does not start playback there and shows "This video can't be played on this device". The same text reaches onPlaybackError, and the exact reason (device level and required level) goes to logcat.

When the SDK can't read the device's level, it applies neither rule and leaves the decision to the license server. While casting, the receiver plays and licenses the video itself. The phone's level counts again when playback comes back to the phone: the cap applies there, and a video the phone can't license stops with the same message.

Keep the API key server-side where you can

The accessKey passed to initialize is your library's API key. It can manage content, not just read it. Apps that only play content do not need it at all: initialise with BunnyStreamApi.initialize(context, libraryId) and the SDK plays through the public play endpoints. If the library has token authentication on, have a backend sign the playback tokens and hand the app only what it needs (video ids, tokens), so neither key ships in the APK.