byteark_player_flutter 2.0.1 copy "byteark_player_flutter: ^2.0.1" to clipboard
byteark_player_flutter: ^2.0.1 copied to clipboard

ByteArkPlayerFlutter is a Flutter plugin for the ByteArk Player, designed to enable seamless video playback and advanced player management within your Flutter applications.

ByteArk Player Plugin #

Pub Version

The ByteArk Player plugin is a powerful and flexible video player package designed for seamless integration into Flutter applications. It supports HLS / DASH / MP4 playback, DRM-protected streams (Widevine + FairPlay), VAST/VMAP ads, Picture-in-Picture, multi-player layouts, and Lighthouse analytics — across Android, iOS, and Flutter Web with a single Dart API.

Disclaimer: This is the non-commercial version of ByteArk Player. Commercial use and/or business support requires a license. Please contact sales@byteark.com for more information about our solutions.

Android iOS Web
Support SDK 21+ (compileSdk 35, AGP 8+) iOS 14.0+, Xcode 17+ Flutter 3.22+, modern browsers (Chrome/Edge/Safari)

The screenshot

💡 Contributing? Read CONTRIBUTING.md for the MR-title and commit conventions.


Contents #


What's new in 2.0 #

⚠️ 2.0 has breaking changes. Highlights:

  • ByteArkPlayerItem.urlsources: List<ByteArkPlayerSource> (non-empty list, asserted at construction).
  • ByteArkDrm(widevineDrm:, fairPlayDrm:) → sealed ByteArkDrm base; use WidevineDrm(…) / FairPlayDrm(…) directly per source.
  • Cross-platform DRM declaration — no more Platform.is* URL branching in your host code.
  • Web DRM end-to-end via the rewritten DrmConfigMapper + new WebSourcesMapper.
  • FairPlayDrm.cerfificateUrl misspelled alias removed (use certificateUrl).
  • Web sizing now requires a CSS override or aspectRatio config. The plugin no longer forwards fill: true to the SDK; host apps add .video-js { width: 100% !important; height: 100% !important } to web/index.html or set ByteArkPlayerConfig.aspectRatio: 'W:H'. See Web configuration.
  • Item metadata forwarded to each web source. ByteArkPlayerItem.title, subtitle, mediaId, and posterImage are now spread onto every emitted JS source as title, subtitle, videoId, and poster. title and videoId are required by the web SDK when Lighthouse is enabled. Mobile already forwards all four via the native mappers (subtitle maps to the iOS SDK's detail field). The web path used to drop all four.

See Migrating from 1.x to 2.0 for the full migration with before/after code.


Installation #

Prerequisites #

Before integrating, request the following credentials from the ByteArk team (sales@byteark.com):

  • Android and iOS license keys — passed to ByteArkPlayerLicenseKey(android: ..., iOS: ...). Without valid keys the SDK silently refuses to render the player.
  • GitLab Maven private tokens — required on Android to pull the ByteArk Player and ByteArk Lighthouse Maven repos. Stored in android/local.properties (see Android configuration below).
  • SSH access to ByteArk's iOS spec repositories on GitHub — required so CocoaPods can fetch byteark-player-sdk-ios-specs and lighthouse-sdk-native-ios-specs.
  • Lighthouse projectId (optional) — only needed if you enable Lighthouse analytics via ByteArkLighthouseSetting.

Flutter integration #

  1. Add the dependency:

    flutter pub add byteark_player_flutter
    

    Or manually under dependencies: in pubspec.yaml:

    dependencies:
      byteark_player_flutter: ^2.0.0 # Use the latest version from pub.dev.
    
  2. Import in your Dart code:

    import 'package:byteark_player_flutter/presentation/byteark_player.dart';
    // ...
    ByteArkPlayer(playerConfig: playerConfig, controller: controller)
    

iOS configuration #

⚠️ Xcode 17+ required. The vendor SDK (ByteArkPlayerSDK ~> 0.4.0) ships as a Swift 6.3 build, which Xcode 16 cannot type-check. If you must stay on Xcode 16, pin byteark_player_flutter: 1.1.6 in your pubspec.yaml.

  1. CocoaPods installs the SDK directly from a private GitHub repository over SSH. If you haven't set an SSH key on your GitHub account, follow Adding a new SSH key to your GitHub account.

  2. Open your ios/Podfile and add the source lines:

    platform :ios, '14.0'
    # ...
    source 'https://github.com/CocoaPods/Specs.git'
    source 'https://github.com/byteark/byteark-player-sdk-ios-specs.git'
    source 'https://github.com/byteark/lighthouse-sdk-native-ios-specs.git'
    
  3. Install the Pods:

    cd ios && pod install --repo-update
    
  4. Open the generated .xcworkspace in Xcode and build.

Android configuration #

⚠️ Toolchain floors. Host app must use AGP ≥ 8.6, Gradle ≥ 8.14, Kotlin ≥ 1.9, compileSdk ≥ 35, and JDK 17 for both sourceCompatibility and kotlinOptions.jvmTarget. Older toolchains will fail at Gradle sync.

  1. Update android/app/src/main/AndroidManifest.xml:

    • Add required permissions inside <manifest>:

      <uses-permission android:name="android.permission.INTERNET"/>
      <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
      <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
      <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
      
    • Set an AppCompat-based theme inside <activity>:

      <meta-data
          android:name="io.flutter.embedding.android.NormalTheme"
          android:resource="@style/Theme.AppCompat"/>
      
    • Declare the ByteArk Player Service inside <application>:

      <service android:name="com.byteark.bytearkplayercore.handler.exoplayer.service.ByteArkPlayerService"
          android:enabled="true"
          android:exported="true">
        <intent-filter>
          <action android:name="androidx.media3.session.MediaLibraryService"/>
          <action android:name="android.media.browse.MediaBrowserService"/>
        </intent-filter>
      </service>
      
    • If ads are enabled, add your AdMob app ID:

      <meta-data
          android:name="com.google.android.gms.ads.APPLICATION_ID"
          android:value="ca-app-pub-3940256099942544~3347511713"/>
      

      ⚠️ The ID above is Google's public sample AdMob app ID. Replace it with your own before shipping; apps released with the sample ID risk being flagged by Google Play.

  2. Extend FlutterFragmentActivity in MainActivity.kt:

    import io.flutter.embedding.android.FlutterFragmentActivity
    
    class MainActivity: FlutterFragmentActivity()
    
  3. Configure android/local.properties with your GitLab tokens:

    gitLabByteArkPlayerPrivateToken=[YOUR_PRIVATE_TOKEN]
    gitLabByteArkLighthousePrivateToken=[YOUR_PRIVATE_TOKEN]
    

Web configuration #

Flutter Web support wraps the ByteArk Player Web SDK. Setup is two HTML edits; no Dart-side changes from the mobile path.

  1. Add the ByteArk Player Web <script> tag to your host app's web/index.html inside <head>, before the Flutter bootstrap script:

    <head>
      <!-- ... your existing tags ... -->
      <script defer src="https://byteark-sdk.cdn.byteark.com/player/v2/byteark-player.min.js"></script>
    </head>
    <body>
      <script src="flutter_bootstrap.js" async></script>
    </body>
    

    The plugin pins the v2 major version of ByteArk Player Web. The plugin's major bumps in lockstep with the SDK major — don't point the script tag at a different major or you'll hit a runtime TypeError on the first SDK call.

  2. Add the .video-js size override to the same <head> so the JS player respects the Flutter-managed container size:

    <style>
      .video-js { width: 100% !important; height: 100% !important; }
    </style>
    

    The plugin no longer forwards a fill: true flag to the SDK — without this override, the SDK falls back to its videojs-derived ~432×243 pixel default and ignores the Flutter container. !important is required because the SDK writes inline width / height on .video-js at construction. As an alternative to the CSS override, set ByteArkPlayerConfig.aspectRatio (e.g. '9:16') — see API reference → ByteArkPlayerConfig.

  3. That's it. The plugin asserts window.bytearkPlayer exists at the first ByteArkPlayer widget mount; if the script tag is missing it throws a StateError naming this section.

Deployment-specific concerns (CSP directives, iframe permissions, version pinning details, responsive layout) are documented in Web platform notes below.

Cross-platform notes

A few ByteArkPlayerConfig fields behave differently on web; the public Dart API is the same on all three platforms but the runtime behaviour diverges per the table below.

Field / API Web behaviour
licenseKey Silently ignored. ByteArk Player Web has no license-key equivalent. Web-only consumers pass empty strings.
autoPlay: true (or null) Maps to ByteArk Player Web's 'any' mode — the SDK attempts audible autoplay first, falls back to muted if the browser blocks it, then waits for a user gesture if both attempts fail. Mobile is unaffected (true is straight autoplay there).
ByteArkAdsSettings.autoplayAdsMuted Web-only field. Maps to the SDK's autoplayadsmuted option; set to true to start preroll ads muted on browsers that block audible-ads autoplay. Mobile silently ignores.
aspectRatio: 'W:H' (e.g. '9:16') Web-only field. Maps to the SDK's aspectRatio option — videojs sizes the player to width × H/W instead of using the source's intrinsic ratio. Useful for portrait reels layouts. Mobile silently ignores; on mobile, drive the aspect ratio via Flutter's AspectRatio widget instead.
ByteArkPlayerItem.title / subtitle / mediaId / posterImage Forwarded to every emitted JS source as title / subtitle / videoId / poster respectively. The web SDK puts metadata on each source rather than on the player; for multi-source declarations (e.g. Multi-DRM FairPlay + Widevine) the same metadata is repeated across each source because conceptually they're the same media. Mobile reads these from the Item directly — same Dart inputs work on all three platforms. title and videoId are required by the web SDK when Lighthouse is enabled.
secureSurface: true Mobile-only (Android FLAG_SECURE). On web the plugin emits a one-time debugPrint warning and otherwise no-ops. Do not rely on this flag for content protection on web — use a DRM source instead.
onPlayerEnterPictureInPictureMode / onPlayerExitPictureInPictureMode Chromium and Safari only. Firefox doesn't expose Picture-in-Picture JavaScript events, so the Listener stays silent on Firefox even though the user can still toggle PiP via the browser's native video controls. No error fires — it's a browser-API gap, not a bug. toggleFullScreen() itself works cross-browser via the standard Fullscreen API.
ByteArkPlayerEventChannel.stream iOS/Android only. On web the stream emits no events; consume Player events through ByteArkPlayerListener instead.

Quick start #

A minimal Flutter app that mounts the player, drives playback through a controller, and listens for events.

💡 Looking for runnable demos? The repo ships with a sample app under example/ containing screens for the basic player, the controller API, listener events, ads, multi-DRM, playlist, vertical video, seek, progress tracking, and Lighthouse analytics. Run it with cd example && flutter run after providing license keys.

import 'package:byteark_player_flutter/data/byteark_player_config.dart';
import 'package:byteark_player_flutter/data/byteark_player_item.dart';
import 'package:byteark_player_flutter/data/byteark_player_license_key.dart';
import 'package:byteark_player_flutter/data/byteark_player_source.dart';
import 'package:byteark_player_flutter/domain/byteark_player_listener.dart';
import 'package:byteark_player_flutter/domain/method_channel/byteark_player_controller.dart';
import 'package:byteark_player_flutter/presentation/byteark_player.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  late final ByteArkPlayerController _controller;
  late final ByteArkPlayerConfig _config;

  @override
  void initState() {
    super.initState();

    // Step 1: Create the controller with an optional listener.
    _controller = ByteArkPlayerController(
      listener: ByteArkPlayerListener(
        onPlayerReady: () => debugPrint('Player is ready.'),
        onAdsStart: (data) => debugPrint('Ad started: ${data.toMap()}'),
      ),
    );

    // Step 2: Define the video source(s).
    final item = ByteArkPlayerItem(
      sources: [
        ByteArkPlayerSource(
          url:
              'https://byteark-playertzxedwv.stream-playlist.byteark.com/streams/TZyZheqEJUwC/playlist.m3u8',
        ),
      ],
    );

    // Step 3: Configure the player.
    _config = ByteArkPlayerConfig(
      licenseKey: ByteArkPlayerLicenseKey(
        android: 'ANDROID_KEY', // Replace with your Android license key.
        iOS: 'IOS_KEY',         // Replace with your iOS license key.
      ),
      playerItem: item,
    );
  }

  @override
  void dispose() {
    // Step 4: Always dispose the controller you created.
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('ByteArk Player Demo')),
        body: Column(
          children: [
            // Step 5: Embed the player.
            AspectRatio(
              aspectRatio: 16 / 9,
              child: ByteArkPlayer(
                playerConfig: _config,
                controller: _controller,
              ),
            ),
            const SizedBox(height: 16),
            // Step 6: Drive playback through the controller.
            ElevatedButton(
              onPressed: _controller.pause,
              child: const Text('Pause'),
            ),
          ],
        ),
      ),
    );
  }
}

