Semi Design VideoPlayer 视频播放器组件实战指南:从基础播放到清晰度切换与原生能力控制
2026/9/24 15:58:52 网站建设 项目流程

Semi Design VideoPlayer 视频播放器组件实战指南:从基础播放到清晰度切换与原生能力控制

【免费下载链接】semi-design🚀A modern, comprehensive, flexible design system and React UI library, AI-friendly built-in.🎨Provide 3000+ Design Tokens, easy to build your design system. Make Semi Design to Any Design.🧑🏻‍💻 Design to Code in one click项目地址: https://gitcode.com/gh_mirrors/se/semi-design

Semi Design 的VideoPlayer@douyinfe/semi-ui)是一个开箱即用的 React 视频播放器组件,本文围绕其在当前仓库中的官方文档(content/plus/videoPlayer/index-en-US.md,中文版见 content/plus/videoPlayer/index.md)展开,逐一讲解从引入、基础播放、菜单栏定制、倍速/音量/清晰度/线路切换、章节标记到通过 ref 操控原生<video>元素的完整用法,并结合仓库源码揭示其底层实现原理(Foundation 状态机、进度条分区渲染、键盘快捷键、全屏滚动位置恢复等),帮助你在业务中快速落地一个功能完备的播放器。

快速开始:引入与基本用法

VideoPlayer与 Semi Design 其他组件一样,直接从@douyinfe/semi-ui引入即可(组件在 packages/semi-ui/index.ts 中被统一导出,实现位于 packages/semi-ui/videoPlayer/index.tsx):

import { VideoPlayer } from '@douyinfe/semi-ui';

基本使用只需两个核心属性:通过src传入视频地址,通过poster传入视频封面地址,再配合height指定高度:

import React from 'react'; import { VideoPlayer } from '@douyinfe/semi-ui'; () => { const src = "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/vchart/landingPage/vchart-show-video.mp4"; const poster = "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/poster2.jpeg"; return ( <VideoPlayer height={630} src={src} poster={poster} /> ); };

组件内部渲染的是原生<video>元素(controls={false},由 Semi 自绘控件层),因此浏览器原生的视频格式支持范围(MP4/WebM 等)都直接适用。从实现看(packages/semi-ui/videoPlayer/index.tsx),它还额外挂载了一个<track kind="captions">用于字幕,与captionsSrc属性对应。

定制菜单栏控件:controlsList

播放器底部菜单栏默认展示全部控件,可通过controlsList按需裁剪展示项。它接受一个字符串数组,可选的控件标识与默认值如下:

控件标识含义
play播放 / 暂停
next重新播放(Restart 图标)
time当前时间 / 总时长
volume音量(Popover 悬浮滑杆)
playbackRate倍速选择
quality清晰度选择
route线路选择
mirror镜像翻转
fullscreen全屏
pictureInPicture画中画

默认值(完整数组):

['play', 'next', 'time', 'volume', 'playbackRate', 'quality', 'route', 'mirror', 'fullscreen', 'pictureInPicture']

例如只保留播放、时间、音量、倍速和全屏:

import React from 'react'; import { VideoPlayer } from '@douyinfe/semi-ui'; () => { const src = "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/vchart/landingPage/vchart-show-video.mp4"; const poster = "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/poster2.jpeg"; const controlsList = ['play', 'time', 'volume', 'playbackRate', 'fullscreen']; return ( <VideoPlayer height={630} src={src} poster={poster} controlsList={controlsList} /> ); };

从源码看,菜单栏渲染时的显隐判断由 Foundation 的shouldShowControlItem(name)完成(packages/semi-foundation/videoPlayer/foundation.ts),即判断该名称是否存在于controlsList中;对应的控件名称常量定义在 packages/semi-foundation/videoPlayer/constants.ts。因此传入任何不在上表内的字符串都不会渲染对应控件。

循环播放与快进快退

循环播放

通过loop开启循环播放(直接透传给原生videoloop属性):

import React from 'react'; import { VideoPlayer } from '@douyinfe/semi-ui'; () => { const src = "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/vchart/landingPage/vchart-show-video.mp4"; const poster = "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/poster2.jpeg"; return ( <VideoPlayer height={630} src={src} poster={poster} loop={true} /> ); };

