270 lines
8.4 KiB
Dart
270 lines
8.4 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:reelbox/media/native_player.dart';
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
const channel = MethodChannel('org.reelbox/media');
|
|
setUp(() => debugDefaultTargetPlatformOverride = TargetPlatform.android);
|
|
tearDown(() {
|
|
debugDefaultTargetPlatformOverride = null;
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
|
.setMockMethodCallHandler(channel, null);
|
|
});
|
|
|
|
test('awaits exit and sends exact URL, title and checkpoint', () async {
|
|
final exit = Completer<Map<String, Object>>();
|
|
MethodCall? received;
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
|
.setMockMethodCallHandler(channel, (call) {
|
|
received = call;
|
|
return exit.future;
|
|
});
|
|
var returned = false;
|
|
const url = 'https://media.example.org/a%2Fb.m3u8?sig=a%2Bz&x=1';
|
|
final pending =
|
|
NativePlayerBridge.open(
|
|
url: url,
|
|
title: 'عنوان — episode',
|
|
position: const Duration(milliseconds: 12345),
|
|
autoplay: false,
|
|
externalPackage: 'vlc',
|
|
orientation: 'landscape',
|
|
videoFit: 'cover',
|
|
speed: 1.25,
|
|
subtitleEnabled: false,
|
|
).then((result) {
|
|
returned = true;
|
|
return result;
|
|
});
|
|
await Future<void>.delayed(Duration.zero);
|
|
expect(returned, isFalse);
|
|
expect(received!.method, 'openNativePlayer');
|
|
expect(received!.arguments, containsPair('url', url));
|
|
expect(received!.arguments, containsPair('positionMs', 12345));
|
|
expect(received!.arguments, containsPair('autoplay', false));
|
|
expect(received!.arguments, containsPair('externalPackage', 'vlc'));
|
|
expect(received!.arguments, containsPair('subtitleEnabled', false));
|
|
expect(received!.arguments, containsPair('orientation', 'landscape'));
|
|
expect(received!.arguments, containsPair('videoFit', 'cover'));
|
|
expect(received!.arguments, containsPair('speed', 1.25));
|
|
exit.complete({
|
|
'positionMs': 42000,
|
|
'durationMs': 90000,
|
|
'opened': true,
|
|
'completed': false,
|
|
});
|
|
final result = await pending;
|
|
expect(result.position.inMilliseconds, 42000);
|
|
expect(result.duration.inMilliseconds, 90000);
|
|
expect(result.opened, isTrue);
|
|
expect(result.completed, isFalse);
|
|
});
|
|
|
|
test(
|
|
'local and loopback sources and optional subtitles are unchanged',
|
|
() async {
|
|
final calls = <MethodCall>[];
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
|
.setMockMethodCallHandler(channel, (call) async {
|
|
calls.add(call);
|
|
return {
|
|
'positionMs': 0,
|
|
'durationMs': 0,
|
|
'opened': false,
|
|
'completed': false,
|
|
};
|
|
});
|
|
for (final url in [
|
|
'/data/user/0/org.reelbox.app/files/فیلم.mkv',
|
|
'http://127.0.0.1:49123/media/lease/index.m3u8?token=a%2Bz',
|
|
]) {
|
|
final result = await NativePlayerBridge.open(
|
|
url: url,
|
|
title: 'Film',
|
|
subtitleUrl: 'https://media.example.org/sub.vtt?token=a%2Fz',
|
|
hardwareAcceleration: false,
|
|
pipEnabled: false,
|
|
);
|
|
expect(calls.last.arguments, containsPair('url', url));
|
|
expect(
|
|
calls.last.arguments,
|
|
containsPair(
|
|
'subtitleUrl',
|
|
'https://media.example.org/sub.vtt?token=a%2Fz',
|
|
),
|
|
);
|
|
expect(
|
|
calls.last.arguments,
|
|
containsPair('hardwareAcceleration', false),
|
|
);
|
|
expect(calls.last.arguments, containsPair('pipEnabled', false));
|
|
expect(result.opened, isFalse);
|
|
}
|
|
expect(calls, hasLength(2));
|
|
},
|
|
);
|
|
|
|
test(
|
|
'rejects malformed exit maps instead of manufacturing progress',
|
|
() async {
|
|
for (final response in [
|
|
null,
|
|
{
|
|
'positionMs': -1,
|
|
'durationMs': 10,
|
|
'opened': true,
|
|
'completed': false,
|
|
},
|
|
{
|
|
'positionMs': double.nan,
|
|
'durationMs': 10,
|
|
'opened': true,
|
|
'completed': false,
|
|
},
|
|
{
|
|
'positionMs': 0x7fffffffffffffff,
|
|
'durationMs': 0,
|
|
'opened': true,
|
|
'completed': false,
|
|
},
|
|
{
|
|
'positionMs': 1.5,
|
|
'durationMs': 10,
|
|
'opened': true,
|
|
'completed': false,
|
|
},
|
|
{
|
|
'positionMs': 10,
|
|
'durationMs': -1,
|
|
'opened': true,
|
|
'completed': false,
|
|
},
|
|
{
|
|
'positionMs': 10,
|
|
'durationMs': 20,
|
|
'opened': 'true',
|
|
'completed': true,
|
|
},
|
|
]) {
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
|
.setMockMethodCallHandler(channel, (_) async => response);
|
|
await expectLater(
|
|
NativePlayerBridge.open(
|
|
url: 'https://media.example.org/video.mp4',
|
|
title: 'Film',
|
|
),
|
|
throwsFormatException,
|
|
);
|
|
}
|
|
final bounded = NativePlaybackResult.fromMap({
|
|
'positionMs': 1000,
|
|
'durationMs': 100,
|
|
'opened': false,
|
|
'completed': true,
|
|
});
|
|
expect(bounded.position.inMilliseconds, 100);
|
|
expect(bounded.completed, isFalse);
|
|
},
|
|
);
|
|
|
|
test('completion requires a credible near-end checkpoint, not just EOF', () {
|
|
for (final sample in <Map<String, Object>>[
|
|
{'positionMs': 180000, 'durationMs': 3600000},
|
|
{'positionMs': 180000, 'durationMs': 0},
|
|
{'positionMs': 0, 'durationMs': 3600000},
|
|
{'positionMs': 3600001, 'durationMs': 3600000},
|
|
{'positionMs': 3597000, 'durationMs': 3600000},
|
|
{'positionMs': 3600000, 'durationMs': 3600000, 'opened': false},
|
|
{'positionMs': 3600000, 'durationMs': 3600000, 'completed': false},
|
|
]) {
|
|
final result = NativePlaybackResult.fromMap({
|
|
'opened': true,
|
|
'completed': true,
|
|
...sample,
|
|
});
|
|
expect(result.completed, isFalse, reason: sample.toString());
|
|
}
|
|
for (final position in [3599500, 3600000]) {
|
|
final result = NativePlaybackResult.fromMap({
|
|
'positionMs': position,
|
|
'durationMs': 3600000,
|
|
'opened': true,
|
|
'completed': true,
|
|
});
|
|
expect(result.completed, isTrue);
|
|
}
|
|
});
|
|
|
|
test('platform failures and busy results propagate without retry', () async {
|
|
var calls = 0;
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
|
.setMockMethodCallHandler(channel, (_) async {
|
|
calls++;
|
|
throw PlatformException(code: 'NATIVE_PLAYER_BUSY');
|
|
});
|
|
await expectLater(
|
|
NativePlayerBridge.open(
|
|
url: 'https://media.example.org/video.mp4',
|
|
title: 'Film',
|
|
),
|
|
throwsA(isA<PlatformException>()),
|
|
);
|
|
expect(calls, 1);
|
|
});
|
|
|
|
test('unsupported platforms do not call Android', () async {
|
|
debugDefaultTargetPlatformOverride = TargetPlatform.linux;
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
|
.setMockMethodCallHandler(
|
|
channel,
|
|
(_) async => fail('Android was invoked'),
|
|
);
|
|
await expectLater(
|
|
NativePlayerBridge.open(
|
|
url: 'https://media.example.org/video.mp4',
|
|
title: 'Film',
|
|
),
|
|
throwsUnsupportedError,
|
|
);
|
|
});
|
|
|
|
test('invalid launch preferences fail before starting an activity', () async {
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
|
.setMockMethodCallHandler(channel, (_) async {
|
|
fail('Invalid preferences reached Android');
|
|
});
|
|
await expectLater(
|
|
NativePlayerBridge.open(
|
|
url: 'https://media.example.org/video.mp4',
|
|
title: 'Film',
|
|
speed: double.nan,
|
|
),
|
|
throwsArgumentError,
|
|
);
|
|
await expectLater(
|
|
NativePlayerBridge.open(
|
|
url: 'https://media.example.org/video.mp4',
|
|
title: 'Film',
|
|
orientation: 'invalid',
|
|
),
|
|
throwsArgumentError,
|
|
);
|
|
await expectLater(
|
|
NativePlayerBridge.open(
|
|
url: 'https://media.example.org/video.mp4',
|
|
title: 'Film',
|
|
videoFit: 'invalid',
|
|
),
|
|
throwsArgumentError,
|
|
);
|
|
await expectLater(
|
|
NativePlayerBridge.open(url: '', title: 'Film'),
|
|
throwsArgumentError,
|
|
);
|
|
});
|
|
}
|