深入理解 HTML5 - 音视频录制

准备工作

  1. 必须有摄像头设备
  2. 必须使用HTTPS协议

如果找不到摄像头设备, 会出现以下错误:

如果不使用 HTTPS, 会出现以下错误:

提示用户授权:

获取设备 - getUserMedia

HTML5的 getUserMedia API 为用户提供访问硬件设备媒体 (摄像头、视频、音频、地理位置等) 的接口,基于该接口,开发者可以在不依赖任何浏览器插件的条件下访问硬件媒体设备。

getUserMedia API 最初是navigator.getUserMedia,目前已被最新Web标准废除,变更为navigator.mediaDevices.getUserMedia(),但浏览器支持情况不如旧版API普及。

navigator.mediaDevices.getUserMedia() 方法提示用户允许使用一个视频和/或一个音频输入设备,例如相机或屏幕共享和/或麦克风。如果用户给予许可,就返回一个Promise对象,MediaStream对象作为此Promise对象的Resolved[成功]状态的回调函数参数,相应的,如果用户拒绝了许可,或者没有媒体可用的情况下PermissionDeniedError或者NotFoundError作为此PromiseRejected[失败]状态的回调函数参数。注意,由于用户不会被要求必须作出允许或者拒绝的选择,所以返回的Promise对象可能既不会触发resolve也不会触发 reject

语法:

navigator.mediaDevices.getUserMedia(constraints)
.then(function(mediaStream) { ... })
.catch(function(error) { ... })

参考:

约束条件 - MediaStreamConstraints

getUserMedia() 函数仅接收一个参数, constraintsMediaStreamConstraints 对象, 指定请求的媒体类型,包含video和audio,必须至少一个类型或者两个同时可以被指定。如果浏览器无法找到指定的媒体类型或者无法满足相对应的参数要求,那么返回的Promise对象就会处于rejected[失败]状态,NotFoundError作为rejected[失败]回调的参数。

约束项可以具有下面这两个属性中的一个或两个:

  • video 表示是否需要视频轨道
  • audio 表示是否需要音频轨道

同事启用音频和视频:

let constraints = {
  audio: true,
  video: true
}

如果对视频或者音频指定了true,则生成的流将被要求拥有特定的媒体轨道,否则对 getUserMedia()的调用将导致 NotFoundError 错误的出现。

音频和视频属相可以是下面这两种类型的值:

  • 像上面的例子一样是一个布尔值
  • 一个MediaTrackConstraints对象,其提供了像宽度和高度这样必须与轨道匹配的特定属性

MediaTrackConstraints

视频约束: 分辨率

可以使用宽度和高度属性从网络摄像头请求一定的分辨率。

let constraints = {
  audio: true,
  video: {
    width: 1280,
    height: 720
  }
}

注意

经测试, 在移动端的width和height与设备的宽高正好相反, width指定的是设备的高, height指定的是设备的宽, PC端没做测试, 由于没有网络摄像头和麦克风

浏览器会尽可能的保持这个数据,但是也有可能会返回一个分辨率不一样的流。根据我的经验,这常常是因为摄像头不支持请求的分辨率造成的。也可能是由于在Mac上或者Chrome浏览器其他的标签页上有另一个程序的getUserMedia()覆盖了约束。当然也有可能存在其他原因。

你可以点击这个链接使用WebRTC摄像头分辨率查询器来看看自己的浏览器和摄像头都支持什么样的分辨率。

关键字

如果分辨率对于你来说很重要,而且设备和浏览器不能够保证分辨率的时候,你可以用min, max, ideal和exact关键字来帮助你从任何设备中得到最佳的分辨率。这些关键字可以应用到任何MediaTrackConstraint属性中。

  • min 最小尺寸
  • max 最大尺寸
  • ideal 理想尺寸, 若设置会优先寻找此分辨率或最接近此分辨率的设备
  • exact 约束为此尺寸

比如:

let constraints = {
  audio: false,
  video: {
    width: {
      min: 1024,
      max: 1920,
      ideal: 1280
    },
    height: {
      min: 776,
      max: 1080,
      ideal: 720
    }
  }
}