Features #

Listener callbacks #

Every player and ad event surfaces as a callback on ByteArkPlayerListener. Pass the listener to the controller at construction (or swap it later via setListener). Common lifecycle callbacks:

final controller = ByteArkPlayerController(
  listener: ByteArkPlayerListener(
    onPlayerReady: () => debugPrint('Player is ready.'),
    onPlaybackPlay: () => debugPrint('Playback started.'),
    onPlaybackPause: () => debugPrint('Playback paused.'),
    onPlaybackEnded: () => debugPrint('Playback ended.'),
    onPlaybackError: (e) => debugPrint('Error: ${e.code} — ${e.msg}'),
    onPlayerEnterFullscreen: () => debugPrint('Entered fullscreen.'),
  ),
);

The listener also carries the full ad-lifecycle (onAdsRequest, onAdsBreakStart, onAdsStart, the quartile callbacks, onAdsCompleted, onAdsError, onAllAdsCompleted). See API reference → ByteArkPlayerListener for the complete callback list.

Ads (VAST / VMAP via IMA) #

Set ByteArkAdsSettings.adTagUrl to a VAST or VMAP URL; the IMA plugin auto-enables when that field is non-empty.

final config = ByteArkPlayerConfig(
  licenseKey: ByteArkPlayerLicenseKey(android: '', iOS: ''),
  playerItem: ByteArkPlayerItem(sources: [
    ByteArkPlayerSource(url: 'https://...master.m3u8'),
  ]),
  adsSettings: ByteArkAdsSettings(
    adTagUrl: 'https://pubads.g.doubleclick.net/gampad/ads?iu=...',
    autoplayAdsMuted: true, // web-only — recommended for browsers that block audible-ads autoplay
  ),
);

The same ByteArkPlayerListener ad callbacks fire on all three platforms. Web only: the IMA SDK needs CSP entries for imasdk.googleapis.com and *.doubleclick.net — see Web platform notes → Consolidated CSP.

Fullscreen + Picture-in-Picture #

The controller exposes toggleFullScreen() cross-platform. Picture-in-Picture works automatically on supported browsers / OSes — there's no explicit PiP method, but onPlayerEnterPictureInPictureMode / onPlayerExitPictureInPictureMode fire when the user triggers PiP via the browser's native chrome.

ElevatedButton(
  onPressed: _controller.toggleFullScreen,
  child: const Text('Fullscreen'),
);

Browser-API gap: Firefox doesn't expose PiP JavaScript events, so the Listener stays silent there even though the user can still trigger PiP via the browser's native video controls.

Multiple players on one screen #

The plugin supports more than one ByteArkPlayer instance on screen at the same time. Each controller carries its own playerId and filters incoming events accordingly.

The rules:

  • Create one controller per widget. Never share a controller between two ByteArkPlayer widgets, and never omit controller: for two widgets that need to be independent.
  • Each controller owns its dispose(). When a screen tears down, dispose every controller you created. Skipping one leaks the native session.
  • The event channel is broadcast. All controllers subscribe to the same underlying native stream, then filter by playerId. Performance is fine for the small handful of concurrent players a typical screen needs.
final controllerA = ByteArkPlayerController(listener: listenerForA);
final controllerB = ByteArkPlayerController(listener: listenerForB);

// In build():
Column(
  children: [
    ByteArkPlayer(playerConfig: configA, controller: controllerA),
    ByteArkPlayer(playerConfig: configB, controller: controllerB),
  ],
);

// In dispose():
controllerA.dispose();
controllerB.dispose();

Lighthouse analytics #

ByteArk Lighthouse tracks per-session viewer behaviour. Two pieces glue together: the player-level setting (your Lighthouse project ID) and per-item metadata (user / video attributes for analysis).

final config = ByteArkPlayerConfig(
  licenseKey: ByteArkPlayerLicenseKey(android: '', iOS: ''),
  playerItem: ByteArkPlayerItem(
    mediaId: 'media_123',
    sources: [ByteArkPlayerSource(url: 'https://...master.m3u8')],
    lighthouseMetaData: ByteArkPlayerLighthouseMetaData(
      userId: 'user_42',
      videoTitle: 'The Great Adventure',
      // ... see ByteArkPlayerLighthouseMetaData dartdoc for the full field list
    ),
  ),
  lighthouseSetting: ByteArkLighthouseSetting(
    projectId: 'YOUR_PROJECT_ID',
    debug: false,
  ),
);

DRM-protected playback (Widevine + FairPlay) #

Declare both Widevine (DASH) and FairPlay (HLS) sources in the same sources list. The platform / browser picks the source it can play — no Platform.is* branching in your host code:

  • iOS plays the first non-DRM or FairPlay source.
  • Android plays the first non-DRM or Widevine source.
  • Web hands the full list to the SDK; the SDK auto-selects per browser (Widevine on Chromium / Edge / Android browsers, FairPlay on Safari).
final config = ByteArkPlayerConfig(
  licenseKey: ByteArkPlayerLicenseKey(android: '', iOS: ''),
  playerItem: ByteArkPlayerItem(
    sources: [
      ByteArkPlayerSource.drm(
        url: 'https://...master.m3u8',
        drm: FairPlayDrm(
          licenseUrl: 'https://license.example.com/fp',
          certificateUrl: 'https://license.example.com/fp-cert',
          licenseRequestHeaders: {'Authorization': 'Bearer ...'},
        ),
      ),
      ByteArkPlayerSource.drm(
        url: 'https://...master.mpd',
        drm: WidevineDrm(
          licenseUrl: 'https://license.example.com/wv',
          licenseRequestHeaders: {'Authorization': 'Bearer ...'},
        ),
      ),
    ],
  ),
);

