native_video_player_plugin 1.3.0
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),
],
),
],
),
),
),
],
),
);
}
}