鸿蒙 Flutter 音视频跨设备协同实战:从本地播放到多端同步(2025 进阶版)

在鸿蒙全场景生态中,音视频作为核心交互载体,其跨设备协同能力直接决定用户体验。鸿蒙的分布式媒体服务与 Flutter 的跨端 UI 优势结合,可实现 “手机选片、平板续播、智慧屏投屏” 的无缝体验。本文基于 Flutter 3.24+、鸿蒙 API 12 及分布式媒体 SDK,从音视频基础播放、跨设备投屏、进度同步到多端控制,提供一套完整的实战方案,包含可直接复用的代码与官方资源链接,助力开发者快速落地鸿蒙 Flutter 音视频应用。

一、音视频跨设备协同核心认知:技术融合的价值与场景

鸿蒙 Flutter 音视频协同并非简单的 “多端播放”,而是基于分布式软总线实现的 “媒体资源共享、播放状态同步、控制指令互通”,其核心价值在于打破设备边界,构建全场景媒体体验。

1.1 技术融合的核心优势

技术能力 鸿蒙分布式媒体服务优势 Flutter 优势 融合价值
媒体资源管理 支持跨设备媒体库共享,统一管理多设备音视频文件 跨端统一的媒体列表 UI,适配手机 / 平板 / 智慧屏屏幕尺寸 一次开发实现多设备媒体浏览,资源调用无需重复拷贝
播放状态同步 基于分布式数据服务(DDS)实现毫秒级状态同步 响应式 UI 框架,状态变更实时刷新播放进度条、控制按钮 设备切换时播放进度无缝衔接,无卡顿 / 重复播放问题
跨设备控制 支持分布式控制指令(播放 / 暂停 / 快进)跨设备传输 统一的控制组件封装,适配不同设备交互方式(触屏 / 遥控器) 手机可控制智慧屏播放,平板可接管手机音频,操作逻辑一致
硬件资源调度 自动识别设备硬件能力(如智慧屏音响、手机摄像头) 自适应渲染框架,根据设备性能调整播放分辨率 自动匹配最优硬件资源,如视频在智慧屏渲染、音频在音箱输出

1.2 典型应用场景

  • 场景 1:多设备续播:用户在手机上观看视频,回家后打开智慧屏,视频自动从手机当前进度续播,音频切换到智慧屏音响输出。
  • 场景 2:跨设备投屏:平板上播放 PPT 时,将视频画面投屏到智慧屏,平板保留控制权限,可实时调整播放进度。
  • 场景 3:多端协同录制:手机作为摄像头采集画面,平板作为监视器显示预览,智慧屏实时投屏展示,实现多设备协同拍摄。

1.3 技术栈版本要求(2025 必看)

  • 开发工具:DevEco Studio 4.3+(支持分布式媒体调试)、VS Code(Flutter 开发)
  • 核心框架:Flutter SDK ≥3.24.0(鸿蒙适配版)、鸿蒙 SDK ≥API 12
  • 关键依赖:
    • ohos_flutter_media ^2.8.0(鸿蒙音视频核心插件)
    • flutter_ijkplayer ^0.8.5(Flutter 音视频播放内核)
    • ohos_distributed_media ^1.6.0(分布式媒体资源管理)
  • 设备要求:至少 2 台鸿蒙 3.2 + 设备(需支持分布式软总线与媒体服务)

官方资源链接:

二、项目初始化与基础配置:音视频协同的前提

音视频跨设备协同需提前配置媒体权限、分布式服务与播放内核,以下是完整的初始化流程。

2.1 项目结构设计(音视频场景最佳实践)

plaintext

ohos_flutter_media_demo/
├── lib/
│   ├── main.dart                # 应用入口
│   ├── pages/                   # 核心页面
│   │   ├── media_list_page.dart # 音视频列表页(资源浏览)
│   │   ├── player_page.dart     # 播放页(本地+跨设备)
│   │   └── device_control_page.dart # 多设备控制页
│   ├── media/                   # 音视频核心模块
│   │   ├── media_player.dart    # 播放内核封装(ijkplayer)
│   │   ├── media_manager.dart   # 媒体资源管理(分布式)
│   │   └── device_sync.dart     # 跨设备状态同步
│   └── widgets/                 # 通用组件
│       ├── player_control.dart  # 播放控制组件(播放/暂停/快进)
│       └── device_selector.dart # 设备选择组件(投屏目标)
├── ohos/                        # 鸿蒙工程目录
│   ├── config.json              # 权限与分布式配置
│   └── native/                  # 鸿蒙原生媒体能力封装
└── pubspec.yaml                 # 依赖配置

2.2 依赖配置(pubspec.yaml)

yaml

name: ohos_flutter_media_demo
description: 鸿蒙Flutter音视频跨设备协同演示
version: 1.0.0+1

