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

Plugin for implementing video player.

example/lib/main.dart

import 'dart:developer';
import 'dart:async';

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

void main() => 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://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4";
  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 {
                          final position = await controller.getPosition();
                          print(position);
                        },
                        child: Text("Get Position"),
                      ),
                      SizedBox(width: 8),
                      ElevatedButton(
                        onPressed: () async {
                          final duration = await controller.getDuration();
                          print(duration);
                        },
                        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();
    // WidgetsBinding.instance.addPostFrameCallback((_) {
    //   controller.load(widget.url);
    // });
  }

  @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);

                    int attempts = 0;
                    int duration = 0;
                    while (duration == 0 && attempts < 10) {
                      await Future.delayed(Duration(milliseconds: 200));
                      duration = await controller.getDuration();
                      attempts++;
                    }
                    print('Video duration: $duration');
                    print('Attempts to get duration: $attempts');

                    // 2. Ждём готовности
                    await Future.delayed(Duration(milliseconds: 500));

                    // 3. Проверяем валидность позиции
                    if (duration > 0 &&
                        widget.initialPosition > 0 &&
                        widget.initialPosition < duration) {
                      // 3.1. Перематываем на нужную позицию
                      await controller.seekTo(widget.initialPosition);
                      print('Seek to position: ${widget.initialPosition}');
                    } else {
                      print(
                        'Initial position $widget.initialPosition is not valid or duration is 0. Skipping seek.',
                      );
                    }
                    await Future.delayed(Duration(milliseconds: 250));
                    // 4. Проверяем результат seek
                    final actualPosition = await controller.getPosition();
                    if (actualPosition == 0 && widget.initialPosition > 0) {
                      print(
                        'Seek to position $widget.initialPosition failed. Retrying...',
                      );
                      // Повторная попытка
                      await controller.seekTo(widget.initialPosition);
                      final finalActualPosition = await controller
                          .getPosition();
                      print(
                        'Final actual position after retry: $finalActualPosition',
                      );
                    } else {
                      print(
                        'Seek to position $widget.initialPosition successful. Actual position: $actualPosition',
                      );
                    }

                    // 5. Возобновляем воспроизведение если нужно
                    if (widget.wasPlaying) {
                      await controller.play();
                    } else {
                      await controller.pause();
                    }

                    print(
                      'Fullscreen video initialized at position: ${await controller.getPosition()}',
                    );
                  } catch (e) {
                    print('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();
    // _updatePosition();
  }

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

  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: () {
              widget.controller.load(url: widget.url, isLoop: widget.isLoop);
              _updatePosition();
            },
          ),
          // Затемнённый фон и интерфейс
          if (true)
            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),
                      ],
                    ),
                  ],
                ),
              ),
            ),
        ],
      ),
    );
  }
}