Merge pull request #220 from Daydreamer-riri/main

feat: 新增媒体通知
feat: 新增通知小图标
feat: 关闭后台播放时不显示媒体通知
This commit is contained in:
Infinite
2023-10-27 00:10:41 +08:00
committed by GitHub
20 changed files with 384 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: 528 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 962 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,7 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M18,13c0,3.31 -2.69,6 -6,6s-6,-2.69 -6,-6s2.69,-6 6,-6v4l5,-5l-5,-5v4c-4.42,0 -8,3.58 -8,8c0,4.42 3.58,8 8,8s8,-3.58 8,-8H18z"/>
<path android:fillColor="@android:color/white" android:pathData="M10.86,15.94l0,-4.27l-0.09,0l-1.77,0.63l0,0.69l1.01,-0.31l0,3.26z"/>
<path android:fillColor="@android:color/white" android:pathData="M12.25,13.44v0.74c0,1.9 1.31,1.82 1.44,1.82c0.14,0 1.44,0.09 1.44,-1.82v-0.74c0,-1.9 -1.31,-1.82 -1.44,-1.82C13.55,11.62 12.25,11.53 12.25,13.44zM14.29,13.32v0.97c0,0.77 -0.21,1.03 -0.59,1.03c-0.38,0 -0.6,-0.26 -0.6,-1.03v-0.97c0,-0.75 0.22,-1.01 0.59,-1.01C14.07,12.3 14.29,12.57 14.29,13.32z"/>
</vector>

View File

@ -0,0 +1,7 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M11.99,5V1l-5,5l5,5V7c3.31,0 6,2.69 6,6s-2.69,6 -6,6s-6,-2.69 -6,-6h-2c0,4.42 3.58,8 8,8s8,-3.58 8,-8S16.41,5 11.99,5z"/>
<path android:fillColor="@android:color/white" android:pathData="M10.89,16h-0.85v-3.26l-1.01,0.31v-0.69l1.77,-0.63h0.09V16z"/>
<path android:fillColor="@android:color/white" android:pathData="M15.17,14.24c0,0.32 -0.03,0.6 -0.1,0.82s-0.17,0.42 -0.29,0.57s-0.28,0.26 -0.45,0.33s-0.37,0.1 -0.59,0.1s-0.41,-0.03 -0.59,-0.1s-0.33,-0.18 -0.46,-0.33s-0.23,-0.34 -0.3,-0.57s-0.11,-0.5 -0.11,-0.82V13.5c0,-0.32 0.03,-0.6 0.1,-0.82s0.17,-0.42 0.29,-0.57s0.28,-0.26 0.45,-0.33s0.37,-0.1 0.59,-0.1s0.41,0.03 0.59,0.1c0.18,0.07 0.33,0.18 0.46,0.33s0.23,0.34 0.3,0.57s0.11,0.5 0.11,0.82V14.24zM14.32,13.38c0,-0.19 -0.01,-0.35 -0.04,-0.48s-0.07,-0.23 -0.12,-0.31s-0.11,-0.14 -0.19,-0.17s-0.16,-0.05 -0.25,-0.05s-0.18,0.02 -0.25,0.05s-0.14,0.09 -0.19,0.17s-0.09,0.18 -0.12,0.31s-0.04,0.29 -0.04,0.48v0.97c0,0.19 0.01,0.35 0.04,0.48s0.07,0.24 0.12,0.32s0.11,0.14 0.19,0.17s0.16,0.05 0.25,0.05s0.18,-0.02 0.25,-0.05s0.14,-0.09 0.19,-0.17s0.09,-0.19 0.11,-0.32s0.04,-0.29 0.04,-0.48V13.38z"/>
</vector>

View File

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

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

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

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';

View File

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

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));
}),
],
);
}
@ -1007,6 +1021,7 @@ class PlPlayerController {
_instance = null;
// 关闭所有视频页面恢复亮度
resetBrightness();
videoPlayerServiceHandler.clear();
} catch (err) {
print(err);
}

View File

@ -0,0 +1,227 @@
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';
import 'package:audio_session/audio_session.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;
late AudioSession session;
bool _playInterrupted = false; // 暂时无用
VideoPlayerServiceHandler() {
revalidateSetting();
initSession();
}
Future<void> initSession() async {
session = await AudioSession.instance;
session.configure(const AudioSessionConfiguration.speech());
session.interruptionEventStream.listen((event) {
if (event.begin) {
switch (event.type) {
case AudioInterruptionType.duck:
// duck or pause
// break;
case AudioInterruptionType.pause:
case AudioInterruptionType.unknown:
pause();
_playInterrupted = true;
break;
}
} else {
switch (event.type) {
case AudioInterruptionType.duck:
// unduck
// break;
case AudioInterruptionType.pause:
if (_playInterrupted) play();
break;
case AudioInterruptionType.unknown:
break;
}
_playInterrupted = false;
}
});
// 耳机拔出暂停
session.becomingNoisyEventStream.listen((_) {
pause();
});
}
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 (playing) {
_playInterrupted = false;
session.setActive(true);
} else {
session.setActive(false);
}
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,
));
}
}

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: "direct main"
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,11 @@ 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
audio_session: ^0.1.16
# 音量、亮度、屏幕控制
flutter_volume_controller: ^1.3.0