在这里插入图片描述
内容由熊哈哈O_o大佬指导创作:https://blog.csdn.net/qq_61024956?type=blog

📋 前言

react-native-video 是 React Native 社区最流行的视频播放库,提供了一套完整的视频播放解决方案。它支持本地视频、网络视频、直播流等多种视频源,提供了丰富的播放控制功能(播放、暂停、seek、倍速播放等),并且完全兼容 Android、iOS 和 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

  • 主要功能:

    • 支持本地视频、网络视频、直播流
    • 丰富的播放控制(播放、暂停、seek、倍速)
    • 支持画中画模式
    • 支持字幕、多音轨选择
    • 兼容 Android、iOS 和 HarmonyOS
  • 兼容性验证:

    • RNOH: 0.72.96; SDK: HarmonyOS 6.0.0 Release SDK; IDE: DevEco Studio 6.0.0.858; ROM: 6.0.0.112;
    • RNOH: 0.72.33; SDK: HarmonyOS NEXT B1; IDE: DevEco Studio: 5.0.3.900; ROM: Next.0.0.71;
    • RNOH: 0.77.18; SDK: HarmonyOS 6.0.0 Release SDK; IDE: DevEco Studio 6.0.0.858; ROM: 6.0.0.112;

为什么需要这个库?

  • 功能完整: 提供完整的视频播放解决方案
  • 性能优异: 原生渲染,流畅播放
  • 跨平台: 在三端提供一致的播放体验
  • 易于集成: API 简单直观
  • 丰富的控制: 支持播放、暂停、seek、倍速等多种控制

📦 安装步骤

1. 使用 npm 安装

根据您的 RN 版本选择对应的包名:

npm install @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,需要手动配置原生端代码。本文采用方法二:直接链接源码的方式。

1. 在工程根目录的 oh-package.json5 添加 overrides 字段

首先需要使用 DevEco Studio 打开项目里的 HarmonyOS 工程 harmony

打开 harmony/oh-package.json5,添加以下配置:

{
  ...
  "overrides": {
    "@rnoh/react-native-openharmony": "^0.72.90"
  }
}

2. 引入原生端代码

目前有两种方法:

方法一:通过 har 包引入

[!TIP] har 包位于三方库安装路径的 harmony 文件夹下。

打开 entry/oh-package.json5,添加以下依赖:

"dependencies": {
    "@react-native-ohos/react-native-video": "file:../../node_modules/@react-native-ohos/react-native-video/harmony/rn_video.har"
}

点击右上角的 sync 按钮

或者在终端执行:

cd entry
ohpm install
方法二:直接链接源码(我用的这个)

目前 DevEco Studio 不支持通过源码引入外部 module,我们推荐使用直接链接源码的方式,将源码通过操作改成 harmony 工程的内部模块。

步骤 1: 把 <RN工程>/node_modules/@react-native-ohos/react-native-video/harmony 目录下的源码 rn_video 复制到 harmony(鸿蒙壳工程)工程根目录下。

步骤 2: 在 harmony 工程根目录的 build-profile.template.json5(若存在)和 build-profile.json5 添加以下模块:

modules: [
  ...
  {
    name: '<xxx>',
    srcPath: './<xxx>',
  },
  {
    name: 'rn_video',
    srcPath: './rn_video',
  }
]

步骤 3: 打开 rn_video/oh-package.json5,修改 react-native-openharmony 和项目的版本一致。

步骤 4: 打开 entry/oh-package.json5,添加以下依赖:

"dependencies": {
  "@rnoh/react-native-openharmony": "0.72.90",
  "@react-native-ohos/react-native-video": "file:../rn_video"
}

步骤 5: 点击 DevEco Studio 右上角的 sync 按钮

3. 配置 CMakeLists 和引入 RNCVideoPackage

打开 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(OH_MODULES "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_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,noexecstack,-s -fPIE -pie")
set(WITH_HITRACE_SYSTRACE 1) # for other CMakeLists.txt files to use
add_compile_definitions(WITH_HITRACE_SYSTRACE)

add_subdirectory("${RNOH_CPP_DIR}" ./rn)

# RNOH_BEGIN: manual_package_linking_1
add_subdirectory("../../../../sample_package/src/main/cpp" ./sample-package)
+ add_subdirectory("${OH_MODULES}/@react-native-ohos/react-native-video/src/main/cpp" ./video)
# RNOH_END: manual_package_linking_1

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)

# RNOH_BEGIN: manual_package_linking_2
target_link_libraries(rnoh_app PUBLIC rnoh_sample_package)
+ target_link_libraries(rnoh_app PUBLIC rnoh_video)
# RNOH_END: manual_package_linking_2

打开 entry/src/main/cpp/PackageProvider.cpp,添加:

