前端修仙路 - WebRTC之设备管理

本文详细介绍了WebRTC中的设备管理,包括如何获取用户权限、管理音频和视频设备流,以及屏幕共享和音频可视化的方法。重点讲解了getUserMediaAPI的使用和不同设备类型的管理策略。
摘要由CSDN通过智能技术生成

WebRTC-设备管理

WebRTC(Web Real-Time Communications)是一项实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流和(或)音频流或者其他任意数据的传输。

本文主要介绍关于摄像头、麦克风,扬声器、屏幕共享相关控制和调用。

申请权限与设备开启

// getUserMedia获取设备流的同时,浏览器会发起权限申请。
navigator.mediaDevices.getUserMedia({audio:true,video:true}).then((stream)=>{
    // 通过流,获取所有轨道,并终止监听,因为此处仅是为了提前获取权限,所以不需要一开始就监听音视频流
    stream.getTracks().forEach((track)=>{
        track.stop()
    })
}).catch(function (err) {
    /* 处理 error */
  })
  • 火狐浏览器终止流后无法获取设备列表具体信息,如果需要获取设备列表的,enumerateDevices获取完后再调用track.stop()终止监听。
  • https站点,或localhost才支持摄像头麦克风的获取,也可以通过配置浏览器设置,跳过校验。

获取设备列表

// 枚举获取所有设备列表,包含扬声器,
navigator.mediaDevices
    .enumerateDevices()
    .then((devices) => {
        devices.forEach((device) => {
        // kind 设备类型,"videoinput" 摄像头  "audioinput" 麦克风  "audiooutput" 扬声器
        // label 设备名
        // deviceId 设备id
        console.log(`${device.kind}: ${device.label} id = ${device.deviceId}`);
        });
    })
    .catch((err) => {
        console.error(`${err.name}: ${err.message}`);
    });
// output:
// videoinput: FaceTime HD Camera (Built-in) id=csO9c0YpAf274OuCPUA53CNE0YHlIr2yXCi+SqfBZZ8=
// audioinput: default (Built-in Microphone) id=RKxXByjnabbADGQNNZqLVLdmXlS0YkETYCIbg+XxnvM=
// audioinput: Built-in Microphone id=r2/xw1xUPIyZunfV1lGrKOma5wTOvCkWfZ368XCndm0=
  • 注意,获取设备列表需要先调navigator.mediaDevices.getUserMedia申请权限,否则有可能拿不到设备label名字。

设备开启(监听音视频流)