快进快退

seekTime用于设定快进快退的时间跨度(单位:秒,默认 10 秒)。除了在进度条上点击/拖拽跳转外,还可以通过键盘左右方向键快进快退。下面的示例把seekTimeSelect联动,让用户自由选择 5s / 10s / 15s:

import React from 'react'; import { VideoPlayer, Select } from '@douyinfe/semi-ui'; () => { const [seekTime, setSeekTime] = useState(5); const src = "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/vchart/landingPage/vchart-show-video.mp4"; const poster = "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/poster2.jpeg"; return ( <div> <span style={{ marginBottom: 10 }}>Please select the fast forward and rewind time</span> <Select value={seekTime} style={{ width: 100, marginLeft: 10 }} onChange={(value) => setSeekTime(value)} optionList={[ { label: '5s', value: 5 }, { label: '10s', value: 10 }, { label: '15s', value: 15 }, ]} placeholder='Please select the fast forward and rewind time' /> <VideoPlayer height={630} style={{ marginTop: 10 }} src={src} poster={poster} seekTime={seekTime} /> </div> ); };

键盘交互的底层实现在 packages/semi-foundation/videoPlayer/foundation.ts:空格键触发播放/暂停,ArrowLeft/ArrowRight分别执行currentTime - seekTime/currentTime + seekTime。值得注意的细节是,只有当焦点位于播放器容器内部时键盘事件才会生效(通过videoWrapper.contains(document.activeElement)判断),避免与其他页面的可交互元素产生按键冲突。默认seekTime为 10(numbers.DEFAULT_SEEK_TIME,见 packages/semi-foundation/videoPlayer/constants.ts)。

播放速率:playbackRateList 与 defaultPlaybackRate

通过playbackRateList自定义倍速选择列表,每一项为{ label, value }结构:

import React from 'react'; import { VideoPlayer } from '@douyinfe/semi-ui'; () => { const playbackRateList = [ { label: '0.5x', value: 0.5 }, { label: '1.0x', value: 1 }, { label: '1.5x', value: 1.5 }, { label: '2.0x', value: 2 }, ]; const src = "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/vchart/landingPage/vchart-show-video.mp4"; const poster = "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/poster2.jpeg"; return ( <VideoPlayer height={630} src={src} poster={poster} playbackRateList={playbackRateList} /> ); };

文档 API 表说明:不传playbackRateList时,默认展示 6 种播放速率——0.5、0.75、1.0、1.25、1.5 和 2.0。需要补充的是,查看当前仓库的 packages/semi-foundation/videoPlayer/constants.ts,代码中实际声明的DEFAULT_PLAYBACK_RATE数组目前包含 5 项:2.0x、1.5x、1.25x、1.0x、0.75x(注意暂未包含 0.5x),如果你的业务对默认倍速列表有强约束,建议显式传入playbackRateList以保证行为可控。

切换倍速时组件会调用原生video.playbackRate,并通过onRateChange(rate: number)回调通知外部(packages/semi-foundation/videoPlayer/foundation.ts),同时右上角会弹出短暂的通知提示(如 "播放速度已切换为 2.0x")。

音量控制:volume 与 muted

volume用于设置初始音量,取值范围 0 - 100(默认 100);设置mutedtrue则静音播放。二者都支持受控/非受控使用:

import React from 'react'; import { VideoPlayer } from '@douyinfe/semi-ui'; () => { const src = "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/vchart/landingPage/vchart-show-video.mp4"; const poster = "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/poster2.jpeg"; return ( <VideoPlayer height={630} src={src} poster={poster} muted={true} /> ); };

音量调整的底层逻辑见 packages/semi-foundation/videoPlayer/foundation.ts:组件把 0-100 的整数音量换算为原生的 0-1 浮点值(video.volume = volume / 100),音量被调为 0 时自动将muted置为true;点击音量图标则执行静音/取消静音的切换,取消静音时会恢复静音前的音量值。音量菜单中的竖向滑杆复用自AudioSlider(audioPlayer 组件),悬停音量图标即可弹出(packages/semi-ui/videoPlayer/index.tsx)。