#include "RNOH/PackageProvider.h"
#include "generated/RNOHGeneratedPackage.h"
#include "SamplePackage.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<SamplePackage>(ctx),
+     std::make_shared<RNCVideoPackage>(ctx)
    };
}

4. 在 ArkTs 侧引入 RNCVideo 组件

找到 function 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
+   })
+ }
...
}

[!TIP] 本库使用了混合方案,需要添加组件名。

entry/src/main/ets/pages/index.etsentry/src/main/ets/rn/LoadBundle.ets 找到常量 arkTsComponentNames 在其数组里添加组件名

const arkTsComponentNames: Array<string> = [
+ RNC_VIDEO_TYPE
  ];

5. 在 ArkTs 侧引入 RNCVideoPackage

打开 entry/src/main/ets/RNPackagesFactory.ts,添加:

+ import { RNCVideoPackage } from '@react-native-ohos/react-native-video/ts';

export function createRNPackages(ctx: RNPackageContext): RNPackage[] {
  return [
    new SamplePackage(ctx),
+   new RNCVideoPackage(ctx)
  ];
}

6. 运行

点击右上角的 sync 按钮

或者在终端执行:

cd entry
ohpm install

然后编译、运行即可。

💻 完整代码示例

下面是一个完整的示例,展示了 react-native-video 的各种使用场景:

import React, { useState, useRef } from 'react';
import {
  View,
  StyleSheet,
  Text,
  ScrollView,
  TouchableOpacity,
  Switch,
  TextInput,
} from 'react-native';
import RNCVideo, { type VideoRef } from 'react-native-video';
import {
  type OnPlaybackStateChangedData,
  OnSeekData,
} from 'react-native-video';