Browser ↔ key-system support matrix:

Browser Supported key system Notes
Chrome, Edge (desktop) Widevine Chrome 70+, Edge 107+
Chrome on Android Widevine Chrome 70+
Safari on macOS / iOS / iPadOS FairPlay Safari 14+ / iOS 12+ / iPadOS 13+
Firefox Widevine Firefox ships Widevine via Google's CDM
PlayReady Not supported. The web SDK does not include a PlayReady key-system handler; the sealed ByteArkDrm hierarchy does not carry a PlayReady subclass either.

Lifecycle caveat (web). The bytearkShaka plugin is registered when the JS player is constructed. If you mount with a non-DRM sources list and later call switchMediaSource(...) with a DRM source, Shaka isn't available and DRM decryption will fail. The web backend logs a debugPrint warning when it detects this. Workaround: mount the Player with at least one DRM source so Shaka is registered at construction.

DRM on web also requires specific CSP entries — see Web platform notes → Consolidated CSP.


Web platform notes #

Deployment-specific concerns for Flutter Web. Cross-platform features (DRM declaration, ads, listener callbacks) are documented in Features above; this section covers what you need to know once you actually deploy to web.

Consolidated CSP directives #

Combine the directives below into your host app's Content-Security-Policy based on which features you use. Lines are additive — a fully-featured player (HLS + DRM + ads) needs the union of all four:

script-src 'self' https://byteark-sdk.cdn.byteark.com https://imasdk.googleapis.com;
worker-src 'self' blob:;
media-src 'self' blob: https://*.byteark.com https://*.doubleclick.net;
connect-src 'self' https://*.byteark.com https://*.doubleclick.net https://pubads.g.doubleclick.net https://license.example.com;
img-src 'self' https://*.doubleclick.net data:;

Substitute private license-server and ad-server origins for the placeholders. Without worker-src 'self' blob: the Shaka DRM plugin fails to initialise; without the appropriate connect-src entries the player loads but can't fetch DRM licenses or ad tags.

Subsets per feature:

Feature Required directives
Basic HLS / DASH playback script-src https://byteark-sdk.cdn.byteark.com, media-src https://*.byteark.com
DRM (Widevine / FairPlay) + worker-src 'self' blob:, + connect-src for your license server
Ads (IMA) + script-src https://imasdk.googleapis.com, + connect-src + media-src + img-src for *.doubleclick.net

Embedding in an iframe #

When the Flutter Web build is rendered inside an <iframe> (a common embed-in-customer-site pattern), the iframe needs explicit permissions for the browser features the player relies on:

<iframe
  src="https://your-flutter-app.example.com/"
  frameborder="0"
  allowfullscreen
  referrerpolicy="origin"
  allow="autoplay *; encrypted-media *; fullscreen *; picture-in-picture *; screen-wake-lock *;"
  style="width: 100%; height: 100%; border: 0;"
></iframe>
Attribute Why the player needs it
allowfullscreen + allow="fullscreen *" controller.toggleFullScreen()
allow="autoplay *" the autoplay-policy 'any' mapping — without it, the fallback hits "wait for user gesture" on first paint
allow="encrypted-media *" EME for Widevine / FairPlay DRM
allow="picture-in-picture *" PiP enter / exit events
allow="screen-wake-lock *" keep the screen on during playback
referrerpolicy="origin" many license servers and signed-URL endpoints check the Referer header against the host origin

Version pinning #

The plugin's major version moves in lockstep with the ByteArk Player Web SDK major:

Plugin major Targets SDK major
1.x v2
2.x v2 (current)
3.x TBD

The plugin asserts typeof window.bytearkPlayer === 'function' at first widget mount but does not runtime-check the SDK's reported version (the SDK doesn't expose one). The <script> URL in your web/index.html should match the major the plugin targets — pointing at a different SDK major produces a TypeError on the first SDK method call. When upgrading the plugin across majors, bump the <script> URL too.

Responsive layout #

The ByteArkPlayer widget fills its parent on web (default width / height are double.infinity); compose with AspectRatio, fixed SizedBox, or Expanded just like on mobile.

AspectRatio(
  aspectRatio: 16 / 9,
  child: ByteArkPlayer(playerConfig: config, controller: controller),
);

The underlying ByteArk Player Web instance does not auto-fit its Flutter-managed container by default — pick one of:

  • CSS override (recommended for most apps). Add the .video-js { width: 100% !important; height: 100% !important } rule from Web configuration to your web/index.html. The SDK then respects the Flutter-managed container width/height.
  • aspectRatio config option. Set ByteArkPlayerConfig.aspectRatio: '16:9' (or '9:16' for reels-style) and the SDK sizes the player to width × H/W using its own CSS. When combined with the override above and a matching Flutter AspectRatio wrap, all three agree on the ratio and coexist — that's what the example's Vertical demo does. When the ratios disagree, the override's height: 100% overrides the SDK's computed ratio and the player visibly snaps to the container instead. Either match the ratios across all three or pick a single sizing mechanism per player.

API reference #

The property tables below cover cross-platform behavior and conventions that aren't fully captured by per-field dartdoc. For full per-field documentation, see the dartdoc on pub.dev.

ByteArkPlayerItem #

Represents one piece of media along with the metadata used to render its chrome. Carries a non-empty sources list — each entry is a (URL + optional DRM scheme) pair.

class ByteArkPlayerItem {
  final String? mediaId;
  final String? posterImage;
  final String? title;
  final String? subtitle;
  final String? shareUrl;
  final List<ByteArkPlayerSource> sources;
  final ByteArkPlayerLighthouseMetaData? lighthouseMetaData;
}
Property Type Description
mediaId String? A unique identifier for the media item.
posterImage String? URL of the poster image.
title String? Title of the media item.
subtitle String? Subtitle / short description.
shareUrl String? URL for sharing.
sources (required) List<ByteArkPlayerSource> One or more playable Sources. Must be non-empty (asserted at construction). iOS picks the first non-DRM ∪ FairPlay source, Android picks the first non-DRM ∪ Widevine source, web hands the list to the SDK which auto-selects per browser.
lighthouseMetaData ByteArkPlayerLighthouseMetaData? Lighthouse analytics metadata for this item.

