169 lines
5.3 KiB
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;
|
|
}
|