environment:
  sdk: '>=3.24.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter
  # 鸿蒙音视频核心插件
  ohos_flutter_media: ^2.8.0
  # 分布式媒体资源管理
  ohos_distributed_media: ^1.6.0
  # Flutter播放内核
  flutter_ijkplayer: ^0.8.5
  # 状态管理(播放进度/设备状态)
  provider: ^6.1.1
  # 序列化工具(状态同步)
  json_annotation: ^4.8.1

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^2.4.4
  json_serializable: ^6.7.1

flutter:
  uses-material-design: true
  # 媒体资源配置
  assets:
    - assets/videos/
    - assets/audios/

2.3 鸿蒙权限与媒体配置(ohos/config.json)

音视频跨设备协同需声明媒体访问、分布式同步、网络等权限,同时配置分布式媒体服务支持:

json

{
  "app": {
    "bundleName": "com.example.mediademo",
    "versionName": "1.0.0",
    "versionCode": 1
  },
  "module": {
    "package": "com.example.mediademo",
    "name": ".MainAbility",
    "mainAbility": "com.example.mediademo.MainAbility",
    "deviceType": ["phone", "tablet", "tv"],
    "distributedNotificationEnabled": true,
    "distributedMediaEnabled": true, # 开启分布式媒体服务
    "reqPermissions": [
      # 媒体访问权限
      {
        "name": "ohos.permission.READ_MEDIA",
        "reason": "需要读取本地音视频文件",
        "usedScene": {"ability": ["MainAbility"], "when": "always"}
      },
      {
        "name": "ohos.permission.WRITE_MEDIA",
        "reason": "需要保存音视频文件",
        "usedScene": {"ability": ["MainAbility"], "when": "always"}
      },
      # 分布式权限
      {
        "name": "ohos.permission.DISTRIBUTED_DATASYNC",
        "reason": "需要跨设备同步播放状态",
        "usedScene": {"ability": ["MainAbility"], "when": "always"}
      },
      {
        "name": "ohos.permission.GET_DISTRIBUTED_DEVICE_INFO",
        "reason": "需要发现跨设备媒体设备",
        "usedScene": {"ability": ["MainAbility"], "when": "always"}
      },
      # 网络权限(在线媒体播放)
      {
        "name": "ohos.permission.INTERNET",
        "reason": "需要播放在线音视频",
        "usedScene": {"ability": ["MainAbility"], "when": "always"}
      }
    ]
  }
}

三、核心实战一:本地音视频播放与基础控制

在实现跨设备协同前,需先完成本地音视频播放功能,基于flutter_ijkplayer封装通用播放内核,支持本地文件与在线资源。

3.1 播放内核封装(media/media_player.dart)

dart

import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
import 'package:flutter/services.dart';

/// 音视频播放内核封装(支持本地/在线资源)
class MediaPlayer {
  late IjkMediaController _controller;
  // 播放状态回调
  Function(PlayerStatus status)? onStatusChanged;
  // 播放进度回调(每秒更新)
  Function(Duration current, Duration total)? onProgressChanged;

  MediaPlayer() {
    _initController();
    _listenPlayerStatus();
    _listenProgress();
  }

  // 初始化播放控制器
  void _initController() {
    _controller = IjkMediaController();
    // 配置播放参数(硬件解码、音量等)
    _controller.setOption(IjkMediaOption.playerCategory, "mediacodec", 1); // 开启硬件解码
    _controller.setOption(IjkMediaOption.playerCategory, "volume", 100); // 初始音量
  }

  // 监听播放状态变化
  void _listenPlayerStatus() {
    _controller.addListener(() {
      final status = _controller.value.playerState;
      if (onStatusChanged != null) {
        switch (status) {
          case PlayerState.playing:
            onStatusChanged!(PlayerStatus.playing);
            break;
          case PlayerState.paused:
            onStatusChanged!(PlayerStatus.paused);
            break;
          case PlayerState.completed:
            onStatusChanged!(PlayerStatus.completed);
            break;
          default:
            onStatusChanged!(PlayerStatus.idle);
        }
      }
    });
  }

  // 监听播放进度(每秒更新一次)
  void _listenProgress() {
    _controller.addListener(() {
      final current = _controller.value.position;
      final total = _controller.value.duration;
      if (current != null && total != null && onProgressChanged != null) {
        onProgressChanged!(current, total);
      }
    });
  }

  // 加载媒体资源(本地路径/在线URL)
  Future<void> loadMedia(String mediaPath, {bool isLocal = true}) async {
    try {
      if (isLocal) {
        // 本地资源:需拼接完整路径
        final assetPath = await rootBundle.loadString('assets/path_config.json');
        final localPath = "$assetPath/${mediaPath.split('/').last}";
        await _controller.setNetworkDataSource(localPath, autoPlay: false);
      } else {
        // 在线资源:直接设置URL
        await _controller.setNetworkDataSource(mediaPath, autoPlay: false);
      }
    } catch (e) {
      throw Exception("加载媒体资源失败:$e");
    }
  }

  // 播放控制:播放/暂停
  void togglePlay() {
    if (_controller.value.playerState == PlayerState.playing) {
      _controller.pause();
    } else {
      _controller.play();
    }
  }

