aves/lib/services/media/media_session_service.dart

139 lines
4.1 KiB
Dart

import 'dart:async';
import 'package:aves/model/entry/entry.dart';
import 'package:aves_utils/aves_utils.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves_video/aves_video.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:get_it/get_it.dart';
abstract class MediaSessionService {
Stream<MediaCommandEvent> get mediaCommands;
Future<void> update({
required AvesEntry entry,
required AvesVideoController controller,
required bool canSkipToNext,
required bool canSkipToPrevious,
});
Future<void> release();
}
class PlatformMediaSessionService implements MediaSessionService, Disposable {
static const _platformObject = MethodChannel('deckers.thibault/aves/media_session');
final List<StreamSubscription> _subscriptions = [];
final EventChannel _mediaCommandChannel = const OptionalEventChannel('deckers.thibault/aves/media_command');
final StreamController _streamController = StreamController.broadcast();
PlatformMediaSessionService() {
_subscriptions.add(_mediaCommandChannel.receiveBroadcastStream().listen((event) => _onMediaCommand(event as Map?)));
}
@override
FutureOr onDispose() {
_subscriptions
..forEach((sub) => sub.cancel())
..clear();
}
@override
Stream<MediaCommandEvent> get mediaCommands => _streamController.stream.where((event) => event is MediaCommandEvent).cast<MediaCommandEvent>();
@override
Future<void> update({
required AvesEntry entry,
required AvesVideoController controller,
required bool canSkipToNext,
required bool canSkipToPrevious,
}) async {
try {
await _platformObject.invokeMethod('update', <String, dynamic>{
'uri': entry.uri,
'title': entry.bestTitle,
'durationMillis': controller.duration,
'state': _toPlatformState(controller.status),
'positionMillis': controller.currentPosition,
'playbackSpeed': controller.speed,
'canSkipToNext': canSkipToNext,
'canSkipToPrevious': canSkipToPrevious,
});
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
}
@override
Future<void> release() async {
try {
await _platformObject.invokeMethod('release');
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
}
String _toPlatformState(VideoStatus status) {
switch (status) {
case VideoStatus.paused:
return 'paused';
case VideoStatus.playing:
return 'playing';
case VideoStatus.idle:
case VideoStatus.initialized:
case VideoStatus.completed:
case VideoStatus.error:
return 'stopped';
}
}
void _onMediaCommand(Map? fields) {
if (fields == null) return;
final command = fields['command'] as String?;
MediaCommandEvent? event;
switch (command) {
case 'play':
event = const MediaCommandEvent(MediaCommand.play);
case 'pause':
event = const MediaCommandEvent(MediaCommand.pause);
case 'skip_to_next':
event = const MediaCommandEvent(MediaCommand.skipToNext);
case 'skip_to_previous':
event = const MediaCommandEvent(MediaCommand.skipToPrevious);
case 'stop':
event = const MediaCommandEvent(MediaCommand.stop);
case 'seek':
final position = fields['position'] as int?;
if (position != null) {
event = MediaSeekCommandEvent(MediaCommand.stop, position: position);
}
}
if (event != null) {
_streamController.add(event);
}
}
}
enum MediaCommand { play, pause, skipToNext, skipToPrevious, stop, seek }
@immutable
class MediaCommandEvent extends Equatable {
final MediaCommand command;
@override
List<Object?> get props => [command];
const MediaCommandEvent(this.command);
}
@immutable
class MediaSeekCommandEvent extends MediaCommandEvent {
final int position;
@override
List<Object?> get props => [...super.props, position];
const MediaSeekCommandEvent(super.command, {required this.position});
}