/* * 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 createState() => _SplashVideoPlayerState(); } class _SplashVideoPlayerState extends State { 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 _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); } } }