欢迎加入开源鸿蒙跨平台社区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.etsentry/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 离开应用时自动进入画中画

注意:画中画的进入和退出需要通过 VideoRefenterPictureInPicture()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;

📝 最佳实践

  1. 使用 poster 属性显示封面图

    <Video
      source={{ uri: videoUrl }}
      poster={thumbnailUrl}
      posterResizeMode="cover"
    />
    
  2. 监听缓冲状态优化用户体验

    onBuffer={(e) => {
      if (e.isBuffering) {
        showLoadingIndicator();
      } else {
        hideLoadingIndicator();
      }
    }}
    
  3. 画中画功能提升用户体验

    // 通过 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);
      }}
    />
    
  4. 错误处理必不可少

    onError={(e) => {
      console.error('播放错误:', e.error);
      Alert.alert('播放失败', e.error.errorString);
    }}
    
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