flutter_native_player 2.0.0 copy "flutter_native_player: ^2.0.0" to clipboard
flutter_native_player: ^2.0.0 copied to clipboard

A Flutter plugin for Android, iOS for playing back video on a Widget surface with full customization support.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_native_player/flutter_native_player.dart';
import 'package:flutter_native_player/flutter_native_player_controller.dart';
import 'package:flutter_native_player/method_manager/playback_state.dart';
import 'package:flutter_native_player/model/duration_state.dart';
import 'package:flutter_native_player/model/player_resource.dart';
import 'package:flutter_native_player/model/player_subtitle_resource.dart';
import 'package:flutter_native_player/model/quality_model.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Native Player Demo',
      theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
      home: const HomeScreen(),
    );
  }
}

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Flutter Native Player')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () => Navigator.push(
                context,
                MaterialPageRoute(builder: (_) => const BasicPlayerExample()),
              ),
              child: const Text('Basic Player (No Controls)'),
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () => Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (_) => const CustomControlsExample(),
                ),
              ),
              child: const Text('Custom Controls Example'),
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () => Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (_) => const ExternalControllerExample(),
                ),
              ),
              child: const Text('External Controller Example'),
            ),
          ],
        ),
      ),
    );
  }
}

/// Example 1: Basic player with no controls (just the video)
class BasicPlayerExample extends StatelessWidget {
  const BasicPlayerExample({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Basic Player')),
      body: Center(
        child: FlutterNativePlayer(
          playerResource: PlayerResource(
            videoUrl:
                "https://p-events-delivery.akamaized.net/2109isftrwvmiekgrjkbbhxhfbkxjkoj/m3u8/vod_index.m3u8",
          ),
          playWhenReady: true,
          width: double.infinity,
          height: 250,
        ),
      ),
    );
  }
}

/// Example 2: Player with custom overlay controls
class CustomControlsExample extends StatefulWidget {
  const CustomControlsExample({Key? key}) : super(key: key);

  @override
  State<CustomControlsExample> createState() => _CustomControlsExampleState();
}

class _CustomControlsExampleState extends State<CustomControlsExample> {
  String videoUrl =
      "https://p-events-delivery.akamaized.net/2109isftrwvmiekgrjkbbhxhfbkxjkoj/m3u8/vod_index.m3u8";

  final playerSubtitleResource = [
    PlayerSubtitleResource(
      language: "English",
      subtitleUrl:
          "https://raw.githubusercontent.com/Pisey-Nguon/Player-Resource/master/%5BEnglish%5D%20Apple%20Event%20%E2%80%94%20October%2013%20%5BDownSub.com%5D.srt",
    ),
    PlayerSubtitleResource(
      language: "Japanese",
      subtitleUrl:
          "https://raw.githubusercontent.com/Pisey-Nguon/Player-Resource/master/%5BJapanese%5D%20Apple%20Event%20%E2%80%94%20October%2013%20%5BDownSub.com%5D.srt",
    ),
  ];

