Play Vimeo videos inside your Flutter app — like any normal video.
VimeoNativePlayer(url: 'https://vimeo.com/76979871')That is the whole setup. No Vimeo account. No API key. No sign up.
- No WebView. Plays the real video file with the native player, not an embedded Vimeo web page in a hidden browser.
- Direct HLS and MP4 streams. Resolves any Vimeo link to its
.m3u8adaptive stream or progressive MP4 URL, with every quality and resolution listed for you. - Autoplay with sound, looping, muting, seeking, and fullscreen — all the things a WebView embed cannot do reliably on mobile.
- Subtitles and captions. Every
.vtttext track, with language and label. - Unlisted videos work. Private share links like
vimeo.com/123456/abc123resolve normally. - Video metadata: title, duration, thumbnail, width, height, aspect ratio, fps, owner, live status, 360°/spatial flag.
- Use any player you like. Take the resolved stream URL and hand it to
video_player,chewie,better_player,media_kit, or your own. - Android, iOS, macOS and Web. Pure Dart — no platform channels of its own.
A Vimeo link like vimeo.com/76979871 is a web page, not a video file.
Video players cannot play a web page. They need the address of the real video.
Other Vimeo packages solve this by putting the Vimeo website inside your app, in a small hidden browser (a "WebView"). That causes two real problems:
1. The video will not start on its own with sound. Phones block web pages from auto-playing sound. So the user must tap play, or the video plays silent.
2. The user can accidentally leave your app. The Vimeo page has real links on it (logo, title, author). One wrong tap opens a browser, and your user is gone.
This package works differently. It finds the address of the real video file and plays it with the phone's own video player — the same way your app plays any other video.
So:
| WebView packages | This package | |
|---|---|---|
| Starts automatically with sound | ✗ | ✓ |
| User can tap out to a browser | ✗ Yes, they can | ✓ No, they cannot |
| Looks like your app | ✗ Looks like a website | ✓ Native |
| Works on new Vimeo videos | Varies | ✓ |
Almost every failure is a Vimeo privacy setting, not a bug. Vimeo has two settings that work independently, and this catches nearly everyone out:
- "Who can watch?" controls vimeo.com.
- "Where can this be embedded?" controls everywhere else — including your app.
A video can be perfectly public on vimeo.com and still refuse to play in your app, because the second setting says so. That is why "but it works in my browser" is not evidence that the link is fine.
| Symptom | Cause | Fix |
|---|---|---|
| Plays in a browser, not in your app | Embed privacy | On Vimeo: Share → "Where can this be embedded?" → Anywhere |
| "Because of its privacy settings, this video cannot be played here" | Domain-level embed privacy | Same as above. "Specific domains" can never work in an app — an app has no domain to whitelist |
| Worked earlier, fails now | The stream URL expired | Don't store stream URLs. Store the Vimeo link and resolve again |
| Some videos work, others don't | Privacy is set per video | Check the failing video's own settings, then set an account-wide default |
| Unlisted video won't play | The link lost its privacy hash | Use the whole link — vimeo.com/123456789/abc123def, not just the number |
| Password-protected video | Not supported | Cannot be played. Remove the password |
The package tells you which of these it is. Each maps to an error you can catch:
| Error | Meaning |
|---|---|
VimeoEmbedNotAllowedException |
Embed privacy blocks apps. Only the owner can fix it |
VimeoPrivateLinkRequiredException |
Unlisted video, and the link had no hash |
VimeoNotFoundException |
The video really is gone |
VimeoRestrictedException |
Withheld for some other reason (password, fully private) |
Every one of these carries Vimeo's own explanation in .reason when Vimeo
gave one, so your logs show exactly what Vimeo said.
This package cannot and will not bypass Vimeo's privacy settings. If a video is restricted, the owner has decided that deliberately, and the only fix is on Vimeo. Any package claiming otherwise is either breaking that choice or about to stop working.
Step 1. Open your pubspec.yaml file and add this under dependencies:
dependencies:
vimeo_native_player: ^1.1.0Step 2. In your terminal, run:
flutter pub getStep 3 (Android only). Open
android/app/src/main/AndroidManifest.xml and make sure this line is inside
the <manifest> tag:
<uses-permission android:name="android.permission.INTERNET"/>Most apps already have it.
iPhone needs nothing extra.
This is a complete, working app. Copy it into lib/main.dart and run it.
import 'package:flutter/material.dart';
import 'package:vimeo_native_player/vimeo_native_player.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('My Video')),
body: Center(
child: AspectRatio(
aspectRatio: 16 / 9,
child: VimeoNativePlayer(
url: 'https://vimeo.com/76979871',
),
),
),
),
);
}
}Always put the player inside something that gives it a size, like
AspectRatio or SizedBox. If you do not, Flutter does not know how big to
make the video, and you may see an error or a blank screen.
// Good — it has a size
AspectRatio(aspectRatio: 16 / 9, child: VimeoNativePlayer(url: myUrl))
// Also good
SizedBox(height: 220, child: VimeoNativePlayer(url: myUrl))
// Bad — no size given
VimeoNativePlayer(url: myUrl)All of these work:
https://vimeo.com/76979871
https://vimeo.com/76979871/abc123def
https://player.vimeo.com/video/76979871?h=abc123def
https://vimeo.com/channels/staffpicks/76979871
https://vimeo.com/groups/motion/videos/76979871
https://vimeo.com/album/2222222/video/76979871
https://vimeo.com/manage/videos/76979871
https://vimeo.com/user12345678/videos/76979871
https://vimeo.com/76979871?share=copy
The extra letters after the number (like abc123def) are for unlisted
videos — private share links that Vimeo gives you. Those work too. Just paste
the whole link.
If your app has both YouTube and Vimeo links, you can check which is which:
if (VimeoUrl.isVimeo(myLink)) {
// show the Vimeo player
} else {
// show your YouTube player
}All of these are optional. Use only the ones you need.
VimeoNativePlayer(
url: 'https://vimeo.com/76979871',
autoPlay: true, // start by itself (default: true)
looping: false, // repeat forever when it ends (default: false)
muted: false, // start with no sound (default: false)
showControls: true, // show play/pause bar (default: true)
allowFullScreen: true, // let user go fullscreen (default: true)
startAt: Duration(seconds: 30), // begin 30 seconds in
useVideoAspectRatio: true, // match the video's own shape
progressColor: Colors.teal, // colour of the progress bar
)VimeoNativePlayer(
url: 'https://vimeo.com/76979871',
onResolved: (video) {
print('Title: ${video.title}');
print('Length: ${video.duration}');
},
onError: (error) {
print('Something went wrong: ${error.message}');
},
)VimeoNativePlayer(
url: 'https://vimeo.com/76979871',
loadingBuilder: (context) => const Center(
child: Text('Loading your video...'),
),
errorBuilder: (context, error, retry) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(error.message),
ElevatedButton(onPressed: retry, child: const Text('Try again')),
],
),
),
)retry is a ready-made function. Put it on a button and it loads the video
again.
You do not have to use our player. If you already use another player
(better_player, media_kit, or your own), just ask this package for the
video address and use it however you like.
final video = await VimeoResolver().resolve('https://vimeo.com/76979871');
print(video.title); // The New Vimeo Player
print(video.duration); // 0:01:02
print(video.thumbnailUrl); // the cover picture
print(video.bestStream!.url); // <-- the real video address
// Now give that address to any player you want
myOwnPlayer.open(video.bestStream!.url);| What you write | What you get |
|---|---|
video.title |
The video's name |
video.duration |
How long it is |
video.thumbnailUrl |
Cover picture address |
video.bestStream!.url |
The video address to play |
video.width, video.height |
Video size in pixels |
video.aspectRatio |
Its shape, e.g. 1.77 for widescreen |
video.fps |
Frames per second |
video.owner?.name |
Who uploaded it |
video.textTracks |
Subtitles (see below) |
video.isLive |
Is it a live broadcast? |
video.isSpatial |
Is it a 360° video? |
video.expiresAt |
When the video address stops working |
video.streams |
Every available address, best one first |
If the video has subtitles, you get them as a list:
for (final track in video.textTracks) {
print(track.label); // "English"
print(track.language); // "en"
print(track.url); // a .vtt subtitle file
print(track.isAutoGenerated); // true = made by computer, less accurate
}Everything above works with no Vimeo account. But if you own the videos and keep them private, no public route can reach them — that is what private means. For that case only, you can supply a Vimeo API access token:
final resolver = VimeoResolver(accessToken: 'your-token');
final video = await resolver.resolve('https://vimeo.com/76979871');The token is used only after the normal routes have failed, so ordinary
public videos cost no extra request. It must belong to the account that owns
the video, and needs the video_files scope to return playable files (a paid
Vimeo plan). Get one from the
Vimeo developer console.
Anything inside a released app can be pulled back out of it — an API token is not a secret once it is on someone else's phone. A leaked token gives whoever finds it control of your whole Vimeo account.
Do this instead: keep the token on a server you control. Have your app ask your server for the video, let the server resolve it, and return just the stream URL. The URL expires within the hour, so it is safe to hand out; the token is not.
Subtitles are not returned on this route — Vimeo publishes those on a separate endpoint, and fetching it would cost a second request on every resolve. Use the ordinary tokenless route if you need captions.
The video address (bestStream!.url) stops working after about one hour.
Vimeo does this on purpose.
Wrong — saving the address in your database:
final video = await VimeoResolver().resolve(url);
database.save(video.bestStream!.url); // ✗ will break in an hourRight — save the normal Vimeo link, and ask again each time:
database.save('https://vimeo.com/76979871'); // ✓ never expires
// later, when you want to play it:
final video = await VimeoResolver().resolve(savedLink);The cover picture (thumbnailUrl) is different — that one does not
expire, so you can save it safely.
Every error has a message that is safe to show your user.
try {
final video = await VimeoResolver().resolve(url);
} on VimeoEmbedNotAllowedException {
print('The owner must allow this video to be embedded anywhere.');
} on VimeoPrivateLinkRequiredException {
print('Use the full private link, including the hash.');
} on VimeoNotFoundException {
print('This video was deleted.');
} on VimeoRestrictedException {
print('This video is private.');
} on VimeoNetworkException {
print('No internet. Please try again.');
} on VimeoException catch (e) {
print(e.message); // catches everything else
}Order matters: put the specific errors before the general ones, or the general one catches everything first.
| Error | What it means | What to do |
|---|---|---|
VimeoInvalidUrlException |
Not a Vimeo video link | Check the link |
VimeoNotFoundException |
Video was deleted | Nothing you can do |
VimeoEmbedNotAllowedException |
Embed privacy blocks apps | Owner sets embedding to "Anywhere" |
VimeoPrivateLinkRequiredException |
Unlisted link lost its hash | Use the full link with the hash |
VimeoRestrictedException |
Private or password protected | Ask the owner |
VimeoNetworkException |
No internet | Offer a "Try again" button |
VimeoNoStreamException |
Vimeo has no playable file | Rare; check on Vimeo |
VimeoParseException |
Vimeo changed their website | Update this package |
The last two are subtypes of VimeoRestrictedException, so code that already
catches that keeps working unchanged.
Every error carries a message safe to show a user. Refusals also carry
.reason — Vimeo's own sentence, such as "Because of its privacy settings,
this video cannot be played here." — which is what you want in your logs.
Do I need a Vimeo account or API key? No. Nothing at all.
Does it cost money? No. The package is free and Vimeo does not charge for this.
Does it work on iPhone and Android? Yes, both. Also macOS and web.
Can I play private videos?
Unlisted videos work with no setup — use the whole link, including the hash
(vimeo.com/123456/abc123). Videos you own that are fully private need an
access token. Other people's
private and password-protected videos will not play: that is the owner's
choice, and we respect it.
It plays on vimeo.com but not in my app. Why? Vimeo's embed privacy is separate from its viewing privacy. See Why isn't my video playing? — this is the single most common problem, and the fix is on Vimeo, not in your code.
Why is the video slow to start? The first time, it asks Vimeo for the video address. After that it remembers for an hour, so it is instant.
My video is blank / I see an error about size.
You forgot to wrap it in AspectRatio or SizedBox. See
One important rule above.
Can I download the video? This package does not download videos. Please respect video owners' rights.
Being honest about the limits:
- No bypassing privacy settings. If a video's embed privacy excludes apps, only its owner can change that, on Vimeo. The package will tell you that is what happened — it will not work around it.
- No password-protected videos. You will get a
VimeoRestrictedException. - No DRM-protected videos. These will not play.
- 360° videos play flat. You can tell it is a 360 video with
video.isSpatial, but you cannot look around inside it. - No downloading. Streaming only.
- It depends on Vimeo. This package reads the same information Vimeo's own player reads. Vimeo has not promised to keep it the same forever. If they change it, videos stop playing and this package needs an update. If that happens, please open an issue.
flutter test # run all tests
flutter test --exclude-tags live # skip tests that need internetSome tests talk to Vimeo for real. Those are the important ones — they are how we find out quickly if Vimeo changed something. Please keep them passing.
Bug reports and pull requests are welcome at the issue tracker.
MIT — free to use in any project, including commercial ones.