Skip to content

上节课让大家去看看 B 站直播用的是哪种媒体流,大家去看了没有?没有也没关系,实际上现阶段基本上都是 FLV或者HLS的,毕竟大型直播以及搭配 CDN 等都有成熟的案例,而现阶段,WebRTC在直播场景中还是有局限性的。虽然大型直播没有,但是小型的直播还是蛮多的,而我们这节课就利用上节 WebRTC推流到 SRS 流媒体服务器后,再用 WebRTC去拉流完成直播。

当然,上节课推流后得到的两种流地址都是可以直接作为直播源的,但是在拉流速度上和WebRTC还有差别,接下来我们就来看看,用 WebRTC和 SRS 如何提高拉流的效率,大家再和 FLVHLS拉流对比下,看下具体的差异。

WebRTC 拉流

  1. 获取已知要拉取的流 ID,即推流地址中的 streamId。你可以把这个流 ID 当作是直播间的房间号,具有唯一性。

  1. 初始化 WebRTC核心关联对象 PeerConnection实例,同时监听远程媒体流。
const that = this
if(that.pc){
        that.pc.close();
}
that.pc = await new PeerConnection(null);
//注意这里和推流参数的区别
that.pc.addTransceiver("audio", {direction: "recvonly"});
that.pc.addTransceiver("video", {direction: "recvonly"});
//这里监听远程媒体流过来
that.pc.ontrack  = function (e) {
        that.setDomVideoTrick(e.track)
}
//创建会话信令
let offer = await that.pc.createOffer();
//本地添加一份
await that.pc.setLocalDescription(offer)
  1. 通过 SRS 开放 API 交换基础信令 SDP,与本地同步。
//组装参数 按照API格式
let data = {
  "api": this.$srsServerAPIURL+"rtc/v1/play/",
  "streamurl": this.$srsServerRTCURL+streamId,
  "sdp": offer.sdp
}
//交换
axios.post(this.$srsServerAPIURL+'rtc/v1/play/',data)
.then( async res => {
        res = res.data
        console.log(res)
        if(res.code === 0){
        //得到流媒体服务器应答的信令,添加到本地核心关联实例化对象的种
                await that.pc.setRemoteDescription(new RTCSessionDescription({type: 'answer', sdp: res.sdp}))
        }
}).catch(err => {
        console.error("SRS 拉流异常",err)
})
  1. 监听到媒体流后挂载到本地 DOM 元素。
setDomVideoTrick(trick){
    // this.scanvideodomId 为本地页面已存在的video标签ID
      let video = document.getElementById(this.scanvideodomId)
      let stream = video.srcObject
      if(stream){
              stream.addTrack(trick)
      }else {
              stream = new MediaStream()
              stream.addTrack(trick)
              video.srcObject = stream
              video.controls = true;
              video.autoplay = true;
              video.muted = true
      }
}

通过以上步骤,我们就可以直接通过 WebRTC订阅到发布的媒体流了。而不是用之前的 HLS流或者 FLV 格式流去点播视频画面,给大家对比看下:

可以看到,我在直接推流后,右侧直播预览位置几乎立马显示画面,而后面我复制的 FLV 流去播放器播放则需要加载至少一秒钟,这就是WebRTC在流媒体直播领域的优势。

说完拉流,我们再说说直播过程中其他的功能,比如音视频的控制、切换,以及更高大上的连麦。

直播过程中音视频控制

音视频控制

看下面代码,是不是和《10 | 会议实战:实时通话过程中音频、视频画面实时控制切换》中媒体控制的代码很类似?是的,只要是 WebRTC 相关,不论是用 SRS 流媒体服务,还是网关 Janus 服务,其控制的核心都是核心关联对象PeerConnection

//音频控制 pc为 peerconnection 实例化后的对象
audioControl(b){
       if(this.pc){
               this.audioStatus = !this.audioStatus 
              const senders = this.pc.getSenders();
              const send = senders.find((s) => s.track.kind === 'audio')
              send.track.enabled = b 
       }else{
               this.$message.error("请先点击推流")
       }
}
//视频控制
audioControl(b){
       if(this.pc){
               this.videoStatus= !this.videoStatus
              const senders = this.pc.getSenders();
              const send = senders.find((s) => s.track.kind === 'video')
              send.track.enabled = b 
       }else{
               this.$message.error("请先点击推流")
       }
}

音视频切换

这里我们使用屏幕分享来实现这个功能。

  1. 获取屏幕分享流。
async getShareMedia(){
    const constraints = {
            video:{width:1920,height:1080},
            audio:false
    };
    return await navigator.mediaDevices.getDisplayMedia(constraints).catch(handleError);
}
  1. 通过核心实例化对象切换媒体流。
async changeVideo(){
       if(!this.pc){
               this.$message.error("请先点击推流")
               return
       }
       //这里获取上一步的屏幕分享流
       this.shareStream = await this.getShareMedia()
       //提取第一个视频Track
       const [videoTrack] = this.shareStream.getVideoTracks();
       //获取发送器
       const senders = this.pc.getSenders();
       const send = senders.find((s) => s.track.kind === 'video')
       //替换视频Track
       send.replaceTrack(videoTrack)
       //更改按钮状态
       this.shareStatus = true
}

