esite_flutter_player 0.1.1 copy "esite_flutter_player: ^0.1.1" to clipboard
esite_flutter_player: ^0.1.1 copied to clipboard

PlatformAndroid

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';
    }
  }
}
0
likes
130
points
150
downloads

Publisher

verified publisheresite-lab.com

Weekly Downloads

Professional Flutter plugin for secure DRM-protected video playback on Android. Features Widevine/PlayReady DRM, emulator detection, screenshot/screen recording blocking (FLAG_SECURE), adaptive streaming, quality selection, and comprehensive playback controls using Media3 ExoPlayer. Enterprise-ready security for premium content delivery.

Topics

#flutter #video #drm #exoplayer #security

Documentation

API reference

License

unknown (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on esite_flutter_player

Packages that implement esite_flutter_player