vesper_player 0.4.3-rc.1 copy "vesper_player: ^0.4.3-rc.1" to clipboard
vesper_player: ^0.4.3-rc.1 copied to clipboard

Flutter-facing Vesper Player controller and Hole Punch view.

vesper_player #

A cross-platform Flutter video player built around native-first backends:

  • Android uses ExoPlayer through the Vesper Android host kit
  • iOS uses AVPlayer through the Vesper iOS host kit

The package exposes one Dart API surface so host apps can keep playback, track selection, resilience, download, preload, and benchmark capture flows aligned across platforms.

Platform Support #

Feature Android iOS
Local files
Progressive HTTP
HLS
DASH ✅ DASH-to-HLS bridge for VOD / live fMP4
Live streams
Live DVR
Track selection
Adaptive bitrate (ABR) ⚠️ Constrained + best-effort fixed-track on iOS 15+
Buffering / retry policy
Download management
Preload
DRM direct playback ✅ Widevine through native Media3 direct paths ✅ FairPlay through native AVPlayer direct paths
System playback controls ✅ MediaSession notification + FGS ✅ Now Playing / RemoteCommand
External playback ✅ Optional vesper_player_external_playback package ✅ AirPlay route picker via vesper_player_ui

The main package registers Android and iOS implementations only. It inherits the deliberate native product boundary: Android API 26+ on arm64-v8a, and iOS 17+ on arm64 devices and Apple Silicon Simulator. Older OS versions and legacy ABIs are not a compatibility backlog. Desktop Flutter implementations are not shipped.

Installation #

Use the hosted package for normal application integration. The Android and iOS federated implementations resolve automatically:

dependencies:
  vesper_player: 0.4.3-rc.1
  # Optional unified Android Cast / DLNA external playback.
  vesper_player_external_playback: 0.4.3-rc.1
  # Optional stage controls and AirPlay route button.
  vesper_player_ui: 0.4.3-rc.1
  # Optional normalized-resource playback.
  vesper_player_source_normalizer_ffmpeg: 0.4.3-rc.1
  # Optional post-download MP4 remux.
  vesper_player_remux_ffmpeg: 0.4.3-rc.1

Repository source-checkout development writes ignored local dependency overrides with:

./scripts/vesper flutter local-overrides

External hosts that intentionally consume a source checkout must provide root-level overrides for the federated packages that vesper_player resolves through hosted constraints. The following example includes the core family plus two optional packages:

dependencies:
  vesper_player:
    path: path/to/rust-player-sdk/lib/flutter/vesper_player
  # Optional unified Android Cast / DLNA external playback.
  vesper_player_external_playback:
    path: path/to/rust-player-sdk/lib/flutter/vesper_player_external_playback
  # Optional stage controls and AirPlay route button.
  vesper_player_ui:
    path: path/to/rust-player-sdk/lib/flutter/vesper_player_ui

dependency_overrides:
  vesper_player_platform_interface:
    path: path/to/rust-player-sdk/lib/flutter/vesper_player_platform_interface
  vesper_player_android:
    path: path/to/rust-player-sdk/lib/flutter/vesper_player_android
  vesper_player_ios:
    path: path/to/rust-player-sdk/lib/flutter/vesper_player_ios

Add each other optional Vesper package as a direct hosted, path, or Git dependency only when the host enables that feature.

Native identifier baseline #

The Flutter package names remain vesper_player_*. Native registration uses the io.github.umbrella22 reverse-DNS root:

  • player MethodChannel: io.github.umbrella22.vesper_player
  • player EventChannel: io.github.umbrella22.vesper_player/events
  • Android plugin package: io.github.umbrella22.vesper.player.flutter.android
  • iOS Swift package and module: VesperPlayerKit

These identifiers replaced the unreleased io.github.ikaros names without aliases. Apps that only use Flutter plugin auto-registration need no manual registration code; custom channels, native imports, manifest entries, and serialized first-party plugin references must use the new identifiers.

Quick Start #

Minimal playback #

import 'package:vesper_player/vesper_player.dart';

// 1. Create a controller.
final controller = await VesperPlayerController.create(
  initialSource: VesperPlayerSource.hls(
    uri: 'https://example.com/stream.m3u8',
    label: 'Sample video',
  ),
);