function VideoDemo() {
  const [muted, setMuted] = useState(true);
  const [paused, setPaused] = useState(false);
  const [repeat, setRepeat] = useState(true);
  const [controls, setControls] = useState(false);
  const [disableFocus, setDisableFocus] = useState(false);
  const [uri, setUri] = useState(
    'https://res.vmallres.com//uomcdn/CN/cms/202210/C75C7E20060F3E909F2998E13C3ABC03.mp4'
  );
  const [resizeMode, setResizeMode] = useState<'contain' | 'cover' | 'stretch'>('contain');
  const [seekSec, setSeekSec] = useState(5000);

  const [onVideoLoad, setOnVideoLoad] = useState('onVideoLoad');
  const [onVideoLoadStart, setOnVideoLoadStart] = useState('onVideoLoadStart');
  const [onVideoError, setOnVideoError] = useState('onVideoError');
  const [onVideoProgress, setOnVideoProgress] = useState('onVideoProgress');
  const [onVideoEnd, setOnVideoEnd] = useState('onVideoEnd');
  const [onVideoBuffer, setOnVideoBuffer] = useState('onVideoBuffer');
  const [onPlaybackStateChanged, setPlaybackStateChanged] = useState('onPlaybackStateChanged');
  
  const [enterPictureInPictureOnLeave, setEnterPictureInPictureOnLeave] = useState(false);

  const videoRef = useRef<VideoRef>(null);

  const toggleMuted = () => {
    setMuted((prevMuted) => !prevMuted);
  };

  const toggleControls = () => {
    setControls((prevControls) => !prevControls);
  };

  const togglePaused = () => {
    setPaused((prevPaused) => !prevPaused);
  };

  const toggleRepeat = () => {
    setRepeat((prevRepeat) => !prevRepeat);
  };

  const toggleDisableFocus = () => {
    setDisableFocus((prevDisableFocus) => !prevDisableFocus);
  };

  const firstVideo = () => {
    setUri('https://vjs.zencdn.net/v/oceans.mp4');
  };

  const secondVideo = () => {
    setUri('https://res.vmallres.com//uomcdn/CN/cms/202210/C75C7E20060F3E909F2998E13C3ABC03.mp4');
  };

  const changeResizeMode = (mode: string) => {
    setResizeMode(mode as any);
  };

  return (
    <ScrollView style={styles.container}>
      <View style={styles.content}>
        <Text style={styles.title}>网络视频播放器</Text>
  
        {/* 视频播放器 */}
        <RNCVideo
          style={styles.video}
          ref={videoRef}
          source={{ uri, isNetwork: true }}
          paused={paused}
          muted={muted}
          enterPictureInPictureOnLeave={enterPictureInPictureOnLeave}
          resizeMode={resizeMode}
          controls={controls}
          repeat={repeat}
          volume={1}
          disableFocus={disableFocus}
          poster="https://res.vmallres.com/pimages/uomcdn/CN/pms/202304/sbom/4002010007801/group/800_800_9B1356F1330EADDCB20D35D2AE1F46E0.jpg"
          onLoad={(e) => {
            setOnVideoLoad(
              `onVideoLoad currentTime =${e.currentTime}s duration =${e.duration}s width =${e.naturalSize.width} orientation =${e.naturalSize.orientation}`
            );
            setOnVideoError('onVideoError error = ok');
          }}
          onLoadStart={(e) => {
            setOnVideoLoadStart(
              `onVideoLoadStart isNetwork =${e.isNetwork} type=${e.type} uri=${e.uri}`
            );
          }}
          onProgress={(e) => {
            setOnVideoProgress(
              `onVideoProgress currentTime =${e.currentTime} playableDuration=${e.playableDuration} seekableDuration=${e.seekableDuration}`
            );
          }}
          onSeek={(data: OnSeekData) => {
            console.log('onSeek');
          }}
          onError={(e) => {
            setOnVideoError(`onVideoError error =${e.error}`);
          }}
          onEnd={() => {
            setOnVideoEnd('onVideoEnd completed');
          }}
          onBuffer={(e) => {
            setOnVideoBuffer(`onVideoBuffer :${e.isBuffering}`);
          }}
          onPlaybackStateChanged={(data: OnPlaybackStateChangedData) => {
            console.log('onPlaybackStateChanged ' + JSON.stringify(data));
            setPlaybackStateChanged('onPlaybackStateChanged : ' + JSON.stringify(data));
          }}
          onReadyForDisplay={() => {
            console.log('onReadyForDisplay');
          }}
        />

        {/* 事件信息 */}
        <View style={styles.infoSection}>
          <Text style={styles.infoText}>{onVideoLoad}</Text>
          <Text style={styles.infoText}>{onVideoError}</Text>
          <Text style={styles.infoText}>{onVideoLoadStart}</Text>
          <Text style={styles.infoText}>{onVideoProgress}</Text>
          <Text style={styles.infoText}>{onVideoEnd}</Text>
          <Text style={styles.infoText}>{onVideoBuffer}</Text>
          <Text style={styles.infoText}>{onPlaybackStateChanged}</Text>
        </View>

        {/* 视频源切换 */}
        <Text style={styles.sectionTitle}>切换视频源</Text>
        <View style={styles.buttonRow}>
          <TouchableOpacity
            style={[styles.button, styles.buttonBlue]}
            onPress={firstVideo}
          >
            <Text style={styles.buttonText}>视频1</Text>
          </TouchableOpacity>
          <TouchableOpacity
            style={[styles.button, styles.buttonRed]}
            onPress={secondVideo}
          >
            <Text style={styles.buttonText}>视频2</Text>
          </TouchableOpacity>
        </View>

        {/* ResizeMode 切换 */}
        <Text style={styles.sectionTitle}>缩放模式</Text>
        <View style={styles.buttonRow}>
          {['contain', 'stretch', 'cover'].map((mode) => (
            <TouchableOpacity
              key={mode}
              style={[
                styles.button,
                resizeMode === mode && styles.buttonActive,
              ]}
              onPress={() => changeResizeMode(mode)}
            >
              <Text style={styles.buttonText}>{mode}</Text>
            </TouchableOpacity>
          ))}
        </View>

        {/* Seek 控制 */}
        <Text style={styles.sectionTitle}>Seek 控制</Text>
        <View style={styles.seekRow}>
          <TextInput
            style={styles.input}
            placeholder="输入秒数"
            keyboardType="numeric"
            onChangeText={(text) => {
              const newText = text.replace(/[^\d]+/, '');
              setSeekSec(Number(newText));
            }}
            value={seekSec.toString()}
          />
          <TouchableOpacity
            style={styles.button}
            onPress={() => videoRef.current?.seek(seekSec)}
          >
            <Text style={styles.buttonText}>Seek</Text>
          </TouchableOpacity>
        </View>

        {/* 控制按钮 */}
        <Text style={styles.sectionTitle}>播放控制</Text>
        <View style={styles.buttonRow}>
          <TouchableOpacity
            style={styles.button}
            onPress={togglePaused}
          >
            <Text style={styles.buttonText}>暂停: {paused.toString()}</Text>
          </TouchableOpacity>
          <TouchableOpacity
            style={styles.button}
            onPress={toggleMuted}
          >
            <Text style={styles.buttonText}>静音: {muted.toString()}</Text>
          </TouchableOpacity>
          <TouchableOpacity
            style={styles.button}
            onPress={toggleControls}
          >
            <Text style={styles.buttonText}>控件: {controls.toString()}</Text>
          </TouchableOpacity>
          <TouchableOpacity
            style={styles.button}
            onPress={toggleRepeat}
          >
            <Text style={styles.buttonText}>循环: {repeat.toString()}</Text>
          </TouchableOpacity>
        </View>

        {/* 画中画控制 */}
        <Text style={styles.sectionTitle}>画中画</Text>
        <View style={styles.row}>
          <Text style={styles.label}>应用返回桌面时自动启动画中画:</Text>
          <Switch
            trackColor={{ false: '#767577', true: '#81b0ff' }}
            thumbColor={enterPictureInPictureOnLeave ? '#f5dd4b' : '#f4f3f4'}
            ios_backgroundColor="#3e3e3e"
            onValueChange={setEnterPictureInPictureOnLeave}
            value={enterPictureInPictureOnLeave}
          />
        </View>
        <View style={styles.buttonRow}>
          <TouchableOpacity
            style={styles.button}
            onPress={() => videoRef.current?.enterPictureInPicture()}
          >
            <Text style={styles.buttonText}>开启画中画</Text>
          </TouchableOpacity>
          <TouchableOpacity
            style={styles.button}
            onPress={() => videoRef.current?.exitPictureInPicture()}
          >
            <Text style={styles.buttonText}>关闭画中画</Text>
          </TouchableOpacity>
        </View>
      </View>
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5',
  },
  content: {
    padding: 20,
  },
  title: {
    fontSize: 20,
    fontWeight: 'bold',
    marginBottom: 20,
    textAlign: 'center',
  },
  video: {
    width: '100%',
    height: 260,
    backgroundColor: '#000',
    marginBottom: 20,
  },
  infoSection: {
    marginBottom: 20,
    padding: 10,
    backgroundColor: '#fff',
    borderRadius: 8,
  },
  infoText: {
    fontSize: 12,
    color: '#666',
    marginBottom: 4,
  },
  sectionTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    marginBottom: 10,
    marginTop: 20,
  },
  buttonRow: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    gap: 10,
    marginBottom: 10,
  },
  button: {
    paddingHorizontal: 15,
    paddingVertical: 10,
    backgroundColor: '#42a5f5',
    borderRadius: 8,
    minWidth: 80,
  },
  buttonBlue: {
    backgroundColor: '#42a5f5',
  },
  buttonRed: {
    backgroundColor: '#ef5350',
  },
  buttonActive: {
    backgroundColor: '#66bb6a',
  },
  buttonText: {
    color: '#fff',
    fontSize: 14,
    fontWeight: '500',
    textAlign: 'center',
  },
  seekRow: {
    flexDirection: 'row',
    alignItems: 'center',
    gap: 10,
  },
  input: {
    flex: 1,
    height: 40,
    borderWidth: 1,
    borderColor: '#ddd',
    borderRadius: 8,
    paddingHorizontal: 10,
    backgroundColor: '#fff',
  },
  row: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    marginBottom: 10,
  },
  label: {
    fontSize: 14,
    color: '#333',
  },
});

