lum_splash_video/lib/widgets/splash_video_player_w.dart

313 lines
9.4 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() {
switch (videoConfig.videoVisibilityEnum) {
case VisibilityEnum.useFullScreen:
return SizedBox.fromSize(
size: MediaQuery.sizeOf(context),
child: Video(
controller: controller,
controls: NoVideoControls,
),
);
case VisibilityEnum.useAspectRatio:
return AspectRatio(
aspectRatio: player.state.width != null && player.state.height != null
? player.state.width! / player.state.height!
: 16 / 9,
child: Video(
controller: controller,
controls: NoVideoControls,
),
);
case VisibilityEnum.none:
return Video(
controller: controller,
controls: NoVideoControls,
);
}
}
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);
}
}
}