// 2. Embed the view in your widget tree.
VesperPlayerView(controller: controller)

// 3. Start playback.
await controller.play();

// 4. Dispose when the widget goes away.
await controller.dispose();

Listen to playback state #

// Snapshot stream: emits when player state changes.
controller.snapshots.listen((snapshot) {
  print('Playback state: ${snapshot.playbackState}');
  print('Position: ${snapshot.timeline.positionMs}ms');
  print('Buffering: ${snapshot.isBuffering}');
  print('Retry attempts: ${snapshot.resiliencePolicy.retry.maxAttempts}');
});

// Event stream: emits errors and lifecycle events.
controller.events.listen((event) {
  if (event is VesperPlayerErrorEvent) {
    print('Error: ${event.error.message}');
  }
});

// You can also read the latest snapshot directly.
final snapshot = controller.snapshot;

VesperPlayerSnapshot is the authoritative runtime view of the active backend. It carries timeline state, capabilities, current track selection, the effective runtime video variant through effectiveVideoTrackId, explicit fixed-track settling state through fixedTrackStatus, raw runtime bitrate and size evidence through videoVariantObservation, the effective resiliencePolicy, and the latest surfaced playback error.

Core API #

VesperPlayerController #

The primary control surface for playback.

final controller = await VesperPlayerController.create(
  initialSource: VesperPlayerSource.hls(uri: 'https://example.com/stream.m3u8'),
  renderSurfaceKind: VesperPlayerRenderSurfaceKind.auto,
  resiliencePolicy: const VesperPlaybackResiliencePolicy.resilient(),
  trackPreferencePolicy: const VesperTrackPreferencePolicy(
    preferredAudioLanguage: 'en',
    preferredSubtitleLanguage: 'en',
  ),
);

await controller.selectSource(
  VesperPlayerSource.local(uri: '/path/to/video.mp4'),
);
await controller.play();
await controller.pause();
await controller.togglePause();
await controller.stop();

await controller.seekBy(10000);
await controller.seekToRatio(0.5);
await controller.seekToLiveEdge();

await controller.setPlaybackRate(1.5);

Mobile source and seek commands have native completion semantics. A successful selectSource completes only after the current source publishes a stable VOD, live DVR, or non-seekable live timeline. The three seek methods complete only after the active native route confirms the current seek. Superseded commands throw VesperPlayerCommandException with isObsolete == true; they reject only their originating Future and do not replace the current snapshot error.

VesperPlayerView #

Embeds the native video surface into Flutter UI.

On Android, VesperPlayerController.create(renderSurfaceKind: ...) controls the native surface used by VesperPlayerView. The default auto mode uses SurfaceView so Flutter 3.44+ hosts can take the high-fidelity native video path by default. Select textureView explicitly when a screen depends on complex Flutter overlays, scrolling, clipping, rounded corners, or animation-heavy composition. iOS accepts the option for API symmetry but always uses the platform's AVPlayer-backed surface.

VesperPlayerView(
  controller: controller,
  visible: true,
  overlay: Stack(
    children: [
      // Your overlay UI goes here.
    ],
  ),
)

System Playback #

System playback integration is optional and controlled from the Flutter controller. It enables the platform media session, lock-screen / notification controls, and background audio continuation for the active player.

final status = await controller.getSystemPlaybackPermissionStatus();
if (status == VesperSystemPlaybackPermissionStatus.denied) {
  await controller.requestSystemPlaybackPermissions();
}
await controller.configureSystemPlayback(
  const VesperSystemPlaybackConfiguration(
    metadata: VesperSystemPlaybackMetadata(
      title: 'Sample video',
      artist: 'Vesper Player SDK',
      contentUri: 'https://example.com/stream.m3u8',
    ),
    controls: VesperSystemPlaybackControls.videoDefault(),
  ),
);

await controller.updateSystemPlaybackMetadata(
  const VesperSystemPlaybackMetadata(title: 'Next episode'),
);
await controller.clearSystemPlayback();

The default configuration is enabled, continues audio in the background, shows system controls, and enables 10-second seek back / play-pause / seek forward system media actions. Custom seek offsets are clamped to 1-60 seconds, and showSeekActions: false removes seek actions even when controls includes them. The SDK supports one active system media session: the most recently configured controller owns system controls.

