媒体流
大约 6 分钟
媒体流
1. navigator.mediaDevices.getUserMedia(constraints)
- 通过这个 API 可以获取媒体流权限,方法的参数是一个配置对象,可以配置媒体流类型以及分辨率等信息。可以通过
navigator.mediaDevices.getSupportedConstraints()
获取constraints
参数中具体支持的配置项 - 浏览器要获取摄像头权限需要开启本地端口或者 https 服务
// 可配置 audio video为true 获取麦克风和摄像头的权限
// 默认constraints参数
let constraints = {
audio: true,
video: true,
}
// 获取本地音视频流
async function getLocalStream(constraints: MediaStreamConstraints) {
// 获取媒体流
const stream = await navigator.mediaDevices.getUserMedia(constraints)
return stream
}
// video 中也可以设置设备 id、前后置摄像头以及获取视频的宽高
navigator.mediaDevices.getUserMedia({
audio: false,
video: {
width: 1280,
height: 720,
},
})
stream(管理 tarck 的集合)
stream 上有 2 个重要的属性
- active 代表该流是否为活动状态
- id 代表流的唯一值
在 stream 上有操作 track 的一些增删查方法:
- addTrack: 将 track 添加到 stream 中
- getTracks getVideoTracks getAudioTracks:会返回 track list
- getTrackById: track 是有 id 属性,可以根据 id 在 stream 中获取该 track,如果不存在会返回 null
- removeTrack: 传入值是 MediaStreamTrack 对象,而非 trackId
订阅 track 相关的事件:
- onaddtrack
- onremovetrack
track(视频/音频轨道)