  bool _showControls = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Custom Controls')),
      body: SingleChildScrollView(
        child: Column(
          children: [
            FlutterNativePlayer(
              playerResource: PlayerResource(
                videoUrl: videoUrl,
                playerSubtitleResources: playerSubtitleResource,
              ),
              playWhenReady: true,
              width: double.infinity,
              height: 250,
              overlayBuilder:
                  (context, controller, playbackState, durationState) {
                    return CustomPlayerControls(
                      controller: controller,
                      playbackState: playbackState,
                      durationState: durationState,
                      showControls: _showControls,
                      onToggleControls: () {
                        setState(() {
                          _showControls = !_showControls;
                        });
                      },
                    );
                  },
              loadingBuilder: (context, controller) {
                return const Center(
                  child: CircularProgressIndicator(
                    valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
                  ),
                );
              },
            ),
            const SizedBox(height: 20),
            const Padding(
              padding: EdgeInsets.all(16),
              child: Text(
                'This example shows custom controls built using the overlayBuilder. '
                'Tap the video to toggle controls visibility.',
                textAlign: TextAlign.center,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

/// Custom player controls widget
class CustomPlayerControls extends StatefulWidget {
  final FlutterNativePlayerController controller;
  final PlaybackState playbackState;
  final DurationState? durationState;
  final bool showControls;
  final VoidCallback onToggleControls;

  const CustomPlayerControls({
    Key? key,
    required this.controller,
    required this.playbackState,
    required this.durationState,
    required this.showControls,
    required this.onToggleControls,
  }) : super(key: key);

  @override
  State<CustomPlayerControls> createState() => _CustomPlayerControlsState();
}

class _CustomPlayerControlsState extends State<CustomPlayerControls> {
  bool _isDragging = false;
  double _dragValue = 0.0;
  double? _lastSeekValue;

  String _formatDuration(Duration? duration) {
    if (duration == null) return '00:00';
    final minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0');
    final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0');
    if (duration.inHours > 0) {
      final hours = duration.inHours.toString().padLeft(2, '0');
      return '$hours:$minutes:$seconds';
    }
    return '$minutes:$seconds';
  }

  IconData _getPlayPauseIcon() {
    switch (widget.playbackState) {
      case PlaybackState.play:
        return Icons.pause;
      case PlaybackState.finish:
        return Icons.replay;
      default:
        return Icons.play_arrow;
    }
  }

  void _showQualitySelector(BuildContext context) {
    final qualities = widget.controller.getAvailableQualities();
    if (qualities.isEmpty) return;

    showModalBottomSheet(
      context: context,
      builder: (context) => SafeArea(
        child: SingleChildScrollView(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              const Padding(
                padding: EdgeInsets.all(16),
                child: Text(
                  'Select Quality',
                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                ),
              ),
              ...qualities.map(
                (quality) => ListTile(
                  leading: quality.urlQuality == widget.controller.currentQualityUrl
                      ? const Icon(Icons.check, color: Colors.blue)
                      : const SizedBox(width: 24),
                  title: Text(_getQualityLabel(quality)),
                  onTap: () {
                    widget.controller.changeQuality(quality);
                    Navigator.pop(context);
                  },
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  String _getQualityLabel(QualityModel quality) {
    if (quality.height == 0) return 'Auto';
    return '${quality.height}p';
  }

  void _showSpeedSelector(BuildContext context) {
    final speeds = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0];

    showModalBottomSheet(
      context: context,
      builder: (context) => SafeArea(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Padding(
              padding: EdgeInsets.all(16),
              child: Text(
                'Playback Speed',
                style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
              ),
            ),
            ...speeds.map(
              (speed) => ListTile(
                leading: speed == widget.controller.currentSpeed
                    ? const Icon(Icons.check, color: Colors.blue)
                    : const SizedBox(width: 24),
                title: Text('${speed}x'),
                onTap: () {
                  widget.controller.setPlaybackSpeed(speed);
                  Navigator.pop(context);
                },
              ),
            ),
          ],
        ),
      ),
    );
  }

  void _showSubtitleSelector(BuildContext context) {
    final subtitles = widget.controller.getAvailableSubtitles();
    if (subtitles.isEmpty) {
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(const SnackBar(content: Text('No subtitles available')));
      return;
    }

    showModalBottomSheet(
      context: context,
      builder: (context) => SafeArea(
        child: SingleChildScrollView(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              const Padding(
                padding: EdgeInsets.all(16),
                child: Text(
                  'Select Subtitle',
                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                ),
              ),
              ...subtitles.map(
                (subtitle) => ListTile(
                  leading: subtitle.name == widget.controller.currentSubtitle?.name
                      ? const Icon(Icons.check, color: Colors.blue)
                      : const SizedBox(width: 24),
                  title: Text(subtitle.name ?? 'Unknown'),
                  onTap: () {
                    widget.controller.changeSubtitle(subtitle);
                    Navigator.pop(context);
                  },
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: widget.onToggleControls,
      behavior: HitTestBehavior.opaque,
      child: AnimatedOpacity(
        opacity: widget.showControls ? 1.0 : 0.0,
        duration: const Duration(milliseconds: 300),
        child: IgnorePointer(
          ignoring: !widget.showControls,
          child: Container(
            decoration: const BoxDecoration(
              gradient: LinearGradient(
                begin: Alignment.topCenter,
                end: Alignment.bottomCenter,
                colors: [
                  Colors.black54,
                  Colors.transparent,
                  Colors.transparent,
                  Colors.black54,
                ],
              ),
            ),
            child: Stack(
              children: [
                Column(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    // Top bar with settings
                    Padding(
                      padding: const EdgeInsets.all(8),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.end,
                      children: [
                        IconButton(
                          icon: const Icon(Icons.subtitles, color: Colors.white),
                          onPressed: () => _showSubtitleSelector(context),
                        ),
                        IconButton(
                          icon: const Icon(Icons.speed, color: Colors.white),
                          onPressed: () => _showSpeedSelector(context),
                        ),
                        IconButton(
                          icon: const Icon(
                            Icons.high_quality,
                            color: Colors.white,
                          ),
                          onPressed: () => _showQualitySelector(context),
                        ),
                      ],
                    ),
                  ),
              
               
              
                  // Bottom bar with progress
                  Padding(
                    padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
                    child: Column(
                      children: [
                        // Progress slider
                        SliderTheme(
                          data: SliderTheme.of(context).copyWith(
                            activeTrackColor: Colors.blue,
                            inactiveTrackColor: Colors.white30,
                            thumbColor: Colors.blue,
                            thumbShape: const RoundSliderThumbShape(
                              enabledThumbRadius: 6,
                            ),
                            overlayShape: const RoundSliderOverlayShape(
                              overlayRadius: 12,
                            ),
                            trackHeight: 3,
                          ),
                          child: Slider(
                            value: _getSliderValue(),
                            min: 0,
                            max: _getSliderMax(),
                            onChangeStart: (value) {
                              setState(() {
                                _isDragging = true;
                                _dragValue = value;
                              });
                            },
                            onChanged: (value) {
                              // Only update if changed by at least 100ms to prevent jitter
                              if ((_dragValue - value).abs() > 100) {
                                setState(() {
                                  _dragValue = value;
                                });
                              }
                            },
                            onChangeEnd: (value) {
                              setState(() {
                                _isDragging = false;
                                _lastSeekValue = value;
                              });
                              widget.controller.seekTo(
                                Duration(milliseconds: value.toInt()),
                              );
                            },
                          ),
                        ),
                        // Time labels
                        Row(
                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
                          children: [
                            Text(
                              _formatDuration(_getCurrentProgress()),
                              style: const TextStyle(
                                color: Colors.white,
                                fontSize: 12,
                              ),
                            ),
                            Text(
                              _formatDuration(widget.durationState?.total),
                              style: const TextStyle(
                                color: Colors.white,
                                fontSize: 12,
                              ),
                            ),
                          ],
                        ),
                      ],
                    ),
                  ),
                ],
              ),
                 // Center play/pause button
                Align(
                  alignment: Alignment.center,
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                    children: [
                      IconButton(
                        icon: const Icon(Icons.replay_10),
                        iconSize: 40,
                        color: Colors.white,
                        onPressed: () => widget.controller.seekBackward(),
                      ),
                      Opacity(
                        opacity: widget.playbackState != PlaybackState.loading ? 1.0 : 0.0,
                        child: IconButton(
                          icon: Icon(_getPlayPauseIcon()),
                          iconSize: 60,
                          color: Colors.white,
                          onPressed: () => widget.controller.playOrPause(),
                        ),
                      ),
                      IconButton(
                        icon: const Icon(Icons.forward_10),
                        iconSize: 40,
                        color: Colors.white,
                        onPressed: () => widget.controller.seekForward(),
                      ),
                    ],
                  ),
                ),
              ]
            ),
          ),
        ),
      ),
    );
  }

  double _getSliderValue() {
    if (_isDragging) {
      return _dragValue;
    }
    // If we just seeked, keep showing that value until progress catches up
    if (_lastSeekValue != null) {
      final currentProgress = (widget.durationState?.progress.inMilliseconds ?? 0).toDouble();
      // Check if progress has caught up (within 500ms threshold)
      if ((currentProgress - _lastSeekValue!).abs() < 500) {
        _lastSeekValue = null;
      } else {
        return _lastSeekValue!;
      }
    }
    if((widget.durationState?.progress.inMilliseconds ?? 0) > (widget.durationState?.total?.inMilliseconds ?? 0)) {
      return (widget.durationState?.total?.inMilliseconds ?? 0).toDouble();
    }
    return (widget.durationState?.progress.inMilliseconds ?? 0).toDouble();
  }

  double _getSliderMax() {
    final max = (widget.durationState?.total?.inMilliseconds ?? 0).toDouble();
    return max > 0 ? max : 1.0;
  }

  Duration? _getCurrentProgress() {
    if (_isDragging) {
      return Duration(milliseconds: _dragValue.toInt());
    }
    if (_lastSeekValue != null) {
      return Duration(milliseconds: _lastSeekValue!.toInt());
    }
    return widget.durationState?.progress;
  }
}

/// Example 3: Using external controller
class ExternalControllerExample extends StatefulWidget {
  const ExternalControllerExample({Key? key}) : super(key: key);

  @override
  State<ExternalControllerExample> createState() =>
      _ExternalControllerExampleState();
}

class _ExternalControllerExampleState extends State<ExternalControllerExample> {
  final _controller = FlutterNativePlayerController();

  String videoUrl =
      "https://p-events-delivery.akamaized.net/2109isftrwvmiekgrjkbbhxhfbkxjkoj/m3u8/vod_index.m3u8";

  PlaybackState _playbackState = PlaybackState.loading;
  DurationState? _durationState;

  @override
  void initState() {
    super.initState();

    // Listen to state changes
    _controller.playbackStateStream.listen((state) {
      if (mounted) {
        setState(() => _playbackState = state);
      }
    });

    _controller.durationStateStream.listen((state) {
      if (mounted) {
        setState(() => _durationState = state);
      }
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  String _formatDuration(Duration? duration) {
    if (duration == null) return '00:00';
    final minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0');
    final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0');
    return '$minutes:$seconds';
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('External Controller')),
      body: Column(
        children: [
          // Video player without overlay (bare video)
          FlutterNativePlayer(
            playerResource: PlayerResource(videoUrl: videoUrl),
            controller: _controller,
            playWhenReady: true,
            width: double.infinity,
            height: 250,
          ),

          // External controls
          Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              children: [
                // Status display
                Text(
                  'Status: ${_playbackState.name}',
                  style: const TextStyle(fontSize: 16),
                ),
                const SizedBox(height: 8),
                Text(
                  '${_formatDuration(_durationState?.progress)} / '
                  '${_formatDuration(_durationState?.total)}',
                  style: const TextStyle(fontSize: 16),
                ),
                const SizedBox(height: 16),

                // Progress bar
                LinearProgressIndicator(
                  value: _getProgress(),
                  backgroundColor: Colors.grey[300],
                ),
                const SizedBox(height: 24),

                // Playback controls
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: [
                    ElevatedButton.icon(
                      onPressed: () => _controller.seekBackward(),
                      icon: const Icon(Icons.replay_10),
                      label: const Text('-10s'),
                    ),
                    ElevatedButton.icon(
                      onPressed: () => _controller.playOrPause(),
                      icon: Icon(
                        _playbackState == PlaybackState.play
                            ? Icons.pause
                            : Icons.play_arrow,
                      ),
                      label: Text(
                        _playbackState == PlaybackState.play ? 'Pause' : 'Play',
                      ),
                    ),
                    ElevatedButton.icon(
                      onPressed: () => _controller.seekForward(),
                      icon: const Icon(Icons.forward_10),
                      label: const Text('+10s'),
                    ),
                  ],
                ),
                const SizedBox(height: 16),

                // Speed controls
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: [
                    OutlinedButton(
                      onPressed: () => _controller.setPlaybackSpeed(0.5),
                      child: const Text('0.5x'),
                    ),
                    OutlinedButton(
                      onPressed: () => _controller.setPlaybackSpeed(1.0),
                      child: const Text('1x'),
                    ),
                    OutlinedButton(
                      onPressed: () => _controller.setPlaybackSpeed(1.5),
                      child: const Text('1.5x'),
                    ),
                    OutlinedButton(
                      onPressed: () => _controller.setPlaybackSpeed(2.0),
                      child: const Text('2x'),
                    ),
                  ],
                ),
                const SizedBox(height: 16),

                // Seek controls
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: [
                    TextButton(
                      onPressed: () =>
                          _controller.seekTo(const Duration(seconds: 0)),
                      child: const Text('Start'),
                    ),
                    TextButton(
                      onPressed: () =>
                          _controller.seekTo(const Duration(seconds: 30)),
                      child: const Text('0:30'),
                    ),
                    TextButton(
                      onPressed: () =>
                          _controller.seekTo(const Duration(minutes: 1)),
                      child: const Text('1:00'),
                    ),
                    TextButton(
                      onPressed: () =>
                          _controller.seekTo(const Duration(minutes: 2)),
                      child: const Text('2:00'),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  double _getProgress() {
    if (_durationState == null) return 0.0;
    final total = _durationState!.total?.inMilliseconds ?? 0;
    if (total == 0) return 0.0;
    return _durationState!.progress.inMilliseconds / total;
  }
}
10
likes
150
points
57
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter plugin for Android, iOS for playing back video on a Widget surface with full customization support.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

collection, flutter, flutter_widget_from_html_core, plugin_platform_interface

More

Packages that depend on flutter_native_player

Packages that implement flutter_native_player