Host apps still own platform declarations. iOS apps must include UIBackgroundModes = audio when background playback is intended. Android apps must merge or declare foreground-service media playback permissions. Android 13+ exempts media-session playback notifications from the runtime notification permission, so a denied POST_NOTIFICATIONS result must not stop playback; use the permission API only for app-controlled notification UX.

AirPlay and Cast #

For iOS route selection, depend on vesper_player_ui and place VesperAirPlayRouteButton near your player controls, or VesperAirPlayRouteIconButton in a VesperPlayerStage top-bar action slot:

VesperAirPlayRouteButton(controller: controller)

The button is backed by AVRoutePickerView and prioritizes video-capable routes by default. Users can also continue to route from Control Center. AirDrop is file sharing, not media playback routing.

For unified Android Cast / DLNA control, depend on the optional vesper_player_external_playback package. It keeps discovery, relay, and Cast Framework dependencies outside the default player package:

final external = VesperExternalPlaybackController();
await external.startDiscovery();
await external.connect(route.routeId);
await external.load(
  VesperExternalPlaybackMediaItem(
    sources: <VesperPlayerSource>[source],
    metadata: const VesperSystemPlaybackMetadata(title: 'Sample video'),
  ),
);

Cast route selection still uses the system Cast route button. DLNA routes are reported through VesperExternalPlaybackController.routes. Sources with headers, local files, and content:// inputs are served through a tokenized local HTTP relay when the proxy policy allows it. Cast V2 direct playback still supports remote http / https HLS, DASH, and progressive sources with the default Google receiver. DRM, transcoding, DASH manifest rewrite, and custom receiver flows are not supported by this package.

Android hosts that use DLNA discovery or relay-backed local playback must declare their own cleartext HTTP policy in the app manifest or Android network security configuration. The SDK packages do not enable app-wide cleartext traffic.

Use VesperExternalRouteIconButton() from vesper_player_external_playback in a VesperPlayerStage top-bar action slot on Android to surface the system Cast route button. VesperExternalRouteButton() remains available for existing control rows.

VesperPlayerSource #

VesperPlayerSource.hls(uri: 'https://example.com/stream.m3u8')
VesperPlayerSource.dash(
  uri: 'https://example.com/manifest.mpd',
  headers: <String, String>{
    'Referer': 'https://example.com/player',
    'User-Agent': 'VesperPlayer',
  },
)
VesperPlayerSource.local(uri: '/storage/emulated/0/Movies/video.mp4')
VesperPlayerSource.remote(uri: 'https://example.com/video.mp4')

DRM sources #

VesperPlayerDrmConfiguration is a native playback configuration. Flutter sends it through the platform channel as drmConfiguration; Android maps Widevine to Media3, and iOS maps FairPlay to AVPlayer. The Flutter plugin does not process, decrypt, remux, preload, download, relay, or inspect protected media frames.

VesperPlayerSource.dash(
  uri: 'https://example.com/manifest.mpd',
  drmConfiguration: const VesperPlayerDrmConfiguration(
    keySystem: 'widevine',
    licenseUri: 'https://license.example.com/widevine',
    licenseHeaders: <String, String>{
      'Authorization': 'Bearer token',
    },
  ),
)

VesperPlayerSource.hls(
  uri: 'https://example.com/fairplay/master.m3u8',
  drmConfiguration: const VesperPlayerDrmConfiguration(
    keySystem: 'fairPlay',
    licenseUri: 'https://license.example.com/fairplay',
    fairPlayCertificateUri: 'https://license.example.com/fairplay.cer',
  ),
)

Only direct native playback routes support DRM. SourceNormalizer, SDK-managed native-frame playback, the iOS DASH bridge, download, preload, remux, and external playback relay routes reject DRM sources with an unsupported capability error. Private encryption schemes should be handled by a separate pre-decryption adapter before a normal source is given to the player; they are not part of the current Widevine / FairPlay DRM contract.

Snapshot Listenable #

VesperPlayerController also exposes snapshotListenable, a ValueNotifier<VesperPlayerSnapshot> you can pass directly to ValueListenableBuilder for granular widget rebuilds without subscribing to the snapshots stream:

ValueListenableBuilder<VesperPlayerSnapshot>(
  valueListenable: controller.snapshotListenable,
  builder: (context, snapshot, _) => Text('${snapshot.timeline.positionMs} ms'),
)

