feat: 新增媒体通知

This commit is contained in:
Riri
2023-10-25 16:27:14 +08:00
parent eda8a5c6a7
commit 0a5dea0535
16 changed files with 235 additions and 21 deletions

View File

@ -45,7 +45,7 @@
android:fullBackupContent="false"
tools:replace="android:allowBackup">
<activity
android:name=".MainActivity"
android:name="com.ryanheise.audioservice.AudioServiceActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
@ -226,6 +226,24 @@
</intent-filter>
</activity>
<service
android:name="com.ryanheise.audioservice.AudioService"
android:foregroundServiceType="mediaPlayback"
android:exported="true"
tools:ignore="Instantiatable">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
<receiver
android:name="com.ryanheise.audioservice.MediaButtonReceiver"
android:exported="true"
tools:ignore="Instantiatable">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
@ -238,6 +256,8 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!--
Media access permissions.
Android 13 or higher.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -103,5 +103,9 @@
</array>
</dict>
</array>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
</dict>
</plist>

View File

@ -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);

View File

@ -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';
@ -51,12 +52,14 @@ class _VideoIntroPanelState extends State<VideoIntroPanel>
_futureBuilderFuture = videoIntroController.queryVideoIntro();
videoIntroController.videoDetail.listen((value) {
videoDetail = value;
videoPlayerServiceHandler.onVideoIntroChange(value);
});
}
@override
void dispose() {
videoIntroController.onClose();
videoPlayerServiceHandler.onVideoIntroDispose();
super.dispose();
}

View File

@ -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));
}),
],
);
}

View File

@ -0,0 +1,116 @@
import 'package:audio_service/audio_service.dart';
import 'package:pilipala/models/video_detail_res.dart';
import 'package:get/get.dart';
import 'package:pilipala/plugin/pl_player/index.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',
),
);
}
class VideoPlayerServiceHandler extends BaseAudioHandler
with QueueHandler, SeekHandler {
static final List<MediaItem> _item = [];
@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 {
mediaItem.add(newMediaItem);
addQueueItem(newMediaItem);
}
Future<void> setPlaybackState(PlayerStatus status, bool isBuffering) async {
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_stat_replay_10'),
if (playing) MediaControl.pause else MediaControl.play,
MediaControl.fastForward
.copyWith(androidIcon: 'drawable/ic_stat_forward_10'),
],
playing: playing,
systemActions: const {
MediaAction.seek,
},
));
}
onStatusChange(PlayerStatus status, bool isBuffering) {
if (_item.isEmpty) return;
setPlaybackState(status, isBuffering);
}
onVideoIntroChange(VideoDetailData data) {
Map argMap = Get.arguments;
final heroTag = argMap['heroTag'];
final mediaItem = MediaItem(
id: heroTag,
title: data.title ?? "",
artist: data.owner?.name ?? "",
duration: Duration(seconds: data.duration ?? 0),
artUri: Uri.parse(data.pic ?? ""),
);
setMediaItem(mediaItem);
_item.add(mediaItem);
}
onVideoIntroDispose() {
playbackState.add(playbackState.value.copyWith(
processingState: AudioProcessingState.idle,
playing: false,
));
stop();
_item.removeLast();
if (_item.isNotEmpty) {
setMediaItem(_item.last);
}
if (_item.isEmpty) {
playbackState
.add(playbackState.value.copyWith(updatePosition: Duration.zero));
}
}
onPositionChange(Duration position) {
playbackState.add(playbackState.value.copyWith(
updatePosition: position,
));
}
}

View File

@ -0,0 +1,8 @@
import 'audio_handler.dart';
late VideoPlayerServiceHandler videoPlayerServiceHandler;
Future<void> setupServiceLocator() async {
final audio = await initAudioService();
videoPlayerServiceHandler = audio;
}

View File

@ -5,6 +5,8 @@
import FlutterMacOS
import Foundation
import audio_service
import audio_session
import connectivity_plus
import device_info_plus
import dynamic_color
@ -20,6 +22,8 @@ import url_launcher_macos
import wakelock_plus
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin"))
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))

View File