直播连麦

从 WebRTC 推流到 SRS 流媒体服务器,再到从流媒体服务器拉流,这个过程中我们注意到,实例化PeerConnection后的核心对象中, addTransceiver方法中direction参数为sendonlyrecvonly,这个参数是什么意思呢?看下面表格:

参数RTCRtpSenderRTCRtpReceiver
sendrecv提供和发送 RTP 数据包(媒体信息)接收 RTP 包(媒体信息),也接收对等方 RTP 数据包
sendonly提供和发送 RTP 数据包(媒体信息)不接受 RTP 数据包
recvonly不提供和发送 RTP 数据包,也就是说本地有流媒体,但是你无法给对面发送接收 RTP 数据包
inactive不提供和发送 RTP 数据包不接收 RTP 数据包

表头中RTCRtpSenderRTCRtpReceiver这两个东西,你可以理解为手机充电线的那个充电头,一端接收,另一端输出,永久配对且缺一不可。而对于 WebRTC而言,它们的作用就是描述和控制媒体输出和输入,sendonly代表只发送媒体数据但是不接受,recvonly则相反,仅接收不发送媒体数据。

通过上面参数我们发现,当前拉流端推流端与 SRS 流服务器建立的 RTC 连接对于媒体接收和发送而言是单向的,不能通过已经建立的链接去反向发送媒体流,比如拉流端(观众)给推流端(主播)发送视频或音频。既然这样,那我们如何去实现 “直播连麦” 功能呢?

很简单,既然大家都在同一个直播间,我们可以让观众端在申请连麦同意后主动推流给 SRS 流媒体服务器,成功后再告诉主播观众推流的 流ID,然后让主播拉流不就可以了?

连麦实战

  1. 申请连麦。在申请的时候携带唯一的流 ID,确保预留且不重复的。
//服务端增加socket事件 
//申请连麦
s.on('applyMic',(data) => {
        let targetUid = data['targetUid']
        oneToOne(targetUid,getMsg('applyMic',"apply mic",200,data))
})
//同意
s.on('acceptApplyMic',(data) => {
        let targetUid = data['targetUid']
        oneToOne(targetUid,getMsg('acceptApplyMic',"acceptApplyMic mic",200,data))
})
//拒绝
s.on('refuseApplyMic',(data) => {
        let targetUid = data['targetUid']
        oneToOne(targetUid,getMsg('refuseApplyMic',"refuseApplyMic mic",200,data))
})


//客户端(包括主播和观众端 连接同一个socket服务器)并监听对应事件
applyMic(){
      let tid =  getParams('tid')//主播ID
      let params ={        "userId": getParams('userId'),"targetUid":tid,streamId:getParams('userId')+'-'+tid}
      this.linkSocket.emit('applyMic',params)
}
  1. 主播同意。同意后直接先根据观众发的流拉流即可。

if(e['type'] === 'applyMic'){
    //自动同意 根据自己的业务调整 这里我设置的是有连麦直接同意
    let params ={ "userId": getParams('userId'),"targetUid":e.data.userId}
    that.linkSocket.emit('acceptApplyMic',params)
    let remoteStreamId = e.data.streamId
    //直接拉流即可 等有流推进来则自动会加载出来
    that.$refs['srsRtcPullApplyMic'].getPullSdp(remoteStreamId)
}
  1. 观众端收到同意后开始推流。这一步就是普通的 WebRTC 直接推流即可。
  2. 主播端稍等即可加载出画面,开始双向通话。

至此我们的主播连麦完成了。

项目演示

  1. 打开项目,主播访问下面模块:

但是请注意启动后台,socket-server文件夹中的后台。

然后携带请求参数访问:

//指定房间号和用户ID  如果在自己的改造项目中可以写表单然后进行下一步 这里我为了演示 直接在URL携带参数
http://localhost:8082/srs-rtc-push?userId=999&roomId=111
  1. 点击推流,右上角则会直接用 RTC 去拉流预览,成功则自动会在直播预览那里显示画面,否则会弹出失败提示框。

  1. 点击麦克风或者摄像头切换,以及屏幕分享可以查看右上角预览画面变更(注意默认右上角画面是静音的,请手动开启)。
  2. 访问直播间模块页面,携带参数为推流页面的流 ID。

//携带个人信息+直播间流ID+tid(主播ID)
http://localhost:8082/srs-live-room?liveroomid=localStream-1673368291508&tid=999&userId=1010&roomId=111
  1. 点击页面右侧“申请连麦”,观察推流模块画面以及当前页面控制台。

本节相关源码

相关源码地址

课后题

在申请连麦那里我做了简化,直接自动同意连麦人员画面,在实际过程中肯定是不行的,请大家优化这个步骤,比如实现主播同意、拒绝的弹窗提醒等。