跳至主要內容

媒体流

星星大约 6 分钟

媒体流

1. navigator.mediaDevices.getUserMedia(constraints)open in new window

  • 通过这个 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 的集合)

  1. stream 上有 2 个重要的属性

    • active 代表该流是否为活动状态
    • id 代表流的唯一值
  2. 在 stream 上有操作 track 的一些增删查方法:

    • addTrack: 将 track 添加到 stream 中
    • getTracks getVideoTracks getAudioTracks:会返回 track list
    • getTrackById: track 是有 id 属性,可以根据 id 在 stream 中获取该 track,如果不存在会返回 null
    • removeTrack: 传入值是 MediaStreamTrack 对象,而非 trackId
  3. 订阅 track 相关的事件:

    • onaddtrack
    • onremovetrack

track(视频/音频轨道)

tracks
tracks
  1. 属性(除了 enabled 外,全是只读):
    • id: 代表唯一值
    • kind: "audio" | "video"
    • enabled: 表示该轨道是否可用,可以被手动设置,设置为 false 后,视频黑屏,音频静音
    • label: 和 mediaDevices.enumerateDevices 返回值设备的 label 相对应
    • muted: 是否静音
    • readyState: 枚举值, "live"表示输入设备正常连接,"ended"表示没有更多的数据
  2. 方法
    • getConstraints(): 返回创建该轨道的配置,即当前 track 对应的constraints
    • applyConstraints(): 给该轨道应用新的配置
    • getSettings(): 会返回包含浏览器默认添加的在内的所有配置,也就是轨道的所有配置
    • getCapabilities(): 方法返回一个 MediaTrackCapabilities 对象,此对象表示每个可调节属性的值或者范围,该特性依赖于平台和 user agent。eg:视频宽高的 min 和 max
    • clone(): 克隆一个 track 的备份,和 stream 一样,会产出一个新的 id
    • stop(): stop 后,readyState 的状态就变成了 ended

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)open in new window

使用方法与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.MediaRecorderopen in new window

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>
上次编辑于:
贡献者: wanghongjie