bmdrm_mobile 0.0.27
bmdrm_mobile: ^0.0.27 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/bmdrm_resume_store.dart';
const String _sessionJson =
'{"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}';
class Lesson {
const Lesson(this.id, this.title);
final String id;
final String title;
}
// Each lesson tracks its own resume position under its id.
const _lessons = [
Lesson('lesson-1', 'Lesson 1 · DRM playback'),
Lesson('lesson-2', 'Lesson 2 · Adaptive streaming'),
];
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
final scheme = ColorScheme.fromSeed(seedColor: Colors.indigo);
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'BMDRM Player',
theme: ThemeData(useMaterial3: true, colorScheme: scheme),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
List<ResumeEntry> _inProgress = const [];
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
final entries = await VideoPlayerController.continueWatching();
if (mounted) setState(() => _inProgress = entries);
}
Future<void> _open(Lesson lesson) async {
await Navigator.of(
context,
).push(MaterialPageRoute(builder: (_) => PlayerPage(lesson: lesson)));
_load();
}
String _titleFor(String id) =>
_lessons.firstWhere((l) => l.id == id, orElse: () => Lesson(id, id)).title;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('BMDRM Player')),
body: ListView(
children: [
if (_inProgress.isNotEmpty) ...[
const _SectionTitle('Continue watching'),
for (final e in _inProgress)
ListTile(
leading: const Icon(Icons.play_circle_fill),
title: Text(_titleFor(e.resumeId)),
subtitle: LinearProgressIndicator(value: e.progress),
trailing: Text('${(e.progress * 100).round()}%'),
onTap: () => _open(Lesson(e.resumeId, _titleFor(e.resumeId))),
),
const Divider(height: 24),
],
const _SectionTitle('Lessons'),
for (final lesson in _lessons)
ListTile(
leading: const Icon(Icons.ondemand_video),
title: Text(lesson.title),
onTap: () => _open(lesson),
),
],
),
);
}
}
class _SectionTitle extends StatelessWidget {
const _SectionTitle(this.text);
final String text;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text(text, style: Theme.of(context).textTheme.titleMedium),
);
}
}
class PlayerPage extends StatefulWidget {
const PlayerPage({super.key, required this.lesson});
final Lesson lesson;
@override
State<PlayerPage> createState() => _PlayerPageState();
}
class _PlayerPageState extends State<PlayerPage> {
// Also the handle for the quality picker below — see _QualityPicker.
final BmdrmFullscreenController _fsController = BmdrmFullscreenController();
@override
void dispose() {
_fsController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final videoHeight = (size.width * 9 / 16)
.clamp(0.0, size.height * 0.7)
.toDouble();
return Scaffold(
appBar: AppBar(title: Text(widget.lesson.title)),
body: SingleChildScrollView(
child: Column(
children: [
BmdrmVideoPlayer.drmFromBackendSession(
backendSession: jsonDecode(_sessionJson),
width: double.infinity,
height: videoHeight,
autoPlay: true,
resumeId: widget.lesson.id,
resumeFromLastPosition: true,
fullscreenController: _fsController,
onVideoQualityChanged: (quality) =>
debugPrint('Video quality is now ${quality.label}'),
fullscreenDrawerBuilder: (context, controller) =>
_AboutDrawer(onClose: controller.closeDrawer),
fullscreenDrawerToggle: const BmdrmDrawerToggle(
label: 'À propos de cette leçon',
),
),
_QualityPicker(controller: _fsController),
],
),
),
);
}
}
/// App-drawn video quality picker.
///
/// Android already has one inside the player (the gear button), but AVKit has
/// nowhere to hang a quality control, so on iOS a picker only exists if the app
/// draws it — from `videoQualities` / `selectVideoQuality`, as done here. The
/// controller is a `ChangeNotifier`, so the list and the current selection both
/// arrive as notifications once native knows the stream's renditions.
class _QualityPicker extends StatelessWidget {
const _QualityPicker({required this.controller});
final BmdrmFullscreenController controller;
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (context, _) {
final qualities = controller.videoQualities;
return ListTile(
leading: const Icon(Icons.high_quality_outlined),
title: const Text('Video quality'),
subtitle: Text(
qualities.isEmpty
? 'Only one rendition, or not known yet'
: controller.currentVideoQuality.label,
),
trailing: PopupMenuButton<int>(
enabled: qualities.isNotEmpty,
initialValue: controller.currentVideoQuality.id,
onSelected: controller.selectVideoQuality,
itemBuilder: (context) => <PopupMenuEntry<int>>[
// -1 is Auto; it is never part of the reported list.
const PopupMenuItem<int>(value: -1, child: Text('Auto')),
for (final quality in qualities)
PopupMenuItem<int>(
value: quality.id,
child: Text(quality.label),
),
],
),
);
},
);
}
}
/// Example content for the player's fullscreen drawer. Any Flutter widget works
/// here — 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) {
return 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(
'About this lesson',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700),
),
),
IconButton(icon: const Icon(Icons.close), onPressed: onClose),
],
),
),
const Expanded(
child: Padding(
padding: EdgeInsets.all(16),
child: Text(
'Your custom fullscreen panel goes here — put any Flutter '
'widgets you like beside the video.',
),
),
),
],
),
);
}
}