关键字不仅可以用于分辨率约束, 也适用于其他约束

视频约束: 获取移动设备的前置或者后置摄像头

我们可以使用视频轨道约束的facingMode属性。可接受的值有:user(前置摄像头),environment(后置摄像头),leftright

优先使用前置摄像头(如果有的话)

let constraints = {
  audio: true,
  video: {
    facingMode: "user"
  }
}

强制使用后置摄像头

let constraints = {
  audio: true,
  video: {
    facingMode: {
      exact: "environment"
    }
  }
}

视频约束: 帧速率

因为帧速率(frameRate)不仅对视频质量,还对带宽有着直接影响,所以在某些情况下,比如通过低带宽连接发布视频流的时候,限制帧速率可能是个好主意。

let constraints = {
  audio: true,
  video: {
    width: 720,
    height: 1080,
    frameRate: {
      ideal: 60,
      min: 10
    }
  }
}

使用特定的网络摄像头或者麦克风

下面这个约束属性同时适用于音频和视频轨道:deviceId。它指定了被用于捕捉流的设备ID。这个设备ID是唯一的,并且在同一个来源的会话中是相同的。你需要首先使用 MediaDevices.enumerateDevices() 来获取设备id。

一旦你知道了deviceId之后,你就可以要求指定的摄像头和麦克风了:

let constraints = {
  audio: {
    deviceId: {
      extra: 'xxx'
    }
  },
  video: {
    deviceId: {
      extra: 'xxx'
    }
  }
}

还有一个属性是groupId,这是一个实体设备中所有媒体源共享的ID。举个例子,在同一个耳机上的麦克风和扬声器就会共享同样的groupId。

音频约束

  • sampleRate 指定一个所需的采样率,不确定它应该被用作编码设置还是作为硬件要求,越高越好(比如CD的采样率就是44000 samples/s或44kHz)。
  • sampleSize 每个采样点大小的位数,越高越好(CD的采样大小为16 bits/sample)
  • volume 从0(静音)到1(最大音量)取值,被用作每个样本值的乘数
  • echoCancellation 是否使用回声消除来尝试去除通过麦克风回传到扬声器的音频, 默认值是 on
  • autoGainControl 是否要修改麦克风的输入音量
  • noiseSuppression 是否尝试去除音频信号中的背景噪声, 可选 true 或 false
  • latency 以秒为单位,控制开始处理声音和下一步可以使用数据之间的时间,想不明白你为什么想要设更高的延迟,但是音频编解码器的延时确实有所不同。
  • channelCount 规定了单声道的时候为1,立体声的时候为2。

参考:

MediaDevices.getSupportedConstraints

MediaDevices.getSupportedConstraints() 这个函数会返回一个字典,列出用户代理支持的约束。

点击这里测试你的浏览器支持的所有约束。

let supportedConstraints = navigator.mediaDevices.getSupportedConstraints();
for (let constraint in supportedConstraints) {
  if (supportedConstraints.hasOwnProperty(constraint)) {
    console.log(constraint)
  }
}

MediaStreamTrack.getSettings

还可以通过track.getSettings检查有哪些约束是支持的。它会返回一个包括了所有可用约束的对象,包括了那些浏览器虽然支持的,但是默认值没有通过代码改变的。

navigator.mediaDevices.getUserMedia(constraints).then(stream => {
  stream.getTracks().forEach(function(track) {
    console.log(track.getSettings());
  });
})

数据流 - MediaStream

成功回调函数seccessCallback的参数streamMediaStream的对象,表示媒体内容的数据流,可以通过URL.createObjectURL转换后设置为VideoAudio元素的src属性来使用,部分较新的浏览器也可以直接设置为srcObject属性来使用。

失败回调函数errorCallback的参数error,可能的异常有:

  • AbortError:硬件问题
  • NotAllowedError:用户拒绝了当前的浏览器实例的访问请求;或者用户拒绝了当前会话的访问;或者用户在全局范围内拒绝了所有媒体访问请求。
  • NotFoundError:找不到满足请求参数的媒体类型。
  • NotReadableError:操作系统上某个硬件、浏览器或者网页层面发生的错误导致设备无法被访问。
  • OverConstrainedError:指定的要求无法被设备满足。
  • SecurityError:安全错误,在getUserMedia() 被调用的 Document 上面,使用设备媒体被禁止。这个机制是否开启或者关闭取决于单个用户的偏好设置。
  • TypeError:类型错误,constraints对象未设置[空],或者都被设置为false

参考:

列出媒体设备 - enumerateDevices

通过 navigator.mediaDevices.enumerateDevices() 可获取所有媒体设备, 方便进行设备切换, 成功的回调参数为 MediaDeviceInfo 对象列表

一个简单的示例:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>enumerateDevices</title>
</head>
<body>
  <div id="container">
    <h1>enumerateDevices</h1>
    <div class="select">
      <label for="audioSource">Audio source: </label><select id="audioSource"></select>
    </div>
    <div class="select">
      <label for="videoSource">Video source: </label><select id="videoSource"></select>
    </div>
    <video muted autoplay></video>
  </div>
  <script>
    let videoElement = document.querySelector('video');
    let audioSelect = document.querySelector('select#audioSource');
    let videoSelect = document.querySelector('select#videoSource');
    navigator.mediaDevices.enumerateDevices()
      .then(gotDevices).then(getStream).catch(handleError);
    audioSelect.onchange = getStream;
    videoSelect.onchange = getStream;
    // 获取设备
    function gotDevices(deviceInfos) {
      for (let i = 0; i !== deviceInfos.length; ++i) {
        let deviceInfo = deviceInfos[i];
        console.log(deviceInfo);
        let option = document.createElement('option');
        option.value = deviceInfo.deviceId;
        // 音频设备
        if (deviceInfo.kind === 'audioinput') {
          option.text = deviceInfo.label || 'microphone ' + (audioSelect.length + 1);
          audioSelect.appendChild(option);
        // 视频设备
        } else if (deviceInfo.kind === 'videoinput') {
          option.text = deviceInfo.label || 'camera ' + (videoSelect.length + 1);
          videoSelect.appendChild(option);
        } else {
          console.log('Found one other kind of source/device: ', deviceInfo);
        }
      }
    }
    function getStream() {
      if (window.stream) {
        window.stream.getTracks().forEach(function(track) {
          track.stop();
        });
      }
      let constraints = {
        audio: {
          deviceId: {exact: audioSelect.value}
        },
        video: {
          deviceId: {exact: videoSelect.value}
        }
      };
      navigator.mediaDevices.getUserMedia(constraints).then(gotStream).catch(handleError);
    }
    function gotStream(stream) {
      window.stream = stream; // make stream available to console
      videoElement.srcObject = stream;
    }
    function handleError(error) {
      console.log('Error: ', error);
    }
  </script>
</body>
</html>

注意到上面有两个 then, 第一个 then 为列出设备(mediaDevices.enumerateDevices)成功或的回调, 第二个then为获取设备(mediaDevices.getUserMedia)成功后的回调, 上面通过绑定 deviceId 来完成不同设备的切换

效果:

编码类型支持程度

MediaRecorder.isTypeSupported() 方法会判断其 MIME 格式能否被客户端记录。

使用以下程序可以检测设备的编码类型支持程度

var types = [
              "video/webm",
              "audio/webm",
              "video/webm\;codecs=vp8",
              "video/webm\;codecs=daala",
              "video/webm\;codecs=h264",
              "audio/webm\;codecs=opus",
              "video/mpeg"
            ];
for (var i in types) {
  console.log( "Is " + types[i] + " supported? " + (MediaRecorder.isTypeSupported(types[i]) ? "Maybe!" : "Nope :("));
}

参考:

音视频录制 - MediaRecorder

MediaRecorder 用于将音视频流转化为二进制数据, 基本使用为:

navigator.mediaDevices.getUserMedia(constraints).then(stream => {
  this.recorder = new window.MediaRecorder(stream, this.mediaOptions);
  this.recorder.ondataavailable = this.getRecordingData;
  this.recorder.onstop = this.saveRecordingData;
}, error => {
  alert(error);
});
...
// 获取录制数据
function getRecordingData (e) {
  this.chunks.push(e.data)
}
// 保存录制数据
function saveRecordingData () {
  let blob = new Blob(this.chunks, { 'type' : 'video/webm;codecs=h264' })
  this.currVideo = URL.createObjectURL(blob)
  document.getElementById("video").srcObject = this.currVideo
  this.chunks = [];
}
...
document.getElementById("btn1").onClick = function () {
  this.recorder.start()
}
document.getElementById("btn2").onClick = function () {
  this.recorder.stop()
}

构造函数包括两个参数:

new MediaRecorder(stream[, options]);

第一个参数 stream 为使用 getUserMedia 获取到的流, 第二个参数为参数选项, 主要包括:

  • mimeType: mime类型, 使用 MediaRecorder.isTypeSupported 可查看支持的类型, 包括:
    • ["video/webm", "audio/webm", "video/webm;codecs=vp8", "video/webm;codecs=daala", "video/webm;codecs=h264", "audio/webm;codecs=opus", "video/mpeg"]
  • audioBitsPerSecond: 音频码率
  • videoBitsPerSecond: 视频码率
  • bitsPerSecond: 比特率, 包括音频和视频码率

常用方法有:

  • start 开始录制
  • stop 结束录制, 首先触发 ondataavailable, 然后触发 onstop

我们常常会在 ondataavailable 收集录制好的数据, 然后在 onstop 对这些数据进行处理

上面的示例, 录制完成后将数据转化为 Blob, 使用 URL.createObjectURL(blob) 将Blob转化为DataURL, 格式为 blob:https://test.xiaoyulive.top/xxxxx, 将之置于 video/audio/img 的 src 属性即可

参考:

实时音视频 - WebRTC

实例: 捕捉摄像头并进行拍照

一个简单的示例, 演示如何调用摄像头并进行拍照:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>MediaDevices</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
</head>
<body>
  <!--video用于显示媒体设备的视频流,自动播放-->
  <video id="video" autoplay style="width: 240px;height: 320px"></video>
  <!--按钮-->
  <div style="display: flex;">
    <div>
      <div><button id="capture1">拍照</button></div>
      <canvas id="canvas1" width="240" height="320" style="width: 240px;height: 320px"></canvas>
    </div>
    <div>
      <div><button id="capture2">摄像</button></div>
      <canvas id="canvas2" width="240" height="320" style="width: 240px;height: 320px"></canvas>
    </div>
  </div>
  <a download='snap.png' id="download">下载图片</a>
  <script>
    let video = document.getElementById("video");
    let canvas1 = document.getElementById("canvas1");
    let context1 = canvas1.getContext("2d");
    let canvas2 = document.getElementById("canvas2");
    let context2 = canvas2.getContext("2d");
    //访问用户媒体设备的兼容方法
    let mediaDevices = navigator.mediaDevices || navigator.webkitGetUserMedia;
    if (mediaDevices) {
      mediaDevices.getUserMedia({ audio: true, video: true })
        .then(stream => {
          //兼容webkit内核浏览器
          let url = window.URL || window.webkitURL;
          //将视频流设置为video元素的源
          video.src = url.createObjectURL(stream);
          //播放视频
          video.play();
        })
        .catch(error => {
          console.log("访问用户媒体设备失败:",error.name,error.message);
        });
    } else {
      alert("你的浏览器不支持访问用户媒体设备");
    }
    // 拍照
    document.getElementById("capture1").addEventListener("click",function(){
      context1.drawImage(video,0,0,240,320);
      // 设置下载图片的数据
      document.getElementById('download').href = canvas1.toDataURL('image/png');
    });
    // 摄像模拟 (canvas定时截取)
    document.getElementById("capture2").addEventListener("click",function(){
      setInterval(() => {
        context2.drawImage(video,0,0,240,320);
      }, 10);
    });
  </script>
</body>
</html>

一个 Vue 的示例:

<template lang='pug'>
  #CaptureImage
    .video(v-show="!showImage"): video.capture(
      ref="video"
      autoplay
      muted
    )
    .capture(v-show="showImage"): canvas(
      ref="canvas"
      :width="media.width" :height="media.height"
    )
    .controller
      .row
        .btn(@click="capture" v-show="!showImage") 拍摄
        .btn(@click="reCapture" v-show="showImage") 重拍
        .btn(@click="saveAndExit" v-show="showImage") 保存
      .row.camera
        .text 切换摄像头
        mt-switch(v-model="camera" @change="switchChange")
    c-Loading(v-show="uploading" text="照片上传中, 请稍等...")
</template>
<script>
  import {mapState} from "vuex"
  export default {
    name: "CaptureImage",
    data () {
      return {
        media: {
          width: 1080,
          height: 720
        },
        camera: true, // 前置后置摄像头切换
        cameras: [], // 摄像头列表
        currImage: null, // 当前录制的视频路径
        currBlob: null, // 当前视频二进制
        stream: null, // 视频流
        uploading: false, // 上传中
        showImage: false, // 显示播放器
      }
    },
    computed: {
      ...mapState({
        chosenChapter: state => state.chapter.chosen
      })
    },
    mounted () {
      if (!navigator.mediaDevices) {
        alert('您的浏览器不支持获取用户设备');
        this.$router.back()
        return;
      }
      this.video = this.$refs.video;
      this.canvas = this.$refs.canvas;
      this.ctx = this.canvas.getContext('2d');
      this.getDevices()
    },
    methods: {
      // 获取设备
      getDevices () {
        navigator.mediaDevices.enumerateDevices()
          .then(this.gotDevices).then(this.requestAccess)
          .catch(error => { alert('getDevices error: ' + error) });
      },
      gotDevices(deviceInfos) {
        for (let i = 0; i !== deviceInfos.length; ++i) {
          let deviceInfo = deviceInfos[i];
          if (deviceInfo.kind === 'videoinput') {
            this.cameras.push({
              value: deviceInfo.deviceId,
              kind: deviceInfo.kind,
              label: deviceInfo.label || 'camera ' + (this.cameras.length + 1),
            })
          }
        }
      },
      requestAccess() {
        if (this.stream) {
          this.stream.getTracks().forEach(function(track) {
            track.stop();
            console.log(track.getSettings())
          });
        }
        let constraints = {
          audio: false,
          video: {
            // 手机屏幕中宽高相反
            width: this.media.height,
            height: this.media.width,
            frameRate: {
              ideal: 60,
              min: 10
            },
            facingMode: this.camera ? 'environment' : 'user',
            deviceId: this.cameras[Number(this.camera)].value
          }
        }
        navigator.mediaDevices.getUserMedia(constraints).then(stream => {
          this.stream = stream;
          this.video.srcObject = stream;
        }, error => {
          alert(error);
        });
      },
      // 拍摄
      capture () {
        this.ctx.drawImage(this.video, 0, 0, this.media.width, this.media.height)
        this.canvas.toBlob((blob) => {
          this.currBlob = blob
          this.currImage = URL.createObjectURL(blob);
        });
        this.showImage = true
      },
      reCapture () {
        this.showImage = false
      },
      // 上传到OSS
      async save () {
        this.uploading = true
        let res = await this.$utils.uploadToOss(this.currBlob, [], 'image', {ext: '.jpg'})
        this.currImage = res.url
        this.uploading = false
      },
      // 保存并退出
      async saveAndExit () {
        await this.save()
        await this.$postData('addRes', {
          resPath: this.currImage // 资源路径
        })
        this.$router.back()
      },
      // 切换摄像头
      switchChange () {
        this.requestAccess()
      },
    }
  }
</script>
<style scoped lang='stylus'>
  .video, .capture
    video, canvas
      width: 9rem
      height: 6rem
      margin 0 auto
      display: block
      border: 1px solid #000
  .controller
    margin-top: 1em
    .row
      display: flex
      justify-content space-around
      margin-top: .2rem
      &.camera
        display: flex
        justify-content: flex-start
        align-items: center
        .text
          margin-right: 1em
          font-size: 1.2em
      .btn
        width: 2rem
        height: 2rem
        display: flex
        justify-content: center
        align-items: center
        background-color: rgba(35,184,255,1)
        color: #fff
        border-radius: 50%
        font-size: .6rem
</style>

在采集的过程中, 直接将视频流传入 video 的 src 即可, 建议设置为 autoplay, 否则需要手动调用 video.play(), 一定要设置 muted, 否则将会出现麦克风回声。

拍照的逻辑是借用 canvas 完成的, 直接将当前 video 的画面截取即可。

以上示例部分API没放出, utils.uploadToOsspostData 为我自己封装的上传到 OSS 及数据请求的方法, 自行补全或脑补

除此之外, 还有直接使用 input 采集图像视频的方法:

<!--调用手机的拍照功能和直接打开系统文件目录。-->
<input type="file" accept="image/*" capture="camera">
<!--调用手机的录像功能和直接打开系统文件目录。-->
<input type="file" accept="video/*" capture="camcorder">
<!--调用手机的录像功能和直接打开系统文件目录。苹果手机调出来的是拍照和录像安卓手机调出来的录音功能-->
<input type="file" accept="audio/*" capture="microphone">

参考:

实例: 捕获摄像头并进行录像

一个简单的示例, 演示如何调用摄像头并进行录像:

<template lang='pug'>
#CaptureVideo
  .video: video.capture(
    v-show='!showVideo'
    ref="video"
    autoplay
    muted
  )
  .video: video(
    v-show="showVideo"
    :src="currVideo"
    controls
  )
  .controller
    .row
      .btn(@click="record" v-if='!showVideo && !recording') 录制
      .btn(@click="stop" v-if="recording") 停止
      .btn(@click="recordAgain" v-if='showVideo') 重录
      .btn(@click="saveAndExit" v-if='showVideo') 保存
    .row.camera(v-if="!showVideo && !recording")
      .text 切换摄像头
      mt-switch(v-model="camera" @change="switchChange")
    .row(v-if='recording')
      div 剩余录制时间 {{Math.floor(remainTime/60)}}:{{Math.floor(remainTime%60)}}
  c-Loading(v-show="uploading" text="视频上传中, 请稍等...")
