#437 tv: media session
This commit is contained in:
parent
ad86eacd39
commit
aa3397ad8a
6 changed files with 214 additions and 0 deletions
|
@ -183,6 +183,7 @@ dependencies {
|
||||||
implementation 'androidx.core:core-ktx:1.9.0'
|
implementation 'androidx.core:core-ktx:1.9.0'
|
||||||
implementation 'androidx.exifinterface:exifinterface:1.3.5'
|
implementation 'androidx.exifinterface:exifinterface:1.3.5'
|
||||||
implementation 'androidx.lifecycle:lifecycle-process:2.5.1'
|
implementation 'androidx.lifecycle:lifecycle-process:2.5.1'
|
||||||
|
implementation 'androidx.media:media:1.6.0'
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
|
|
||||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||||
|
|
|
@ -83,6 +83,7 @@ open class MainActivity : FlutterActivity() {
|
||||||
MethodChannel(messenger, HomeWidgetHandler.CHANNEL).setMethodCallHandler(HomeWidgetHandler(this))
|
MethodChannel(messenger, HomeWidgetHandler.CHANNEL).setMethodCallHandler(HomeWidgetHandler(this))
|
||||||
MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(this))
|
MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(this))
|
||||||
MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(this))
|
MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(this))
|
||||||
|
MethodChannel(messenger, MediaSessionHandler.CHANNEL).setMethodCallHandler(MediaSessionHandler(this))
|
||||||
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
|
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
|
||||||
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
|
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
|
||||||
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
|
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
|
||||||
|
|
|
@ -0,0 +1,144 @@
|
||||||
|
package deckers.thibault.aves.channel.calls
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.media.session.PlaybackState
|
||||||
|
import android.net.Uri
|
||||||
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
|
import android.util.Log
|
||||||
|
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||||
|
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
|
||||||
|
import deckers.thibault.aves.utils.FlutterUtils
|
||||||
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
|
import io.flutter.plugin.common.MethodCall
|
||||||
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class MediaSessionHandler(private val context: Context) : MethodCallHandler {
|
||||||
|
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
|
private val sessions = HashMap<Uri, MediaSessionCompat>()
|
||||||
|
|
||||||
|
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
when (call.method) {
|
||||||
|
"update" -> ioScope.launch { safeSuspend(call, result, ::update) }
|
||||||
|
"release" -> ioScope.launch { safe(call, result, ::release) }
|
||||||
|
else -> result.notImplemented()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun update(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
|
val title = call.argument<String>("title")
|
||||||
|
val durationMillis = call.argument<Number>("durationMillis")?.toLong()
|
||||||
|
val stateString = call.argument<String>("state")
|
||||||
|
val positionMillis = call.argument<Number>("positionMillis")?.toLong()
|
||||||
|
val playbackSpeed = call.argument<Number>("playbackSpeed")?.toFloat()
|
||||||
|
|
||||||
|
if (uri == null || title == null || durationMillis == null || stateString == null || positionMillis == null || playbackSpeed == null) {
|
||||||
|
result.error("update-args", "missing arguments", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val state = when (stateString) {
|
||||||
|
STATE_STOPPED -> PlaybackStateCompat.STATE_STOPPED
|
||||||
|
STATE_PAUSED -> PlaybackStateCompat.STATE_PAUSED
|
||||||
|
STATE_PLAYING -> PlaybackStateCompat.STATE_PLAYING
|
||||||
|
else -> {
|
||||||
|
result.error("update-state", "unknown state=$stateString", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var actions = PlaybackStateCompat.ACTION_PLAY_PAUSE or PlaybackStateCompat.ACTION_SEEK_TO
|
||||||
|
actions = if (state == PlaybackState.STATE_PLAYING) {
|
||||||
|
actions or PlaybackStateCompat.ACTION_PAUSE or PlaybackStateCompat.ACTION_STOP
|
||||||
|
} else {
|
||||||
|
actions or PlaybackStateCompat.ACTION_PLAY
|
||||||
|
}
|
||||||
|
|
||||||
|
val playbackState = PlaybackStateCompat.Builder()
|
||||||
|
.setState(
|
||||||
|
state,
|
||||||
|
positionMillis,
|
||||||
|
playbackSpeed,
|
||||||
|
System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
.setActions(actions)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
var session = sessions[uri]
|
||||||
|
if (session == null) {
|
||||||
|
session = MediaSessionCompat(context, "aves-$uri")
|
||||||
|
sessions[uri] = session
|
||||||
|
|
||||||
|
val metadata = MediaMetadataCompat.Builder()
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
|
||||||
|
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, durationMillis)
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, uri.toString())
|
||||||
|
.build()
|
||||||
|
session.setMetadata(metadata)
|
||||||
|
|
||||||
|
val callback: MediaSessionCompat.Callback = object : MediaSessionCompat.Callback() {
|
||||||
|
override fun onPlay() {
|
||||||
|
super.onPlay()
|
||||||
|
Log.d(LOG_TAG, "TLAD onPlay uri=$uri")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
Log.d(LOG_TAG, "TLAD onPause uri=$uri")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
Log.d(LOG_TAG, "TLAD onStop uri=$uri")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSeekTo(pos: Long) {
|
||||||
|
super.onSeekTo(pos)
|
||||||
|
Log.d(LOG_TAG, "TLAD onSeekTo uri=$uri pos=$pos")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FlutterUtils.runOnUiThread {
|
||||||
|
session.setCallback(callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
session.setPlaybackState(playbackState)
|
||||||
|
|
||||||
|
if (!session.isActive) {
|
||||||
|
session.isActive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
result.success(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun release(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
|
|
||||||
|
if (uri == null) {
|
||||||
|
result.error("release-args", "missing arguments", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessions[uri]?.release()
|
||||||
|
|
||||||
|
result.success(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val LOG_TAG = LogUtils.createTag<MediaSessionHandler>()
|
||||||
|
const val CHANNEL = "deckers.thibault/aves/media_session"
|
||||||
|
|
||||||
|
const val STATE_STOPPED = "stopped"
|
||||||
|
const val STATE_PAUSED = "paused"
|
||||||
|
const val STATE_PLAYING = "playing"
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import 'package:aves/services/device_service.dart';
|
||||||
import 'package:aves/services/media/embedded_data_service.dart';
|
import 'package:aves/services/media/embedded_data_service.dart';
|
||||||
import 'package:aves/services/media/media_edit_service.dart';
|
import 'package:aves/services/media/media_edit_service.dart';
|
||||||
import 'package:aves/services/media/media_fetch_service.dart';
|
import 'package:aves/services/media/media_fetch_service.dart';
|
||||||
|
import 'package:aves/services/media/media_session_service.dart';
|
||||||
import 'package:aves/services/media/media_store_service.dart';
|
import 'package:aves/services/media/media_store_service.dart';
|
||||||
import 'package:aves/services/metadata/metadata_edit_service.dart';
|
import 'package:aves/services/metadata/metadata_edit_service.dart';
|
||||||
import 'package:aves/services/metadata/metadata_fetch_service.dart';
|
import 'package:aves/services/metadata/metadata_fetch_service.dart';
|
||||||
|
@ -34,6 +35,7 @@ final DeviceService deviceService = getIt<DeviceService>();
|
||||||
final EmbeddedDataService embeddedDataService = getIt<EmbeddedDataService>();
|
final EmbeddedDataService embeddedDataService = getIt<EmbeddedDataService>();
|
||||||
final MediaEditService mediaEditService = getIt<MediaEditService>();
|
final MediaEditService mediaEditService = getIt<MediaEditService>();
|
||||||
final MediaFetchService mediaFetchService = getIt<MediaFetchService>();
|
final MediaFetchService mediaFetchService = getIt<MediaFetchService>();
|
||||||
|
final MediaSessionService mediaSessionService = getIt<MediaSessionService>();
|
||||||
final MediaStoreService mediaStoreService = getIt<MediaStoreService>();
|
final MediaStoreService mediaStoreService = getIt<MediaStoreService>();
|
||||||
final MetadataEditService metadataEditService = getIt<MetadataEditService>();
|
final MetadataEditService metadataEditService = getIt<MetadataEditService>();
|
||||||
final MetadataFetchService metadataFetchService = getIt<MetadataFetchService>();
|
final MetadataFetchService metadataFetchService = getIt<MetadataFetchService>();
|
||||||
|
@ -52,6 +54,7 @@ void initPlatformServices() {
|
||||||
getIt.registerLazySingleton<EmbeddedDataService>(PlatformEmbeddedDataService.new);
|
getIt.registerLazySingleton<EmbeddedDataService>(PlatformEmbeddedDataService.new);
|
||||||
getIt.registerLazySingleton<MediaEditService>(PlatformMediaEditService.new);
|
getIt.registerLazySingleton<MediaEditService>(PlatformMediaEditService.new);
|
||||||
getIt.registerLazySingleton<MediaFetchService>(PlatformMediaFetchService.new);
|
getIt.registerLazySingleton<MediaFetchService>(PlatformMediaFetchService.new);
|
||||||
|
getIt.registerLazySingleton<MediaSessionService>(PlatformMediaSessionService.new);
|
||||||
getIt.registerLazySingleton<MediaStoreService>(PlatformMediaStoreService.new);
|
getIt.registerLazySingleton<MediaStoreService>(PlatformMediaStoreService.new);
|
||||||
getIt.registerLazySingleton<MetadataEditService>(PlatformMetadataEditService.new);
|
getIt.registerLazySingleton<MetadataEditService>(PlatformMetadataEditService.new);
|
||||||
getIt.registerLazySingleton<MetadataFetchService>(PlatformMetadataFetchService.new);
|
getIt.registerLazySingleton<MetadataFetchService>(PlatformMetadataFetchService.new);
|
||||||
|
|
57
lib/services/media/media_session_service.dart
Normal file
57
lib/services/media/media_session_service.dart
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:aves/services/common/services.dart';
|
||||||
|
import 'package:aves/widgets/viewer/video/controller.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
abstract class MediaSessionService {
|
||||||
|
Future<void> update(AvesVideoController controller);
|
||||||
|
|
||||||
|
Future<void> release(String uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlatformMediaSessionService implements MediaSessionService {
|
||||||
|
static const _platformObject = MethodChannel('deckers.thibault/aves/media_session');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> update(AvesVideoController controller) async {
|
||||||
|
final entry = controller.entry;
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
} on PlatformException catch (e, stack) {
|
||||||
|
await reportService.recordError(e, stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> release(String uri) async {
|
||||||
|
try {
|
||||||
|
await _platformObject.invokeMethod('release', <String, dynamic>{
|
||||||
|
'uri': uri,
|
||||||
|
});
|
||||||
|
} 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/video_playback.dart';
|
import 'package:aves/model/video_playback.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
|
@ -8,6 +10,7 @@ import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
abstract class AvesVideoController {
|
abstract class AvesVideoController {
|
||||||
|
final List<StreamSubscription> _subscriptions = [];
|
||||||
final AvesEntry _entry;
|
final AvesEntry _entry;
|
||||||
final bool persistPlayback;
|
final bool persistPlayback;
|
||||||
|
|
||||||
|
@ -19,10 +22,15 @@ abstract class AvesVideoController {
|
||||||
|
|
||||||
AvesVideoController(AvesEntry entry, {required this.persistPlayback}) : _entry = entry {
|
AvesVideoController(AvesEntry entry, {required this.persistPlayback}) : _entry = entry {
|
||||||
entry.visualChangeNotifier.addListener(onVisualChanged);
|
entry.visualChangeNotifier.addListener(onVisualChanged);
|
||||||
|
_subscriptions.add(statusStream.listen((event) => mediaSessionService.update(this)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@mustCallSuper
|
@mustCallSuper
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
|
_subscriptions
|
||||||
|
..forEach((sub) => sub.cancel())
|
||||||
|
..clear();
|
||||||
|
await mediaSessionService.release(entry.uri);
|
||||||
entry.visualChangeNotifier.removeListener(onVisualChanged);
|
entry.visualChangeNotifier.removeListener(onVisualChanged);
|
||||||
await _savePlaybackState();
|
await _savePlaybackState();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue