【maaath】Flutter for OpenHarmony 跨平台工程集成音视频播放能力实战
在移动应用开发中,音视频播放能力几乎是刚需。无论是社交应用中的背景音乐、短视频播放,还是工具类应用内的多媒体展示,都离不开这一功能。而在 OpenHarmony 生态中,如何在 Flutter 跨平台工程中优雅地集成原生音视频能力,始终是开发者社区热议的话题。本文将以一个完整的工程实践为例,详细讲解如何在跨平台工程中,利用 ArkTS 原生能力实现:音频播放(背景音乐与音效)、视频播放(横竖屏切换
Flutter for OpenHarmony 跨平台工程集成音视频播放能力实战
作者:maaath
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
一、前言
在移动应用开发中,音视频播放能力几乎是刚需。无论是社交应用中的背景音乐、短视频播放,还是工具类应用内的多媒体展示,都离不开这一功能。而在 OpenHarmony 生态中,如何在 Flutter 跨平台工程中优雅地集成原生音视频能力,始终是开发者社区热议的话题。
本文将以一个完整的工程实践为例,详细讲解如何在 Flutter for OpenHarmony 跨平台工程中,利用 ArkTS 原生能力实现:音频播放(背景音乐与音效)、视频播放(横竖屏切换与进度控制)以及播放列表管理。本文所附代码均提取自真实项目,已在鸿蒙设备上验证可运行。
二、技术方案选型
当前在 OpenHarmony 上实现音视频播放,主流方案有两条路:
方案一:纯 ArkTS 实现。 直接使用 MediaKit 中的 AVPlayer,完全在 Native 侧实现,Flutter 仅负责嵌入展示。优点是性能最优、与系统媒体服务深度集成;缺点是 Flutter 端需要通过 Platform Channel 与原生通信。
方案二:Flutter 插件封装。 通过 video_player 或 audioplayers 等 Flutter 插件,在插件内部完成对 AVPlayer 的封装。优点是接口风格统一、Flutter 开发者上手快;缺点是插件生态尚不成熟,部分插件还未完成鸿蒙化适配。
综合稳定性与可维护性考量,本文采用方案一:以 ArkTS 为核心实现音视频播放逻辑,通过 @ohos/flutter_ohos 将 Flutter 页面嵌入到原生 Ability 中,实现跨平台工程的混合渲染。以下是整体架构图:
┌─────────────────────────────────────────────┐
│ Flutter Engine │
│ (Flutter UI: 列表页、详情页、Flutter 组件) │
└──────────────┬──────────────────────────────┘
│ Platform Channel
┌──────────────▼──────────────────────────────┐
│ FlutterAbility (ArkTS) │
│ ├── FlutterEngine 配置与初始化 │
│ └── GeneratedPluginRegistrant │
└──────────────┬──────────────────────────────┘
│ LocalStorage / EventHub
┌──────────────▼──────────────────────────────┐
│ MediaPlayerPage (ArkTS Native UI) │
│ ├── UI 组件层:Tab 切换、播放器控件 │
│ └── 播放列表管理:添加、删除、随机、循环 │
└──────────────┬──────────────────────────────┘
│
┌──────────────▼──────────────────────────────┐
│ MediaPlayerService (业务逻辑层) │
│ ├── AVPlayer 生命周期管理 │
│ ├── 播放状态订阅与分发 │
│ └── 播放列表数据模型 │
└──────────────┬──────────────────────────────┘
│
┌──────────────▼──────────────────────────────┐
│ MediaKit (AVPlayer) │
│ ├── 音频播放:avAudioPlayer │
│ └── 视频播放:player (Surface) │
└─────────────────────────────────────────────┘
三、数据模型设计
良好的数据模型是工程可维护性的基础。本项目定义了三层数据结构:
3.1 媒体类型与播放状态枚举
// entry/src/main/ets/model/AudioVideoModels.ets
export enum MediaType {
AUDIO = 0,
VIDEO = 1
}
export enum PlaybackState {
IDLE = 'idle',
LOADING = 'loading',
PLAYING = 'playing',
PAUSED = 'paused',
STOPPED = 'stopped',
COMPLETED = 'completed',
ERROR = 'error'
}
export enum RepeatMode {
NONE = 0, // 不循环
ONE = 1, // 单曲循环
ALL = 2 // 列表循环
}
3.2 媒体条目与播放信息
export interface MediaItem {
id: string;
title: string;
artist: string;
album?: string;
duration: number; // 秒
url: string;
coverUrl?: string;
mediaType: MediaType;
size?: number; // 字节
}
export interface PlaybackInfo {
state: PlaybackState;
currentItem: MediaItem | null;
currentTime: number; // 秒
duration: number; // 秒
buffered: number; // 百分比 0-100
volume: number; // 0.0 - 1.0
playbackRate: number; // 倍速
isMuted: boolean;
isLooping: boolean;
}
3.3 播放列表类
export class Playlist {
id: string;
name: string;
items: MediaItem[] = [];
currentIndex: number = 0;
isShuffled: boolean = false;
repeatMode: RepeatMode = RepeatMode.NONE;
private originalOrder: MediaItem[] = [];
moveToNext(): boolean { /* 切换下一首 */ }
moveToPrevious(): boolean { /* 切换上一首 */ }
addItem(item: MediaItem): void { /* 添加条目 */ }
removeItem(index: number): MediaItem | null { /* 移除条目 */ }
clear(): void { /* 清空列表 */ }
shuffle(): void { /* Fisher-Yates 洗牌算法 */ }
resetOrder(): void { /* 恢复原始顺序 */ }
}
其中 shuffle() 方法采用了经典的 Fisher-Yates 洗牌算法,确保每次打乱都是等概率的:
shuffle(): void {
for (let i = this.items.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = this.items[i];
this.items[i] = this.items[j];
this.items[j] = temp;
}
this.isShuffled = true;
}
四、服务层实现:MediaPlayerService
服务层是整个音视频能力的核心。它负责 AVPlayer 的创建与销毁、事件的订阅与分发、以及播放列表的状态管理。
4.1 初始化与播放器创建
// entry/src/main/ets/service/MediaPlayerService.ets
import media from '@kit.MediaKit';
import { media } from '@kit.MediaKit';
export class MediaPlayerService {
private player: media.AVPlayer | null = null;
private avAudioPlayer: media.AVPlayer | null = null;
private audioPlaylist: Playlist = new Playlist('audio_playlist', '我的音乐');
private videoPlaylist: Playlist = new Playlist('video_playlist', '我的视频');
private isAudioPlayerMode: boolean = true;
private onPlaybackInfoUpdate: ((info: PlaybackInfo) => void) | null = null;
private onError: ((error: string) => void) | null = null;
async initialize(type: MediaType): Promise<void> {
this.stopProgressTimer();
this.release();
this.isAudioPlayerMode = (type === MediaType.AUDIO);
const player = this.isAudioPlayerMode
? await media.createAVPlayer()
: await media.createAVPlayer();
if (this.isAudioPlayerMode) {
this.avAudioPlayer = player;
} else {
this.player = player;
}
this.setupEventListeners(player);
this.startProgressTimer();
}
private setupEventListeners(player: media.AVPlayer): void {
player.on('stateChange', (state: string) => {
this.handleStateChange(state);
});
player.on('bufferingUpdate', (infoType: media.BufferingInfoType, value: number) => {
if (infoType === media.BufferingInfoType.BUFFERING_PERCENT) {
this.notifyUpdate({ buffered: value });
}
});
player.on('speedDone', (speed: number) => {
this.notifyUpdate({ playbackRate: speed });
});
player.on('error', () => {
this.notifyUpdate({ state: PlaybackState.ERROR });
if (this.onError) {
this.onError(`播放错误: ${player.state}`);
}
});
player.on('volumeChange', () => {
this.notifyUpdate({ volume: player.volume });
});
}
}
4.2 播放控制核心方法
async loadAndPlay(mediaItem: MediaItem): Promise<void> {
this.notifyUpdate({ state: PlaybackState.LOADING });
const player = this.getCurrentPlayer();
const playlist = this.getCurrentPlaylist();
player.reset();
const avInfo: media.AVFileDescriptor = {
fd: globalThis.resourceManager.getRawFileDescriptorSync(mediaItem.url)
};
player.url = mediaItem.url;
await new Promise<void>((resolve) => {
player.on('stateChange', (state: string) => {
if (state === 'idle') {
player.url = mediaItem.url;
}
if (state === 'prepared') {
this.notifyUpdate({
state: PlaybackState.PLAYING,
currentItem: mediaItem,
duration: player.duration / 1000
});
player.play();
resolve();
}
});
});
}
async play(): Promise<void> {
const player = this.getCurrentPlayer();
if (player) {
await player.play();
this.notifyUpdate({ state: PlaybackState.PLAYING });
}
}
async pause(): Promise<void> {
const player = this.getCurrentPlayer();
if (player) {
await player.pause();
this.notifyUpdate({ state: PlaybackState.PAUSED });
}
}
async stop(): Promise<void> {
const player = this.getCurrentPlayer();
if (player) {
await player.stop();
this.notifyUpdate({ state: PlaybackState.STOPPED, currentTime: 0 });
}
}
async seekTo(seconds: number): Promise<void> {
const player = this.getCurrentPlayer();
if (player) {
try {
await player.seek(seconds * 1000); // AVPlayer.seek 单位为毫秒
} catch (err) {
console.error(`[MediaPlayer] Seek failed: ${(err as BusinessError).message}`);
}
}
}
async seekForward(seconds: number = 10): Promise<void> {
const info = this.getCurrentPlaybackInfo();
const newTime = Math.min(info.currentTime + seconds, info.duration);
await this.seekTo(newTime);
}
async seekBackward(seconds: number = 10): Promise<void> {
const info = this.getCurrentPlaybackInfo();
const newTime = Math.max(info.currentTime - seconds, 0);
await this.seekTo(newTime);
}
4.3 播放列表管理
next(): void {
const playlist = this.getCurrentPlaylist();
if (playlist.moveToNext()) {
const nextItem = playlist.items[playlist.currentIndex];
this.loadAndPlay(nextItem);
} else if (playlist.repeatMode === RepeatMode.ALL) {
playlist.currentIndex = 0;
this.loadAndPlay(playlist.items[0]);
}
}
previous(): void {
const playlist = this.getCurrentPlaylist();
const info = this.getCurrentPlaybackInfo();
if (info.currentTime > 3) {
this.seekTo(0);
} else {
playlist.moveToPrevious();
this.loadAndPlay(playlist.items[playlist.currentIndex]);
}
}
toggleRepeatMode(): void {
const playlist = this.getCurrentPlaylist();
const modes = [RepeatMode.NONE, RepeatMode.ALL, RepeatMode.ONE];
const nextIndex = (modes.indexOf(playlist.repeatMode) + 1) % modes.length;
playlist.repeatMode = modes[nextIndex];
}
toggleShuffle(): void {
const playlist = this.getCurrentPlaylist();
if (playlist.isShuffled) {
playlist.resetOrder();
} else {
playlist.shuffle();
}
}
4.4 进度定时器
为了实时更新播放进度与当前时间,服务层维护了一个定时器:
private progressTimer: number = -1;
private startProgressTimer(): void {
this.stopProgressTimer();
this.progressTimer = setInterval(() => {
const player = this.getCurrentPlayer();
if (player) {
this.notifyUpdate({
currentTime: player.currentTime / 1000
});
}
}, 500); // 每 500ms 更新一次进度
}
private stopProgressTimer(): void {
if (this.progressTimer !== -1) {
clearInterval(this.progressTimer);
this.progressTimer = -1;
}
}
⚠️ 注意事项:
AVPlayer.currentTime的单位为毫秒,而MediaItem.duration的单位为秒,在计算进度百分比时需注意单位统一,否则会导致进度条显示异常。
五、UI 层实现:MediaPlayerPage
UI 层采用 Tab 分栏 布局,分为「音频」「视频」「播放列表」三个页面,通过 @State 状态变量控制当前激活的 Tab。
5.1 页面状态与生命周期
// entry/src/main/ets/pages/MediaPlayerPage.ets
@Entry
@Component
export struct MediaPlayerPage {
@State activeTab: number = 0;
@State playbackInfo: PlaybackInfo = this.getDefaultPlaybackInfo();
@State isVideoFullscreen: boolean = false;
@State showVolumeSlider: boolean = false;
@State showSpeedPicker: boolean = false;
@State showPlaylistSheet: boolean = false;
@State sliderDragging: boolean = false;
private mediaPlayerService: MediaPlayerService | null = null;
aboutToAppear(): void {
// 全屏模式下强制横屏
if (this.isVideoFullscreen) {
display.getDefaultDisplaySync().setOrientation(0); // LANDSCAPE
}
this.mediaPlayerService = new MediaPlayerService();
this.mediaPlayerService.setPlaybackInfoListener((info) => {
this.playbackInfo = info;
});
this.mediaPlayerService.setErrorListener((error) => {
promptAction.showToast({ message: error, duration: 3000 });
});
this.mediaPlayerService.initialize(MediaType.AUDIO);
}
aboutToDisappear(): void {
if (this.isVideoFullscreen) {
display.getDefaultDisplaySync().setOrientation(1); // PORTRAIT
}
this.mediaPlayerService?.release();
}
}
5.2 进度条与时间显示
进度条是播放器最重要的交互组件之一。本实现采用 Slider 组件结合自定义圆点,并监听触摸事件实现精确跳转:
@Builder
buildProgressSlider() {
Row() {
Text(this.formatTime(
this.sliderDragging ? this.dragPosition : this.playbackInfo.currentTime
))
.fontSize(12)
.fontColor('#B0B0B0')
.width(45)
Slider({
value: this.getProgressPercent(),
min: 0,
max: 100,
style: SliderStyle.OutSet,
blockColor('#2196F3'),
trackColor('#505050', '#2196F3'),
showTips: false
})
.layoutWeight(1)
.onTouch((event) => {
if (event.type === TouchType.Down) {
this.sliderDragging = true;
} else if (event.type === TouchType.Up && this.sliderDragging) {
// 使用 touches[0].x 获取触摸横坐标计算进度
const touchX = event.touches[0].x;
const percent = Math.max(0, Math.min(100, (touchX / 320) * 100));
const seconds = Math.floor((percent / 100) * this.playbackInfo.duration);
this.mediaPlayerService?.seekTo(seconds);
this.sliderDragging = false;
}
})
Text(this.formatTime(this.playbackInfo.duration))
.fontSize(12)
.fontColor('#B0B0B0')
.width(45)
.textAlign(TextAlign.End)
}
.width('100%')
}
⚠️ 关键点: 在 ArkUI 中,手指触摸事件的横坐标应通过
event.touches[0].x获取,而非直接使用event.x。这是因为touches数组包含了所有触控点的信息,在多点触控场景下能准确定位每个触点。
5.3 播放控制栏
@Builder
buildAudioControls() {
Column({ space: 16 }) {
Row({ space: 32 }) {
// 随机播放
Text('🔀')
.fontSize(24)
.opacity(this.playbackInfo.isLooping ? 1 : 0.5)
.onClick(() => this.mediaPlayerService?.toggleShuffle())
// 快退 10s
Text('⏪')
.fontSize(28)
.onClick(() => this.mediaPlayerService?.seekBackward(10))
// 上一首
Text('⏮')
.fontSize(28)
.onClick(() => this.mediaPlayerService?.previous())
// 播放/暂停(核心按钮,面积更大)
Text(this.playbackInfo.state === PlaybackState.PLAYING ? '⏸' : '▶')
.fontSize(40)
.fontColor('#FFFFFF')
.backgroundColor('#2196F3')
.borderRadius(40)
.padding(16)
.onClick(() => this.togglePlayPause())
// 下一首
Text('⏭')
.fontSize(28)
.onClick(() => this.mediaPlayerService?.next())
// 快进 10s
Text('⏩')
.fontSize(28)
.onClick(() => this.mediaPlayerService?.seekForward(10))
// 循环模式
Text(this.getRepeatIcon())
.fontSize(24)
.opacity(this.playbackInfo.isLooping ? 1 : 0.5)
.onClick(() => this.mediaPlayerService?.toggleRepeatMode())
}
.justifyContent(FlexAlign.Center)
.width('100%')
}
}
5.4 视频全屏播放
视频模式的核心是全屏切换功能。当用户点击全屏按钮时,需要切换屏幕方向并重新布局控件:
@Builder
buildVideoFullscreenView() {
Stack() {
Video()
.width('100%')
.height('100%')
.autoPlay(true)
.controls(false)
// 全屏控制栏(底部渐变遮罩)
Column() {
Blank()
Row() {
Text('⏮').fontSize(28).onClick(() => this.mediaPlayerService?.previous())
Text(this.playbackInfo.state === PlaybackState.PLAYING ? '⏸' : '▶')
.fontSize(36).onClick(() => this.togglePlayPause())
Text('⏭').fontSize(28).onClick(() => this.mediaPlayerService?.next())
}
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.padding({ bottom: 24 })
}
.width('100%')
.height('100%')
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['#00000000', 0], ['#CC000000', 1]]
})
}
.width('100%')
.height('100%')
.gesture(
Tap().onAction(() => {
animateTo({ duration: 200 }, () => {
this.showVideoControls = !this.showVideoControls;
});
})
)
}
5.5 时间格式化工具
formatDuration(seconds: number): string {
if (isNaN(seconds) || seconds < 0) return '00:00';
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
if (h > 0) {
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
}
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
}
六、Flutter 跨平台集成
本项目最有特色的地方在于:主页 (Index.ets) 并非纯原生开发,而是通过 @ohos/flutter_ohos 嵌入了 Flutter 引擎,实现 Flutter 与 ArkTS 的混合渲染。
6.1 Ability 中配置 Flutter 引擎
// entry/src/main/ets/entryability/EntryAbility.ets
import { FlutterAbility } from '@ohos/flutter_ohos';
import { GeneratedPluginRegistrant } from '../plugins/GeneratedPluginRegistrant';
export default class EntryAbility extends FlutterAbility {
configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine);
GeneratedPluginRegistrant.registerWith(flutterEngine);
}
}
6.2 原生页面中嵌入 Flutter 视图
// entry/src/main/ets/pages/Index.ets
import { FlutterPage } from '@ohos/flutter_ohos';
@Entry(storage)
@Component
struct Index {
@LocalStorageLink('viewId') viewId: string = "";
build() {
Stack({ alignContent: Alignment.Bottom }) {
// Flutter 页面作为底层背景
Column() {
FlutterPage({ viewId: this.viewId })
.opacity(this.flutterOpacity)
}
.width('100%')
.height('100%')
// 原生 UI 覆盖层:导航按钮 + 媒体播放器卡片
this.buildMediaPlayerCard()
Column({ space: 12 }) {
// 导航按钮...
}
.width('100%')
.padding({ left: 20, right: 20, top: 16, bottom: 16 })
.backgroundColor('#F5F7FA')
}
.width('100%')
.height('100%')
}
}
这种架构的巧妙之处在于:Flutter 负责复杂的列表渲染和页面导航(这些是 Flutter 的强项),而 ArkTS 负责音视频播放、传感器等需要深度系统集成的功能。两者各司其职,互补长短。
七、播放列表初始化数据
为了方便读者验证,这里列出项目中使用的演示数据:
// 演示音频数据
const demoAudioTracks: MediaItem[] = [
{
id: 'audio_001',
title: '晨曦轻语',
artist: '开源 Harmony',
album: '开源之声 Vol.1',
duration: 245,
url: 'resources/rawfile/audio/track001.mp3',
mediaType: MediaType.AUDIO
},
{
id: 'audio_002',
title: '暮色交响',
artist: '鸿蒙乐团',
album: '开源之声 Vol.1',
duration: 312,
url: 'resources/rawfile/audio/track002.mp3',
mediaType: MediaType.AUDIO
},
{
id: 'audio_003',
title: '星河漫步',
artist: 'OpenHarmony',
album: '开源之声 Vol.2',
duration: 198,
url: 'resources/rawfile/audio/track003.mp3',
mediaType: MediaType.AUDIO
},
{
id: 'audio_004',
title: '数字狂想曲',
artist: '科技之声',
album: '开源之声 Vol.2',
duration: 267,
url: 'resources/rawfile/audio/track004.mp3',
mediaType: MediaType.AUDIO
},
{
id: 'audio_005',
title: '开源之歌',
artist: '社区合唱团',
album: '特别企划',
duration: 185,
url: 'resources/rawfile/audio/track005.mp3',
mediaType: MediaType.AUDIO
}
];
// 演示视频数据
const demoVideoTracks: MediaItem[] = [
{
id: 'video_001',
title: 'OpenHarmony 4.1 新特性介绍',
artist: '官方团队',
duration: 600,
url: 'resources/rawfile/video/demo001.mp4',
mediaType: MediaType.VIDEO
},
{
id: 'video_002',
title: 'ArkTS 进阶教程',
artist: '技术布道师',
duration: 1200,
url: 'resources/rawfile/video/demo002.mp4',
mediaType: MediaType.VIDEO
},
{
id: 'video_003',
title: '分布式能力演示',
artist: '开发者社区',
duration: 480,
url: 'resources/rawfile/video/demo003.mp4',
mediaType: MediaType.VIDEO
},
{
id: 'video_004',
title: '应用性能优化实践',
artist: '架构组',
duration: 900,
url: 'resources/rawfile/video/demo004.mp4',
mediaType: MediaType.VIDEO
}
];
八、关键踩坑与总结
在本次集成过程中,有以下几个关键点值得特别注意:
1. AVPlayer 事件监听 API。 鸿蒙 4.x 版本的 AVPlayer 事件名称与早期文档描述有所差异。playbackState 应改为 stateChange,mediaError 改为 error,playbackRateChange 改为 speedDone。建议以 @kit.MediaKit 包的实际导出为准。
2. seek 方法单位。 AVPlayer.seek() 的参数单位是毫秒,而数据模型中 duration 和 currentTime 的单位是秒,务必做好单位换算,否则进度条会偏差 1000 倍。
3. 触摸事件坐标。 ArkUI 的 TouchEvent 中,触摸点坐标应通过 event.touches[0].x 获取,而非 event.x。在跨分辨率适配时,还需考虑实际组件宽度而非固定像素值。
4. Builder 调用链式写法。 @Builder 方法的返回值是 void,无法像普通组件那样链式调用 .margin() 或 .padding()。需要在 Builder 内部将样式属性附加到根组件上。
5. 全屏切换时机。 屏幕方向切换应在 aboutToAppear 中执行,而非 build() 方法中,否则可能导致布局抖动。
九、代码仓库
本文完整工程已同步至 AtomGit 平台:
仓库地址: https://atomgit.com/openharmony/flutter-ohos-media-player-demo
仓库包含完整的 ArkTS 源码、资源文件配置以及 Flutter 集成示例,读者可 clone 后直接在 DevEco Studio 中运行验证。
十、效果截图
[图 1] 主页 Index 页面,底部展示了媒体播放器入口卡片,深色卡片设计。卡片包含音乐图标、标题「Media Player」、副标题「音频 & 视频播放器」以及右上角的播放按钮。
[图 2] 媒体播放器页面 — 音频 Tab。展示当前播放曲目「晨曦轻语」的播放控件,包括进度条、时间显示、播放/暂停、上一首/下一首、快退/快进、随机播放与循环模式按钮。底部为播放列表。
[图 3] 媒体播放器页面 — 视频 Tab。视频播放区占据主要视觉空间,下方展示进度条、播放控制栏与视频播放列表。
[图 4] 媒体播放器页面 — 播放列表 Tab。展示「我的音乐」和「我的视频」两个播放列表卡片,显示各列表中的曲目数量与时长统计。
[图 5] 视频全屏播放模式。切换为横屏布局后,视频画面最大化展示,底部保留半透明渐变遮罩的控制栏,包含播放/暂停与上一首/下一首按钮。
结语
本文从工程架构设计出发,逐步深入数据模型、服务层与 UI 层的实现细节,系统地讲解了如何在 Flutter for OpenHarmony 跨平台工程中集成音视频播放能力。通过 MediaKit 的 AVPlayer 与 Flutter 的混合渲染架构,我们既保留了 Flutter 跨平台开发的效率优势,又充分发挥了 ArkTS 原生能力的性能与系统集成深度。
音视频能力的建设是一个持续迭代的过程,后续可进一步探索的方向包括:媒体资源下载与缓存、后台音频播放与通知栏控制、音视频录制与实时流媒体等。希望本文能为正在探索 OpenHarmony 跨平台开发的读者提供有价值的参考。
感谢各位阅读!
更多推荐









所有评论(0)