[verified] Publish ReelBox source under GPLv3
This commit is contained in:
commit
db2b505ca3
445 files changed
+105274
No files matched your search
@@ -0,0 +1,114 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Exit checkpoint from the embedded Android LibVLC activity. `opened` means
|
||||
/// the decoder actually reached Playing, not merely that Android opened a view.
|
||||
class NativePlaybackResult {
|
||||
const NativePlaybackResult({
|
||||
required this.position,
|
||||
required this.duration,
|
||||
required this.opened,
|
||||
required this.completed,
|
||||
});
|
||||
|
||||
final Duration position, duration;
|
||||
final bool opened, completed;
|
||||
|
||||
factory NativePlaybackResult.fromMap(Map<dynamic, dynamic> value) {
|
||||
int milliseconds(String key) {
|
||||
final raw = value[key];
|
||||
// Duration stores microseconds in a signed 64-bit integer on Android.
|
||||
if (raw is! int ||
|
||||
raw < 0 ||
|
||||
raw > 0x7fffffffffffffff ~/ Duration.microsecondsPerMillisecond) {
|
||||
throw FormatException('Invalid native playback $key');
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
final position = milliseconds('positionMs');
|
||||
final duration = milliseconds('durationMs');
|
||||
if (value['opened'] is! bool || value['completed'] is! bool) {
|
||||
throw const FormatException('Invalid native playback flags');
|
||||
}
|
||||
// An EOF flag alone must not advance the playlist. Validate the raw timing
|
||||
// before its display bounds are applied, including unknown-duration input.
|
||||
final atEnd =
|
||||
duration > 0 &&
|
||||
position > 0 &&
|
||||
position <= duration &&
|
||||
duration - position <= (duration ~/ 50).clamp(1, 2000);
|
||||
return NativePlaybackResult(
|
||||
position: Duration(
|
||||
milliseconds: duration > 0 && position > duration ? duration : position,
|
||||
),
|
||||
duration: Duration(milliseconds: duration),
|
||||
opened: value['opened'] as bool,
|
||||
completed: value['completed'] == true && value['opened'] == true && atEnd,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Standalone native player, independent of the Flutter PlayerPage/decoder.
|
||||
///
|
||||
/// The future resolves ONLY when the activity exits. Keep any offline loopback
|
||||
/// server lease alive until then; source URLs are forwarded byte-for-byte.
|
||||
/// Callers own history persistence, ID association and local lease cleanup.
|
||||
/// Validation/platform failures may throw. If Android cannot launch the activity,
|
||||
/// the result has `opened: false`; no path fabricates successful playback.
|
||||
class NativePlayerBridge {
|
||||
static const _channel = MethodChannel('org.reelbox/media');
|
||||
|
||||
static bool get supported =>
|
||||
!kIsWeb && defaultTargetPlatform == TargetPlatform.android;
|
||||
|
||||
static Future<NativePlaybackResult> open({
|
||||
required String url,
|
||||
required String title,
|
||||
Duration position = Duration.zero,
|
||||
bool autoplay = true,
|
||||
String externalPackage = 'ask',
|
||||
String orientation = 'auto',
|
||||
String videoFit = 'contain',
|
||||
double speed = 1.0,
|
||||
bool subtitleEnabled = true,
|
||||
String? subtitleUrl,
|
||||
bool hardwareAcceleration = true,
|
||||
bool pipEnabled = true,
|
||||
}) async {
|
||||
if (!supported) {
|
||||
throw UnsupportedError('The embedded native player requires Android.');
|
||||
}
|
||||
if (url.trim().isEmpty) throw ArgumentError.value(url, 'url');
|
||||
if (!speed.isFinite || speed < .25 || speed > 3) {
|
||||
throw ArgumentError.value(speed, 'speed', 'Must be between 0.25 and 3');
|
||||
}
|
||||
if (!const ['auto', 'portrait', 'landscape'].contains(orientation)) {
|
||||
throw ArgumentError.value(orientation, 'orientation');
|
||||
}
|
||||
if (!const ['contain', 'cover', 'fill'].contains(videoFit)) {
|
||||
throw ArgumentError.value(videoFit, 'videoFit');
|
||||
}
|
||||
final response = await _channel.invokeMapMethod<dynamic, dynamic>(
|
||||
'openNativePlayer',
|
||||
{
|
||||
'url': url,
|
||||
'title': title,
|
||||
'positionMs': position.inMilliseconds < 0 ? 0 : position.inMilliseconds,
|
||||
'autoplay': autoplay,
|
||||
'externalPackage': externalPackage,
|
||||
'orientation': orientation,
|
||||
'videoFit': videoFit,
|
||||
'speed': speed,
|
||||
'subtitleEnabled': subtitleEnabled,
|
||||
'subtitleUrl': ?subtitleUrl,
|
||||
'hardwareAcceleration': hardwareAcceleration,
|
||||
'pipEnabled': pipEnabled,
|
||||
},
|
||||
);
|
||||
if (response == null) {
|
||||
throw const FormatException('Native player returned no exit checkpoint');
|
||||
}
|
||||
return NativePlaybackResult.fromMap(response);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user