  // 进度控制:跳转指定时间
  void seekTo(Duration duration) {
    _controller.seekTo(duration);
  }

  // 释放资源(页面销毁时调用)
  void dispose() {
    _controller.dispose();
  }

  // 获取播放控制器(供UI层使用)
  IjkMediaController get controller => _controller;
}

/// 播放状态枚举
enum PlayerStatus { idle, playing, paused, completed, error }

3.2 播放页实现(pages/player_page.dart)

dart

import 'package:flutter/material.dart';
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
import 'package:ohos_flutter_media_demo/media/media_player.dart';
import 'package:ohos_flutter_media_demo/widgets/player_control.dart';

/// 音视频播放页(本地播放)
class PlayerPage extends StatefulWidget {
  final String mediaPath; // 媒体路径(本地/在线)
  final bool isLocal;     // 是否为本地资源
  final String mediaTitle; // 媒体标题

  const PlayerPage({
    super.key,
    required this.mediaPath,
    this.isLocal = true,
    required this.mediaTitle,
  });

  @override
  State<PlayerPage> createState() => _PlayerPageState();
}

class _PlayerPageState extends State<PlayerPage> {
  late MediaPlayer _mediaPlayer;
  PlayerStatus _currentStatus = PlayerStatus.idle;
  Duration _currentProgress = Duration.zero;
  Duration _totalDuration = Duration.zero;

  @override
  void initState() {
    super.initState();
    _initPlayer();
  }

  // 初始化播放器
  void _initPlayer() {
    _mediaPlayer = MediaPlayer();
    // 监听播放状态
    _mediaPlayer.onStatusChanged = (status) {
      setState(() => _currentStatus = status);
    };
    // 监听播放进度
    _mediaPlayer.onProgressChanged = (current, total) {
      setState(() {
        _currentProgress = current;
        _totalDuration = total;
      });
    };
    // 加载媒体资源
    _mediaPlayer.loadMedia(widget.mediaPath, isLocal: widget.isLocal);
  }

  // 格式化时间(秒转分:秒)
  String _formatDuration(Duration duration) {
    final minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0');
    final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0');
    return "$minutes:$seconds";
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.mediaTitle),
        backgroundColor: Colors.black87,
        titleTextStyle: const TextStyle(color: Colors.white),
        iconTheme: const IconThemeData(color: Colors.white),
      ),
      body: Container(
        color: Colors.black,
        child: Column(
          children: [
            // 播放视图
            Expanded(
              child: IjkPlayerView(
                controller: _mediaPlayer.controller,
                options: IjkPlayerOptions(
                  initOptions: [
                    "-i", widget.mediaPath, // 输入媒体路径
                    "-vol", "100",          // 音量
                  ],
                ),
                // 加载中占位图
                loadingWidget: const Center(
                  child: CircularProgressIndicator(color: Colors.blue),
                ),
                // 播放错误占位图
                errorWidget: const Center(
                  child: Text("播放失败,请检查资源路径", color: Colors.white),
                ),
              ),
            ),
            // 播放进度条
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
              child: Column(
                children: [
                  // 进度条
                  Slider(
                    value: _currentProgress.inSeconds.toDouble(),
                    max: _totalDuration.inSeconds.toDouble(),
                    min: 0,
                    activeColor: Colors.blue,
                    inactiveColor: Colors.grey,
                    onChanged: (value) {
                      _mediaPlayer.seekTo(Duration(seconds: value.toInt()));
                    },
                  ),
                  // 时间显示(当前/总时长)
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Text(
                        _formatDuration(_currentProgress),
                        style: const TextStyle(color: Colors.white, fontSize: 12),
                      ),
                      Text(
                        _formatDuration(_totalDuration),
                        style: const TextStyle(color: Colors.white, fontSize: 12),
                      ),
                    ],
                  ),
                ],
              ),
            ),
            // 播放控制组件
            PlayerControlWidget(
              status: _currentStatus,
              onPlayToggle: () => _mediaPlayer.togglePlay(),
              onSeekForward: () => _mediaPlayer.seekTo(
                _currentProgress + const Duration(seconds: 10),
              ),
              onSeekBackward: () => _mediaPlayer.seekTo(
                _currentProgress - const Duration(seconds: 10),
              ),
            ),
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    _mediaPlayer.dispose();
    super.dispose();
  }
}

3.3 播放控制组件(widgets/player_control.dart)

dart

import 'package:flutter/material.dart';
import 'package:ohos_flutter_media_demo/media/media_player.dart';

/// 播放控制组件(播放/暂停/快进/快退)
class PlayerControlWidget extends StatelessWidget {
  final PlayerStatus status;
  final VoidCallback onPlayToggle;
  final VoidCallback onSeekForward;
  final VoidCallback onSeekBackward;