ByteArkPlayerSource #

A single playable delivery of a media item — a URL plus an optional MIME type hint and an optional DRM scheme.

class ByteArkPlayerSource {
  final String url;
  final String? type;     // optional MIME hint
  final ByteArkDrm? drm;  // null = non-DRM source
}

Two construction paths:

  • Non-DRM: ByteArkPlayerSource(url: '…')drm is null, type is null unless you pass it.
  • DRM: ByteArkPlayerSource.drm(url: '…', drm: WidevineDrm(…) | FairPlayDrm(…))drm is required non-null. type defaults from the DRM scheme (FairPlay → application/x-mpegURL, Widevine → application/dash+xml); pass type: to override.

ByteArkPlayerConfig #

class ByteArkPlayerConfig {
  final ByteArkPlayerLicenseKey licenseKey;
  final bool? autoPlay;
  final bool? control;
  final bool? seekButtons;
  final int? seekTime;
  final ByteArkPlayerItem? playerItem;
  final bool? fullScreenButton;
  final bool? settingButton;
  final ByteArkLighthouseSetting? lighthouseSetting;
  final ByteArkAdsSettings? adsSettings;
  final bool? secureSurface;
  final ByteArkPlayerSubtitleSize? subtitleSize;
  final bool? subtitleBackgroundEnabled;
  final int? subtitlePaddingBottomPercentage;
  final String? aspectRatio;
}
Property Type Description
licenseKey (required) ByteArkPlayerLicenseKey License keys for Android + iOS. Silently ignored on web.
autoPlay bool? Automatically start playback when ready. Defaults to true. On web, true/null map to the SDK's 'any' mode (audible → muted → wait-for-gesture).
control bool? Show playback controls on the player UI. Defaults to true.
seekButtons bool? Show seek buttons. Defaults to true.
seekTime int? Seconds to advance per seekForward / seekBackward. Defaults to 30.
playerItem (required) ByteArkPlayerItem? The media item to play.
fullScreenButton bool? Show the fullscreen toggle. Defaults to true.
settingButton bool? Show the settings button. Defaults to true.
lighthouseSetting ByteArkLighthouseSetting? Lighthouse analytics project setting.
adsSettings ByteArkAdsSettings? IMA-based ad settings.
secureSurface bool? Android-only FLAG_SECURE. No-op on iOS and web — see Cross-platform notes. Defaults to false.
subtitleSize ByteArkPlayerSubtitleSize? Subtitle size. Defaults to medium.
subtitleBackgroundEnabled bool? Show a background behind subtitles. Defaults to true.
subtitlePaddingBottomPercentage int? (1-100) Subtitle bottom padding as a percentage of player height. Defaults to 10.
aspectRatio String? Web-only. Forwarded to the SDK's aspectRatio option as 'W:H' (e.g. '9:16' for reels). Mobile silently ignores.

ByteArkPlayerLicenseKey #

final licenseKey = ByteArkPlayerLicenseKey(
  android: 'YOUR_ANDROID_LICENSE_KEY',
  iOS: 'YOUR_IOS_LICENSE_KEY',
);
Property Type Description
android (required) String License key issued for the Android SDK.
iOS (required) String License key issued for the iOS SDK.

Web-only consumers pass empty strings — the web SDK has no license-key equivalent.

ByteArkAdsSettings #

final adsSettings = ByteArkAdsSettings(
  adTagUrl: 'https://pubads.g.doubleclick.net/gampad/ads?...',
);
Property Type Description
adTagUrl (required) String? VAST / VMAP tag URL returning the ad payload.
enableDefaultCompanionSlot bool? Enable the default companion ad slot.
defaultCompanionSize Pair<int, int>? Width / height of the default companion slot.
autoplayAdsMuted bool? Web-only. Start preroll ads muted on browsers that block audible-ads autoplay.

ByteArkDrm #

Sealed base class for a single DRM scheme + its credentials. Subclasses are WidevineDrm (Chromium / Edge / Android browsers, Android native) and FairPlayDrm (Safari, iOS native). Use one per ByteArkPlayerSource.drm; declare two sources in ByteArkPlayerItem.sources for cross-platform DRM coverage.

sealed class ByteArkDrm { /* … */ }

switch (source.drm) is exhaustive — adding a future scheme is a single compile-flagged spot per consumer. PlayReady is intentionally not supported.

WidevineDrm

Property Type Description
licenseUrl (required) String URL of the Widevine license server.
licenseRequestHeaders Map<String, String>? Extra HTTP headers (e.g. Authorization). The web mapper translates this to the SDK's [{name, value}] array shape internally.

FairPlayDrm

Property Type Description
licenseUrl (required) String URL of the FairPlay license server (SPC → CKC).
certificateUrl (required) String URL of the FairPlay application certificate.
licenseRequestHeaders Map<String, String>? Extra HTTP headers.

The 1.x cerfificateUrl misspelled alias is removed in 2.0. Use certificateUrl.

ByteArkPlayerMediaTrack #

A track descriptor used by getAudios / getSubtitles / getResolutions and accepted by their corresponding set* methods.

Property Type Description
id String? Native track identifier.
name String? Display name (e.g. English, 720p).
language String? BCP-47 / ISO 639 language code where applicable.

Pass null to controller.setSubtitle(null) to disable subtitles.

ByteArkPlayerSubtitleSize #

Enum controlling subtitle text size, expressed as a percentage of the video height. Default is medium.

Value Size
minimum 1%
extraTiny 2%
tiny 3%
extraSmall 4%
small 5%
medium 6%
large 7%
extraLarge 8%
maximum 9%

ByteArkLighthouseSetting #

Property Type Description
projectId (required) String The unique identifier for your Lighthouse project.
debug bool? Enables debug mode for Lighthouse tracking. Defaults to false.