</template>
<script>
import {mapState} from "vuex"
export default {
  name: "CaptureVideo",
  data () {
    return {
      media: {
        width: 1080,
        height: 720
      },
      camera: true, // 前置后置摄像头切换
      cameras: [], // 摄像头列表
      mediaOptions: {
        audioBitsPerSecond : 128000, // 音频码率
        videoBitsPerSecond : 2500000, // 视频码率
        mimeType : 'video/webm;codecs=h264'  // 编码格式
      }, // MediaRecorder 选项
      chunks: [],
      recording: false,
      currVideo: null, // 当前录制的视频路径
      currBlob: null, // 当前视频二进制
      recorder: null, // MediaRecorder
      stream: null, // 视频流
      uploading: false, // 上传中
      showVideo: false, // 显示播放器
      interval: null, // 计时器
      totalTime: 60 * 3, // 剩余总时间
      remainTime: 60 * 3, // 剩余录制时间
    }
  },
  computed: {
    ...mapState({
      chosenChapter: state => state.chapter.chosen
    })
  },
  mounted () {
    if (!navigator.mediaDevices) {
      alert('您的浏览器不支持获取用户设备');
      this.$router.back()
      return;
    }
    if (!window.MediaRecorder) {
      alert('您的浏览器不支持录音');
      this.$router.back()
      return;
    }
    this.video = this.$refs.video;
    this.getDevices()
  },
  methods: {
    // 获取设备
    getDevices () {
      navigator.mediaDevices.enumerateDevices()
        .then(this.gotDevices).then(this.requestAccess)
        .catch(error => { alert('getDevices error: ' + error) });
    },
    gotDevices(deviceInfos) {
      for (let i = 0; i !== deviceInfos.length; ++i) {
        let deviceInfo = deviceInfos[i];
        if (deviceInfo.kind === 'videoinput') {
          this.cameras.push({
            value: deviceInfo.deviceId,
            kind: deviceInfo.kind,
            label: deviceInfo.label || 'camera ' + (this.cameras.length + 1),
          })
        }
      }
    },
    requestAccess() {
      if (this.stream) {
        this.stream.getTracks().forEach(function(track) {
          track.stop();
        });
      }
      let constraints = {
        audio: true,
        video: {
          // 手机屏幕中宽高相反
          width: this.media.height,
          height: this.media.width,
          frameRate: {
            ideal: 60,
            min: 10
          },
          facingMode: this.camera ? 'environment' : 'user',
          // deviceId: this.cameras[Number(this.camera)].value
        }
      }
      navigator.mediaDevices.getUserMedia(constraints).then(stream => {
        this.recorder = new window.MediaRecorder(stream, this.mediaOptions);
        this.stream = stream;
        this.video.srcObject = stream;
        this.recorder.ondataavailable = this.getRecordingData;
        this.recorder.onstop = this.saveRecordingData;
      }, error => {
        alert(error);
      });
    },
    // 获取录制数据
    getRecordingData (e) {
      this.chunks.push(e.data)
    },
    // 保存录制数据
    async saveRecordingData () {
      this.recording = false
      if (this.showVideo) {
        let blob = new Blob(this.chunks, { 'type' : 'video/webm;codecs=h264' })
        this.currBlob = blob
        this.currVideo = URL.createObjectURL(blob)
        this.chunks = [];
        await this.save()
      }
    },
    // 录制
    record () {
      this.recording = true
      this.recorder.start();
      this.interval = setInterval(() => {
        -- this.remainTime
        if (this.remainTime <= 0 ) {
          clearInterval(this.interval)
          this.remainTime = this.totalTime
          this.stop()
        }
      }, 1000)
    },
    // 重录
    recordAgain () {
      this.showVideo = false
      this.currVideo = null
      this.record()
    },
    // 停止
    async stop () {
      clearInterval(this.interval)
      this.remainTime = this.totalTime
      this.showVideo = true
      this.recorder.stop();
    },
    // 上传到OSS
    async save () {
      this.uploading = true
      let res = await this.$utils.uploadToOss(this.currBlob, [], 'video', {ext: '.mp4'})
      this.currVideo = res.url
      this.uploading = false
    },
    // 保存并退出
    async saveAndExit () {
      await this.$postData('addRes', {
        resPath: this.currVideo, // 资源路径
      })
      this.$router.back()
    },
    // 切换摄像头
    switchChange () {
      if (this.recording) {
        this.showVideo = false
        this.recorder.stop();
      }
      this.currVideo = null
      this.requestAccess()
    },
  }
}
</script>
<style scoped lang='stylus'>
.video
  video, img
    width: 9rem
    height: 6rem
    margin 0 auto
    display: block
    border: 1px solid #000
.controller
  margin-top: 1em
  .row
    display: flex
    justify-content space-around
    margin-top: .2rem
    &.camera
      display: flex
      justify-content: flex-start
      align-items: center
      .text
        margin-right: 1em
        font-size: 1.2em
    .btn
      width: 2rem
      height: 2rem
      display: flex
      justify-content: center
      align-items: center
      background-color: rgba(35,184,255,1)
      color: #fff
      border-radius: 50%
      font-size: .6rem
</style>

以上示例, 通过 MediaRecorder 采集音视频流, 最后将采集的 Blob 上传到 OSS

参考:

实例: 捕获录音设备并进行录音

一个简单的示例, 演示如何调用麦克风并进行录音, 其实也就是getUserMedia的约束少了一个video:

<template lang='pug'>
  #CaptureAudio
    .audio: audio(
      :src="currAudio"
      controls
      v-if="showAudio"
    )
    .controller
      .row
        .btn(@click="record" v-if='!showAudio && !recording') 录音
        .btn(@click="stop" v-if="recording") 停止
        .btn(@click="recordAgain" v-if='showAudio') 重录
        .btn(@click="saveAndExit" v-if='showAudio') 保存
      .row(v-if='recording')
        div 剩余录制时间 {{Math.floor(remainTime/60)}}:{{Math.floor(remainTime%60)}}
    canvas(ref="canvas" v-show="false")
    c-Loading(v-show="uploading" text="音频上传中, 请稍等...")