Preload Budget #

VesperPreloadBudgetPolicy can be supplied at controller creation to cap preload concurrency, memory, disk, and warm-up window:

final controller = await VesperPlayerController.create(
  preloadBudgetPolicy: const VesperPreloadBudgetPolicy(
    maxConcurrentTasks: 2,
    maxMemoryBytes: 64 * 1024 * 1024,
    warmupWindowMs: 8000,
  ),
);

Benchmark Configuration #

VesperBenchmarkConfiguration can be supplied at controller creation when you need native host-kit benchmark events during profiling:

final controller = await VesperPlayerController.create(
  benchmarkConfiguration: const VesperBenchmarkConfiguration(
    enabled: true,
    includeRawEvents: true,
    maxBufferedEvents: 2048,
    consoleLogging: true,
  ),
);

enabled turns on benchmark capture. consoleLogging is separate and remains off by default; keep it disabled in normal app builds unless you are actively tracing startup or playback behavior.

Track Selection And ABR #

final catalog = controller.snapshot.trackCatalog;
final audioTracks = catalog.audioTracks;
final videoTracks = catalog.videoTracks;

await controller.setAudioTrackSelection(
  VesperTrackSelection.track(audioTracks.first.id),
);

await controller.setAudioTrackSelection(const VesperTrackSelection.auto());
await controller.setSubtitleTrackSelection(
  const VesperTrackSelection.disabled(),
);

await controller.setSubtitleStyle(
  const VesperSubtitleStyle(fontScale: 1.25, visible: true),
);

final sourceWithSubtitles = VesperPlayerSource(
  uri: 'https://example.com/video.mp4',
  label: 'Video',
  kind: VesperPlayerSourceKind.remote,
  protocol: VesperPlayerSourceProtocol.progressive,
  externalSubtitles: const <VesperExternalSubtitleSource>[
    VesperExternalSubtitleSource(
      id: 'english-main',
      uri: 'https://example.com/subtitles/en.srt',
      mimeType: VesperExternalSubtitleSource.mimeSubrip,
      language: 'en',
      label: 'English',
    ),
  ],
);

await controller.setAbrPolicy(
  const VesperAbrPolicy.constrained(maxHeight: 720),
);

await controller.setAbrPolicy(
  VesperAbrPolicy.fixedTrack(videoTracks.last.id),
);

0.4 Subtitle Migration #

  • Android setSubtitleTrackSelection is suspending and iOS is async throws; the Dart Future<void> completes only after native confirmation.
  • Replace VesperSubtitleSideLoad with VesperExternalSubtitleSource and give every source-local track a non-empty unique id.
  • Replace subtitleConfigurations with externalSubtitles. The old names are deprecated aliases for source migration only.
  • Read snapshot.trackSelection.subtitle, confirmedSubtitle, and effectiveSubtitleTrackId as requested, confirmed, and currently rendered state. Do not parse or depend on the format of track ids.
  • subtitleState.catalogState and selectionState are independent. A selection failure preserves a ready catalog and the last confirmed/effective selection.
  • Subtitle command failures are thrown as VesperSubtitleException with code, phase, trackId, retriable, commandId, and sourceEpoch.

On iOS, VesperAbrPolicy.fixedTrack(...) is implemented as best-effort HLS variant pinning on iOS 15+, not exact AVPlayer video-track switching. Single- axis constraints such as VesperAbrPolicy.constrained(maxHeight: 720) are also supported on iOS HLS, but they are restored only after the current variant catalog is ready so the missing dimension can be inferred safely. Check supportsAbrFixedTrack and supportsVideoTrackSelection before exposing that control in product UI.

Android and iOS both surface the currently active adaptive variant through controller.snapshot.effectiveVideoTrackId. Flutter UI can combine that with trackCatalog.videoTracks to show the actual quality currently in use during auto or constrained ABR.

Both mobile backends also surface controller.snapshot.videoVariantObservation when they have direct runtime evidence for the currently rendered adaptive variant. On Android that is derived from ExoPlayer's active videoFormat; on iOS it is derived from AVPlayer access-log bitrate plus presentation size. Flutter UI can use this signal to explain what the player is currently rendering even when a stable effectiveVideoTrackId is not available yet.

