bmdrm_mobile 0.0.15
bmdrm_mobile: ^0.0.15 copied to clipboard
A Flutter plugin for DRM-protected video playback with watermark support.
example/lib/main.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:bmdrm_mobile/bmdrm_video_player.dart';
import 'package:bmdrm_mobile/dart_drm_service.dart' as drm_service;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'BMDRM Player',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const MyHomePage(),
);
}
}
enum DrmMethod { drmMinimal, drmWithSession, drmFromBackendSession }
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final DrmMethod _selectedMethod = DrmMethod.drmFromBackendSession;
// Replace with your actual credentials from BMDRM dashboard
final String _apiKey = "YOUR_API_KEY_HERE";
final String _userId = "YOUR_USER_ID_HERE";
final String _videoId = "YOUR_VIDEO_ID_HERE";
// Replace with your actual pre-generated backend session JSON
final String _preGeneratedSessionJson = '''
{"edge":"https://YOUR_EDGE_SERVER.video-crypt.com","videoId":"YOUR_VIDEO_ID","signature":"YOUR_SIGNATURE","userId":"YOUR_USER_ID","subscriptionId":"YOUR_SUBSCRIPTION_ID","sessionId":"YOUR_SESSION_ID","nonce":"YOUR_NONCE","date":0}
''';
// Replace with your actual DRM session data
final String _drmSessionDataJson = '''
{
"edgeName": "https://your-edge-server.com",
"token": "your-session-token",
"ecdsaKey": "your-ecdsa-key",
"drmServerUrl": "https://your-license-server.com/license"
}
''';
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final videoHeight = screenWidth * 9 / 16;
return Scaffold(
backgroundColor: Colors.red,
body: SafeArea(
child: Column(
children: [
// Method selector
// Player
Expanded(child: Center(child: _buildPlayer(videoHeight))),
],
),
),
);
}
Widget _buildPlayer(double videoHeight) {
try {
switch (_selectedMethod) {
case DrmMethod.drmMinimal:
return _buildDrmMinimalPlayer(videoHeight);
case DrmMethod.drmWithSession:
return _buildDrmWithSessionPlayer(videoHeight);
case DrmMethod.drmFromBackendSession:
return _buildDrmFromBackendSessionPlayer(videoHeight);
}
} catch (e) {
return Container(
width: double.infinity,
height: videoHeight,
color: Colors.black,
child: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Error: $e',
style: const TextStyle(color: Colors.red),
textAlign: TextAlign.center,
),
),
),
);
}
}
Widget _buildDrmMinimalPlayer(double videoHeight) {
return BmdrmVideoPlayer.drmMinimal(
apiKey: _apiKey,
userId: _userId,
videoId: _videoId,
watermarkConfig: null,
width: double.infinity,
height: videoHeight,
autoPlay: true,
onError: (error) {
debugPrint('DRM Minimal Error: $error');
},
onPlay: () {
debugPrint('DRM Minimal: Video started playing');
},
onPause: () {
debugPrint('DRM Minimal: Video paused');
},
);
}
Widget _buildDrmWithSessionPlayer(double videoHeight) {
try {
final sessionMap = jsonDecode(_drmSessionDataJson);
final drmSessionData = drm_service.DrmSessionData.fromJson(sessionMap);
return BmdrmVideoPlayer.drmWithSession(
drmSessionData: drmSessionData,
watermarkConfig: null,
width: double.infinity,
height: videoHeight,
autoPlay: true,
onError: (error) {
debugPrint('DRM With Session Error: $error');
},
onPlay: () {
debugPrint('DRM With Session: Video started playing');
},
onPause: () {
debugPrint('DRM With Session: Video paused');
},
);
} catch (e) {
return Container(
width: double.infinity,
height: videoHeight,
color: Colors.black,
child: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Error parsing session data: $e\n\nPlease update _drmSessionDataJson with valid session data.',
style: const TextStyle(color: Colors.red),
textAlign: TextAlign.center,
),
),
),
);
}
}
Widget _buildDrmFromBackendSessionPlayer(double videoHeight) {
try {
final sessionMap = jsonDecode(_preGeneratedSessionJson);
return BmdrmVideoPlayer.drmFromBackendSession(
backendSession: sessionMap,
watermarkConfig: null,
width: double.infinity,
height: videoHeight,
autoPlay: true,
// Last-position resume. This is ON by default; playback automatically
// resumes where the viewer left off and the position is saved on the
// device as they watch. The key falls back to the session's videoId, so
// `resumeId` here is optional — pass it to use your own stable key.
resumeId: _videoId,
resumeFromLastPosition: true,
// ── Customizable fullscreen drawer ──────────────────────────────────
// Passing a builder turns on the Flutter-rendered fullscreen: while in
// fullscreen a toggle button opens this panel beside the video. The app
// owns the content entirely — put any widgets here.
fullscreenDrawerIcon: const Icon(Icons.menu_book_rounded),
fullscreenDrawerBuilder: (context, controller) =>
_AboutDrawer(onClose: controller.closeDrawer),
onError: (error) {
debugPrint('DRM From Backend Session Error: $error');
},
onPlay: () {
debugPrint('DRM From Backend Session: Video started playing');
},
onPause: () {
debugPrint('DRM From Backend Session: Video paused');
},
);
} catch (e, stack) {
debugPrint('Error parsing backend session: $e');
debugPrint('Stack trace: $stack');
return Container(
width: double.infinity,
height: videoHeight,
color: Colors.black,
child: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Error parsing backend session: $e',
style: const TextStyle(color: Colors.red),
textAlign: TextAlign.center,
),
),
),
);
}
}
}
/// Example content for the fullscreen drawer: a tabbed "À propos" panel similar
/// to the reference design. This lives entirely in the host app — the plugin
/// only provides the sliding panel and the toggle button.
class _AboutDrawer extends StatelessWidget {
const _AboutDrawer({required this.onClose});
final VoidCallback onClose;
@override
Widget build(BuildContext context) {
const tabs = ['Ressources', 'Description', 'Q&R', 'Notes'];
return DefaultTabController(
length: tabs.length,
initialIndex: tabs.length - 1,
child: SafeArea(
left: false,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 8, 4),
child: Row(
children: [
const Expanded(
child: Text(
'À propos',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
),
),
),
IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: onClose,
),
],
),
),
TabBar(
isScrollable: true,
tabAlignment: TabAlignment.start,
tabs: [for (final t in tabs) Tab(text: t)],
),
const Expanded(
child: TabBarView(
children: [
_DrawerPlaceholder(icon: Icons.folder_outlined, label: 'Ressources'),
_DrawerPlaceholder(icon: Icons.description_outlined, label: 'Description'),
_DrawerPlaceholder(icon: Icons.forum_outlined, label: 'Questions & réponses'),
_DrawerPlaceholder(
icon: Icons.sticky_note_2_outlined,
label: 'Appuyez sur "+" pour écrire votre première remarque',
),
],
),
),
],
),
),
);
}
}
class _DrawerPlaceholder extends StatelessWidget {
const _DrawerPlaceholder({required this.icon, required this.label});
final IconData icon;
final String label;
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 40, color: Colors.grey),
const SizedBox(height: 12),
Text(
label,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.grey),
),
],
),
),
);
}
}