  const PlayerControlWidget({
    super.key,
    required this.status,
    required this.onPlayToggle,
    required this.onSeekForward,
    required this.onSeekBackward,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.black87,
      padding: const EdgeInsets.symmetric(vertical: 16),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          // 快退10秒
          IconButton(
            icon: const Icon(Icons.fast_rewind, color: Colors.white, size: 32),
            onPressed: onSeekBackward,
          ),
          const SizedBox(width: 32),
          // 播放/暂停
          IconButton(
            icon: Icon(
              status == PlayerStatus.playing 
                  ? Icons.pause_circle_filled 
                  : Icons.play_circle_filled,
              color: Colors.white,
              size: 48,
            ),
            onPressed: onPlayToggle,
          ),
          const SizedBox(width: 32),
          // 快进10秒
          IconButton(
            icon: const Icon(Icons.fast_forward, color: Colors.white, size: 32),
            onPressed: onSeekForward,
          ),
        ],
      ),
    );
  }
}

四、核心实战二:跨设备投屏与播放状态同步

跨设备投屏的核心是 “媒体资源地址共享 + 播放状态实时同步”,基于鸿蒙分布式媒体服务与 DDS 实现。

4.1 分布式媒体资源管理(media/media_manager.dart)

dart

import 'package:ohos_distributed_media/ohos_distributed_media.dart';
import 'package:ohos_flutter_media_demo/media/device_sync.dart';

/// 分布式媒体资源管理器(获取跨设备媒体列表、共享资源地址)
class MediaManager {
  static final MediaManager _instance = MediaManager._internal();
  factory MediaManager() => _instance;
  MediaManager._internal();

  // 分布式媒体服务实例
  final _distributedMedia = OhosDistributedMedia();
  // 设备同步工具
  final _deviceSync = DeviceSync();

  // 获取所有设备的媒体列表(本地+跨设备)
  Future<List<MediaItem>> getDistributedMediaList() async {
    try {
      // 1. 获取本地媒体列表
      final localMedia = await _getLocalMediaList();
      // 2. 获取跨设备媒体列表(已连接设备)
      final connectedDevices = await _deviceSync.getConnectedDevices();
      final crossDeviceMedia = await Future.wait(
        connectedDevices.map((device) => _getDeviceMediaList(device.deviceId)),
      );
      // 3. 合并本地与跨设备媒体列表
      final allMedia = localMedia + crossDeviceMedia.expand((e) => e).toList();
      return allMedia;
    } catch (e) {
      throw Exception("获取分布式媒体列表失败:$e");
    }
  }

  // 获取本地媒体列表
  Future<List<MediaItem>> _getLocalMediaList() async {
    final localFiles = await _distributedMedia.getLocalMedia(
      mediaType: MediaType.video, // 筛选视频类型
    );
    return localFiles.map((file) => MediaItem(
      id: file.id,
      title: file.name,
      path: file.path,
      deviceId: "local", // 本地设备标识
      deviceName: "本地设备",
      isLocal: true,
    )).toList();
  }

  // 获取指定设备的媒体列表
  Future<List<MediaItem>> _getDeviceMediaList(String deviceId) async {
    final deviceName = await _deviceSync.getDeviceName(deviceId);
    final deviceFiles = await _distributedMedia.getRemoteMedia(
      deviceId: deviceId,
      mediaType: MediaType.video,
    );
    return deviceFiles.map((file) => MediaItem(
      id: file.id,
      title: file.name,
      path: file.remotePath, // 远程资源路径(分布式访问地址)
      deviceId: deviceId,
      deviceName: deviceName,
      isLocal: false,
    )).toList();
  }

  // 共享本地媒体到指定设备(供投屏使用)
  Future<String> shareLocalMediaToDevice(
    String localMediaPath, 
    String targetDeviceId,
  ) async {
    try {
      // 通过分布式媒体服务共享本地资源,生成远程访问路径
      final remotePath = await _distributedMedia.shareLocalMedia(
        localPath: localMediaPath,
        targetDeviceId: targetDeviceId,
      );
      return remotePath;
    } catch (e) {
      throw Exception("共享媒体到设备失败:$e");
    }
  }
}

/// 媒体资源模型
class MediaItem {
  final String id;          // 唯一标识
  final String title;       // 媒体标题
  final String path;        // 资源路径(本地路径/远程访问路径)
  final String deviceId;    // 设备ID(本地为"local")
  final String deviceName;  // 设备名称
  final bool isLocal;       // 是否为本地资源

  MediaItem({
    required this.id,
    required this.title,
    required this.path,
    required this.deviceId,
    required this.deviceName,
    required this.isLocal,
  });
}

/// 媒体类型枚举
enum MediaType { video, audio, image }

4.2 跨设备状态同步(media/device_sync.dart)

dart

import 'package:ohos_flutter_distributed/ohos_flutter_distributed.dart';
import 'package:json_annotation/json_annotation.dart';

part 'device_sync.g.dart';

/// 跨设备状态同步工具(设备发现、连接、播放状态同步)
class DeviceSync {
  static final DeviceSync _instance = DeviceSync._internal();
  factory DeviceSync() => _instance;
  DeviceSync._internal();

