Manage videos and collections
Read, edit, organise and clean up the videos in a library through videoRepository and
collectionRepository.
Prerequisites
net.bunny:apidependencyBunnyStreamApi.initialize(...)called, see Getting started- All repository methods are
suspend- call them from a coroutine
val videos = BunnyStreamApi.getInstance().videoRepository
val collections = BunnyStreamApi.getInstance().collectionRepository
Every method returns BunnyResult<T> — Ok with the value or Err with a typed BunnyError.
See Handle errors for the taxonomy and the terminal/transient split.
To put a video into the library in the first place, see Upload videos.
List and read
videos.listVideos(
libraryId = 12345L,
page = 1,
itemsPerPage = 20,
search = "keynote", // optional, matches the title
orderBy = "date", // or "title"
collectionId = null, // restrict to one collection
).fold(
onOk = { page -> render(page.items, page.totalItems) },
onErr = { error -> showError(error.message) },
)
videos.getVideo(libraryId, videoId)
A Video carries everything the API knows about it: status, lengthSeconds, width/height,
availableResolutions, captions, chapters, moments, metaTags, view counts and watch time.
Two conveniences over the raw API:
availableResolutionsandoutputCodecsarrive asList<String>, already split.width,heightandframeratearenulluntil transcoding has measured them, where the API reports0. A layout will not compute an aspect ratio from a placeholder.
Waiting for a video to finish encoding
status walks CREATED → UPLOADED → PROCESSING → TRANSCODING → FINISHED, and a freshly uploaded
video is not playable until it gets there. Poll getVideo while it is still in flight:
val settled = setOf(
VideoModelStatus.FINISHED,
VideoModelStatus.ERROR,
VideoModelStatus.UPLOAD_FAILED,
)
while (video.status !in settled) {
delay(5_000)
video = videos.getVideo(libraryId, videoId).getOrNull() ?: break
}
encodeProgress is a percentage you can show while waiting.
Play data
fetchVideoPlayData returns the URLs plus the library's player configuration in one call. This is
what the player uses internally; reach for it when you drive playback yourself.
videos.fetchVideoPlayData(
libraryId, videoId,
token = signedToken, // required when the library has token authentication on
expires = expiryEpochSeconds,
)
See Secure playback for how tokens are produced. Sign them on your server, never in the app.
Edit metadata
videos.updateVideo(
libraryId, videoId,
UpdateVideoRequest(
title = "Keynote, final cut",
collectionId = "collection-guid",
chapters = listOf(Chapter(title = "Intro", startSeconds = 0, endSeconds = 42)),
),
)
Only the fields you set are sent; everything left null keeps its current value.
Thumbnails
videos.setThumbnail(libraryId, videoId, thumbnailUrl) // an image already reachable by URL
videos.uploadThumbnail(libraryId, videoId, imageFile) // or one from the device
Captions
videos.addCaption(
libraryId, videoId,
AddCaptionRequest(languageCode = "en", label = "English", captionsFileBase64 = vttAsBase64),
)
videos.deleteCaption(libraryId, videoId, languageCode = "en")
Bunny can also produce them for you, see AI features below.
Import from a URL
Instead of uploading bytes from the device, have Bunny fetch the file:
videos.fetchNewVideo(
libraryId,
FetchVideoRequest(url = "https://example.com/source.mp4", title = "Imported"),
collectionId = "collection-guid",
thumbnailTime = 5_000, // milliseconds into the video
)
// Replace an existing video's content from a new source URL
videos.refetchVideo(libraryId, videoId, FetchVideoRequest(url = newUrl))
The call returns as soon as Bunny accepts the job; the video then goes through the normal encoding states.
Re-encode, repackage, reclaim storage
videos.reencodeVideo(libraryId, videoId) // library's current settings
videos.reencodeUsingCodec(libraryId, videoId, VideoCodec.HEVC) // a specific codec
videos.repackageVideo(libraryId, videoId, keepOriginalFiles = true)
Deleting renditions is how you reclaim storage, and it is irreversible — the only way back is a re-encode, which needs the original file. Ask the server what would happen first:
val preview = videos.deleteResolutions(
libraryId, videoId,
resolutions = listOf("240p", "360p"),
dryRun = true, // report what would be deleted, delete nothing
)
Then run it for real. Each flag deletes something different:
| flag | removes |
|---|---|
resolutions |
the renditions you name |
deleteAllResolutions |
every rendition, ignoring resolutions |
deleteNonConfiguredResolutions |
renditions the library no longer produces |
deleteMp4Files |
the MP4 fallback files - playback falls back to these when HLS is unavailable |
deleteOriginal |
the uploaded master, after which the video can never be re-encoded |
fetchVideoResolutions tells you what exists before you decide, and fetchVideoStorageSize breaks
down where the storage is going.
Statistics and the retention heatmap
videos.fetchVideoStatistics(libraryId, videoId, dateFrom = "2026-07-01", dateTo = "2026-07-31")
videos.fetchVideoHeatmap(libraryId, videoId) // Map<offset, viewers>
videos.fetchVideoHeatmapData(libraryId, videoId) // play data with the heatmap included
Pass videoId = null to fetchVideoStatistics for library-wide numbers, and hourly = true to
aggregate per hour instead of per day.
AI features
videos.smartGenerate(
libraryId, videoId,
SmartGenerateRequest(
generateTitle = true,
generateDescription = true,
generateChapters = true,
generateMoments = true,
sourceLanguage = "en",
),
)
videos.transcribeVideo(
libraryId, videoId,
TranscribeVideoRequest(sourceLanguage = "en", targetLanguages = listOf("de", "fr")),
force = false, // true re-runs it on a video that already has a transcription
)
Both are asynchronous on Bunny's side. Poll getVideo and read smartGenerateStatus and the
captions list to see the results land.
Delete
videos.deleteVideo(libraryId, videoId)
Permanent, and it takes everything derived from the video with it.
Collections
Collections are named groupings a video belongs to, through its collectionId.
collections.listCollections(libraryId, page = 1, itemsPerPage = 20, includeThumbnails = true)
collections.getCollection(libraryId, collectionId)
collections.createCollection(libraryId, name = "Conference 2026")
collections.updateCollection(libraryId, collectionId, name = "Conference 2026 - day 1")
collections.deleteCollection(libraryId, collectionId)
A VideoCollection reports videoCount, totalSizeBytes and previewVideoIds; ask for
includeThumbnails to also get previewImageUrls for a grid.
Deleting a collection does not delete its videos, they just stop belonging to it.
Gotchas
- A mutation can fail with HTTP 200. Bunny answers
success = falsefor validation failures and wrong-state requests; the repository turns that into anErr, so check the result rather than assuming a call that did not throw did what you asked. deleteResolutionsanddeleteVideocannot be undone. UsedryRunfirst.- Statistics dates are
yyyy-MM-ddstrings. - A video that is still encoding has no
availableResolutionsand no dimensions yet. Do not build a quality picker from a video that has not reachedFINISHED.
Working example: LibraryScreen and PlayerScreen in the
demo app.