import 'dart:io' show File; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/grpc/bilibili/app/listener/v1.pb.dart' show DetailItem; import 'package:PiliPlus/models_new/download/bili_download_entry_info.dart'; import 'package:PiliPlus/models_new/live/live_room_info_h5/data.dart'; import 'package:PiliPlus/models_new/pgc/pgc_info_model/episode.dart'; import 'package:PiliPlus/models_new/video/video_detail/data.dart'; import 'package:PiliPlus/models_new/video/video_detail/page.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_status.dart'; import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/path_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:audio_service/audio_service.dart'; import 'package:path/path.dart' as path; Future initAudioService() { return AudioService.init( builder: VideoPlayerServiceHandler.new, config: const AudioServiceConfig( androidNotificationChannelId: 'com.example.piliplus.audio', androidNotificationChannelName: 'Audio Service ${Constants.appName}', androidNotificationOngoing: true, androidStopForegroundOnPause: true, fastForwardInterval: Duration(seconds: 10), rewindInterval: Duration(seconds: 10), androidNotificationChannelDescription: 'Media notification channel', androidNotificationIcon: 'drawable/ic_notification_icon', ), ); } class VideoPlayerServiceHandler extends BaseAudioHandler with SeekHandler { static final List _item = []; bool enableBackgroundPlay = Pref.enableBackgroundPlay; Future? Function()? onPlay; Future? Function()? onPause; Future? Function(Duration position)? onSeek; @override Future play() { return onPlay?.call() ?? PlPlayerController.playIfExists() ?? Future.syncValue(null); // player.play(); } @override Future pause() { return onPause?.call() ?? PlPlayerController.pauseIfExists(); // player.pause(); } @override Future seek(Duration position) { playbackState.add( playbackState.value.copyWith( updatePosition: position, ), ); return (onSeek?.call(position) ?? PlPlayerController.seekToIfExists(position, isSeek: false)); // await player.seekTo(position); } void setMediaItem(MediaItem newMediaItem) { if (!enableBackgroundPlay) return; // if (kDebugMode) { // debugPrint("此时调用栈为:"); // debugPrint(newMediaItem); // debugPrint(newMediaItem.title); // debugPrint(StackTrace.current.toString()); // } if (!mediaItem.isClosed) mediaItem.add(newMediaItem); } void setPlaybackState( PlayerStatus status, bool isBuffering, bool isLive, ) { if (!enableBackgroundPlay || _item.isEmpty || !PlPlayerController.instanceExists()) { return; } final AudioProcessingState processingState; if (status.isCompleted) { processingState = AudioProcessingState.completed; } else if (isBuffering) { processingState = AudioProcessingState.buffering; } else { processingState = AudioProcessingState.ready; } final playing = status.isPlaying; playbackState.add( playbackState.value.copyWith( processingState: isBuffering ? AudioProcessingState.buffering : processingState, controls: [ if (!isLive) MediaControl.rewind.copyWith( androidIcon: 'drawable/ic_baseline_replay_10_24', ), if (playing) MediaControl.pause else MediaControl.play, if (!isLive) MediaControl.fastForward.copyWith( androidIcon: 'drawable/ic_baseline_forward_10_24', ), ], playing: playing, systemActions: const { MediaAction.seek, }, ), ); } void onStatusChange(PlayerStatus status, bool isBuffering, isLive) { if (!enableBackgroundPlay) return; if (_item.isEmpty) return; setPlaybackState(status, isBuffering, isLive); } void onVideoDetailChange( dynamic data, int cid, String herotag, { String? artist, String? cover, }) { if (!enableBackgroundPlay) return; // if (kDebugMode) { // debugPrint('当前调用栈为:'); // debugPrint(StackTrace.current); // } if (!PlPlayerController.instanceExists()) return; if (data == null) return; Uri getUri(String? cover) => Uri.parse(ImageUtils.safeThumbnailUrl(cover)); late final id = '$cid$herotag'; final MediaItem mediaItem; switch (data) { case VideoDetailData(:final pages): if (pages != null && pages.length > 1) { final current = pages.firstWhereOrNull((e) => e.cid == cid); mediaItem = MediaItem( id: id, title: current?.part ?? '', artist: data.owner?.name, duration: Duration(seconds: current?.duration ?? 0), artUri: getUri(data.pic), ); } else { mediaItem = MediaItem( id: id, title: data.title ?? '', artist: data.owner?.name, duration: Duration(seconds: data.duration ?? 0), artUri: getUri(data.pic), ); } case EpisodeItem(): mediaItem = MediaItem( id: id, title: data.showTitle ?? data.longTitle ?? data.title ?? '', artist: artist, duration: data.from == 'pugv' ? Duration(seconds: data.duration ?? 0) : Duration(milliseconds: data.duration ?? 0), artUri: getUri(data.cover), ); case RoomInfoH5Data(): mediaItem = MediaItem( id: id, title: data.roomInfo?.title ?? '', artist: data.anchorInfo?.baseInfo?.uname, artUri: getUri(data.roomInfo?.cover), isLive: true, ); case Part(): mediaItem = MediaItem( id: id, title: data.part ?? '', artist: artist, duration: Duration(seconds: data.duration ?? 0), artUri: getUri(cover), ); case DetailItem(:final arc): mediaItem = MediaItem( id: id, title: arc.title, artist: data.owner.name, duration: Duration(seconds: arc.duration.toInt()), artUri: getUri(arc.cover), ); case BiliDownloadEntryInfo(): final coverFile = File( path.join(data.entryDirPath, PathUtils.coverName), ); final uri = coverFile.existsSync() ? coverFile.absolute.uri : getUri(data.cover); mediaItem = MediaItem( id: id, title: data.showTitle, artist: data.ownerName, duration: Duration(milliseconds: data.totalTimeMilli), artUri: uri, ); default: return; } // if (kDebugMode) debugPrint("exist: ${PlPlayerController.instanceExists()}"); if (!PlPlayerController.instanceExists()) return; _item.add(mediaItem); setMediaItem(mediaItem); } void onVideoDetailDispose(String herotag) { if (!enableBackgroundPlay) return; if (_item.isNotEmpty) { _item.removeWhere((item) => item.id.endsWith(herotag)); } if (_item.isNotEmpty) { playbackState.add( playbackState.value.copyWith( processingState: AudioProcessingState.idle, playing: false, ), ); setMediaItem(_item.last); stop(); } } void clear() { if (!enableBackgroundPlay) return; mediaItem.add(null); _item.clear(); /** * if (playbackState.processingState == AudioProcessingState.idle && previousState?.processingState != AudioProcessingState.idle) { await AudioService._stop(); } */ if (playbackState.value.processingState == AudioProcessingState.idle) { playbackState.add( PlaybackState( processingState: AudioProcessingState.completed, playing: false, ), ); } playbackState.add( PlaybackState( processingState: AudioProcessingState.idle, playing: false, ), ); } void onPositionChange(Duration position) { if (!enableBackgroundPlay || _item.isEmpty || !PlPlayerController.instanceExists()) { return; } playbackState.add( playbackState.value.copyWith( updatePosition: position, ), ); } }