1. HTML5 동영상 재생
일반적으로 페이지에서 재생하는 비디오는 video
태그를 통해 src 속성에 표시해야 하는 비디오 주소를 추가하고 브라우저는 너비, 높이, 자동 재생 여부, 루프 및 다른 속성, 브라우저의 기본 비디오 컨트롤을 통해 비디오를 재생합니다.
<video controls width="250">
<source src="/media/cc0-videos/flower.webm" type="video/webm">
<source src="/media/cc0-videos/flower.mp4" type="video/mp4">
Sorry, your browser doesn't support embedded videos.
</video>
复制代码
2. 공통 이벤트 및 속성
일반적인 구현:
속성 | 설명하다 |
---|---|
자동 재생 | 이 속성을 지정하면 데이터가 로드될 때까지 기다리지 않고 직접 재생됩니다. |
버퍼링된 | 미디어가 캐시된 시간 범위를 읽을 수 있습니다. |
통제 수단 | 사용자가 볼륨, 일시 중지, 다시 시작 등과 같은 비디오 재생을 제어할 수 있습니다. |
제어 목록 | 컨트롤이 지정되면 controlslist는 브라우저가 미디어 요소의 디스플레이 컨트롤을 선택하는 데 도움이 될 수 있습니다(수신 매개변수: nodownload, nofullscreen ....). |
현재 시간 | 값을 설정한 후 비디오는 현재 재생의 시작 시간으로 설정합니다. |
용량 | 오디오 볼륨의 상대 값을 0.0에서 1.0 사이로 설정하거나 현재 볼륨의 상대 값을 쿼리합니다. |
음소거 | 파일 음소거, 음소거 또는 음소거 해제 여부 |
시작 시간 | 일반적으로 0, 스트리밍 미디어나 0부터 시작하지 않는 리소스라면 0이 아니다. |
지속 | 읽기 전용 미디어 파일의 전체 길이입니다. 일부 미디어(예: 알 수 없는 라이브 스트림, 웹캐스트, WebRTC의 미디어)에는 이 값이 없고 NAN을 반환합니다. |
일시 중지 | 읽기 전용이 일시 중지되었는지 여부 |
끝났다 | 오디오/비디오 재생이 종료되었는지 여부만 읽기 |
높이 너비 | 비디오의 높이/너비(css 픽셀) |
고리 | 지정하면 비디오가 끝까지 도달하면 비디오가 시작된 위치로 자동으로 돌아갑니다. |
포스터 | 비디오 커버, 재생 중 이미지가 표시되지 않음 |
예압 | 없음: 비디오를 미리 캐시하지 않음, 메타데이터: 합리적으로 소스 데이터를 가져옵니다. 자동: 이 비디오를 먼저 로드해야 합니다. |
src | 동영상을 삽입할 URL |
일반적인 이벤트:
이벤트 | 트리거 타이밍 |
---|---|
로드스타트 | 로딩 시작 |
지속 시간 변경 | duration 속성 값이 수정될 때 트리거됩니다. |
환율변동 | 재생 속도가 변경될 때 발생 |
추구 | seeking 寻找中 点击一个为(缓存)下载的区域 |
seeked | seeked 寻找完成时触发 |
play | 开始播放时触发 |
waiting | 播放由于下一帧数据未获取到导致播放停止,但是播放器没有主动预期其停止,仍然在努力的获取数据,简单的说就是在等待下一帧视频数据,暂时还无法播放。 |
playing | 我们能看到视频时触发,也就是真正处于播放状态 |
canplay | 浏览器可以播放媒体文件,但是没有足够的数据支撑到播放结束,需要不停缓存更多内容 |
pause | 暂停播放时触发 |
ended | 视频停止,media已经播放到终点时触发, loop 的情况下不会触发 |
volumechange | 音量改变时触发 |
loadedmetadata | 获取视频meta信息完毕,这个时候播放器已经获取到了视频时长和视频资源的文件大小。 |
loadeddata | media中的首帧已经加载时触发, 视频播放器第一次完成了当前播放位置的视频渲染。 |
abort | 客户端主动终止下载(不是因为错误引起) |
error | video.error.code: 1.用户终止 2.网络错误 3.解码错误 4.URL无效 |
canplaythrough | 浏览器可以播放文件,不需要停止缓存更多内容 |
progress | 客户端请求数据 |
timeupdate | 当video.currentTime发生改变时触发该事件 |
stalled | 网速失速 |
suspend | 延迟下载 |
方法:
方法 | 描述 |
---|---|
play() | 播放视频 |
pause() | 暂停视频 |
canPlayType() | 测试video元素是否支持给定MIME类型的文件 |
requestFullscreen() / mozRequestFullScreen() / webkitRequestFullScreen() | 全屏 |
3. 自定义视频播放器
首先需要去掉video
身上的属性controls
属性,将所有播放的动作交由我们自己控制。
3.1 自定义播放或暂停
const playBtn = document.getElementById('playBtnId');
playBtn.addEventListener('click', function() {
if (video.paused) {
video.play();
playBtn.textContent = '||'; // 切换样式
} else {
video.pause();
playBtn.textContent = '>'; // 切换样式
}
});
复制代码
3.2 音量控制
// 音量增加
const volIncBtn = document.getElementById('volIncId');
volIncBtn.addEventListener('click', function() {
video.volume > 0.9 ? (video.volume = 1) : (video.volume += 0.1);
});
// 音量减小
const volDecBtn = document.getElementById('volDecId');
volDecBtn.addEventListener('click', function() {
video.volume < 0.1 ? (video.volume = 0) : (video.volume -= 0.1);
});
复制代码
3.3 静音
const mutedBtn = document.getElementById('mutedId');
mutedBtn.addEventListener('click', function() {
video.muted = !video.muted;
mutedBtn.textContent = video.muted ? '恢复' : '静音';
});
复制代码
3.4 播放快进/快退
- 快进
const speedUpBtn = document.getElementById(speedUpId);
let _speed = 1;
speedUpBtn.addEventListener('click', function() {
_speed = _speed * 2;
if (_speed > 4) {
_speed = 1;
}
video.playbackRate = _speed;
speedUpBtn.textContent = _speed === 1 ? '快进' : '快进x' + _speed;
});
复制代码
- 快退
const backBtn = document.getElementById(backBtnId);
let back_speed = 1;
let _t;
backBtn.addEventListener('click', function() {
back_speed = back_speed * 2;
if (back_speed > 4) {
video.playbackRate = 1;
back_speed = 1;
clearInterval(_t);
} else {
video.playbackRate = 0;
clearInterval(_t);
_t = setInterval(function() {
video.currentTime -= back_speed * 0.1;
}, 100);
}
backBtn.textContent = back_speed === 1 ? '快退' : '快退x' + back_speed;
});
复制代码
3.5 全屏
const fullScreenBtn = document.getElementById(fullScreenId);
const fullScreen = function() {
fullScreenBtn.addEventListener('click', function() {
if (video.requestFullscreen) {
video.requestFullscreen();
} else if (video.mozRequestFullScreen) {
video.mozRequestFullScreen();
} else if (video.webkitRequestFullScreen) {
video.webkitRequestFullScreen();
}
});
};
复制代码
3.6 进度条和时间显示
const getTime = function() {
// 当前播放时间
nowTime.textContent = 0;
// 总时长
duration.textContent = 0;
video.addEventListener('timeupdate', function() {
// 当前播放时间, parseTime: 格式化时间
nowTime.textContent = parseTime(video.currentTime);
// 计算进度条
const percent = video.currentTime / video.duration;
playProgress.style.width = percent * progressWrap.offsetWidth + 'px';
});
video.addEventListener('loadedmetadata', function() {
// 更新视频总时长
duration.textContent = parseTime(video.duration);
});
};
复制代码
3.7 진행률 표시줄을 수동으로 클릭하여 빨리 감기(비디오 점프)
progressWrap.addEventListener('click', function(e) {
if (video.paused || video.ended) {
video.play();
}
const length = e.pageX - progressWrap.offsetLeft;
const percent = length / progressWrap.offsetWidth;
playProgress.style.width = percent * progressWrap.offsetWidth + 'px';
video.currentTime = percent * video.duration;
});
复制代码
4. 비디오 세그먼트 로딩
HTML5 비디오 플레이어에 대한 위의 정보를 확인한 다음 서버에서 클라이언트로 비디오를 가져와야 합니다.
스트리밍 비디오는 비디오 파일이 클 때 권장되며 모든 크기를 지원합니다. 를 활용 fs.createReadStream()
하여 서버는 전체 파일을 한 번에 메모리로 읽는 대신 스트림에서 파일을 읽을 수 있습니다. 그런 다음 범위 요청을 통해 클라이언트에 비디오를 보냅니다. 그리고 클라이언트는 페이지가 서버에서 전체 비디오를 다운로드할 때까지 기다릴 필요가 없으며 비디오가 시작되기 몇 초 전에 서버를 요청할 수 있으며 요청하는 동안 비디오를 재생할 수 있습니다.
- fs.statSync(): 파일의 통계 정보를 얻기 위한 메소드로, 현재 로드된 청크가 파일의 끝에 도달하면 파일 크기를 알 수 있다.
fileSize = fs.statSync(filePath).size
- fs.createReadStream(): 지정된 파일에 대한 스트림 생성 fs.createReadStream(filePath, { start, end })
- 전체 청크의 크기를 반환합니다. endChunk - startChunk.
- HTTP 206: 중단 없이 프런트 엔드에 데이터 블록을 제공하는 데 사용됩니다. 재요청 시 다음 정보가 필요합니다.
- '콘텐츠 범위': '바이트 chunkStart-chunkEnd/chunkSize'
- '허용 범위': '바이트'
- '콘텐츠 길이': chunkSize
- '콘텐츠 유형': '동영상/webm'
여기서는 에그 프레임워크를 사용하여 범위 요청 비디오 기능을 구현합니다.
async getVideo() {
const { ctx } = this;
const req = ctx.request;
try {
const homedir = `${process.env.HOME || process.env.USERPROFILE}/`;
const filePath = path.resolve(`${process.env.NODE_ENV === 'development' ? '' : homedir}${req.query.filePath}`);
const range = req.headers.range;
const fileSize = fs.statSync(filePath).size;
if (range) {
const positions = range.replace(/bytes=/, '').split('-');
const start = parseInt(positions[0], 10);
const end = positions[1] ? parseInt(positions[1], 10) : fileSize - 1;
const chunksize = end - start + 1;
if (start >= fileSize) {
ctx.status = 416;
ctx.body =
'Requested range not satisfiable\n' + start + ' >= ' + fileSize;
return;
}
ctx.status = 206;
const header = {
'Accept-Ranges': 'bytes',
'Content-Type': 'video/webm',
'Content-Length': chunksize,
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'cache-control': 'public,max-age=31536000',
};
ctx.set(header);
ctx.body = fs
.createReadStream(filePath, {
start,
end,
autoClose: true,
})
.on('err', err => {
console.log(`[Video Play]: ${req.url}, 'pip stream error`);
ctx.body = err;
ctx.status = 500;
});
} else {
this.ctx.set('Content-Length', fileSize);
this.ctx.set('Content-Type', 'video/webm');
this.ctx.status = 200;
this.ctx.body = fs.createReadStream(filePath);
}
} catch (err) {
console.log(err);
ctx.body = err;
ctx.status = 500;
}
}
复制代码