max2000_dual_video 0.0.5
max2000_dual_video: ^0.0.5 copied to clipboard
Native synchronized dual-video playback for Flutter on Android (Media3 / ExoPlayer) and iOS (AVFoundation).
example/lib/main.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:max2000_dual_video/max2000_dual_video.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final _engine = Max2000DualVideoEngine();
String _mediaDir = '';
String _statusLine = 'Not loaded yet.';
DualVideoEngineState? _state;
StreamSubscription<DualVideoEngineState>? _stateSubscription;
@override
void initState() {
super.initState();
_engine.getMediaDir().then((dir) {
if (!mounted) return;
setState(() => _mediaDir = dir);
});
_stateSubscription = _engine.stateStream.listen((state) {
if (!mounted) return;
setState(() => _state = state);
});
}
@override
void dispose() {
_stateSubscription?.cancel();
_engine.dispose();
super.dispose();
}
String get _pathA => '$_mediaDir/videoA.mp4';
String get _pathB => '$_mediaDir/videoB.mp4';
Future<void> _load() async {
try {
await _engine.load(pathA: _pathA, pathB: _pathB);
setState(() => _statusLine = 'Loaded A=$_pathA, B=$_pathB');
} catch (e) {
setState(() => _statusLine = 'Load failed: $e');
}
}
Future<void> _play() => _engine.play();
Future<void> _pause() => _engine.pause();
Future<void> _stop() => _engine.stop();
Future<void> _nudgeOffset(int deltaMs) => _engine.nudgeOffsetMs(deltaMs);
Future<void> _setSpeed(double speed) => _engine.setSpeed(speed);
Future<void> _stepFrame(FrameStepDirection direction) =>
_engine.stepFrame(direction);
Widget _playerColumn(
String label,
int? textureId,
PlayerState? playerState,
) {
return Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(label, textAlign: TextAlign.center),
Expanded(
child: Container(
color: Colors.black,
child: Max2000DualVideoPlayerView(textureId: textureId),
),
),
Text(
_playerStatusText(playerState),
style: const TextStyle(fontSize: 12),
),
],
),
);
}
String _playerStatusText(PlayerState? s) {
if (s == null) return 'no status yet';
return 'pos=${s.positionMs}ms dur=${s.durationMs}ms '
'playing=${s.isPlaying} state=${s.playbackState.name} '
'buffering=${s.isBuffering} outOfRange=${s.outOfRange} '
'fps=${s.frameRate?.toStringAsFixed(2) ?? '?'}'
'${s.frameRateKnown ? '' : ' (assumed)'} '
'error=${s.error}';
}
String _engineStatusText() {
final s = _state;
if (s == null) return 'no engine status yet';
return 'master=${s.masterPositionMs}ms playing=${s.isPlaying} '
'speed=${s.speed}x offset=${s.offsetMs}ms drift=${s.driftMs}ms';
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Dual Video Engine')),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'adb push videoA.mp4 "$_pathA"\nadb push videoB.mp4 "$_pathB"',
style: const TextStyle(fontSize: 12),
),
const SizedBox(height: 4),
Text(_statusLine, style: const TextStyle(fontSize: 12)),
Text(_engineStatusText(), style: const TextStyle(fontSize: 12)),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(onPressed: _load, child: const Text('Load')),
ElevatedButton(onPressed: _play, child: const Text('Play')),
ElevatedButton(
onPressed: _pause,
child: const Text('Pause'),
),
ElevatedButton(onPressed: _stop, child: const Text('Stop')),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const Text('Offset:'),
ElevatedButton(
onPressed: () => _nudgeOffset(-33),
child: const Text('-33ms'),
),
ElevatedButton(
onPressed: () => _nudgeOffset(33),
child: const Text('+33ms'),
),
const Text('Frame:'),
ElevatedButton(
onPressed: () => _stepFrame(FrameStepDirection.backward),
child: const Text('<'),
),
ElevatedButton(
onPressed: () => _stepFrame(FrameStepDirection.forward),
child: const Text('>'),
),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const Text('Speed:'),
for (final speed in const [0.25, 0.5, 1.0, 2.0])
ElevatedButton(
onPressed: () => _setSpeed(speed),
child: Text('${speed}x'),
),
],
),
const SizedBox(height: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_playerColumn('Player A', _state?.textureIdA, _state?.playerA),
_playerColumn('Player B', _state?.textureIdB, _state?.playerB),
],
),
),
],
),
),
),
);
}
}