「这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战」
HTML <video> 元素用于在HTML或者XHTML文档中嵌入媒体播放器,用于支持文档内的视频播放。
-
currentTime:读取CurentTime返回一个双精度浮点值,指示以秒为单位的媒体的当前播放位置。如果video尚未开始播放,则会在开始播放后返回偏移量。通过CurentTime将当前播放位置设置为给定时间,会在加载媒体时将媒体查找到该位置(从指定的位置开始播放)。 -
duration(只读):一个双精度浮点值,它指示媒体的持续时间(总长度),以秒为单位,在媒体的时间线上。 -
volume:音量 -
playbackrate:播放速度 -
play():播放视频 -
pause():暂停视频
更多属性:developer.mozilla.org/zh-CN/docs/…
实现思路:
- 播放 / 暂停
// 获取 dom 节点
const player = document.querySelector('.player')
const video = player.querySelector('.viewer')
const toggle = player.querySelector('.toggle')
当 video 标签和 控制按钮被点击时,切换播放状态
video.addEventListener('click', togglePlay)
toggle.addEventListener('click', togglePlay)
判断 video 的状态,如果当前是暂停,则调用 video 的 play。
// 切换播放状态
function togglePlay() {
const method = video.paused ? 'play' : 'pause'
video[method]()
}
- 切换播放状态图标,对 video 进行播放状态的监听
video.addEventListener('play', updatedToggle)
video.addEventListener('pause', updatedToggle)
// 切换播放状态图标
function updatedToggle() {
const icon = video.paused ? '►' : '❚ ❚'
toggle.textContent = icon
}
- 实现前进后退功能
获取页面上的两个前进后退按钮,为其添加 click 事件。
const skipButtons = player.querySelectorAll('[data-skip]')
skipButtons.forEach(skipButton => skipButton.addEventListener('click', skip))
获取 skip 按钮上的自定义属性,改变 video 的 currentTime
function skip() {
video.currentTime += parseFloat(this.dataset.skip)
}
- 改变播放速度和音量
获取页面上控制音量和播放速度的两个按钮,为其添加 change 事件和 mousemove 事件,实现点击或拖动改变的效果。
const ranges = player.querySelectorAll('.player__slider')
ranges.forEach(range => range.addEventListener('change', handleRangeUpdate))
ranges.forEach(range => range.addEventListener('mousemove', handleRangeUpdate))
// 改变播放速度和音量
function handleRangeUpdate() {
video[this.name] = this.value
}
- 实现播放进度条的颜色移动
const progressBar = player.querySelector('.progress__filled')
监听 video 标签的 timeupdate
video.addEventListener('timeupdate', handleProgress)
用当前播放的时间 video.currentTime 除以视频的总时长 video.duration
// 改变进度条
function handleProgress() {
const percent = (video.currentTime / video.duration) * 100;
progressBar.style.flexBasis = `${percent}%`;
}
- 拖动进度条,视频切换到对应的播放时间
const progress = player.querySelector('.progress')
需要一个变量,控制切换是否生效
let mousedown = false
监听 progress 的 click,mousemove,mousedown,mouseup 四个事件
let mousedown = false
progress.addEventListener('click', scrub)
progress.addEventListener('mousemove', (e) => mousedown && scrub(e))
progress.addEventListener('mousedown', () => mousedown = true)
progress.addEventListener('mouseup', () => mousedown = false)
// 点击切换播放进度
function scrub(e) {
const scrubTime = (e.offsetX / progress.offsetWidth) * video.duration
video.currentTime = scrubTime
}
基本思路:
隐藏原生 video 控件,实现原生控件的功能。
<Controls
progressEl={progressEl}
volumeEl={volumeEl}
controls={controls}
isPlaying={isPlaying}
volume={volume}
currentTime={currentVideoTime}
duration={videoDuration}
muted={muted}
markers={markers}
onPlayClick={play}
onPauseClick={pause}
onProgressClick={handleProgressClick}
onVolumeClick={handleVolumeClick}
onMuteClick={handleMuteClick}
onFullScreenClick={handleFullScreenClick}
onMarkerClick={handleMarkerClick}
/>
传入的 controls 的是控件的数量,通过该数组来控制显示。
播放 / 暂停
{controls.includes('play') ? (
<button
className={isPlaying ? 'pause' : 'play'}
onClick={isPlaying ? onPauseClick : onPlayClick}
>
{isPlaying ? 'Pause' : 'Play'}
</button>
) : null}
显示播放时间
对传入的时间做一些格式化操作
const getTimeCode = (secs: number): string => {
let secondsNumber = secs ? parseInt(String(secs), 10) : 0;
let hours = Math.floor(secondsNumber / 3600);
let minutes = Math.floor((secondsNumber - hours * 3600) / 60);
let seconds = secondsNumber - hours * 3600 - minutes * 60;
let hoursStr: string = String(hours);
let minutesStr: string = String(minutes);
let secondsStr: string = String(seconds);
if (hours < 10) {
hoursStr = '0' + hours;
}
if (minutes < 10) {
minutesStr = '0' + minutes;
}
if (seconds < 10) {
secondsStr = '0' + seconds;
}
return `${
hoursStr !== '00' ? hoursStr + ':' : ''
}${minutesStr}:${secondsStr}`;
};
const durationTimeCode = getTimeCode(Math.ceil(duration));
const currentTimeCode =
currentTime !== duration ? getTimeCode(currentTime) : durationTimeCode;
{controls.includes('time') ? (
<div className="time">
{currentTimeCode}/{durationTimeCode}
</div>
) : null}
进度条组件,通过 ref 向父组件暴露子组件的引用。Marker 是标记点组件,接收传入的数组,向外暴露 onMarkerClick 事件。
{controls.includes('progress') ? (
<div className="progress-wrap">
<progress ref={progressEl} max="100" onClick={onProgressClick}>
0% played
</progress>
{markers &&
markers.map((marker, index) => {
return (
<Marker
key={index}
marker={marker}
duration={duration}
onMarkerClick={onMarkerClick}
/>
);
})}
</div>
) : null}
Marker 组件本质上是有 i 标签组成的一个个小点,通过传入的 duration 和 time 计算出 position 的值。
if (duration) {
const percent = time <= duration ? time / duration : 1;
return `calc(${percent * 100}% - 2px)`;
}
<i
id={id}
className="react-video-marker"
title={title}
style={{
background: color,
left: getPosition()
}}
onClick={() => {
onMarkerClick(marker);
}}
/>
控制音量组件
{controls.includes('volume') ? (
<div className="volume-wrap">
<progress
ref={volumeEl}
max="100"
value={volume * 100}
onClick={onVolumeClick}
>
{volume * 100}% volume
</progress>
<button
className={muted ? 'no-volume' : 'volume'}
onClick={onMuteClick}
>
Volume
</button>
</div>
) : null}
全屏组件
{controls.includes('full-screen') ? (
<button className="full-screen" onClick={onFullScreenClick}>
FullScreen
</button>
) : null}
进度条事件如下:
- play(点击播放)
- pause(点击暂停)
- handleProgressClick(进度条点击事件)
- handleVolumeClick(改变音量事件)
- handleMuteClick(点击静音事件)
- handleFullScreenClick(点击全屏事件)
- handleMarkerClick(标记点事件)
Controls 组件通过 isPlaying 来判断当前的播放状态。当触发 play 和 pause 事件时,应当改变 isPlaying 的值。
play 和 pause
// play
const play = () => {
if (!videoRef.current) return;
const playPromise = videoRef.current.play();
playPromise &&
playPromise
.then(() => { })
.catch(e => {
console.log('Operation is too fast, audio play fails');
});
setIsPlaying(true);
};
// pause
const pause = () => {
if (!videoRef.current) return;
videoRef.current.pause();
setIsPlaying(false);
};
handleProgressClick
点击进度条,期望就是 video 的时间可以到达点击的位置。
- e.clientX:鼠标相对于浏览器窗口可视区域的X,Y坐标,可视区域不包括工具栏和滚动条。
- element.getBoundingClientRect():返回的结果是包含完整元素的最小矩形,并且拥有
left,top,right,bottom,x,y,width, 和height这几个以像素为单位的只读属性用于描述整个边框。除了width和height以外的属性是相对于视图窗口的左上角来计算的。
- 获取当前页面滚动条横坐标的位置:document.body.scrollLeft
const handleProgressClick = (e: Event) => {
const x =
e['clientX'] -
progressEl.current!.getBoundingClientRect().left +
document.body.scrollLeft;
const percentage =
(x * progressEl.current!.max) / progressEl.current!.offsetWidth;
videoRef.current!.currentTime =
(percentage / 100) * videoRef.current!.duration;
}; (percentage / 100) * videoRef.current!.duration; };
handleVolumeClick
音量的改变与进度条同理。当改变的音量的时候,静音状态应该设置为 false。
const handleVolumeClick = e => {
const y =
volumeEl.current!.offsetWidth -
(e.clientY -
volumeEl.current!.getBoundingClientRect().top +
document.body.scrollTop);
const percentage =
(y * volumeEl.current!.max) / volumeEl.current!.offsetWidth;
videoRef.current!.muted = false;
const volume = videoRef.current.volume;
onVolumechange?.(volume);
setVolume(percentage / 100);
};
handleMuteClick,设置 muted 的状态
const handleMuteClick = () => {
if (muted) {
videoRef.current!.muted = false;
setVideoMuted(false);
} else {
videoRef.current!.muted = true;
setVolume(0);
setVideoMuted(true);
}
};
handleFullScreenClick
通过 isFullScreen 来控制是否全屏。点击全屏的时候,调用当前元素上的 requestFullscreen 方法,退出全屏时,调用 document 的 exitFullscreen 方法。
const handleFullScreenClick = () => {
const videoWrap = document.getElementsByClassName('react-video-wrap')[0];
if (isFullScreen) {
document.body.classList.remove('react-video-full-screen');
if (document['exitFullscreen']) {
document['exitFullscreen']();
} else if (document['mozCancelFullScreen']) {
document['mozCancelFullScreen']();
} else if (document['webkitExitFullscreen']) {
document['webkitExitFullscreen']();
} else if (document['msExitFullscreen']) {
document['msExitFullscreen']();
}
} else {
document.body.classList.add('react-video-full-screen');
if (videoWrap['requestFullscreen']) {
videoWrap['requestFullscreen']();
} else if (videoWrap['mozRequestFullScreen']) {
videoWrap['mozRequestFullScreen']();
} else if (videoWrap['webkitRequestFullscreen']) {
videoWrap['webkitRequestFullscreen']();
} else if (videoWrap['msRequestFullscreen']) {
videoWrap['msRequestFullscreen']();
}
}
setIsFullScreen(!isFullScreen);
};
handleMarkerClick
点击标记点,进度条到达标记点的时间
const handleMarkerClick = (marker: object) => {
videoRef.current!.currentTime = marker['time'];
onMarkerClick(marker);
};