- 属性(除了 enabled 外,全是只读):
- id: 代表唯一值
- kind: "audio" | "video"
- enabled: 表示该轨道是否可用,可以被手动设置,设置为 false 后,视频黑屏,音频静音
- label: 和 mediaDevices.enumerateDevices 返回值设备的 label 相对应
- muted: 是否静音
- readyState: 枚举值, "live"表示输入设备正常连接,"ended"表示没有更多的数据
- 方法
- getConstraints(): 返回创建该轨道的配置,即当前 track 对应的
constraints
值 - applyConstraints(): 给该轨道应用新的配置
- getSettings(): 会返回包含浏览器默认添加的在内的所有配置,也就是轨道的所有配置
- getCapabilities(): 方法返回一个 MediaTrackCapabilities 对象,此对象表示每个可调节属性的值或者范围,该特性依赖于平台和 user agent。eg:视频宽高的 min 和 max
- clone(): 克隆一个 track 的备份,和 stream 一样,会产出一个新的 id
- stop(): stop 后,readyState 的状态就变成了 ended
- getConstraints(): 返回创建该轨道的配置,即当前 track 对应的
2.结合 video 和 canvas 可实现的拍照功能
通过获取已经在播放媒体流的 video 标签,然后将其绘制到 canvas 上,再通过 toDataURL
方法将 canvas 转换为 base64 图片。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.10/dist/vue.js"></script>
</head>
<body>
<div id="app">
<div>
<video id="localVideo" autoplay playsinline muted></video>
<button @click="takePhoto()">拍照</button>
<div v-for="(item,index) in imgList" :key="index" class="item">
<img :src="item" alt="33" />
</div>
</div>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
imgList: [],
}
},
mounted() {
this.getLocalStream({
audio: false,
video: true,
})
},
methods: {
// 获取本地音视频流
async getLocalStream(constraints) {
// 获取媒体流
const stream = await navigator.mediaDevices.getUserMedia(constraints)
// 将媒体流设置到 video 标签上播放
this.playLocalStream(stream)
},
// 播放本地视频流
playLocalStream(stream) {
const videoEl = document.getElementById('localVideo')
videoEl.srcObject = stream
},
// 拍照
takePhoto() {
const videoEl = document.getElementById('localVideo')
const canvas = document.createElement('canvas')
canvas.width = videoEl.videoWidth
canvas.height = videoEl.videoHeight
const ctx = canvas.getContext('2d')
ctx.drawImage(videoEl, 0, 0, canvas.width, canvas.height)
this.imgList.push(canvas.toDataURL('image/png'))
// // 添加滤镜
const filterList = [
'blur(5px)', // 模糊
'brightness(0.5)', // 亮度
'contrast(200%)', // 对比度
'grayscale(100%)', // 灰度
'hue-rotate(90deg)', // 色相旋转
'invert(100%)', // 反色
'opacity(90%)', // 透明度
'saturate(200%)', // 饱和度
'saturate(20%)', // 饱和度
'sepia(100%)', // 褐色
'drop-shadow(4px 4px 8px blue)', // 阴影
]
for (let i = 0; i < filterList.length; i++) {
ctx.filter = filterList[i]
ctx.drawImage(videoEl, 0, 0, canvas.width, canvas.height)
this.imgList.push(canvas.toDataURL('image/png'))
}
console.log('imgList', this.imgList)
},
},
})
</script>
</body>
</html>
3.navigator.mediaDevices.enumerateDevices
- 通过这个 API 可以获取到设备列表(移动设备的后置摄像等),并且可以通过
deviceId
来切换设备,kind
字段表示设备类型,eg:videoinput
表示视频输入设备
// 获取所有视频输入设备
async function getDevices() {
const devices = await navigator.mediaDevices.enumerateDevices()
console.log('devices', devices)
let videoDevices = devices.filter((device) => device.kind === 'videoinput')
}
// 切换设备
function handleDeviceChange(deviceId: string) {
const stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: {
deviceId: { exact: deviceId },
},
})
}
// 切换前后摄像头
// 通过指定 facingMode 来实现,facingMode 有 4 个值,分别是 user(前)、environment(后) 和 left、right
function switchCamera() {
let constraints = {
video: true, // 开启默认摄像头
audio: true,
}
constraints.video = {
// 强制切换前后摄像头时,当摄像头不支持时,会报一个OverconstrainedError[无法满足要求的错误]
// facingMode: { exact: 'environment' },
// 也可以这样当前后摄像头不支持切换时,会继续使用当前摄像头,好处是不会报错
facingMode: 'environment',
}
}
4.navigator.mediaDevices.getDisplayMedia(constraints)
使用方法与getUserMedia
类似,可以获取屏幕的媒体流(屏幕共享)
// 获取屏幕共享的媒体流
async function shareScreen() {
let localStream = await navigator.mediaDevices.getDisplayMedia({
audio: true,
video: true,
})
// 播放本地视频流
playStream(localStream)
}
// 在视频标签中播放视频流
function playStream(stream: MediaStream) {
const video = document.querySelector('#localVideo')
video.srcObject = stream
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.10/dist/vue.js"></script>
</head>
<body>
<div id="app">
<div>
<video id="localVideo" autoplay playsinline muted></video>
<button @click="shareScreen()">屏幕共享</button>
</div>
</div>
<script>
new Vue({
el: '#app',
data() {
return {}
},
mounted() {},
methods: {
// 播放本地视频流
playLocalStream(stream) {
const videoEl = document.getElementById('localVideo')
videoEl.srcObject = stream
},
// 共享屏幕
async shareScreen() {
let stream = await navigator.mediaDevices.getDisplayMedia({
audio: true,
video: true,
})
this.playLocalStream(stream)
},
},
})
</script>
</body>
</html>
5.MediaRecorder
MediaRecorder
是录制媒体流的 API 但是 MediaRecorder
对 mimeType 参数的支持是有限的。所以我们需要通过 MediaRecorder.isTypeSupported
来判断当前浏览器是否支持我们需要的 mimeType。
- chrome 中 MediaRecorder 支持的 mimeType 如下:
'video/webm' 'video/webm;codecs=vp8' 'video/webm;codecs=vp9' 'video/webm;codecs=h264' 'video/x-matroska;codecs=avc1'
- 常用的视频编码
['vp9', 'vp9.0', 'vp8', 'vp8.0', 'avc1', 'av1', 'h265', 'h264']
- 常用的视频格式
['webm','mp4','ogg','mov','avi','wmv','flv','mkv','ts','x-matroska']
- 浏览器 video 标签支持的格式,容器封装格式有:MP4、WebM 和 Ogg
- MP4 : 使用 h.264 编码的视频和 aac 编码的音频
- WebM :使用 VP8 视频编解码器和 Vorbis 音频编解码器
- Ogg:使用 Theora 视频编解码器和 Vorbis 音频编解码器
const options = {
audioBitsPerSecond: 128000,
videoBitsPerSecond: 2500000, //返回视频采用的编码比率
mimeType: 'video/webm; codecs="vp8,opus"',
}
const mediaRecorder = new MediaRecorder(localStream, options)
// 录制开始
mediaRecorder.start()
mediaRecorder.ondataavailable = e => {
// 收集录制的媒体流
}
// 停止录制 同时触发dataavailable事件
mediaRecorder.onstop = (e: Event) => {
// 生成blob后可供下载(URL.createObjectURL + a标签) 或者 使用 ffmpeg 处理视频
const blob = new Blob([e.data], { type: 'video/mp4' })
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.10/dist/vue.js"></script>
</head>
<body>
<div id="app">
<div>
<video id="localVideo" autoplay playsinline muted></video>
<button @click="shareScreen()">屏幕共享</button>
<button @click="open()">打开摄像头</button>
<button @click="record()">{{timeCount === 0 ? '开始录制' : '终止录制 | ' + timeCount}}</button>
<button @click="downloadBlob()">下载录制视频</button>
</div>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
stream: null,
timeCount: 0,
mediaRecorder: null,
videoBlob: null,
}
},
mounted() {},
methods: {
// 播放本地视频流
playLocalStream(stream) {
const videoEl = document.getElementById('localVideo')
videoEl.srcObject = stream
},
// 打开摄像头
async open() {
this.stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true,
})
this.playLocalStream(this.stream)
},
// 共享屏幕
async shareScreen() {
this.stream = await navigator.mediaDevices.getDisplayMedia({
audio: {
echoCancellation: true, // 回音消除
noiseSuppression: true, // 噪音抑制
autoGainControl: true, // 自动增益
},
video: {
width: 1920, // 视频宽度
height: 1080, // 视频高度
frameRate: 60, // 帧率
aspectRatio: 16 / 9, // 宽高比
},
})
this.playLocalStream(this.stream)
},
// 录制
record() {
if (!this.stream) {
console.error('得先获取本地音视频流')
return
}
if (this.mediaRecorder && this.mediaRecorder.state === 'recording') {
this.mediaRecorder.stop()
return
}
const options = {
audioBitsPerSecond: 128000,
videoBitsPerSecond: 2500000,
mimeType: 'video/webm; codecs="vp8,opus"',
}
const chunks = [] // 收集媒体流
let timer = null // 定时器
this.mediaRecorder = new MediaRecorder(this.stream, options)
this.mediaRecorder.start()
this.mediaRecorder.ondataavailable = e => {
console.log(e)
chunks.push(e.data)
}
this.mediaRecorder.onstart = () => {
// 计时
timer = setInterval(() => {
this.timeCount++
}, 1000)
}
this.mediaRecorder.onstop = e => {
this.timeCount = 0
clearInterval(timer)
// 将录制的数据合并成一个 Blob 对象
this.videoBlob = new Blob(chunks, { type: 'video/mp4' }) // "video/mp4"
chunks.length = 0
}
},
// 下载 Blob
downloadBlob() {
let blob = this.videoBlob
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${new Date().getTime()}.${blob.type.split('/')[1].split(';')[0]}`
a.click()
URL.revokeObjectURL(url)
},
},
})
</script>
</body>
</html>