You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

使用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

关键修正点说明

  1. 避免跨isolate传递Player:现在PlayerVideoController都在前台页面的isolate中创建,确保Video组件能直接获取视频帧渲染,彻底解决黑屏问题。
  2. Handler做代理而非持有者VideoMediaHandler不再自己管理Player,只负责接收通知栏的控制指令并转发给前台Player,同时同步前台Player的状态到通知栏。
  3. 生命周期监听:通过WidgetsBindingObserver监听App退到后台的事件,确保前台服务持续运行,避免被系统回收。
  4. 状态双向同步:前台Player的播放状态、进度、缓冲变化都会实时同步到通知栏,通知栏的操作也会实时反馈到前台Player。
  5. 加载体验优化:给Video组件添加了加载占位动画,避免用户看到空白黑屏误以为功能异常。

额外注意事项

  • 优先使用真机测试,部分Android模拟器对硬件加速视频渲染支持不佳,可能仍会出现黑屏。
  • 确保Android设备API级别≥26(对应Android 8.0),因为使用了foregroundServiceType属性。
  • 若测试本地视频,需注意文件权限和路径配置。

火山引擎 最新活动