native_video_player_plugin 2.1.10 copy "native_video_player_plugin: ^2.1.10" to clipboard
native_video_player_plugin: ^2.1.10 copied to clipboard

Plugin for implementing video player.

example/lib/main.dart

import 'dart:async';
import 'package:permission_handler/permission_handler.dart';
import 'package:flutter/material.dart';
import 'package:native_video_player_plugin/native_video_player_plugin.dart';

void main() {
  // ensureImagePermissions().then((granted) {
  //   if (!granted) {
  //     print("Permission to access photos not granted");
  //   }
  // });
  runApp(MaterialApp(home: VideoPage()));
}

class VideoPage extends StatefulWidget {
  const VideoPage({super.key});

  @override
  State<VideoPage> createState() => _VideoPageState();
}

class _VideoPageState extends State<VideoPage> {
  final controller = NativeVideoPlayerController();
  // String status = "Status: idle";
  bool isLooping = false;
  String url =
      "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8";
  // "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4?authkey=JWT AIzaSy";
  // "https://live-hls-abr-cdn.livepush.io/live/bigbuckbunnyclip/index.m3u8";

  double width = 320;
  double height = 180;
  double aspectRatio = 16 / 9;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Native Player")),
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              Padding(
                padding: EdgeInsets.all(8.0),
                child: PlayerStatusText(controller: controller),
              ),
              Container(
                decoration: BoxDecoration(
                  border: Border.all(color: Colors.red, width: 2),
                  color: Colors.black26,
                ),
                width: width,
                height: height,
                child: CustomVideoPlayerOverlay(
                  controller: controller,
                  url: url,
                  isLoop: isLooping,
                ),
              ),

              SizedBox(height: 16),
              // StatusWidget(controller: controller),
              SizedBox(height: 8),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      ElevatedButton(
                        onPressed: () => controller.load(
                          url: url,
                          // , true
                        ),
                        child: Text("Load"),
                      ),
                      SizedBox(width: 8),
                      ElevatedButton(
                        onPressed: controller.play,
                        child: Text("Play"),
                      ),
                    ],
                  ),
                  Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      SizedBox(width: 8),
                      ElevatedButton(
                        onPressed: controller.pause,
                        child: Text("Pause"),
                      ),
                    ],
                  ),
                  Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      ElevatedButton(
                        onPressed: () async {
                          ensureImagePermissions().then((granted) {
                            if (!granted) {
                              ScaffoldMessenger.of(context).showSnackBar(
                                SnackBar(
                                  content: Text(
                                    "Permission to access photos not granted",
                                  ),
                                ),
                              );
                            }
                          });
                          final fileName = await showDialog<String>(
                            context: context,
                            builder: (context) {
                              final controller = TextEditingController();
                              return AlertDialog(
                                title: Text('Имя файла'),
                                content: TextField(
                                  controller: controller,
                                  decoration: InputDecoration(
                                    hintText: 'screenshot.png',
                                  ),
                                ),
                                actions: [
                                  TextButton(
                                    onPressed: () =>
                                        Navigator.of(context).pop(),
                                    child: Text('Отмена'),
                                  ),
                                  TextButton(
                                    onPressed: () => Navigator.of(
                                      context,
                                    ).pop(controller.text.trim()),
                                    child: Text('OK'),
                                  ),
                                ],
                              );
                            },
                          );
                          if (fileName == null) return;
                          final result = await controller
                              .screenshotAndSaveToGallery(
                                fileName: fileName.isEmpty ? null : fileName,
                              );
                          ScaffoldMessenger.of(context).showSnackBar(
                            SnackBar(
                              content: Text(
                                result['success'] == true
                                    ? 'Скриншот сохранён: ${result['message']}'
                                    : 'Ошибка: ${result['message']}',
                              ),
                            ),
                          );
                        },
                        child: Text("Скриншот"),
                      ),
                      SizedBox(width: 8),
                      ElevatedButton(
                        onPressed: () async {
                          final position = await controller.getPosition();
                          debugPrint(position.toString());
                        },
                        child: Text("Get Position"),
                      ),
                      SizedBox(width: 8),
                      ElevatedButton(
                        onPressed: () async {
                          final duration = await controller.getDuration();
                          debugPrint(duration.toString());
                        },
                        child: Text("Get Duration"),
                      ),
                    ],
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class PlayerStatusWidget extends StatefulWidget {
  final NativeVideoPlayerController controller;
  const PlayerStatusWidget({super.key, required this.controller});

  @override
  State<PlayerStatusWidget> createState() => _PlayerStatusWidgetState();
}