export default VideoDemo;

🎨 实际应用场景

react-native-video 可以应用于以下实际场景:

  1. 视频播放器: 实现完整的视频播放功能
  2. 直播应用: 支持直播流的播放
  3. 视频课程: 播放在线视频课程
  4. 社交媒体: 用户上传视频的播放
  5. 画中画: 支持画中画模式,边看视频边做其他事情

⚠️ 注意事项与最佳实践

1. 视频源配置

// ✅ 网络视频
source={{ uri: 'https://example.com/video.mp4', isNetwork: true }}

// ✅ 本地视频
source={{ uri: require('./video.mp4') }}

// ✅ 直播流
source={{ uri: 'rtmp://example.com/live/stream' }}

2. 视频控制

使用 ref 控制视频播放:

const videoRef = useRef<RNCVideo>(null);

// Seek 到指定位置
videoRef.current?.seek(5000);

// 暂停/播放
videoRef.current?.setNativeProps({ paused: true });

3. 性能优化

  • 避免同时播放多个视频
  • 使用 poster 属性添加封面图,提升加载体验
  • 使用 resizeMode 控制视频缩放方式

4. HarmonyOS 特殊处理

在 HarmonyOS 上,确保:

  • 已正确配置原生端代码(参考上述 HarmonyOS 配置步骤)
  • 测试不同视频格式的兼容性
  • 注意 HarmonyOS 设备的音频焦点问题

5. 画中画

画中画功能需要系统权限,确保:

  • 应用有悬浮窗口权限
  • 视频支持画中画模式
  • 正确配置 enterPictureInPictureOnLeave 属性

关键要点回顾

  • 安装依赖: npm install @react-native-ohos/react-native-video
  • 配置平台: 通过 har 包或直接链接源码,配置 CMakeLists.txt、PackageProvider.cpp、RNPackagesFactory.ts 和 buildCustomComponent
  • 集成代码: 使用 Video 组件
  • 支持功能: 网络视频、本地视频、直播流、画中画等
  • 播放控制: 播放、暂停、seek、倍速、循环等
  • 重要: 本库使用了混合方案,需要在 ArkTs 侧添加组件名和 RNCVideo 组件

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

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

更多推荐