</template>
<script>
  import {mapState} from "vuex"
  export default {
    name: "CaptureAudio",
    data () {
      return {
        chunks: [],
        recording: false,
        currAudio: null, // 当前录制的视频路径
        currBlob: null, // 当前视频二进制
        recorder: null, // MediaRecorder
        uploading: false, // 上传中
        showAudio: false, // 显示播放器
        interval: null, // 计时器
        totalTime: 60 * 3, // 剩余总时间
        remainTime: 60 * 3, // 剩余录制时间
      }
    },
    computed: {
      ...mapState({
        chosenChapter: state => state.chapter.chosen
      })
    },
    mounted () {
      if (!navigator.mediaDevices) {
        alert('您的浏览器不支持获取用户设备');
        this.$router.back()
        return;
      }
      if (!window.MediaRecorder) {
        alert('您的浏览器不支持录音');
        this.$router.back()
        return;
      }
      this.requestAccess()
    },
    methods: {
      // 获取设备权限
      requestAccess() {
        let constraints = {
          audio: {
            sampleRate: 48000,
            channelCount: 0, // 0 捕获所有支持的声道 1 单声道 2 立体声
            volume: 1, // 0~1
            noiseSuppression: true, // 抑制背景噪音
          },
          video: false
        }
        navigator.mediaDevices.getUserMedia(constraints).then(stream => {
          this.recorder = new window.MediaRecorder(stream);
          console.log("recorder", this.recorder)
          this.recorder.ondataavailable = this.getRecordingData;
          this.recorder.onstop = this.saveRecordingData;
        }, error => {
          alert(error);
        });
      },
      // 获取录制数据
      getRecordingData (e) {
        console.log("getRecordingData", e)
        this.chunks.push(e.data)
      },
      async saveRecordingData () {
        clearInterval(this.interval)
        this.remainTime = this.totalTime
        this.showAudio = true
        this.recording = false
        let blob = new Blob(this.chunks, { 'type' : 'audio/ogg; codecs=opus' })
        this.currBlob = blob
        console.log("blob", blob)
        console.log("blob.size", blob.size)
        this.currAudio = URL.createObjectURL(blob)
        console.log(this.currAudio)
        this.chunks = []
        await this.save()
      },
      // 录制
      record () {
        this.recording = true
        this.recorder.start();
        this.interval = setInterval(() => {
          -- this.remainTime
          if (this.remainTime <= 0 ) {
            clearInterval(this.interval)
            this.remainTime = this.totalTime
            this.stop()
          }
        }, 1000)
      },
      // 重录
      recordAgain () {
        this.showAudio = false
        this.currAudio = null
        this.record()
      },
      // 停止
      async stop () {
        this.recorder.stop();
      },
      // 上传到OSS
      async save () {
        this.uploading = true
        let res = await this.$utils.uploadToOss(this.currBlob, [], 'audio', {ext: '.mp3'})
        this.currAudio = res.url
        this.uploading = false
      },
      // 保存并退出
      async saveAndExit () {
        await this.$postData('addRes', {
          resPath: this.currAudio, // 资源路径
        })
        this.$router.back()
      },
    }
  }
</script>
<style scoped lang='stylus'>
  .audio
    audio
      width: 100%
  .controller
    margin-top: 1em
    .row
      display: flex
      justify-content space-around
      margin-top: .2rem
      &.camera
        display: flex
        justify-content: flex-start
        align-items: center
        .text
          margin-right: 1em
          font-size: 1.2em
      .btn
        width: 2rem
        height: 2rem
        display: flex
        justify-content: center
        align-items: center
        background-color: rgba(35,184,255,1)
        color: #fff
        border-radius: 50%
        font-size: .6rem
</style>

以上示例, 通过 MediaRecorder 采集音频流, 最后将采集的 Blob 上传到 OSS

参考:

参考资料

MDN

相关项目

其他

MIT Licensed | Copyright © 2018-present 滇ICP备16006294号

Design by Quanzaiyu | Power by VuePress