清晰度与线路切换:qualityList / routeList

播放器原生支持多清晰度与多线路切换,二者机制完全对称:

  • 清晰度:qualityList设置清晰度列表,defaultQuality设置初始清晰度,onQualityChange回调中自行更新src
  • 线路:routeList设置线路列表,defaultRoute设置初始线路,onRouteChange回调中自行更新src
import React from 'react'; import { VideoPlayer } from '@douyinfe/semi-ui'; () => { const [src, setSrc] = useState('https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/vchart/landingPage/vchart-show-video.mp4'); const playList = [ { src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/vchart/landingPage/vchart-show-video.mp4', quality: '1080p', }, { src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/video/vchart-show-video-480p.mp4', quality: '480p', }, ]; const poster = "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/poster2.jpeg"; const updateVideoSource = (quality) => { const source = playList.find((item) => item.quality === quality); setSrc(source.src); }; return ( <VideoPlayer height={630} src={src} poster={poster} defaultQuality={'1080p'} qualityList={[ { label: '1080p', value: '1080p' }, { label: '480p', value: '480p' }, ]} onQualityChange={(quality) => { console.log('quality change', quality); updateVideoSource(quality); }} /> ); };

这里有一个对用户体验很关键的实现细节:切换清晰度/线路后,视频地址src变化会导致原生视频重新加载。组件通过restorePlayPosition()(packages/semi-foundation/videoPlayer/foundation.ts)在loadeddata事件中自动恢复切换前的播放位置,并且如果切换前正处于播放状态,会继续保持播放——避免用户切清晰度后从头再看一遍。同时getDerivedStateFromProps会同步外部传入的src变化到内部 state(packages/semi-ui/videoPlayer/index.tsx)。

章节标记:markers

markers允许在进度条上划分多个章节区间,每项包含start(起始时间点,秒)和title(章节标题):

import React from 'react'; import { VideoPlayer } from '@douyinfe/semi-ui'; () => { const src = "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/vchart/landingPage/vchart-show-video.mp4"; const poster = "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/poster2.jpeg"; const markers = [ { start: 0, title: 'Start' }, { start: 4, title: 'Function Introduction' }, { start: 38, title: 'Figma Plugin' }, { start: 51, title: 'Ending' } ]; return ( <VideoPlayer height={630} src={src} poster={poster} markers={markers} /> ); };

markers的渲染逻辑位于进度条组件 packages/semi-ui/videoPlayer/videoProgress.tsx:组件会把相邻标记之间的区间切分成独立的分段(MarkerListItem),每个分段按start / max * 100%计算left、按(end - start) / max * 100%计算width铺满整条进度条。这样带来的能力是:

  • 已播放进度、缓冲进度按分段分别着色,视觉上呈"分段填充"效果(getPlayedWidth/getLoadedWidth,见 packages/semi-foundation/videoPlayer/progressFoundation.ts);
  • 悬停进度条时,Tooltip 会同时展示"当前所在章节标题 + 对应时间点"(packages/semi-ui/videoPlayer/videoProgress.tsx);
  • 点击/拖拽跳转时,进度条 handle 会自动归属到所在章节分段。

Marker数据结构定义在 packages/semi-foundation/videoPlayer/progressFoundation.ts,包含start: numbertitle: string两个字段。不传markers时,进度条退化为单分段的普通进度条。

主题:theme

theme用于切换播放器主题,可选'dark'(默认)与'light',注意它只影响播放器的背景色,不影响控件排布:

import React from 'react'; import { VideoPlayer } from '@douyinfe/semi-ui'; () => { const src = "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/vchart/landingPage/vchart-show-video.mp4"; const poster = "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/poster2.jpeg"; return ( <VideoPlayer height={630} src={src} poster={poster} theme={'light'} /> ); };

从源码看,theme被拼接为semi-videoPlayer-wrapper-dark/light这样的修饰类作用于视频外层容器(packages/semi-ui/videoPlayer/index.tsx),对应的 SCSS 变量定义在 packages/semi-foundation/videoPlayer/videoPlayer.scss 与 variables.scss 中,你可以像定制其他 Semi 组件一样通过 Design Token 调整相关背景色。

