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

PlatformAndroid

Secure DRM-enabled video playback plugin for Flutter using Android Media3 ExoPlayer with Widevine DRM support.

example/lib/main.dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';

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(),
    );
  }
}

enum TestScenario {
  validAuth,
  esiteApi,
  expiredAuth,
  invalidAuth,
  noAuth,
  wrongScheme,
  networkError,
  invalidUrl,
}

extension TestScenarioExtension on TestScenario {
  String get description {
    switch (this) {
      case TestScenario.validAuth:
        return 'Valid Auth (Should Work)';
      case TestScenario.esiteApi:
        return 'ESite API (Live Fetch)';
      case TestScenario.expiredAuth:
        return 'Expired Token (DRM Error)';
      case TestScenario.invalidAuth:
        return 'Invalid License URL (Network Error)';
      case TestScenario.noAuth:
        return 'Missing Auth Header (DRM Error)';
      case TestScenario.wrongScheme:
        return 'Wrong DRM Scheme (Error)';
      case TestScenario.networkError:
        return 'Network Error (Invalid Domain)';
      case TestScenario.invalidUrl:
        return 'Invalid URL Format (Error)';
    }
  }
}

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;
  final List<String> _eventLog = [];

  StreamSubscription<ESitePlayerState>? _stateSubscription;
  StreamSubscription<ESiteDrmEvent>? _drmSubscription;
  StreamSubscription<ESitePlayerError>? _errorSubscription;

  static const int _maxEventLogSize = 30;

  void _addToLog(String message) {
    if (!mounted) return;
    setState(() {
      _eventLog.insert(
        0,
        '${DateTime.now().toString().substring(11, 19)} - $message',
      );
      if (_eventLog.length > _maxEventLogSize) {
        _eventLog.removeRange(_maxEventLogSize, _eventLog.length);
      }
    });
  }

  bool _isLoading = false;

  void _launchScenario(TestScenario scenario) async {
    _cancelSubscriptions();
    controller?.dispose();

    setState(() {
      _errorMessage = null;
      _currentState = ESitePlayerState.idle;
      _eventLog.clear();
      _isLoading = false;
    });

    _addToLog('Launching scenario: ${scenario.name}');

    ESitePlayerConfig config;

    if (scenario == TestScenario.esiteApi) {
      setState(() => _isLoading = true);
      _addToLog('Fetching entitlement from ESite API...');
      try {
        config = await _fetchEsiteConfig();
        if (!mounted) return;
        _addToLog('API response received, launching player');
      } catch (e) {
        if (!mounted) return;
        setState(() {
          _isLoading = false;
          _errorMessage = 'API fetch failed: $e';
        });
        _addToLog('Error: API fetch failed - $e');
        return;
      }
      setState(() => _isLoading = false);
    } else {
      config = _getConfigForScenario(scenario);
    }

    controller = ESitePlayerController(config);

    _stateSubscription = controller!.stateStream.listen((state) {
      if (mounted) {
        setState(() {
          _currentState = state;
        });
        _addToLog('State: ${state.name}');
      }
    });

    _drmSubscription = controller!.drmStream.listen((event) {
      _addToLog('DRM: ${event.type.name} - ${event.message ?? ''}');
    });

    _errorSubscription = controller!.errorStream.listen((error) {
      _addToLog('Error: ${error.code.name} - ${error.message}');
      if (mounted) {
        setState(() {
          _errorMessage = '${error.code.name}: ${error.message}';
        });
      }
    });

    controller!.launchPlayer();
  }

  Future<ESitePlayerConfig> _fetchEsiteConfig() async {
    final client = HttpClient();
    try {
      final request = await client.getUrl(Uri.parse(
        'https://video-service.esite-lab.com/api/v1/public/videos/c03016d41f341bc4452ac8e610670a3d/widevine?quality=360',
      ));
      request.headers.set('Api-Key', 'sk_test_dfisa02dikbl');
      final response = await request.close();
      final body = await response.transform(utf8.decoder).join();

      if (response.statusCode != 200) {
        throw Exception('HTTP ${response.statusCode}: $body');
      }

      final json = jsonDecode(body) as Map<String, dynamic>;
      final entitlementMessage = json['entitlement_message'] as String;
      final licenseServerUrl = json['license_server_url'] as String;
      final manifestUrl = json['manifest_url'] as String;

      return ESitePlayerConfig(
        sourceUrl: manifestUrl,
        drm: ESiteDrmConfig(
          licenseUrl: licenseServerUrl,
          scheme: ESiteDrmScheme.widevine,
          licenseHeaders: {
            'X-AxDRM-Message': entitlementMessage,
          },
        ),
        autoPlay: true,
        preventScreenshots: true,
        preventScreenRecording: true,
        primaryColor: const Color(0xFF6C63FF),
        secondaryColor: const Color(0xFFFFFFFF),
      );
    } finally {
      client.close();
    }
  }

  void _cancelSubscriptions() {
    _stateSubscription?.cancel();
    _drmSubscription?.cancel();
    _errorSubscription?.cancel();
    _stateSubscription = null;
    _drmSubscription = null;
    _errorSubscription = null;
  }

  ESitePlayerConfig _getConfigForScenario(TestScenario scenario) {
    switch (scenario) {
      case TestScenario.esiteApi:
        throw StateError('esiteApi scenario is handled via async fetch');
      case TestScenario.validAuth:
        return ESitePlayerConfig(
          sourceUrl:
              'https://media.axprod.net/TestVectors/Cmaf/protected_1080p_h264_cbcs/manifest.mpd',
          drm: ESiteDrmConfig(
            licenseUrl:
                'https://drm-widevine-licensing.axprod.net/AcquireLicense?AxDrmMessage=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICIzMDJmODBkZC00MTFlLTQ4ODYtYmNhNS1iYjFmODAxOGEwMjQiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAicm9LQWcwdDdKaTFpNDNmd3YremZ0UT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiUG9saWN5IEEiLAogICAgICAgICJwbGF5cmVhZHkiOiB7CiAgICAgICAgICAibWluX2RldmljZV9zZWN1cml0eV9sZXZlbCI6IDE1MCwKICAgICAgICAgICJwbGF5X2VuYWJsZXJzIjogWwogICAgICAgICAgICAiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3IgogICAgICAgICAgXQogICAgICAgIH0KICAgICAgfQogICAgXQogIH0KfQ._NfhLVY7S6k8TJDWPeMPhUawhympnrk6WAZHOVjER6M',
            scheme: ESiteDrmScheme.widevine,
            licenseHeaders: {},
          ),
          autoPlay: true,
          preventScreenshots: false,
          preventScreenRecording: false,
          primaryColor: const Color(0xFF6C63FF),
          secondaryColor: const Color(0xFFFFFFFF),
        );

      case TestScenario.expiredAuth:
        return ESitePlayerConfig(
          sourceUrl:
              'https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_1080p.mpd',
          drm: ESiteDrmConfig(
            licenseUrl:
                'https://drm-widevine-licensing.axtest.net/AcquireLicense',
            scheme: ESiteDrmScheme.widevine,
            licenseHeaders: {
              'X-AxDRM-Message': 'expired_or_invalid_token_here',
            },
          ),
          autoPlay: false,
          preventScreenshots: true,
          preventScreenRecording: true,
        );

      case TestScenario.invalidAuth:
        return ESitePlayerConfig(
          sourceUrl:
              'https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_1080p.mpd',
          drm: ESiteDrmConfig(
            licenseUrl: 'https://invalid-drm-server.example.com/license',
            scheme: ESiteDrmScheme.widevine,
            licenseHeaders: {'X-AxDRM-Message': 'some_token'},
          ),
          autoPlay: false,
          preventScreenshots: true,
          preventScreenRecording: true,
        );

      case TestScenario.noAuth:
        return ESitePlayerConfig(
          sourceUrl:
              'https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_1080p.mpd',
          drm: ESiteDrmConfig(
            licenseUrl:
                'https://drm-widevine-licensing.axtest.net/AcquireLicense',
            scheme: ESiteDrmScheme.widevine,
            licenseHeaders: {},
          ),
          autoPlay: false,
          preventScreenshots: true,
          preventScreenRecording: true,
        );

      case TestScenario.wrongScheme:
        return ESitePlayerConfig(
          sourceUrl:
              'https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_1080p.mpd',
          drm: ESiteDrmConfig(
            licenseUrl:
                'https://drm-widevine-licensing.axtest.net/AcquireLicense',
            scheme: ESiteDrmScheme.playready,
            licenseHeaders: {
              'X-AxDRM-Message':
                  'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiYjMzNjRlYjUtNTFmNi00YWUzLThjOTgtMzNjZWQ1ZTMxYzc4IiwibWVzc2FnZSI6eyJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImtleXMiOlt7ImlkIjoiOWViNDA1MGQtZTQ0Yi00ODAyLTkzMmUtMjdkNzUwODNlMjY2IiwiZW5jcnlwdGVkX2tleSI6ImxLM09qSExZVzI0Y3Rkbmd5VmRkY2c9PSJ9XX19.4Adl1Qd9Br5n_UCFU-MnpLm3bNJIRrCmCeIxqZqnUNQ',
            },
          ),
          autoPlay: false,
          preventScreenshots: true,
          preventScreenRecording: true,
        );

      case TestScenario.networkError:
        return ESitePlayerConfig(
          sourceUrl: 'https://nonexistent-domain-test-12345.com/video.mpd',
          drm: ESiteDrmConfig(
            licenseUrl: 'https://nonexistent-domain-test-12345.com/license',
            scheme: ESiteDrmScheme.widevine,
            licenseHeaders: {'Authorization': 'Bearer token'},
          ),
          autoPlay: false,
          preventScreenshots: true,
          preventScreenRecording: true,
        );

      case TestScenario.invalidUrl:
        return ESitePlayerConfig(
          sourceUrl: 'not-a-valid-url',
          drm: ESiteDrmConfig(
            licenseUrl: 'also-not-valid',
            scheme: ESiteDrmScheme.widevine,
            licenseHeaders: {},
          ),
          autoPlay: false,
          preventScreenshots: true,
          preventScreenRecording: true,
        );
    }
  }

  @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'),
      ),
      body: Column(
        children: [
          // Status bar
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
            color: Colors.grey[850],
            child: Row(
              children: [
                Icon(
                  _getStateIcon(_currentState),
                  color: _getStateColor(_currentState),
                  size: 20,
                ),
                const SizedBox(width: 8),
                Text(
                  'State: ${_currentState.name}',
                  style: const TextStyle(fontSize: 14),
                ),
                if (_errorMessage != null) ...[
                  const SizedBox(width: 12),
                  Expanded(
                    child: Text(
                      _errorMessage!,
                      style: const TextStyle(color: Colors.red, fontSize: 12),
                      overflow: TextOverflow.ellipsis,
                    ),
                  ),
                ],
              ],
            ),
          ),

          // Scenario selector + Event log
          Expanded(
            child: DefaultTabController(
              length: 2,
              child: Column(
                children: [
                  const TabBar(
                    tabs: [
                      Tab(text: 'Scenarios'),
                      Tab(text: 'Events'),
                    ],
                  ),
                  Expanded(
                    child: TabBarView(
                      children: [
                        _buildScenarioSelector(),
                        _buildEventsTab(),
                      ],
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildScenarioSelector() {
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        const Text(
          'Select a scenario to launch the native player:',
          style: TextStyle(color: Colors.grey),
        ),
        const SizedBox(height: 12),
        if (_isLoading)
          const Padding(
            padding: EdgeInsets.symmetric(vertical: 24),
            child: Center(
              child: Column(
                children: [
                  CircularProgressIndicator(),
                  SizedBox(height: 12),
                  Text(
                    'Fetching entitlement from API...',
                    style: TextStyle(color: Colors.grey),
                  ),
                ],
              ),
            ),
          ),
        ...TestScenario.values.map(
          (scenario) => Padding(
            padding: const EdgeInsets.only(bottom: 8),
            child: ElevatedButton(
              onPressed: () => _launchScenario(scenario),
              style: ElevatedButton.styleFrom(
                padding: const EdgeInsets.all(16),
                backgroundColor: (scenario == TestScenario.validAuth ||
                        scenario == TestScenario.esiteApi)
                    ? Colors.green[700]
                    : Colors.blue[700],
              ),
              child: Row(
                children: [
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          scenario.description,
                          style: const TextStyle(
                            fontSize: 15,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        const SizedBox(height: 2),
                        Text(
                          _getScenarioDetails(scenario),
                          style: const TextStyle(
                            fontSize: 12,
                            color: Colors.white70,
                          ),
                        ),
                      ],
                    ),
                  ),
                  const Icon(Icons.launch, size: 20),
                ],
              ),
            ),
          ),
        ),
      ],
    );
  }

  String _getScenarioDetails(TestScenario scenario) {
    switch (scenario) {
      case TestScenario.validAuth:
        return 'Axinom v10 CMAF test content with valid Widevine license';
      case TestScenario.esiteApi:
        return 'Fetch entitlement from ESite API, then play with Widevine DRM';
      case TestScenario.expiredAuth:
        return 'Simulate expired authentication token';
      case TestScenario.invalidAuth:
        return 'Test with wrong license server URL';
      case TestScenario.noAuth:
        return 'Attempt playback without auth headers';
      case TestScenario.wrongScheme:
        return 'Use PlayReady for Widevine content';
      case TestScenario.networkError:
        return 'Test network connectivity errors';
      case TestScenario.invalidUrl:
        return 'Test malformed content URLs';
    }
  }

  Widget _buildEventsTab() {
    return Column(
      children: [
        Container(
          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
          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',
                        ),
                      ),
                    );
                  },
                ),
        ),
      ],
    );
  }

  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;
    }
  }
}
0
likes
140
points
31
downloads

Documentation

API reference

Publisher

verified publisheresite-lab.com

Weekly Downloads

Secure DRM-enabled video playback plugin for Flutter using Android Media3 ExoPlayer with Widevine DRM support.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on esite_flutter_player

Packages that implement esite_flutter_player