ByteArkPlayerController #

Drives playback. Created by the host, passed to a ByteArkPlayer widget, and disposed by the host.

Method Description
setListener(ByteArkPlayerListener? listener) Replace (or clear) the listener.
play() Start or resume playback.
pause() Pause playback.
togglePlayback() Toggle between playing and paused.
seekForward() Seek forward by seekTime seconds.
seekBackward() Seek backward by seekTime seconds.
seekTo(int position) Seek to a specific position in seconds.
switchMediaSource(ByteArkPlayerConfig config) Switch to a new media source.
toggleFullScreen() Toggle fullscreen mode.
dispose() Release resources. Always call on a controller you created.
currentPosition() Current playback position in seconds. Returns null if unavailable.
getCurrentAudio() / getAudios() / setAudio(track) Audio-track query + selection.
getCurrentSubtitle() / getSubtitles() / setSubtitle(track?) Subtitle-track query + selection. Pass null to disable subtitles.
getCurrentResolution() / getResolutions() / setResolution(track) Resolution / quality query + selection.
getCurrentPlaybackSpeed() / getAvailablePlaybackSpeeds() / setPlaybackSpeed(speed) Playback-speed query + selection.
getCurrentTime() Current playback time in seconds. Returns 0 if unavailable.
getDuration() Total media duration in seconds. Returns 0 if unavailable.

ByteArkPlayerListener #

A callback bag passed to the controller. Each callback is optional — set only the ones you care about.

Callback Fires when
onPlayerReady The player is ready for interaction.
onPlayerLoadingMetadata Media metadata starts loading.
onPlaybackFirstPlay The media starts playing for the first time.
onPlaybackPlay Playback resumes.
onPlaybackPause Playback is paused.
onPlaybackSeeking / onPlaybackSeeked Seek begins / completes.
onPlaybackEnded Playback reaches the end.
onPlaybackTimeupdate Periodic playback-time tick.
onPlaybackBuffering / onPlaybackBuffered Buffering begins / completes.
onPlaybackResolutionChanged The active resolution changes.
onPlaybackPlaylistItemChanged The playlist advances to a new item.
onPlaybackError (ByteArkPlayerPlaybackError) A playback error occurs.
onPlayerEnterFullscreen / onPlayerExitFullscreen Fullscreen enter / exit.
onPlayerEnterPictureInPictureMode / onPlayerExitPictureInPictureMode PiP enter / exit. Chromium and Safari only — see Cross-platform notes.
onAdsRequest / onAdsBreakStart / onAdsBreakEnd Ad request and break boundaries.
onAdsStart / onAdsCompleted / onAdsSkipped (ByteArkPlayerAdsData) Ad lifecycle.
onAdsFirstQuartile / onAdsMidPoint / onAdsThirdQuartile (ByteArkPlayerAdsData) Ad quartile beacons.
onAdsImpressed / onAdsClicked (ByteArkPlayerAdsData) Ad impression / click.
onAllAdsCompleted All ads in the break have finished.
onAdsError (ByteArkPlayerAdsErrorData) An error in the ad manager.

ByteArkPlayerEventChannel #

A legacy mobile-only event stream. On web it emits no events — consume Player events through ByteArkPlayerListener instead. New code should use the listener API on all platforms; the event channel is retained for backward compatibility with mobile-only host apps that subscribed to the raw stream before the listener landed.


Migrating from 1.x to 2.0 #

2.0 reshapes how media sources and DRM credentials are declared. The public widget, controller, and listener APIs are unchanged; the breaking change is concentrated in ByteArkPlayerItem and the DRM data classes.

Design reference: ADR-0004.

TL;DR #

  1. Replace ByteArkPlayerItem(url: …, drm: ByteArkDrm(…)) with ByteArkPlayerItem(sources: [ByteArkPlayerSource(…) | .drm(…)]).
  2. ByteArkDrm is now a sealed base — its 1.x dual-field ByteArkDrm(widevineDrm: …, fairPlayDrm: …) constructor is gone. Use WidevineDrm(…) or FairPlayDrm(…) directly as a ByteArkPlayerSource.drm value.
  3. Drop Platform.is* URL branching in your host — one cross-platform sources: [...] declaration replaces it.
  4. The misspelled cerfificateUrl alias on FairPlayDrm is removed. Use certificateUrl.

The rest of this section shows the before/after for the common cases.

Case 1 — non-DRM single source #

The simplest case: one URL, no DRM. Wrap the URL in a single-element sources list.

Before (1.x):

ByteArkPlayerItem(
  url: 'https://example.com/playlist.m3u8',
)

After (2.0):

ByteArkPlayerItem(
  sources: [
    ByteArkPlayerSource(url: 'https://example.com/playlist.m3u8'),
  ],
)

Case 2 — cross-platform DRM (drop Platform.is* branching) #

The 1.x pattern required the host to branch on Platform.is* to pick the URL because the plugin only carried one URL at a time. 2.0 replaces that with a multi-source declaration that compiles once and runs everywhere.

Before (1.x):

import 'dart:io';

String _mediaUrl() {
  if (Platform.isAndroid) return 'https://example.com/playlist.mpd';
  if (Platform.isIOS)     return 'https://example.com/playlist.m3u8';
  return '';
}

ByteArkDrm _drm() {
  if (Platform.isAndroid) {
    return ByteArkDrm(
      widevineDrm: WidevineDrm(
        licenseUrl: 'https://license.example.com/wv',
        licenseRequestHeaders: {'Authorization': 'Bearer …'},
      ),
    );
  }
  if (Platform.isIOS) {
    return ByteArkDrm(
      fairPlayDrm: FairPlayDrm(
        licenseUrl: 'https://license.example.com/fp',
        certificateUrl: 'https://license.example.com/fp-cert',
        licenseRequestHeaders: {'Authorization': 'Bearer …'},
      ),
    );
  }
  return ByteArkDrm();
}

final item = ByteArkPlayerItem(url: _mediaUrl(), drm: _drm());

After (2.0):

