ReactNative项目OpenHarmony三方库集成实战:react-native-video
视频播放是现代移动应用的核心功能之一,无论是短视频应用、在线教育平台、还是企业培训系统,都需要稳定高效的视频播放能力。是 React Native 生态中最流行的视频播放组件,支持多种视频格式、流媒体协议、以及丰富的播放控制功能。本文将详细介绍如何在 HarmonyOS 平台上集成和使用这个强大的视频播放库。库名称版本信息6.13.2: 支持 RN 0.72 版本6.14.0: 支持 RN 0.7
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

📋 前言
视频播放是现代移动应用的核心功能之一,无论是短视频应用、在线教育平台、还是企业培训系统,都需要稳定高效的视频播放能力。react-native-video 是 React Native 生态中最流行的视频播放组件,支持多种视频格式、流媒体协议、以及丰富的播放控制功能。本文将详细介绍如何在 HarmonyOS 平台上集成和使用这个强大的视频播放库。
🎯 库简介
基本信息
- 库名称:
@react-native-ohos/react-native-video - 版本信息:
6.13.2: 支持 RN 0.72 版本6.14.0: 支持 RN 0.77 版本
- 官方仓库: https://github.com/react-native-oh-library/react-native-video
- 主要功能:
- 🎬 支持本地和网络视频播放
- 📡 支持多种流媒体协议(HLS、DASH、MP4等)
- 🎛️ 完整的播放控制(播放、暂停、快进、音量等)
- 📐 多种缩放模式(contain、cover、stretch等)
- 🖼️ 视频封面图支持
- 📺 画中画模式支持
- 🔊 静音和音量控制
- 📊 播放进度和缓冲状态监听
为什么选择 react-native-video?
| 特性 | 原生Video组件 | react-native-video |
|---|---|---|
| 跨平台一致性 | ⚠️ 需分别处理 | ✅ 统一API |
| 流媒体支持 | ⚠️ 有限 | ✅ HLS/DASH |
| 播放控制 | ⚠️ 基础 | ✅ 完整 |
| 进度监听 | ⚠️ 有限 | ✅ 详细 |
| 画中画 | ❌ 不支持 | ✅ 支持 |
| 封面图 | ❌ 不支持 | ✅ 支持 |
| HarmonyOS | ❌ 不支持 | ✅ 完整支持 |
兼容性验证
在以下环境验证通过:
- RNOH: 0.72.90; SDK: HarmonyOS 6.0.0 Release SDK; IDE: DevEco Studio 6.0.2; ROM: 6.0.0
⚠️ 注意:version >= 5.2.3 的版本需要在 DevEco Studio 5.0.1(API13) 及以上版本编译。
📦 安装步骤
1. 使用 npm 安装
本文基于0.72.90版本开发
在项目根目录执行以下命令:
# RN 0.72 版本
npm install @react-native-ohos/react-native-video@6.13.2-rc.1
# 或者使用 yarn
yarn add @react-native-ohos/react-native-video@6.13.2-rc.1
2. 验证安装
安装完成后,检查 package.json 文件,应该能看到新增的依赖:
{
"dependencies": {
"@react-native-ohos/react-native-video": "6.13.2-rc.1",
// ... 其他依赖
}
}
🔧 HarmonyOS 平台配置 ⭐
由于 HarmonyOS 暂不支持 AutoLink(部分版本支持),需要手动配置原生端代码。
Link支持情况
| 版本 | 是否支持AutoLink | RN版本 |
|---|---|---|
| ~6.14.0 | ❌ No | 0.77 |
| ~6.13.2 | ✅ Yes | 0.72 |
| <= 6.13.1@deprecated | ❌ No | 0.72 |
💡 提示:如果使用支持AutoLink的版本且工程已接入AutoLink,可跳过ManualLink配置。
手动配置步骤(ManualLink)
1. 在工程根目录的 oh-package.json5 添加 overrides 字段

