298 lines
9.0 KiB
Dart
298 lines
9.0 KiB
Dart
/*
|
|
* Copyright (c) 2025
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
* SOFTWARE.
|
|
*/
|
|
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:media_kit/media_kit.dart';
|
|
import 'package:media_kit_video/media_kit_video.dart';
|
|
import 'package:splash_video/video_config.dart';
|
|
import 'package:splash_video/splash_video_controller.dart';
|
|
import 'package:splash_video/video_source.dart';
|
|
import 'package:splash_video/utils.dart';
|
|
import 'package:splash_video/splash_video_enums.dart';
|
|
|
|
/// A widget that displays a video splash screen using media_kit
|
|
class SplashVideoPlayer extends StatefulWidget {
|
|
/// Creates a VideoSplash widget
|
|
const SplashVideoPlayer({
|
|
super.key,
|
|
required this.source,
|
|
this.controller,
|
|
this.videoConfig,
|
|
this.onSplashDuration,
|
|
this.onVideoError,
|
|
this.backgroundColor,
|
|
});
|
|
|
|
/// Optional controller for managing video playback
|
|
final SplashVideoController? controller;
|
|
|
|
/// Configuration options for the video playback
|
|
final VideoConfig? videoConfig;
|
|
|
|
/// The source of the video (asset, file, network, or bytes)
|
|
final Source source;
|
|
|
|
/// Callback invoked when the video duration is determined
|
|
final OnSplashDuration? onSplashDuration;
|
|
|
|
/// Callback invoked when video fails to load
|
|
final OnVideoError? onVideoError;
|
|
|
|
/// Background color displayed behind the video
|
|
final Color? backgroundColor;
|
|
|
|
@override
|
|
State<SplashVideoPlayer> createState() => _SplashVideoPlayerState();
|
|
}
|
|
|
|
class _SplashVideoPlayerState extends State<SplashVideoPlayer> {
|
|
late Player player;
|
|
late VideoController controller;
|
|
bool isInitialized = false;
|
|
String? errorMessage;
|
|
|
|
VideoConfig get videoConfig => widget.videoConfig ?? const VideoConfig();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_initializePlayer();
|
|
}
|
|
|
|
Future<void> _initializePlayer() async {
|
|
try {
|
|
if (kDebugMode) {
|
|
debugPrint('SplashVideoPlayer: Starting initialization...');
|
|
debugPrint(' Source: ${widget.source}');
|
|
}
|
|
|
|
// Create the Player instance
|
|
player = Player();
|
|
|
|
if (kDebugMode) {
|
|
debugPrint(' ✓ Player created');
|
|
}
|
|
|
|
// Attach player to controller if provided
|
|
widget.controller?.attach(player);
|
|
|
|
// Create the VideoController
|
|
controller = VideoController(player);
|
|
|
|
if (kDebugMode) {
|
|
debugPrint(' ✓ VideoController created');
|
|
}
|
|
|
|
// Configure the player
|
|
await player.setVolume(videoConfig.volume);
|
|
|
|
// Disable audio track if audio is not enabled
|
|
if (!videoConfig.enableAudio) {
|
|
await player.setAudioTrack(AudioTrack.no());
|
|
}
|
|
|
|
// Set loop mode if controller specifies looping
|
|
if (widget.controller?.loopVideo == true) {
|
|
await player.setPlaylistMode(PlaylistMode.single);
|
|
}
|
|
|
|
// Load the video from source
|
|
final media = _getMediaFromSource();
|
|
if (kDebugMode) {
|
|
debugPrint(' ✓ Media created, opening...');
|
|
}
|
|
|
|
await player.open(media, play: false);
|
|
|
|
if (kDebugMode) {
|
|
debugPrint(' ✓ Media opened');
|
|
}
|
|
|
|
// Listen for errors
|
|
player.stream.error.listen((error) {
|
|
if (mounted && error.isNotEmpty) {
|
|
if (kDebugMode) {
|
|
debugPrint(' ✗ Player error: $error');
|
|
}
|
|
setState(() {
|
|
errorMessage = error;
|
|
});
|
|
|
|
// Call error callback if provided
|
|
widget.onVideoError?.call(error);
|
|
}
|
|
});
|
|
|
|
// Listen for when the player is ready and has duration
|
|
player.stream.duration.listen((duration) {
|
|
if (kDebugMode) {
|
|
debugPrint(' Duration update: $duration');
|
|
}
|
|
|
|
if (duration != Duration.zero && !isInitialized) {
|
|
if (mounted) {
|
|
if (kDebugMode) {
|
|
debugPrint(' ✓ Video initialized with duration: $duration');
|
|
}
|
|
setState(() {
|
|
isInitialized = true;
|
|
});
|
|
}
|
|
|
|
// Notify about the duration
|
|
widget.onSplashDuration?.call(duration);
|
|
|
|
// Call the initialization callback
|
|
videoConfig.onPlayerInitialized?.call(player);
|
|
|
|
// Play immediately if configured
|
|
if (videoConfig.playImmediately) {
|
|
if (kDebugMode) {
|
|
debugPrint(' ▶ Starting playback');
|
|
}
|
|
player.play();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Clean up subscription when widget is disposed
|
|
// Store it so we can cancel in dispose
|
|
} catch (e) {
|
|
if (mounted) {
|
|
final error = 'Failed to initialize video: $e';
|
|
if (kDebugMode) {
|
|
debugPrint(' ✗ Initialization failed: $error');
|
|
}
|
|
setState(() {
|
|
errorMessage = error;
|
|
});
|
|
|
|
// Call error callback if provided
|
|
widget.onVideoError?.call(error);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
// Don't dispose player if it's managed by a controller
|
|
if (widget.controller == null) {
|
|
// Dispose player after a short delay to avoid disposal during navigation
|
|
Future.delayed(const Duration(milliseconds: 150)).then((_) {
|
|
player.dispose();
|
|
});
|
|
}
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// Show error UI if error occurred and no callback provided
|
|
if (errorMessage != null && widget.onVideoError == null) {
|
|
return ColoredBox(
|
|
color: widget.backgroundColor ?? Colors.black,
|
|
child: Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(24.0),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(
|
|
Icons.error_outline,
|
|
color: Colors.red,
|
|
size: 64,
|
|
),
|
|
const SizedBox(height: 16),
|
|
const Text(
|
|
'Video Load Error',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
errorMessage!,
|
|
style: const TextStyle(
|
|
color: Colors.white70,
|
|
fontSize: 14,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return isInitialized
|
|
? ColoredBox(
|
|
color: widget.backgroundColor ?? Colors.transparent,
|
|
child: videoConfig.useSafeArea
|
|
? SafeArea(child: Center(child: _buildMediaWidget()))
|
|
: Center(child: _buildMediaWidget()),
|
|
)
|
|
: const SizedBox.shrink();
|
|
}
|
|
|
|
Widget _buildMediaWidget() {
|
|
// Use the fit parameter directly - much simpler!
|
|
// The Video widget handles all the sizing logic internally
|
|
return SizedBox.expand(
|
|
child: Video(
|
|
controller: controller,
|
|
controls: NoVideoControls,
|
|
fit: videoConfig.scale.toBoxFit(),
|
|
fill: widget.backgroundColor ?? const Color(0xFF000000),
|
|
),
|
|
);
|
|
}
|
|
|
|
Media _getMediaFromSource() {
|
|
switch (widget.source) {
|
|
case AssetSource assetSource:
|
|
return Media('asset:///${assetSource.path}');
|
|
case DeviceFileSource deviceFileSource:
|
|
return Media(deviceFileSource.file.path);
|
|
case NetworkFileSource networkFileSource:
|
|
final url = networkFileSource.url;
|
|
if (url != null) {
|
|
return Media(url.toString());
|
|
} else {
|
|
throw SplashVideoException(
|
|
message: "URL can't be null when playing a remote video",
|
|
);
|
|
}
|
|
case BytesSource bytesSource:
|
|
// For bytes, we need to write to a temporary file
|
|
// media_kit doesn't support direct byte playback
|
|
final file = File.fromRawPath(bytesSource.bytes);
|
|
return Media(file.path);
|
|
}
|
|
}
|
|
}
|