final item = ByteArkPlayerItem(
  sources: [
    ByteArkPlayerSource.drm(
      url: 'https://example.com/playlist.m3u8',
      drm: FairPlayDrm(
        licenseUrl: 'https://license.example.com/fp',
        certificateUrl: 'https://license.example.com/fp-cert',
        licenseRequestHeaders: {'Authorization': 'Bearer …'},
      ),
    ),
    ByteArkPlayerSource.drm(
      url: 'https://example.com/playlist.mpd',
      drm: WidevineDrm(
        licenseUrl: 'https://license.example.com/wv',
        licenseRequestHeaders: {'Authorization': 'Bearer …'},
      ),
    ),
  ],
);

No dart:io import. No Platform.is* branching. iOS picks the FairPlay source, Android picks the Widevine source, web hands the whole list to the SDK which auto-selects per browser.

Case 3 — single-platform DRM (e.g. Android-only Widevine) #

If you only ship Android, declare one source.

Before (1.x):

ByteArkPlayerItem(
  url: 'https://example.com/playlist.mpd',
  drm: ByteArkDrm(
    widevineDrm: WidevineDrm(licenseUrl: 'https://license.example.com/wv'),
  ),
)

After (2.0):

ByteArkPlayerItem(
  sources: [
    ByteArkPlayerSource.drm(
      url: 'https://example.com/playlist.mpd',
      drm: WidevineDrm(licenseUrl: 'https://license.example.com/wv'),
    ),
  ],
)

Case 4 — removed cerfificateUrl alias #

The 1.x misspelled cerfificateUrl parameter on FairPlayDrm was @Deprecated and has been removed in 2.0. Migrate to the canonical name.

Before (1.x, deprecated):

FairPlayDrm(
  licenseUrl: '…',
  cerfificateUrl: 'https://example.com/cert', // ← typo'd field
)

After (2.0):

FairPlayDrm(
  licenseUrl: '…',
  certificateUrl: 'https://example.com/cert',
)

Case 5 — web sizing (fill: true removed) #

In 1.x the plugin hardcoded fill: true in the JS options it passed to the ByteArk Web SDK, which made the player auto-expand to fill its Flutter-managed container. 2.0 removes that hardcoded flag so host apps can choose how to size the player. Without an explicit choice, the SDK falls back to its videojs-derived ~432×243 pixel default and ignores the Flutter container — the player will render as a small fixed box regardless of your AspectRatio/SizedBox/Expanded wrapping.

Pick one of the two approaches below per host app.

Option A — CSS override (web-only, retains 1.x behaviour).

Add this <style> block to your web/index.html <head>:

<style>
  .video-js { width: 100% !important; height: 100% !important; }
</style>

The override propagates the Flutter host <div>'s 100% × 100% sizing down to videojs's .video-js element. !important is required because the SDK writes inline width/height at construction. Flutter layout (AspectRatio, SizedBox, Expanded) drives the size as before.

Option B — aspectRatio config (web-only, no HTML edit).

Set ByteArkPlayerConfig.aspectRatio to a 'W:H' string:

ByteArkPlayerConfig(
  licenseKey: ByteArkPlayerLicenseKey(android: '', iOS: ''),
  playerItem: item,
  aspectRatio: '16:9',  // or '9:16' for reels-style
)

On web the SDK manages the 16:9 (or 9:16) frame internally. Mobile silently ignores aspectRatio — drive the ratio there with Flutter's AspectRatio widget.

Combining the two with disagreeing ratios breaks the layout — when aspectRatio is set the SDK computes height from container width; the CSS override then forces height to 100%, which only matches the computed ratio if the container's own dimensions already do. The example's Vertical demo combines both at a single 9:16 ratio and works because everything agrees. Match the ratios across the Flutter parent, the aspectRatio config, and (if applicable) any wrapping AspectRatio widget — or pick one mechanism per player.

Caveat — bytearkShaka is register-once at construction #

On web, the SDK's Shaka plugin is registered when the JS player is constructed — based on whether the initial config carries any DRM source. switchMediaSource(...) can't register the plugin after the fact.

If your app starts with a non-DRM Player and later switches to DRM sources, Shaka won't be loaded and DRM decryption will fail silently. The web backend logs a debugPrint warning when it detects this. Workaround: mount the Player with at least one DRM source in the initial sources list so Shaka is registered at construction.

Type rename summary #

1.x 2.0
ByteArkPlayerItem.url (required) removed — use sources: [...]
ByteArkPlayerItem.drm removed — set DRM per-source via ByteArkPlayerSource.drm
ByteArkPlayerItem.sources new — non-empty List<ByteArkPlayerSource>
ByteArkDrm(widevineDrm: …, fairPlayDrm: …) removedByteArkDrm is now a sealed base; use WidevineDrm(…) or FairPlayDrm(…) directly
WidevineDrm(...) unchanged constructor; now extends ByteArkDrm
FairPlayDrm(...) constructor unchanged except cerfificateUrl removed; now extends ByteArkDrm
ByteArkPlayerSource new value type — non-DRM via primary constructor, DRM via named .drm() factory

Behavioural changes #

  • ByteArkPlayerItem.sources is asserted non-empty at construction. In 2.0 this is an unconditional throw ArgumentError.value(...), so it fires in both debug and release builds.
  • iOS source-selection error. If no source in sources is compatible with iOS (only Widevine sources, no non-DRM fallback), the iOS backend throws PlayerConfigMapperError.noCompatibleSource and logs the cause to the Xcode console rather than silently mounting an empty player.
  • Android source-selection error. Same on the Kotlin side — IllegalStateException with a Widevine-named error message if no compatible source is found.
  • DrmConfigMapper (web) emits the correct SDK field names — Widevine url (not licenseUrl); FairPlay processSpcUrl + certificateUrl; licenseRequestHeaders as a [{name, value}] array (not a flat map). The 1.x mapper shipped with placeholder field names; DRM on web has not actually worked end-to-end until 2.0.

