esite_flutter_player 0.1.1
esite_flutter_player: ^0.1.1 copied to clipboard
Professional Flutter plugin for secure DRM-protected video playback on Android. Features Widevine/PlayReady DRM, emulator detection, screenshot/screen recording blocking (FLAG_SECURE), adaptive strea [...]
example/lib/main.dart
import 'dart:async';
import 'package:esite_flutter_player/esite_flutter_player.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ESite Flutter Player Demo',
theme: ThemeData.dark(),
home: const PlayerDemoPage(),
);
}
}
// Test scenarios - simplified to essential security and functionality tests
enum TestScenario {
validAuthNoProtection,
validAuthWithProtection,
invalidAuth,
emulatorProtection,
screenshotProtection,
}
extension TestScenarioExtension on TestScenario {
String get description {
switch (this) {
case TestScenario.validAuthNoProtection:
return '✅ Valid Auth - No Security';
case TestScenario.validAuthWithProtection:
return '✅ Valid Auth - Full Security';
case TestScenario.invalidAuth:
return '❌ Invalid Authentication';
case TestScenario.emulatorProtection:
return '🚫 Emulator Detection';
case TestScenario.screenshotProtection:
return '🔒 Screenshot Protection';
}
}
}
class PlayerDemoPage extends StatefulWidget {
const PlayerDemoPage({super.key});
@override
State<PlayerDemoPage> createState() => _PlayerDemoPageState();
}
class _PlayerDemoPageState extends State<PlayerDemoPage> {
ESitePlayerController? controller;
ESitePlayerState _currentState = ESitePlayerState.idle;
String? _errorMessage;
bool _isMuted = false;
double _volume = 1.0;
double _playbackSpeed = 1.0;
List<Map<String, dynamic>> _videoTracks = [];
String? _selectedTrackId;
TestScenario? _currentScenario;
bool _isPlayerInitialized = false;
final List<String> _eventLog = [];
// Store subscriptions to cancel them
StreamSubscription<ESitePlayerState>? _stateSubscription;
StreamSubscription<ESiteDrmEvent>? _drmSubscription;
StreamSubscription<ESitePlayerError>? _errorSubscription;
static const int _maxEventLogSize =
30; // Limit log size to prevent memory issues
@override
void initState() {
super.initState();
// Don't auto-initialize - let user select scenario first
}
void _addToLog(String message) {
if (!mounted) return;
setState(() {
_eventLog.insert(
0,
'${DateTime.now().toString().substring(11, 19)} - $message',
);
// More aggressive limit to prevent memory bloat
if (_eventLog.length > _maxEventLogSize) {
_eventLog.removeRange(_maxEventLogSize, _eventLog.length);
}
});
}
void _initializePlayer(TestScenario scenario) {
// Cancel existing subscriptions first
_cancelSubscriptions();
// Dispose existing controller if any
controller?.dispose();
setState(() {
_currentScenario = scenario;
_errorMessage = null;
_currentState = ESitePlayerState.idle;
_isPlayerInitialized = false;
_videoTracks = [];
_selectedTrackId = null;
_eventLog.clear();
});
_addToLog('Initializing player with scenario: ${scenario.name}');
final config = _getConfigForScenario(scenario);
controller = ESitePlayerController(config);
controller!.initialize();
setState(() {
_isPlayerInitialized = true;
});
// Subscribe to streams and store subscriptions
_stateSubscription = controller!.stateStream.listen((state) {
if (mounted) {
setState(() {
_currentState = state;
});
_addToLog('State changed: ${state.name}');
if (state == ESitePlayerState.ready) {
_loadVideoTracks();
}
}
});
_drmSubscription = controller!.drmStream.listen((event) {
_addToLog('DRM event: ${event.type.name} - ${event.message ?? ''}');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('DRM: ${event.type.name}'),
duration: const Duration(seconds: 2),
backgroundColor: event.type == ESiteDrmEventType.licenseFailed
? Colors.red
: Colors.blue,
),
);
}
});
_errorSubscription = controller!.errorStream.listen((error) {
_addToLog('Error: ${error.code.name} - ${error.message}');
if (mounted) {
setState(() {
_errorMessage = '${error.code.name}: ${error.message}';
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error: ${error.message}'),
backgroundColor: Colors.red,
duration: const Duration(seconds: 4),
),
);
}
});
}
void _cancelSubscriptions() {
_stateSubscription?.cancel();
_drmSubscription?.cancel();
_errorSubscription?.cancel();
_stateSubscription = null;
_drmSubscription = null;
_errorSubscription = null;
}
ESitePlayerConfig _getConfigForScenario(TestScenario scenario) {
// Axinom v10 CMAF test content (valid)
const validSourceUrl =
'https://media.axprod.net/TestVectors/Cmaf/protected_1080p_h264_cbcs/manifest.mpd';
const validLicenseUrl =
'https://drm-widevine-licensing.axprod.net/AcquireLicense?AxDrmMessage=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICIzMDJmODBkZC00MTFlLTQ4ODYtYmNhNS1iYjFmODAxOGEwMjQiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAicm9LQWcwdDdKaTFpNDNmd3YremZ0UT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiUG9saWN5IEEiLAogICAgICAgICJwbGF5cmVhZHkiOiB7CiAgICAgICAgICAibWluX2RldmljZV9zZWN1cml0eV9sZXZlbCI6IDE1MCwKICAgICAgICAgICJwbGF5X2VuYWJsZXJzIjogWwogICAgICAgICAgICAiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3IgogICAgICAgICAgXQogICAgICAgIH0KICAgICAgfQogICAgXQogIH0KfQ._NfhLVY7S6k8TJDWPeMPhUawhympnrk6WAZHOVjER6M';
switch (scenario) {
case TestScenario.validAuthNoProtection:
// Working playback with NO security - for development/testing
return ESitePlayerConfig(
sourceUrl: validSourceUrl,
drm: ESiteDrmConfig(
licenseUrl: validLicenseUrl,
scheme: ESiteDrmScheme.widevine,
licenseHeaders: {},
),
autoPlay: false,
securityConfig: const ESiteSecurityConfig.disabled(),
);
case TestScenario.validAuthWithProtection:
// Working playback with FULL security - production mode
return ESitePlayerConfig(
sourceUrl: validSourceUrl,
drm: ESiteDrmConfig(
licenseUrl: validLicenseUrl,
scheme: ESiteDrmScheme.widevine,
licenseHeaders: {},
),
autoPlay: false,
securityConfig: ESiteSecurityConfig.maximum(
onSecurityViolation: (violation) {
_addToLog('🚨 Security violation: ${violation.message}');
},
),
);
case TestScenario.invalidAuth:
// Invalid license URL - should fail with network/auth error
return ESitePlayerConfig(
sourceUrl: validSourceUrl,
drm: ESiteDrmConfig(
licenseUrl: 'https://invalid-drm-server.example.com/license',
scheme: ESiteDrmScheme.widevine,
licenseHeaders: {},
),
autoPlay: false,
securityConfig: const ESiteSecurityConfig.disabled(),
);
case TestScenario.emulatorProtection:
// Tests emulator detection - blocks on emulator, works on real device
return ESitePlayerConfig(
sourceUrl: validSourceUrl,
drm: ESiteDrmConfig(
licenseUrl: validLicenseUrl,
scheme: ESiteDrmScheme.widevine,
licenseHeaders: {},
),
autoPlay: false,
securityConfig: ESiteSecurityConfig(
blockEmulators: true, // Will block if running on emulator
enableScreenProtection: false, // Focus on emulator detection only
onSecurityViolation: (violation) {
_addToLog('🚫 Emulator detected: ${violation.message}');
},
),
);
case TestScenario.screenshotProtection:
// Tests screenshot blocking - press play then try to screenshot
return ESitePlayerConfig(
sourceUrl: validSourceUrl,
drm: ESiteDrmConfig(
licenseUrl: validLicenseUrl,
scheme: ESiteDrmScheme.widevine,
licenseHeaders: {},
),
autoPlay: false,
securityConfig: ESiteSecurityConfig(
blockEmulators: false, // Allow emulator for testing
enableScreenProtection: true, // Focus on screenshot protection
onSecurityViolation: (violation) {
_addToLog('🔒 Screenshot blocked: ${violation.message}');
},
),
);
}
}
Future<void> _loadVideoTracks() async {
try {
final tracks = await controller!.getAvailableVideoTracks();
if (mounted) {
setState(() {
_videoTracks = tracks;
});
_addToLog('Loaded ${tracks.length} video tracks');
}
} catch (e) {
_addToLog('Failed to load video tracks: $e');
}
}
@override
void dispose() {
_cancelSubscriptions();
controller?.dispose();
controller = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: const Text('ESite Player Test Suite'),
actions: [
IconButton(
icon: const Icon(Icons.info_outline),
onPressed: _showAboutDialog,
),
],
),
body: Column(
children: [
// Player View
Expanded(
flex: 3,
child: Container(color: Colors.black, child: _buildPlayerArea()),
),
// Controls Panel
Expanded(
flex: 3,
child: Container(
color: Colors.grey[900],
child: _isPlayerInitialized
? _buildControlsPanel()
: _buildScenarioSelector(),
),
),
],
),
);
}
Widget _buildPlayerArea() {
if (!_isPlayerInitialized) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.play_circle_outline, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'Select a test scenario below',
style: TextStyle(color: Colors.grey, fontSize: 16),
),
],
),
);
}
if (_errorMessage != null) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(_getErrorIcon(_errorMessage!), color: Colors.red, size: 64),
const SizedBox(height: 16),
Text(
_getErrorTitle(_errorMessage!),
style: const TextStyle(
color: Colors.red,
fontSize: 18,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
_errorMessage!,
style: const TextStyle(color: Colors.red, fontSize: 14),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
onPressed: () {
setState(() {
_isPlayerInitialized = false;
_currentScenario = null;
_errorMessage = null;
});
controller?.dispose();
},
icon: const Icon(Icons.arrow_back),
label: const Text('Try Another'),
),
],
),
],
),
),
);
}
return controller != null
? ESitePlayerView(controller: controller!)
: const Center(child: CircularProgressIndicator());
}
Widget _buildScenarioSelector() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'🧪 Test Scenarios',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'Select a scenario to test different authentication and error states:',
style: TextStyle(color: Colors.grey),
),
const SizedBox(height: 16),
...TestScenario.values.map(
(scenario) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: ElevatedButton(
onPressed: () => _initializePlayer(scenario),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(16),
backgroundColor: _getScenarioColor(scenario),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
scenario.description,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
_getScenarioDetails(scenario),
style: const TextStyle(
fontSize: 12,
color: Colors.white70,
),
),
],
),
),
const Icon(Icons.arrow_forward_ios),
],
),
),
),
),
],
),
);
}
String _getScenarioDetails(TestScenario scenario) {
switch (scenario) {
case TestScenario.validAuthNoProtection:
return 'Axinom test content, all security disabled (development mode)';
case TestScenario.validAuthWithProtection:
return 'Axinom test content with full security (emulator + screenshot blocking)';
case TestScenario.invalidAuth:
return 'Invalid license server - tests error handling';
case TestScenario.emulatorProtection:
return 'Blocks on emulator, works on real device';
case TestScenario.screenshotProtection:
return 'Press PLAY then try screenshot - should be blocked';
}
}
Color _getScenarioColor(TestScenario scenario) {
switch (scenario) {
case TestScenario.validAuthNoProtection:
return Colors.blue[700]!; // Basic test - no security
case TestScenario.validAuthWithProtection:
return Colors.green[700]!; // Success with full security
case TestScenario.invalidAuth:
return Colors.orange[700]!; // Expected error
case TestScenario.emulatorProtection:
return Colors.purple[700]!; // Security feature test
case TestScenario.screenshotProtection:
return Colors.indigo[700]!; // Security feature test
}
}
Widget _buildControlsPanel() {
return DefaultTabController(
length: 3,
child: Column(
children: [
Container(
color: Colors.grey[850],
child: Column(
children: [
// Current Scenario
Container(
padding: const EdgeInsets.all(12),
color: Colors.grey[800],
child: Row(
children: [
Expanded(
child: Text(
'Testing: ${_currentScenario?.description ?? 'Unknown'}',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
IconButton(
icon: const Icon(Icons.close, size: 20),
onPressed: () {
setState(() {
_isPlayerInitialized = false;
_currentScenario = null;
});
controller?.dispose();
},
tooltip: 'Change Scenario',
),
],
),
),
// State Display
Container(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Icon(
_getStateIcon(_currentState),
color: _getStateColor(_currentState),
),
const SizedBox(width: 12),
Text(
'State: ${_currentState.name}',
style: const TextStyle(fontSize: 14),
),
],
),
),
const TabBar(
tabs: [
Tab(
icon: Icon(Icons.play_circle, size: 20),
text: 'Controls',
),
Tab(icon: Icon(Icons.settings, size: 20), text: 'Settings'),
Tab(icon: Icon(Icons.list, size: 20), text: 'Events'),
],
),
],
),
),
Expanded(
child: TabBarView(
children: [
_buildControlsTab(),
_buildSettingsTab(),
_buildEventsTab(),
],
),
),
],
),
);
}
Widget _buildControlsTab() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Playback Controls
Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
icon: const Icon(Icons.skip_previous),
onPressed: () async {
final hasPrev = await controller!.hasPrevious();
if (hasPrev) {
await controller!.previous();
}
},
tooltip: 'Previous',
),
IconButton(
icon: Icon(
_currentState == ESitePlayerState.playing
? Icons.pause_circle_filled
: Icons.play_circle_filled,
),
iconSize: 64,
onPressed: () async {
if (_currentState == ESitePlayerState.playing) {
await controller!.pause();
} else {
await controller!.play();
}
},
tooltip: _currentState == ESitePlayerState.playing
? 'Pause'
: 'Play',
),
IconButton(
icon: const Icon(Icons.skip_next),
onPressed: () async {
final hasNext = await controller!.hasNext();
if (hasNext) {
await controller!.next();
}
},
tooltip: 'Next',
),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton.icon(
onPressed: () => controller!.seekBackward(
const Duration(seconds: 10),
),
icon: const Icon(Icons.replay_10, size: 20),
label: const Text('-10s'),
),
ElevatedButton.icon(
onPressed: () => controller!.seekForward(
const Duration(seconds: 10),
),
icon: const Icon(Icons.forward_10, size: 20),
label: const Text('+10s'),
),
ElevatedButton.icon(
onPressed: () => controller!.seekForward(
const Duration(seconds: 30),
),
icon: const Icon(Icons.forward_30, size: 20),
label: const Text('+30s'),
),
],
),
],
),
),
),
],
),
);
}
Widget _buildSettingsTab() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Volume Control
Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Volume',
style: TextStyle(fontWeight: FontWeight.bold),
),
Row(
children: [
IconButton(
icon: Icon(
_isMuted ? Icons.volume_off : Icons.volume_up,
),
onPressed: () async {
final newMuted = !_isMuted;
setState(() {
_isMuted = newMuted;
});
await controller!.setMuted(newMuted);
},
),
Expanded(
child: Slider(
value: _volume,
min: 0.0,
max: 1.0,
onChanged: (value) async {
setState(() {
_volume = value;
_isMuted = value == 0.0;
});
await controller!.setVolume(value);
},
),
),
Text(
'${(_volume * 100).toInt()}%',
style: const TextStyle(fontSize: 14),
),
],
),
],
),
),
),
const SizedBox(height: 12),
// Speed Control
Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Playback Speed: ${_playbackSpeed}x',
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Slider(
value: _playbackSpeed,
min: 0.5,
max: 4.0,
divisions: 7,
label: '${_playbackSpeed}x',
onChanged: (value) async {
setState(() {
_playbackSpeed = value;
});
await controller!.setPlaybackSpeed(value);
},
),
Wrap(
spacing: 8,
children: [0.5, 1.0, 1.5, 2.0, 4.0]
.map(
(speed) => ChoiceChip(
label: Text('${speed}x'),
selected: _playbackSpeed == speed,
onSelected: (selected) async {
if (selected) {
setState(() {
_playbackSpeed = speed;
});
await controller!.setPlaybackSpeed(speed);
}
},
),
)
.toList(),
),
],
),
),
),
const SizedBox(height: 12),
// Track Selection
if (_videoTracks.isNotEmpty)
Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Video Quality',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: _videoTracks.map((track) {
final trackId = track['id'] as String;
final label = track['label'] as String;
final isSelected = trackId == _selectedTrackId;
return ChoiceChip(
label: Text(label),
selected: isSelected,
onSelected: (selected) async {
if (selected) {
setState(() {
_selectedTrackId = trackId;
});
await controller!.setVideoTrack(trackId);
}
},
);
}).toList(),
),
],
),
),
),
],
),
);
}
Widget _buildEventsTab() {
return Column(
children: [
Container(
padding: const EdgeInsets.all(12),
color: Colors.grey[850],
child: Row(
children: [
const Expanded(
child: Text(
'Event Log',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
TextButton.icon(
onPressed: () {
setState(() {
_eventLog.clear();
});
},
icon: const Icon(Icons.clear_all, size: 16),
label: const Text('Clear'),
),
],
),
),
Expanded(
child: _eventLog.isEmpty
? const Center(
child: Text(
'No events yet',
style: TextStyle(color: Colors.grey),
),
)
: ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: _eventLog.length,
itemBuilder: (context, index) {
return Container(
margin: const EdgeInsets.only(bottom: 4),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[850],
borderRadius: BorderRadius.circular(4),
),
child: Text(
_eventLog[index],
style: const TextStyle(
fontSize: 12,
fontFamily: 'monospace',
),
),
);
},
),
),
],
);
}
void _showAboutDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('ESite Player Test Suite'),
content: const SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Test Scenarios:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
SizedBox(height: 12),
Text('✅ Valid Auth - No Security'),
Text(' Tests basic playback without protection'),
SizedBox(height: 8),
Text('✅ Valid Auth - Full Security'),
Text(' Tests production mode with all security'),
SizedBox(height: 8),
Text('❌ Invalid Authentication'),
Text(' Tests error handling'),
SizedBox(height: 8),
Text('🚫 Emulator Detection'),
Text(' Blocks on emulator, works on device'),
SizedBox(height: 8),
Text('🔒 Screenshot Protection'),
Text(' Blocks screenshots during playback'),
SizedBox(height: 16),
Text('Features:', style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Text('• DRM support (Widevine/PlayReady)'),
Text('• Screenshot/recording blocking'),
Text('• Emulator detection'),
Text('• Full playback controls'),
Text('• Quality selection'),
Text('• Volume & speed control'),
Text('• Event logging'),
SizedBox(height: 12),
Text(
'Note: Test on a real device for full DRM functionality.',
style: TextStyle(
fontStyle: FontStyle.italic,
color: Colors.orange,
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
),
);
}
IconData _getStateIcon(ESitePlayerState state) {
switch (state) {
case ESitePlayerState.playing:
return Icons.play_circle_filled;
case ESitePlayerState.paused:
return Icons.pause_circle_filled;
case ESitePlayerState.buffering:
return Icons.hourglass_empty;
case ESitePlayerState.ready:
return Icons.check_circle;
case ESitePlayerState.completed:
return Icons.check_circle_outline;
case ESitePlayerState.error:
return Icons.error;
default:
return Icons.circle_outlined;
}
}
Color _getStateColor(ESitePlayerState state) {
switch (state) {
case ESitePlayerState.playing:
return Colors.green;
case ESitePlayerState.paused:
return Colors.orange;
case ESitePlayerState.buffering:
return Colors.blue;
case ESitePlayerState.ready:
return Colors.green;
case ESitePlayerState.completed:
return Colors.grey;
case ESitePlayerState.error:
return Colors.red;
default:
return Colors.grey;
}
}
IconData _getErrorIcon(String errorMessage) {
final lowerError = errorMessage.toLowerCase();
if (lowerError.contains('network') || lowerError.contains('connection')) {
return Icons.wifi_off;
} else if (lowerError.contains('drm') ||
lowerError.contains('license') ||
lowerError.contains('auth')) {
return Icons.lock_outline;
} else if (lowerError.contains('format') ||
lowerError.contains('decoder') ||
lowerError.contains('supported')) {
return Icons.video_settings;
} else if (lowerError.contains('timeout')) {
return Icons.access_time;
} else {
return Icons.error_outline;
}
}
String _getErrorTitle(String errorMessage) {
final lowerError = errorMessage.toLowerCase();
if (lowerError.contains('network') || lowerError.contains('connection')) {
return 'Connection Problem';
} else if (lowerError.contains('drm') ||
lowerError.contains('license') ||
lowerError.contains('auth')) {
return 'Access Denied';
} else if (lowerError.contains('format') ||
lowerError.contains('decoder') ||
lowerError.contains('supported')) {
return 'Format Not Supported';
} else if (lowerError.contains('timeout')) {
return 'Request Timeout';
} else if (lowerError.contains('configuration')) {
return 'Configuration Error';
} else {
return 'Playback Error';
}
}
}