打开 harmony/oh-package.json5,添加以下配置:
{
// ... 其他配置
"overrides": {
"@rnoh/react-native-openharmony": "0.72.90"
}
}
2. 引入原生端代码
通过 HAR 包引入(推荐)
打开 harmony/entry/oh-package.json5,添加以下依赖:
"dependencies": {
"@rnoh/react-native-openharmony": "0.72.90",
"@react-native-ohos/react-native-video": "file:../../node_modules/@react-native-ohos/react-native-video/harmony/rn_video.har"
}
点击 DevEco Studio 右上角的 sync 按钮,或者在终端执行:
cd harmony/entry
ohpm install
3. 配置 CMakeLists 和引入 RNCVideoPackage
⚠️ 注意:若使用的是 <= 6.13.1 版本,请跳过此章节。
修改 entry/src/main/cpp/CMakeLists.txt
project(rnapp)
cmake_minimum_required(VERSION 3.4.1)
set(CMAKE_SKIP_BUILD_RPATH TRUE)
set(RNOH_APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
set(NODE_MODULES "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../node_modules")
set(RNOH_CPP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../react-native-harmony/harmony/cpp")
set(LOG_VERBOSITY_LEVEL 1)
set(CMAKE_ASM_FLAGS "-Wno-error=unused-command-line-argument -Qunused-arguments")
set(CMAKE_CXX_FLAGS "-fstack-protector-strong -Wl,-z,relro,-z,now,-z,noexecstack -s -fPIE -pie")
set(WITH_HITRACE_SYSTRACE 1)
+ set(OH_MODULES "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules")
add_compile_definitions(WITH_HITRACE_SYSTRACE)
add_subdirectory("${RNOH_CPP_DIR}" ./rn)
# 添加 Video 模块
+ add_subdirectory("${OH_MODULES}/@react-native-ohos/react-native-video/src/main/cpp" ./video)
file(GLOB GENERATED_CPP_FILES "./generated/*.cpp")
add_library(rnoh_app SHARED
${GENERATED_CPP_FILES}
"./PackageProvider.cpp"
"${RNOH_CPP_DIR}/RNOHAppNapiBridge.cpp"
)
target_link_libraries(rnoh_app PUBLIC rnoh)
# 链接 Video 库
+ target_link_libraries(rnoh_app PUBLIC rnoh_video)
修改 entry/src/main/cpp/PackageProvider.cpp
#include "RNOH/PackageProvider.h"
#include "generated/RNOHGeneratedPackage.h"
+ #include "RNCVideoPackage.h"
using namespace rnoh;
std::vector<std::shared_ptr<Package>> PackageProvider::getPackages(Package::Context ctx) {
return {
std::make_shared<RNOHGeneratedPackage>(ctx),
+ std::make_shared<RNCVideoPackage>(ctx),
};
}
4. 在 ArkTs 侧引入 RNCVideo 组件 ⭐
⚠️ 重要:本库使用了混合方案,需要在 ArkTS 侧注册组件。
找到 buildCustomComponent 函数,一般位于 entry/src/main/ets/pages/Index.ets 或 entry/src/main/ets/rn/LoadBundle.ets,添加:
import { RNCVideo, RNC_VIDEO_TYPE } from "@react-native-ohos/react-native-video"
@Builder
function buildCustomRNComponent(ctx: ComponentBuilderContext) {
// ... 其他组件
+ if (ctx.componentName === RNC_VIDEO_TYPE) {
+ RNCVideo({
+ ctx: ctx.rnComponentContext,
+ tag: ctx.tag
+ })
+ }
}
然后在同一文件中找到 arkTsComponentNames 常量数组,添加组件名:
const arkTsComponentNames: Array<string> = [
// ... 其他组件名
+ RNC_VIDEO_TYPE
];
5. 在 ArkTs 侧引入 RNCVideoPackage
打开 harmony/entry/src/main/ets/RNPackagesFactory.ts,添加:
import type { RNPackageContext, RNPackage } from 'rnoh/ts';
+ import { RNCVideoPackage } from '@react-native-ohos/react-native-video/ts';
export function createRNPackages(ctx: RNPackageContext): RNPackage[] {
return [
// ... 其他包
+ new RNCVideoPackage(ctx),
];
}
6. 同步并运行
点击 DevEco Studio 右上角的 sync 按钮,然后编译运行即可。
📖 API 详解
🔷 核心属性(Props)
1. source - 视频源配置 ⭐
source 是 Video 组件最核心的属性,用于配置视频的加载源。
source: {
uri: string; // 视频 URL(必填)
headers?: object; // 自定义请求头(可选)
isNetwork?: boolean; // 是否网络视频
type?: string; // 视频类型
}
| 参数 | 类型 | 必填 | 说明 | HarmonyOS支持 |
|---|---|---|---|---|
uri |
string | ✅ | 视频URL地址 | ✅ |
headers |
object | ❌ | 自定义HTTP请求头 | ✅ |
isNetwork |
boolean | ❌ | 是否网络视频 | ✅ |
type |
string | ❌ | 视频MIME类型 | ✅ |
使用示例:
// 网络视频
<Video
source={{ uri: 'https://example.com/video.mp4', isNetwork: true }}
style={styles.video}
/>
// 本地视频
<Video
source={require('./video.mp4')}
style={styles.video}
/>
// 带请求头
<Video
source={{
uri: 'https://example.com/protected.mp4',
headers: { Authorization: 'Bearer token' },
isNetwork: true
}}
style={styles.video}
/>
2. resizeMode - 缩放模式 🎨
控制视频如何适应容器尺寸。
resizeMode: 'none' | 'contain' | 'cover' | 'stretch';
| 模式 | 说明 | 效果描述 |
|---|---|---|
none |
原始尺寸 | 视频原尺寸显示 |
contain |
等比缩放,完整显示 | 视频完整显示,可能有留白 |
cover |
等比缩放,填满容器 | 视频填满容器,可能被裁剪 |
stretch |
拉伸填满 | 视频拉伸填满容器,可能变形 |
3. 播放控制属性
| 属性 | 类型 | 默认值 | 说明 | HarmonyOS支持 |
|---|---|---|---|---|
paused |
boolean | false | 是否暂停 | ✅ |
muted |
boolean | false | 是否静音 | ✅ |
volume |
number | 1.0 | 音量(0-1) | ✅ |
repeat |
boolean | false | 是否循环播放 | ✅ |
rate |
number | 1.0 | 播放速率 | ✅ |
controls |
boolean | false | 显示原生控制条 | ✅ |
disableFocus |
boolean | false | 禁用焦点 | ✅ |
4. 封面图属性
| 属性 | 类型 | 说明 | HarmonyOS支持 |
|---|---|---|---|
poster |
string | 封面图URL | ✅ |
posterResizeMode |
string | 封面图缩放模式 | ✅ |
5. 画中画属性
| 属性 | 类型 | 说明 | HarmonyOS支持 |
|---|---|---|---|
enterPictureInPictureOnLeave |
boolean | 离开应用时自动进入画中画 | ✅ |
注意:画中画的进入和退出需要通过
VideoRef的enterPictureInPicture()和exitPictureInPicture()方法控制,而不是通过属性。
🔷 播放状态回调(Events)
1. onLoad - 加载成功 ✅
视频元数据加载成功时触发。
onLoad: (event: OnLoadData) => void;
interface OnLoadData {
currentPosition: number; // 当前位置(秒)
duration: number; // 总时长(秒)
naturalSize: {
width: number; // 视频原始宽度
height: number; // 视频原始高度
orientation: string; // 方向
};
}
使用示例:
<Video
source={{ uri: videoUrl }}
onLoad={(e) => {
console.log(`视频加载成功`);
console.log(`时长: ${e.duration}秒`);
console.log(`尺寸: ${e.naturalSize.width}x${e.naturalSize.height}`);
}}
/>
2. onLoadStart - 开始加载
视频开始加载时触发。
onLoadStart: (event: OnLoadStartData) => void;
interface OnLoadStartData {
isNetwork: boolean; // 是否网络视频
type: string; // 视频类型
uri: string; // 视频地址
}
3. onProgress - 播放进度 📊
视频播放过程中持续触发。
onProgress: (event: OnProgressData) => void;
interface OnProgressData {
currentTime: number; // 当前播放时间(秒)
playableDuration: number; // 已缓冲可播放时长(秒)
seekableDuration: number; // 可拖动时长(秒)
}
使用示例:
<Video
source={{ uri: videoUrl }}
onProgress={(e) => {
const progress = (e.currentTime / duration) * 100;
console.log(`播放进度: ${progress.toFixed(1)}%`);
}}
/>
4. onBuffer - 缓冲状态
视频缓冲状态变化时触发。
onBuffer: (event: OnBufferData) => void;
interface OnBufferData {
isBuffering: boolean; // 是否正在缓冲
}
5. onError - 播放错误 ❌
视频播放出错时触发。
onError: (event: OnErrorData) => void;
interface OnErrorData {
error: {
errorString: string; // 错误信息
errorException: string; // 异常类型
};
}
6. onEnd - 播放结束
视频播放结束时触发。
onEnd: () => void;
7. onPlaybackStateChanged - 播放状态变化
播放状态改变时触发。
onPlaybackStateChanged: (event: OnPlaybackStateChangedData) => void;
interface OnPlaybackStateChangedData {
isPlaying: boolean; // 是否正在播放
isSeeking: boolean; // 是否正在拖动
}
8. onPictureInPictureStatusChanged - 画中画状态变化
画中画状态改变时触发。
onPictureInPictureStatusChanged: (event) => void;
// event.isActive: boolean - 是否处于画中画模式
🔷 组件方法(Methods)
通过 ref 调用组件方法:
import { VideoRef } from 'react-native-video';
const videoRef = useRef<VideoRef>(null);
<Video ref={videoRef} ... />
1. seek - 跳转播放位置
videoRef.current?.seek(seconds);
2. presentFullscreenPlayer - 进入全屏
videoRef.current?.presentFullscreenPlayer();
3. dismissFullscreenPlayer - 退出全屏
videoRef.current?.dismissFullscreenPlayer();
💻 完整代码示例
下面是一个完整的视频播放器示例:
import React, { useState, useRef, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
TextInput,
Switch,
Alert,
} from 'react-native';
import Video, {
VideoRef,
OnLoadData,
OnProgressData,
OnBufferData,
OnPlaybackStateChangedData,
OnPictureInPictureStatusChangedData,
} from 'react-native-video';
function VideoPlayerDemo() {
const videoRef = useRef<VideoRef>(null);
// 视频源
const [videoUri, setVideoUri] = useState(
'https://res.vmallres.com//uomcdn/CN/cms/202210/C75C7E20060F3E909F2998E13C3ABC03.mp4'
);
// 播放控制
const [paused, setPaused] = useState(false);
const [muted, setMuted] = useState(false);
const [repeat, setRepeat] = useState(false);
const [controls, setControls] = useState(true);
// 缩放模式
const [resizeMode, setResizeMode] = useState<'none' | 'contain' | 'cover' | 'stretch'>('contain');
// 画中画
const [enterPictureInPictureOnLeave, setEnterPictureInPictureOnLeave] = useState(false);
const [isPictureInPicture, setIsPictureInPicture] = useState(false);
// 视频信息
const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState(0);
const [isBuffering, setIsBuffering] = useState(false);
const [isPlaying, setIsPlaying] = useState(false);
// 跳转秒数
const [seekSeconds, setSeekSeconds] = useState('10');
// 加载成功
const handleLoad = useCallback((e: OnLoadData) => {
setDuration(e.duration);
console.log(`视频加载成功: ${e.duration}秒, ${e.naturalSize.width}x${e.naturalSize.height}`);
}, []);
// 加载开始
const handleLoadStart = useCallback((e: any) => {
console.log('开始加载视频:', e.uri);
}, []);
// 播放进度
const handleProgress = useCallback((e: OnProgressData) => {
setCurrentTime(e.currentTime);
}, []);
// 缓冲状态
const handleBuffer = useCallback((e: OnBufferData) => {
setIsBuffering(e.isBuffering);
console.log('缓冲状态:', e.isBuffering ? '缓冲中' : '缓冲完成');
}, []);
// 播放结束
const handleEnd = useCallback(() => {
console.log('播放结束');
Alert.alert('提示', '视频播放结束');
}, []);
// 播放错误
const handleError = useCallback((e: any) => {
console.error('播放错误:', e.error);
Alert.alert('错误', `视频播放失败: ${e.error.errorString}`);
}, []);
// 播放状态变化
const handlePlaybackStateChanged = useCallback((e: OnPlaybackStateChangedData) => {
setIsPlaying(e.isPlaying);
console.log('播放状态:', JSON.stringify(e));
}, []);
// 画中画状态变化
const handlePictureInPictureStatusChanged = useCallback((e: OnPictureInPictureStatusChangedData) => {
console.log('画中画状态:', e.isActive);
setIsPictureInPicture(e.isActive);
}, []);
// 进入画中画
const enterPiP = useCallback(() => {
videoRef.current?.enterPictureInPicture();
}, []);
// 退出画中画
const exitPiP = useCallback(() => {
videoRef.current?.exitPictureInPicture();
}, []);
// 跳转
const handleSeek = useCallback(() => {
const seconds = parseInt(seekSeconds, 10);
if (!isNaN(seconds)) {
videoRef.current?.seek(seconds);
}
}, [seekSeconds]);
// 格式化时间
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
// 切换视频源
const videoSources = [
{
name: '华为视频',
uri: 'https://res.vmallres.com//uomcdn/CN/cms/202210/C75C7E20060F3E909F2998E13C3ABC03.mp4',
},
{
name: 'Ocean',
uri: 'https://vjs.zencdn.net/v/oceans.mp4',
},
];
return (
<ScrollView style={styles.container}>
{/* 视频播放器 */}
<View style={styles.videoContainer}>
<Video
ref={videoRef}
source={{ uri: videoUri, isNetwork: true }}
style={styles.video}
resizeMode={resizeMode}
paused={paused}
muted={muted}
repeat={repeat}
controls={controls}
volume={1}
enterPictureInPictureOnLeave={enterPictureInPictureOnLeave}
poster="https://res.vmallres.com/pimages/uomcdn/CN/pms/202304/sbom/4002010007801/group/800_800_9B1356F1330EADDCB20D35D2AE1F46E0.jpg"
posterResizeMode="cover"
onLoad={handleLoad}
onLoadStart={handleLoadStart}
onProgress={handleProgress}
onBuffer={handleBuffer}
onEnd={handleEnd}
onError={handleError}
onPlaybackStateChanged={handlePlaybackStateChanged}
onPictureInPictureStatusChanged={handlePictureInPictureStatusChanged}
/>
{/* 播放状态指示器 */}
{isBuffering && (
<View style={styles.bufferingIndicator}>
<Text style={styles.bufferingText}>缓冲中...</Text>
</View>
)}
</View>
{/* 播放信息 */}
<View style={styles.infoContainer}>
<Text style={styles.infoText}>
{formatTime(currentTime)} / {formatTime(duration)}
</Text>
<Text style={styles.statusText}>
状态: {isPlaying ? '播放中' : '已暂停'}
{isBuffering ? ' (缓冲中)' : ''}
{isPictureInPicture ? ' (画中画)' : ''}
</Text>
</View>
{/* 视频源切换 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>视频源</Text>
<View style={styles.buttonRow}>
{videoSources.map((source, index) => (
<TouchableOpacity
key={index}
style={[
styles.button,
videoUri === source.uri && styles.buttonActive,
]}
onPress={() => setVideoUri(source.uri)}
>
<Text style={styles.buttonText}>{source.name}</Text>
</TouchableOpacity>
))}
</View>
</View>
{/* 缩放模式 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>缩放模式</Text>
<View style={styles.buttonRow}>
{(['none', 'contain', 'cover', 'stretch'] as const).map((mode) => (
<TouchableOpacity
key={mode}
style={[
styles.button,
resizeMode === mode && styles.buttonActive,
]}
onPress={() => setResizeMode(mode)}
>
<Text style={styles.buttonText}>{mode}</Text>
</TouchableOpacity>
))}
</View>
</View>
{/* 播放控制 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>播放控制</Text>
<View style={styles.buttonRow}>
<TouchableOpacity
style={[styles.button, paused && styles.buttonActive]}
onPress={() => setPaused(!paused)}
>
<Text style={styles.buttonText}>{paused ? '播放' : '暂停'}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, muted && styles.buttonActive]}
onPress={() => setMuted(!muted)}
>
<Text style={styles.buttonText}>{muted ? '取消静音' : '静音'}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, repeat && styles.buttonActive]}
onPress={() => setRepeat(!repeat)}
>
<Text style={styles.buttonText}>{repeat ? '取消循环' : '循环'}</Text>
</TouchableOpacity>
</View>
</View>
{/* 跳转控制 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>跳转控制</Text>
<View style={styles.seekRow}>
<TextInput
style={styles.seekInput}
value={seekSeconds}
onChangeText={setSeekSeconds}
keyboardType="numeric"
placeholder="秒数"
/>
<TouchableOpacity style={styles.seekButton} onPress={handleSeek}>
<Text style={styles.buttonText}>跳转</Text>
</TouchableOpacity>
</View>
</View>
{/* 画中画 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>画中画</Text>
<View style={styles.buttonRow}>
<TouchableOpacity
style={[styles.button, isPictureInPicture && styles.buttonActive]}
onPress={isPictureInPicture ? exitPiP : enterPiP}
>
<Text style={styles.buttonText}>
{isPictureInPicture ? '退出画中画' : '进入画中画'}
</Text>
</TouchableOpacity>
</View>
<View style={styles.switchRow}>
<Text style={styles.switchLabel}>离开应用时自动进入画中画</Text>
<Switch
value={enterPictureInPictureOnLeave}
onValueChange={setEnterPictureInPictureOnLeave}
/>
</View>
</View>
{/* 控制条开关 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>显示设置</Text>
<View style={styles.switchRow}>
<Text style={styles.switchLabel}>显示原生控制条</Text>
<Switch
value={controls}
onValueChange={setControls}
/>
</View>
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5F5F5',
},
videoContainer: {
position: 'relative',
backgroundColor: '#000',
},
video: {
width: '100%',
height: 220,
},
bufferingIndicator: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
bufferingText: {
color: '#FFF',
fontSize: 16,
},
infoContainer: {
padding: 16,
backgroundColor: '#FFF',
borderBottomWidth: 1,
borderBottomColor: '#E0E0E0',
},
infoText: {
fontSize: 18,
fontWeight: 'bold',
color: '#333',
textAlign: 'center',
},
statusText: {
fontSize: 14,
color: '#666',
textAlign: 'center',
marginTop: 4,
},
section: {
padding: 16,
backgroundColor: '#FFF',
marginTop: 8,
},
sectionTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#333',
marginBottom: 12,
},
buttonRow: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
},
button: {
paddingHorizontal: 16,
paddingVertical: 10,
backgroundColor: '#E0E0E0',
borderRadius: 8,
minWidth: 80,
alignItems: 'center',
},
buttonActive: {
backgroundColor: '#007AFF',
},
buttonText: {
fontSize: 14,
color: '#333',
},
seekRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
seekInput: {
flex: 1,
height: 44,
borderWidth: 1,
borderColor: '#E0E0E0',
borderRadius: 8,
paddingHorizontal: 12,
fontSize: 16,
backgroundColor: '#FFF',
},
seekButton: {
paddingHorizontal: 20,
paddingVertical: 12,
backgroundColor: '#007AFF',
borderRadius: 8,
},
switchRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 8,
},
switchLabel: {
fontSize: 15,
color: '#333',
},
});
export default VideoPlayerDemo;
📝 最佳实践
-
使用 poster 属性显示封面图
<Video source={{ uri: videoUrl }} poster={thumbnailUrl} posterResizeMode="cover" /> -
监听缓冲状态优化用户体验
onBuffer={(e) => { if (e.isBuffering) { showLoadingIndicator(); } else { hideLoadingIndicator(); } }} -
画中画功能提升用户体验
// 通过 ref 控制画中画 const videoRef = useRef<VideoRef>(null); // 进入画中画 const enterPiP = () => { videoRef.current?.enterPictureInPicture(); }; // 退出画中画 const exitPiP = () => { videoRef.current?.exitPictureInPicture(); }; // Video 组件配置 <Video ref={videoRef} enterPictureInPictureOnLeave={true} onPictureInPictureStatusChanged={(e) => { console.log('画中画状态:', e.isActive); }} /> -
错误处理必不可少
onError={(e) => { console.error('播放错误:', e.error); Alert.alert('播放失败', e.error.errorString); }}
更多推荐


所有评论(0)