Notes from previous minor upgrades #

  • 1.1.x → 1.2.0 moved playback control off the widget and onto a ByteArkPlayerController. The widget no longer exposes play(), pause(), seekTo(), dispose(), etc. — drive playback through a controller you create yourself and pass to the widget (see Quick start). If you're upgrading from 1.1.x, this change still applies in 2.0.
  • Xcode 17+ is required (vendor SDK was built with Swift 6.3). Hosts on Xcode 16 should pin byteark_player_flutter: 1.1.6.
  • Android toolchain floors raised: AGP 8.6+, Gradle 8.14+, Kotlin 1.9+, compileSdk 35, JDK 17.

Troubleshooting #

"Player area is blank, no error" #

The most common cause is empty licenseKey strings — the SDK refuses to render anything when the keys are empty. Set real values on ByteArkPlayerLicenseKey(android: ..., iOS: ...).

iOS build fails with "main actor-isolated instance method 'play()' has different actor isolation" or "this SDK is not supported by the compiler" #

The vendor ByteArkPlayerSDK xcframework was built with a specific Swift toolchain (currently Swift 6.3.2 / Xcode 17). Hosts on an older Xcode hit a swiftinterface mismatch.

  • Fix: upgrade to Xcode 17 or newer, OR pin the plugin to byteark_player_flutter: 1.1.6 until you can upgrade.

pod install fails fetching ByteArkPlayerSDK or ByteArkPlayerSDKLighthousePlugin #

CocoaPods pulls these from private GitHub spec repos (byteark/byteark-player-sdk-ios-specs, byteark/lighthouse-sdk-native-ios-specs) over SSH.

  • Ensure your SSH key is added to your GitHub account and that the ByteArk team has granted your account access to those repos.
  • Verify the source lines in your Podfile match the ones in iOS configuration.

The plugin's android/build.gradle reads two GitLab Maven tokens from android/local.properties:

gitLabByteArkPlayerPrivateToken=...
gitLabByteArkLighthousePrivateToken=...
  • Make sure both lines exist in android/local.properties (this file is gitignored — it must be created on every developer machine and on CI).
  • Request the tokens from the ByteArk team if you don't have them.

Android build fails with "Your project's Android Gradle Plugin version is lower than Flutter's minimum supported version" #

You're below the toolchain floors. Bump to AGP 8.6+ / Gradle 8.14+ / Kotlin 1.9+ / compileSdk 35 / JDK 17 as listed in Android configuration.

Two ByteArkPlayer widgets on screen — listeners fire for the wrong player #

Each ByteArkPlayerController is scoped by its own playerId. As long as every widget receives its own controller (ByteArkPlayer(controller: controllerA) vs ByteArkPlayer(controller: controllerB)), events stay isolated. Sharing one controller between two widgets, or omitting the controller parameter for both, will cross-fire events.

ByteArkPlayerItem throws ArgumentError: "requires at least one source" #

You passed sources: [] (an empty list) — the constructor enforces non-empty in both debug and release builds. Add at least one ByteArkPlayerSource to the list.

iOS: "noCompatibleSource — a FairPlay or non-DRM source is required" #

Your sources list contains only Widevine entries. iOS can't play Widevine — add a FairPlay source or a non-DRM fallback to the list.

Android: "No source in ByteArkPlayerItem.sources is playable on Android — a Widevine or non-DRM source is required" #

Same shape as the iOS error above, but reversed — add a Widevine source or a non-DRM fallback.

Web: DRM-protected video plays initially but fails after switchMediaSource #

The bytearkShaka plugin is register-once at construction. If you mount with non-DRM sources and switch to DRM later, Shaka isn't loaded. Mount the Player with at least one DRM source so Shaka is registered at construction. See DRM-protected playback → Lifecycle caveat.

Web platform — common failure modes #

Symptom Likely cause Fix
StateError: ByteArk Player Web SDK is not loaded on first ByteArkPlayer build The <script> tag is missing from web/index.html. Add the canonical v2 tag from Web configuration.
Video pane is blank, browser console shows TypeError: window.bytearkPlayer is not a function after the script tag is in place Wrong SDK major (pointing at a v1 or v3 URL while the plugin targets v2), or the script failed to load (404 / blocked). Verify the URL is https://byteark-sdk.cdn.byteark.com/player/v2/byteark-player.min.js and the request succeeds in DevTools' Network tab.
Video loads but won't autoplay on first visit (iPhone Safari, Android Chrome first-visit) Browser audible-autoplay policy blocked playback. Expected — the SDK's 'any' fallback already retries muted. The video appears with sound muted; the user can unmute. Set autoplayAdsMuted: true if ads are involved.
Browser console: Refused to load the script / Refused to connect / Refused to create a worker Host app's Content Security Policy is missing one or more directives. See Consolidated CSP directives. The most common miss is worker-src 'self' blob: (Shaka DRM workers).
Player is invisible inside an <iframe>; clicking the fullscreen button does nothing Iframe is missing the allow permissions. Apply the full allow="autoplay *; encrypted-media *; fullscreen *; picture-in-picture *; screen-wake-lock *;" attribute documented in Embedding in an iframe.
DRM-protected video plays on Chrome but not Safari (or vice versa) Only one key-system source provided. Declare both FairPlayDrm (HLS) and WidevineDrm (DASH) sources in the same sources list — see DRM-protected playback.
onPlayerEnterPictureInPictureMode never fires on Firefox Browser-API gap: Firefox doesn't expose Picture-in-Picture JavaScript events. Expected. The user can still trigger PiP via Firefox's native video controls, but Listener callbacks stay silent on Firefox.
ByteArkPlayerEventChannel.stream emits nothing on web EventChannel.stream is iOS/Android-only on web. Use the ByteArkPlayerListener API — it works cross-platform.

Need help? #

Contact sales@byteark.com for licensing / credential issues, or open an issue in the project tracker for plugin-level bugs.


Contributing #

Read CONTRIBUTING.md for MR-title conventions (Conventional Commits 1.0.0), commit-message guidance, and the release workflow.

5
likes
150
points
249
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

ByteArkPlayerFlutter is a Flutter plugin for the ByteArk Player, designed to enable seamless video playback and advanced player management within your Flutter applications.

Homepage

License

BSD-3-Clause (license)

Dependencies

flutter, flutter_web_plugins, plugin_platform_interface, uuid, web

More

Packages that depend on byteark_player_flutter

Packages that implement byteark_player_flutter