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.etsentry/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 离开应用时自动进入画中画

注意 :画中画的进入和退出需要通过 VideoRefenterPictureInPicture()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);
    }}
相关推荐
請你喝杯Java2 小时前
基于 TypeScript React Next.js 的 AI 产品技术栈调研报告
javascript·react.js·typescript
Irene19912 小时前
JavaScript中的深克隆和浅克隆的区别(“浅克隆”和“浅复制”通常指的是同一个概念)
javascript·深克隆·浅克隆
兔年鸿运Q小Q2 小时前
vue 使用public数据
前端·javascript·vue.js
wuhen_n2 小时前
开发环境优化完全指南:告别等待,让开发如丝般顺滑
前端·javascript·vue.js
时寒的笔记2 小时前
js逆向入门03_会展中心案例&shuwei观察&ji思录
开发语言·前端·javascript
执行部之龙2 小时前
js垃圾回收
javascript·gc回收
小道士写程序2 小时前
3D雷达锥体 - Cesium兼容版
javascript
大黄说说3 小时前
Vue 3 + Vite 高性能项目最佳实践(2026 版)
前端·javascript·vue.js
予你@。3 小时前
# Vue2 + Element UI 表格合并实战:第二列按「第一列 + 第二列」条件合并
前端·javascript·vue.js