124 lines
3.5 KiB
Dart
124 lines
3.5 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:plezy/media/playback_report_metadata.dart';
|
|
import 'package:plezy/services/jellyfin_client.dart';
|
|
import 'package:plezy/services/live_session_tracker.dart';
|
|
|
|
class _FakeJellyfinClient implements JellyfinClient {
|
|
final calls = <String>[];
|
|
final startGate = Completer<void>();
|
|
|
|
@override
|
|
Future<void> reportPlaybackStarted({
|
|
required String itemId,
|
|
required Duration position,
|
|
Duration? duration,
|
|
String? playSessionId,
|
|
String? playMethod,
|
|
String? liveStreamId,
|
|
String? mediaSourceId,
|
|
int? audioStreamIndex,
|
|
int? subtitleStreamIndex,
|
|
}) async {
|
|
await startGate.future;
|
|
calls.add('started:$itemId:$playSessionId:$mediaSourceId:$liveStreamId');
|
|
}
|
|
|
|
@override
|
|
Future<void> reportPlaybackProgress({
|
|
required String itemId,
|
|
required Duration position,
|
|
required Duration duration,
|
|
bool isPaused = false,
|
|
String? playSessionId,
|
|
String? playMethod,
|
|
String? liveStreamId,
|
|
String? mediaSourceId,
|
|
int? audioStreamIndex,
|
|
int? subtitleStreamIndex,
|
|
}) async {
|
|
calls.add('${isPaused ? 'paused' : 'playing'}:$itemId:$playSessionId:$mediaSourceId:$liveStreamId');
|
|
}
|
|
|
|
@override
|
|
Future<void> reportPlaybackStopped({
|
|
required String itemId,
|
|
required Duration position,
|
|
Duration? duration,
|
|
String? playSessionId,
|
|
String? liveStreamId,
|
|
String? mediaSourceId,
|
|
PlaybackReportMetadata report = const PlaybackReportMetadata.live(),
|
|
}) async {
|
|
calls.add('stopped:$itemId:$playSessionId:$mediaSourceId:$liveStreamId');
|
|
}
|
|
|
|
@override
|
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
|
}
|
|
|
|
void main() {
|
|
test('coalesces duplicate live starts and orders stop after in-flight start', () async {
|
|
final client = _FakeJellyfinClient();
|
|
final tracker = JellyfinLiveSessionTracker(
|
|
playSessionId: 'live-session-1',
|
|
mediaSourceId: 'source-1',
|
|
liveStreamId: 'live-stream-1',
|
|
);
|
|
|
|
final first = tracker.report(
|
|
client: client,
|
|
itemId: 'channel-1',
|
|
state: 'playing',
|
|
position: Duration.zero,
|
|
duration: Duration.zero,
|
|
);
|
|
final second = tracker.report(
|
|
client: client,
|
|
itemId: 'channel-1',
|
|
state: 'playing',
|
|
position: const Duration(seconds: 1),
|
|
duration: Duration.zero,
|
|
);
|
|
await Future<void>.delayed(Duration.zero);
|
|
final stopped = tracker.report(
|
|
client: client,
|
|
itemId: 'channel-1',
|
|
state: 'stopped',
|
|
position: const Duration(seconds: 2),
|
|
duration: Duration.zero,
|
|
);
|
|
|
|
await Future<void>.delayed(Duration.zero);
|
|
expect(client.calls, isEmpty);
|
|
|
|
client.startGate.complete();
|
|
await Future.wait([first, second, stopped]);
|
|
|
|
expect(client.calls, [
|
|
'started:channel-1:live-session-1:source-1:live-stream-1',
|
|
'stopped:channel-1:live-session-1:source-1:live-stream-1',
|
|
]);
|
|
});
|
|
|
|
test('stopping before the first heartbeat still reports the negotiated live identity', () async {
|
|
final client = _FakeJellyfinClient();
|
|
final tracker = JellyfinLiveSessionTracker(
|
|
playSessionId: 'live-session-1',
|
|
mediaSourceId: 'source-1',
|
|
liveStreamId: 'live-stream-1',
|
|
);
|
|
|
|
await tracker.report(
|
|
client: client,
|
|
itemId: 'channel-1',
|
|
state: 'stopped',
|
|
position: Duration.zero,
|
|
duration: Duration.zero,
|
|
);
|
|
|
|
expect(client.calls, ['stopped:channel-1:live-session-1:source-1:live-stream-1']);
|
|
});
|
|
}
|