记录学习过程,如有错误欢迎指出
输出一下我的观点
video标签元素样式都挺丑的,我相信大家不会反对吧.
那就让我们一起美化它!
分析结构
- 视频本身
- 控制台
- 播放暂停按钮
- 时间戳
- 音量
- 全屏
- 倍数
- 进度条
目录结构
我比较小喜欢styled-components
npm install styled-components
src/
App.tsx
styled.ts
utils/
components/
开搞
import { Wrap, Video } from './style';
export const App(){
//后续必然会用到ref
const wrapRef = useRef(null) as React.RefObject<HTMLDivElement> | null;
const videoRef = useRef(null) as React.RefObject<HTMLVideoElement> | null;
return
(
<Wrap ref={wrapRef}>
<Video
ref={videoRef}
preload="metadata"
>
<source src="videoUrl" />
</Video>
</Wrap>
)
}
实现点击视频播放/暂停
...
const [isPlay, setIsPlay] = useState(false);
// 播放暂停
const playOrPause = () => {
if (videoRef !== null && videoRef.current) {
// 暂停
let flagCopy = { ...flag };
if (isPlay) {
videoRef.current.pause();
setIsPlay(false);
} else {
// 播放
videoRef.current.play();
setIsPlay(true);
}
setFlag(flagCopy);
}
};
return
(
<Wrap ref={wrapRef}>
<Video
...
onClick={playOrPause}
>
<source src="videoUrl" />
</Video>
</Wrap>
)
}
播放暂停按钮
Pause和Play无任何逻辑,仅是导出的播放和暂停按钮的svg
...
{/* 播放 暂停 */}
<div onClick={playOrPause}>
{isPlay ? <Pause></Pause> : <Play></Play>}
</div>
...
时间戳
梳理一下:时间戳 = 当前视频播放数据 + / + 视频总时间
想要获取到视频的时间肯定是要等视频第一帧加载完毕的时候.
import {...,StampTime} from 'style.ts'
...
const [sharedState, setSharedState] = useState({
height: props.high, //传入的视频高度
width: props.long, //传入的视频宽度
offsetLeft: 0, //wrap的左偏移量
offsetWidth: 0, //wrap的宽度 === 传入的视频高度
duration: 0, //视频总时间 单位为秒
formatDuration: '00:00:00', //格式化后的总时间
});
// 视频加载第一帧后获取各种信息
const getInfo = async () => {
if (
wrapRef !== null &&
wrapRef.current &&
videoRef !== null &&
videoRef.current
) {
let shared = { ...sharedState };
shared.offsetLeft = wrapRef.current.offsetLeft;
shared.offsetWidth = wrapRef.current.offsetWidth;
shared.duration = videoRef.current.duration;
shared.formatDuration = await formatTime(videoRef.current.duration);
setSharedState(shared);
let flagCopy = { ...flag };
flagCopy.volume = videoRef.current.volume;
videoRef.current.pause();
setIsPlay(false);
setFlag(flagCopy);
}
};
...
<Video
...
onClick={playOrPause}
onLoadedData={getInfo}
>
<source src="videoUrl" />
</Video>
{/* 时间戳 */}
<StampTime>
{flag.formatCurrentTime} / {sharedState.formatDuration}
</StampTime>
...
进度条
- 进度条:
- 背景进度条 (为缓冲到的那根)
- 缓存进度条 (在我们看视频时,进度条上一般都有一根灰色的进度条在前进)
- 进度条 (当前时间映射到进度条上的比例)
import {...,Bar,BackBar,ProgressBar,BufferBar} from 'style.ts'
...
const [barState, setBarState] = useState({
height: '2px',
bufferWidth: '0px',
progressWidth: '0px',
});
/**
* bar相关
*/
const mouseEnterBar = () => {
let flagCopy = { ...flag };
flagCopy.isEnterBar = true;
let barStateCopy = { ...barState };
barStateCopy.height = '4px';
setBarState(barStateCopy);
setFlag(flagCopy);
};
const mouseLeaveBar = () => {
if (!flag.isEnterBar) return;
let flagCopy = { ...flag };
flagCopy.isEnterBar = false;
let barStateCopy = { ...barState };
barStateCopy.height = '2px';
let DetailVideoStateCopy = { ...DetailVideoState };
DetailVideoStateCopy.display = 'none';
setDetailVideoState(DetailVideoStateCopy);
setBarState(barStateCopy);
setFlag(flagCopy);
};
const mouseInBarMove = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
if (!flag.isEnterBar) return false;
if (DetailVideoRef !== null && DetailVideoRef.current) {
let res = getDetailLeft(sharedState.offsetLeft + 20, e.pageX);
let px = getForwordPx(sharedState.offsetLeft + 20, e.pageX);
let time = forwordPxToVideoTime(
px,
sharedState.duration,
sharedState.offsetWidth - 40
);
let DetailVideoStateCopy = { ...DetailVideoState };
DetailVideoRef.current.currentTime = time;
DetailVideoStateCopy.left = res + 'px';
DetailVideoStateCopy.display = 'block';
setDetailVideoState(DetailVideoStateCopy);
}
return false;
};
const mouseDownBar = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
let flagCopy = { ...flag };
flagCopy.isCurrentClickBar = true;
setFlag(flagCopy);
let BarStateCopy = { ...barState };
let px = getForwordPx(sharedState.offsetLeft + 20, e.pageX);
BarStateCopy.progressWidth = px + 'px';
setBarState(BarStateCopy);
let time = forwordPxToVideoTime(
px,
sharedState.duration,
sharedState.offsetWidth - sharedState.offsetLeft - 40
);
if (videoRef !== null && videoRef.current) {
videoRef.current.currentTime = time;
}
};
const mouseUpBar = () => {
let flagCopy = { ...flag };
flagCopy.isCurrentClickBar = false;
if (isPlay === false) {
setIsPlay(true);
if (videoRef !== null && videoRef.current) {
videoRef.current.play();
}
}
setFlag(flagCopy);
};
...
{/* 进度条 */}
<Bar
onMouseEnter={mouseEnterBar}
onMouseLeave={mouseLeaveBar}
onMouseMove={e => {
mouseInBarMove(e);
}}
onMouseDown={e => {
mouseDownBar(e);
}}
onMouseUp={mouseUpBar}
>
{/* 进度条 */}
<ProgressBar
style={{
width: barState.progressWidth,
height: barState.height,
backgroundColor: sharedState.progressColor,
}}
></ProgressBar>
{/* 缓冲条 */}
<BufferBar
style={{
width: barState.bufferWidth,
height: barState.height,
backgroundColor: sharedState.bufferColor,
}}
></BufferBar>
{/* 背景条 */}
<BackBar style={{ height: barState.height }}></BackBar>
</Bar>
...
音量
/**
* 音量控制
*/
const increaseVolume = () => {
if (videoRef !== null && videoRef.current) {
videoRef.current.muted = false;
if (videoRef.current.volume === 1) {
} else {
videoRef.current.volume = (videoRef.current.volume * 10 + 1) / 10;
}
let flagCopy = { ...flag };
flagCopy.volume = videoRef.current.volume;
setFlag(flagCopy);
}
};
const decreaseVolume = () => {
if (videoRef !== null && videoRef.current) {
videoRef.current.muted = false;
if (videoRef.current.volume === 0) {
if (!videoRef.current.muted) {
videoRef.current.muted = true;
}
} else {
videoRef.current.volume = (videoRef.current.volume * 10 - 1) / 10;
}
let flagCopy = { ...flag };
flagCopy.volume = videoRef.current.volume;
setFlag(flagCopy);
}
};
const volumeChange = () => {
if (!nonStateDep.isShowVolumeBack) {
let nonStateDepCopy = { ...nonStateDep };
nonStateDepCopy.isShowVolumeBack = true;
setNonStateDep(nonStateDepCopy);
}
if (timer.current.volumeTimer) {
clearTimeout(timer.current.volumeTimer);
}
timer.current.volumeTimer = setTimeout(() => {
let nonStateDepCopy = { ...nonStateDep };
nonStateDepCopy.isShowVolumeBack = false;
setNonStateDep(nonStateDepCopy);
}, 2000);
};
useEffect(() => {
//监听上下键
window.onkeydown = (e: KeyboardEvent) => {
switch (e.code) {
case 'Space':
break;
case 'ArrowUp':
increaseVolume();
break;
case 'ArrowDown':
decreaseVolume();
break;
}
}
// clear cyle
return () => {
window.onkeydown = null;
};
}, []);
...
{/* 音量 */}
{nonStateDep.isShowVolumeBack ? (
<VolumeBack>
<Volume>
{flag.volume === 0 ? (
<svg
className="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="1770"
width="30"
height="30"
>
<path ...
fill="#000000"
p-id="1771"
></path>
</svg>
) : (
<svg
className="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="1403"
width="30"
height="30"
>
<path ...
p-id="1404"
fill="#000000"
></path>
</svg>
)}
{flag.volume === 0 ? '静音' : flag.volume * 100 + '%'}
</Volume>
</VolumeBack>
) : null}
...
视频预览
描述:就是鼠标移到进度条时,可以显示出鼠标指向的视频时间的画面
实现方法:
- 方法1:canvas截图
- 方法2:隐藏一个video标签,移入进度条的时候显示,移除隐藏(这种方法有个弊端,就是会重复请求视频,相当于用户看一个视频的时候,实际开了两个视频.所以这个隐藏video应该为其src填入一个压缩后的视频链接)
推荐一个视频压缩网站ocompress.com/cn/video
我这里实现的方法2.
鼠标移入进度条,获取当前鼠标位置对应的视频时间,将这个视频时间赋给预览图video的currentTime
import {...,DetailVideo} from 'style.ts'
...
const DetailVideoRef = useRef(null) as React.RefObject<
HTMLVideoElement
> | null;
...
{/* 预览图 */}
<DetailVideo
ref={DetailVideoRef}
muted
style={{
left: DetailVideoState.left,
display: DetailVideoState.display,
}}
>
<source src={sharedState.minVideoUrl} />
</DetailVideo>
loading
主要事件onWaiting,onPlaying
// 等待缓冲
const waitLoadVideo = () => {
if (videoRef !== null && videoRef.current) {
// 暂停
let flagCopy = { ...flag };
setIsPlay(false);
flagCopy.isWaitBuffer = true;
setFlag(flagCopy);
let nonStateDepCopy = { ...nonStateDep };
nonStateDepCopy.isShowLoadingBack = true;
setNonStateDep(nonStateDepCopy);
}
};
// 等待缓存结束
const waitEnd = () => {
if (videoRef !== null && videoRef.current) {
// 暂停
let flagCopy = { ...flag };
setIsPlay(true);
flagCopy.isWaitBuffer = false;
setFlag(flagCopy);
let nonStateDepCopy = { ...nonStateDep };
nonStateDepCopy.isShowLoadingBack = false;
setNonStateDep(nonStateDepCopy);
}
};
...
<Video
...
onWaiting={waitLoadVideo}
onPlaying={waitEnd}
>
</Viode>
{/* loading */}
{nonStateDep.isShowLoadingBack ? (
<Load>{flag.isWaitBuffer ? <Loading></Loading> : null}</Load>
) : null}
...
全屏 & 倍数
这个就留给大家自己思考吧
ps:不要使用video自带的全屏事件,会覆盖我们自己的样式.
倍数借助属性:playbackRate.
源码
//尚未完工,切换用于工作项目
npm i react-customize-player
github:github.com/caro1xxx/re…
记录学习过程,如有错误欢迎指出