@ -57,6 +57,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.11.0"
audio_service:
dependency: "direct main"
description:
name: audio_service
sha256: a4d989f1225ea9621898d60f23236dcbfc04876fa316086c23c5c4af075dbac4
url: "https://pub.dev"
source: hosted
version: "0.18.12"
audio_service_platform_interface:
dependency: transitive
description:
name: audio_service_platform_interface
sha256: "8431a455dac9916cc9ee6f7da5620a666436345c906ad2ebb7fa41d18b3c1bf4"
url: "https://pub.dev"
source: hosted
version: "0.1.1"
audio_service_web:
dependency: transitive
description:
name: audio_service_web
sha256: "523e64ddc914c714d53eec2da85bba1074f08cf26c786d4efb322de510815ea7"
url: "https://pub.dev"
source: hosted
version: "0.1.1"
audio_session:
dependency: transitive
description:
name: audio_session
sha256: "8a2bc5e30520e18f3fb0e366793d78057fb64cd5287862c76af0c8771f2a52ad"
url: "https://pub.dev"
source: hosted
version: "0.1.16"
audio_video_progress_bar:
dependency: "direct main"
description:
@ -213,10 +245,10 @@ packages:
dependency: transitive
description:
name: collection
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
url: "https://pub.dev"
source: hosted
version: "1.17.1"
version: "1.17.2"
connectivity_plus:
dependency: "direct main"
description:
@ -373,18 +405,18 @@ packages:
dependency: "direct main"
description:
name: extended_image
sha256: e77d18f956649ba6e5ecebd0cb68542120886336a75ee673788145bd4c3f0767
sha256: b4d72a27851751cfadaf048936d42939db7cd66c08fdcfe651eeaa1179714ee6
url: "https://pub.dev"
source: hosted
version: "8.0.2"
version: "8.1.1"
extended_image_library:
dependency: transitive
description:
name: extended_image_library
sha256: bb8d08c504ebc73d476ec1c99451a61f12e95538869e734fc4f55a3a2d5c98ec
sha256: "8bf87c0b14dcb59200c923a9a3952304e4732a0901e40811428834ef39018ee1"
url: "https://pub.dev"
source: hosted
version: "3.5.3"
version: "3.6.0"
extended_list:
dependency: transitive
description:
@ -665,10 +697,10 @@ packages:
dependency: transitive
description:
name: intl
sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
url: "https://pub.dev"
source: hosted
version: "0.18.0"
version: "0.18.1"
io:
dependency: transitive
description:
@ -737,18 +769,18 @@ packages:
dependency: transitive
description:
name: matcher
sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb"
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
url: "https://pub.dev"
source: hosted
version: "0.12.15"
version: "0.12.16"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
version: "0.5.0"
media_kit:
dependency: "direct main"
description:
@ -1191,10 +1223,10 @@ packages:
dependency: transitive
description:
name: source_span
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
version: "1.10.0"
sqflite:
dependency: transitive
description:
@ -1279,10 +1311,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
url: "https://pub.dev"
source: hosted
version: "0.5.1"
version: "0.6.0"
timing:
dependency: transitive
description:
@ -1475,6 +1507,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.2"
web:
dependency: transitive
description:
name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
url: "https://pub.dev"
source: hosted
version: "0.1.4-beta"
web_socket_channel:
dependency: transitive
description:
@ -1564,5 +1604,5 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.0.0 <4.0.0"
flutter: ">=3.10.0"
dart: ">=3.1.0-185.0.dev <4.0.0"
flutter: ">=3.13.0"

View File

@ -49,7 +49,7 @@ dependencies:
# 图片
cached_network_image: ^3.3.0
extended_image: ^8.0.2
extended_image: ^8.1.1
saver_gallery: ^2.0.1
# 存储
@ -86,7 +86,10 @@ dependencies:
# 视频播放器
media_kit: ^1.1.10 # Primary package.
media_kit_video: ^1.2.4 # For video rendering.
media_kit_libs_video: ^1.0.4
media_kit_libs_video: ^1.0.4
# 媒体通知
audio_service: ^0.18.12
# 音量、亮度、屏幕控制
flutter_volume_controller: ^1.3.0