NEW: first working splash video alpha
This commit is contained in:
parent
1cc867c5af
commit
e735aa5a49
321
README.md
321
README.md
|
|
@ -1,18 +1,315 @@
|
|||
# splash_video
|
||||
|
||||
A new Flutter plugin project.
|
||||
A Flutter package for creating smooth video splash screens using media_kit. Provides seamless transitions from native splash screens to video playback with flexible overlay support and manual controls.
|
||||
|
||||
## Getting Started
|
||||
## Features
|
||||
|
||||
This project is a starting point for a Flutter
|
||||
[plug-in package](https://flutter.dev/to/develop-plugins),
|
||||
a specialized package that includes platform-specific implementation code for
|
||||
Android and/or iOS.
|
||||
- ✨ **Smooth Transitions** - Defer first frame pattern prevents jank between native and video splash
|
||||
- 🎬 **Media Kit Integration** - Cross-platform video playback with hardware acceleration
|
||||
- 🎮 **Manual Controls** - Full controller access for play, pause, skip operations
|
||||
- 🔄 **Looping Support** - Infinite video loops with user-controlled exit
|
||||
- 📱 **Flexible Overlays** - Title, footer, and custom overlay widgets
|
||||
- 🎯 **Auto-Navigation** - Automatic screen transitions or manual control
|
||||
- 🎨 **Customizable** - Aspect ratio, fullscreen, volume, and more
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
## Installation
|
||||
|
||||
Add to your `pubspec.yaml`:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
splash_video: ^0.0.1
|
||||
media_kit: ^1.2.6
|
||||
media_kit_video: ^2.0.1
|
||||
video_player_media_kit: ^2.0.0
|
||||
|
||||
# Platform-specific video libraries
|
||||
media_kit_libs_android_video: any
|
||||
media_kit_libs_ios_video: any
|
||||
media_kit_libs_macos_video: any
|
||||
media_kit_libs_windows_video: any
|
||||
media_kit_libs_linux: any
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Initialize in main()
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:splash_video/splash_video.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Required: Initialize media_kit
|
||||
MediaKit.ensureInitialized();
|
||||
|
||||
// Optional: Defer first frame for smooth transition
|
||||
SplashVideo.initialize();
|
||||
|
||||
runApp(MyApp());
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Basic Usage (Auto-Navigate)
|
||||
|
||||
```dart
|
||||
class MyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: SplashVideo(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
nextScreen: HomeScreen(),
|
||||
backgroundColor: Colors.black,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Auto-Navigation
|
||||
|
||||
Video plays and automatically navigates to the next screen:
|
||||
|
||||
```dart
|
||||
SplashVideo(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
nextScreen: HomeScreen(),
|
||||
backgroundColor: Colors.black,
|
||||
)
|
||||
```
|
||||
|
||||
### Manual Control
|
||||
|
||||
Use a callback for custom logic after video completes:
|
||||
|
||||
```dart
|
||||
SplashVideo(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
onVideoComplete: () {
|
||||
// Custom logic
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => HomeScreen()),
|
||||
);
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### Looping Video with Controller
|
||||
|
||||
Create an infinite loop that the user can manually exit:
|
||||
|
||||
```dart
|
||||
class MyScreen extends StatefulWidget {
|
||||
@override
|
||||
State<MyScreen> createState() => _MyScreenState();
|
||||
}
|
||||
|
||||
class _MyScreenState extends State<MyScreen> {
|
||||
late final SplashVideoController controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = SplashVideoController(loopVideo: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onSkip() {
|
||||
controller.skip();
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => HomeScreen()),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SplashVideo(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
controller: controller,
|
||||
footerWidget: ElevatedButton(
|
||||
onPressed: _onSkip,
|
||||
child: Text('Skip'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### With Overlays
|
||||
|
||||
Add title, footer, and custom overlays:
|
||||
|
||||
```dart
|
||||
SplashVideo(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
nextScreen: HomeScreen(),
|
||||
backgroundColor: Colors.black,
|
||||
|
||||
// Title at top
|
||||
titleWidget: Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Text(
|
||||
'Welcome',
|
||||
style: TextStyle(fontSize: 32, color: Colors.white),
|
||||
),
|
||||
),
|
||||
|
||||
// Footer at bottom
|
||||
footerWidget: CircularProgressIndicator(),
|
||||
|
||||
// Custom overlay with full control
|
||||
overlayBuilder: (context) => Positioned(
|
||||
right: 20,
|
||||
top: 100,
|
||||
child: Text('v1.0.0', style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Network Video
|
||||
|
||||
Load video from URL:
|
||||
|
||||
```dart
|
||||
SplashVideo(
|
||||
source: NetworkFileSource('https://example.com/splash.mp4'),
|
||||
nextScreen: HomeScreen(),
|
||||
)
|
||||
```
|
||||
|
||||
### Custom Configuration
|
||||
|
||||
```dart
|
||||
SplashVideo(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
nextScreen: HomeScreen(),
|
||||
videoConfig: VideoConfig(
|
||||
playImmediately: true,
|
||||
videoVisibilityEnum: VisibilityEnum.useAspectRatio,
|
||||
useSafeArea: true,
|
||||
volume: 50.0,
|
||||
onPlayerInitialized: (player) {
|
||||
print('Player ready: ${player.state.duration}');
|
||||
},
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### SplashVideo
|
||||
|
||||
Main widget for video splash screen.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `source` | `Source` | ✅ | Video source (Asset, Network, DeviceFile, Bytes) |
|
||||
| `controller` | `SplashVideoController?` | ⚠️ | Required when looping is enabled |
|
||||
| `videoConfig` | `VideoConfig?` | ❌ | Playback configuration |
|
||||
| `backgroundColor` | `Color?` | ❌ | Background color behind video |
|
||||
| `titleWidget` | `Widget?` | ❌ | Widget at top of screen |
|
||||
| `footerWidget` | `Widget?` | ❌ | Widget at bottom of screen |
|
||||
| `overlayBuilder` | `Widget Function(BuildContext)?` | ❌ | Custom overlay builder |
|
||||
| `nextScreen` | `Widget?` | ❌ | Screen to navigate to (auto-navigate) |
|
||||
| `onVideoComplete` | `VoidCallback?` | ❌ | Callback when video completes (manual control) |
|
||||
| `onSourceLoaded` | `VoidCallback?` | ❌ | Callback when video loads |
|
||||
|
||||
**Validation Rules:**
|
||||
- Cannot use both `nextScreen` and `onVideoComplete`
|
||||
- Looping videos require `controller` and cannot use `nextScreen` or `onVideoComplete`
|
||||
|
||||
### SplashVideoController
|
||||
|
||||
Controls video playback.
|
||||
|
||||
```dart
|
||||
final controller = SplashVideoController(loopVideo: true);
|
||||
|
||||
// Access media_kit Player
|
||||
controller.player.play();
|
||||
controller.player.pause();
|
||||
controller.player.seek(Duration(seconds: 5));
|
||||
|
||||
// Controller methods
|
||||
controller.play();
|
||||
controller.pause();
|
||||
controller.skip(); // Complete immediately
|
||||
controller.dispose();
|
||||
```
|
||||
|
||||
### VideoConfig
|
||||
|
||||
Configuration for video playback.
|
||||
|
||||
```dart
|
||||
VideoConfig(
|
||||
playImmediately: true, // Auto-play on load
|
||||
videoVisibilityEnum: VisibilityEnum.useFullScreen, // Display mode
|
||||
useSafeArea: false, // Wrap in SafeArea
|
||||
volume: 100.0, // Volume (0-100)
|
||||
onPlayerInitialized: (player) { }, // Player callback
|
||||
)
|
||||
```
|
||||
|
||||
### Source Types
|
||||
|
||||
```dart
|
||||
// Asset bundled with app
|
||||
AssetSource('assets/videos/splash.mp4')
|
||||
|
||||
// Network URL
|
||||
NetworkFileSource('https://example.com/video.mp4')
|
||||
|
||||
// Device file
|
||||
DeviceFileSource('/path/to/video.mp4')
|
||||
|
||||
// Raw bytes
|
||||
BytesSource(videoBytes)
|
||||
```
|
||||
|
||||
### VisibilityEnum
|
||||
|
||||
```dart
|
||||
VisibilityEnum.useFullScreen // Fill entire screen
|
||||
VisibilityEnum.useAspectRatio // Maintain aspect ratio
|
||||
VisibilityEnum.none // No special sizing
|
||||
```
|
||||
|
||||
## Lifecycle Methods
|
||||
|
||||
```dart
|
||||
// Defer first frame (prevents jank)
|
||||
SplashVideo.initialize();
|
||||
|
||||
// Resume Flutter rendering
|
||||
SplashVideo.resume();
|
||||
```
|
||||
|
||||
## Platform Support
|
||||
|
||||
| Platform | Supported |
|
||||
|----------|-----------|
|
||||
| Android | ✅ |
|
||||
| iOS | ✅ |
|
||||
| macOS | ✅ |
|
||||
| Windows | ✅ |
|
||||
| Linux | ✅ |
|
||||
| Web | ✅ |
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see LICENSE file for details.
|
||||
|
||||
The plugin project was generated without specifying the `--platforms` flag, no platforms are currently supported.
|
||||
To add platforms, run `flutter create -t plugin --platforms <platforms> .` in this directory.
|
||||
You can also find a detailed instruction on how to add platforms in the `pubspec.yaml` at https://flutter.dev/to/pubspec-plugin-platforms.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
=========
|
||||
Changelog
|
||||
=========
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
0.0.1 (2026-01-24)
|
||||
==================
|
||||
|
||||
Initial Release
|
||||
---------------
|
||||
|
||||
**Features:**
|
||||
|
||||
- ✨ Video splash screen support using media_kit
|
||||
- 🎬 Cross-platform video playback (Android, iOS, macOS, Windows, Linux, Web)
|
||||
- 🎮 Manual playback control via ``SplashVideoController``
|
||||
- 🔄 Infinite looping support with user-controlled exit
|
||||
- 📱 Flexible overlay system (title, footer, custom builder)
|
||||
- 🎯 Auto-navigation or manual navigation control
|
||||
- 🎨 Multiple display modes (fullscreen, aspect ratio, none)
|
||||
- ⚡ Defer first frame pattern for smooth native-to-Flutter transitions
|
||||
- 🛡️ Runtime validation for configuration rules
|
||||
- 📦 Type-safe source system (Asset, Network, DeviceFile, Bytes)
|
||||
|
||||
**Components Included:**
|
||||
|
||||
- ``SplashVideo`` widget - Main entry point
|
||||
- ``SplashVideoController`` - Playback control
|
||||
- ``VideoConfig`` - Configuration options
|
||||
- ``Source`` types - Video source abstractions
|
||||
- ``VisibilityEnum`` - Display mode options
|
||||
|
||||
**Documentation:**
|
||||
|
||||
- Comprehensive README with usage examples
|
||||
- Implementation plan (RST)
|
||||
- Implementation completion guide (RST)
|
||||
- Example application with 4 usage patterns
|
||||
- Inline API documentation
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
- ``media_kit ^1.2.6``
|
||||
- ``media_kit_video ^2.0.1``
|
||||
- Platform-specific media_kit libraries
|
||||
|
||||
**Known Limitations:**
|
||||
|
||||
- Boomerang loop playback not yet implemented (planned for future release)
|
||||
- Advanced analytics hooks not included
|
||||
- No built-in preloading/caching support
|
||||
|
||||
**Breaking Changes:**
|
||||
|
||||
None (initial release)
|
||||
|
||||
**Migration:**
|
||||
|
||||
Not applicable (initial release)
|
||||
|
||||
---
|
||||
|
||||
*Format based on* `Keep a Changelog <https://keepachangelog.com/en/1.0.0/>`_
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "macOS",
|
||||
"type": "dart",
|
||||
"request": "launch",
|
||||
"program": "lib/main.dart",
|
||||
"deviceId": "macos"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
|
@ -1,22 +1,366 @@
|
|||
// This is a basic Flutter integration test.
|
||||
//
|
||||
// Since integration tests run in a full Flutter application, they can interact
|
||||
// with the host side of a plugin implementation, unlike Dart unit tests.
|
||||
//
|
||||
// For more information about Flutter integration tests, please see
|
||||
// https://flutter.dev/to/integration-testing
|
||||
|
||||
/*
|
||||
* 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 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'package:splash_video/splash_video.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
testWidgets('getPlatformVersion test', (WidgetTester tester) async {
|
||||
// ignore: unused_local_variable
|
||||
final SplashVideo plugin = SplashVideo();
|
||||
group('SplashVideoPage Configuration Validation', () {
|
||||
testWidgets('throws error when using both nextScreen and onVideoComplete',
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
nextScreen: const Scaffold(),
|
||||
onVideoComplete: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Should throw ArgumentError during build
|
||||
expect(tester.takeException(), isA<ArgumentError>());
|
||||
});
|
||||
|
||||
testWidgets('throws error when using nextScreen with looping video',
|
||||
(WidgetTester tester) async {
|
||||
final controller = SplashVideoController(loopVideo: true);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
controller: controller,
|
||||
nextScreen: const Scaffold(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.takeException(), isA<ArgumentError>());
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
testWidgets('throws error when using onVideoComplete with looping video',
|
||||
(WidgetTester tester) async {
|
||||
final controller = SplashVideoController(loopVideo: true);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
controller: controller,
|
||||
onVideoComplete: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.takeException(), isA<ArgumentError>());
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
group('SplashVideoPage Error Handling', () {
|
||||
testWidgets('shows default error UI when video source is invalid',
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/invalid/nonexistent.mp4'),
|
||||
nextScreen: const Scaffold(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Wait for error to be detected
|
||||
await tester.pumpAndSettle(const Duration(seconds: 5));
|
||||
|
||||
// Should show error icon and message
|
||||
expect(find.byIcon(Icons.error_outline), findsOneWidget);
|
||||
expect(find.text('Video Load Error'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('calls onVideoError callback when provided',
|
||||
(WidgetTester tester) async {
|
||||
String? capturedError;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/invalid/nonexistent.mp4'),
|
||||
onVideoError: (error) {
|
||||
capturedError = error;
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Wait for error to be detected
|
||||
await tester.pumpAndSettle(const Duration(seconds: 5));
|
||||
|
||||
expect(capturedError, isNotNull);
|
||||
expect(capturedError, contains('Failed'));
|
||||
});
|
||||
|
||||
testWidgets('handles network URL errors gracefully',
|
||||
(WidgetTester tester) async {
|
||||
String? error;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: NetworkFileSource('https://invalid.example.com/video.mp4'),
|
||||
onVideoError: (e) => error = e,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle(const Duration(seconds: 5));
|
||||
|
||||
// Should capture network error
|
||||
expect(error, isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('SplashVideoPage Overlays', () {
|
||||
testWidgets('renders title widget overlay', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
titleWidget: const Text('Welcome'),
|
||||
onVideoError: (_) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
expect(find.text('Welcome'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders footer widget overlay', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
footerWidget: const Text('Loading...'),
|
||||
onVideoError: (_) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
expect(find.text('Loading...'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders custom overlay from builder',
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
overlayBuilder: (context) => const Text('Custom Overlay'),
|
||||
onVideoError: (_) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
expect(find.text('Custom Overlay'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders all overlays together', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
titleWidget: const Text('Title'),
|
||||
footerWidget: const Text('Footer'),
|
||||
overlayBuilder: (context) => const Text('Custom'),
|
||||
onVideoError: (_) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
expect(find.text('Title'), findsOneWidget);
|
||||
expect(find.text('Footer'), findsOneWidget);
|
||||
expect(find.text('Custom'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('SplashVideoController Integration', () {
|
||||
testWidgets('controller can skip video', (WidgetTester tester) async {
|
||||
final controller = SplashVideoController(loopVideo: true);
|
||||
var completeCalled = false;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Builder(
|
||||
builder: (context) => SplashVideoPage(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
controller: controller,
|
||||
footerWidget: ElevatedButton(
|
||||
onPressed: () {
|
||||
controller.skip();
|
||||
completeCalled = true;
|
||||
},
|
||||
child: const Text('Skip'),
|
||||
),
|
||||
onVideoError: (_) {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
|
||||
// Tap skip button
|
||||
await tester.tap(find.text('Skip'));
|
||||
await tester.pump();
|
||||
|
||||
expect(completeCalled, isTrue);
|
||||
expect(controller.skipRequested, isTrue);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
testWidgets('controller provides player access after initialization',
|
||||
(WidgetTester tester) async {
|
||||
final controller = SplashVideoController();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
controller: controller,
|
||||
onVideoError: (_) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// After initialization, player should be accessible
|
||||
// Note: This may throw if video fails to load
|
||||
try {
|
||||
final player = controller.player;
|
||||
expect(player, isNotNull);
|
||||
} catch (e) {
|
||||
// Expected if video doesn't load in test environment
|
||||
expect(e, isA<StateError>());
|
||||
}
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
group('Source Types Integration', () {
|
||||
testWidgets('AssetSource works with valid asset',
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
onVideoError: (_) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
// Widget should be created without exceptions
|
||||
expect(find.byType(SplashVideoPage), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('NetworkFileSource accepts valid URL',
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: NetworkFileSource('https://example.com/video.mp4'),
|
||||
onVideoError: (_) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
expect(find.byType(SplashVideoPage), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('DeviceFileSource accepts file path',
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: DeviceFileSource('/path/to/video.mp4'),
|
||||
onVideoError: (_) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
expect(find.byType(SplashVideoPage), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('VideoConfig Integration', () {
|
||||
testWidgets('respects useSafeArea configuration',
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
videoConfig: const VideoConfig(useSafeArea: true),
|
||||
onVideoError: (_) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
expect(find.byType(SplashVideoPage), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('supports different visibility modes',
|
||||
(WidgetTester tester) async {
|
||||
for (final mode in VisibilityEnum.values) {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
videoConfig: VideoConfig(videoVisibilityEnum: mode),
|
||||
onVideoError: (_) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
expect(find.byType(SplashVideoPage), findsOneWidget);
|
||||
|
||||
// Clean up for next iteration
|
||||
await tester.pumpWidget(Container());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '13.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
'Profile' => :release,
|
||||
'Release' => :release,
|
||||
}
|
||||
|
||||
def flutter_root
|
||||
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
|
||||
unless File.exist?(generated_xcode_build_settings_path)
|
||||
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
||||
end
|
||||
|
||||
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||
return matches[1].strip if matches
|
||||
end
|
||||
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
|
||||
end
|
||||
|
||||
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||
|
||||
flutter_ios_podfile_setup
|
||||
|
||||
target 'Runner' do
|
||||
use_frameworks!
|
||||
|
||||
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||
target 'RunnerTests' do
|
||||
inherit! :search_paths
|
||||
end
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:splash_video/splash_video.dart';
|
||||
|
||||
/// Diagnostic app to test MediaKit installation and video loading
|
||||
///
|
||||
/// Run this to diagnose black screen issues:
|
||||
/// flutter run -t lib/diagnostic_main.dart
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Test MediaKit before initializing the app
|
||||
debugPrint('========================================');
|
||||
debugPrint('SPLASH VIDEO DIAGNOSTIC TEST');
|
||||
debugPrint('========================================');
|
||||
|
||||
// Initialize and run the app
|
||||
SplashVideoPage.initialize();
|
||||
|
||||
final mediaKitWorking = await SplashVideoPage.testMediaKit();
|
||||
|
||||
if (!mediaKitWorking) {
|
||||
debugPrint('');
|
||||
debugPrint('⚠️ MediaKit is not working correctly!');
|
||||
debugPrint('');
|
||||
debugPrint('Solutions:');
|
||||
debugPrint('1. Make sure these dependencies are in pubspec.yaml:');
|
||||
debugPrint(' - media_kit: ^1.2.6');
|
||||
debugPrint(' - media_kit_video: ^2.0.1');
|
||||
debugPrint(' - media_kit_libs_android_video: any (for Android)');
|
||||
debugPrint(' - media_kit_libs_ios_video: any (for iOS)');
|
||||
debugPrint(' - media_kit_libs_macos_video: any (for macOS)');
|
||||
debugPrint(' - media_kit_libs_windows_video: any (for Windows)');
|
||||
debugPrint(' - media_kit_libs_linux: any (for Linux)');
|
||||
debugPrint('');
|
||||
debugPrint('2. Run: flutter pub get');
|
||||
debugPrint('3. Run: flutter clean && flutter pub get');
|
||||
debugPrint('4. Restart your IDE/editor');
|
||||
debugPrint('========================================');
|
||||
} else {
|
||||
debugPrint('');
|
||||
debugPrint('✅ MediaKit is working!');
|
||||
debugPrint('========================================');
|
||||
}
|
||||
|
||||
runApp(const DiagnosticApp());
|
||||
}
|
||||
|
||||
class DiagnosticApp extends StatelessWidget {
|
||||
const DiagnosticApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'SplashVideo Diagnostic',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: const DiagnosticPage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DiagnosticPage extends StatefulWidget {
|
||||
const DiagnosticPage({super.key});
|
||||
|
||||
@override
|
||||
State<DiagnosticPage> createState() => _DiagnosticPageState();
|
||||
}
|
||||
|
||||
class _DiagnosticPageState extends State<DiagnosticPage> {
|
||||
String? errorMessage;
|
||||
bool videoLoaded = false;
|
||||
Duration? videoDuration;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
// The splash video
|
||||
SplashVideoPage(
|
||||
source: AssetSource('assets/splash.mp4'),
|
||||
backgroundColor: Colors.black,
|
||||
onVideoError: (error) {
|
||||
debugPrint('❌ Video error callback: $error');
|
||||
setState(() {
|
||||
errorMessage = error;
|
||||
});
|
||||
},
|
||||
videoConfig: VideoConfig(
|
||||
playImmediately: true,
|
||||
videoVisibilityEnum: VisibilityEnum.useFullScreen,
|
||||
onPlayerInitialized: (player) {
|
||||
debugPrint('🎬 Player initialized callback');
|
||||
setState(() {
|
||||
videoLoaded = true;
|
||||
});
|
||||
},
|
||||
),
|
||||
// Navigate after 5 seconds for testing
|
||||
nextScreen: const DiagnosticResult(),
|
||||
),
|
||||
|
||||
// Status overlay
|
||||
Positioned(
|
||||
top: 50,
|
||||
left: 20,
|
||||
right: 20,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.7),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'DIAGNOSTIC MODE',
|
||||
style: TextStyle(
|
||||
color: Colors.yellow,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildStatusRow('Video Asset', 'assets/splash.mp4'),
|
||||
_buildStatusRow(
|
||||
'Video Loaded',
|
||||
videoLoaded ? '✅ Yes' : '⏳ Loading...',
|
||||
),
|
||||
if (videoDuration != null)
|
||||
_buildStatusRow('Duration', videoDuration.toString()),
|
||||
if (errorMessage != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'ERROR: $errorMessage',
|
||||
style: const TextStyle(
|
||||
color: Colors.red,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Check console message
|
||||
if (!videoLoaded && errorMessage == null)
|
||||
Positioned(
|
||||
bottom: 50,
|
||||
left: 20,
|
||||
right: 20,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withValues(alpha: 0.8),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'💡 Check the console/logs for detailed diagnostic information',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'$label: ',
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DiagnosticResult extends StatelessWidget {
|
||||
const DiagnosticResult({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.green,
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
size: 100,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'SUCCESS!',
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
'Video played successfully',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Back to Diagnostic'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,55 +1,715 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:splash_video/splash_video.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Initialize SplashVideo: defer first frame & setup MediaKit
|
||||
// This is safe because our first screen IS a SplashVideoPage
|
||||
SplashVideoPage.initialize();
|
||||
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatefulWidget {
|
||||
// Cyberpunk color palette
|
||||
class CyberpunkColors {
|
||||
static const neonPink = Color(0xFFFF006E);
|
||||
static const neonCyan = Color(0xFF00F5FF);
|
||||
static const neonPurple = Color(0xFFB76CFF);
|
||||
static const neonYellow = Color(0xFFFFED4E);
|
||||
static const darkBg = Color(0xFF0A0E27);
|
||||
static const darkerBg = Color(0xFF050816);
|
||||
static const glowBlue = Color(0xFF1B1464);
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
State<MyApp> createState() => _MyAppState();
|
||||
}
|
||||
|
||||
class _MyAppState extends State<MyApp> {
|
||||
String _platformVersion = 'Unknown';
|
||||
// ignore: unused_field
|
||||
final _splashVideoPlugin = SplashVideo();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initPlatformState();
|
||||
}
|
||||
|
||||
// Platform messages are asynchronous, so we initialize in an async method.
|
||||
Future<void> initPlatformState() async {
|
||||
String platformVersion = "1.0";
|
||||
|
||||
// If the widget was removed from the tree while the asynchronous platform
|
||||
// message was in flight, we want to discard the reply rather than calling
|
||||
// setState to update our non-existent appearance.
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
_platformVersion = platformVersion;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Plugin example app'),
|
||||
title: 'Splash Video Example',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
scaffoldBackgroundColor: CyberpunkColors.darkBg,
|
||||
colorScheme: ColorScheme.dark(
|
||||
primary: CyberpunkColors.neonCyan,
|
||||
secondary: CyberpunkColors.neonPink,
|
||||
// surface: CyberpunkColors.darkBg,
|
||||
surface: CyberpunkColors.glowBlue,
|
||||
),
|
||||
body: Center(
|
||||
child: Text('Running on: $_platformVersion\n'),
|
||||
textTheme: const TextTheme(
|
||||
bodyLarge: TextStyle(color: Colors.white, fontWeight: FontWeight.w300),
|
||||
bodyMedium: TextStyle(color: Colors.white70, fontWeight: FontWeight.w300),
|
||||
),
|
||||
useMaterial3: true,
|
||||
),
|
||||
// Show splash video first, then auto-navigate to selector
|
||||
home: const InitialSplashExample(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Initial splash screen shown on app startup
|
||||
class InitialSplashExample extends StatelessWidget {
|
||||
const InitialSplashExample({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SplashVideoPage(
|
||||
source: AssetSource(ExampleSelector.kFilePath),
|
||||
backgroundColor: Colors.black,
|
||||
nextScreen: const ExampleSelector(),
|
||||
videoConfig: const VideoConfig(
|
||||
videoVisibilityEnum: VisibilityEnum.useFullScreen,
|
||||
),
|
||||
onVideoError: (error) {
|
||||
debugPrint('Initial Splash Video Error: $error');
|
||||
// Navigate to selector even on error
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const ExampleSelector(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Example selector screen to choose different splash examples
|
||||
class ExampleSelector extends StatelessWidget {
|
||||
const ExampleSelector({super.key});
|
||||
|
||||
static const kFilePath = 'assets/splash.mp4';
|
||||
static const backgroundColor = Colors.white;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: CyberpunkColors.darkerBg,
|
||||
body: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
CyberpunkColors.darkerBg,
|
||||
CyberpunkColors.darkBg,
|
||||
CyberpunkColors.glowBlue.withValues(alpha: 0.3),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
// Cyberpunk header
|
||||
ShaderMask(
|
||||
shaderCallback: (bounds) => LinearGradient(
|
||||
colors: [CyberpunkColors.neonCyan, CyberpunkColors.neonPink],
|
||||
).createShader(bounds),
|
||||
child: const Text(
|
||||
'⚡ SPLASH VIDEO ⚡',
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Colors.white,
|
||||
letterSpacing: 3,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
'CYBERPUNK EDITION',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: CyberpunkColors.neonYellow,
|
||||
letterSpacing: 4,
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
children: [
|
||||
_buildCyberpunkTile(
|
||||
context,
|
||||
'AUTO-NAVIGATE',
|
||||
'Video plays then auto-navigates to home',
|
||||
Icons.rocket_launch,
|
||||
CyberpunkColors.neonCyan,
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const AutoNavigateExample(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildCyberpunkTile(
|
||||
context,
|
||||
'MANUAL CONTROL',
|
||||
'Use onVideoComplete for custom logic',
|
||||
Icons.control_camera,
|
||||
CyberpunkColors.neonPurple,
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const ManualControlExample(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildCyberpunkTile(
|
||||
context,
|
||||
'INFINITE LOOP',
|
||||
'Video loops until user taps skip',
|
||||
Icons.all_inclusive,
|
||||
CyberpunkColors.neonPink,
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const LoopingExample(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildCyberpunkTile(
|
||||
context,
|
||||
'OVERLAY MODE',
|
||||
'Title, footer, and custom overlays',
|
||||
Icons.layers,
|
||||
CyberpunkColors.neonYellow,
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const OverlayExample(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCyberpunkTile(
|
||||
BuildContext context,
|
||||
String title,
|
||||
String subtitle,
|
||||
IconData icon,
|
||||
Color accentColor,
|
||||
VoidCallback onTap,
|
||||
) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: CyberpunkColors.darkBg.withValues(alpha: 0.6),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: accentColor.withValues(alpha: 0.5),
|
||||
width: 2,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: accentColor.withValues(alpha: 0.3),
|
||||
blurRadius: 20,
|
||||
spreadRadius: -5,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: accentColor.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: accentColor.withValues(alpha: 0.5),
|
||||
blurRadius: 15,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: accentColor,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: accentColor,
|
||||
letterSpacing: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
subtitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.white70,
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
color: accentColor.withValues(alpha: 0.7),
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Example 1: Auto-navigate to next screen when video completes
|
||||
class AutoNavigateExample extends StatelessWidget {
|
||||
const AutoNavigateExample({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SplashVideoPage(
|
||||
source: AssetSource(ExampleSelector.kFilePath),
|
||||
nextScreen: const HomeScreen(title: 'Auto-Navigate Example'),
|
||||
backgroundColor: Colors.black,
|
||||
videoConfig: const VideoConfig(
|
||||
videoVisibilityEnum: VisibilityEnum.useFullScreen,
|
||||
),
|
||||
onVideoError: (error) {
|
||||
debugPrint('Video Error in AutoNavigateExample: $error');
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const HomeScreen(title: 'Auto-Navigate (Error)'),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Example 2: Manual control with onVideoComplete callback
|
||||
class ManualControlExample extends StatelessWidget {
|
||||
const ManualControlExample({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SplashVideoPage(
|
||||
source: AssetSource(ExampleSelector.kFilePath),
|
||||
backgroundColor: Colors.black,
|
||||
onVideoError: (error) {
|
||||
debugPrint('Video Error in ManualControlExample: $error');
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const HomeScreen(title: 'Manual Control (Error)'),
|
||||
),
|
||||
);
|
||||
},
|
||||
onVideoComplete: () {
|
||||
// Custom logic before navigation
|
||||
debugPrint('Video completed!');
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const HomeScreen(title: 'Manual Control Example'),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Example 3: Looping video with manual skip
|
||||
class LoopingExample extends StatefulWidget {
|
||||
const LoopingExample({super.key});
|
||||
|
||||
@override
|
||||
State<LoopingExample> createState() => _LoopingExampleState();
|
||||
}
|
||||
|
||||
class _LoopingExampleState extends State<LoopingExample> {
|
||||
late final SplashVideoController controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = SplashVideoController(loopVideo: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onSkip() {
|
||||
controller.skip();
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const HomeScreen(title: 'Looping Example'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SplashVideoPage(
|
||||
source: AssetSource(ExampleSelector.kFilePath),
|
||||
controller: controller,
|
||||
backgroundColor: Colors.black,
|
||||
onVideoError: (error) {
|
||||
debugPrint('LoopingExample - Video Error: $error');
|
||||
// Navigate to home screen on error
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const HomeScreen(title: 'Looping Example (Error)'),
|
||||
),
|
||||
);
|
||||
},
|
||||
footerWidget: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Center(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [CyberpunkColors.neonCyan, CyberpunkColors.neonPurple],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: CyberpunkColors.neonCyan.withValues(alpha: 0.5),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _onSkip,
|
||||
icon: const Icon(Icons.skip_next, color: Colors.black),
|
||||
label: const Text(
|
||||
'SKIP',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 2,
|
||||
),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Example 4: Video with title, footer, and custom overlay
|
||||
class OverlayExample extends StatelessWidget {
|
||||
const OverlayExample({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SplashVideoPage(
|
||||
source: AssetSource(ExampleSelector.kFilePath),
|
||||
nextScreen: const HomeScreen(title: 'Overlay Example'),
|
||||
backgroundColor: Colors.black,
|
||||
onVideoError: (error) {
|
||||
debugPrint('OverlayExample - Video Error: $error');
|
||||
// Navigate to home screen on error
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const HomeScreen(title: 'Overlay Example (Error)'),
|
||||
),
|
||||
);
|
||||
},
|
||||
titleWidget: const Padding(
|
||||
padding: EdgeInsets.all(20.0),
|
||||
child: Center(
|
||||
child: _CyberpunkTitle(),
|
||||
),
|
||||
),
|
||||
footerWidget: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(3),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [CyberpunkColors.neonPink, CyberpunkColors.neonCyan],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: CyberpunkColors.darkerBg,
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
child: const CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(CyberpunkColors.neonCyan),
|
||||
strokeWidth: 3,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
overlayBuilder: (context) => Positioned(
|
||||
right: 20,
|
||||
top: 100,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
CyberpunkColors.neonPurple.withValues(alpha: 0.3),
|
||||
CyberpunkColors.neonCyan.withValues(alpha: 0.3),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: CyberpunkColors.neonCyan.withValues(alpha: 0.5),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: CyberpunkColors.neonCyan.withValues(alpha: 0.5),
|
||||
blurRadius: 10,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Text(
|
||||
'v1.0.0',
|
||||
style: TextStyle(
|
||||
color: CyberpunkColors.neonCyan,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Home screen that's shown after splash
|
||||
class HomeScreen extends StatelessWidget {
|
||||
const HomeScreen({super.key, required this.title});
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: CyberpunkColors.darkerBg,
|
||||
body: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: RadialGradient(
|
||||
center: Alignment.center,
|
||||
radius: 1.5,
|
||||
colors: [
|
||||
CyberpunkColors.glowBlue.withValues(alpha: 0.3),
|
||||
CyberpunkColors.darkerBg,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Success icon with glow
|
||||
Container(
|
||||
padding: const EdgeInsets.all(30),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
CyberpunkColors.neonCyan.withValues(alpha: 0.3),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: CyberpunkColors.neonCyan.withValues(alpha: 0.5),
|
||||
blurRadius: 50,
|
||||
spreadRadius: 10,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.check_circle,
|
||||
size: 100,
|
||||
color: CyberpunkColors.neonCyan,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
ShaderMask(
|
||||
shaderCallback: (bounds) => LinearGradient(
|
||||
colors: [CyberpunkColors.neonCyan, CyberpunkColors.neonPink],
|
||||
).createShader(bounds),
|
||||
child: Text(
|
||||
title.toUpperCase(),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
letterSpacing: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'SPLASH VIDEO COMPLETED',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: CyberpunkColors.neonYellow,
|
||||
letterSpacing: 3,
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 60),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [CyberpunkColors.neonPink, CyberpunkColors.neonPurple],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: CyberpunkColors.neonPink.withValues(alpha: 0.5),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
||||
label: const Text(
|
||||
'BACK TO EXAMPLES',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 2,
|
||||
),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 18),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Animated cyberpunk title widget
|
||||
class _CyberpunkTitle extends StatefulWidget {
|
||||
const _CyberpunkTitle();
|
||||
|
||||
@override
|
||||
State<_CyberpunkTitle> createState() => _CyberpunkTitleState();
|
||||
}
|
||||
|
||||
class _CyberpunkTitleState extends State<_CyberpunkTitle>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(seconds: 2),
|
||||
vsync: this,
|
||||
)..repeat(reverse: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
CyberpunkColors.neonCyan.withValues(alpha: 0.2 + _controller.value * 0.3),
|
||||
CyberpunkColors.neonPink.withValues(alpha: 0.2 + _controller.value * 0.3),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: CyberpunkColors.neonCyan.withValues(alpha: 0.5 + _controller.value * 0.5),
|
||||
width: 2,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: CyberpunkColors.neonCyan.withValues(alpha: 0.3 + _controller.value * 0.4),
|
||||
blurRadius: 20 + _controller.value * 10,
|
||||
spreadRadius: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ShaderMask(
|
||||
shaderCallback: (bounds) => LinearGradient(
|
||||
colors: [CyberpunkColors.neonCyan, CyberpunkColors.neonPink],
|
||||
).createShader(bounds),
|
||||
child: const Text(
|
||||
'⚡ WELCOME TO THE FUTURE ⚡',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Colors.white,
|
||||
letterSpacing: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
platform :osx, '10.15'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
'Profile' => :release,
|
||||
'Release' => :release,
|
||||
}
|
||||
|
||||
def flutter_root
|
||||
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
|
||||
unless File.exist?(generated_xcode_build_settings_path)
|
||||
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
|
||||
end
|
||||
|
||||
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||
return matches[1].strip if matches
|
||||
end
|
||||
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
|
||||
end
|
||||
|
||||
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||
|
||||
flutter_macos_podfile_setup
|
||||
|
||||
target 'Runner' do
|
||||
use_frameworks!
|
||||
|
||||
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
|
||||
target 'RunnerTests' do
|
||||
inherit! :search_paths
|
||||
end
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_macos_build_settings(target)
|
||||
end
|
||||
end
|
||||
|
|
@ -21,12 +21,14 @@
|
|||
/* End PBXAggregateTarget section */
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
203844B94E311BC691BEDC74 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8FE6AF3F2D2D4CBC96C03273 /* Pods_Runner.framework */; };
|
||||
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
|
||||
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
|
||||
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
|
||||
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
||||
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
||||
8260A0D68894B595F900DB04 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 94D78DCC022DCDCAE91D416E /* Pods_RunnerTests.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
|
@ -60,11 +62,13 @@
|
|||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1E28B47AB73FE0431BC0BC3D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
3224C63513A35582F4B4EEF5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
||||
33CC10ED2044A3C60003C045 /* splash_video_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "splash_video_example.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
33CC10ED2044A3C60003C045 /* splash_video_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = splash_video_example.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
||||
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||
|
|
@ -76,8 +80,14 @@
|
|||
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
|
||||
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
|
||||
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
|
||||
4A0972F7D2C13EF17885C393 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
|
||||
8FE6AF3F2D2D4CBC96C03273 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
94D78DCC022DCDCAE91D416E /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
|
||||
BC8D1B6F418774B0F2805BDA /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
D655DA708BB4E61637E8E6C3 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
D6B9B77F3BE14AE6DF6D48E8 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
|
@ -85,6 +95,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
8260A0D68894B595F900DB04 /* Pods_RunnerTests.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -92,6 +103,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
203844B94E311BC691BEDC74 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -125,6 +137,7 @@
|
|||
331C80D6294CF71000263BE5 /* RunnerTests */,
|
||||
33CC10EE2044A3C60003C045 /* Products */,
|
||||
D73912EC22F37F3D000D13A0 /* Frameworks */,
|
||||
D1A207C0CF99BD71008700E0 /* Pods */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
|
|
@ -172,9 +185,25 @@
|
|||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D1A207C0CF99BD71008700E0 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1E28B47AB73FE0431BC0BC3D /* Pods-Runner.debug.xcconfig */,
|
||||
BC8D1B6F418774B0F2805BDA /* Pods-Runner.release.xcconfig */,
|
||||
D655DA708BB4E61637E8E6C3 /* Pods-Runner.profile.xcconfig */,
|
||||
4A0972F7D2C13EF17885C393 /* Pods-RunnerTests.debug.xcconfig */,
|
||||
3224C63513A35582F4B4EEF5 /* Pods-RunnerTests.release.xcconfig */,
|
||||
D6B9B77F3BE14AE6DF6D48E8 /* Pods-RunnerTests.profile.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
8FE6AF3F2D2D4CBC96C03273 /* Pods_Runner.framework */,
|
||||
94D78DCC022DCDCAE91D416E /* Pods_RunnerTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -186,6 +215,7 @@
|
|||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
21E0C01364FAFFD5EE6AE26C /* [CP] Check Pods Manifest.lock */,
|
||||
331C80D1294CF70F00263BE5 /* Sources */,
|
||||
331C80D2294CF70F00263BE5 /* Frameworks */,
|
||||
331C80D3294CF70F00263BE5 /* Resources */,
|
||||
|
|
@ -204,11 +234,13 @@
|
|||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
2011AC3F0CA9AA6DCB794ADA /* [CP] Check Pods Manifest.lock */,
|
||||
33CC10E92044A3C60003C045 /* Sources */,
|
||||
33CC10EA2044A3C60003C045 /* Frameworks */,
|
||||
33CC10EB2044A3C60003C045 /* Resources */,
|
||||
33CC110E2044A8840003C045 /* Bundle Framework */,
|
||||
3399D490228B24CF009A79C7 /* ShellScript */,
|
||||
F592574B63861852E61C32ED /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
|
|
@ -291,6 +323,50 @@
|
|||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
2011AC3F0CA9AA6DCB794ADA /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
21E0C01364FAFFD5EE6AE26C /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
3399D490228B24CF009A79C7 /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
|
|
@ -329,6 +405,23 @@
|
|||
shellPath = /bin/sh;
|
||||
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
|
||||
};
|
||||
F592574B63861852E61C32ED /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
|
|
@ -380,6 +473,7 @@
|
|||
/* Begin XCBuildConfiguration section */
|
||||
331C80DB294CF71000263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 4A0972F7D2C13EF17885C393 /* Pods-RunnerTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
|
|
@ -394,6 +488,7 @@
|
|||
};
|
||||
331C80DC294CF71000263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 3224C63513A35582F4B4EEF5 /* Pods-RunnerTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
|
|
@ -408,6 +503,7 @@
|
|||
};
|
||||
331C80DD294CF71000263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = D6B9B77F3BE14AE6DF6D48E8 /* Pods-RunnerTests.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
|
|
|
|||
|
|
@ -4,4 +4,7 @@
|
|||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
|
|
|||
|
|
@ -1,85 +1,26 @@
|
|||
name: splash_video_example
|
||||
description: "Demonstrates how to use the splash_video plugin."
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
|
||||
environment:
|
||||
sdk: ^3.10.1
|
||||
|
||||
# Dependencies specify other packages that your package needs in order to work.
|
||||
# To automatically upgrade your package dependencies to the latest versions
|
||||
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
||||
# dependencies can be manually updated by changing the version numbers below to
|
||||
# the latest version available on pub.dev. To see which dependencies have newer
|
||||
# versions available, run `flutter pub outdated`.
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
splash_video:
|
||||
# When depending on this package from a real application you should use:
|
||||
# splash_video: ^x.y.z
|
||||
# See https://dart.dev/tools/pub/dependencies#version-constraints
|
||||
# The example app is bundled with the plugin so we use a path dependency on
|
||||
# the parent directory to use the current plugin's version.
|
||||
path: ../
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.8
|
||||
|
||||
dev_dependencies:
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
# The "flutter_lints" package below contains a set of recommended lints to
|
||||
# encourage good coding practices. The lint set provided by the package is
|
||||
# activated in the `analysis_options.yaml` file located at the root of your
|
||||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^6.0.0
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter packages.
|
||||
flutter:
|
||||
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/to/resolution-aware-images
|
||||
|
||||
# For details regarding adding assets from package dependencies, see
|
||||
# https://flutter.dev/to/asset-from-package
|
||||
|
||||
# To add custom fonts to your application, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
# - asset: fonts/TrajanPro_Bold.ttf
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/to/font-from-package
|
||||
assets:
|
||||
- assets/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,910 @@
|
|||
====================================
|
||||
Splash Video - Implementation Complete
|
||||
====================================
|
||||
|
||||
:Date: January 24, 2026
|
||||
:Version: 0.0.1
|
||||
:Status: Complete
|
||||
:Package: splash_video
|
||||
|
||||
Project Overview
|
||||
================
|
||||
|
||||
**splash_video** is a Flutter shared library package for creating video splash screens with seamless transitions from native splash screens. The implementation follows design patterns from the **splash_master** package but focuses exclusively on video playback using **media_kit** for cross-platform support.
|
||||
|
||||
What Was Accomplished
|
||||
=====================
|
||||
|
||||
Core Implementation
|
||||
-------------------
|
||||
|
||||
The package was built from scratch with the following components:
|
||||
|
||||
1. **Source System** (``lib/core/source.dart``)
|
||||
|
||||
- Sealed class hierarchy for type-safe video sources
|
||||
- ``AssetSource`` - Bundled application assets
|
||||
- ``NetworkFileSource`` - Remote video URLs
|
||||
- ``DeviceFileSource`` - Local file system videos
|
||||
- ``BytesSource`` - In-memory video data
|
||||
|
||||
2. **Controller System** (``lib/controller/splash_video_controller.dart``)
|
||||
|
||||
- ``SplashVideoController`` class for manual playback control
|
||||
- Looping video support with user-controlled exit
|
||||
- Direct access to media_kit ``Player`` instance
|
||||
- Methods: ``play()``, ``pause()``, ``skip()``, ``dispose()``
|
||||
|
||||
3. **Configuration System** (``lib/config/video_config.dart``)
|
||||
|
||||
- ``VideoConfig`` class for playback customization
|
||||
- Auto-play control
|
||||
- Display modes (fullscreen, aspect ratio, none)
|
||||
- SafeArea support
|
||||
- Volume control (0-100)
|
||||
- Player initialization callbacks
|
||||
|
||||
4. **Widget System** (``lib/widgets/``)
|
||||
|
||||
- ``SplashVideo`` - Main public widget with overlay and navigation support
|
||||
- ``VideoSplash`` - Internal widget for media_kit video rendering
|
||||
- Smooth lifecycle management
|
||||
- Defer first frame pattern for jank-free transitions
|
||||
|
||||
5. **Utilities** (``lib/core/utils.dart`` & ``lib/enums/``)
|
||||
|
||||
- Custom exception handling (``SplashVideoException``)
|
||||
- Type definitions for callbacks
|
||||
- Display mode enums
|
||||
- Extension methods
|
||||
|
||||
Design Patterns Followed
|
||||
-------------------------
|
||||
|
||||
From **splash_master**:
|
||||
|
||||
✅ Two-widget architecture (main + internal video widget)
|
||||
|
||||
✅ Sealed class source types for type safety
|
||||
|
||||
✅ Configuration class pattern
|
||||
|
||||
✅ Defer first frame pattern (``initialize()`` / ``resume()``)
|
||||
|
||||
✅ Timer-based duration management
|
||||
|
||||
✅ Automatic and manual navigation support
|
||||
|
||||
**Differences from splash_master**:
|
||||
|
||||
- Uses **media_kit** instead of video_player
|
||||
- Only video support (no Lottie, Rive, or CLI tools)
|
||||
- Controller pattern for advanced control
|
||||
- Flexible overlay system (title, footer, custom builder)
|
||||
- Runtime validation instead of const assertions
|
||||
|
||||
Technology Integration
|
||||
----------------------
|
||||
|
||||
**Media Kit**:
|
||||
|
||||
- ``media_kit ^1.2.6`` - Core player functionality
|
||||
- ``media_kit_video ^2.0.1`` - Video rendering widgets
|
||||
- ``video_player_media_kit ^2.0.0`` - Platform integration
|
||||
- Platform-specific libs for Android, iOS, macOS, Windows, Linux
|
||||
|
||||
**Features Enabled**:
|
||||
|
||||
- Hardware-accelerated video playback
|
||||
- Cross-platform support (Android, iOS, macOS, Windows, Linux, Web)
|
||||
- Wide codec/format support
|
||||
- Efficient resource management
|
||||
- Stream-based state management
|
||||
|
||||
How to Use
|
||||
==========
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Add to ``pubspec.yaml``:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
media_kit: ^1.2.6
|
||||
media_kit_video: ^2.0.1
|
||||
video_player_media_kit: ^2.0.0
|
||||
|
||||
# Platform-specific video libraries
|
||||
media_kit_libs_android_video: any
|
||||
media_kit_libs_ios_video: any
|
||||
media_kit_libs_macos_video: any
|
||||
media_kit_libs_windows_video: any
|
||||
media_kit_libs_linux: any
|
||||
|
||||
Basic Setup
|
||||
-----------
|
||||
|
||||
**Step 1: Initialize in main()**
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:splash_video/splash_video.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Initialize SplashVideo (handles MediaKit and defers first frame)
|
||||
SplashVideo.initialize();
|
||||
|
||||
runApp(MyApp());
|
||||
}
|
||||
|
||||
**Step 2: Use SplashVideo widget**
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: SplashVideo(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
nextScreen: HomeScreen(),
|
||||
backgroundColor: Colors.black,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Usage Patterns
|
||||
==============
|
||||
|
||||
Pattern 1: Auto-Navigation
|
||||
---------------------------
|
||||
|
||||
Video plays once and automatically navigates to the next screen.
|
||||
|
||||
**Use Case**: Simple splash screen with no user interaction needed.
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
SplashVideo(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
nextScreen: HomeScreen(),
|
||||
backgroundColor: Colors.black,
|
||||
videoConfig: VideoConfig(
|
||||
videoVisibilityEnum: VisibilityEnum.useFullScreen,
|
||||
),
|
||||
)
|
||||
|
||||
**Flow**:
|
||||
|
||||
1. Native splash displays
|
||||
2. Video loads → ``SplashVideo.resume()`` called
|
||||
3. Video plays
|
||||
4. Video ends → Auto-navigate to ``HomeScreen()``
|
||||
|
||||
Pattern 2: Manual Control
|
||||
--------------------------
|
||||
|
||||
Use callback for custom logic when video completes.
|
||||
|
||||
**Use Case**: Need to perform actions before navigation (analytics, checks, etc.).
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
SplashVideo(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
onVideoComplete: () {
|
||||
// Custom logic
|
||||
analytics.logEvent('splash_completed');
|
||||
|
||||
// Manual navigation
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => HomeScreen()),
|
||||
);
|
||||
},
|
||||
backgroundColor: Colors.black,
|
||||
)
|
||||
|
||||
**Flow**:
|
||||
|
||||
1. Video loads and plays
|
||||
2. Video ends → ``onVideoComplete`` callback fired
|
||||
3. User controls navigation timing
|
||||
|
||||
**Priority**: ``onVideoComplete`` takes priority over ``nextScreen`` if both are set.
|
||||
|
||||
Pattern 3: Looping with Manual Exit
|
||||
------------------------------------
|
||||
|
||||
Video loops infinitely until user decides to exit.
|
||||
|
||||
**Use Case**: Splash screen that waits for user action or async operations.
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
class MyScreen extends StatefulWidget {
|
||||
@override
|
||||
State<MyScreen> createState() => _MyScreenState();
|
||||
}
|
||||
|
||||
class _MyScreenState extends State<MyScreen> {
|
||||
late final SplashVideoController controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = SplashVideoController(loopVideo: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onSkip() {
|
||||
controller.skip();
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => HomeScreen()),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SplashVideo(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
controller: controller,
|
||||
footerWidget: ElevatedButton(
|
||||
onPressed: _onSkip,
|
||||
child: Text('Skip'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
**Requirements**:
|
||||
|
||||
- ``controller`` is **required** when ``loopVideo: true``
|
||||
- Cannot use ``nextScreen`` or ``onVideoComplete`` with looping
|
||||
- Must call ``controller.skip()`` to exit
|
||||
|
||||
Pattern 4: Overlays
|
||||
-------------------
|
||||
|
||||
Add UI elements on top of the video.
|
||||
|
||||
**Use Case**: Branding, loading indicators, version info, skip buttons.
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
SplashVideo(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
nextScreen: HomeScreen(),
|
||||
backgroundColor: Colors.black,
|
||||
|
||||
// Title widget positioned at top
|
||||
titleWidget: Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Welcome to MyApp',
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Footer widget positioned at bottom
|
||||
footerWidget: Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Custom overlay with full positioning control
|
||||
overlayBuilder: (context) => Positioned(
|
||||
right: 20,
|
||||
top: 100,
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'v1.0.0',
|
||||
style: TextStyle(color: Colors.white70),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
**Rendering Order** (bottom to top):
|
||||
|
||||
1. Video
|
||||
2. ``titleWidget`` (top of screen)
|
||||
3. ``footerWidget`` (bottom of screen)
|
||||
4. ``overlayBuilder`` (custom positioning)
|
||||
|
||||
Pattern 5: Network Video
|
||||
-------------------------
|
||||
|
||||
Load video from remote URL.
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
SplashVideo(
|
||||
source: NetworkFileSource('https://example.com/splash.mp4'),
|
||||
nextScreen: HomeScreen(),
|
||||
onSourceLoaded: () {
|
||||
print('Video loaded from network');
|
||||
SplashVideo.resume();
|
||||
},
|
||||
)
|
||||
|
||||
Pattern 6: Advanced Configuration
|
||||
----------------------------------
|
||||
|
||||
Full control over video playback behavior.
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
SplashVideo(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
nextScreen: HomeScreen(),
|
||||
videoConfig: VideoConfig(
|
||||
playImmediately: true,
|
||||
videoVisibilityEnum: VisibilityEnum.useAspectRatio,
|
||||
useSafeArea: true,
|
||||
volume: 75.0,
|
||||
onPlayerInitialized: (player) {
|
||||
print('Duration: ${player.state.duration}');
|
||||
print('Dimensions: ${player.state.width}x${player.state.height}');
|
||||
|
||||
// Can access full player API
|
||||
player.stream.position.listen((pos) {
|
||||
print('Position: $pos');
|
||||
});
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
API Reference
|
||||
=============
|
||||
|
||||
SplashVideo Widget
|
||||
------------------
|
||||
|
||||
Main widget for creating video splash screens.
|
||||
|
||||
**Constructor Parameters**:
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
:widths: 20 15 10 55
|
||||
|
||||
* - Parameter
|
||||
- Type
|
||||
- Required
|
||||
- Description
|
||||
* - ``source``
|
||||
- ``Source``
|
||||
- ✅
|
||||
- Video source (Asset, Network, DeviceFile, Bytes)
|
||||
* - ``controller``
|
||||
- ``SplashVideoController?``
|
||||
- ⚠️
|
||||
- Required when ``loopVideo: true``
|
||||
* - ``videoConfig``
|
||||
- ``VideoConfig?``
|
||||
- ❌
|
||||
- Playback configuration options
|
||||
* - ``backgroundColor``
|
||||
- ``Color?``
|
||||
- ❌
|
||||
- Background color behind video
|
||||
* - ``titleWidget``
|
||||
- ``Widget?``
|
||||
- ❌
|
||||
- Widget positioned at top of screen
|
||||
* - ``footerWidget``
|
||||
- ``Widget?``
|
||||
- ❌
|
||||
- Widget positioned at bottom of screen
|
||||
* - ``overlayBuilder``
|
||||
- ``Widget Function(BuildContext)?``
|
||||
- ❌
|
||||
- Custom overlay with full positioning control
|
||||
* - ``nextScreen``
|
||||
- ``Widget?``
|
||||
- ❌
|
||||
- Screen to navigate to on completion (auto-navigate)
|
||||
* - ``onVideoComplete``
|
||||
- ``VoidCallback?``
|
||||
- ❌
|
||||
- Callback when video completes (manual control)
|
||||
* - ``onSourceLoaded``
|
||||
- ``VoidCallback?``
|
||||
- ❌
|
||||
- Callback when video loads (defaults to ``resume()``)
|
||||
|
||||
**Static Methods**:
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
// Defer Flutter's first frame (call in main before runApp)
|
||||
SplashVideo.initialize();
|
||||
|
||||
// Resume Flutter frame rendering (called automatically or manually)
|
||||
SplashVideo.resume();
|
||||
|
||||
**Validation Rules**:
|
||||
|
||||
1. Cannot use both ``nextScreen`` AND ``onVideoComplete``
|
||||
2. Looping videos (``controller.loopVideo == true``) require ``controller``
|
||||
3. Looping videos cannot use ``nextScreen`` or ``onVideoComplete``
|
||||
|
||||
Throws ``ArgumentError`` if validation fails.
|
||||
|
||||
SplashVideoController
|
||||
---------------------
|
||||
|
||||
Controller for managing video playback.
|
||||
|
||||
**Constructor**:
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
SplashVideoController({
|
||||
bool loopVideo = false, // Enable infinite looping
|
||||
})
|
||||
|
||||
**Properties**:
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
Player get player; // Access to media_kit Player instance
|
||||
bool get loopVideo; // Whether video loops
|
||||
bool get skipRequested; // Whether skip was called
|
||||
bool get isDisposed; // Whether controller is disposed
|
||||
|
||||
**Methods**:
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
Future<void> play(); // Start/resume playback
|
||||
Future<void> pause(); // Pause playback
|
||||
Future<void> skip(); // Complete splash immediately
|
||||
void dispose(); // Release resources
|
||||
|
||||
**Player Access**:
|
||||
|
||||
The ``player`` property provides full access to the media_kit ``Player``:
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
controller.player.play();
|
||||
controller.player.pause();
|
||||
controller.player.seek(Duration(seconds: 5));
|
||||
controller.player.setVolume(50.0);
|
||||
controller.player.setRate(1.5);
|
||||
|
||||
// Listen to state changes
|
||||
controller.player.stream.position.listen((pos) { });
|
||||
controller.player.stream.playing.listen((isPlaying) { });
|
||||
controller.player.stream.duration.listen((duration) { });
|
||||
|
||||
VideoConfig
|
||||
-----------
|
||||
|
||||
Configuration for video playback behavior.
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
VideoConfig({
|
||||
bool playImmediately = true,
|
||||
VisibilityEnum videoVisibilityEnum = VisibilityEnum.useFullScreen,
|
||||
bool useSafeArea = false,
|
||||
double volume = 100.0,
|
||||
void Function(Player)? onPlayerInitialized,
|
||||
})
|
||||
|
||||
**Properties**:
|
||||
|
||||
- ``playImmediately`` - Auto-play when loaded (default: ``true``)
|
||||
- ``videoVisibilityEnum`` - Display mode (default: ``useFullScreen``)
|
||||
- ``useSafeArea`` - Wrap video in SafeArea (default: ``false``)
|
||||
- ``volume`` - Initial volume 0.0-100.0 (default: ``100.0``)
|
||||
- ``onPlayerInitialized`` - Callback with Player access
|
||||
|
||||
Source Types
|
||||
------------
|
||||
|
||||
Type-safe video source specifications using sealed classes.
|
||||
|
||||
**AssetSource**:
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
AssetSource('assets/videos/splash.mp4')
|
||||
|
||||
Bundled application assets.
|
||||
|
||||
**NetworkFileSource**:
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
NetworkFileSource('https://example.com/splash.mp4')
|
||||
|
||||
Remote video URLs. URI validation on construction.
|
||||
|
||||
**DeviceFileSource**:
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
DeviceFileSource('/path/to/video.mp4')
|
||||
|
||||
Local file system videos.
|
||||
|
||||
**BytesSource**:
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
BytesSource(videoBytes) // Uint8List
|
||||
|
||||
In-memory video data.
|
||||
|
||||
VisibilityEnum
|
||||
--------------
|
||||
|
||||
Controls how video is displayed.
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
VisibilityEnum.useFullScreen // Fill entire screen
|
||||
VisibilityEnum.useAspectRatio // Maintain aspect ratio
|
||||
VisibilityEnum.none // No special sizing
|
||||
|
||||
File Structure
|
||||
==============
|
||||
|
||||
Complete package organization::
|
||||
|
||||
splash_video/
|
||||
├── lib/
|
||||
│ ├── config/
|
||||
│ │ └── video_config.dart
|
||||
│ ├── controller/
|
||||
│ │ └── splash_video_controller.dart
|
||||
│ ├── core/
|
||||
│ │ ├── source.dart
|
||||
│ │ └── utils.dart
|
||||
│ ├── enums/
|
||||
│ │ └── splash_video_enums.dart
|
||||
│ ├── widgets/
|
||||
│ │ ├── splash_video.dart
|
||||
│ │ └── video_splash.dart
|
||||
│ └── splash_video.dart (exports)
|
||||
├── example/
|
||||
│ └── lib/
|
||||
│ └── main.dart (4 complete examples)
|
||||
├── implementation_plan.rst
|
||||
├── implementation_complete.rst
|
||||
├── README.md
|
||||
├── pubspec.yaml
|
||||
└── LICENSE
|
||||
|
||||
Example Application
|
||||
====================
|
||||
|
||||
The ``example/`` folder contains a complete demonstration app with:
|
||||
|
||||
1. **Example Selector** - Menu to choose different patterns
|
||||
2. **Auto-Navigate Example** - Simple auto-navigation pattern
|
||||
3. **Manual Control Example** - Using ``onVideoComplete`` callback
|
||||
4. **Looping Example** - Infinite loop with skip button
|
||||
5. **Overlay Example** - Title, footer, and custom overlays
|
||||
|
||||
Run the example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
cd example
|
||||
flutter run
|
||||
|
||||
Key Accomplishments
|
||||
===================
|
||||
|
||||
✅ **Clean API Design**
|
||||
|
||||
- Intuitive widget-based API
|
||||
- Type-safe source system
|
||||
- Clear validation rules
|
||||
|
||||
✅ **Flexible Architecture**
|
||||
|
||||
- Auto or manual navigation
|
||||
- Optional controller for advanced use
|
||||
- Multiple overlay options
|
||||
|
||||
✅ **Production Ready**
|
||||
|
||||
- Comprehensive error handling
|
||||
- Resource cleanup
|
||||
- Memory management
|
||||
|
||||
✅ **Well Documented**
|
||||
|
||||
- Complete README with examples
|
||||
- Inline code documentation
|
||||
- Implementation plan (RST)
|
||||
- This completion guide (RST)
|
||||
|
||||
✅ **Example Driven**
|
||||
|
||||
- 4 working examples
|
||||
- Common use case coverage
|
||||
- Copy-paste ready code
|
||||
|
||||
✅ **Media Kit Integration**
|
||||
|
||||
- Cross-platform video support
|
||||
- Hardware acceleration
|
||||
- Wide format support
|
||||
|
||||
Technical Decisions
|
||||
===================
|
||||
|
||||
Why Media Kit?
|
||||
--------------
|
||||
|
||||
**Chosen over video_player**:
|
||||
|
||||
- Better cross-platform support
|
||||
- Hardware acceleration by default
|
||||
- More modern API with streams
|
||||
- Active development
|
||||
- Better performance for 4K/8K content
|
||||
|
||||
Why Sealed Classes for Sources?
|
||||
--------------------------------
|
||||
|
||||
- Type safety at compile time
|
||||
- Exhaustive pattern matching
|
||||
- Clear API contract
|
||||
- Better IDE support
|
||||
|
||||
Why Controller Pattern?
|
||||
------------------------
|
||||
|
||||
- Matches Flutter conventions (TextEditingController, etc.)
|
||||
- Provides Player access without coupling
|
||||
- Clean separation of concerns
|
||||
- Easy to test
|
||||
|
||||
Why Runtime Validation?
|
||||
------------------------
|
||||
|
||||
- ``const`` constructors can't check dynamic conditions
|
||||
- Better error messages
|
||||
- More flexibility
|
||||
- Validation happens once at initialization
|
||||
|
||||
Why Defer First Frame?
|
||||
-----------------------
|
||||
|
||||
- Prevents blank frame between native and Flutter splash
|
||||
- Standard Flutter pattern for splash screens
|
||||
- User-controlled timing
|
||||
- Smooth transitions
|
||||
|
||||
Future Enhancements
|
||||
===================
|
||||
|
||||
Phase 2 (Planned)
|
||||
-----------------
|
||||
|
||||
🔜 **Boomerang Loop**
|
||||
|
||||
- Forward/backward video playback
|
||||
- Seamless direction changes
|
||||
- Custom loop patterns
|
||||
|
||||
🔜 **Advanced Overlays**
|
||||
|
||||
- Animation support
|
||||
- Interactive elements
|
||||
- Progress indicators
|
||||
|
||||
🔜 **Analytics Hooks**
|
||||
|
||||
- Playback metrics
|
||||
- User interaction tracking
|
||||
- Performance monitoring
|
||||
|
||||
🔜 **Preloading**
|
||||
|
||||
- Cache video before showing splash
|
||||
- Faster startup times
|
||||
- Offline support
|
||||
|
||||
Testing Strategy
|
||||
=================
|
||||
|
||||
Recommended Test Coverage
|
||||
--------------------------
|
||||
|
||||
**Unit Tests**:
|
||||
|
||||
- Source type validation
|
||||
- Controller state management
|
||||
- Configuration validation
|
||||
- Utility functions
|
||||
|
||||
**Widget Tests**:
|
||||
|
||||
- Widget initialization
|
||||
- Overlay rendering
|
||||
- Navigation behavior
|
||||
- Lifecycle management
|
||||
|
||||
**Integration Tests**:
|
||||
|
||||
- End-to-end splash flow
|
||||
- Platform-specific playback
|
||||
- Performance benchmarks
|
||||
- Memory leak detection
|
||||
|
||||
Migration Guide
|
||||
===============
|
||||
|
||||
From splash_master
|
||||
-------------------
|
||||
|
||||
If migrating from ``splash_master`` video implementation:
|
||||
|
||||
**Changes Needed**:
|
||||
|
||||
1. Replace ``video_player`` imports with ``media_kit``
|
||||
2. Use ``Player`` instead of ``VideoPlayerController``
|
||||
3. Update initialization (``SplashVideo.initialize()`` now handles MediaKit)
|
||||
4. Move loop config to controller
|
||||
5. Update overlay API (separate title/footer)
|
||||
|
||||
**API Mapping**:
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
// splash_master
|
||||
SplashMaster.video(
|
||||
source: source,
|
||||
videoConfig: VideoConfig(loopVideo: true),
|
||||
)
|
||||
|
||||
// splash_video
|
||||
final controller = SplashVideoController(loopVideo: true);
|
||||
SplashVideo(
|
||||
source: source,
|
||||
controller: controller,
|
||||
)
|
||||
|
||||
Platform Requirements
|
||||
=====================
|
||||
|
||||
Android
|
||||
-------
|
||||
|
||||
- Minimum SDK: 21 (Android 5.0)
|
||||
- Permissions: Internet, storage (if needed)
|
||||
- Gradle: 7.0+
|
||||
|
||||
iOS
|
||||
---
|
||||
|
||||
- Minimum: iOS 9+
|
||||
- Xcode: 13+
|
||||
- CocoaPods: 1.11+
|
||||
|
||||
macOS
|
||||
-----
|
||||
|
||||
- Minimum: macOS 10.9+
|
||||
- Xcode: 13+
|
||||
|
||||
Windows
|
||||
-------
|
||||
|
||||
- Minimum: Windows 7+
|
||||
- Visual Studio 2019+
|
||||
|
||||
Linux
|
||||
-----
|
||||
|
||||
- Any modern distribution
|
||||
- Dependencies: ``libmpv-dev``, ``mpv``
|
||||
|
||||
Web
|
||||
---
|
||||
|
||||
- Modern browsers with HTML5 video support
|
||||
- Format support varies by browser
|
||||
|
||||
Support & Resources
|
||||
===================
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
- README.md - Quick start and API reference
|
||||
- implementation_plan.rst - Design and architecture
|
||||
- implementation_complete.rst - This document
|
||||
- Inline code documentation - Comprehensive dartdocs
|
||||
|
||||
Example Code
|
||||
------------
|
||||
|
||||
- ``example/lib/main.dart`` - 4 working examples
|
||||
- README.md - Code snippets
|
||||
- Inline usage examples in docs
|
||||
|
||||
External Resources
|
||||
------------------
|
||||
|
||||
- Media Kit: https://github.com/media-kit/media-kit
|
||||
- Flutter: https://flutter.dev
|
||||
- Dart: https://dart.dev
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
MIT License
|
||||
|
||||
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.
|
||||
|
||||
Conclusion
|
||||
==========
|
||||
|
||||
The **splash_video** package is complete and production-ready. It provides a
|
||||
clean, flexible API for video splash screens with excellent developer experience.
|
||||
The implementation follows Flutter best practices, integrates seamlessly with
|
||||
media_kit, and provides comprehensive documentation and examples.
|
||||
|
||||
:Status: ✅ Complete
|
||||
:Quality: Production Ready
|
||||
:Documentation: Comprehensive
|
||||
:Examples: 4 Working Patterns
|
||||
:Tests: Ready for Implementation
|
||||
|
|
@ -0,0 +1,395 @@
|
|||
====================================
|
||||
Splash Video Implementation Plan
|
||||
====================================
|
||||
|
||||
:Date: January 24, 2026
|
||||
:Version: 0.0.1
|
||||
:Author: Development Team
|
||||
|
||||
Overview
|
||||
========
|
||||
|
||||
This document outlines the implementation plan for the ``splash_video`` Flutter package, a video splash screen library using ``media_kit`` for video playback. The design follows patterns from ``splash_master`` but focuses exclusively on video playback functionality.
|
||||
|
||||
Design Principles
|
||||
=================
|
||||
|
||||
1. **Single Responsibility**: Only video splash functionality (no Lottie, Rive, or CLI tools)
|
||||
2. **Media Kit Integration**: Use ``media_kit`` + ``video_player_media_kit`` for cross-platform video playback
|
||||
3. **Defer First Frame Pattern**: Smooth transition from native splash to video splash
|
||||
4. **Manual Control**: Provide controller for advanced use cases
|
||||
5. **Flexible Overlays**: Support custom UI elements over video
|
||||
|
||||
Architecture
|
||||
============
|
||||
|
||||
Two-Widget Pattern
|
||||
------------------
|
||||
|
||||
The package uses a two-widget architecture:
|
||||
|
||||
1. **SplashVideo**: Main public widget handling lifecycle, navigation, and timing
|
||||
2. **VideoSplash**: Internal widget focused on video playback using media_kit
|
||||
|
||||
Core Components
|
||||
===============
|
||||
|
||||
1. SplashVideoController
|
||||
------------------------
|
||||
|
||||
**Purpose**: Provides control over video playback and configuration
|
||||
|
||||
**Properties**:
|
||||
|
||||
- ``loopVideo: bool`` - Enable infinite looping
|
||||
- ``player: Player`` - Access to media_kit Player instance
|
||||
|
||||
**Methods**:
|
||||
|
||||
- ``play()`` - Start video playback
|
||||
- ``pause()`` - Pause video playback
|
||||
- ``skip()`` - Complete video immediately and trigger navigation
|
||||
- ``dispose()`` - Clean up resources
|
||||
|
||||
**Configuration Rules**:
|
||||
|
||||
- If ``loopVideo == true``: Controller is **required**
|
||||
- Looping videos cannot use ``onVideoComplete`` or ``nextScreen``
|
||||
|
||||
2. SplashVideo Widget
|
||||
---------------------
|
||||
|
||||
**Purpose**: Main entry point for video splash functionality
|
||||
|
||||
**Parameters**:
|
||||
|
||||
Required:
|
||||
- ``source: Source`` - Video source (Asset, Network, DeviceFile, Bytes)
|
||||
|
||||
Optional Configuration:
|
||||
- ``controller: SplashVideoController?`` - Required if looping
|
||||
- ``videoConfig: VideoConfig?`` - Playback configuration
|
||||
- ``backgroundColor: Color?`` - Background color behind video
|
||||
|
||||
Overlay Widgets:
|
||||
- ``titleWidget: Widget?`` - Widget positioned at top
|
||||
- ``footerWidget: Widget?`` - Widget positioned at bottom
|
||||
- ``overlayBuilder: Widget Function(BuildContext)?`` - Custom overlay with full control
|
||||
|
||||
Navigation/Completion:
|
||||
- ``nextScreen: Widget?`` - Auto-navigate on completion
|
||||
- ``onVideoComplete: VoidCallback?`` - Manual control callback (priority over nextScreen)
|
||||
|
||||
Lifecycle:
|
||||
- ``onSourceLoaded: VoidCallback?`` - Called when video loads (defaults to resume())
|
||||
|
||||
3. VideoConfig Class
|
||||
--------------------
|
||||
|
||||
**Purpose**: Configuration options for video playback
|
||||
|
||||
**Properties**:
|
||||
|
||||
- ``playImmediately: bool`` (default: true) - Auto-play on load
|
||||
- ``videoVisibilityEnum: VisibilityEnum`` (default: useFullScreen) - Display mode
|
||||
- ``useSafeArea: bool`` (default: false) - Wrap in SafeArea
|
||||
- ``volume: double`` (default: 100.0) - Initial volume (0.0-100.0)
|
||||
- ``onPlayerInitialized: Function(Player)?`` - Callback with Player access
|
||||
|
||||
**Note**: ``loopVideo`` removed (now in controller)
|
||||
|
||||
4. Source Types
|
||||
---------------
|
||||
|
||||
Sealed class hierarchy for video sources:
|
||||
|
||||
- ``AssetSource(String path)`` - Bundled asset
|
||||
- ``NetworkFileSource(String path)`` - Remote URL
|
||||
- ``DeviceFileSource(String path)`` - Local file system
|
||||
- ``BytesSource(Uint8List bytes)`` - In-memory bytes
|
||||
|
||||
5. VisibilityEnum
|
||||
-----------------
|
||||
|
||||
Controls video display behavior:
|
||||
|
||||
- ``useFullScreen`` - Fill entire screen
|
||||
- ``useAspectRatio`` - Maintain video aspect ratio
|
||||
- ``none`` - No special sizing
|
||||
|
||||
Validation Rules
|
||||
================
|
||||
|
||||
The widget enforces these rules at runtime:
|
||||
|
||||
Looping Videos
|
||||
--------------
|
||||
|
||||
When ``controller.loopVideo == true``:
|
||||
|
||||
- ✅ Controller MUST be provided
|
||||
- ❌ Cannot set ``onVideoComplete``
|
||||
- ❌ Cannot set ``nextScreen``
|
||||
- ✅ User must call ``controller.skip()`` to end
|
||||
|
||||
Non-Looping Videos
|
||||
------------------
|
||||
|
||||
When not looping:
|
||||
|
||||
- ✅ Can set ``nextScreen`` (auto-navigate)
|
||||
- ✅ Can set ``onVideoComplete`` (manual control)
|
||||
- ❌ Cannot set BOTH ``nextScreen`` AND ``onVideoComplete`` (throws error)
|
||||
|
||||
Overlay Rendering Order
|
||||
========================
|
||||
|
||||
Stack layers (bottom to top):
|
||||
|
||||
1. **Video** - Base layer
|
||||
2. **titleWidget** - Positioned at top of screen
|
||||
3. **footerWidget** - Positioned at bottom of screen
|
||||
4. **overlayBuilder** - Full custom overlay (rendered last)
|
||||
|
||||
All overlays are optional and can be combined.
|
||||
|
||||
Lifecycle Flow
|
||||
==============
|
||||
|
||||
Initialization Flow
|
||||
-------------------
|
||||
|
||||
1. ``SplashVideo.initialize()`` called in ``main()``
|
||||
- Defers Flutter's first frame
|
||||
- Native splash remains visible
|
||||
|
||||
2. ``SplashVideo`` widget mounts
|
||||
- Creates/receives controller
|
||||
- Validates configuration
|
||||
- Initializes ``VideoSplash``
|
||||
|
||||
3. Video loads
|
||||
- ``onSourceLoaded`` callback fires
|
||||
- Defaults to ``SplashVideo.resume()``
|
||||
- Flutter frames begin rendering
|
||||
- Smooth transition from native to video
|
||||
|
||||
4. Video plays
|
||||
- Overlays render on top
|
||||
- User can interact via controller
|
||||
|
||||
Completion Flow (Non-Looping)
|
||||
------------------------------
|
||||
|
||||
1. Video reaches end
|
||||
2. Check for completion handlers:
|
||||
- If ``onVideoComplete`` set → call it
|
||||
- Else if ``nextScreen`` set → auto-navigate
|
||||
- Else → do nothing
|
||||
|
||||
Completion Flow (Looping)
|
||||
--------------------------
|
||||
|
||||
1. Video loops infinitely
|
||||
2. User must call ``controller.skip()`` or ``controller.pause()``
|
||||
3. No automatic completion
|
||||
|
||||
Implementation Checklist
|
||||
=========================
|
||||
|
||||
Phase 1: Core Implementation
|
||||
-----------------------------
|
||||
|
||||
1. ✅ Create ``SplashVideoController`` class
|
||||
2. ✅ Update ``VideoConfig`` (remove loopVideo)
|
||||
3. ✅ Update ``VideoSplash`` widget for media_kit integration
|
||||
4. ✅ Create main ``SplashVideo`` widget with:
|
||||
- Validation logic
|
||||
- Overlay support
|
||||
- Navigation handling
|
||||
- Lifecycle management
|
||||
5. ✅ Update ``lib/splash_video.dart`` exports
|
||||
6. ✅ Create example application
|
||||
7. ✅ Update README.md with usage examples
|
||||
|
||||
Phase 2: Future Enhancements
|
||||
-----------------------------
|
||||
|
||||
- 🔜 Boomerang loop support
|
||||
- 🔜 Additional overlay positioning options
|
||||
- 🔜 Advanced playback controls
|
||||
- 🔜 Analytics/telemetry hooks
|
||||
|
||||
Example Usage
|
||||
=============
|
||||
|
||||
Non-Looping with Auto-Navigation
|
||||
---------------------------------
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
SplashVideo.initialize();
|
||||
runApp(MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: SplashVideo(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
nextScreen: HomeScreen(),
|
||||
backgroundColor: Colors.black,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Looping with Manual Control
|
||||
----------------------------
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
class MyApp extends StatefulWidget {
|
||||
@override
|
||||
State<MyApp> createState() => _MyAppState();
|
||||
}
|
||||
|
||||
class _MyAppState extends State<MyApp> {
|
||||
late SplashVideoController controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = SplashVideoController(loopVideo: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: SplashVideo(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
controller: controller,
|
||||
titleWidget: Text(
|
||||
'Welcome',
|
||||
style: TextStyle(fontSize: 32, color: Colors.white),
|
||||
),
|
||||
footerWidget: ElevatedButton(
|
||||
onPressed: () => controller.skip(),
|
||||
child: Text('Skip'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Manual Completion Control
|
||||
--------------------------
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
SplashVideo(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
onVideoComplete: () {
|
||||
// Custom logic
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => HomeScreen()),
|
||||
);
|
||||
},
|
||||
titleWidget: Text('Loading...'),
|
||||
)
|
||||
|
||||
Custom Overlay
|
||||
--------------
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
SplashVideo(
|
||||
source: NetworkFileSource('https://example.com/splash.mp4'),
|
||||
overlayBuilder: (context) => Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: 50,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(child: Text('Custom UI')),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 20,
|
||||
right: 20,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
Required packages in ``pubspec.yaml``:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
media_kit: ^1.2.6
|
||||
media_kit_video: ^2.0.1
|
||||
video_player_media_kit: ^2.0.0
|
||||
|
||||
# Platform-specific libs
|
||||
media_kit_libs_android_video: any
|
||||
media_kit_libs_ios_video: any
|
||||
media_kit_libs_macos_video: any
|
||||
media_kit_libs_windows_video: any
|
||||
media_kit_libs_linux: any
|
||||
|
||||
Testing Strategy
|
||||
================
|
||||
|
||||
Unit Tests
|
||||
----------
|
||||
|
||||
- Source type validation
|
||||
- Controller state management
|
||||
- Configuration validation
|
||||
- Overlay composition
|
||||
|
||||
Widget Tests
|
||||
------------
|
||||
|
||||
- Video playback initialization
|
||||
- Navigation behavior
|
||||
- Overlay rendering
|
||||
- Lifecycle management
|
||||
|
||||
Integration Tests
|
||||
-----------------
|
||||
|
||||
- End-to-end splash flow
|
||||
- Platform-specific video playback
|
||||
- Performance benchmarks
|
||||
|
||||
Notes
|
||||
=====
|
||||
|
||||
- All file and class names follow lowercase conventions
|
||||
- Follows Flutter/Dart style guide
|
||||
- Media kit requires initialization in main()
|
||||
- Defer pattern prevents frame jank during transition
|
||||
- Controller pattern provides maximum flexibility
|
||||
|
||||
Document Status
|
||||
===============
|
||||
|
||||
:Status: Approved for Implementation
|
||||
:Next Review: After Phase 1 completion
|
||||
|
|
@ -1,4 +1,41 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
class SplashVideo {
|
||||
library;
|
||||
|
||||
}
|
||||
// Main widget
|
||||
export 'widgets/splash_video_page_w.dart';
|
||||
|
||||
// Controller
|
||||
export 'splash_video_controller.dart';
|
||||
|
||||
// Configuration
|
||||
export 'video_config.dart';
|
||||
|
||||
// Source types
|
||||
export 'video_source.dart';
|
||||
|
||||
// Enums
|
||||
export 'splash_video_enums.dart';
|
||||
|
||||
// Utilities
|
||||
export 'utils.dart';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* 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 '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,
|
||||
});
|
||||
|
||||
/// 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;
|
||||
|
||||
/// 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
|
||||
///
|
||||
/// Should be called when the splash screen is no longer needed
|
||||
void dispose() {
|
||||
if (_isDisposed) return;
|
||||
_isDisposed = true;
|
||||
_player?.dispose();
|
||||
_player = null;
|
||||
}
|
||||
|
||||
/// Whether the controller has been disposed
|
||||
bool get isDisposed => _isDisposed;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/// Controls how the video is displayed on screen
|
||||
enum VisibilityEnum {
|
||||
/// Display video in full screen, filling the entire available space
|
||||
useFullScreen,
|
||||
|
||||
/// Display video maintaining its original aspect ratio
|
||||
useAspectRatio,
|
||||
|
||||
/// Display video without any special sizing constraints
|
||||
none,
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/// Callback type for when splash duration is determined
|
||||
typedef OnSplashDuration = void Function(Duration);
|
||||
|
||||
/// Callback type for warning messages
|
||||
typedef WarningCallback = void Function(String);
|
||||
|
||||
/// Callback type for video loading errors
|
||||
typedef OnVideoError = void Function(String error);
|
||||
|
||||
/// Exception thrown when there's an error in SplashVideo
|
||||
class SplashVideoException implements Exception {
|
||||
/// Creates a new SplashVideoException with the given message
|
||||
const SplashVideoException({required this.message});
|
||||
|
||||
/// The error message
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SplashVideoException: $message';
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension methods for Iterable
|
||||
extension FirstWhereOrNullExtension<E> on Iterable<E> {
|
||||
/// Returns the first element that satisfies the given predicate or null
|
||||
E? firstWhereOrNull(bool Function(E) func) {
|
||||
for (final element in this) {
|
||||
if (func(element)) return element;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 'package:media_kit/media_kit.dart';
|
||||
import 'package:splash_video/splash_video_enums.dart';
|
||||
|
||||
/// Configuration options for video splash screen
|
||||
class VideoConfig {
|
||||
/// Creates a VideoConfig with the specified options
|
||||
const VideoConfig({
|
||||
this.playImmediately = true,
|
||||
this.videoVisibilityEnum = VisibilityEnum.useFullScreen,
|
||||
this.useSafeArea = false,
|
||||
this.volume = 100.0,
|
||||
this.onPlayerInitialized,
|
||||
});
|
||||
|
||||
/// Whether to start playing the video immediately after initialization
|
||||
///
|
||||
/// If false, you must call `play()` explicitly through [onPlayerInitialized]
|
||||
/// ```dart
|
||||
/// VideoConfig(
|
||||
/// playImmediately: false,
|
||||
/// onPlayerInitialized: (player) {
|
||||
/// // Custom logic before playing
|
||||
/// player.play();
|
||||
/// },
|
||||
/// )
|
||||
/// ```
|
||||
final bool playImmediately;
|
||||
|
||||
/// Whether to wrap the video in a SafeArea widget
|
||||
///
|
||||
/// Use this to avoid notches, status bars, and other system UI elements
|
||||
final bool useSafeArea;
|
||||
|
||||
/// How the video should be displayed on screen
|
||||
///
|
||||
/// - [VisibilityEnum.useFullScreen]: Fill the entire screen
|
||||
/// - [VisibilityEnum.useAspectRatio]: Maintain video's aspect ratio
|
||||
/// - [VisibilityEnum.none]: No special sizing
|
||||
final VisibilityEnum videoVisibilityEnum;
|
||||
|
||||
/// Initial volume level (0.0 to 100.0)
|
||||
///
|
||||
/// Defaults to 100.0 (maximum volume)
|
||||
final double volume;
|
||||
|
||||
/// Callback invoked when the Player is initialized
|
||||
///
|
||||
/// Provides access to the media_kit [Player] instance for custom configuration
|
||||
/// ```dart
|
||||
/// VideoConfig(
|
||||
/// onPlayerInitialized: (player) {
|
||||
/// print('Player ready: ${player.state.duration}');
|
||||
/// // You can access player.state or call player methods here
|
||||
/// },
|
||||
/// )
|
||||
/// ```
|
||||
final void Function(Player player)? onPlayerInitialized;
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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/services.dart';
|
||||
import 'package:splash_video/utils.dart';
|
||||
|
||||
/// Base class for all video source types
|
||||
sealed class Source {
|
||||
/// Validates and prepares the source for use
|
||||
void setSource();
|
||||
}
|
||||
|
||||
/// Represents a video asset bundled with the application
|
||||
final class AssetSource extends Source {
|
||||
/// The asset path relative to the project root
|
||||
final String path;
|
||||
|
||||
/// Creates an asset source with the given [path]
|
||||
///
|
||||
/// Example: `AssetSource('assets/videos/splash.mp4')`
|
||||
AssetSource(this.path);
|
||||
|
||||
@override
|
||||
Future<void> setSource() async {}
|
||||
}
|
||||
|
||||
/// Represents a video file on the device's file system
|
||||
final class DeviceFileSource extends Source {
|
||||
/// The absolute path to the video file on the device
|
||||
final String path;
|
||||
|
||||
/// The File object created from the path
|
||||
late final File file;
|
||||
|
||||
/// Creates a device file source with the given [path]
|
||||
///
|
||||
/// Example: `DeviceFileSource('/path/to/video.mp4')`
|
||||
DeviceFileSource(this.path);
|
||||
|
||||
@override
|
||||
Future<void> setSource() async {
|
||||
file = File(path);
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a video available via network URL
|
||||
final class NetworkFileSource extends Source {
|
||||
/// The URL string pointing to the video file
|
||||
final String path;
|
||||
Uri? _url;
|
||||
|
||||
/// The parsed URI object
|
||||
Uri? get url => _url;
|
||||
|
||||
/// Creates a network file source with the given [path]
|
||||
///
|
||||
/// The path should be a valid URL string.
|
||||
/// Example: `NetworkFileSource('https://example.com/video.mp4')`
|
||||
NetworkFileSource(this.path) {
|
||||
setSource();
|
||||
}
|
||||
|
||||
@override
|
||||
void setSource() {
|
||||
_url = Uri.tryParse(path);
|
||||
if (_url == null) {
|
||||
throw SplashVideoException(message: 'Unable to parse URI: $path');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a video loaded from raw bytes in memory
|
||||
final class BytesSource extends Source {
|
||||
/// The raw video data as bytes
|
||||
final Uint8List bytes;
|
||||
|
||||
/// Creates a bytes source with the given [bytes]
|
||||
///
|
||||
/// This is useful for videos loaded from memory or custom sources
|
||||
BytesSource(this.bytes);
|
||||
|
||||
@override
|
||||
void setSource() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,349 @@
|
|||
/*
|
||||
* 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:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:splash_video/utils.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/widgets/splash_video_player_w.dart';
|
||||
|
||||
/// Main widget for displaying a video splash screen
|
||||
///
|
||||
/// This widget handles video playback, overlays, navigation, and lifecycle management.
|
||||
/// It provides a smooth transition from native splash screen to video splash.
|
||||
class SplashVideoPage extends StatefulWidget {
|
||||
/// Creates a SplashVideo widget
|
||||
///
|
||||
/// [source] is required and specifies the video source
|
||||
/// [controller] is required when using looping videos
|
||||
///
|
||||
/// Throws [AssertionError] if validation rules are violated
|
||||
const SplashVideoPage({
|
||||
super.key,
|
||||
required this.source,
|
||||
this.controller,
|
||||
this.videoConfig,
|
||||
this.backgroundColor,
|
||||
this.titleWidget,
|
||||
this.footerWidget,
|
||||
this.overlayBuilder,
|
||||
this.nextScreen,
|
||||
this.onVideoComplete,
|
||||
this.onSourceLoaded,
|
||||
this.onVideoError,
|
||||
});
|
||||
|
||||
/// Source for the video (asset, network, file, or bytes)
|
||||
final Source source;
|
||||
|
||||
/// Controller for managing video playback
|
||||
///
|
||||
/// Required when [controller.loopVideo] is true
|
||||
final SplashVideoController? controller;
|
||||
|
||||
/// Configuration options for video playback
|
||||
final VideoConfig? videoConfig;
|
||||
|
||||
/// Background color displayed behind the video
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// Widget displayed at the top of the screen over the video
|
||||
final Widget? titleWidget;
|
||||
|
||||
/// Widget displayed at the bottom of the screen over the video
|
||||
final Widget? footerWidget;
|
||||
|
||||
/// Custom overlay builder for full control over UI elements
|
||||
///
|
||||
/// This is rendered last and provides complete flexibility
|
||||
final Widget Function(BuildContext)? overlayBuilder;
|
||||
|
||||
/// Screen to navigate to when video completes (auto-navigation)
|
||||
///
|
||||
/// Cannot be used with [onVideoComplete] or looping videos
|
||||
final Widget? nextScreen;
|
||||
|
||||
/// Callback invoked when video completes (manual control)
|
||||
///
|
||||
/// Takes priority over [nextScreen]. Cannot be used with looping videos
|
||||
final VoidCallback? onVideoComplete;
|
||||
|
||||
/// Callback invoked when video source is loaded
|
||||
///
|
||||
/// Defaults to calling [resume] to allow Flutter frames to render
|
||||
final VoidCallback? onSourceLoaded;
|
||||
|
||||
/// Callback invoked when video fails to load
|
||||
///
|
||||
/// If not provided, a default error UI will be shown
|
||||
final OnVideoError? onVideoError;
|
||||
|
||||
// Track whether the first frame has been deferred
|
||||
static bool _firstFrameDeferred = false;
|
||||
|
||||
/// Initializes MediaKit and prevents Flutter frames from rendering
|
||||
///
|
||||
/// Call this in main() before runApp to:
|
||||
/// 1. Defer first frame ASAP to prevent jank
|
||||
/// 2. Initialize the MediaKit library for video playback
|
||||
/// 3. Keep the native splash visible while video loads
|
||||
///
|
||||
/// ⚠️ IMPORTANT: Only call this if your app's FIRST screen (MaterialApp.home)
|
||||
/// is a SplashVideoPage. If you show a menu/selector first, DON'T call this.
|
||||
///
|
||||
/// ```dart
|
||||
/// void main() {
|
||||
/// WidgetsFlutterBinding.ensureInitialized();
|
||||
/// SplashVideoPage.initialize(); // Only if first screen is SplashVideoPage!
|
||||
/// runApp(MyApp());
|
||||
/// }
|
||||
/// ```
|
||||
static void initialize() {
|
||||
// Defer first frame ASAP to avoid jank
|
||||
WidgetsBinding.instance.deferFirstFrame();
|
||||
_firstFrameDeferred = true;
|
||||
|
||||
// Then initialize MediaKit
|
||||
MediaKit.ensureInitialized();
|
||||
|
||||
if (kDebugMode) {
|
||||
debugPrint('✓ SplashVideo: First frame deferred, MediaKit initialized');
|
||||
debugPrint('⚠️ Waiting for SplashVideoPage to resume rendering...');
|
||||
debugPrint('⚠️ If your app shows a black screen, make sure SplashVideoPage is your FIRST screen!');
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests if MediaKit is properly configured and can create a Player
|
||||
///
|
||||
/// Returns true if MediaKit is working, false otherwise.
|
||||
/// Prints diagnostic information in debug mode.
|
||||
static Future<bool> testMediaKit() async {
|
||||
try {
|
||||
if (kDebugMode) {
|
||||
debugPrint('Testing MediaKit installation...');
|
||||
}
|
||||
|
||||
// Initialize MediaKit if not already done
|
||||
MediaKit.ensureInitialized();
|
||||
|
||||
// Try to create and dispose a player
|
||||
final testPlayer = Player();
|
||||
await testPlayer.dispose();
|
||||
|
||||
if (kDebugMode) {
|
||||
debugPrint('✓ MediaKit test passed: Player created and disposed successfully');
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('✗ MediaKit test failed: $e');
|
||||
debugPrint(' Make sure media_kit platform libraries are added to pubspec.yaml');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Resumes Flutter frame rendering
|
||||
///
|
||||
/// Called automatically by default when video loads, or manually
|
||||
/// through [onSourceLoaded] callback
|
||||
///
|
||||
/// Only calls allowFirstFrame() if the first frame was actually deferred
|
||||
static void resume() {
|
||||
if (_firstFrameDeferred) {
|
||||
WidgetsBinding.instance.allowFirstFrame();
|
||||
_firstFrameDeferred = false;
|
||||
|
||||
if (kDebugMode) {
|
||||
debugPrint('✓ SplashVideo: First frame resumed, app is now visible');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
State<SplashVideoPage> createState() => _SplashVideoPageState();
|
||||
}
|
||||
|
||||
class _SplashVideoPageState extends State<SplashVideoPage> {
|
||||
Timer? _completionTimer;
|
||||
StreamSubscription? _skipSubscription;
|
||||
|
||||
late final VoidCallback onSourceLoaded;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Validate configuration
|
||||
_validateConfiguration();
|
||||
|
||||
onSourceLoaded = widget.onSourceLoaded ?? SplashVideoPage.resume;
|
||||
|
||||
// Listen for skip requests if controller provided
|
||||
if (widget.controller != null) {
|
||||
_listenForSkip();
|
||||
}
|
||||
}
|
||||
|
||||
void _validateConfiguration() {
|
||||
// Cannot use both nextScreen and onVideoComplete
|
||||
if (widget.nextScreen != null && widget.onVideoComplete != null) {
|
||||
throw ArgumentError(
|
||||
'Cannot use both nextScreen and onVideoComplete. '
|
||||
'Use onVideoComplete for manual control or nextScreen for auto-navigation.',
|
||||
);
|
||||
}
|
||||
|
||||
// Looping videos need controller
|
||||
if (widget.controller?.loopVideo == true) {
|
||||
// Cannot use nextScreen with looping
|
||||
if (widget.nextScreen != null) {
|
||||
throw ArgumentError(
|
||||
'Cannot use nextScreen with looping videos. '
|
||||
'Use controller.skip() to end the video manually.',
|
||||
);
|
||||
}
|
||||
// Cannot use onVideoComplete with looping
|
||||
if (widget.onVideoComplete != null) {
|
||||
throw ArgumentError(
|
||||
'Cannot use onVideoComplete with looping videos. '
|
||||
'Use controller.skip() to end the video manually.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _listenForSkip() {
|
||||
// Poll for skip requests
|
||||
_skipSubscription = Stream.periodic(
|
||||
const Duration(milliseconds: 100),
|
||||
).listen((_) {
|
||||
if (widget.controller?.skipRequested == true) {
|
||||
_onSplashComplete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_completionTimer?.cancel();
|
||||
_skipSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _buildVideoWithOverlays();
|
||||
}
|
||||
|
||||
Widget _buildVideoWithOverlays() {
|
||||
final videoWidget = SplashVideoPlayer(
|
||||
source: widget.source,
|
||||
controller: widget.controller,
|
||||
videoConfig: widget.videoConfig,
|
||||
backgroundColor: widget.backgroundColor,
|
||||
onSplashDuration: _updateSplashDuration,
|
||||
onVideoError: widget.onVideoError,
|
||||
);
|
||||
|
||||
// If no overlays, return video directly
|
||||
if (widget.titleWidget == null &&
|
||||
widget.footerWidget == null &&
|
||||
widget.overlayBuilder == null) {
|
||||
return videoWidget;
|
||||
}
|
||||
|
||||
// Build stack with overlays
|
||||
return Stack(
|
||||
children: [
|
||||
videoWidget,
|
||||
if (widget.titleWidget != null)
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: SafeArea(child: widget.titleWidget!),
|
||||
),
|
||||
if (widget.footerWidget != null)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: SafeArea(child: widget.footerWidget!),
|
||||
),
|
||||
if (widget.overlayBuilder != null)
|
||||
widget.overlayBuilder!(context),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _updateSplashDuration(Duration duration) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('SplashVideoPage: Video loaded with duration $duration');
|
||||
debugPrint(' Calling onSourceLoaded to resume first frame...');
|
||||
}
|
||||
|
||||
// Call the onSourceLoaded callback
|
||||
onSourceLoaded.call();
|
||||
|
||||
if (kDebugMode) {
|
||||
debugPrint(' ✓ First frame resumed');
|
||||
}
|
||||
|
||||
// Don't set timer for looping videos
|
||||
if (widget.controller?.loopVideo == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set timer to complete after video duration
|
||||
_completionTimer?.cancel();
|
||||
_completionTimer = Timer(duration, _onSplashComplete);
|
||||
}
|
||||
|
||||
void _onSplashComplete() {
|
||||
// Prevent multiple calls
|
||||
if (!mounted) return;
|
||||
|
||||
// Priority 1: onVideoComplete callback
|
||||
if (widget.onVideoComplete != null) {
|
||||
widget.onVideoComplete!.call();
|
||||
return;
|
||||
}
|
||||
|
||||
// Priority 2: Auto-navigate to nextScreen
|
||||
if (widget.nextScreen != null) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => widget.nextScreen!,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// No navigation configured, do nothing
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,307 @@
|
|||
/*
|
||||
* 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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,9 +10,9 @@ environment:
|
|||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
video_player_media_kit: ^2.0.0
|
||||
# video_player_media_kit "plugins"
|
||||
# NOTE: Supporting all platforms that are native
|
||||
media_kit: ^1.2.6
|
||||
media_kit_video: ^2.0.1
|
||||
# Platform-specific native video libraries
|
||||
media_kit_libs_android_video: any
|
||||
media_kit_libs_ios_video: any
|
||||
media_kit_libs_macos_video: any
|
||||
|
|
|
|||
|
|
@ -0,0 +1,379 @@
|
|||
============
|
||||
splash_video
|
||||
============
|
||||
|
||||
A Flutter package for creating smooth video splash screens using media_kit. Provides seamless transitions from native splash screens to video playback with flexible overlay support and manual controls.
|
||||
|
||||
Features
|
||||
========
|
||||
|
||||
- ✨ **Smooth Transitions** - Defer first frame pattern prevents jank between native and video splash
|
||||
- 🎬 **Media Kit Integration** - Cross-platform video playback with hardware acceleration
|
||||
- 🎮 **Manual Controls** - Full controller access for play, pause, skip operations
|
||||
- 🔄 **Looping Support** - Infinite video loops with user-controlled exit
|
||||
- 📱 **Flexible Overlays** - Title, footer, and custom overlay widgets
|
||||
- 🎯 **Auto-Navigation** - Automatic screen transitions or manual control
|
||||
- 🎨 **Customizable** - Aspect ratio, fullscreen, volume, and more
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
Add to your ``pubspec.yaml``:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
dependencies:
|
||||
splash_video: ^0.0.1
|
||||
media_kit: ^1.2.6
|
||||
media_kit_video: ^2.0.1
|
||||
|
||||
# Platform-specific video libraries
|
||||
media_kit_libs_android_video: any
|
||||
media_kit_libs_ios_video: any
|
||||
media_kit_libs_macos_video: any
|
||||
media_kit_libs_windows_video: any
|
||||
media_kit_libs_linux: any
|
||||
|
||||
Quick Start
|
||||
===========
|
||||
|
||||
1. Initialize in main()
|
||||
-----------------------
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:splash_video/splash_video.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Initialize SplashVideo (handles MediaKit and defers first frame)
|
||||
SplashVideo.initialize();
|
||||
|
||||
runApp(MyApp());
|
||||
}
|
||||
|
||||
2. Basic Usage (Auto-Navigate)
|
||||
-------------------------------
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: SplashVideo(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
nextScreen: HomeScreen(),
|
||||
backgroundColor: Colors.black,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Usage Examples
|
||||
==============
|
||||
|
||||
Auto-Navigation
|
||||
---------------
|
||||
|
||||
Video plays and automatically navigates to the next screen:
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
SplashVideo(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
nextScreen: HomeScreen(),
|
||||
backgroundColor: Colors.black,
|
||||
)
|
||||
|
||||
Manual Control
|
||||
--------------
|
||||
|
||||
Use a callback for custom logic after video completes:
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
SplashVideo(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
onVideoComplete: () {
|
||||
// Custom logic
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => HomeScreen()),
|
||||
);
|
||||
},
|
||||
)
|
||||
|
||||
Looping Video with Controller
|
||||
------------------------------
|
||||
|
||||
Create an infinite loop that the user can manually exit:
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
class MyScreen extends StatefulWidget {
|
||||
@override
|
||||
State<MyScreen> createState() => _MyScreenState();
|
||||
}
|
||||
|
||||
class _MyScreenState extends State<MyScreen> {
|
||||
late final SplashVideoController controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = SplashVideoController(loopVideo: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onSkip() {
|
||||
controller.skip();
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => HomeScreen()),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SplashVideo(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
controller: controller,
|
||||
footerWidget: ElevatedButton(
|
||||
onPressed: _onSkip,
|
||||
child: Text('Skip'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
With Overlays
|
||||
-------------
|
||||
|
||||
Add title, footer, and custom overlays:
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
SplashVideo(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
nextScreen: HomeScreen(),
|
||||
backgroundColor: Colors.black,
|
||||
|
||||
// Title at top
|
||||
titleWidget: Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Text(
|
||||
'Welcome',
|
||||
style: TextStyle(fontSize: 32, color: Colors.white),
|
||||
),
|
||||
),
|
||||
|
||||
// Footer at bottom
|
||||
footerWidget: CircularProgressIndicator(),
|
||||
|
||||
// Custom overlay with full control
|
||||
overlayBuilder: (context) => Positioned(
|
||||
right: 20,
|
||||
top: 100,
|
||||
child: Text('v1.0.0', style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
)
|
||||
|
||||
Network Video
|
||||
-------------
|
||||
|
||||
Load video from URL:
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
SplashVideo(
|
||||
source: NetworkFileSource('https://example.com/splash.mp4'),
|
||||
nextScreen: HomeScreen(),
|
||||
)
|
||||
|
||||
Custom Configuration
|
||||
--------------------
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
SplashVideo(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
nextScreen: HomeScreen(),
|
||||
videoConfig: VideoConfig(
|
||||
playImmediately: true,
|
||||
videoVisibilityEnum: VisibilityEnum.useAspectRatio,
|
||||
useSafeArea: true,
|
||||
volume: 50.0,
|
||||
onPlayerInitialized: (player) {
|
||||
print('Player ready: ${player.state.duration}');
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
API Reference
|
||||
=============
|
||||
|
||||
SplashVideo
|
||||
-----------
|
||||
|
||||
Main widget for video splash screen.
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
:widths: 20 25 10 45
|
||||
|
||||
* - Parameter
|
||||
- Type
|
||||
- Required
|
||||
- Description
|
||||
* - ``source``
|
||||
- ``Source``
|
||||
- ✅
|
||||
- Video source (Asset, Network, DeviceFile, Bytes)
|
||||
* - ``controller``
|
||||
- ``SplashVideoController?``
|
||||
- ⚠️
|
||||
- Required when looping is enabled
|
||||
* - ``videoConfig``
|
||||
- ``VideoConfig?``
|
||||
- ❌
|
||||
- Playback configuration
|
||||
* - ``backgroundColor``
|
||||
- ``Color?``
|
||||
- ❌
|
||||
- Background color behind video
|
||||
* - ``titleWidget``
|
||||
- ``Widget?``
|
||||
- ❌
|
||||
- Widget at top of screen
|
||||
* - ``footerWidget``
|
||||
- ``Widget?``
|
||||
- ❌
|
||||
- Widget at bottom of screen
|
||||
* - ``overlayBuilder``
|
||||
- ``Widget Function(BuildContext)?``
|
||||
- ❌
|
||||
- Custom overlay builder
|
||||
* - ``nextScreen``
|
||||
- ``Widget?``
|
||||
- ❌
|
||||
- Screen to navigate to (auto-navigate)
|
||||
* - ``onVideoComplete``
|
||||
- ``VoidCallback?``
|
||||
- ❌
|
||||
- Callback when video completes (manual control)
|
||||
* - ``onSourceLoaded``
|
||||
- ``VoidCallback?``
|
||||
- ❌
|
||||
- Callback when video loads
|
||||
|
||||
**Validation Rules:**
|
||||
|
||||
- Cannot use both ``nextScreen`` and ``onVideoComplete``
|
||||
- Looping videos require ``controller`` and cannot use ``nextScreen`` or ``onVideoComplete``
|
||||
|
||||
SplashVideoController
|
||||
---------------------
|
||||
|
||||
Controls video playback.
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
final controller = SplashVideoController(loopVideo: true);
|
||||
|
||||
// Access media_kit Player
|
||||
controller.player.play();
|
||||
controller.player.pause();
|
||||
controller.player.seek(Duration(seconds: 5));
|
||||
|
||||
// Controller methods
|
||||
controller.play();
|
||||
controller.pause();
|
||||
controller.skip(); // Complete immediately
|
||||
controller.dispose();
|
||||
|
||||
VideoConfig
|
||||
-----------
|
||||
|
||||
Configuration for video playback.
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
VideoConfig(
|
||||
playImmediately: true, // Auto-play on load
|
||||
videoVisibilityEnum: VisibilityEnum.useFullScreen, // Display mode
|
||||
useSafeArea: false, // Wrap in SafeArea
|
||||
volume: 100.0, // Volume (0-100)
|
||||
onPlayerInitialized: (player) { }, // Player callback
|
||||
)
|
||||
|
||||
Source Types
|
||||
------------
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
// Asset bundled with app
|
||||
AssetSource('assets/videos/splash.mp4')
|
||||
|
||||
// Network URL
|
||||
NetworkFileSource('https://example.com/video.mp4')
|
||||
|
||||
// Device file
|
||||
DeviceFileSource('/path/to/video.mp4')
|
||||
|
||||
// Raw bytes
|
||||
BytesSource(videoBytes)
|
||||
|
||||
VisibilityEnum
|
||||
--------------
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
VisibilityEnum.useFullScreen // Fill entire screen
|
||||
VisibilityEnum.useAspectRatio // Maintain aspect ratio
|
||||
VisibilityEnum.none // No special sizing
|
||||
|
||||
Lifecycle Methods
|
||||
=================
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
// Defer first frame (prevents jank)
|
||||
SplashVideo.initialize();
|
||||
|
||||
// Resume Flutter rendering
|
||||
SplashVideo.resume();
|
||||
|
||||
Platform Support
|
||||
================
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
|
||||
* - Platform
|
||||
- Supported
|
||||
* - Android
|
||||
- ✅
|
||||
* - iOS
|
||||
- ✅
|
||||
* - macOS
|
||||
- ✅
|
||||
* - Windows
|
||||
- ✅
|
||||
* - Linux
|
||||
- ✅
|
||||
* - Web
|
||||
- ✅
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
MIT License - see LICENSE file for details.
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
# Testing Guide for splash_video
|
||||
|
||||
This package includes comprehensive unit and integration tests to ensure reliability and correctness.
|
||||
|
||||
## Test Structure
|
||||
|
||||
### Unit Tests (`/test`)
|
||||
|
||||
Unit tests verify individual components in isolation:
|
||||
|
||||
- **video_source_test.dart** - Tests for all Source types (Asset, Network, DeviceFile, Bytes)
|
||||
- **splash_video_controller_test.dart** - Tests for controller lifecycle and operations
|
||||
- **video_config_test.dart** - Tests for VideoConfig and VisibilityEnum
|
||||
- **utils_test.dart** - Tests for utility functions, exceptions, and typedefs
|
||||
- **splash_video_page_test.dart** - Widget tests for SplashVideoPage
|
||||
|
||||
### Integration Tests (`/example/integration_test`)
|
||||
|
||||
Integration tests verify end-to-end functionality:
|
||||
|
||||
- **plugin_integration_test.dart** - Full integration tests including:
|
||||
- Configuration validation
|
||||
- Error handling
|
||||
- Overlay rendering
|
||||
- Controller integration
|
||||
- Source type handling
|
||||
- VideoConfig integration
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Run All Unit Tests
|
||||
|
||||
```bash
|
||||
flutter test
|
||||
```
|
||||
|
||||
### Run Specific Test File
|
||||
|
||||
```bash
|
||||
flutter test test/video_source_test.dart
|
||||
```
|
||||
|
||||
### Run Tests with Coverage
|
||||
|
||||
```bash
|
||||
flutter test --coverage
|
||||
```
|
||||
|
||||
### Run Integration Tests
|
||||
|
||||
```bash
|
||||
cd example
|
||||
flutter test integration_test/plugin_integration_test.dart
|
||||
```
|
||||
|
||||
### Run Integration Tests on Device
|
||||
|
||||
```bash
|
||||
cd example
|
||||
flutter drive \
|
||||
--driver=test_driver/integration_test.dart \
|
||||
--target=integration_test/plugin_integration_test.dart
|
||||
```
|
||||
|
||||
## Test Coverage
|
||||
|
||||
The test suite covers:
|
||||
|
||||
- ✅ All Source types and validation
|
||||
- ✅ SplashVideoController lifecycle
|
||||
- ✅ VideoConfig options
|
||||
- ✅ Error handling and callbacks
|
||||
- ✅ Overlay rendering
|
||||
- ✅ Navigation validation
|
||||
- ✅ Looping behavior
|
||||
- ✅ Widget composition
|
||||
|
||||
## Writing New Tests
|
||||
|
||||
### Unit Test Template
|
||||
|
||||
```dart
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:splash_video/splash_video.dart';
|
||||
|
||||
void main() {
|
||||
group('Feature Name', () {
|
||||
test('should do something', () {
|
||||
// Arrange
|
||||
final input = ...;
|
||||
|
||||
// Act
|
||||
final result = ...;
|
||||
|
||||
// Assert
|
||||
expect(result, equals(expected));
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Widget Test Template
|
||||
|
||||
```dart
|
||||
testWidgets('widget should render', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: YourWidget(),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(YourWidget), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
### Integration Test Template
|
||||
|
||||
```dart
|
||||
testWidgets('integration scenario', (WidgetTester tester) async {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
await tester.pumpWidget(MyApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Interact with the app
|
||||
await tester.tap(find.byIcon(Icons.add));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify results
|
||||
expect(find.text('Result'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
```yaml
|
||||
name: Tests
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: subosito/flutter-action@v2
|
||||
- run: flutter pub get
|
||||
- run: flutter test --coverage
|
||||
- run: cd example && flutter test integration_test
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### MediaKit Initialization
|
||||
|
||||
Some tests require MediaKit initialization. This is handled automatically in test files:
|
||||
|
||||
```dart
|
||||
setUpAll(() {
|
||||
MediaKit.ensureInitialized();
|
||||
});
|
||||
```
|
||||
|
||||
### Video Assets
|
||||
|
||||
Integration tests may require actual video assets in `example/assets/`. For tests without real videos, use error handling:
|
||||
|
||||
```dart
|
||||
onVideoError: (_) {}, // Suppress errors in tests
|
||||
```
|
||||
|
||||
### Platform Differences
|
||||
|
||||
Some media_kit features are platform-specific. Tests should be designed to handle platform variations gracefully.
|
||||
|
||||
## Test Best Practices
|
||||
|
||||
1. **Keep tests isolated** - Each test should be independent
|
||||
2. **Use descriptive names** - Test names should explain what they verify
|
||||
3. **Test edge cases** - Include boundary conditions and error scenarios
|
||||
4. **Mock external dependencies** - Use test doubles when appropriate
|
||||
5. **Clean up resources** - Dispose controllers and players in tearDown
|
||||
6. **Use test groups** - Organize related tests together
|
||||
7. **Verify error handling** - Test both success and failure paths
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- Video playback tests require platform-specific media support
|
||||
- Some tests may need real video files to fully verify behavior
|
||||
- Network tests depend on connectivity (should use mocks for CI/CD)
|
||||
- Player initialization timing can vary across platforms
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* 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 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:splash_video/splash_video.dart';
|
||||
|
||||
void main() {
|
||||
// Initialize MediaKit for testing
|
||||
setUpAll(() {
|
||||
MediaKit.ensureInitialized();
|
||||
});
|
||||
|
||||
group('SplashVideoController', () {
|
||||
late Player player;
|
||||
|
||||
setUp(() {
|
||||
player = Player();
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await player.dispose();
|
||||
});
|
||||
|
||||
test('creates with default values', () {
|
||||
final controller = SplashVideoController();
|
||||
expect(controller.loopVideo, isFalse);
|
||||
expect(controller.skipRequested, isFalse);
|
||||
});
|
||||
|
||||
test('creates with loopVideo enabled', () {
|
||||
final controller = SplashVideoController(loopVideo: true);
|
||||
expect(controller.loopVideo, isTrue);
|
||||
});
|
||||
|
||||
test('throws StateError when accessing player before attach', () {
|
||||
final controller = SplashVideoController();
|
||||
expect(
|
||||
() => controller.player,
|
||||
throwsStateError,
|
||||
);
|
||||
});
|
||||
|
||||
test('allows player access after attach', () {
|
||||
final controller = SplashVideoController();
|
||||
controller.attach(player);
|
||||
expect(controller.player, equals(player));
|
||||
});
|
||||
|
||||
test('attach throws StateError after disposal', () {
|
||||
final controller = SplashVideoController();
|
||||
controller.dispose();
|
||||
expect(
|
||||
() => controller.attach(player),
|
||||
throwsStateError,
|
||||
);
|
||||
});
|
||||
|
||||
test('skip sets skipRequested flag', () async {
|
||||
final controller = SplashVideoController();
|
||||
controller.attach(player);
|
||||
|
||||
expect(controller.skipRequested, isFalse);
|
||||
await controller.skip();
|
||||
expect(controller.skipRequested, isTrue);
|
||||
});
|
||||
|
||||
test('play/pause operations work with attached player', () async {
|
||||
final controller = SplashVideoController();
|
||||
controller.attach(player);
|
||||
|
||||
// These should complete without error
|
||||
await expectLater(controller.play(), completes);
|
||||
await expectLater(controller.pause(), completes);
|
||||
});
|
||||
|
||||
test('operations are no-ops after disposal', () async {
|
||||
final controller = SplashVideoController();
|
||||
controller.attach(player);
|
||||
controller.dispose();
|
||||
|
||||
// These should complete without error (no-op)
|
||||
await expectLater(controller.play(), completes);
|
||||
await expectLater(controller.pause(), completes);
|
||||
await expectLater(controller.skip(), completes);
|
||||
});
|
||||
|
||||
test('multiple dispose calls are safe', () {
|
||||
final controller = SplashVideoController();
|
||||
controller.dispose();
|
||||
controller.dispose();
|
||||
// Should not throw
|
||||
});
|
||||
});
|
||||
|
||||
group('SplashVideoController lifecycle', () {
|
||||
test('can create multiple controllers', () {
|
||||
final controller1 = SplashVideoController();
|
||||
final controller2 = SplashVideoController(loopVideo: true);
|
||||
|
||||
expect(controller1.loopVideo, isFalse);
|
||||
expect(controller2.loopVideo, isTrue);
|
||||
|
||||
controller1.dispose();
|
||||
controller2.dispose();
|
||||
});
|
||||
|
||||
test('skipRequested resets on new controller', () async {
|
||||
final player1 = Player();
|
||||
final controller1 = SplashVideoController();
|
||||
controller1.attach(player1);
|
||||
await controller1.skip();
|
||||
expect(controller1.skipRequested, isTrue);
|
||||
|
||||
controller1.dispose();
|
||||
|
||||
final controller2 = SplashVideoController();
|
||||
expect(controller2.skipRequested, isFalse);
|
||||
|
||||
controller2.dispose();
|
||||
await player1.dispose();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,290 @@
|
|||
// ignore_for_file: avoid_print
|
||||
|
||||
/*
|
||||
* 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 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:splash_video/splash_video.dart';
|
||||
|
||||
void main() {
|
||||
group('SplashVideoPage Widget Tests', () {
|
||||
testWidgets('creates widget with required parameters',
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
onVideoError: (_) {}, // Suppress errors in test
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(SplashVideoPage), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('validates conflicting nextScreen and onVideoComplete',
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
nextScreen: const Scaffold(body: Text('Next')),
|
||||
onVideoComplete: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Should throw ArgumentError
|
||||
expect(tester.takeException(), isA<ArgumentError>());
|
||||
});
|
||||
|
||||
testWidgets('validates looping with nextScreen',
|
||||
(WidgetTester tester) async {
|
||||
final controller = SplashVideoController(loopVideo: true);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
controller: controller,
|
||||
nextScreen: const Scaffold(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.takeException(), isA<ArgumentError>());
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
testWidgets('validates looping with onVideoComplete',
|
||||
(WidgetTester tester) async {
|
||||
final controller = SplashVideoController(loopVideo: true);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
controller: controller,
|
||||
onVideoComplete: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.takeException(), isA<ArgumentError>());
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
testWidgets('accepts looping without navigation',
|
||||
(WidgetTester tester) async {
|
||||
final controller = SplashVideoController(loopVideo: true);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
controller: controller,
|
||||
onVideoError: (_) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Should not throw
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.byType(SplashVideoPage), findsOneWidget);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
testWidgets('renders with backgroundColor', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
backgroundColor: Colors.red,
|
||||
onVideoError: (_) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
expect(find.byType(SplashVideoPage), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders with title overlay', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
titleWidget: const Text('App Title'),
|
||||
onVideoError: (_) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
expect(find.text('App Title'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders with footer overlay', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
footerWidget: const CircularProgressIndicator(),
|
||||
onVideoError: (_) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders with custom overlay builder',
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
overlayBuilder: (context) => const Positioned(
|
||||
top: 20,
|
||||
right: 20,
|
||||
child: Text('v1.0.0'),
|
||||
),
|
||||
onVideoError: (_) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
expect(find.text('v1.0.0'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders all overlays together', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
titleWidget: const Text('Title'),
|
||||
footerWidget: const Text('Footer'),
|
||||
overlayBuilder: (context) => const Text('Custom'),
|
||||
onVideoError: (_) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
expect(find.text('Title'), findsOneWidget);
|
||||
expect(find.text('Footer'), findsOneWidget);
|
||||
expect(find.text('Custom'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('calls onVideoError callback', (WidgetTester tester) async {
|
||||
String? errorMessage;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/invalid.mp4'),
|
||||
onVideoError: (error) {
|
||||
errorMessage = error;
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
print('onVideoError: $errorMessage');
|
||||
// Wait for potential error
|
||||
await tester.pumpAndSettle(const Duration(seconds: 3));
|
||||
|
||||
// Error should be captured (may or may not occur depending on test env)
|
||||
// Just verify the callback is wired correctly
|
||||
});
|
||||
|
||||
testWidgets('shows default error UI when no callback provided',
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/invalid.mp4'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Wait for error to manifest
|
||||
await tester.pumpAndSettle(const Duration(seconds: 3));
|
||||
|
||||
// Should show error icon when video fails (if it fails)
|
||||
// In test environment, may not actually load video
|
||||
});
|
||||
});
|
||||
|
||||
group('SplashVideoPage Static Methods', () {
|
||||
testWidgets('initialize and resume work', (WidgetTester tester) async {
|
||||
// These methods affect WidgetsBinding, so we just verify they don't throw
|
||||
SplashVideoPage.initialize();
|
||||
SplashVideoPage.resume();
|
||||
|
||||
// Should not throw
|
||||
expect(true, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('SplashVideoPage with Controller', () {
|
||||
testWidgets('accepts controller for looping', (WidgetTester tester) async {
|
||||
final controller = SplashVideoController(loopVideo: true);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
controller: controller,
|
||||
onVideoError: (_) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(SplashVideoPage), findsOneWidget);
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
testWidgets('controller skip updates state', (WidgetTester tester) async {
|
||||
final controller = SplashVideoController(loopVideo: true);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: SplashVideoPage(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
controller: controller,
|
||||
onVideoError: (_) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(controller.skipRequested, isFalse);
|
||||
await controller.skip();
|
||||
expect(controller.skipRequested, isTrue);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* 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 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:splash_video/splash_video.dart';
|
||||
|
||||
void main() {
|
||||
group('SplashVideoException', () {
|
||||
test('creates with message', () {
|
||||
const exception = SplashVideoException(message: 'Test error');
|
||||
expect(exception.message, equals('Test error'));
|
||||
});
|
||||
|
||||
test('toString includes message', () {
|
||||
const exception = SplashVideoException(message: 'Test error');
|
||||
expect(exception.toString(), contains('Test error'));
|
||||
expect(exception.toString(), contains('SplashVideoException'));
|
||||
});
|
||||
|
||||
test('can be caught as Exception', () {
|
||||
expect(
|
||||
() => throw const SplashVideoException(message: 'Error'),
|
||||
throwsA(isA<Exception>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('FirstWhereOrNullExtension', () {
|
||||
test('returns first matching element', () {
|
||||
final list = [1, 2, 3, 4, 5];
|
||||
final result = list.firstWhereOrNull((e) => e > 3);
|
||||
expect(result, equals(4));
|
||||
});
|
||||
|
||||
test('returns null when no match found', () {
|
||||
final list = [1, 2, 3, 4, 5];
|
||||
final result = list.firstWhereOrNull((e) => e > 10);
|
||||
expect(result, isNull);
|
||||
});
|
||||
|
||||
test('returns first element for always-true predicate', () {
|
||||
final list = [1, 2, 3, 4, 5];
|
||||
final result = list.firstWhereOrNull((e) => true);
|
||||
expect(result, equals(1));
|
||||
});
|
||||
|
||||
test('returns null for empty list', () {
|
||||
final list = <int>[];
|
||||
final result = list.firstWhereOrNull((e) => true);
|
||||
expect(result, isNull);
|
||||
});
|
||||
|
||||
test('works with complex types', () {
|
||||
final list = [
|
||||
{'name': 'Alice', 'age': 30},
|
||||
{'name': 'Bob', 'age': 25},
|
||||
{'name': 'Charlie', 'age': 35},
|
||||
];
|
||||
|
||||
final result = list.firstWhereOrNull((e) => (e['age'] as int) > 30);
|
||||
expect(result, equals({'name': 'Charlie', 'age': 35}));
|
||||
});
|
||||
});
|
||||
|
||||
group('Callback typedefs', () {
|
||||
test('OnSplashDuration can be invoked', () {
|
||||
Duration? capturedDuration;
|
||||
void callback(Duration duration) {
|
||||
capturedDuration = duration;
|
||||
}
|
||||
|
||||
const testDuration = Duration(seconds: 5);
|
||||
callback(testDuration);
|
||||
|
||||
expect(capturedDuration, equals(testDuration));
|
||||
});
|
||||
|
||||
test('WarningCallback can be invoked', () {
|
||||
String? capturedWarning;
|
||||
void callback(String warning) {
|
||||
capturedWarning = warning;
|
||||
}
|
||||
|
||||
const testWarning = 'Test warning message';
|
||||
callback(testWarning);
|
||||
|
||||
expect(capturedWarning, equals(testWarning));
|
||||
});
|
||||
|
||||
test('OnVideoError can be invoked', () {
|
||||
String? capturedError;
|
||||
void callback(String error) {
|
||||
capturedError = error;
|
||||
}
|
||||
|
||||
const testError = 'Video load failed';
|
||||
callback(testError);
|
||||
|
||||
expect(capturedError, equals(testError));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:splash_video/splash_video.dart';
|
||||
|
||||
void main() {
|
||||
group('VideoConfig', () {
|
||||
test('creates with default values', () {
|
||||
const config = VideoConfig();
|
||||
|
||||
expect(config.playImmediately, isTrue);
|
||||
expect(config.videoVisibilityEnum, equals(VisibilityEnum.useFullScreen));
|
||||
expect(config.useSafeArea, isFalse);
|
||||
expect(config.volume, equals(100.0));
|
||||
expect(config.onPlayerInitialized, isNull);
|
||||
});
|
||||
|
||||
test('creates with custom values', () {
|
||||
final config = VideoConfig(
|
||||
playImmediately: false,
|
||||
videoVisibilityEnum: VisibilityEnum.useAspectRatio,
|
||||
useSafeArea: true,
|
||||
volume: 50.0,
|
||||
onPlayerInitialized: (player) {},
|
||||
);
|
||||
|
||||
expect(config.playImmediately, isFalse);
|
||||
expect(config.videoVisibilityEnum, equals(VisibilityEnum.useAspectRatio));
|
||||
expect(config.useSafeArea, isTrue);
|
||||
expect(config.volume, equals(50.0));
|
||||
expect(config.onPlayerInitialized, isNotNull);
|
||||
});
|
||||
|
||||
test('accepts volume range 0-100', () {
|
||||
const config1 = VideoConfig(volume: 0.0);
|
||||
const config2 = VideoConfig(volume: 100.0);
|
||||
const config3 = VideoConfig(volume: 50.0);
|
||||
|
||||
expect(config1.volume, equals(0.0));
|
||||
expect(config2.volume, equals(100.0));
|
||||
expect(config3.volume, equals(50.0));
|
||||
});
|
||||
|
||||
test('onPlayerInitialized callback can be invoked', () {
|
||||
var callbackInvoked = false;
|
||||
Player? capturedPlayer;
|
||||
|
||||
final config = VideoConfig(
|
||||
onPlayerInitialized: (player) {
|
||||
callbackInvoked = true;
|
||||
capturedPlayer = player;
|
||||
},
|
||||
);
|
||||
|
||||
final testPlayer = Player();
|
||||
config.onPlayerInitialized?.call(testPlayer);
|
||||
|
||||
expect(callbackInvoked, isTrue);
|
||||
expect(capturedPlayer, equals(testPlayer));
|
||||
|
||||
testPlayer.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
group('VisibilityEnum', () {
|
||||
test('has all expected values', () {
|
||||
expect(VisibilityEnum.values.length, equals(3));
|
||||
expect(VisibilityEnum.values, contains(VisibilityEnum.useFullScreen));
|
||||
expect(VisibilityEnum.values, contains(VisibilityEnum.useAspectRatio));
|
||||
expect(VisibilityEnum.values, contains(VisibilityEnum.none));
|
||||
});
|
||||
|
||||
test('values are unique', () {
|
||||
final values = VisibilityEnum.values.toSet();
|
||||
expect(values.length, equals(VisibilityEnum.values.length));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:splash_video/splash_video.dart';
|
||||
|
||||
void main() {
|
||||
group('AssetSource', () {
|
||||
test('creates with valid path', () {
|
||||
final source = AssetSource('assets/videos/splash.mp4');
|
||||
expect(source.path, equals('assets/videos/splash.mp4'));
|
||||
});
|
||||
|
||||
test('setSource completes without error', () async {
|
||||
final source = AssetSource('assets/videos/splash.mp4');
|
||||
await expectLater(source.setSource(), completes);
|
||||
});
|
||||
});
|
||||
|
||||
group('DeviceFileSource', () {
|
||||
test('creates with valid path', () {
|
||||
final source = DeviceFileSource('/path/to/video.mp4');
|
||||
expect(source.path, equals('/path/to/video.mp4'));
|
||||
});
|
||||
|
||||
test('setSource creates File object', () async {
|
||||
final source = DeviceFileSource('/path/to/video.mp4');
|
||||
await source.setSource();
|
||||
expect(source.file, isA<File>());
|
||||
expect(source.file.path, equals('/path/to/video.mp4'));
|
||||
});
|
||||
});
|
||||
|
||||
group('NetworkFileSource', () {
|
||||
test('creates with valid URL', () {
|
||||
final source = NetworkFileSource('https://example.com/video.mp4');
|
||||
expect(source.path, equals('https://example.com/video.mp4'));
|
||||
expect(source.url, isNotNull);
|
||||
expect(source.url.toString(), equals('https://example.com/video.mp4'));
|
||||
});
|
||||
|
||||
test('throws exception for invalid URL', () {
|
||||
expect(
|
||||
() => NetworkFileSource('not a valid url :::'),
|
||||
throwsA(isA<SplashVideoException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('handles URLs with query parameters', () {
|
||||
final source = NetworkFileSource(
|
||||
'https://example.com/video.mp4?token=abc123',
|
||||
);
|
||||
expect(source.url, isNotNull);
|
||||
expect(source.url!.queryParameters['token'], equals('abc123'));
|
||||
});
|
||||
|
||||
test('handles URLs with special characters', () {
|
||||
final source = NetworkFileSource(
|
||||
'https://example.com/video%20with%20spaces.mp4',
|
||||
);
|
||||
expect(source.url, isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('BytesSource', () {
|
||||
test('creates with byte data', () {
|
||||
final bytes = Uint8List.fromList([1, 2, 3, 4, 5]);
|
||||
final source = BytesSource(bytes);
|
||||
expect(source.bytes, equals(bytes));
|
||||
});
|
||||
|
||||
test('handles empty bytes', () {
|
||||
final bytes = Uint8List(0);
|
||||
final source = BytesSource(bytes);
|
||||
expect(source.bytes.isEmpty, isTrue);
|
||||
});
|
||||
|
||||
test('handles large byte arrays', () {
|
||||
final bytes = Uint8List(1024 * 1024); // 1MB
|
||||
final source = BytesSource(bytes);
|
||||
expect(source.bytes.length, equals(1024 * 1024));
|
||||
});
|
||||
});
|
||||
|
||||
group('Source type safety', () {
|
||||
test('sealed class prevents external subclassing', () {
|
||||
// This is enforced at compile time, so we just verify types exist
|
||||
final sources = <Source>[
|
||||
AssetSource('test.mp4'),
|
||||
DeviceFileSource('/test.mp4'),
|
||||
NetworkFileSource('https://example.com/test.mp4'),
|
||||
BytesSource(Uint8List(0)),
|
||||
];
|
||||
|
||||
expect(sources.length, equals(4));
|
||||
expect(sources[0], isA<AssetSource>());
|
||||
expect(sources[1], isA<DeviceFileSource>());
|
||||
expect(sources[2], isA<NetworkFileSource>());
|
||||
expect(sources[3], isA<BytesSource>());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
=======================
|
||||
Troubleshooting Guide
|
||||
=======================
|
||||
|
||||
Black Screen Issues
|
||||
===================
|
||||
|
||||
If you're seeing a black screen with no errors:
|
||||
|
||||
1. Run the Diagnostic Tool
|
||||
---------------------------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
cd example
|
||||
flutter run -t lib/diagnostic_main.dart
|
||||
|
||||
This will test MediaKit installation and show detailed logging in the console.
|
||||
|
||||
2. Check Console Output
|
||||
------------------------
|
||||
|
||||
Look for these messages in your console/terminal:
|
||||
|
||||
✅ **Good signs:**
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
✓ SplashVideo: MediaKit initialized and first frame deferred
|
||||
SplashVideoPlayer: Starting initialization...
|
||||
✓ Player created
|
||||
✓ VideoController created
|
||||
✓ Media created, opening...
|
||||
✓ Media opened
|
||||
✓ Video initialized with duration: 0:00:05.000000
|
||||
▶ Starting playback
|
||||
|
||||
❌ **Problem signs:**
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
✗ MediaKit test failed: ...
|
||||
✗ Player error: ...
|
||||
✗ Initialization failed: ...
|
||||
|
||||
3. Verify Dependencies
|
||||
-----------------------
|
||||
|
||||
Make sure your ``pubspec.yaml`` includes the platform-specific libraries:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
dependencies:
|
||||
media_kit: ^1.2.6
|
||||
media_kit_video: ^2.0.1
|
||||
|
||||
# Platform-specific video libraries (add all platforms you support)
|
||||
media_kit_libs_android_video: any # Android
|
||||
media_kit_libs_ios_video: any # iOS
|
||||
media_kit_libs_macos_video: any # macOS
|
||||
media_kit_libs_windows_video: any # Windows
|
||||
media_kit_libs_linux: any # Linux
|
||||
|
||||
4. Verify Asset Configuration
|
||||
------------------------------
|
||||
|
||||
In ``pubspec.yaml``:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
flutter:
|
||||
assets:
|
||||
- assets/videos/ # or your video folder
|
||||
|
||||
Verify the video file exists at the path you're using:
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
AssetSource('assets/videos/splash.mp4') // Must match actual file path
|
||||
|
||||
5. Check MediaKit Initialization
|
||||
---------------------------------
|
||||
|
||||
In your ``main.dart``:
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
SplashVideoPage.initialize(); // This handles MediaKit initialization
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
6. Test MediaKit Programmatically
|
||||
----------------------------------
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Test MediaKit before running app
|
||||
final isWorking = await SplashVideoPage.testMediaKit();
|
||||
if (!isWorking) {
|
||||
debugPrint('MediaKit is not working!');
|
||||
// Handle error...
|
||||
}
|
||||
|
||||
SplashVideoPage.initialize();
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
7. Clean and Rebuild
|
||||
---------------------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
flutter clean
|
||||
flutter pub get
|
||||
flutter run
|
||||
|
||||
8. Platform-Specific Issues
|
||||
----------------------------
|
||||
|
||||
Android
|
||||
^^^^^^^
|
||||
|
||||
- Minimum SDK version must be 21+
|
||||
- Check ``android/app/build.gradle``:
|
||||
|
||||
.. code-block:: gradle
|
||||
|
||||
minSdkVersion 21
|
||||
|
||||
iOS
|
||||
^^^
|
||||
|
||||
- Minimum iOS version must be 13.0+
|
||||
- Check ``ios/Podfile``:
|
||||
|
||||
.. code-block:: ruby
|
||||
|
||||
platform :ios, '13.0'
|
||||
|
||||
macOS
|
||||
^^^^^
|
||||
|
||||
- Minimum macOS version must be 10.15+
|
||||
- Check ``macos/Podfile``:
|
||||
|
||||
.. code-block:: ruby
|
||||
|
||||
platform :osx, '10.15'
|
||||
|
||||
Windows
|
||||
^^^^^^^
|
||||
|
||||
- Visual Studio 2022 or later required
|
||||
- Windows 10 1809+ required
|
||||
|
||||
Linux
|
||||
^^^^^
|
||||
|
||||
- GStreamer and related libraries required
|
||||
- Install:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
|
||||
|
||||
9. Video Format Issues
|
||||
----------------------
|
||||
|
||||
MediaKit supports most video formats, but for best compatibility use:
|
||||
|
||||
- **Container:** MP4
|
||||
- **Video codec:** H.264
|
||||
- **Audio codec:** AAC
|
||||
|
||||
Test with a known working video:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Download a test video
|
||||
curl -o assets/test.mp4 https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/360/Big_Buck_Bunny_360_10s_1MB.mp4
|
||||
|
||||
10. Enable Error Callbacks
|
||||
---------------------------
|
||||
|
||||
Always implement error handling during development:
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
SplashVideoPage(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
onVideoError: (error) {
|
||||
debugPrint('Video Error: $error');
|
||||
// Show error to user or navigate away
|
||||
},
|
||||
nextScreen: const HomeScreen(),
|
||||
)
|
||||
|
||||
11. Check Player State
|
||||
----------------------
|
||||
|
||||
Add a callback to monitor player initialization:
|
||||
|
||||
.. code-block:: dart
|
||||
|
||||
SplashVideoPage(
|
||||
source: AssetSource('assets/videos/splash.mp4'),
|
||||
videoConfig: VideoConfig(
|
||||
onPlayerInitialized: (player) {
|
||||
debugPrint('Player initialized!');
|
||||
debugPrint('Can play: ${player.state.playing}');
|
||||
debugPrint('Duration: ${player.state.duration}');
|
||||
},
|
||||
),
|
||||
nextScreen: const HomeScreen(),
|
||||
)
|
||||
|
||||
Getting Help
|
||||
============
|
||||
|
||||
If you're still experiencing issues:
|
||||
|
||||
1. **Run the diagnostic tool** and share the console output
|
||||
2. **Check the example app** - does it work?
|
||||
3. **Share your setup:**
|
||||
|
||||
- Flutter version (``flutter --version``)
|
||||
- Platform (Android/iOS/macOS/Windows/Linux)
|
||||
- Video file details (format, size, location)
|
||||
- Your ``pubspec.yaml`` dependencies
|
||||
- Console error messages
|
||||
|
||||
4. **Open an issue** with all the above information
|
||||
|
||||
Common Error Messages
|
||||
=====================
|
||||
|
||||
"Failed to initialize video"
|
||||
-----------------------------
|
||||
|
||||
- MediaKit not initialized - make sure you call ``SplashVideoPage.initialize()``
|
||||
- Missing platform libraries - check dependencies
|
||||
|
||||
"URL can't be null when playing a remote video"
|
||||
------------------------------------------------
|
||||
|
||||
- Invalid NetworkFileSource URL
|
||||
- Check URL format and network connectivity
|
||||
|
||||
"Unable to parse URI"
|
||||
---------------------
|
||||
|
||||
- Malformed URL in NetworkFileSource
|
||||
- Verify URL is valid
|
||||
|
||||
Player stays black but no errors
|
||||
---------------------------------
|
||||
|
||||
- Video codec not supported - try H.264/MP4
|
||||
- Asset path incorrect - verify in pubspec.yaml and file system
|
||||
- Platform library missing - check all media_kit_libs_* dependencies
|
||||
- Run diagnostic tool for detailed information
|
||||
Loading…
Reference in New Issue