  // 分布式服务实例
  final _distributed = OhosDistributed();
  // 已连接设备列表
  List<DeviceInfo> _connectedDevices = [];
  List<DeviceInfo> get connectedDevices => _connectedDevices;

  // 初始化设备发现与连接监听
  Future<void> init() async {
    // 监听设备发现事件
    _distributed.deviceDiscoveryStream.listen((devices) {
      _connectedDevices = devices
          .where((device) => device.isConnected)
          .map((device) => DeviceInfo(
                deviceId: device.deviceId,
                deviceName: device.deviceName,
                deviceType: device.deviceType,
              ))
          .toList();
    });
    // 启动设备发现
    await _distributed.startDeviceDiscovery();
  }

  // 获取已连接设备列表
  Future<List<DeviceInfo>> getConnectedDevices() async {
    if (_connectedDevices.isEmpty) {
      await _distributed.startDeviceDiscovery();
      // 等待设备发现(最多等待3秒)
      await Future.delayed(const Duration(seconds: 3));
    }
    return _connectedDevices;
  }

  // 获取设备名称
  Future<String> getDeviceName(String deviceId) async {
    final device = await _distributed.getDeviceInfo(deviceId);
    return device.deviceName;
  }

  // 连接目标设备
  Future<bool> connectDevice(String deviceId) async {
    try {
      return await _distributed.connectDevice(deviceId);
    } catch (e) {
      print("连接设备失败:$e");
      return false;
    }
  }

  // 同步播放状态到目标设备
  Future<void> syncPlayerState(
    String targetDeviceId,
    PlayerStateSync state,
  ) async {
    try {
      await _distributed.syncData(
        key: "media_player_state_${state.mediaId}",
        value: state.toJson(),
        syncMode: SyncMode.SPECIFIC_DEVICE,
        targetDeviceId: targetDeviceId,
      );
    } catch (e) {
      throw Exception("同步播放状态失败:$e");
    }
  }

  // 监听播放状态变化(接收其他设备的同步数据)
  void listenPlayerStateChanges(Function(PlayerStateSync) onStateChanged) {
    _distributed.dataSyncStream.listen((data) {
      if (data.key.startsWith("media_player_state_")) {
        final state = PlayerStateSync.fromJson(data.value);
        onStateChanged(state);
      }
    });
  }
}

/// 设备信息模型
class DeviceInfo {
  final String deviceId;    // 设备唯一ID
  final String deviceName;  // 设备名称
  final String deviceType;  // 设备类型(phone/tablet/tv)

  DeviceInfo({
    required this.deviceId,
    required this.deviceName,
    required this.deviceType,
  });
}

/// 播放状态同步模型(支持序列化)
@JsonSerializable()
class PlayerStateSync {
  final String mediaId;         // 媒体唯一ID
  final String mediaPath;       // 媒体路径(远程访问地址)
  final Duration currentProgress; // 当前播放进度
  final PlayerStatus status;    // 播放状态

  PlayerStateSync({
    required this.mediaId,
    required this.mediaPath,
    required this.currentProgress,
    required this.status,
  });

  // 序列化
  Map<String, dynamic> toJson() => _$PlayerStateSyncToJson(this);
  // 反序列化
  factory PlayerStateSync.fromJson(Map<String, dynamic> json) =>
      _$PlayerStateSyncFromJson(json);
}

4.3 跨设备投屏功能实现(修改 player_page.dart)

在原有播放页基础上添加 “设备选择” 与 “投屏” 功能,支持将本地播放流转到其他设备:

dart

// 在PlayerPage类中添加以下代码
import 'package:ohos_flutter_media_demo/media/media_manager.dart';
import 'package:ohos_flutter_media_demo/media/device_sync.dart';
import 'package:ohos_flutter_media_demo/widgets/device_selector.dart';

class _PlayerPageState extends State<PlayerPage> {
  // 新增:分布式工具实例
  final _mediaManager = MediaManager();
  final _deviceSync = DeviceSync();
  // 新增:投屏目标设备
  DeviceInfo? _targetDevice;
  // 新增:是否处于投屏状态
  bool _isCasting = false;

  @override
  void initState() {
    super.initState();
    _initPlayer();
    // 新增:初始化设备同步
    _deviceSync.init();
    // 新增:监听其他设备的播放状态同步
    _deviceSync.listenPlayerStateChanges((state) {
      if (state.mediaId == widget.mediaPath.split('/').last) {
        setState(() {
          _currentProgress = state.currentProgress;
          _currentStatus = state.status;
        });
        // 同步进度到本地播放器
        _mediaPlayer.seekTo(state.currentProgress);
      }
    });
  }