通过 ref 获取原生 video 元素

VideoPlayer支持ref/forwardRef,可以直接拿到原生<video>元素,实现比组件自带控件更灵活的控制,例如多个视频的同步播放/暂停:

import React, { useRef } from 'react'; import { VideoPlayer, Button } from '@douyinfe/semi-ui'; () => { const videoRef1 = useRef(); const videoRef2 = useRef(); const src = "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/vchart/landingPage/vchart-show-video.mp4"; const poster = "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/poster2.jpeg"; const handlePlayAll = () => { const v1 = videoRef1.current; const v2 = videoRef2.current; if (v1) v1.play(); if (v2) v2.play(); }; const handlePauseAll = () => { const v1 = videoRef1.current; const v2 = videoRef2.current; if (v1) v1.pause(); if (v2) v2.pause(); }; return ( <div> <div style={{ marginBottom: 12 }}> <Button onClick={handlePlayAll} style={{ marginRight: 8 }}>Play All</Button> <Button onClick={handlePauseAll}>Pause All</Button> </div> <div style={{ display: 'flex', gap: 12 }}> <VideoPlayer ref={videoRef1} src={src} poster={poster} height={315} width="50%" /> <VideoPlayer ref={videoRef2} src={src} poster={poster} height={315} width="50%" /> </div> </div> ); };

底层实现上,组件对外导出的是React.forwardRef包装(packages/semi-ui/videoPlayer/index.tsx),在getVideoRef()中兼容了函数式 ref 与对象式 ref 两种写法(packages/semi-ui/videoPlayer/index.tsx),拿到 ref 后即可调用play()pause()currentTimevolumerequestPictureInPicture()等任意原生HTMLVideoElementAPI。

API 一览

VideoPlayer 属性

PropertiesDescriptionTypeDefault Value
autoPlayWhether to play automaticallybooleanfalse
captionsSrcCaptions sourcestring-
classNameClass namestring-
clickToPlayWhether to enable click to playbooleantrue
controlsListSet the menu bar to display controls. All controls are displayed by default.string[]['play', 'next', 'time', 'volume', 'playbackRate', 'quality', 'route', 'mirror', 'fullscreen', 'pictureInPicture']
crossOriginThis enum attribute indicates whether CORS is used to fetch the video. CORS-enabled resources can be reused in 'canvas' elements without being polluted. Allowed values are 'anonymous' and 'use-credentials''anonymous' | 'use-credentials'-
defaultPlaybackRateDefault playback ratenumber1
defaultQualityDefault video resolutionstring-
defaultRouteDefault line (route)string-
forwardRefPass the ref of the native video element for more flexible controlReact.Ref<HTMLVideoElement>-
heightHeightstring | number-
loopWhether to enable loop playbackbooleanfalse
markersChapter markersMarker[]-
mutedWhether to play silentlybooleanfalse
onPausePause callback() => void-
onPlayPlay callback() => void-
onQualityChangeSwitch quality callback(quality: string) => void-
onRateChangeSwitch rate callback(rate: number) => void-
onRouteChangeSwitch route callback(route: string) => void-
onVolumeChangeAdjust volume callback(volume: number) => void-
playbackRateListRate list. By default 6 playback rates are displayed: 0.5, 0.75, 1.0, 1.25, 1.5 and 2.0Array<{ label: string; value: number }>-
posterPosterstring-
qualityListQuality listArray<{ label: string; value: string }>-
routeListRoute listArray<{ label: string; value: string }>-
seekTimeFast forward and rewind time (seconds)number10
srcVideo playback addressstring-
styleStyleCSSProperties-
themeTheme setting, different themes give different background colors'dark' | 'light''dark'
volumeDefault volume (0 - 100)number100
widthWidthstring | number-

完整 TypeScript 接口定义见 packages/semi-ui/videoPlayer/index.tsx,默认值集中在static defaultProps(packages/semi-ui/videoPlayer/index.tsx),其中volumeseekTimedefaultPlaybackRate的默认值 100 / 10 / 1 来自 packages/semi-foundation/videoPlayer/constants.ts。

