video 与 audio
分别仅支持video/mp4
、video/webm
、video/ogg
和 audio/mp3
、audio/wav
、audio/ogg
格式文件,其他格式需通过插件支持
常用属性
- src 资源链接,或使用
source
标签
如有多个source
标签,则依次尝试,取第一个有效的为准 - controls 显示控制面板
- autoplay 自动播放(大部分浏览器无效,见下文)
- loop 自动循环
- muted 静音
- preload (
autoplay
时失效,必然加载)- auto(自动加载)
- meta(只加载元数据)
- none(不加载)
- poster 播放前显示的图像,仅对video有效
- height 仅对video有效
- width 仅对video有效
<audio controls>
<source src="/i/horse.ogg" type="audio/ogg">
<source src="/i/horse.mp3" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
事件顺序
当音频/视频处于加载过程中时,会依次发生以下事件:
- loadstart 可用于判断当前浏览器是否支持自动load和自动播放,如不支持,则需要考虑hack方法
- durationchange 时长数据发生变化
- loadedmetadata 载入元数据 包括宽高和时长,此时如果元素宽高未指定,则会自适应宽高
- loadeddata 当前帧的数据已加载,但没有足够的数据来播放指定音频/视频的下一帧时
- progress 正在下载
- canplay 部分可播放,此时播放按钮变为可点击状态
- canplaythrough 已经全部加载完毕,可以完整播放
自动播放(包括autoplay
属性和.play
方法)
对已经播放的媒体再次执行load,会导致重新播放。
为兼容不同浏览器,建议总是对未自动播放做处理:
var promise = document.querySelector('video').play();
if (promise !== undefined) {
promise.then(_ => {
// 开始播放
}).catch(error => {
// 引导用户进行页面交互
});
}
- 普通Web端
默认禁止自动播放(包括video/radio/Web Audio API),仅以下情况支持:- 媒体处于
muted
状态
注意:如一开始因处于muted
而自动播放,之后通过代码解除muted
,则播放会中止 - 用户进行了点击等界面交互后
注意:在vue等spa中,路由切换前后本质上是同一个页面,因此之前路由做过的用户交互也算 - (Chrome)用户之前在本站播放过有声音的视频,且媒体参与指数达标
- 媒体处于
- iframe允许自动播放。
<iframe src="./bg.mp3" allow="autoplay"/>
- 手机微信端
document.addEventListener("WeixinJSBridgeReady", function () {//不需要wx.config
document.getElementById('bgAudio').play();
}, false);
- APP内嵌webview
通过对webview容器的属性设置,允许自动播放 - Web Audio API
加载现有的音频文件,或创建新的音频
获取视频文件宽高信息
<input type="file" id="fileButton">
fileButton.addEventListener('change', evt => {
const file = fileButton.files[0]
const url = URL.createObjectURL(file)
const video = document.createElement('video')
video.onloadedmetadata = evt => {
URL.revokeObjectURL(url)//释放
console.log(video.videoWidth, video.videoHeight)
}
video.src = url
video.load() // fetches metadata
})
加密(防下载)
-
createObjectURL
资源URL会变成blob:
开头的虚拟地址,隐藏真实地址
var video = document.getElementById("video");
var xhr = new XMLHttpRequest();
xhr.open("GET", "./test.mp4", true);
xhr.responseType = "blob";
xhr.onload = function () {
if (this.status == 200) {
var blob = this.response;
video.onload = function (e) { window.URL.revokeObjectURL(video.src); };
video.src = window.URL.createObjectURL(blob);
}
}
xhr.send();
- controlslist="nodownload" 并覆盖 contextmenu
让视频没有默认的下载按钮,并隐藏右键另存为
canvas
通过在画布中逐帧渲染视频内容,也可以播放视频
render()
function render() {
window.requestAnimationFrame(render)
ctx.clearRect(0, 0,canvas.width,canvas.height)
ctx.drawImage(video, 0, 0,width,height) //绘制视频
}
流媒体协议
流媒体协议都属于应用层,建立在TCP/IP协议之上
RTSP(Real Time Streaming Protocol,实时流协议)
通常包含两个传输层协议:RTP(Real-time Transport Protocol,实时传输协议)和 RTCP(Real-time Transport Control Protocol,实时传输控制协议),结合使用:
RTP使用偶数端口号,用于传输数据(默认基于UDP协议)。
RTCP使用与RTP相邻的下一位奇数端口号,监控并管理数据传输
RTSP允许双向实时数据传输,如客户端向服务器端发送请求,实现回放、快进等操作,并基于RTP和RTCP实现视频流本身的传输
浏览器不支持该协议,不建议在web端使用
延迟极低(UDP,1秒以内)RTMP(Real Time Messaging Protocol,实时消息传输协议)
基于TCP长连接,是Adobe为Flash播放器开发的传输协议,传输需要Flash控件支持
传输flv
(Flash Video)或f4v
媒体流,播放需要Flash控件支持
浏览器已不支持RTMP传输(因2021年后主流浏览器弃用Flash),通常在流媒体服务器将 RTMP 转为成 HTTP-FLV,再推给web端
延迟中等(通常有2~5秒延迟)HTTP-FLV
将音视频数据封装成 FLV,然后通过 HTTP 长连接传输
兼容性极佳(本质上就是HTTP协议),主流浏览器均支持该传输
延迟中等(通常有2~5秒延迟)HLS(HTTP Live Streaming,HTTP实时流)
基于HTTP协议,由Apple提出
通过m3u8文件下载多个ts文件依次播放
兼容性极佳,主流浏览器均支持该传输
通常会下载完数个ts文件(客户端表现为缓冲)再开始播放,以保证播放流畅性,因此延迟较高(5秒以上。假设m3u8包含5个TS文件,每个时长5秒,则延迟为25秒),不适合做直播和监控MPEG-DASH(Dynamic Adaptive Streaming over HTTP)
类似HLS,把流切分为很小的片段。
直接建立在MSE上,比HLS兼容性更好。
同理,延迟较高,不适合做直播和监控-
WebSocket
兼容性极佳,主流浏览器均支持该传输
延迟取决于具体的传输和解析方式:- 小项目,可用WebSocket不断获取图片,渲染到 <img> 标签即可
- 大项目,可传输流媒体配合
MSE
或JSMpeg
等使用
-
WebRTC(Web Real-Time Communication,网页实时通信)
WebRTC 是 基于RTP/RTCP 的一整套 API,包含了流媒体协议在内的众多功能
支持捕获客户端本地的媒体流进行P2P传输(点对点,无服务器接入)
兼容性一般,IE11等老版本浏览器不支持。
延迟极低(1秒以内,因基于RTP),适用于视频会议等实时通讯场景,但P2P场景下因带宽限制,不建议过多(超过4人)用户同时推流- 捕获客户端音视频,对应接口 MediaStream
最初是navigator.getUserMedia
,最新Web标准变更为navigator.mediaDevices.getUserMedia
- 音视频传输,对应接口 RTCPeerConnection
- 任意数据传输,对应接口 RTCDataChannel
- 前端轻量插件:
Camera.js
- 框架:
PeerJS
、webRTC.io
、SimpleWebRTC
、easyrtc
等
- 捕获客户端音视频,对应接口 MediaStream
原生实现摄像头调用与切换:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>WebRTC 摄像头调用与切换</title>
</head>
<body>
<div>
<button onclick="startVideo()">startVideo</button>
<button onclick="closeMedia()">closeMedia</button>
<button onclick="captureImage()">captureImage</button>
</div>
<br>
<div id="deviceList"></div>
<br>
<video id="video" src=""></video>
<br>
<canvas id="canvas" width="640" height="480"></canvas>
</body>
<script>
// 兼容处理实现通用的 navigator.mediaDevices.getUserMedia
(function generateGetUserMedia() {
if (navigator.mediaDevices === undefined) navigator.mediaDevices = {};
// 为没有 navigator.mediaDevices.getUserMedia 的环境手动实现该方法
if (navigator.mediaDevices.getUserMedia === undefined) {
console.log("尝试封装 navigator.mediaDevices.getUserMedia")
navigator.mediaDevices.getUserMedia = (constraints) => {
// 通过不同浏览器可能的位置获取 getUserMedia
const getUserMedia =
navigator.getUserMedia || //旧版Chrome
(navigator.getUserMedia =
navigator.mozGetUserMedia || //Firefox
navigator.webkitGetUserMedia || //webkit内核
navigator.msGetUserMedia);
// 一些浏览器根本没实现它,则返回promise reject来保持接口一致性
if (!getUserMedia) {
return Promise.reject(new Error("该浏览器暂不支持访问摄像头!"));
}
// 否则,为老的navigator.getUserMedia方法包裹一个Promise以符合新版的标准
return new Promise((resolve, reject) => {
getUserMedia.call(navigator, constraints, resolve, reject);
});
};
}
})();
// 获取指定类型的设备列表
(function getDeviceList(kind = "videoinput") {
return navigator.mediaDevices.enumerateDevices()
.then(deviceInfos => {
// console.log("deviceInfos", deviceInfos)
//筛选摄像头
return deviceInfos.filter(deviceItem => deviceItem.kind == kind); //audioinput audiooutput videoinput 音频输入输出、视频输入
}).catch(error => console.error("enumerateDevices", error));
})()
.then(deviceList => {
deviceList.forEach(deviceItem => {
let deviceButton = document.createElement("button")
deviceButton.innerHTML = deviceItem.label;
deviceButton.style.marginRight = "10px";
deviceButton.addEventListener("click", () => {
startVideo(deviceItem.deviceId);
})
document.getElementById("deviceList").appendChild(deviceButton)
})
});
let myMediaStream;
// 播放视频
function startVideo(deviceId) {
const constraints = {
audio: false, //禁用录音。
// video: true, //启用摄像头。
video: {
width: 1920, //等价于 width: { ideal: 1920 }
height: {
ideal: 1080,
min: 1, //不大于min的媒体来源会被舍弃
max: 100 //大于max的媒体来源不会被舍弃,只会自动裁剪
},
facingMode: "user", // 等价于 facingMode: { ideal: "user" } 优先使用前置摄像头(如果有的话)
// facingMode: { exact: "environment" } // 强制使用后置摄像头
},
};
// 强制指定摄像头id
if (deviceId) {
constraints.video.deviceId = {
exact: deviceId
};
}
console.log("startVideo", constraints)
window.navigator.mediaDevices
.getUserMedia(constraints)
.then((stream) => { // 该stream为一个MediaStream对象
myMediaStream = stream;
const video = document.getElementById("video");
if ("srcObject" in video) { // 新版Chrome做法
video.srcObject = stream;
} else { // 旧浏览器做法
//兼容webkit内核
var compatibleURL = window.URL || window.webkitURL;
video.src = compatibleURL.createObjectURL(stream);
}
video.onloadedmetadata = (e) => {
video.play();
};
})
//用户拒绝了许可,或者浏览器无法找到指定的媒体类型/无法满足对应的参数要求
.catch((err) => {
console.log("getUserMedia 失败", err.name + ": " + err.message);
})
// 注意,由于用户不会被要求必须作出允许或者拒绝的选择,所以返回的Promise对象可能既不会触发resolve也不会触发 reject
}
// 关闭摄像头/麦克风
function closeMedia() {
if (myMediaStream) {
// tracks数组是按getUserMedia入参对象的倒序排列的,例如{video: true,audio: true}则tracks[0]为audio
myMediaStream.getTracks().forEach(track => {
track.stop();
});
}
}
// 截取画面
function captureImage() {
var cxt = canvas.getContext('2d');
cxt.drawImage(document.getElementById('video'), 0, 0, 640, 480);
}
</script>
</html>
通过 Vudio.js 实现麦克风音频波形拾取:
var canvas = document.querySelector('#canvas')
navigator.mediaDevices.getUserMedia({
audio: true
}).then((stream) => {
// 调用Vudio
var vudio = new Vudio(stream, canvas, {
accuracy: 256,
width: 1024,
height: 200,
waveform: {
fadeSide: false,
maxHeight: 200,
verticalAlign: 'middle',
horizontalAlign: 'center',
color: '#2980b9'
}
})
vudio.dance()
}).catch((error) => {
console.error(error.name || error)
})
m3u
M3U文件是一个记录索引的纯文本文件(如使用UTF-8编码,即为M3U8),记录了本地或云端的文件URL列表。
常见的在线视频网站,通过ffmpeg
将完整视频切割成多个.ts
文件(Transport Stream,该编码方式的文件可独立解码、无缝拼接),并生成m3u8索引文件。
- 对于点播,客户端读取m3u8后,按顺序下载资源,依次播放。
- 对于直播,客户端需要不断下载新的m3u8,获取新的资源并播放。
切片时,也可以生成多种清晰度的版本(同样在m3u8中记录),播放时可根据用户选择或网络状况,切换到其他清晰度的资源进行播放。
一次切分后,视频资源通过普通HTTP协议传输即可,相比于 RTSP 协议对信号源服务器压力更小。
m3u8文件规范
每一行是一个 URI,或空行,或以#
开头的字符串,行内无空白字符。
以#EXT
开头的为标签,其他#
开头的是注释-
如何观看m3u8指向的视频
- 使用第三方在线转换工具
-
PotPlayer-打开-打开链接
,即可解析m3u8观看视频 - 直接用
ffmpeg
合成
ffmpeg
跨平台多媒体处理工具,可用于视频的采集、切割(截图)、合成、转码、加水印
windows build版下载下载ffmpeg-n5.0-latest-win64-gpl-5.0.zip
切割为ts文件
ffmpeg.exe -i ../../test.mp4 -hls_time 10 -hls_list_size 0 -f hls new.m3u8
生成多个.ts
文件和一个new.m3u8
文件,每个ts文件长10秒左右
- -hls_time
每片秒数,默认为2
实际间隔和视频本身GOP(两个关键帧之间的时间间隔)有关,可配合-force_key_frames
使用 - -force_key_frames "expr:gte(t,n_forced*2)"
强制每2秒产生一个关键帧 - -hls_list_size
m3u8中保存的条数,默认为5(只保留最后生成的5条)
为0时保存所有 - -f hls
保存为流媒体
转换格式
ffmpeg.exe -i ../../test.mp4 ../../test.avi
web端解码播放
MSE(Media Source Extensions,媒体源扩展)
MSE API 为浏览器内置解码, 扩展了H5中的媒体相关能力
兼容性极佳(IE11也已支持大部分api),web接收到上述流媒体传输后,通过MSE API转化为MP4格式(片段则转为fMP4
),提供给video标签播放
- MediaSource 媒体资源容器
- SourceBuffer 通过 MediaSource 提供给 video 的媒体片段
- URL.createObjectURL() 创建一个指向一个 MediaSource 对象的 URL
jsmpeg
jsmpeg 为 JS软解码,兼容性极佳
仅支持 MPEG1 Video & MP2 Audio 格式(可通过ffmpeg转格式)
采用canvas而非video标签播放,解决了UI样式一致性、国产浏览器劫持video标签等问题
- 通过 ajax 加载点播视频
- 通过 Web Socket 加载直播视频流
Broadway(停更,不建议使用)
Broadway 为 JS软解码,仅支持H.264格式
基于以上解码方式的常用插件:
-
HLS.js,轻量,无UI,专用于HLS(即
.m3u8
文件) -
videojs,自带
video
的UI封装,支持HLS(集成HLS.js),并借助Flash插件支持RTMP -
jsmpeg ,自带
canvas
的UI封装,仅支持MPEG1格式 - Streamedian,专用于RTSP
- flv.js,b站开源插件,可在无Flash支持下播放 flv
- tcplayerlite ,腾讯播放器
- cyberplayer ,百度播放器
- DPlayer,弹幕视频播放器
videojs示例:
* 引入js和css后,对所有video标签生效
* 直接在html的video标签中加入`data-setup`属性配置属性
* 通过`videojs()`方法传入video元素或其id,初始化并配置属性
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>videojs</title>
<link href="https://vjs.zencdn.net/7.10.2/video-js.css" rel="stylesheet" />
</head>
<body>
<video id="my-video" class="video-js" data-setup='{"height":"300","width":"300"}'>
<!-- <source src="test.mp4" type="video/mp4" /> -->
<p class="vjs-no-js">
please enable JavaScript
</p>
</video>
<div id="btn" onclick="clickMe()">clickme</div>
</body>
<script src="https://vjs.zencdn.net/7.10.2/video.min.js"></script>
<script>
videojs.options.autoplay = true;//全局配置
var myPlayer = videojs('my-video', {
//video原有基础属性/标签
muted: true,
controls: true,
loop: false,
autoplay: false,
sources: [{
src: "test.mp4", type: "video/mp4"
}],
//videojs新增属性
// aspectRatio:"10:1",//长宽比(优先级高于宽高属性)
playbackRates: [0.5, 1, 1.5, 3],//可选播放速度
// fluid:true,
},
function onPlayerReady() {
console.log("muted", this.muted());
this.muted(false);
this.volume(0.5);//调整音量 0~1
this.play();
videojs.log("播放开始了1!");
});
//改变播放内容
myPlayer.src({ type: 'video/mp4', src: 'http://www.example.com/path/to/video.mp4' });
videojs.getPlayer(document.getElementById('my-video')).on("ended", function () {
videojs.log("播放结束了1!");
});
videojs.getPlayer('my-video').on("ended", function () {
videojs.log("播放结束了2!");
this.dispose();//销毁该实例,解除所有绑定事件并移除对应video元素
console.log("是否已销毁", this.isDisposed());
});
videojs.getPlayers()['my-video'].ready(function () {
videojs.log("播放开始了2!");
console.log("isFullscreen", myPlayer.isFullscreen());
});
function clickMe() {//requestFullscreen需要user gesture之后才能触发
console.log("isFullscreen", myPlayer.isFullscreen());
// document.documentElement.requestFullscreen();
myPlayer.requestFullscreen();
setTimeout(() => {//3秒后退出全屏
document.exitFullscreen();
}, 3000);
};
</script>
</html>