  // 新增:打开设备选择弹窗
  void _openDeviceSelector() {
    showModalBottomSheet(
      context: context,
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
      ),
      builder: (context) => DeviceSelectorWidget(
        onDeviceSelected: (device) {
          setState(() => _targetDevice = device);
          Navigator.pop(context);
        },
      ),
    );
  }

  // 新增:启动投屏
  Future<void> _startCasting() async {
    if (_targetDevice == null) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text("请先选择投屏目标设备")),
      );
      return;
    }
    // 1. 连接目标设备
    final isConnected = await _deviceSync.connectDevice(_targetDevice!.deviceId);
    if (!isConnected) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text("设备连接失败,请重试")),
      );
      return;
    }
    // 2. 共享本地媒体到目标设备(生成远程访问路径)
    final remoteMediaPath = await _mediaManager.shareLocalMediaToDevice(
      widget.mediaPath,
      _targetDevice!.deviceId,
    );
    // 3. 同步当前播放状态到目标设备
    final syncState = PlayerStateSync(
      mediaId: widget.mediaPath.split('/').last,
      mediaPath: remoteMediaPath,
      currentProgress: _currentProgress,
      status: _currentStatus,
    );
    await _deviceSync.syncPlayerState(_targetDevice!.deviceId, syncState);
    // 4. 更新投屏状态
    setState(() => _isCasting = true);
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text("已投屏到${_targetDevice!.deviceName}")),
    );
  }

  // 新增:停止投屏
  Future<void> _stopCasting() async {
    setState(() => _isCasting = false);
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text("已停止投屏")),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.mediaTitle),
        backgroundColor: Colors.black87,
        titleTextStyle: const TextStyle(color: Colors.white),
        iconTheme: const IconThemeData(color: Colors.white),
        // 新增:投屏控制按钮
        actions: [
          IconButton(
            icon: Icon(
              _isCasting ? Icons.cast_connected : Icons.cast,
              color: Colors.white,
              size: 24,
            ),
            onPressed: _isCasting ? _stopCasting : _openDeviceSelector,
          ),
        ],
      ),
      // 原有代码不变...
    );
  }
}

4.4 设备选择组件(widgets/device_selector.dart)

dart

import 'package:flutter/material.dart';
import 'package:ohos_flutter_media_demo/media/device_sync.dart';

/// 设备选择组件(投屏目标选择)
class DeviceSelectorWidget extends StatefulWidget {
  final Function(DeviceInfo) onDeviceSelected;

  const DeviceSelectorWidget({
    super.key,
    required this.onDeviceSelected,
  });

  @override
  State<DeviceSelectorWidget> createState() => _DeviceSelectorWidgetState();
}

class _DeviceSelectorWidgetState extends State<DeviceSelectorWidget> {
  final _deviceSync = DeviceSync();
  List<DeviceInfo> _deviceList = [];

  @override
  void initState() {
    super.initState();
    _getDeviceList();
  }

  // 获取已连接设备列表
  Future<void> _getDeviceList() async {
    final devices = await _deviceSync.getConnectedDevices();
    setState(() => _deviceList = devices);
  }

  // 获取设备图标
  IconData _getDeviceIcon(String deviceType) {
    switch (deviceType.toLowerCase()) {
      case "phone":
        return Icons.phone_android;
      case "tablet":
        return Icons.tablet_android;
      case "tv":
        return Icons.tv;
      default:
        return Icons.device_unknown;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          const Text(
            "选择投屏设备",
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 24),
          if (_deviceList.isEmpty)
            const Center(
              child: Text("未发现已连接的设备,请确保设备在同一局域网"),
            )
          else
            ListView.builder(
              shrinkWrap: true,
              itemCount: _deviceList.length,
              itemBuilder: (context, index) {
                final device = _deviceList[index];
                return ListTile(
                  leading: Icon(
                    _getDeviceIcon(device.deviceType),
                    size: 28,
                    color: Colors.blue,
                  ),
                  title: Text(device.deviceName),
                  subtitle: Text("设备类型:${device.deviceType}"),
                  trailing: const Icon(Icons.arrow_forward_ios, size: 16),
                  onTap: () => widget.onDeviceSelected(device),
                );
              },
            ),
        ],
      ),
    );
  }
}

五、核心实战三:多设备协同控制与进度同步

多设备协同控制支持 “一端控制、多端响应”,如手机暂停播放时,智慧屏同步暂停,核心是通过分布式指令传输实现。

5.1 多设备控制页实现(pages/device_control_page.dart)

dart

import 'package:flutter/material.dart';
import 'package:ohos_flutter_media_demo/media/device_sync.dart';
import 'package:ohos_flutter_media_demo/media/media_player.dart';
import 'package:ohos_flutter_media_demo/widgets/player_control.dart';

/// 多设备协同控制页(控制所有已连接设备的播放状态)
class DeviceControlPage extends StatefulWidget {
  final String mediaId;       // 媒体唯一ID
  final String mediaTitle;    // 媒体标题

  const DeviceControlPage({
    super.key,
    required this.mediaId,
    required this.mediaTitle,
  });

  @override
  State<DeviceControlPage> createState() => _DeviceControlPageState();
}

class _DeviceControlPageState extends State<DeviceControlPage> {
  final _deviceSync = DeviceSync();
  final _mediaPlayer = MediaPlayer();
  List<DeviceInfo> _connectedDevices = [];
  PlayerStatus _currentStatus = PlayerStatus.idle;
  Duration _currentProgress = Duration.zero;