// getUserMedia获取设备流的同时,浏览器会发起权限申请。
navigator.mediaDevices.getUserMedia({audio:true,video:true}).then((stream)=>{
   console.log(stream)
}).catch(function (err) {
    /* 处理 error */
  })
  • getUserMedia(constraints:MediaStreamConstraints):Promise<MediaStream>
    • MediaStreamConstraints 媒体约束对象,指定了请求的媒体类型和相对应的参数,可以调navigator.mediaDevices.getSupportedConstraints()` 查看支持的属性
      • 常见用法如下:
          // audio 开启麦克风  video 开启摄像头
          getUserMedia({audio:true,video:true})
      
      • 也可以传对象,属性为MediaStreamConstraints支持的属性
          // audio 开启麦克风  video 开启摄像头
          getUserMedia({
              audio: {
                  // deviceId:'',
                  echoCancellation: true, //音频开启回音消除
                  // noiseSuppression: true, // 开启降噪
                  // autoGainControl: true // 开启自动增益功能
                  },
              video: {
                  // deviceId:'' // 如果有多个设备,可以指定开启哪个设备
                  width: 1280,  // 获取的视频流宽度
                  height: 720,  // 获取的视频流高度
                  // facingMode:'user'  // 移动端调用前置摄像头
                  // facingMode: { exact: "environment" } 移动端调用后置摄像头 
              }
          })
      

设备关闭

// getUserMedia获取设备流的同时,浏览器会发起权限申请。
// navigator.mediaDevices.getUserMedia({audio:true,video:true}).then((stream)=>{
    // 获取所有轨道,包含视频轨道和音频轨道
   stream.getTracks().forEach((track)=>{
        track.stop()
    })
    // stream.getAudioTracks()  仅获取所有音频轨道
    // stream.getVideoTracks()  仅获取所有视频轨道
    // stream.addTrack(track) 添加轨道
    // stream.removeTrack(track) 移除轨道
// }).catch(function (err) {
    /* 处理 error */
//   })
  • stream:MediaStream 媒体流对象,通过getUserMedia获取,相关api和属性点击查看
  • 流对象也可以给video标签进行预览
const video = document.querySelector('video')
video.srcObject = stream
  • srcObject 通常是MediaStream对象,也可以是MediaSource, Blob 或者 File对象。

扬声器

扬声器的管理比较特殊,它得借助媒体对象(audio,video,AudioContext)才能进行管理,web无法直接操作系统的扬声器,但可以控制媒体对象进行播放,以及扬声器设备的切换。

// audio  音频标签
const audio = document.querySelector('audio')
audio.src = 'https://test.com/xx.mp3'
// 切换扬声器 sinkId 设备id
audio.setSinkId(sinkId)
audio.play()
// video.setSinkId 同理
  • HTMLMediaElement的属性和方法,点击查看
    • audio和video都是继承的这个对象,所以它有的属性和方法,均可以调用
// AudioContext  音频上下文对象,一般用作音频分析,可以做音频可视化,以及对音频做一些特殊处理
// 创建音频上下文实例
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
// 设置音频来源,可以是 HTMLMediaElement 对象。
const source = audioCtx.createMediaElementSource(document.querySelector('audio'))
// audioCtx.createMediaStreamSource(stream) // 也可以是来自媒体流对象 MediaStream
// 切换扬声器 sinkId 设备id
audioCtx.setSinkId(sinkId)
  • sinkId 即扬声器的设备id,可以从MediaDevices.enumerateDevices()获取
  • AudioContext的属性方法点击查看

屏幕共享

在这里插入图片描述


async function startCapture(displayMediaOptions) {
  let captureStream = null;

  try {
    // 开启屏幕捕获,同时会发起权限申请,如果没有权限,返回屏幕捕获的流
    captureStream =
      await navigator.mediaDevices.getDisplayMedia(displayMediaOptions);
  } catch (err) {
    console.error("Error: " + err);
  }
  return captureStream;
}

// 如果指定有audio,浏览器弹窗右下角会有一个是否共享音频的选项
const captureStream = startCapture({video:true,audio:true})
// 拿到捕获流之后,即可通过流取到对应的音视频轨道
let track = captureStream.getVideoTracks()[0]
// captureStream.getAudioTracks 获取所有音频轨道

track.onended = () => {
  //监听手动点击“停止分享”
}
// 停止屏幕捕获
track.stop()
  • getDisplayMedia(constraints:MediaStreamConstraints):Promise<MediaStream>

音频可视化

附上一个封装过的,支持音频文件和麦克风作为来源的可视化vue组件,输出展示为条形

使用

<template>
    <!-- 麦克风作为音源 -->
    <AudioVisual key="microphone" type="microphone"></AudioVisual>
    <!-- 音频文件作为音源 -->
    <AudioVisual key="audio" :volume="1" type="audio" audioUrl="/test.mp3"></AudioVisual>
</template>

组件

<!-- audioVisual组件 -->
<template>
    <div>
        <div class="audio-visual flex flex-cross-center flex-main-between" :style="{width:width+'px',height:height+'px'}">
            <div class="audio-bar" :class="i<=maxIndex&&maxIndex!=0?'active':''" v-for="(item,i) in barCount" :key="item">
            </div>
        </div>
        <audio v-if="audioUrl" ref="audio" controls style="display:none"></audio>
    </div>
</template>
<script>
// import "" from "@component/"
export default {
  name:'audioVisual',
  props:{
    width:{
      type:[String,Number],
      default(){
        return 2.5
      }
    },
    height:{
      type:[String,Number],
      default(){
        return 0.24
      }
    },
    // microphone 麦克风作为音源,audio 音频文件作为音源
    type:{
        type:[String],
        default(){
            return 'microphone'
        } 
    },
    volume:{
        type:[Number],
        default(){
            return 1
        }
    },
    audioUrl:{
        type:[String],
        default(){
            return ''
        } 
    },
    barCount:{
        type:[Number],
        default(){
            return 13
        }
    },
    microphoneStream:{
      type:[MediaStream],
      default(){
          return null
      }
    }
  },
  data(){
    return {
        maxIndex:0
    }
  },
  watch:{
    volume(n,o){
        if(n!=o){
            if(this.$refs.audio){
                this.$refs.audio.volume = this.volume/100
            }
        }
    }
  },
  created(){
    this.$nextTick(()=>{
        this.reload()
    })
  },
  mounted(){
  },
  beforeDestroy(){
    this.clear()
    if(this.audioCtx &&  this.audioCtx.state=='running'){
        this.$refs.audio && this.$refs.audio.pause()
        this.audioCtx.close()
    }
  },
  methods:{
    clear(){
        this.stopDevice()
        clearInterval(this.animateFrame)
    },
    reload(){
        if(this.type =='microphone'){
            this.getMicrophone()
        }else{
            this.initAudio()
        }
        
    },
    playAudio(){
        this.$refs.audio.src= this.audioUrl
        this.$refs.audio.currentTime = 0; // 重新播放
        this.$refs.audio.volume = this.volume/100
        try{
            if(this.$refs.audio && this.$refs.audio.paused){
                this.$refs.audio.play()
            }
        }catch(err){
            this.$refs.audio.oncanplay=()=>{
                if(this.$refs.audio && this.$refs.audio.paused){
                    this.$refs.audio.play()
                }
            }
        }
    },
    initAudio(){
        this.playAudio()
        this.initAudioVirtual(this.$refs.audio)
    },
    async switchDevice(deviceId){
        this.maxIndex = 0
        this.stopDevice()
        if(this.type=='microphone'){
            navigator.mediaDevices
            .getUserMedia({ 'audio': {
                deviceId:deviceId,
                echoCancellation: true, //音频开启回音消除
                // noiseSuppression: true, // 开启降噪
                // autoGainControl: true // 开启自动增益功能
                } })
            .then(this.initAudioVirtual)
            .catch(e => {
                console.error('获取麦克风权限失败', e);
            });
        }else{
            this.$refs.audio.currentTime = 0; // 重新播放
            if(deviceId=='default'){
                return
            }
            await this.audioCtx.setSinkId(deviceId)
            console.log(this.audioCtx.sinkId)
            
        }
    },
    getMicrophone(){
      this.maxIndex = 0
      if(this.microphoneStream){
        this.initAudioVirtual(this.microphoneStream)
        return
      }
      navigator.mediaDevices
      .getUserMedia({ 'audio': {
        echoCancellation: true, //音频开启回音消除
        // noiseSuppression: true, // 开启降噪
        // autoGainControl: true // 开启自动增益功能
      } })
      .then(this.initAudioVirtual)
      .catch(e => {
        console.error('获取麦克风权限失败', e);
      });
    },
    // 关闭设备流监听
    stopDevice(){
        if(this.stream && this.stream.getAudioTracks && !this.microphoneStream){
            this.stream.getAudioTracks().forEach((track)=>{
                track.stop()
            })
        }
        if(this.$refs.audio && !this.$refs.audio.paused){
            this.$refs.audio.pause()
        }
    },
    initAudioVirtual(audioSource){
    //   var canvasCtx = document.querySelector('#canvas').getContext('2d');
      if(this.audioCtx){
        if(this.audioCtx.state=='running'){
            this.audioCtx.close()
        }
        if(this.mediaElementSourceNode){
            this.mediaElementSourceNode.disconnect()
        }
      }
      this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
      this.stream = audioSource
    //   var oscillator = audioCtx.createOscillator();
    //   oscillator.type = "sine";
    //   oscillator.frequency.value = 0
      let audioCtx = this.audioCtx
      var analyser = audioCtx.createAnalyser();
      let source = ''
      let filter = null
      if(this.type=='microphone'){
        // 创建滤波器, 过滤回声
        filter = audioCtx.createBiquadFilter();
        filter.type = 'lowpass'; // 设置为低通滤波器
        filter.frequency.value = 1000; // 设置截止频率
        source = audioCtx.createMediaStreamSource(audioSource);
        source.connect(analyser);
        filter.connect(audioCtx.destination)
        // analyser.connect(audioCtx.destination);
      }else{
        source = this.mediaElementSourceNode ? this.mediaElementSourceNode: audioCtx.createMediaElementSource(audioSource);
        this.mediaElementSourceNode = source
        source.connect(analyser);
        analyser.connect(audioCtx.destination);
      }
      analyser.fftSize = 2048;
      // analyser.fftSize analyser.frequencyBinCount
      var bufferLength = analyser.frequencyBinCount;

    //   const WIDTH = this.width, HEIGHT = this.height
      

      // analyser.getByteTimeDomainData(dataArray);
      clearInterval(this.animateFrame)
      
      this.animateFrame = setInterval(draw.bind(this), 1000/25);

      function draw(){
        // 0-255
        var dataArray = new Uint8Array(bufferLength);
        analyser.getByteFrequencyData(dataArray);
        // console.log(Math.max.apply(null,dataArray))
        // canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
        // canvasCtx.fillStyle = 'rgb(255, 255, 255)';
        // canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
        let BAR_COUNT = this.barCount
        // let barWidth = WIDTH * 0.6 /BAR_COUNT;
        // let barSpacing = (WIDTH - barWidth * BAR_COUNT) / (BAR_COUNT-1) 
        // let barHeight = HEIGHT;
        // 舍弃了低音的一部分音频数据
        let max = Math.max.apply(null,dataArray)
        if(max<50){
          max = 0
        }
        if(this.type=='microphone' && this.microphoneStream){
          const filterValue = 130
          let value = 255-filterValue
          max = max - filterValue
          if(max<0) max = 0
          this.maxIndex = Math.floor(max / (value / BAR_COUNT))
          return
        }
        // console.log(max)

        this.maxIndex = Math.floor(max / (255 / BAR_COUNT))
        // canvas 画条形图,由于canvas画圆角矩形比较麻烦,转用html的方式
        // let x = 0;
        // // 画未激活块
        // for(let i = 0; i < BAR_COUNT; i++) {
        //   canvasCtx.fillStyle = this.inactiveColor;
        //   canvasCtx.fillRect(x,0,barWidth,barHeight);
        //   x += barSpacing + barWidth;
        // }
        // x = 0
        // // 画激活块
        // for(let i = 0; i < BAR_COUNT; i++) {
        //   // 激活色
        //   if(i < maxIndex){
        //     canvasCtx.fillStyle = this.activeColor;
        //     canvasCtx.fillRect(x,0,barWidth,barHeight);
        //   }
        //   x += barSpacing + barWidth;
        // }
      }


    }
  }
}
</script>

<style scoped lang="scss">
.audio-bar{
    width: 0.08rem;
    height: 0.24rem;
    background: #E3E5EE;
    border-radius: 0.04rem;
    &.active{
        background: #28D48C;
    }
}
</style>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值