byteark_player_flutter 2.0.1
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 #
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) |

💡 Contributing? Read
CONTRIBUTING.mdfor the MR-title and commit conventions.
Contents #
- What's new in 2.0
- Installation
- Quick start
- Features
- Web platform notes
- API reference
- Migrating from 1.x to 2.0
- Troubleshooting
- Contributing
What's new in 2.0 #
⚠️ 2.0 has breaking changes. Highlights:
ByteArkPlayerItem.url→sources: List<ByteArkPlayerSource>(non-empty list, asserted at construction).ByteArkDrm(widevineDrm:, fairPlayDrm:)→ sealedByteArkDrmbase; useWidevineDrm(…)/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+ newWebSourcesMapper. FairPlayDrm.cerfificateUrlmisspelled alias removed (usecertificateUrl).- Web sizing now requires a CSS override or
aspectRatioconfig. The plugin no longer forwardsfill: trueto the SDK; host apps add.video-js { width: 100% !important; height: 100% !important }toweb/index.htmlor setByteArkPlayerConfig.aspectRatio: 'W:H'. See Web configuration. - Item metadata forwarded to each web source.
ByteArkPlayerItem.title,subtitle,mediaId, andposterImageare now spread onto every emitted JS source astitle,subtitle,videoId, andposter.titleandvideoIdare required by the web SDK when Lighthouse is enabled. Mobile already forwards all four via the native mappers (subtitlemaps to the iOS SDK'sdetailfield). 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-specsandlighthouse-sdk-native-ios-specs. - Lighthouse
projectId(optional) — only needed if you enable Lighthouse analytics viaByteArkLighthouseSetting.
Flutter integration #
-
Add the dependency:
flutter pub add byteark_player_flutterOr manually under
dependencies:inpubspec.yaml:dependencies: byteark_player_flutter: ^2.0.0 # Use the latest version from pub.dev. -
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, pinbyteark_player_flutter: 1.1.6in yourpubspec.yaml.
-
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.
-
Open your
ios/Podfileand 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' -
Install the Pods:
cd ios && pod install --repo-update -
Open the generated
.xcworkspacein 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 bothsourceCompatibilityandkotlinOptions.jvmTarget. Older toolchains will fail at Gradle sync.
-
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.
-
-
Extend
FlutterFragmentActivityinMainActivity.kt:import io.flutter.embedding.android.FlutterFragmentActivity class MainActivity: FlutterFragmentActivity() -
Configure
android/local.propertieswith 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.
-
Add the ByteArk Player Web
<script>tag to your host app'sweb/index.htmlinside<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
TypeErroron the first SDK call. -
Add the
.video-jssize 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: trueflag to the SDK — without this override, the SDK falls back to its videojs-derived ~432×243 pixel default and ignores the Flutter container.!importantis required because the SDK writes inline width / height on.video-jsat construction. As an alternative to the CSS override, setByteArkPlayerConfig.aspectRatio(e.g.'9:16') — see API reference → ByteArkPlayerConfig. -
That's it. The plugin asserts
window.bytearkPlayerexists at the firstByteArkPlayerwidget mount; if the script tag is missing it throws aStateErrornaming 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 withcd example && flutter runafter 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
ByteArkPlayerwidgets, and never omitcontroller: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
bytearkShakaplugin is registered when the JS player is constructed. If you mount with a non-DRMsourceslist and later callswitchMediaSource(...)with a DRM source, Shaka isn't available and DRM decryption will fail. The web backend logs adebugPrintwarning 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 yourweb/index.html. The SDK then respects the Flutter-managed container width/height. aspectRatioconfig option. SetByteArkPlayerConfig.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 FlutterAspectRatiowrap, all three agree on the ratio and coexist — that's what the example's Vertical demo does. When the ratios disagree, the override'sheight: 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: '…')—drmisnull,typeisnullunless you pass it. - DRM:
ByteArkPlayerSource.drm(url: '…', drm: WidevineDrm(…) | FairPlayDrm(…))—drmis required non-null.typedefaults from the DRM scheme (FairPlay →application/x-mpegURL, Widevine →application/dash+xml); passtype: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
cerfificateUrlmisspelled alias is removed in 2.0. UsecertificateUrl.
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
nulltocontroller.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 #
- Replace
ByteArkPlayerItem(url: …, drm: ByteArkDrm(…))withByteArkPlayerItem(sources: [ByteArkPlayerSource(…) | .drm(…)]). ByteArkDrmis now a sealed base — its 1.x dual-fieldByteArkDrm(widevineDrm: …, fairPlayDrm: …)constructor is gone. UseWidevineDrm(…)orFairPlayDrm(…)directly as aByteArkPlayerSource.drmvalue.- Drop
Platform.is*URL branching in your host — one cross-platformsources: [...]declaration replaces it. - The misspelled
cerfificateUrlalias onFairPlayDrmis removed. UsecertificateUrl.
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: …) |
removed — ByteArkDrm 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.sourcesis asserted non-empty at construction. In 2.0 this is an unconditionalthrow ArgumentError.value(...), so it fires in both debug and release builds.- iOS source-selection error. If no source in
sourcesis compatible with iOS (only Widevine sources, no non-DRM fallback), the iOS backend throwsPlayerConfigMapperError.noCompatibleSourceand logs the cause to the Xcode console rather than silently mounting an empty player. - Android source-selection error. Same on the Kotlin side —
IllegalStateExceptionwith a Widevine-named error message if no compatible source is found. DrmConfigMapper(web) emits the correct SDK field names — Widevineurl(notlicenseUrl); FairPlayprocessSpcUrl+certificateUrl;licenseRequestHeadersas 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 exposesplay(),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+,
compileSdk35, 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.6until 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
Podfilematch the ones in iOS configuration.
Android Gradle sync fails with "local.properties is not found" or token-related errors #
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.