  @override
  void initState() {
    super.initState();
    _initDeviceList();
    _listenPlayerState();
  }

  // 初始化已连接设备列表
  Future<void> _initDeviceList() async {
    final devices = await _deviceSync.getConnectedDevices();
    setState(() => _connectedDevices = devices);
  }

  // 监听播放状态同步(来自其他设备)
  void _listenPlayerState() {
    _deviceSync.listenPlayerStateChanges((state) {
      if (state.mediaId == widget.mediaId) {
        setState(() {
          _currentStatus = state.status;
          _currentProgress = state.currentProgress;
        });
      }
    });
  }

  // 发送控制指令到所有已连接设备
  Future<void> _sendControlCommand(PlayerControlCommand command) async {
    for (final device in _connectedDevices) {
      // 构建控制指令(包含媒体ID、指令类型、进度)
      final controlData = {
        "mediaId": widget.mediaId,
        "commandType": command.type.name,
        "progress": _currentProgress.inSeconds,
      };
      // 发送分布式控制指令
      await _deviceSync.syncData(
        key: "media_control_command_${widget.mediaId}",
        value: controlData,
        syncMode: SyncMode.SPECIFIC_DEVICE,
        targetDeviceId: device.deviceId,
      );
    }
  }

  // 播放/暂停控制
  void _togglePlay() {
    final newStatus = _currentStatus == PlayerStatus.playing
        ? PlayerStatus.paused
        : PlayerStatus.playing;
    setState(() => _currentStatus = newStatus);
    _sendControlCommand(PlayerControlCommand(
      type: newStatus == PlayerStatus.playing
          ? ControlCommandType.play
          : ControlCommandType.pause,
      progress: _currentProgress,
    ));
  }

  // 快进/快退控制
  void _seek(bool isForward) {
    final newProgress = isForward
        ? _currentProgress + const Duration(seconds: 10)
        : _currentProgress - const Duration(seconds: 10);
    setState(() => _currentProgress = newProgress);
    _sendControlCommand(PlayerControlCommand(
      type: ControlCommandType.seek,
      progress: newProgress,
    ));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("多设备控制:${widget.mediaTitle}"),
        backgroundColor: Colors.blue,
        titleTextStyle: const TextStyle(color: Colors.white),
        iconTheme: const IconThemeData(color: Colors.white),
      ),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          children: [
            // 已连接设备列表
            Text(
              "已连接设备(${_connectedDevices.length}台)",
              style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 16),
            Wrap(
              spacing: 12,
              runSpacing: 8,
              children: _connectedDevices.map((device) {
                return Chip(
                  label: Text(device.deviceName),
                  avatar: Icon(_getDeviceIcon(device.deviceType), size: 16),
                  backgroundColor: Colors.blue[100],
                );
              }).toList(),
            ),
            const SizedBox(height: 48),
            // 当前播放状态
            Text(
              "当前状态:${_getStatusText(_currentStatus)}",
              style: const TextStyle(fontSize: 18),
            ),
            const SizedBox(height: 8),
            Text(
              "当前进度:${_formatDuration(_currentProgress)}",
              style: const TextStyle(fontSize: 16, color: Colors.grey),
            ),
            const SizedBox(height: 64),
            // 播放控制组件
            PlayerControlWidget(
              status: _currentStatus,
              onPlayToggle: _togglePlay,
              onSeekForward: () => _seek(true),
              onSeekBackward: () => _seek(false),
            ),
          ],
        ),
      ),
    );
  }

  // 获取设备图标
  IconData _getDeviceIcon(String deviceType) {
    switch (deviceType.toLowerCase()) {
      case "phone":
        return Icons.phone_android;
      case "tablet":
        return Icons.tablet_android;
      case "tv":
        return Icons.tv;
      default:
        return Icons.device_unknown;
    }
  }

  // 播放状态文本
  String _getStatusText(PlayerStatus status) {
    switch (status) {
      case PlayerStatus.playing:
        return "播放中";
      case PlayerStatus.paused:
        return "已暂停";
      case PlayerStatus.completed:
        return "播放完成";
      default:
        return "未播放";
    }
  }

  // 格式化时间
  String _formatDuration(Duration duration) {
    final minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0');
    final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0');
    return "$minutes:$seconds";
  }

  @override
  void dispose() {
    _mediaPlayer.dispose();
    super.dispose();
  }
}

/// 播放控制指令模型
class PlayerControlCommand {
  final ControlCommandType type; // 指令类型(播放/暂停/快进)
  final Duration progress;      // 目标进度(快进/快退时使用)

  PlayerControlCommand({
    required this.type,
    required this.progress,
  });
}

/// 控制指令类型枚举
enum ControlCommandType { play, pause, seek }

5.2 播放页接收控制指令(修改 player_page.dart)

在播放页中添加控制指令监听,接收来自其他设备的控制指令并执行相应操作:

dart

