ReactNative项目OpenHarmony三方库集成实战:react-native-video

欢迎加入开源鸿蒙跨平台社区 :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版本开发

在项目根目录执行以下命令:

bash 复制代码
# 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 文件,应该能看到新增的依赖:

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,添加以下配置:

json5 复制代码
{
  // ... 其他配置
  "overrides": {
    "@rnoh/react-native-openharmony": "0.72.90"
  }
}
2. 引入原生端代码

通过 HAR 包引入(推荐)

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

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 按钮,或者在终端执行:

bash 复制代码
cd harmony/entry
ohpm install
3. 配置 CMakeLists 和引入 RNCVideoPackage

⚠️ 注意:若使用的是 <= 6.13.1 版本,请跳过此章节。

修改 entry/src/main/cpp/CMakeLists.txt

cmake 复制代码
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

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,添加:

typescript 复制代码
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 常量数组,添加组件名:

typescript 复制代码
const arkTsComponentNames: Array<string> = [
  // ... 其他组件名
  + RNC_VIDEO_TYPE
];
5. 在 ArkTs 侧引入 RNCVideoPackage

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

typescript 复制代码
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 组件最核心的属性,用于配置视频的加载源。

typescript 复制代码
source: {
  uri: string;           // 视频 URL(必填)
  headers?: object;      // 自定义请求头(可选)
  isNetwork?: boolean;   // 是否网络视频
  type?: string;         // 视频类型
}
参数 类型 必填 说明 HarmonyOS支持
uri string ✅ 视频URL地址 ✅
headers object ❌ 自定义HTTP请求头 ✅
isNetwork boolean ❌ 是否网络视频 ✅
type string ❌ 视频MIME类型 ✅

使用示例:

typescript 复制代码
// 网络视频
<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 - 缩放模式 🎨

控制视频如何适应容器尺寸。

typescript 复制代码
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 - 加载成功 ✅

视频元数据加载成功时触发。

typescript 复制代码
onLoad: (event: OnLoadData) => void;

interface OnLoadData {
  currentPosition: number;      // 当前位置(秒)
  duration: number;             // 总时长(秒)
  naturalSize: {
    width: number;              // 视频原始宽度
    height: number;             // 视频原始高度
    orientation: string;        // 方向
  };
}

使用示例:

typescript 复制代码
<Video
  source={{ uri: videoUrl }}
  onLoad={(e) => {
    console.log(`视频加载成功`);
    console.log(`时长: ${e.duration}秒`);
    console.log(`尺寸: ${e.naturalSize.width}x${e.naturalSize.height}`);
  }}
/>
2. onLoadStart - 开始加载

视频开始加载时触发。

typescript 复制代码
onLoadStart: (event: OnLoadStartData) => void;

interface OnLoadStartData {
  isNetwork: boolean;    // 是否网络视频
  type: string;          // 视频类型
  uri: string;           // 视频地址
}
3. onProgress - 播放进度 📊

视频播放过程中持续触发。

typescript 复制代码
onProgress: (event: OnProgressData) => void;

interface OnProgressData {
  currentTime: number;          // 当前播放时间(秒)
  playableDuration: number;     // 已缓冲可播放时长(秒)
  seekableDuration: number;     // 可拖动时长(秒)
}

使用示例:

typescript 复制代码
<Video
  source={{ uri: videoUrl }}
  onProgress={(e) => {
    const progress = (e.currentTime / duration) * 100;
    console.log(`播放进度: ${progress.toFixed(1)}%`);
  }}
/>
4. onBuffer - 缓冲状态

视频缓冲状态变化时触发。

typescript 复制代码
onBuffer: (event: OnBufferData) => void;

interface OnBufferData {
  isBuffering: boolean;  // 是否正在缓冲
}
5. onError - 播放错误 ❌

视频播放出错时触发。

typescript 复制代码
onError: (event: OnErrorData) => void;

interface OnErrorData {
  error: {
    errorString: string;   // 错误信息
    errorException: string; // 异常类型
  };
}
6. onEnd - 播放结束

视频播放结束时触发。

typescript 复制代码
onEnd: () => void;
7. onPlaybackStateChanged - 播放状态变化

播放状态改变时触发。

typescript 复制代码
onPlaybackStateChanged: (event: OnPlaybackStateChangedData) => void;

interface OnPlaybackStateChangedData {
  isPlaying: boolean;     // 是否正在播放
  isSeeking: boolean;     // 是否正在拖动
}
8. onPictureInPictureStatusChanged - 画中画状态变化

画中画状态改变时触发。

typescript 复制代码
onPictureInPictureStatusChanged: (event) => void;

// event.isActive: boolean - 是否处于画中画模式

🔷 组件方法(Methods)

通过 ref 调用组件方法:

typescript 复制代码
import { VideoRef } from 'react-native-video';

const videoRef = useRef<VideoRef>(null);

<Video ref={videoRef} ... />
1. seek - 跳转播放位置
typescript 复制代码
videoRef.current?.seek(seconds);
2. presentFullscreenPlayer - 进入全屏
typescript 复制代码
videoRef.current?.presentFullscreenPlayer();
3. dismissFullscreenPlayer - 退出全屏
typescript 复制代码
videoRef.current?.dismissFullscreenPlayer();

💻 完整代码示例

下面是一个完整的视频播放器示例:

typescript 复制代码
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 属性显示封面图

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

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

    typescript 复制代码
    // 通过 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. 错误处理必不可少

    typescript 复制代码
    onError={(e) => {
      console.error('播放错误:', e.error);
      Alert.alert('播放失败', e.error.errorString);
    }}
相关推荐
程序员Sunday4 小时前
JavaScript 事件循环面试题,宏任务与微任务怎么执行|Sunday面试指南
开发语言·javascript·面试·校招·事件循环·程序员sunday
默_笙5 小时前
🌊 向量库和 ES 都查不出"关系",我只好给奶茶建了一张人脉网
前端·javascript
思无邪666 小时前
用 AI 做 JS 逆向:从抓包到复现的完整方法论
开发语言·javascript·人工智能
福兮说7 小时前
浏览器里把 iPhone 的 HEIC 转成 JPG:Chrome 解不开、转完大了七成、拍摄时间全丢,六个坑实测
前端·javascript·图像处理·chrome·ios·iphone·heic
可乐鸡翅yeah_8 小时前
video.js 集成 hls.js 开发 M3U8 播放器,新手高频踩坑
开发语言·前端·javascript·后端·ecmascript·m3u8·音视频在线播放
用户1179536547810 小时前
TRAE vs Claude Code 性能对比:从代码生成、多文件重构到底层吞吐,谁更适合你的日常开发?
javascript
kill52211 小时前
useRef
前端·javascript·react.js
米多科技11 小时前
我做了一个 3D 虚拟世界基底,在上面再搭 3D 世界或元宇宙,事半功倍
javascript·人工智能·github
霍营_托尼11 小时前
纯前端播 40GB 本地视频?我把浏览器改造成了「磁盘流式」播放器
前端·javascript·音视频开发
西柚小萌新12 小时前
【LLM&&AI应用开发 八股文】--4.2.Agent智能体(中)
前端·javascript·react.js