[verified] Publish ReelBox source under GPLv3

This commit is contained in:
Hermes committed 2026-11-12 23:41:13 +00:00
commit db2b505ca3
445 files changed
+105274

No files matched your search

+114
View File
@@ -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);
}
}