bmdrm_mobile 0.0.27
bmdrm_mobile: ^0.0.27 copied to clipboard
A Flutter plugin for DRM-protected video playback with watermark support.
BMDRM Mobile #
A Flutter plugin for DRM-protected video playback with watermark support.
Features #
- DRM Support: Supports Widevine, FairPlay, and PlayReady DRM systems
- Resume Playback: Remembers the last watched position per video on the device and resumes automatically
- Watermark Overlay: Dynamic and static watermark support with customizable positioning and styling
- Platform Optimized: Native Android implementation using ExoPlayer with Media3
- Cryptographic Security: Real ECDSA signature generation and ECDH key exchange
- Session Management: Automatic DRM session generation and management
- Error Handling: Comprehensive error handling and logging
- Code Obfuscation: Production-ready obfuscation support for both Android and iOS
Installation #
Add this to your pubspec.yaml:
dependencies:
bmdrm_mobile: ^0.0.6
Usage #
Basic Usage #
import 'package:bmdrm_mobile/bmdrm_mobile.dart';
// Automatic DRM session generation
BmdrmVideoPlayer.drmMinimal(
apiKey: 'your-api-key',
userId: 'user-123',
videoId: 'video-456',
watermarkConfig: WatermarkConfig(
id: 'watermark-1',
type: 'static',
name: 'My Watermark',
text: 'Protected Content',
alpha: 0.8,
color: '#FFFFFF',
size: 16,
interval: 0,
skip: 0,
x: 10.0,
y: 10.0,
),
onPlay: () {},
onPause: () {},
onError: (error) {},
)
Pre-generated Session Data #
// Using pre-generated DRM session data
BmdrmVideoPlayer.drmWithSession(
drmSessionData: DrmSessionData(
edgeName: 'edge-server',
token: 'session-token',
ecdsaKey: 'signing-key',
drmServerUrl: 'https://drm-server.com/license',
),
watermarkConfig: WatermarkConfig(...),
)
Resume from Last Position #
Playback automatically resumes from where the viewer last stopped. The position
is saved on the device (Android SharedPreferences / iOS UserDefaults) as the
video plays, on pause, and when the player is closed, then restored the next time
the same video is opened. A fully watched video starts over from the beginning.
This is enabled by default. Each video is keyed by a stable resume id, resolved
in this order: the explicit resumeId, then videoId, then the videoId inside a
backend session. If none is available, resume is silently disabled for that playback.
BmdrmVideoPlayer.drmMinimal(
apiKey: 'your-api-key',
userId: 'user-123',
videoId: 'video-456', // used as the resume key when resumeId is omitted
resumeId: 'course-1/lesson-3', // optional explicit, stable key
resumeFromLastPosition: true, // default; set false to always start at 0 and skip saving
)
To forget a saved position (e.g. a "Start over" action) or read it back, use the static helpers (they don't touch any player that's currently on screen):
await VideoPlayerController.clearSavedPosition('video-456'); // forget it
final Duration last = await VideoPlayerController.getSavedPosition('video-456'); // read it
Resume is per device. Positions are not synced across devices or accounts; use the
onPositionChangedcallback if you want to persist progress to your backend.
Customizable Fullscreen Drawer #
Pass a fullscreenDrawerBuilder to show a customizable, app-owned panel beside
the video while in fullscreen — like a "Resources / Notes / Q&A" side panel. The
plugin provides the sliding panel, a toggle button and an exit-fullscreen button;
you provide the content (any Flutter widget).
Passing the builder switches fullscreen to a Flutter-rendered mode (so your widget can be drawn), and the video shrinks to sit beside the open drawer rather than behind it. When you don't pass a builder, fullscreen keeps its native behaviour, unchanged.
BmdrmVideoPlayer.drmFromBackendSession(
backendSession: session,
// Any widget you want. `controller` lets the panel close itself or leave
// fullscreen from a button inside it.
fullscreenDrawerBuilder: (context, controller) => MyLessonPanel(
onClose: controller.closeDrawer,
),
fullscreenDrawerIcon: const Icon(Icons.menu_book_rounded), // toggle button icon
fullscreenDrawerWidth: 360, // optional; defaults to ~38% of width
fullscreenDrawerAlignment: Alignment.centerRight, // or centerLeft
fullscreenDrawerBackgroundColor: Colors.white, // optional
)
Drive it programmatically (e.g. from your own button) with a
BmdrmFullscreenController:
final fsController = BmdrmFullscreenController();
BmdrmVideoPlayer.drmFromBackendSession(
backendSession: session,
fullscreenController: fsController,
fullscreenDrawerBuilder: (context, controller) => MyLessonPanel(),
);
// later:
fsController.enterFullscreen();
fsController.openDrawer();
// fsController.isFullscreen / fsController.isDrawerOpen — and it's a
// ChangeNotifier, so you can listen for changes.
The drawer is rendered by Flutter, so the player must sit under a
MaterialApp(or otherwise have anOverlay/Navigatorancestor), which is the normal case.
Video Quality #
The player reads the resolutions the stream carries (1080p, 720p, …) and lets the viewer pin one instead of leaving every switch to adaptive bitrate. On Android this is built in: the control bar's speed chip is a gear button opening a settings menu with Speed and Quality rows. On iOS AVKit has nowhere to put a quality control, so the plugin exposes quality programmatically and your app draws the picker — the API below is the same on both platforms.
This is not the
qualityyou pass toDartDrmService.generateDrmStreamUrls('h264', '720p', '4k'), which picks the codec profile of the manifest that gets fetched. Everything on the resolution axis is namedvideoQuality.
BmdrmVideoPlayer.drmFromBackendSession(
backendSession: session,
// Fires on every change — Android's in-player menu and selectVideoQuality()
// below both land here.
onVideoQualityChanged: (BmdrmVideoQuality quality) {
debugPrint('now playing ${quality.label}'); // '1080p', or 'Auto'
},
)
Drive it yourself (and on iOS, that is the only way) with a
BmdrmFullscreenController:
final fsController = BmdrmFullscreenController();
BmdrmVideoPlayer.drmFromBackendSession(
backendSession: session,
fullscreenController: fsController,
);
// later — it's a ChangeNotifier, so rebuild your picker when it notifies:
final List<BmdrmVideoQuality> qualities = fsController.videoQualities;
// highest resolution first; each has id, width, height, bitrate and a label
fsController.selectVideoQuality(qualities.first.id); // pin the best rendition
fsController.selectVideoQuality(-1); // back to Auto
// fsController.currentVideoQuality — BmdrmVideoQuality.auto until something
// is pinned.
A pin lasts as long as the media does. Loading a video — a new
BmdrmVideoPlayer, or a setDrmConfig on an existing one — resets the
selection to Auto and empties videoQualities on both platforms, then repopulates
the list once the new stream's renditions are known. Nothing carries a pin from
one video to the next: ids index one stream's rendition list and even a matching
height can mean a different bitrate in another stream. If you want a viewer's
choice to stick across videos, remember it in your app and re-apply it from
onVideoQualityChanged once the new list arrives.
The reverse also holds while one video plays: a pin survives everything the
player does internally, including the player rebuild that DRM configuration
triggers on Android and the item replacement iOS does, so you will not see a
selection silently fall back mid-playback. It can be dropped if the rendition
you pinned disappears from the stream — that arrives as an onVideoQualityChanged
carrying Auto, so treat the callback (not your own last request) as the truth.
Listing renditions needs iOS 15+ (it reads the multivariant playlist). On older iOS the list stays empty and playback runs on Auto; Android has no such floor. The list is also empty until the stream's renditions are known, so hide or disable your picker while it is.
See example/lib/main.dart for a working picker built on this API.
DRM Service Usage #
// Generate DRM session
final sessionJson = await DartDrmService.generateSession(
apiKey: 'your-api-key',
userId: 'user-123',
videoId: 'video-456',
platform: 'Android',
);
// Parse session data
final sessionData = DartDrmService.parseSessionResponse(sessionJson);
// Generate stream URLs
final drmUrls = DartDrmService.generateDrmStreamUrls(
sessionData: sessionData,
streamType: 'widevine',
quality: 'h264',
);
Configuration #
Watermark Configuration #
The WatermarkConfig class supports the following properties:
id: Unique identifier for the watermarktype: Either 'static' or 'dynamic'name: Display name for the watermarktext: The text to displayalpha: Opacity (0.0 to 1.0)color: Hex color code (e.g., '#FFFFFF')size: Font size in pixelsinterval: Animation interval in milliseconds (for dynamic watermarks)skip: Number of intervals to skipx,y: Position as percentage of video dimensions
DRM Session Data #
The DrmSessionData class contains:
edgeName: DRM edge server nametoken: Session authentication tokenecdsaKey: ECDSA signing key for URL signingdrmServerUrl: License server URLkid: Key ID (optional)certificateUrl: Certificate URL (optional)watermark: Embedded watermark configuration (optional)
Platform Support #
- Android: Uses ExoPlayer with Media3 for optimal performance
- iOS: Uses AVPlayer with FairPlay DRM support
- Web: Uses video.js with EME support (planned)
Dependencies #
The plugin requires the following dependencies in your pubspec.yaml:
dependencies:
bmdrm_mobile: ^0.0.1
Internal dependencies (managed by the plugin):
crypto: ^3.0.3http: ^1.1.0pointycastle: ^3.7.3plugin_platform_interface: ^2.0.2
Android Setup #
The plugin automatically includes the necessary Android dependencies:
androidx.media3:media3-exoplayerandroidx.media3:media3-uiandroidx.media3:media3-datasourceandroidx.media3:media3-commonandroidx.media3:media3-sessionandroidx.media3:media3-exoplayer-drm
iOS Setup #
For iOS, ensure you have the following in your ios/Podfile:
platform :ios, '12.0'
Example #
See the example/ directory for a complete working example.
Building for Production #
To build an obfuscated release:
cd example
flutter build apk --release --obfuscate --split-debug-info=build/debug-info
License #
This project is licensed under the MIT License - see the LICENSE file for details.
Contributing #
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Support #
For support, please open an issue on GitHub or contact the development team.