class _PlayerPageState extends State<PlayerPage> {
  @override
  void initState() {
    super.initState();
    _initPlayer();
    _deviceSync.init();
    _listenPlayerStateChanges();
    // 新增:监听控制指令
    _listenControlCommands();
  }

  // 新增:监听控制指令(来自其他设备)
  void _listenControlCommands() {
    _distributed.dataSyncStream.listen((data) {
      if (data.key.startsWith("media_control_command_")) {
        final controlData = data.value as Map<String, dynamic>;
        final mediaId = controlData["mediaId"];
        // 只处理当前媒体的控制指令
        if (mediaId == widget.mediaPath.split('/').last) {
          final commandType = controlData["commandType"];
          final progress = Duration(seconds: controlData["progress"]);
          _handleControlCommand(commandType, progress);
        }
      }
    });
  }

  // 新增:处理控制指令
  void _handleControlCommand(String commandType, Duration progress) {
    switch (commandType) {
      case "play":
        if (_currentStatus != PlayerStatus.playing) {
          _mediaPlayer.togglePlay();
        }
        break;
      case "pause":
        if (_currentStatus == PlayerStatus.playing) {
          _mediaPlayer.togglePlay();
        }
        break;
      case "seek":
        _mediaPlayer.seekTo(progress);
        setState(() => _currentProgress = progress);
        break;
    }
  }
}

六、性能优化与常见问题解决方案

音视频跨设备协同的性能瓶颈主要集中在 “资源加载速度”“状态同步延迟”“播放卡顿”,以下是针对性优化方案。

6.1 性能优化方案

  1. 资源加载优化

    • 预加载远程资源:在设备连接成功后,提前加载目标设备的媒体资源元数据(如时长、分辨率),减少播放等待时间。
    • 自适应码率:根据设备间网络带宽自动调整播放码率,带宽不足时降低分辨率,避免卡顿。

    dart

    // 自适应码率配置(media_player.dart)
    void setAdaptiveBitrate() {
      _controller.setOption(IjkMediaOption.playerCategory, "max-buffer-size", 1024*1024*5); // 5MB缓存
      _controller.setOption(IjkMediaOption.playerCategory, "buffer-duration", 3); // 3秒缓存
    }
    
  2. 状态同步优化

    • 批量同步:将 “进度更新 + 状态变更” 合并为单次同步,减少分布式通信次数。
    • 延迟同步:进度更新间隔从 1 秒调整为 3 秒(非精确场景),降低数据传输压力。

    dart

    // 延迟同步进度(media/device_sync.dart)
    void _delayedSyncProgress() {
      int lastSyncTime = 0;
      _mediaPlayer.onProgressChanged = (current, total) {
        final now = DateTime.now().millisecondsSinceEpoch;
        if (now - lastSyncTime > 3000) { // 每3秒同步一次
          syncPlayerState(targetDeviceId, syncState);
          lastSyncTime = now;
        }
      };
    }
    
  3. 播放卡顿优化

    • 开启硬件解码:优先使用设备硬件解码能力,减少 CPU 占用。
    • 关闭冗余日志:在 Release 模式下关闭播放内核日志输出,减少 IO 开销。

6.2 常见问题与解决方案

问题现象 解决方案
跨设备投屏时资源加载失败 1. 检查设备是否在同一局域网;2. 确认分布式媒体服务权限已声明;3. 验证远程资源路径是否正确(通过shareLocalMediaToDevice返回值)
播放状态同步延迟超过 1 秒 1. 减少同步频率(如 3 秒一次);2. 优先使用 Wi-Fi 连接(蓝牙传输延迟较高);3. 关闭其他占用带宽的应用
智慧屏投屏时画面比例失调 1. 在IjkPlayerView中设置aspectRatioMediaQuery.of(context).size.aspectRatio;2. 根据设备类型动态调整缩放模式
多设备控制时部分设备无响应 1. 检查设备是否已连接(通过_deviceSync.getConnectedDevices()验证);2. 重新发送控制指令(添加重试机制)
播放大文件(>1GB)时内存溢出 1. 增加播放内核缓存(max-buffer-size调整为 10MB);2. 采用分片加载模式(仅加载当前播放片段)

七、总结与扩展学习资源

鸿蒙 Flutter 音视频跨设备协同,通过分布式媒体服务与 Flutter 跨端 UI 的融合,实现了 “资源共享、状态同步、多端控制” 的全场景体验。本文从本地播放到跨设备协同,提供了完整的实战方案,开发者可基于此快速落地音视频应用。

7.1 扩展学习资源

7.2 未来展望

随着鸿蒙生态的发展,音视频跨设备协同将支持更多场景:如多设备音视频通话、分布式直播、AR/VR 媒体协同等。Flutter 也将进一步优化与鸿蒙媒体服务的集成,降低跨设备开发门槛。掌握音视频跨设备协同技术,将成为鸿蒙生态开发者的核心竞争力之一。

如果你在实践中遇到具体问题,欢迎在评论区交流讨论。后续将推出更多进阶内容,如鸿蒙 Flutter 直播开发、AR 媒体协同等,敬请关注!

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