class _PlayerStatusWidgetState extends State<PlayerStatusWidget> {
  PlayerStatus status = PlayerStatus.unknown;
  @override
  initState() {
    super.initState();
    widget.controller.addStatusListener(updateStatus);
  }

  void updateStatus(status) {
    if (status == this.status) return;
    setState(() {
      this.status = status;
    });
  }

  @override
  void dispose() {
    widget.controller.removeStatusListener(updateStatus);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    bool showProgressIndicator =
        (status == PlayerStatus.buffering ||
        status == PlayerStatus.idle ||
        status == PlayerStatus.unknown);
    return Container(
      child: (showProgressIndicator)
          ? CircularProgressIndicator()
          : SizedBox.shrink(),
    );
  }
}

class PlayerStatusText extends StatefulWidget {
  final NativeVideoPlayerController controller;
  const PlayerStatusText({super.key, required this.controller});

  @override
  State<PlayerStatusText> createState() => _PlayerStatusTextState();
}

class _PlayerStatusTextState extends State<PlayerStatusText> {
  PlayerStatus status = PlayerStatus.unknown;
  @override
  initState() {
    super.initState();
    widget.controller.addStatusListener(updateStatus);
  }

  void updateStatus(status) {
    if (status == this.status) return;
    setState(() {
      this.status = status;
    });
  }

  @override
  void dispose() {
    widget.controller.removeStatusListener(updateStatus);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(8.0),
      decoration: BoxDecoration(
        border: Border.all(color: Colors.blue, width: 1),
        borderRadius: BorderRadius.circular(8),
        color: Colors.transparent,
      ),
      child: Text(
        "Status: $status",
        style: TextStyle(fontSize: 16, color: Colors.black54),
      ),
    );
  }
}

class FullscreenVideoPage extends StatefulWidget {
  final String url;
  final int initialPosition;
  final bool wasPlaying;
  const FullscreenVideoPage({
    super.key,
    required this.url,
    required this.initialPosition,
    required this.wasPlaying,
  });

  @override
  State<FullscreenVideoPage> createState() => _FullscreenVideoPageState();
}

class _FullscreenVideoPageState extends State<FullscreenVideoPage> {
  late final NativeVideoPlayerController controller;

