Earlier this year, we published a guide to migrating a Vimeo library into Bunny Stream. It covered three routes: download and re-upload by hand, paste a URL into the dashboard, or point the vimeo2bunny CLI at your account and let it walk the whole library.
Vimeo turned out to be the first of many. The same question kept coming for Wistia, Mux, Cloudflare Stream, JW Player, Brightcove, and for folders of MP4s sitting in an S3 bucket. Each platform hides its download URL somewhere different and has its own idea of what a folder is, so every one of them needed its own adapter.
Today we’re introducing a new migration tool that lives in the bunny.net CLI, bunny stream import. Seven sources, one command, and the same flags no matter which platform you're leaving.
Getting started is easy:
npm install -g @bunny.net/cli
bunny login
bunny stream import
import asks which Stream library the videos should land in, which platform they're coming from, and for the credentials that platform needs. Then it starts the transfer. Add --dry-run to see the plan before anything moves.
How it works
Nothing passes through your machine. For each video, the CLI asks the source platform for a download URL and hands it to Bunny Stream's fetch endpoint. We pull the file straight from Vimeo, S3, or wherever it lives, so neither your disk space nor your connection speed limits how much you can move.
The command returns as soon as every video is queued with Bunny Stream. Fetching and encoding carry on in the background, so a library of thousands of videos doesn't hold your terminal open for hours. Check on it whenever you like:
bunny stream import status --lib 12345 --source vimeo
status lists each video with its Bunny status, encoding progress, and size. If a video remains at zero bytes for more than 30 minutes, it's marked as stalled. This usually means the fetch failed quietly on the source side. Run the import again with --retry-stalled to replace it.
Getting the download URL is the part that differs per platform, and it's the work the command exists to abstract away:
| Source | Where the download URL comes from |
|---|---|
| Vimeo | API download links, available on a Standard plan and above |
| AWS S3 | A pre-signed GetObject URL, signed with your AWS credentials |
| Wistia | Data API asset URLs, preferring the original file, then the best MP4 rendition |
| Mux | Master access or static MP4 renditions enabled before import; assets with neither are skipped |
| Cloudflare Stream | The /downloads endpoint, polled until the MP4 is ready |
| JW Player | The adapter reads the highest-resolution MP4 from the Management API first, then the public Delivery API |
| Brightcove | CMS API sources, with OAuth tokens refreshed as they expire |
Folders come across too. Vimeo folders, Wistia folders, Brightcove folders, and the top-level S3 prefixes (under --prefix, if you set one) become Bunny Stream libraries with matching names. Deeper S3 prefixes roll up into their top-level collection. Mux, Cloudflare Stream, and JW Player have no folder concept, so everything from those lands as uncategorized. --folder limits a run to a single source folder.
Titles, descriptions, and tags copy over wherever the source API provides them.
Importing from Cloudflare Stream
Cloudflare needs an API token with the Stream:Edit permission and your account ID:
export CLOUDFLARE_API_TOKEN=...
export CLOUDFLARE_ACCOUNT_ID=...
bunny stream import --source cloudflare --lib 12345 --dry-run
bunny stream import --source cloudflare --lib 12345
bunny stream import status --lib 12345 --source cloudflare
For each video, the CLI reuses an existing MP4 download or asks Cloudflare to generate one, then waits for it to be ready before handing the URL to Bunny Stream. Videos with Require Signed URLs turned on can’t be fetched, so turn that setting off on them before you import.
That’s it. Each video is tagged with cfStreamId, so a second run later only picks up what's new.
Stop, re-run, resume
Every video that lands in Bunny Stream carries a metaTag that identifies its source video, such as vimeoId, muxAssetId, or cfStreamId. For S3 it’s a hash of the bucket and key, so your bucket layout never shows up in public embeds.
Run the import again and the CLI skips anything already tagged. Videos Bunny Stream is still processing are left alone. Videos that failed on our side are imported again, unless Bunny rejected the file itself (an unsupported resolution or duration, or a corrupt original). Add --clean-failed to retry those too.
| metaTag Source | Title | Description | Tags |
|---|---|---|---|
| Vimeo | ✅ Video name | ✅ | ✅ |
| AWS S3 | ✅ From the object key | ✅ From the description object metadata |
✅ From the tags object metadata (comma-separated) |
| Wistia | ✅ Media name | ✅ | ❌ |
| Mux | ✅ From meta.title, falling back to passthrough |
❌ | ❌ |
| Cloudflare Stream | ✅ From meta.name |
✅ | ❌ |
| JW Player | ✅ Media title | ✅ | ✅ |
| Brightcove | ✅ Video name | ✅ | ✅ |
If a run gets interrupted or finishes with failures, you can pick it up with --resume. It retries the failed videos and keeps the folder scope of the original run. State is saved per source and per library, so imports into different libraries, or from different sources, can run at the same time.
Like the rest of the bunny.net CLI, every prompt has a flag and an environment variable behind it, which works great if you want to automate the import process.
VIMEO_ACCESS_TOKEN=... bunny stream import --source vimeo --lib 12345 --force --output json
bunny stream import status --lib 12345 --source vimeo --output json
A few platforms charge for, or gate, the download that makes this work:
- Vimeo only exposes video files on Standard plans and above, or on a legacy Plus, PRO, Business, or Premium plan. The CLI takes the original download link where there is one and falls back to the highest-resolution MP4, and both need one of those plans.
- Mux needs either temporary master access, which the CLI prefers, or static MP4 renditions on a public playback ID. Master URLs expire 24 hours after Mux prepares them, so run the import soon after you turn master access on.
- Cloudflare Stream MP4 downloads add no storage cost, but Cloudflare bills for the video’s duration each time one is downloaded, and Bunny’s fetch counts as one download per video.
- Wistia asset URLs bypass their analytics and count toward your bandwidth.
As with the Vimeo guide, the import is for content you own or have permission to move.
Embed Bunny Player
Every video that lands in Bunny Stream plays in Bunny Player, so the final step of the move is replacing your existing embeds with it. Each imported video carries its source ID as a metaTag, such as vimeoId, so you can match your old embeds to the new videos. List the library to see each video's ID:
bunny stream videos list --lib 12345
Bunny Player is an iframe, so the embed is one element pointed at the library ID and video ID. bunny stream videos embed <video-id> prints the URL for you:
<iframe
src="<https://player.mediadelivery.net/embed/12345/your-video-guid>"
loading="lazy"
style="width: 100%; aspect-ratio: 16 / 9; border: 0"
allow="autoplay; encrypted-media; picture-in-picture; fullscreen"
allowfullscreen
></iframe>
The framework guides wrap that iframe in a component and drive playback from your own code with player.js. The React guide ends up with a BunnyPlayer component that takes the same two IDs and forwards the player's events:
<BunnyPlayer
libraryId="12345"
videoId="your-video-guid"
onEnded={() => console.log("Video finished")}
/>
There are guides for React, Next.js, Vue/Nuxt, and Svelte/SvelteKit, and the Next.js one includes a client component that holds up under server rendering. The videos that sat on your old platform last week now play on your own site.
Before you publish the new embeds, check who can watch them. The import copies videos, not privacy settings, and a new library plays on any site. A video that was private or unlisted on your old platform is public on Bunny until you lock the library down. Restrict playback to your own domain and require signed embed URLs:
bunny stream libraries security update 12345 --allow-referrer example.com --embed-token
With embed tokens on, bunny stream videos embed <video-id> --expires 1h prints a signed URL. In production, sign a short-lived URL on your server for each page view, using the library's token key. A copied or bookmarked link stops working once it expires.
Start your migration
Install the CLI, log in, and do a dry run to see what would move:
npm install -g @bunny.net/cli
bunny login
bunny stream import --dry-run
The CLI is open source and built in public. If something's broken or the platform you're leaving isn't supported yet, open an issue on GitHub, or come and tell us how your move went in Discord.
Additional resources
Comments require cookies. to view and post.


