orvexa_player 0.1.2
orvexa_player: ^0.1.2 copied to clipboard
A Flutter MP4/HLS video player with customizable controls, fullscreen, video and Widget ads, VAST/VMAP, and opt-in native picture in picture.
Orvexa Player #
A composable Flutter video player for Android, iOS, macOS, Windows, Linux and web, using media_kit on native platforms and HTML video with HLS.js on web. Version 0.1.0 is the initial release; the API may evolve before 1.0. Native picture in picture is experimental and opt-in. See verification for tested capabilities and platform limitations.
The package owns no application state, analytics or storage. Sources are opened only by the host. The optional VAST/VMAP loader makes explicit ad-tag and tracking requests; hosts can replace both transports. One controller owns one native player.
Installation #
dependencies:
orvexa_player: ^0.1.2
Requires Flutter 3.38.1+ and Dart 3.10.0 or later, below Dart 4.0. Complete the platform setup for the targets your application uses. The example shows a minimal player; the full example app also includes default and custom Hugeicons controls, video/Widget ads and VAST/VMAP fixtures.
Quick start #
import 'package:flutter/material.dart';
import 'package:orvexa_player/orvexa_player.dart';
Future<PlayerController> createLessonController() async {
final controller = PlayerController();
await controller.open(
PlayerSource(
id: 'lesson-1',
uri: Uri.parse('https://example.org/lesson.mp4'),
title: 'Lesson one',
),
);
return controller;
}
Widget lessonView(PlayerController controller) => OrvexaPlayer(
controller: controller,
);
// Call after unmounting the presentation.
Future<void> releaseLessonController(PlayerController controller) =>
controller.close();
Create controllers in the owning component's initialization, never in build. dispose() initiates release; close() also lets the owner await its completion. Do not reuse a closed controller. A widget does not dispose a controller supplied by the host.
PlayerSource accepts remote or file URIs and optional HTTP headers. File access remains subject to platform sandbox permissions. MP4/HLS decoding depends on the installed native libraries, codecs and the source. Changing engines cannot guarantee compatibility with every manifest or codec.
controller.state.videoWidth, videoHeight and nullable aspectRatio expose current video metadata for host layouts. Dimensions are zero and the aspect ratio is null until known, including audio-only sources; adaptive playback can update them. Hosts may render controller.engine.buildSurface(fit: ...) directly when providing their own complete presentation and lifecycle policy.
Fully replaceable controls #
OrvexaPlayer(
controller: controller,
fullscreen: fullscreenPolicy,
controls: PlayerControls(
builder: (context, scope) => Center(
child: PlayerButton(
scope: scope,
label: scope.strings[scope.state.playing ? 'pause' : 'play'],
icon: Icon(scope.state.playing ? Icons.pause : Icons.play_arrow),
onPressed: scope.controller.togglePlayback,
),
),
),
);
The default skin uses Hugeicons, a cinematic gradient, idle control hiding, a buffered timeline and a single settings menu. Compact layouts add central ten-second seek actions. Controls stay visible while paused, buffering, interacting or showing ads, and respect reduced motion and keyboard focus.
A complete builder replaces the default skin. header, center, bottom, progress and actions replace individual zones. menu replaces quality, speed, audio, subtitle, chapter and episode presentations. overlay replaces loading, failure/retry, completed, resume and advertisement presentations. All receive a PlayerScope containing controller, state, strings, theme, menu actions and fullscreen actions. Builders can order arbitrary widgets and use any icon package without adding it to this package.
The same configuration and controller are used in fullscreen. A keyed surface is moved between inline and fullscreen presentations, preserving local control state as well as playback state. Default controls use wrapping action targets and scrollable content at constrained sizes. Pointer movement, keyboard focus and interaction reveal the controls; idle hiding applies during ordinary playback.
Themes, languages and access #
PlayerTheme configures foreground, scrim, accent, typography, spacing, radius and target/icon sizes. Default touch targets are at least 48 logical pixels. Controls contain tooltips and semantic status; selections have check marks. Status fades use animationDuration and become immediate when the host requests reduced motion.
PlayerStrings contains English, Spanish, Portuguese, French, Italian, German, Russian, Chinese and Arabic labels. It follows the host locale or an explicitly supplied language. Arabic uses RTL. Override individual labels using PlayerStrings('es', overrides: {'play': 'Ver'}). No localization framework is required. Native engine or host metadata titles remain host-provided strings.
Space toggles playback, arrows seek 10 seconds, M toggles mute, F toggles fullscreen and Escape leaves fullscreen when the player has focus. Tab traversal reaches buttons. Double-tap on the unobstructed video surface toggles fullscreen and can be disabled with touchGestures: false. Enable wheelVolume to adjust volume with the wheel over the player; it is off by default so parent scrolling stays unchanged. Scrollables inside custom controls take priority. More advanced touch gestures are not implemented.
Tracks, chapters, episodes and thumbnails #
Menus show current engine-reported tracks only. Automatic video selection means engine selection, not verified continuous adaptive bitrate switching. parseHlsVariants discovers real master entries and retains their audio/subtitle group references. Manifest-only variants are not selectable: reopening their URLs can lose alternate groups. No fictitious resolution ladder or second warm-up player is created.
Embedded audio/subtitle tracks and external SRT/WebVTT subtitle URLs are selectable. External subtitle requests do not automatically inherit source headers. MediaKitEngine(subtitleStyle: PlayerSubtitleStyle(...)) configures Flutter-rendered subtitle typography, padding and alignment.
Chapters are immutable titles and positions supplied by the host. setEpisodes accepts grouped episodes with asynchronous source resolvers. previous, next, selectEpisode and optional autoAdvance share cancellation protection. No endpoints are assumed.
Use parseThumbnailVtt(text, baseUri) to parse host-fetched WebVTT, then supply PlayerSource.thumbnails. The default slider previews cues while dragging and commits one seek on release. SeekThumbnail also renders individual images and xywh sprites independently. Missing cues/images fall back to a timestamp. Protected thumbnail requests require explicitly supplied headers on a custom SeekThumbnail; media headers are never forwarded to unrelated image origins automatically. A poster URI can be provided separately.
Resume and optional persistence #
initialPosition resumes automatically by default. open(..., resume: ResumeMode.offer) shows continue/start-over actions; ResumeMode.disabled ignores the initial position. onProgress reports content progress for host storage, excluding advertisement time. The example stores positions in memory only.
Implement PlayerPreferenceStore in the host and explicitly call restorePreferences or savePreferences to persist volume, mute, speed and repeat. There is no default disk storage, identifier, analytics or third-party service.
Video and interactive widget advertisements #
VideoAd and WidgetAd share pre-roll, mid-roll and post-roll scheduling and can be mixed in open(..., ads: [...]). Widget ads pause the content without reopening it. Their optional duration counts active playback time; omit it to wait for an interaction that calls completeAd(). skipAfter enables the standard skip action. Pause and fullscreen remain available.
await controller.open(source, ads: [
const WidgetAd(
id: 'interactive-offer',
duration: Duration(seconds: 12),
skipAfter: Duration(seconds: 3),
),
]);
OrvexaPlayer(
controller: controller,
controls: PlayerControls(
advertisement: (context, scope, ad) => Center(
child: FilledButton(
onPressed: scope.controller.completeAd,
child: const Text('Continue video'),
),
),
),
);
The advertisement builder accepts arbitrary Flutter widgets, including forms and buttons, and works with default or custom controls. onAdEvent and activeAd expose PlayerAd; inspect VideoAd or WidgetAd when type-specific fields are needed. A custom overlay builder takes precedence over the advertisement builder.
open(source, ads: [...]) supports pre-roll, timed mid-roll and post-roll. VideoAd includes skip timing and an optional destination. onAdDestination delegates destination opening to the host. Ad callbacks report start, progress, completion, skip and failure; content progress excludes ad time. Seek and track changes are disabled during ads. Content position, playback intent and track selections are restored after an ad using the same engine.
AdFailurePolicy.continueContent is the default; stop exposes failure and stops. An explicit content retry starts without ads. Ads are deduplicated by ID within one open session, so each ID must be unique. Rebuilds and fullscreen do not reopen sources.
VastAdLoader resolves linear VAST creatives and VMAP breaks into VastVideoAd, a VideoAd subtype which can be mixed with direct video and Widget advertisements. It supports bounded wrappers, sequence-ordered pods, impressions, progress/skip/click/error tracking and configurable transports. See VAST/VMAP API and supported subset. IMA and third-party advertising SDK integrations remain unsupported.
Fullscreen and lifecycle #
Fullscreen is automatic: OrvexaPlayer(controller: controller) detects web, Android/iOS, macOS, Windows or Linux. Explicit fullscreen: PlayerFullscreen.auto() is equivalent. Desktop uses window_manager, initializes it on demand, preserves the previous native fullscreen state and waits for transition completion (Cocoa completion events on macOS). No host window channel or startup initialization is required.
On mobile, automatic mode requests immersive system bars, leaves the app's orientation policy unchanged and restores SystemUiMode.edgeToEdge on exit. Flutter cannot read the prior system UI mode; hosts using a different policy can supply PlayerFullscreen.auto(restoreMode: ..., restoreOverlays: ...). Android targeting API 35 may require the platform migration for immersive bars; API 36+ enforces edge-to-edge, so the OS can keep bars visible. The fullscreen route still expands the player. Use PlayerFullscreen.mobile(...) for explicit landscape and orientation restoration, or the callback constructor for custom native policies. Fuchsia is unsupported.
In browsers, automatic fullscreen requests the document Fullscreen API from the user's action, retains custom controls and synchronizes the route when the browser exits with Escape. An existing host fullscreen session is preserved. If document fullscreen is unavailable or denied, the player expands within the browser viewport. Embedding pages must allow fullscreen where required.
Native picture in picture #
Opt in with PlayerController(enablePictureInPicture: true). Default controls show a Hugeicons PiP action on supported Android/iOS devices. Custom controls should call scope.enterPictureInPicture() so Android first prepares the fullscreen player. Observe controller.pictureInPicture and request restoration with exitPictureInPicture().
Android requires API 26+ and host activity PiP declarations. iOS requires iOS 14+, a supported device and the audio background capability; the opt-in iOS backend uses one AVPlayer/AVPlayerLayer for inline, fullscreen and PiP. Its video quality is automatic and subtitle selection covers embedded tracks; external SRT/VTT requires the default backend. Video advertisements retain the same player, while interactive Widget ads wait until PiP closes. See platform setup, lifecycle and limitations.
Web playback #
The same controller, OrvexaPlayer, default controls and custom control builders work with an owned HTML video adapter. No libmpv binary runs in the browser. The example includes a web entry point and both the default and Hugeicons interfaces.
- MP4 and other formats depend on browser codecs. HLS keeps native playback on Apple WebKit and prefers the bundled HLS.js/MSE path on other browsers, including Chromium versions that advertise native HLS. Native HLS is a fallback when MSE is unavailable. No separate CDN script is needed.
- Load the source paused, then let the user press Play. Browser autoplay policy can reject playback started without a user gesture. The example follows this flow on web.
- Host media, playlists, segments, subtitles and thumbnail images with appropriate HTTPS and cross-origin access. Browser security restrictions cannot be bypassed by this package.
- Use HTTP(S) URLs or browser-owned blob URLs. Native
file:URIs and custom media HTTP headers are rejected on web. Use suitable signed URLs or same-origin cookie authentication instead. - Manual internal video/audio track selection is unavailable in the selected web backend. The default quality/audio buttons are disabled on web; custom menus show engine selection without issuing unsupported commands. External subtitle URLs remain subject to the browser's format and CORS support.
- A web-only pointer interception layer keeps Flutter controls clickable above the HTML video element. Native platform controls remain Flutter widgets.
On macOS, Chrome 155 and Firefox 158 have passed MP4/HLS scenarios with video, Widget and mixed advertisements, fullscreen entry/exit by button and Escape, replay, and recovery after a missing source or failed advertisement. These are short scenarios; Safari validation and prolonged playback remain pending. See verification for the fixtures and exact coverage. The original W3C MP4 fixture fails in both standalone HTML video and Orvexa on this host, including installed Chrome; that fixture-specific decoder failure is not caused by the package. The web adapter gives replacement sources their own video pipeline, disposes the previous HLS.js instance, and waits for metadata and surface attachment before playback. Video ads restore the saved content position; post-roll keeps the completed timeline while preparing replay from zero. Error state exposes a safe failureKind (including decode and playback-blocked categories) without retaining backend diagnostic text. Web support is not a promise of identical codec or track behavior across browsers.
The presentation pauses on native interruptions and when a browser tab becomes hidden by default, except while entering or using opted-in native PiP. A visible browser tab losing input focus does not pause playback. It does not auto-resume after interruption. Set pauseOnInactive: false to disable lifecycle-driven pauses. No background service is installed; iOS PiP requires the host background capability above. Device audio interruptions and fullscreen restoration still require platform runtime validation. Detach the presentation before closing its controller.
Rapid source selections use latest-request-wins and serialize native work. Obsolete episode resolvers cannot override later selections. A native open already in flight cannot be physically cancelled through the current adapter: it finishes before queued work or native disposal. A hanging native open can therefore delay shutdown. No on-disk media cache is created by this package; engine buffering and image cache behavior follow upstream defaults. Callbacks are synchronous notifications and must not throw. The adapter does not print native errors or HTTP headers. During verification, native FFmpeg itself emitted media URLs to process stderr; protected URL logging behavior must be audited before production use. On native platforms, prefer runtime authentication headers over secrets embedded in URLs. On web, use short-lived signed URLs or suitable same-origin authentication.
Pass the same explicitly created PlaybackGroup to controllers that must not play concurrently. The group pauses other members before starting playback and is not a global singleton. The example creates its second instance only when requested.
Platform setup #
Use Flutter 3.38.1+ with Dart 3.10.0 or later, below Dart 4.0. See SDK compatibility for the minimum and current SDK checks and dependency resolutions. Runtime dependencies include media_kit (default native playback), media_kit_video (rendering), media_kit_libs_video (native binaries), window_manager (desktop fullscreen), web (browser APIs), pointer_interceptor (web control input), Hugeicons (controls), http and xml (VAST/VMAP). The optional iOS PiP backend uses system AVFoundation/AVKit. Do not combine the native audio-only and video bundles.
- Android: use minSdk 21 or later, Java 17 and compileSdk 36 or later for the resolved native dependencies, and enable
android.permission.INTERNETfor remote sources. Native PiP requires API 26+. No storage or background service permission is required by default. - iOS: use a deployment target of iOS 13 or later, CocoaPods and HTTPS for remote media. Native PiP requires iOS 14+ and a supported device. Background audio is needed only for opted-in PiP and must be enabled by the host. Sandboxed files require host access.
- macOS: sandboxed applications need
com.apple.security.network.clientfor remote playback; user-selected files need appropriate host entitlements/access. Native dependencies currently require CocoaPods. - Windows: include the resolved native video libraries with the application.
- Linux: install the libmpv/runtime and build packages required by the distribution and media_kit. This package does not install system packages.
- Web: serve the generated Flutter assets over HTTP(S), including media_kit's bundled HLS.js asset. No native window manager initialization is needed. Wasm output is not claimed for the selected dependency versions.
Consult the official media_kit configuration for current native requirements. Package metadata claiming platform support is not evidence of successful playback.
Scope #
See architecture, third-party notices and verification and limitations. DRM, offline downloads and IMA/other advertising SDK integrations remain future work. Safari, prolonged playback and native device coverage remain incomplete; declared platform support does not guarantee every codec or device combination.