使用 video.js 这个依赖完成浏览器播放视频资源。尝试了一下,常见资源中,除了 AVI 无法正常播放外,mp4、mkv 都能正常播放与快进。
React 稍微简单配置下即可完成视频播
开发
安装依赖
css
pnpm i video.js
文件树
css
src/components/video-modal/modal.tsx
src/components/video-modal/style.css
src/components/video-modal/video-js.tsx
src/components/video-modal/video-path-context.tsx
文件路径:src/components/video-modal/video-path-context.tsx
创建一个视频播放上下文文件。用于控制需要播放视频的地址。
并插入视频播放的 modal 组件
typescript
'use client'
import createCtx from '@/lib/create-ctx'
import React from 'react'
import VideoModal from '@/components/video-modal/modal'
export const VideoPathContext = createCtx<string>()
export const useVideoPathStore = VideoPathContext.useStore
export const useVideoPathDispatch = VideoPathContext.useDispatch
export const VideoPathProvider: React.FC<React.PropsWithChildren> = ({ children }) => {
return (
<VideoPathContext.ContextProvider value="">
{children}
<VideoModal />
</VideoPathContext.ContextProvider>
)
}
文件路径:src/components/video-modal/modal.tsx
视频播放弹窗,显示条件为是否设置了播放视频地址。
弹窗设置了 destroyOnClose 属性,当弹窗关闭后,销毁弹窗内容。
javascript
'use client'
import React from 'react'
import { Modal } from 'antd'
import { isEmpty } from 'lodash'
import VideoJS from '@/components/video-modal/video-js'
import Player from 'video.js/dist/types/player'
import { useVideoPathDispatch, useVideoPathStore } from '@/components/video-modal/video-path-context'
const VideoModal: React.FC = () => {
const video_path = useVideoPathStore()
const changeVideoPath = useVideoPathDispatch()
const filename = decodeURIComponent(video_path).split('/').pop()
const playerRef = React.useRef<Player | null>(null)
const videoJsOptions = {
autoplay: true,
controls: true,
responsive: true,
fluid: true,
sources: [
{
src: video_path,
type: 'video/mp4',
},
],
}
return (
<Modal
title={filename}
open={!isEmpty(video_path)}
width="75%"
onCancel={() => changeVideoPath('')}
footer={false}
destroyOnClose={true}
>
<VideoJS
options={videoJsOptions}
onReady={(player) => {
playerRef.current = player
player.on('waiting', () => {
console.log('player is waiting')
})
player.on('dispose', () => {
console.log('player will dispose')
})
}}
/>
</Modal>
)
}
export default VideoModal
文件路径:src/components/video-modal/style.css
访 YouTube video.js 样式文件
文件路径:src/components/video-modal/video-js.tsx
Video.js 组件封装
- playbackRates 设置了 [0.5, 1, 1.5, 2] 倍数播放
- 快进 5 秒、快退 5 秒按钮
- 按下键盘的左右键实现快进一帧、后退一帧。(这里是默认每秒 25 帧计算)
- 当组件被销毁时,执行 player.dispose() 方法一同销毁播放器
typescript
import React, { KeyboardEvent } from 'react'
import videoJs from 'video.js'
import 'video.js/dist/video-js.css'
import './style.css'
import Player from 'video.js/dist/types/player'
import styled from 'styled-components'
const VideoMain = styled.div`
.video-js {
padding-top: 70% !important;
}
`
export const VideoJS: React.FC<{ options: any; onReady: (player: Player) => void }> = (props) => {
const video_ref = React.useRef<HTMLDivElement>(null)
const player_ref = React.useRef<Player | null>(null)
const { options, onReady } = props
React.useEffect(() => {
if (!player_ref.current) {
const videoElement = document.createElement('video-js')
videoElement.classList.add('vjs-big-play-centered')
video_ref.current?.appendChild(videoElement)
const player = (player_ref.current = videoJs(
videoElement,
{
playbackRates: [0.5, 1, 1.5, 2],
controlBar: {
skipButtons: {
forward: 5,
backward: 5,
},
},
userActions: {
hotkeys: (event: KeyboardEvent) => {
if (event.key === 'ArrowLeft') {
player_ref.current?.pause()
player_ref.current?.currentTime((player_ref.current?.currentTime() || 0) - 1 / 25)
}
if (event.key === 'ArrowRight') {
player_ref.current?.pause()
player_ref.current?.currentTime((player_ref.current?.currentTime() || 0) + 1 / 25)
}
if (event.key === 'ArrowUp') {
}
if (event.key === 'ArrowDown') {
}
if (event.key === ' ') {
if (player_ref.current?.paused()) {
player_ref.current?.play()?.then()
} else {
player_ref.current?.pause()
}
}
},
},
...options,
},
() => {
videoJs.log('player is ready')
onReady && onReady(player)
},
))
} else {
const player = player_ref.current
player.autoplay(options.autoplay)
player.src(options.sources)
}
}, [onReady, options, video_ref])
React.useEffect(() => {
const player = player_ref.current
return () => {
if (player && !player.isDisposed()) {
player.dispose()
player_ref.current = null
}
}
}, [player_ref])
return (
<div data-vjs-player={true}>
<VideoMain ref={video_ref} />
</div>
)
}
export default VideoJS
文件路径:src/app/path/context.tsx
最后将 VideoPathProvider 组件插入 src/app/path/context.tsx 内
javascript
...
import { VideoPathProvider } from '@/components/video-modal/video-path-context'
export const PathContextProvider: React.FC<React.ProviderProps<ReaddirListType>> = ({ value, children }) => {
return (
<>
...
<VideoPathProvider>{children}</VideoPathProvider>
...
</>
)
}
文件路径:src/components/preview/index.tsx
预览文件,对视频的 icon 点击时设置视频播放地址。
javascript
...
import { useVideoPathDispatch } from '@/components/video-modal/video-path-context'
const Preview: React.FC<{ item: ReaddirItemType }> = ({ item }) => {
...
const videoPathDispatch = useVideoPathDispatch()
if (isVideo(name)) {
return <VideoCameraOutlined onClick={() => videoPathDispatch(staticPath(name))} />
}
...
}
export default Preview
到此完成了视频弹窗播放。