  @override
  void initState() {
    super.initState();
    controller = NativeVideoPlayerController();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: SafeArea(
        child: Stack(
          children: [
            Center(
              child: NativeVideoPlayer(
                controller: controller,
                onViewCreated: () async {
                  try {
                    await controller.load(url: widget.url, isLoop: true);
                    await Future.delayed(Duration(milliseconds: 500));
                    // await controller.play();
                  } catch (e) {
                    debugPrint('Error initializing fullscreen video: $e');
                  }
                },
              ),
            ),
            Positioned(
              top: 16,
              right: 16,
              child: IconButton(
                icon: Icon(Icons.close, color: Colors.white),
                onPressed: () async {
                  final position = await controller.getPosition();
                  final isPlaying = await controller.isPlaying();
                  Navigator.of(
                    context,
                  ).pop({'position': position, 'isPlaying': isPlaying});
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class CustomVideoPlayerOverlay extends StatefulWidget {
  final NativeVideoPlayerController controller;
  final String url;
  final bool isLoop;
  final Map<String, String>? headers;

  const CustomVideoPlayerOverlay({
    super.key,
    required this.controller,
    required this.url,
    this.isLoop = false,
    this.headers,
  });

  @override
  State<CustomVideoPlayerOverlay> createState() =>
      _CustomVideoPlayerOverlayState();
}

class _CustomVideoPlayerOverlayState extends State<CustomVideoPlayerOverlay> {
  bool _showControls = true;
  bool _isPlaying = false;
  int _position = 0;
  int _duration = 1;
  Timer? _hideTimer;

  @override
  void initState() {
    super.initState();
    widget.controller.addStatusListener(_onStatusChanged);
    _startHideTimer();
  }

  @override
  void dispose() {
    widget.controller.removeStatusListener(_onStatusChanged);
    _hideTimer?.cancel();
    super.dispose();
  }

  void _onStatusChanged(PlayerStatus status) async {
    if (status == PlayerStatus.playing) {
      setState(() => _isPlaying = true);
    } else {
      setState(() => _isPlaying = false);
    }
    _updatePosition();
  }

  void _startHideTimer() {
    _hideTimer?.cancel();
    _hideTimer = Timer(const Duration(seconds: 3), () {
      setState(() => _showControls = false);
    });
  }

  void _toggleControls() {
    setState(() => _showControls = !_showControls);
    if (_showControls) _startHideTimer();
  }

  void _onPlayPause() async {
    if (_isPlaying) {
      await widget.controller.pause();
    } else {
      await widget.controller.play();
    }
    setState(() => _isPlaying = !_isPlaying);
    _startHideTimer();
  }

  void _onFullscreen() async {
    final currentPosition = await widget.controller.getPosition();
    final currentStatus = await widget.controller.getStatus();

    final result = await Navigator.of(context).push(
      MaterialPageRoute(
        builder: (_) => FullscreenVideoPage(
          url: widget.url,
          initialPosition: currentPosition,
          wasPlaying: currentStatus == PlayerStatus.playing,
        ),
      ),
    );

    if (result is Map) {
      final newPosition = result['position'] as int? ?? 0;
      final isPlaying = result['isPlaying'] as bool? ?? false;
      if (newPosition > 0) {
        // await widget.controller.seekTo(newPosition);
      }
      if (isPlaying) {
        await widget.controller.play();
      } else {
        await widget.controller.pause();
      }
    }
  }

  void _onSeek(double value) async {
    // await widget.controller.seekTo(value.toInt());
    setState(() => _position = value.toInt());
    _startHideTimer();
  }

  Future<void> _updatePosition() async {
    final pos = await widget.controller.getPosition();
    final dur = await widget.controller.getDuration();
    if (mounted) {
      setState(() {
        _position = pos;
        _duration = dur > 0 ? dur : 1;
      });
    }
    if (_isPlaying) {
      Future.delayed(const Duration(milliseconds: 250), _updatePosition);
    }
  }

  String _formatDuration(int millis) {
    final seconds = (millis / 1000).truncate();
    final minutes = (seconds / 60).truncate();
    final remainingSeconds = seconds % 60;
    return '${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}';
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _toggleControls,
      child: Stack(
        children: [
          NativeVideoPlayer(
            controller: widget.controller,
            onViewCreated: () async {
              await widget.controller.load(
                url: widget.url,
                isLoop: widget.isLoop,
              );
              await _updatePosition();
            },
          ),
          if (false)
            Positioned.fill(
              child: Container(
                color: Colors.black.withValues(alpha: 0.4),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.end,
                  children: [
                    Expanded(
                      child: Center(
                        child: IconButton(
                          iconSize: 64,
                          icon: Icon(
                            _isPlaying ? Icons.pause_circle : Icons.play_circle,
                            color: Colors.white,
                          ),
                          onPressed: _onPlayPause,
                        ),
                      ),
                    ),
                    Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 16.0),
                      child: Row(
                        children: [
                          Text(
                            _formatDuration(_position),
                            style: const TextStyle(color: Colors.white),
                          ),
                          Expanded(
                            child: Slider(
                              value: _position.toDouble().clamp(
                                0,
                                _duration.toDouble(),
                              ),
                              min: 0,
                              max: _duration.toDouble(),
                              onChanged: (value) => _onSeek(value),
                              activeColor: Colors.white,
                              inactiveColor: Colors.white38,
                            ),
                          ),
                          Text(
                            _formatDuration(_duration),
                            style: const TextStyle(color: Colors.white),
                          ),
                        ],
                      ),
                    ),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.end,
                      children: [
                        IconButton(
                          icon: const Icon(
                            Icons.fullscreen,
                            color: Colors.white,
                          ),
                          onPressed: _onFullscreen,
                        ),
                        const SizedBox(width: 8),
                      ],
                    ),
                  ],
                ),
              ),
            ),
        ],
      ),
    );
  }
}

Future<bool> ensureImagePermissions() async {
  if (await Permission.photos.isGranted) {
    return true;
  }

  var status = await Permission.photos.request();

  if (status.isGranted) {
    return true;
  } else {
    print("Доступ к фото не предоставлен");
    return false;
  }
}