lum_splash_video/lib/splash_video_controller.dart

169 lines
5.3 KiB
Dart

/*
* 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<SplashVideoController> _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<void> disposeAll() async {
final controllers = Set<SplashVideoController>.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<void> play() async {
if (_isDisposed) return;
await player.play();
}
/// Pauses video playback
Future<void> 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<void> 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;
}