On iOS, controller.snapshot.fixedTrackStatus provides an explicit runtime signal for best-effort fixedTrack convergence:

  • pending: the host is still waiting for enough runtime evidence to identify the active variant
  • locked: the observed variant has remained on the requested fixed-track target long enough to be treated as stable
  • fallback: sustained runtime evidence shows that the player is still rendering a different variant than the requested target

When fixedTrackStatus is not available on a backend, Flutter UI can still fall back to comparing the requested trackId with effectiveVideoTrackId, but new platform implementations should prefer surfacing the explicit status.

On iOS, a restored fixedTrack request that keeps rendering a different variant after sustained runtime observation is now treated as a non-fatal convergence failure. The host surfaces that through controller.snapshot.lastError and, for restore flows, automatically falls back to constrained ABR using the requested variant limits when possible, otherwise back to automatic ABR.

Live And DVR #

final timeline = controller.snapshot.timeline;

if (timeline.kind == VesperTimelineKind.liveDvr) {
  final seekableRange = timeline.seekableRange!;
  print('Seekable range: ${seekableRange.startMs}ms ~ ${seekableRange.endMs}ms');
  print('Live offset: ${timeline.liveOffsetMs}ms');

  await controller.seekToLiveEdge();

  if (timeline.isAtLiveEdge()) {
    print('Playback is currently at the live edge.');
  }
}

Resilience Policy #

Use VesperPlaybackResiliencePolicy to tune buffering, retry, and cache behavior.

final controller = await VesperPlayerController.create(
  resiliencePolicy: const VesperPlaybackResiliencePolicy.resilient(),
);

final policy = VesperPlaybackResiliencePolicy(
  buffering: const VesperBufferingPolicy.streaming(),
  retry: const VesperRetryPolicy(
    maxAttempts: 5,
    backoff: VesperRetryBackoff.exponential,
    baseDelayMs: 500,
    maxDelayMs: 8000,
  ),
  cache: const VesperCachePolicy.resilient(),
);

await controller.setPlaybackResiliencePolicy(policy);

final effectivePolicy = controller.snapshot.resiliencePolicy;
print('Active buffering preset: ${effectivePolicy.buffering.preset}');

Built-in presets:

Preset Buffering Retry Recommended for
default default default General use
balanced() balanced linear backoff Stable networks
streaming() streaming-first aggressive retries Continuous streaming
resilient() larger buffers exponential backoff x6 Weak networks
lowLatency() low latency fail fast Low-latency live playback

Download Management #

VesperDownloadManager manages local downloads, pause and resume, startup task restore, resumable partial transfers, and progress tracking.

final manager = await VesperDownloadManager.create();

final taskId = await manager.createTask(
  assetId: 'my-video-01',
  source: VesperDownloadSource.fromSource(
    source: VesperPlayerSource.hls(uri: 'https://example.com/stream.m3u8'),
  ),
  profile: const VesperDownloadProfile(
    preferredAudioLanguage: 'en',
    allowMeteredNetwork: false,
  ),
);

manager.snapshots.listen((snapshot) {
  for (final task in snapshot.tasks) {
    final ratio = task.progress.completionRatio;
    print('Task ${task.taskId}: ${(ratio! * 100).toInt()}% state=${task.state}');
  }
});

await manager.pauseTask(taskId!);
await manager.resumeTask(taskId);
await manager.removeTask(taskId);
await manager.dispose();

Mobile prepare-phase download flow #

For remote VOD HLS, static DASH, and FLV downloads, the Android and iOS host kits now run a native prepare phase before transfer starts. Flutter apps can pass the entry-point source into createTask(...) with an empty VesperDownloadAssetIndex; the host kit expands manifests, resolves byte ranges, probes every remote byte total, writes local rewritten manifests or concat lists, and then emits VesperDownloadTaskUpdatedEvent with a full task before the first progress patch.

Recommended host flow:

  1. Insert a temporary "preparing" row in the app UI as soon as the user taps download.
  2. Call createTask(...) with VesperDownloadProfile(targetDirectory: ...). Set targetOutputFormat: VesperDownloadOutputFormat.mp4 for HLS, DASH, and FLV segmented sources when the desired completed artifact is MP4.
  3. Replace the temporary row with the real task and listen to manager.snapshots; the snapshot is updated from taskCreated, taskUpdated, and taskRemoved events, so total bytes and segment counts appear before transfer progress.

