expo-video 演进全览:Expo 跨平台视频播放器的能力图谱与升级指南
【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo
本文以 packages/expo-video/CHANGELOG.md 为脉络主线,结合
packages/expo-video包内的 TypeScript 类型定义、Android(Kotlin/ExoPlayer)与 iOS(Swift/AVFoundation)原生实现、config plugin 源码,系统梳理 Expo 官方视频组件expo-video从 2023 年 10 月首发至今的完整能力演进。读者将掌握该组件当前支持的全部核心特性(多轨选择、DRM、缓存、画中画、全屏、Seek/Scrubbing 优化、背景播放等)、各版本破坏性变更带来的升级注意事项,以及这些能力在仓库源码中的落地位置,可直接用于选型评估与迁移升级。
一、expo-video 是什么:定位与版本演进脉络
expo-video是 Expo 生态中面向 React Native 与 Web 的跨平台、高性能视频组件(见 packages/expo-video/package.json 中 "A cross-platform, performant video component for React Native and Expo with Web support" 的描述),支持 Android、iOS 与 Web 三端,底层分别基于 AndroidX Media3 / ExoPlayer(Android)、AVFoundation / AVPlayer(iOS)与 HTML5 Video(Web)。
从 CHANGELOG.md 的版本序列可以清晰看到该项目的发展轨迹:
- 2023-10-30(0.1.0):iOS 首发;
- 2023-11~2024-06(0.2.x → 1.2.x):Android 首发、事件系统、DRM、背景播放、画中画(PiP)、全屏等能力密集落地;
- 2024-10~2025-08(2.0.0-preview → 3.0.x):事件 API 重构(
useEvent支持)、音轨/字幕轨、缓存、缩略图生成等大特性合入; - 2026-01 起(55.0.0 → 57.0.2):版本号与 Expo SDK 版本序列对齐(从 3.0.15 直接跳至 55.0.0,可推断为跟随 SDK 版本策略),并持续引入 Seek 容差、Scrubbing 模式、自适应码率上限等进阶能力。
截至 2026-07-22,仓库当前版本为57.0.2,Unpublished 段还记录着若干待发布变更(controllerAutoShow、maxResolution、videoChangeFrameRateStrategy等),说明该组件仍在活跃迭代中。
二、核心能力图谱:从 CHANGELOG 提炼的功能版图
将 CHANGELOG 中所有 "New features" 条目按能力域归类,可以得到expo-video的完整功能版图。以下每项能力都可在仓库源码中找到对应落地。
2.1 播放控制基础能力
| 能力 | 引入版本 | 说明 |
|---|---|---|
| 播放/暂停/循环/倍速/当前时间 | 1.1.0 | loop、playbackRate、preservesPitch、currentTime属性 |
| 音量与静音 | 1.1.0(Web)/ 1.2.x | volume、muted,事件系统在 2.0.0-preview.0 拆分为volumeChange与mutedChange |
| 时长与直播信息 | 1.1.9 / 1.2.0 | 全平台duration、isLive;1.2.6 增加currentLiveTimestamp、currentOffsetFromLive等直播进阶配置 |
| 替换源 | 2.1.7 | replaceAsync(异步加载,不阻塞主线程) |
| 直接创建实例 | 2.0.0-preview.1 | 支持new VideoPlayer()直接实例化 |
这些属性、方法与默认值的完整定义见 src/VideoPlayer.types.ts:例如loop默认false、volume默认1.0、playbackRate取值0~16.0、timeUpdateEventInterval默认0(为 0 时不触发timeUpdate)。注意volume与muted相互独立——静音不改变音量值,设置音量也不会自动取消静音。
2.2 事件系统:从首次支持到useEvent友好
事件是 expo-video 交互的核心。CHANGELOG 中事件相关演进包括:
- 1.1.0:Android/iOS 事件支持;
- 1.2.3:Web 事件支持;
- 2.0.0-preview.0(破坏性变更):所有播放器事件返回类型统一为单个对象,以更好支持
useEventhook;同时将volumeChange拆分为volumeChange和mutedChange; - 2.1.0:新增
sourceLoad事件(源元数据加载完成)、VideoView的onFirstFrameRender事件(首帧渲染回调,可用于隐藏封面图); - 1.2.6:新增
timeUpdate事件及配套timeUpdateEventInterval属性。
完整事件清单见 src/VideoPlayerEvents.types.ts,包括statusChange、playingChange、playbackRateChange、volumeChange、mutedChange、playToEnd、timeUpdate、sourceChange、sourceLoad、videoTrackChange/audioTrackChange/subtitleTrackChange及对应的available*TracksChange,iOS 独有isExternalPlaybackActiveChange(AirPlay 状态变更)。sourceLoad的 payload 同时携带duration与三组可用轨道数组(availableVideoTracks/availableSubtitleTracks/availableAudioTracks),是构建"清晰度/字幕/音轨切换面板"的数据基础。
2.3 多轨支持:音轨、字幕与视频轨(含 HLS 细节)
这是 expo-video 区别于轻量视频库的关键能力:
- 2.0.0-preview.2:支持列出与选择字幕轨(closed captions);
- 2.2.0:支持音轨——
player.audioTrack设置当前音轨、player.availableAudioTracks列出可用音轨; - 2.1.0:支持列出可用视频轨与当前播放视频轨;
- 55.0.0:视频轨新增
averageBitrate/peakBitrate(原bitrate标记为 deprecated)、url(HLS 轨 URL)、videoRange(SDR/HLG/PQ); - 55.0.7:HLS 视频轨增加
url字段;AudioTrack/SubtitleTrack增加name、isDefault、autoSelect字段。
在源码层面,轨道相关类型定义于 src/VideoPlayer.types.ts:VideoTrack包含id、url、size、mimeType、isSupported(Android)、bitrate/averageBitrate/peakBitrate、frameRate、videoRange;AudioTrack与SubtitleTrack均含language、label等字段。使用时有两点平台注意(源码 JSDoc 明确标注):
- iOS 上使用 HLS 源时,URL 需含
.m3u8扩展名,或将VideoSource.contentType显式设为'hls',否则视频轨不可用; - CHANGELOG 55.0.16 修复了 HLS 多音轨场景下
availableVideoTracks的重复问题,55.0.7 起 iOS 26+ 的 HLS 视频轨获取逻辑已更新。
2.4 缓存能力与缓存管理 API
2.1.0引入缓存功能。缓存相关约束(CHANGELOG 与类型定义双重确认):
- iOS 上 HLS 源无法使用缓存(平台限制);
- Android/iOS 均不支持对 DRM 保护视频使用缓存;
- 缓存时会考虑 Authorization 等鉴权请求头(Unpublished 段修复项)。
配套的缓存管理 API 定义在 src/VideoModule.ts:
setVideoCacheSizeAsync(sizeBytes):设置缓存上限(字节),默认 1GB,持久生效;缓存按 LRU(最近最少使用)淘汰,实际占用可能略超设定值;clearVideoCacheAsync():清空全部视频缓存;getCurrentVideoCacheSize():查询当前缓存占用字节数。
两个写入类 API 都要求当前不存在任何VideoPlayer实例时方可调用。缓存机制的 iOS 原生实现位于 ios/Cache 目录(VideoCacheManager、CachableRequest、MediaFileHandle等),Unpublished 段还修复了两个缓存崩溃:缓存裁剪期间 open-file 注册表的数据竞争(iOS)、写入缓存时磁盘写满导致NSFileHandleOperationException崩溃(iOS,改用可捕获的 Swift 抛错 API)。
2.5 DRM:ClearKey / PlayReady / Widevine / FairPlay
1.1.0引入 Android/iOS DRM 支持,1.2.0增加 iOS FairPlay base64 证书支持。DRM 类型与选项定义在 src/VideoPlayer.types.ts:
export type DRMType = 'clearkey' | 'fairplay' | 'playready' | 'widevine'; // Android: ClearKey、PlayReady、Widevine;iOS: FairPlay export type DRMOptions = { type: DRMType; licenseServer: string; // 许可服务器 URL headers?: Record<string, string>; // 许可请求头 multiKey?: boolean; // Android 多密钥 DRM contentId?: string; // iOS certificateUrl?: string; // iOS FairPlay 证书 URL base64CertificateData?: string; // iOS base64 证书,设置后忽略 certificateUrl };DRM 选项挂在VideoSourceObject.drm字段上;鉴权头请用DRMOptions.headers而非VideoSourceObject.headers(后者仅用于视频请求本身)。iOS 原生实现见 ios/ContentKeyManager.swift 与 ios/ContentKeyDelegate.swift。
2.6 画中画(PiP)与全屏
PiP 演进:0.3.0(iOS)→1.1.0(Android)→1.2.3起 PiP 必须通过 config plugin 开启 →1.2.6(Web)→ 2.2.2 修复非 16:9 源自动进入 PiP 时的窗口宽高比。
全屏演进:1.1.0(Android 全屏)→1.2.6全屏进入/退出事件 →3.0.0全屏方向与自动退出功能 →3.0.5Android 全屏模式始终启用原生控件(对齐 iOS)→55.0.0移除allowsFullscreenprop(改用fullscreenOptions.enable)。
相关类型定义在 src/VideoView.types.ts:
export type FullscreenOptions = { enable: boolean; // 是否提供全屏入口,默认 true orientation?: FullscreenOrientation; // default/portrait/landscape 等 7 种 autoExitOnRotate?: boolean; // 旋转到非指定方向时自动退出全屏,默认 false keepFullscreenOnPiPStop?: KeepFullscreenOnPiPStopBehavior; // iOS,'always'|'autoEnter'|'never' };VideoView的 PiP 相关 props 包括allowsPictureInPicture、startsPictureInPictureAutomatically(Android 12+ / iOS,默认false)、onPictureInPictureStart/onPictureInPictureStop回调;模块级还有isPictureInPictureSupported()能力探测函数(见 src/VideoModule.ts)。CHANGELOG 中 PiP 相关的 bug fix 非常多(55.0.10 修复 PiP 从全屏自动进入后立即退出的问题等),可见这是多端适配的高频风险区。
2.7 背景播放与 Now Playing 通知
- 1.1.0:背景播放支持;
- 1.1.5 / 1.1.6:iOS / Android 自定义 Now Playing 通知;
- 1.2.6(破坏性变更):
showNowPlayingNotification默认值改为false; - 3.0.9(破坏性变更):Android 上要显示 Now Playing 通知,config plugin 的
supportsBackgroundPlayback必须为true; - 56.1.4:修复与 expo-audio 混用后锁屏控件失效的问题。
相关播放器属性:showNowPlayingNotification(默认false)、staysActiveInBackground(默认false)、keepScreenOnWhilePlaying(默认true,Android 上仅当VideoView可见时生效)。注意 iOS 上 Now Playing 通知依赖音频模式——当audioMixingMode不是doNotMix或auto时该功能不可用(见AudioMixingMode类型 JSDoc)。
背景播放与 PiP 的清单级配置由 config plugin 完成,实现见 plugin/src/withExpoVideo.ts:
export type WithExpoVideoOptions = { supportsBackgroundPlayback?: boolean; // 是否启用背景播放 supportsPictureInPicture?: boolean; // 是否启用 Android/iOS 画中画 };该插件会:向 iOSInfo.plist的UIBackgroundModes写入/移除audio;在 Android 上设置主 Activity 的android:supportsPictureInPicture;开启背景播放时注入FOREGROUND_SERVICE与FOREGROUND_SERVICE_MEDIA_PLAYBACK权限并注册ExpoVideoPlaybackService前台服务(android:foregroundServiceType="mediaPlayback",绑定MediaSessionServiceintent-filter)。
2.8 Seek 精度与 Scrubbing 模式(55.0.0 起的新进阶能力)
55.0.0 引入了面向精细拖动进度条场景的两组配置:
export type SeekTolerance = { toleranceBefore?: number; // 实际 seek 位置可提前的最大秒数,默认 0 toleranceAfter?: number; // 实际 seek 位置可延后的最大秒数,默认 0 };容差越大通常 seek 越快;它影响currentTime赋值、seekBy()的精度,Android 上还影响默认原生控件进度条的拖动精度。
export type ScrubbingModeOptions = { scrubbingModeEnabled?: boolean; // 总开关,默认 false;Android 开启后播放会被抑制 increaseCodecOperatingRate?: boolean; // 是否提升编解码器工作频率,默认 true enableDynamicScheduling?: boolean; // ExoPlayer 动态调度,默认 true useDecodeOnlyFlag?: boolean; // API 34+ 使用 MediaCodec.BUFFER_FLAG_DECODE_ONLY 加速 seek allowSkippingMediaCodecFlush?: boolean; // 允许跳过解码器 flush,默认 true };scrubbingModeOptions建议在用户拖拽进度条的短时段内开启、结束时关闭;Android 上开启后播放被抑制,务必在交互结束后恢复。配合增大seekTolerance可获得最佳拖动体验。
2.9 自适应流控制:maxResolution 与视频刷新率策略(Unpublished 新特性)
Unpublished 段(即尚未发布、已在 main 分支)记录了两项值得关注的 Android 新能力:
maxResolution播放器选项:为自适应视频轨选择设置上限——播放器会选择"不超过该分辨率"的最高质量轨。Android 上是硬约束(若无满足条件的轨则回退到最低分辨率轨);iOS 上是软提示(首选上限),且 iOS 仅对 HLS 源生效。对应属性maxResolution: VideoSize | null(null表示不设限,见 src/VideoPlayer.types.ts)。videoChangeFrameRateStrategyplayer builder 选项:控制 ExoPlayer 是否允许修改显示刷新率以匹配视频帧率。默认'onlyIfSeamless'(仅在无缝切换时匹配);设为'off'可避免自适应刷新率屏(如 Pixel 9/10 系列)在播放 30fps 视频时将整个 App UI(含滚动与动画)压制到 30Hz。帧率匹配主要利好电视类大屏,垂直视频流等场景建议'off'。
这两个选项分别落在VideoPlayer.maxResolution属性与PlayerBuilderOptions.videoChangeFrameRateStrategy字段上。后者在 Android 侧由 android/src/main/java/expo/modules/video/records/PlayerBuilderOptions.kt 声明(含seekBackwardIncrement/seekForwardIncrement,取值会被钳制在 0.001~999 秒之间),枚举定义在 android/src/main/java/expo/modules/video/enums/VideoChangeFrameRateStrategy.kt。
2.10 其他实用能力
- 缩略图生成:2.0.0-preview.0 引入,2.1.0 增加
maxWidth/maxHeight限制;generateThumbnailsAsync(times, options)返回原生图片引用(SharedRef<'image'>),可直接作为expo-image的Image源。相关实现见 ios/Thumbnails 与 Android 侧代码,类型见 src/VideoPlayer.types.ts 的VideoThumbnailOptions。 - 缓冲控制:2.0.0-preview.0 引入
BufferOptions——preferredForwardBufferDuration(Android 默认 20s,iOS 默认 0 即自动)、waitsToMinimizeStalling(iOS)、minBufferForPlayback(Android 默认 2s)、maxBufferBytes(Android 默认 0 即自动)、prioritizeTimeOverSizeThreshold(Android)。 - 音频混合模式:2.0.0-preview.1 引入
audioMixingMode,取值为mixWithOthers/duckOthers/auto/doNotMix。多播放器并发时按doNotMix > auto > duckOthers > mixWithOthers取最高优先级。Unpublished 段修复了 iOS 默认值——按文档改为auto(原为doNotMix)。 - Web 专项:1.2.6 支持 Web PiP;1.2.3 修复
AudioContext提前创建问题;2.1.5 增加playsInline;2.1.9/3.0.0/3.0.1 围绕crossOrigin反复调整(3.0.0 默认改为anonymous后又回退为undefined);3.0.4 增加实验性useAudioNodePlayback(多实例同时播放时不叠加音量,已知可能破坏部分源的音频);3.0.11 提供nativeRef访问底层HTMLVideoElement;55.0.0 修复旧版 Safari 崩溃。 - AirPlay(iOS):3.0.0 完整支持,含设备选择按钮组件
VideoAirPlayButton(见 src/VideoAirPlayButton.ios.tsx)、isExternalPlaybackActive属性及监听;2.0.0-preview.0 增加allowsExternalPlayback控制。 - PHAsset 支持(iOS):3.0.11 支持播放
PHAssetURI,但只能通过replaceAsync()或默认构造函数加载。 - Surface 类型(Android):2.1.6 起可通过
surfaceType: 'surfaceView' | 'textureView'选择渲染表面,默认surfaceView(功耗更低、性能更好),多视频重叠等场景改用textureView。
三、VideoView:视图层能力与控件配置
VideoView是承载播放器的视图组件,核心 props 见 src/VideoView.types.ts:player(可传null,55.0.0 起支持)、nativeControls(默认true;全屏模式下因平台限制始终启用)、contentFit(contain/cover/fill,默认contain)、showsTimecodes(iOS)、requiresLinearPlayback、surfaceType、contentPosition(iOS)、allowsVideoFrameAnalysis(iOS 16+,默认true)等。
Android 专属的buttonOptions(即 CHANGELOG 55.0.6 新增的buttonConfiguration,现已重命名)提供细粒度控件可见性控制:
export type ButtonOptions = { showNext?: boolean; // 默认 false(55.0.6 起隐藏) showPrevious?: boolean; // 默认 false showSeekForward?: boolean; // 默认 true showSeekBackward?: boolean;// 默认 true showSubtitles?: boolean | null; // undefined=有字幕时显示 showSettings?: boolean; // 默认 true showPlayPause?: boolean; // 默认 true showBottomBar?: boolean; // 默认 true;全屏下始终可见 };Unpublished 段新增的controllerAutoShow(Android,默认true)控制原生控件是否在播放开始/暂停/结束时自动显示;设为false后控件不再自动弹出,但仍可点击视图唤起——适合程序化驱动的自动连播列表,避免每次播放时控件闪现。
另外,55.0.6 的PlayerBuilderOptions(seekBackwardIncrement/seekForwardIncrement)会直接作用于原生控件上的快进/快退按钮步长。
四、Hooks 与生命周期:useVideoPlayer / createVideoPlayer
两种创建播放器的方式定义在 src/VideoPlayer.tsx:
useVideoPlayer(source, setup?, playerBuilderOptions?):推荐用法,组件卸载时自动释放播放器。实现细节值得注意——当source变化时复用现有播放器调用replaceAsync而非重建(对应 CHANGELOG Unpublished 段的 "When source changes use replaceAsync instead of re-creating the player"),仅当 builder options 变化或replaceAsync失败时才重建实例。createVideoPlayer(source, playerBuilderOptions?):创建不自动释放的直接实例,需手动管理生命周期。
useVideoPlayer的第三个参数playerBuilderOptions对应 Android 原生PlayerBuilderOptions(seek 步长、videoChangeFrameRateStrategy),类型定义见 src/VideoPlayer.types.ts。注意VideoPlayer.replace()在 iOS 上会同步在主线程加载资源并可能长时间阻塞 UI(源码中 JS 层在调用前会打印弃用警告),应优先使用replaceAsync()。
五、测试与 Mock:jest-expo 下的可测试性
CHANGELOG Unpublished 段记录了一次重要的测试基础设施修复:修复jest-expo预设下导入expo-video抛TypeError: Cannot read properties of undefined (reading 'prototype')的问题——原因是VideoPlayer.tsx在模块加载时对NativeVideoModule.VideoPlayer.prototype.replace打补丁,而自动生成的 mock 表无法表示 SharedObject 类。
修复方案是手写 mock:mocks/ExpoVideo.ts。该文件由jest-expo预设注入到requireNativeModule('ExpoVideo'),实现了带内存状态的VideoPlayer(play/pause/seek/replace 可真实触发状态与事件)与VideoThumbnail,并保持与公共类型定义一致的默认值(如audioMixingMode: 'auto'、bufferOptions的跨平台默认值)。文件头注释明确要求不要用expo-modules-test-core重新生成,以免覆盖手写行为。配套的组件测试可参考 src/tests。
六、破坏性变更速查:升级前必读
汇总 CHANGELOG 中全部 Breaking changes,按版本排序:
| 版本 | 破坏性变更 | 应对建议 |
|---|---|---|
| 2.0.0-preview.0 | 事件返回类型改为单对象;volumeChange拆分为volumeChange+mutedChange;iOS/tvOS 最低版本升至 15.1 | 升级useEvent消费方与事件参数解构 |
| 1.2.6 | showNowPlayingNotification默认改为false | 需要时显式置true |
| 1.2.3 | PiP 必须通过 config plugin 开启 | 配置supportsPictureInPicture |
| 2.1.9 / 3.0.0 / 3.0.1 | WebcrossOrigin默认值两次调整,最终为undefined(不启用 CORS) | 需要 CORS 时显式设置crossOrigin='anonymous' |
| 3.0.5 | Android 全屏模式始终启用原生控件 | 无需处理,行为对齐 iOS |
| 3.0.9 | Android Now Playing 通知依赖supportsBackgroundPlayback: true | 配置 config plugin |
| 55.0.0 | 移除allowsFullscreenprop | 改用fullscreenOptions.enable |
| 55.0.6 | Android 原生控件默认隐藏上一首/下一首按钮 | 用buttonOptions.showNext/showPrevious重新开启 |
| 56.0.0 | 最低版本提升:iOS/tvOS 16.4、macOS 13.4 | 确认工程最低系统版本满足要求 |
| 0.2.0 | AndroidcompileSdkVersion/targetSdkVersion升至 34 | 保持 Gradle 配置同步 |
依赖层面,Android 侧 media3 依赖持续升级(1.4.0 → 1.8.0 → 1.9.0 → 1.9.1,对应 1.2.6 / 3.0.6 / 56.0.0 / Unpublished 段的版本记录),升级时如遇到 ExoPlayer 相关异常可优先检查依赖版本一致性。
七、结语:从变更日志看 expo-video 的设计取向
回看整个 CHANGELOG,expo-video的演进呈现出三条清晰主线:一是性能与体验打磨——replaceAsync异步加载、iOS 主线程减负、缓存并发与磁盘写满崩溃修复、Seek/Scrubbing 优化,均指向"流畅、不卡主线程"的工程目标;二是企业级媒体能力补齐——DRM、多轨选择、HLS 细节字段、缓存管理 API,使其足以支撑点播/直播类生产应用;三是多端一致性收敛——Android 全屏控件、PiP 行为、audioMixingMode默认值等不断向 iOS 与官方文档对齐。
对开发者而言,升级expo-video前建议:对照本文第六节速查表核查破坏性变更;背景播放与 PiP 功能务必确认 config plugin 配置(plugin/src/withExpoVideo.ts);涉及轨道信息的业务优先消费sourceLoad事件与available*Tracks属性;如需精细的进度条拖动体验,从SeekTolerance与ScrubbingModeOptions组合调优入手。
【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考