Merge branch 'main' into fix
This commit is contained in:
@ -16,6 +16,7 @@ import 'package:pilipala/pages/search/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/index.dart';
|
||||
import 'package:pilipala/router/app_pages.dart';
|
||||
import 'package:pilipala/pages/main/view.dart';
|
||||
import 'package:pilipala/services/service_locator.dart';
|
||||
import 'package:pilipala/utils/app_scheme.dart';
|
||||
import 'package:pilipala/utils/data.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
@ -28,6 +29,7 @@ void main() async {
|
||||
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown])
|
||||
.then((_) async {
|
||||
await GStrorage.init();
|
||||
await setupServiceLocator();
|
||||
runApp(const MyApp());
|
||||
// 小白条、导航栏沉浸
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
|
||||
@ -3,6 +3,7 @@ import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/models/video/play/quality.dart';
|
||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||
import 'package:pilipala/services/service_locator.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
import 'widgets/switch_item.dart';
|
||||
@ -37,6 +38,14 @@ class _PlaySettingState extends State<PlaySetting> {
|
||||
defaultValue: BtmProgresBehavior.values.first.code);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
|
||||
// 重新验证媒体通知后台播放设置
|
||||
videoPlayerServiceHandler.revalidateSetting();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;
|
||||
|
||||
@ -12,6 +12,7 @@ import 'package:pilipala/common/widgets/stat/view.dart';
|
||||
import 'package:pilipala/models/video_detail_res.dart';
|
||||
import 'package:pilipala/pages/video/detail/introduction/controller.dart';
|
||||
import 'package:pilipala/pages/video/detail/widgets/ai_detail.dart';
|
||||
import 'package:pilipala/services/service_locator.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
@ -18,6 +18,7 @@ import 'package:pilipala/pages/video/detail/introduction/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/related/index.dart';
|
||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
|
||||
import 'package:pilipala/services/service_locator.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
import 'widgets/header_control.dart';
|
||||
@ -61,7 +62,19 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
heroTag = Get.arguments['heroTag'];
|
||||
videoDetailController = Get.put(VideoDetailController(), tag: heroTag);
|
||||
videoIntroController = Get.put(VideoIntroController(), tag: heroTag);
|
||||
videoIntroController.videoDetail.listen((value) {
|
||||
videoPlayerServiceHandler.onVideoDetailChange(
|
||||
value, videoDetailController.cid.value);
|
||||
});
|
||||
bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);
|
||||
bangumiIntroController.bangumiDetail.listen((value) {
|
||||
videoPlayerServiceHandler.onVideoDetailChange(
|
||||
value, videoDetailController.cid.value);
|
||||
});
|
||||
videoDetailController.cid.listen((p0) {
|
||||
videoPlayerServiceHandler.onVideoDetailChange(
|
||||
bangumiIntroController.bangumiDetail.value, p0);
|
||||
});
|
||||
statusBarHeight = localCache.get('statusBarHeight');
|
||||
autoExitFullcreen =
|
||||
setting.get(SettingBoxKey.enableAutoExit, defaultValue: false);
|
||||
@ -154,6 +167,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
if (videoDetailController.floating != null) {
|
||||
videoDetailController.floating!.dispose();
|
||||
}
|
||||
videoPlayerServiceHandler.onVideoDetailDispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
floating.dispose();
|
||||
super.dispose();
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:flutter_volume_controller/flutter_volume_controller.dart';
|
||||
@ -14,6 +15,7 @@ import 'package:ns_danmaku/ns_danmaku.dart';
|
||||
import 'package:pilipala/http/video.dart';
|
||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
|
||||
import 'package:pilipala/services/service_locator.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:screen_brightness/screen_brightness.dart';
|
||||
@ -526,12 +528,24 @@ class PlPlayerController {
|
||||
}),
|
||||
videoPlayerController!.stream.buffering.listen((event) {
|
||||
isBuffering.value = event;
|
||||
videoPlayerServiceHandler.onStatusChange(
|
||||
playerStatus.status.value, event);
|
||||
}),
|
||||
// videoPlayerController!.stream.volume.listen((event) {
|
||||
// if (!mute.value && _volumeBeforeMute != event) {
|
||||
// _volumeBeforeMute = event / 100;
|
||||
// }
|
||||
// }),
|
||||
// 媒体通知监听
|
||||
onPlayerStatusChanged.listen((event) {
|
||||
videoPlayerServiceHandler.onStatusChange(event, isBuffering.value);
|
||||
}),
|
||||
onPositionChanged.listen((event) {
|
||||
EasyThrottle.throttle(
|
||||
'mediaServicePositon',
|
||||
const Duration(seconds: 1),
|
||||
() => videoPlayerServiceHandler.onPositionChange(event));
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -632,6 +646,7 @@ class PlPlayerController {
|
||||
|
||||
playerStatus.status.value = PlayerStatus.playing;
|
||||
// screenManager.setOverlays(false);
|
||||
audioSessionHandler.setActive(true);
|
||||
}
|
||||
|
||||
/// 暂停播放
|
||||
@ -1007,6 +1022,7 @@ class PlPlayerController {
|
||||
_instance = null;
|
||||
// 关闭所有视频页面恢复亮度
|
||||
resetBrightness();
|
||||
videoPlayerServiceHandler.clear();
|
||||
} catch (err) {
|
||||
print(err);
|
||||
}
|
||||
|
||||
180
lib/services/audio_handler.dart
Normal file
180
lib/services/audio_handler.dart
Normal file
@ -0,0 +1,180 @@
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/models/bangumi/info.dart';
|
||||
import 'package:pilipala/models/video_detail_res.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
Future<VideoPlayerServiceHandler> initAudioService() async {
|
||||
return await AudioService.init(
|
||||
builder: () => VideoPlayerServiceHandler(),
|
||||
config: const AudioServiceConfig(
|
||||
androidNotificationChannelId: 'com.guozhigq.pilipala.audio',
|
||||
androidNotificationChannelName: 'Audio Service Pilipala',
|
||||
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<MediaItem> _item = [];
|
||||
Box setting = GStrorage.setting;
|
||||
bool enableBackgroundPlay = false;
|
||||
|
||||
VideoPlayerServiceHandler() {
|
||||
revalidateSetting();
|
||||
}
|
||||
|
||||
revalidateSetting() {
|
||||
enableBackgroundPlay =
|
||||
setting.get(SettingBoxKey.enableBackgroundPlay, defaultValue: false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> play() async {
|
||||
PlPlayerController.getInstance().play();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pause() async {
|
||||
PlPlayerController.getInstance().pause();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> seek(Duration position) async {
|
||||
playbackState.add(playbackState.value.copyWith(
|
||||
updatePosition: position,
|
||||
));
|
||||
await PlPlayerController.getInstance().seekTo(position);
|
||||
}
|
||||
|
||||
Future<void> setMediaItem(MediaItem newMediaItem) async {
|
||||
if (!enableBackgroundPlay) return;
|
||||
mediaItem.add(newMediaItem);
|
||||
}
|
||||
|
||||
Future<void> setPlaybackState(PlayerStatus status, bool isBuffering) async {
|
||||
if (!enableBackgroundPlay) return;
|
||||
|
||||
final AudioProcessingState processingState;
|
||||
final playing = status == PlayerStatus.playing;
|
||||
if (status == PlayerStatus.completed) {
|
||||
processingState = AudioProcessingState.completed;
|
||||
} else if (isBuffering) {
|
||||
processingState = AudioProcessingState.buffering;
|
||||
} else {
|
||||
processingState = AudioProcessingState.ready;
|
||||
}
|
||||
|
||||
playbackState.add(playbackState.value.copyWith(
|
||||
processingState:
|
||||
isBuffering ? AudioProcessingState.buffering : processingState,
|
||||
controls: [
|
||||
MediaControl.rewind
|
||||
.copyWith(androidIcon: 'drawable/ic_baseline_replay_10_24'),
|
||||
if (playing) MediaControl.pause else MediaControl.play,
|
||||
MediaControl.fastForward
|
||||
.copyWith(androidIcon: 'drawable/ic_baseline_forward_10_24'),
|
||||
],
|
||||
playing: playing,
|
||||
systemActions: const {
|
||||
MediaAction.seek,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
onStatusChange(PlayerStatus status, bool isBuffering) {
|
||||
if (!enableBackgroundPlay) return;
|
||||
|
||||
if (_item.isEmpty) return;
|
||||
setPlaybackState(status, isBuffering);
|
||||
}
|
||||
|
||||
onVideoDetailChange(dynamic data, int cid) {
|
||||
if (!enableBackgroundPlay) return;
|
||||
|
||||
if (data == null) return;
|
||||
Map argMap = Get.arguments;
|
||||
final heroTag = argMap['heroTag'];
|
||||
|
||||
late MediaItem? mediaItem;
|
||||
if (data is VideoDetailData) {
|
||||
if ((data.pages?.length ?? 0) > 1) {
|
||||
final current = data.pages?.firstWhere((element) => element.cid == cid);
|
||||
mediaItem = MediaItem(
|
||||
id: heroTag,
|
||||
title: current?.pagePart ?? "",
|
||||
artist: data.title ?? "",
|
||||
album: data.title ?? "",
|
||||
duration: Duration(seconds: current?.duration ?? 0),
|
||||
artUri: Uri.parse(data.pic ?? ""),
|
||||
);
|
||||
} else {
|
||||
mediaItem = MediaItem(
|
||||
id: heroTag,
|
||||
title: data.title ?? "",
|
||||
artist: data.owner?.name ?? "",
|
||||
duration: Duration(seconds: data.duration ?? 0),
|
||||
artUri: Uri.parse(data.pic ?? ""),
|
||||
);
|
||||
}
|
||||
} else if (data is BangumiInfoModel) {
|
||||
final current =
|
||||
data.episodes?.firstWhere((element) => element.cid == cid);
|
||||
mediaItem = MediaItem(
|
||||
id: heroTag,
|
||||
title: current?.longTitle ?? "",
|
||||
artist: data.title ?? "",
|
||||
duration: Duration(milliseconds: current?.duration ?? 0),
|
||||
artUri: Uri.parse(data.cover ?? ""),
|
||||
);
|
||||
}
|
||||
if (mediaItem == null) return;
|
||||
setMediaItem(mediaItem);
|
||||
_item.add(mediaItem);
|
||||
}
|
||||
|
||||
onVideoDetailDispose() {
|
||||
if (!enableBackgroundPlay) return;
|
||||
|
||||
playbackState.add(playbackState.value.copyWith(
|
||||
processingState: AudioProcessingState.idle,
|
||||
playing: false,
|
||||
));
|
||||
_item.removeLast();
|
||||
if (_item.isNotEmpty) {
|
||||
setMediaItem(_item.last);
|
||||
}
|
||||
if (_item.isEmpty) {
|
||||
playbackState
|
||||
.add(playbackState.value.copyWith(updatePosition: Duration.zero));
|
||||
}
|
||||
stop();
|
||||
}
|
||||
|
||||
clear() {
|
||||
if (!enableBackgroundPlay) return;
|
||||
|
||||
mediaItem.add(null);
|
||||
playbackState.add(PlaybackState(
|
||||
processingState: AudioProcessingState.idle,
|
||||
playing: false,
|
||||
));
|
||||
_item.clear();
|
||||
stop();
|
||||
}
|
||||
|
||||
onPositionChange(Duration position) {
|
||||
if (!enableBackgroundPlay) return;
|
||||
|
||||
playbackState.add(playbackState.value.copyWith(
|
||||
updatePosition: position,
|
||||
));
|
||||
}
|
||||
}
|
||||
53
lib/services/audio_session.dart
Normal file
53
lib/services/audio_session.dart
Normal file
@ -0,0 +1,53 @@
|
||||
import 'package:audio_session/audio_session.dart';
|
||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||
|
||||
class AudioSessionHandler {
|
||||
late AudioSession session;
|
||||
bool _playInterrupted = false;
|
||||
|
||||
setActive(bool active) {
|
||||
session.setActive(active);
|
||||
}
|
||||
|
||||
AudioSessionHandler() {
|
||||
initSession();
|
||||
}
|
||||
|
||||
Future<void> initSession() async {
|
||||
session = await AudioSession.instance;
|
||||
session.configure(const AudioSessionConfiguration.music());
|
||||
|
||||
session.interruptionEventStream.listen((event) {
|
||||
final player = PlPlayerController.getInstance();
|
||||
if (event.begin) {
|
||||
switch (event.type) {
|
||||
case AudioInterruptionType.duck:
|
||||
player.setVolume(player.volume.value * 0.5);
|
||||
break;
|
||||
case AudioInterruptionType.pause:
|
||||
case AudioInterruptionType.unknown:
|
||||
player.pause();
|
||||
_playInterrupted = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
switch (event.type) {
|
||||
case AudioInterruptionType.duck:
|
||||
player.setVolume(player.volume.value * 2);
|
||||
break;
|
||||
case AudioInterruptionType.pause:
|
||||
if (_playInterrupted) PlPlayerController.getInstance().play();
|
||||
break;
|
||||
case AudioInterruptionType.unknown:
|
||||
break;
|
||||
}
|
||||
_playInterrupted = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 耳机拔出暂停
|
||||
session.becomingNoisyEventStream.listen((_) {
|
||||
PlPlayerController.getInstance().pause();
|
||||
});
|
||||
}
|
||||
}
|
||||
11
lib/services/service_locator.dart
Normal file
11
lib/services/service_locator.dart
Normal file
@ -0,0 +1,11 @@
|
||||
import 'audio_handler.dart';
|
||||
import 'audio_session.dart';
|
||||
|
||||
late VideoPlayerServiceHandler videoPlayerServiceHandler;
|
||||
late AudioSessionHandler audioSessionHandler;
|
||||
|
||||
Future<void> setupServiceLocator() async {
|
||||
final audio = await initAudioService();
|
||||
videoPlayerServiceHandler = audio;
|
||||
audioSessionHandler = AudioSessionHandler();
|
||||
}
|
||||
Reference in New Issue
Block a user