Hosts may still pass a prebuilt VesperDownloadAssetIndex for custom catalogs. In that case the native prepare phase completes missing resource sizes before download. Pause, resume, and remove operations should be keyed by taskId, not by URL.

Headers on VesperPlayerSource.headers are forwarded by the Android and iOS host kits during download preparation and transfer. Use them for generic HTTP context such as User-Agent, Referer, Origin, Cookie, or authorization headers; the SDK applies them to manifest reads, size probes, and media transfers, and ignores empty header names or blank values.

The default VesperDownloadConfiguration enables restoreTasksOnStartup and resumePartialDownloads. Android and iOS persist task snapshots under the download base directory, restore interrupted preparing/downloading tasks on the next manager creation, and resume existing partial remote files with range requests when the server supports them. Complete resources stream by default, Range: bytes=<existing>- is used for resume, and fixed Range chunks are used only when rangeChunkBytes is configured. If a server ignores a resume range, only that partial resource is deleted and restarted from byte zero; expired or unavailable URLs fail with a stale-resource error. This is SDK-managed foreground download recovery; OS-managed process-death background transfer is not enabled by default and remains a separate host opt-in design.

On iOS, offline media URLs must be HTTPS because the SDK does not relax App Transport Security for http:// resources. On Android, downloads are stored under the app-private files directory by default. Use shareTaskOutput(...) for the native share sheet or Android FileProvider, and saveTaskOutput(...) for the iOS document export flow or Android 10+ MediaStore Downloads / Movies. On Android 9 and older, use shareTaskOutput(...)/FileProvider or a host-owned export flow because the SDK does not request legacy public storage permissions.

Optional .mp4 export through player-remux-ffmpeg #

player-remux-ffmpeg is an optional dynamic plugin that remuxes downloaded HLS, DASH, or FLV assets into .mp4. Android hosts must package the shared vesper-player-kit-ffmpeg-runtime AAR separately. Flutter iOS hosts embed the signed VesperPlayerRemuxFfmpegPlugin.framework together with the sibling VesperFFmpegAVCodec, VesperFFmpegAVFormat, and VesperFFmpegAVUtil frameworks. Export becomes available only after the host app packages the runtime components and plugin, then selects the packaged plugin with a native VesperPluginReference.

For hosted Flutter integration, add vesper_player_remux_ffmpeg at the same version as vesper_player. Its Android dependency resolves the matching core, remux, and shared FFmpeg runtime Maven closure; its iOS package resolves the matching VesperPlayerRemuxFfmpeg SwiftPM capability product.

final remuxPlugin = VesperPluginReference(
  pluginId: 'io.github.umbrella22.vesper.remux-ffmpeg',
  capabilityInstanceId:
      'io.github.umbrella22.vesper.remux-ffmpeg.post-download',
  transport: VesperPluginTransport.native,
);

final manager = await VesperDownloadManager.create(
  configuration: VesperDownloadConfiguration(
    runPostProcessorsOnCompletion: false,
    postDownloadPluginReferences: <VesperPluginReference>[remuxPlugin],
  ),
);

manager.events.listen((event) {
  if (event is VesperDownloadExportProgressEvent) {
    print('task ${event.taskId}: ${(event.ratio * 100).toInt()}%');
  }
});

await manager.exportTaskOutput(taskId, '/path/to/output.mp4');
await manager.shareTaskOutput(taskId, fileName: 'movie.mp4', mimeType: 'video/mp4');
final savedUri = await manager.saveTaskOutput(
  taskId,
  fileName: 'movie.mp4',
  collection: VesperDownloadPublicCollection.movies,
);

Key points:

  • The reference selects an already packaged Android libvesper_remux_ffmpeg.so or signed iOS remux plugin framework. FFmpeg component frameworks are dependencies, not plugin references.
  • exportTaskOutput(...) triggers the plugin and reports progress through VesperDownloadExportProgressEvent.
  • Hosted consumers use the optional Flutter package above. The source-checkout mobile examples additionally exercise local Gradle generation and the local seven-framework iOS staging path for release verification.
  • Depending on vesper_player alone does not pull FFmpeg into your app. That keeps app size stable when export is not needed.
  • FFmpeg prebuilts are selected through ./scripts/vesper ffmpeg --platform android|ios --profile <name>. The default mobile profiles stay local-only and validate that network and OpenSSL remain disabled.
  • If the host bundles the remux plugin, treat it as an FFmpeg redistribution: include FFmpeg license text and notices, provide corresponding FFmpeg source and configure flags, preserve LGPL relinking rights, and track OpenSSL / libxml2 notices when those libraries are included. See THIRD_PARTY_NOTICES.md.