Marker

PropertiesDescriptionType
startStart time point (seconds)number
titleTitlestring

源码级原理补充

架构:Component + Foundation 双层结构

与其他 Semi Design 组件一致,VideoPlayer采用"UI 组件 + Foundation 逻辑层"的双层架构:UI 层 packages/semi-ui/videoPlayer/index.tsx 负责 JSX 渲染与 DOM 事件绑定,通过adapter对象把状态更新与回调注入逻辑层;逻辑层 packages/semi-foundation/videoPlayer/foundation.ts 承载全部播放控制、事件处理与状态同步。这种解耦让逻辑层可以脱离 React 独立测试与复用。

进度条:分段着色 + Tooltip 章节预览

进度条子组件 packages/semi-ui/videoPlayer/videoProgress.tsx 内部同样维护自己的 Foundation(packages/semi-foundation/videoPlayer/progressFoundation.ts),负责鼠标拖拽、进度百分比计算(限制在 0-1 之间)、handle 显隐(悬停或拖拽时显示)以及文档级mousemove/mouseup监听以实现拖出进度条后仍可继续拖动。已播放与缓冲宽度分别来自currentTimebufferedValue(原生progress事件中取video.buffered.end(...),见 packages/semi-foundation/videoPlayer/foundation.ts)。

事件与体验细节

  • 控制栏自动隐藏:鼠标在播放器内移动时显示控制栏,静止 3 秒后自动隐藏(节流 200ms 处理,packages/semi-foundation/videoPlayer/foundation.ts);播放中鼠标移出播放器也会隐藏控制栏。
  • 全屏滚动位置恢复:进入全屏前记录window.scrollX/scrollY,退出全屏后恢复,兼容 WebKit/Moz/MS 及 iOS Safari(webkitDisplayingFullscreen)的 fullscreen API(packages/semi-foundation/videoPlayer/foundation.ts)。
  • 画中画:调用原生requestPictureInPicture(),并通过leavepictureinpicture事件同步播放状态(packages/semi-foundation/videoPlayer/foundation.ts)。
  • 加载/卡顿提示与错误态:原生waitingstalled事件触发加载中/卡顿通知,error事件渲染带 ErrorSvg 的错误占位(本地化文案来自 packages/semi-ui/locale/source 各语言包的VideoPlayer字段,如loadingstallnoResourcevideoError等)。
  • 时间格式化formatTime工具(packages/semi-ui/videoPlayer/utils.ts)将秒数格式化为mm:ss,超过 1 小时自动升级为h:mm:ss

测试与示例

仓库中已有针对该组件的端到端测试 cypress/e2e/videoPlayer.spec.js,以及 Storybook 演示 packages/semi-ui/videoPlayer/_story/videoPlayer.stories.jsx,可作为了解组件交互行为的补充参考。

设计变量

VideoPlayer的视觉样式由 Design Token 驱动(详见文档末尾的<DesignToken/>区块),背景色、进度条、控件配色等均可在 packages/semi-foundation/videoPlayer/variables.scss 中查看默认取值,并通过 Semi Design 的主题定制机制覆盖,实现与业务品牌一致的外观。

小结

VideoPlayer是一个"零配置可用、按需深度定制"的视频播放器组件:基础场景只需src + poster;进阶场景通过controlsList裁剪控件、qualityList/routeList实现多清晰度多线路、markers实现章节导航、ref获取原生元素实现精细操控。结合本文对 foundation.ts、constants.ts、videoProgress.tsx 等源码的剖析,你既可以在业务中快速接入,也能在遇到复杂定制需求(如切换清晰度保持播放位置、全屏状态管理)时知其所以然。

【免费下载链接】semi-design🚀A modern, comprehensive, flexible design system and React UI library, AI-friendly built-in.🎨Provide 3000+ Design Tokens, easy to build your design system. Make Semi Design to Any Design.🧑🏻‍💻 Design to Code in one click项目地址: https://gitcode.com/gh_mirrors/se/semi-design

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询