/* * Copyright (c) 2026 Malloc LLC (malloc.io) * * 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 'package:media_kit/media_kit.dart'; /// Controller for managing video splash screen playback /// /// Provides control over video playback and access to the underlying /// media_kit Player instance. class SplashVideoController { /// Creates a SplashVideoController /// /// [loopVideo] - When true, video will loop infinitely until manually stopped SplashVideoController({ this.loopVideo = false, }) { _activeControllers.add(this); } /// Whether the video should loop infinitely /// /// When true: /// - Video will restart automatically when it ends /// - User must call [skip] or [pause] to stop playback /// - Cannot use onVideoComplete or nextScreen callbacks final bool loopVideo; Player? _player; bool _isDisposed = false; // Track all active controllers for cleanup during hot restart static final Set _activeControllers = {}; /// Disposes all active SplashVideoController instances /// /// Call this before hot restart to prevent "Callback invoked after it has /// been deleted" crashes. This stops all MPV callback activity before the /// Dart isolate is destroyed. /// /// Usage in main.dart: /// ```dart /// // In a StatefulWidget wrapping your app: /// @override /// void reassemble() { /// SplashVideoController.disposeAll(); /// super.reassemble(); /// } /// ``` static Future disposeAll() async { final controllers = Set.from(_activeControllers); for (final controller in controllers) { controller.dispose(); } _activeControllers.clear(); } /// Returns the number of active (non-disposed) controllers /// /// Useful for debugging resource leaks static int get activeControllerCount => _activeControllers.length; /// Access to the underlying media_kit Player instance /// /// Provides full control over video playback including: /// - Position seeking /// - Volume control /// - Playback rate /// - Stream listeners for state changes /// /// Throws [StateError] if accessed before initialization Player get player { if (_player == null) { throw StateError( 'Player not initialized. Controller must be attached to SplashVideo widget.', ); } return _player!; } /// Internal method to attach the Player instance void attach(Player player) { if (_isDisposed) { throw StateError('Cannot attach player to disposed controller'); } _player = player; } /// Starts or resumes video playback Future play() async { if (_isDisposed) return; await player.play(); } /// Pauses video playback Future pause() async { if (_isDisposed) return; await player.pause(); } /// Completes the video splash immediately /// /// This will: /// - Stop video playback /// - Trigger navigation or completion callbacks /// - Clean up resources Future skip() async { if (_isDisposed) return; await player.pause(); // The SplashVideo widget will handle navigation _skipRequested = true; } bool _skipRequested = false; /// Whether skip was requested by the user bool get skipRequested => _skipRequested; /// Disposes the controller and releases resources /// /// This method pauses the player first to stop MPV callback activity, /// then disposes after a short delay. This helps prevent crashes during /// hot restart when the Dart isolate is destroyed while MPV's native /// thread is still running. /// /// Should be called when the splash screen is no longer needed. void dispose() { if (_isDisposed) return; _isDisposed = true; _activeControllers.remove(this); final player = _player; if (player != null) { // Pause first to stop callback activity before disposing player.pause().then((_) { // Small delay to let MPV thread settle before disposal Future.delayed(const Duration(milliseconds: 50), () { player.dispose(); }); }).catchError((_) { // If pause fails, dispose anyway player.dispose(); }); } _player = null; } /// Whether the controller has been disposed bool get isDisposed => _isDisposed; }