Optional mobile plugin diagnostics #

VesperPlayerController.create(...) accepts three optional mobile plugin configurations:

  • sourceNormalizerConfiguration with disabled, diagnosticsOnly, preflightOnly, preferNormalized, and requireNormalized modes
  • frameProcessorConfiguration with disabled and diagnosticsOnly modes
  • nativeFramePipelineConfiguration with disabled, diagnosticsOnly, preferNativeFrame, and requireNativeFrame modes

All three are disabled by default. Apps can depend on the optional vesper_player_source_normalizer_ffmpeg package and pass VesperSourceNormalizerConfiguration.preferBundled() or VesperSourceNormalizerConfiguration.requireBundled() to use the Android AAR or the iOS host-embedded plugin without app-side path lookup code. On iOS, the optional Flutter package resolves the VesperPlayerSourceNormalizerFfmpeg capability product, which embeds the plugin and its three VesperFFmpeg* dependencies. Flutter does not expose binary paths or an aggregate optional-plugin product. Custom builds also select plugins with VesperPluginReference; the Android embedded registry or iOS host resolver must map the selected identity to its build-time artifact. Flutter does not accept arbitrary plugin binary paths. Android and iOS FFmpeg runtime libraries remain ordinary dynamic dependencies, not plugin references.

SourceNormalizer mobile can load the optional FFmpeg plugin, report capability diagnostics in controller.pluginDiagnostics, and in preflightOnly mode attempt an open/close packet-session check for the selected source. preferNormalized and requireNormalized are opt-in host-kit paths that may replace the platform source with a disk-backed fMP4 or short-window HLS resource. preferNormalized falls back to the original source when normalization fails; requireNormalized reports a source error. Standard HLS and DASH stay native-first by default unless normalization is explicitly required or forced by a test profile. The repository smoke expectations live in fixtures/media/source-normalizer-smoke-matrix.json.

FrameProcessor mobile remains opt-in and is only consumed by the explicit Native Frame Pipeline route. The host kits now report native-frame diagnostics for SourceNormalizer packet input, platform decoder adapter, presenter profile, fallback reason, and counters. iOS local/VOD SDR native-frame playback can run through VideoToolbox, MetalLayer presentation, and the Swift native audio bridge. Android can use the explicit MediaCodec/SurfaceView packet route when the selected plugin references resolve to embedded artifacts. HDR and Dolby Vision stay on platform system playback; the SDK-managed native-frame lane is SDR-only today and is not an HDR-ready path. probePlaybackCapability reports recommendedPlaybackPath = systemPlayer and emits a capability warning with hdrNativeFrameUnsupported instead of treating SDK-managed native-frame as HDR support. requireNativeFrame reports a capability error when the requested native-frame lane is unavailable instead of silently using the system player. Default mobile playback is unchanged.

Download task states:

queued -> preparing -> downloading -> completed
                  \-> paused ->/
                  \-> failed
                  \-> removed

Capability Discovery #

Platform and backend support is reported through VesperPlayerCapabilities, so apps can guard unsupported features without relying on exception handling.

final caps = controller.snapshot.capabilities;

if (caps.supportsDash) {
  // DASH is available on the current backend.
}

if (caps.supportsTrackSelection) {
  // Track selection is supported.
}

if (caps.supportsAbrFixedTrack) {
  // Fixed-track ABR pinning is available on this backend.
  // On iOS this is best-effort variant pinning, not exact track switching.
}

if (caps.isExperimental) {
  // The current backend is still experimental.
}
Package Description
vesper_player_platform_interface Shared platform contract and DTOs
vesper_player_android Android implementation built on ExoPlayer
vesper_player_ios iOS implementation built on AVPlayer
0
likes
0
points
302
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter-facing Vesper Player controller and Hole Punch view.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, vesper_player_android, vesper_player_ios, vesper_player_platform_interface

More

Packages that depend on vesper_player

Packages that implement vesper_player