使用audio_service时Flutter media_kit的Video组件显示黑屏(通知功能正常)
使用audio_service时Flutter media_kit的Video组件显示黑屏(通知功能正常)
我看了你的代码,问题核心是你把Player实例跨isolate传递给了audio_service的handler——因为AudioService的handler是运行在后台独立isolate的,而media_kit的Player是绑定原生资源的对象,不能直接跨isolate共享,这就导致前台的Video组件绑定的VideoController无法正确获取到Player的视频流,最终显示黑屏,而通知功能正常(因为handler的状态监听逻辑是通的,但视频渲染的链路断了)。
下面是修正后的完整实现方案,同时保留audio_service的后台播放和通知控制功能:
修正后的代码实现
1. main.dart 完整代码
import 'package:flutter/material.dart'; import 'package:audio_service/audio_service.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; late VideoMediaHandler handler; Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); MediaKit.ensureInitialized(); // 初始化audio_service,handler作为通知控制的代理 handler = await AudioService.init( builder: () => VideoMediaHandler(), config: const AudioServiceConfig( androidNotificationChannelId: 'com.example.notification', androidNotificationChannelName: 'Video Playback', androidNotificationOngoing: true, ), ); runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( home: Scaffold( body: VideoPlayerPage(), ), ); } } class VideoPlayerPage extends StatefulWidget { const VideoPlayerPage({super.key}); @override State<VideoPlayerPage> createState() => _VideoPlayerPageState(); } class _VideoPlayerPageState extends State<VideoPlayerPage> with WidgetsBindingObserver { late final Player player = Player(); late final VideoController controller = VideoController(player); @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); // 绑定前台Player到handler,用于同步状态和接收通知控制 handler.bindPlayer(player); // 加载视频并初始化 const videoUrl = 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'; player.open(Media(videoUrl)); // 更新通知栏显示的媒体信息 handler.updateMediaItem( url: videoUrl, title: 'Bee Video', artist: 'Flutter Demo', artUri: 'https://picsum.photos/300', ); // 监听Player状态变化,同步到audio_service的通知栏 player.stream.playing.listen((playing) { handler.updatePlaybackState(playing: playing); }); player.stream.position.listen((position) { handler.updatePlaybackState(position: position); }); player.stream.buffer.listen((buffer) { handler.updatePlaybackState(buffer: buffer); }); player.stream.duration.listen((duration) { handler.updateMediaItemDuration(duration); }); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); player.dispose(); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { // App退到后台时,确保前台服务持续运行,避免被系统回收 if (state == AppLifecycleState.paused) { handler.ensureForegroundService(); } } @override Widget build(BuildContext context) { return Center( child: SizedBox( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.width * 9 / 16, child: Video( controller: controller, // 视频加载时显示占位动画 placeholder: const Center(child: CircularProgressIndicator()), ), ), ); } } class VideoMediaHandler extends BaseAudioHandler { Player? _player; // 绑定前台的Player实例,实现指令转发和状态同步 void bindPlayer(Player player) { _player = player; } // 更新通知栏显示的媒体信息 void updateMediaItem({ required String url, required String title, required String artist, required String artUri, }) { mediaItem.add(MediaItem( id: url, title: title, album: artist, artUri: Uri.parse(artUri), )); } // 更新媒体项的时长 void updateMediaItemDuration(Duration duration) { final item = mediaItem.value; if (item != null) { mediaItem.add(item.copyWith(duration: duration)); } } // 更新通知栏的播放状态 void updatePlaybackState({ bool? playing, Duration? position, Duration? buffer, }) { if (_player == null) return; final currentPlaying = playing ?? _player!.state.playing; final currentPosition = position ?? _player!.state.position; final currentBuffer = buffer ?? _player!.state.buffer; playbackState.add(PlaybackState( controls: [ MediaControl.skipToPrevious, currentPlaying ? MediaControl.pause : MediaControl.play, MediaControl.skipToNext, ], playing: currentPlaying, processingState: AudioProcessingState.ready, updatePosition: currentPosition, bufferedPosition: currentBuffer, speed: 1.0, systemActions: const { MediaAction.seek, MediaAction.seekForward, MediaAction.seekBackward, MediaAction.stop, }, )); } // 确保前台服务持续运行 void ensureForegroundService() { playbackState.add(playbackState.value.copyWith( processingState: AudioProcessingState.ready, )); } // 处理通知栏的play按钮点击 @override Future<void> play() async { await _player?.play(); } // 处理通知栏的pause按钮点击 @override Future<void> pause() async { await _player?.pause(); } // 处理通知栏的seek操作 @override Future<void> seek(Duration position) async { await _player?.seek(position); } @override Future<void> stop() async { await _player?.pause(); await AudioService.stop(); } }
2. 你的AndroidManifest.xml配置可以保留不变
已经包含了所有需要的权限和服务声明,这里再确认一下关键项:
<!-- 权限部分 --> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- 服务部分 --> <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>
3. 确保依赖完整
在pubspec.yaml中添加对应平台的media_kit依赖:
dependencies: flutter: sdk: flutter audio_service: ^0.18.11 media_kit: ^1.1.10+1 media_kit_video: ^1.1.8 # 以下是各平台的视频支持库,按需添加 media_kit_libs_android_video: ^1.1.5 media_kit_libs_ios_video: ^1.1.4 media_kit_libs_macos_video: ^1.1.4 media_kit_libs_windows_video: ^1.1.4
关键修正点说明
- 避免跨isolate传递Player:现在
Player和VideoController都在前台页面的isolate中创建,确保Video组件能直接获取视频帧渲染,彻底解决黑屏问题。 - Handler做代理而非持有者:
VideoMediaHandler不再自己管理Player,只负责接收通知栏的控制指令并转发给前台Player,同时同步前台Player的状态到通知栏。 - 生命周期监听:通过
WidgetsBindingObserver监听App退到后台的事件,确保前台服务持续运行,避免被系统回收。 - 状态双向同步:前台Player的播放状态、进度、缓冲变化都会实时同步到通知栏,通知栏的操作也会实时反馈到前台Player。
- 加载体验优化:给
Video组件添加了加载占位动画,避免用户看到空白黑屏误以为功能异常。
额外注意事项
- 优先使用真机测试,部分Android模拟器对硬件加速视频渲染支持不佳,可能仍会出现黑屏。
- 确保Android设备API级别≥26(对应Android 8.0),因为使用了
foregroundServiceType属性。 - 若测试本地视频,需注意文件权限和路径配置。




