欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

用 WebRTC 技术进行实时直播的实现方法

最编程 2024-08-07 07:46:28
...

WebRTC 直播整体链路

image.png

直播的采集发送端一般 obs,app和浏览器。obs是c++的桌面端软件,app客户端 这两个很好理解,可以直接调用底层C++能力。不过浏览器怎么做直播呢?这个就需要用到webrtc技术。

上图是阿里云的直播架构,阿里云是支持RTC终端进行直播的。此文章解析阿里云webrtc sdk是怎么进行推流的。

阿里云webrtc推流测试地址

阿里云RTS SDK说明

获取本地音视频流

browserdevicemanager

browserdevicemanager npm包地址

浏览器驱动管理包是用来获取麦克风、摄像头和屏幕共享图像的。它提供了6个方法:

export interface IAudioConstraints {  
  deviceId?: string;
}

export interface IVideoConstraints {
  deviceId ?: string;
  facingMode ?: FacingMode;
  width ?: number;
  height ?: number;
}

export interface IScreenConstraints {
  audio ?: boolean;
  video ?: boolean;
}

export class BrowserDeviceManager {

  // 检查支持屏幕共享
  checkSupportScreenShare (): boolean;

  // 摄像头
  getCameraList (): Promise<Array<MediaDeviceInfo>>;

  // 麦克风
  getMicList (): Promise<Array<MediaDeviceInfo>>;

  // 视频轨道
  getAudioTrack (constraints: IAudioConstraints): Promise<MediaStreamTrack>;

  // 音频轨道
  getVideoTrack (constraints: IVideoConstraints): Promise<MediaStreamTrack>;

  // 共享屏幕轨道
  getScreenTrack (constraints: IScreenConstraints): Promise<MediaStream>;
}

我们可以在浏览器中测试 fjqgx.github.io/devicemanag…

var deviceManager = new BrowserDeviceManager();
deviceManager.getCameraList().then(list => {
    console.log(list)
});

结果如下,输出是一个数组,数组里面包含了驱动设备的对象。 image.png

获取麦克风同样的原理 image.png

获取到驱动后需要用驱动deviceId,传入到getVideoTrack/geAudioTrack获取对应的视频流/音频流。

deviceManager.getVideoTrack({deviceId: ""}).then((videotrack) => {
  console.log(videotrack);
}).catch((err) => {
  console.log("get video track error:", err);
});

videotrack 视频流轨道返回如下: image.png

captureStream

HTMLMediaElement接口的captureStream() 属性返回一个MediaStream对象,该对象正在流式传输媒体元素中呈现的内容的实时捕获。

MediaStream对象,可被其他媒体处理代码用作音频和/或视频数据的源,或用作WebRTC RTCPeerConnection的源。

除了browserdevicemanager获取浏览器自带的驱动设备,我们也可以通过video标签的captureStream方法获取视频/音频流。

<video id="video" controls crossOrigin="anonymous">
  <source src="本地/远程视频地址" type="video/mp4"/>
  <p>This browser does not support the video element.</p>
</video>
const video = document.getElementById('video');
video.oncanplay = handle; // 这个 canplay 事件必须,不然 video.readyState 不会执行
if(video.readyState >= 3) {
   handle();
}

function handle() {
   const stream = video.captureStream();
   console.log(stream);
   
   const videoTracks = stream.getVideoTracks();
   const audioTracks = stream.getAudioTracks();
   const videotrack = videoTracks[0];
   const audiotrack = audioTracks[0];
   console.log(videotrack)
   console.log(audiotrack)
}

此时你会发现captureStream()获取的流跟new MediaStream()流的结构是一致的。也就意味着new MediaStream()的获取音视频轨道的方法都是适用的。 image.png{: width="100px" height="100px"}

videotrack 视频流轨道返回如下:

image.png

audiotrack 音频轨道返回如下:

image.png

视频轨道和音频轨道不同之处在 kind 一个是 video,一个是 audio。

注意: video.captureStream 在移动端不被支持,移动端需要使用 canvas.captureStream

MediaStream

媒体流对象 MediaStream 是浏览器自带的,可在浏览器调试栏中测试。 此对象是一个构造函数,因此使用它需要进行new

> MediaStream
ƒ MediaStream() { [native code] }

> new MediaStream()

image.png

有了媒体流,我们可以向媒体流中加入音视频轨道。此处的mediaStreamTrack要是视频/音频流轨道,也就是 deviceManager.getVideoTrack({deviceId: ""}).then((videotrack)中返回的 videotrack。也可以是captureStream()中返回的videoTracks[0]。对比我们得知能够加入到track的必须是 MediaStreamTrack类型。

const mediastream = new MediaStream();
mediastream.addTrack(mediaStreamTrack);

当然我们也可以查看音视频轨道是否添加成功。

mediastream.getVideoTracks();

webrtc 建立 p2p 连接

webrtc 官方文档 这里关于方法和参数的类型都有定义,当然在使用 typescript 的时候,引入 @types/webrtc npm包即可。

建立 peer-to-peer connections 的关键是方法 RTCPeerConnection简单的webrtc demo,具体的流程图可以参考browser-to-browser 流程图

从浏览器角度看怎么跟阿里云的GRTN网络实现 webrtc 连接 如下:

image.png

1、设置音视频流

const pc = new RTCPeerConnection();
pc.addTrack(audio Track/video Track, mediaStream);

console.log(pc);

通过以上设置,我们可以通过打印 pc 查看peer上的信息。

image.png

2、sdp内容解析

pc.createOffer({offerToReceiveAudio: true, offerToReceiveVideo: true}).then(offer => {
    // 返回的offer如下图 
})

image.png

SDP 的结构分为两个层级 Session Level 和 Media Level,在 RFC8859 中有详细描述。

- Session Level
    - Media Level

WebRTC 的 SDP 内容要求相对宽松一些,只要满足 v o s t m c b a 行即可。其中offer对象信息如上图,sdp 可以通过 split('\r\n') 解析完整sdp如下:

v=0
o=- 3021539194665226118 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0 1
a=extmap-allow-mixed
a=msid-semantic: WMS 5f38745a-03b7-4820-b174-dee158e6ede4
// 这里是音频
m=audio 9 UDP/TLS/RTP/SAVPF 111 63 103 104 9 0 8 106 105 13 110 112 113 126
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:dF0m
a=ice-pwd:vPjQmvWpKy4TQqw3P95AEkvV
a=ice-options:trickle
a=fingerprint:sha-256 94:CD:D2:AF:64:B8:88:BD:D9:00:9F:DF:2C:8D:F8:DA:A5:6A:0F:A3:38:81:09:99:B6:D5:83:52:47:1D:73:46
a=setup:actpass
a=mid:0
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
a=sendrecv
a=msid:5f38745a-03b7-4820-b174-dee158e6ede4 a624a3c4-40fe-4e05-8ede-221725b3fea4
a=rtcp-mux
a=rtpmap:111 opus/48000/2
a=rtcp-fb:111 transport-cc
a=fmtp:111 minptime=10;useinbandfec=1
a=rtpmap:63 red/48000/2
a=fmtp:63 111/111
a=rtpmap:103 ISAC/16000
a=rtpmap:104 ISAC/32000
a=rtpmap:9 G722/8000
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:106 CN/32000
a=rtpmap:105 CN/16000
a=rtpmap:13 CN/8000
a=rtpmap:110 telephone-event/48000
a=rtpmap:112 telephone-event/32000
a=rtpmap:113 telephone-event/16000
a=rtpmap:126 telephone-event/8000
a=ssrc:3210536033 cname:T85HzRS4OeIoT12k
a=ssrc:3210536033 msid:5f38745a-03b7-4820-b174-dee158e6ede4 a624a3c4-40fe-4e05-8ede-221725b3fea4
// 这里是视频
m=video 9 UDP/TLS/RTP/SAVPF 96 97 102 122 127 121 125 107 108 109 124 120 39 40 45 46 98 99 100 101 123 119 114 115 116
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:dF0m
a=ice-pwd:vPjQmvWpKy4TQqw3P95AEkvV
a=ice-options:trickle
a=fingerprint:sha-256 94:CD:D2:AF:64:B8:88:BD:D9:00:9F:DF:2C:8D:F8:DA:A5:6A:0F:A3:38:81:09:99:B6:D5:83:52:47:1D:73:46
a=setup:actpass
a=mid:1
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:13 urn:3gpp:video-orientation
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
a=sendrecv
a=msid:5f38745a-03b7-4820-b174-dee158e6ede4 ba47d3c0-d108-40f8-8318-607b6e2cbf41
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 goog-remb
a=rtcp-fb:96 transport-cc
a=rtcp-fb:96 ccm fir
a=rtcp-fb:96 nack
a=rtcp-fb:96 nack pli
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96
a=rtpmap:102 H264/90000
a=rtcp-fb:102 goog-remb
a=rtcp-fb:102 transport-cc
a=rtcp-fb:102 ccm fir
a=rtcp-fb:102 nack
a=rtcp-fb:102 nack pli
a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
a=rtpmap:122 rtx/90000
a=fmtp:122 apt=102
a=rtpmap:127 H264/90000
a=rtcp-fb:127 goog-remb
a=rtcp-fb:127 transport-cc
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack
a=rtcp-fb:127 nack pli
a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f
a=rtpmap:121 rtx/90000
a=fmtp:121 apt=127
a=rtpmap:125 H264/90000
a=rtcp-fb:125 goog-remb
a=rtcp-fb:125 transport-cc
a=rtcp-fb:125 ccm fir
a=rtcp-fb:125 nack
a=rtcp-fb:125 nack pli
a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtpmap:107 rtx/90000
a=fmtp:107 apt=125
a=rtpmap:108 H264/90000
a=rtcp-fb:108 goog-remb
a=rtcp-fb:108 transport-cc
a=rtcp-fb:108 ccm fir
a=rtcp-fb:108 nack
a=rtcp-fb:108 nack pli
a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
a=rtpmap:109 rtx/90000
a=fmtp:109 apt=108
a=rtpmap:124 H264/90000
a=rtcp-fb:124 goog-remb
a=rtcp-fb:124 transport-cc
a=rtcp-fb:124 ccm fir
a=rtcp-fb:124 nack
a=rtcp-fb:124 nack pli
a=fmtp:124 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f
a=rtpmap:120 rtx/90000
a=fmtp:120 apt=124
a=rtpmap:39 H264/90000
a=rtcp-fb:39 goog-remb
a=rtcp-fb:39 transport-cc
a=rtcp-fb:39 ccm fir
a=rtcp-fb:39 nack
a=rtcp-fb:39 nack pli
a=fmtp:39 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=4d001f
a=rtpmap:40 rtx/90000
a=fmtp:40 apt=39
a=rtpmap:45 AV1/90000
a=rtcp-fb:45 goog-remb
a=rtcp-fb:45 transport-cc
a=rtcp-fb:45 ccm fir
a=rtcp-fb:45 nack
a=rtcp-fb:45 nack pli
a=rtpmap:46 rtx/90000
a=fmtp:46 apt=45
a=rtpmap:98 VP9/90000
a=rtcp-fb:98 goog-remb
a=rtcp-fb:98 transport-cc
a=rtcp-fb:98 ccm fir
a=rtcp-fb:98 nack
a=rtcp-fb:98 nack pli
a=fmtp:98 profile-id=0
a=rtpmap:99 rtx/90000
a=fmtp:99 apt=98
a=rtpmap:100 VP9/90000
a=rtcp-fb:100 goog-remb
a=rtcp-fb:100 transport-cc
a=rtcp-fb:100 ccm fir
a=rtcp-fb:100 nack
a=rtcp-fb:100 nack pli
a=fmtp:100 profile-id=2
a=rtpmap:101 rtx/90000
a=fmtp:101 apt=100
a=rtpmap:123 H264/90000
a=rtcp-fb:123 goog-remb
a=rtcp-fb:123 transport-cc
a=rtcp-fb:123 ccm fir
a=rtcp-fb:123 nack
a=rtcp-fb:123 nack pli
a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f
a=rtpmap:119 rtx/90000
a=fmtp:119 apt=123
a=rtpmap:114 red/90000
a=rtpmap:115 rtx/90000
a=fmtp:115 apt=114
a=rtpmap:116 ulpfec/90000
a=ssrc-group:FID 637903997 3587432557
a=ssrc:637903997 cname:T85HzRS4OeIoT12k
a=ssrc:637903997 msid:5f38745a-03b7-4820-b174-dee158e6ede4 ba47d3c0-d108-40f8-8318-607b6e2cbf41
a=ssrc:3587432557 cname:T85HzRS4OeIoT12k
a=ssrc:3587432557 msid:5f38745a-03b7-4820-b174-dee158e6ede4 ba47d3c0-d108-40f8-8318-607b6e2cbf41
  • a=group 属于会话级别的参数,用于描述将当前会话中的多个媒体绑定在一个连接中,以 mid 作为描述对象。参考 tools.ietf.org/html/draft-…
  • a=rtcp 用于描述 RTCP 的通信地址,在 WebRTC 已经不常用到,因为 WebRTC 通常会使用 rtcp-mux 方式,也就是 RTP 和 RTCP 使用同一个连接地址。同时因为 ICE 在 WebRTC 中是强制性的,所以 a=rtcp 和 c-line 一般都不会被使用。
  • a=ice-ufrag (ICE Username Fragment),描述当前 ICE 连接临时凭证的用户名部分。
  • a=ice-pwd (ICE Password),描述当前 ICE 连接临时凭证的密码部分。
  • a=ice-options 用于描述 ICE 连接的属性信息,ice-options 的定义有很多种,WebRTC 中常见的有:
    • a=ice-options:trickle client 一边收集 candidate 一边发送给对端并开始连通性检查,可以缩短 ICE 建立连接的时间。
    • a=ice-options:renomination 允许 ICE controlling 一方动态重新提名新的 candidate ,默认情况 Offer 一方为controlling 角色,answer 一方为 controlled 角色;同时 Lite 一方只能为 controlled 角色。
  • a=fingerprint DTLS 通信开始前上方都需要校验证书是否被篡改,检验的依据就是协商阶段的证书指纹信息。常见的指纹校验算法有:sha-1/sha-224/sha-256/sha-384/sha-512
  • a=setup 合法值包括 actpass/active/passive 。在 WebRTC 中 DTLS 主要是为了交换 SRTP 的密钥,定义参考RFC5763。一次 DTLS 通信的角色通常需要协商指定,通常发起 Offer 一方都会设置为 actpass,即由对方来定,这时 Answer 回复 active 或者 passive 即完成了角色的协商,当然如果 Offer 一方指定了 active 或者 passive,Answer 一方就只能选择剩下的那个角色了。
  • a=mid 用于标识 Media ID , 参考RFC5888
  • a=extmap 描述了拓展头部 ID 与实际传输的 RTP 头部拓展内容的映射关系。参考 WebRTC RTP Header Extension 分析RFC5285 详细描述了 RTP Header Extension 在 SDP Offer/Answer 中的使用方式,需要注意的是 Offer 和 Answer 的 ID 并不需要匹配,仅代表各端发送时使用的 ID,URI 才是判断兼容能力的依据。
  • a=sendrecv 用于描述当前 m-line 媒体的流动方向。
  • a=msid 用于标识当前 m-line 作用域所属的 MediaStrteam ID,参考RTC8830
  • a=rtcp-mux 在 RFC5761 中定义,用于标示当前会话将 RTP 和 RTCP 绑定在同一连接地址和端口中。
  • a=rtpmap 的 value 对应 RTP 头部的 Payload Type,长度 7 位,也就是取值范围 0-127,96-127 为自定义,通过 rtpmap 字段进行定义并通过跟随其后的 fmtp 字段来定义属性信息。举a=rtpmap:108 H264/90000 定义了 Payload Type 为 108 的 RTP 用来传输 H.264 格式的媒体,媒体采样频率为 90kHz 。
  • a=rtcp-fb 用于描述一个 Codec 支持的 RTCP Feedback 的类型,常见的有:
    • a=rtcp-fb:120 nack 支持 nack 重传,nack (Negative-Acknowledgment)。
    • a=rtcp-fb:120 nack pli 支持 nack 关键帧重传,PLI (Picture Loss Indication)。
    • a=rtcp-fb:120 ccm fir 支持编码层关键帧请求,CCM (Codec Control Message),FIR (Full Intra Request ),通常与 nack pli 有同样的效果,但是 nack pli 是用于重传时的关键帧请求。
    • a=rtcp-fb:120 goog-remb 支持 REMB (Receiver Estimated Maximum Bitrate)。
    • a=rtcp-fb:120 transport-cc 支持 TCC (Transport Congest Control)。
  • a=fmtp 为对应 codec 的参数信息 (Format Parameters),常见的几种 codec 的 fmtp 举例:
    • opus a=fmtp:111 minptime=10;stereo=0;useinbandfec=1这个 fmtp 描述了一个 Payload Type 为 111 的 opus 媒体编码参数。
    • H.264 a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f 这个 fmtp 描述了一个 Payload Type 为 102 的 H.264 媒体编码参数。
    • RTX a=fmtp:120 apt=102 这个 fmtp 描述了重传的参数,APT (Associated Payload Type),将重传 Payload Tpye 与媒体 Payload Type 进行关联,即 Payload Type 为 120 的 RTP 用于重传 Payload Type 为 102 的媒体信息。
  • a=rtcp-rsize 在 RFC5506 中定义,用于标示当前会话支持 reduced-size RTCP packets 。
  • a=ssrc 用于描述 RTP packet 中 SSRC (Synchronization sources) 字段对应的媒体信息,既用于描述当前 media 中存在该 SSRC ,又用于描述该 SSRC 的属性信息,早期的 Chrome 产生的 SDP 中每个 SSRC 通常有 4 行如下。但这种标记方式并不被 Firefox 认可,在 Firefox 生成的 SDP 中一个 a=ssrc 通常只有一行,例如 a=ssrc:3245185839 cname:Cx4i/VTR51etgjT7
    • cname 是必须的
    • label 对应 MediaStreamTrack ID
    • mslabel 对应 MediaStream ID
    • msid 将 MediaStream ID和 MediaStreamTrack ID 组合在一起

WebRTC 中 answer SDP 中 m-line 不能随意增加和删除,顺序不能随意变更,需要和 Offer SDP 中保持一致。

WebRTC 的 SDP 信息中的 a-line 承载了大多数的信息,主要包括 媒体信息 和 连接信息 :

  • 媒体信息 (RTP Parameters)
    • Codec Parameters
      • a=rtpmap
      • a=fmtp
      • a=rtcp-fb
    • Header Extension Parameters
      • a=extmap
    • Encoding Parameters
      • a=ssrc
      • a=ssrc-group
    • RTCP Parameters
      • a=rtcp
      • a=rtcp-mux
      • a=rtcp-rsize
  • 连接信息
    • ICE Candidate
      • a=candidate
      • a=end-of-candidates
    • ICE Parameters
      • a=ice-options
      • a=ice-lite
      • a=ice-ufrag
      • a=ice-pwd
    • DTLS Parameters
      • a=setup
      • a=fingerprint
  • 其他 Miscellaneous
    • a=sendrecv/sendonly/recvonly/inactive
    • a=group
    • a=mid
    • a=rid
    • a=simulcast
    • a=bundle-only
    • a=msid-semantic
    • a=msid

3、解决音频丢包问题

this.sdpLines = new Array<string>(); 

pc.createOffer({offerToReceiveAudio: true, offerToReceiveVideo: true}).then(offer => { 
    // 示例 
    this.sdpLines = String(offer.sdp).split('\r\n'); 
    addNack();
})

// 增加音频nack重传
public addNack(){
    for (let i = 0; i < this.sdpLines.length; ++i) {
      if (this.sdpLines[i].indexOf('opus') != -1) {
        if (this.sdpLines[i+1].indexOf('rtcp-fb') != -1) {
          let str = this.sdpLines[i+1];
          let arr = str.split(' ');
          if (arr.length == 2) {
            if (this.sdpLines[i+2].indexOf('rtcp-fb') != -1) {
              this.sdpLines.splice(i+3, 0, arr[0] + " nack")
            } else {
              this.sdpLines.splice(i+2, 0, arr[0] + " nack")
            }
          }
        }
      }
    }
}

修改前SDP 与 修改后SDP对比:

image.png

4、缓解丢包花屏问题

this.sdpLines = new Array<string>(); 

pc.createOffer({offerToReceiveAudio: true, offerToReceiveVideo: true}).then(offer => { 
    // 示例 
    this.sdpLines = String(offer.sdp).split('\r\n'); 
    addSps();
})

// 缓解丢包花屏问题
public addSps() {
    this.sdpLines = this.sdpLines.map(line => {
      if (line.includes('a=fmtp:') && line.includes('level-asymmetry-allowed')) {
        line += ';sps-pps-idr-in-keyframe=1';
        return line
      }
      return line;
    })
}

修改前SDP 与 修改后SDP对比:

image.png

5、替换sdp中stream

const enum PUBLISH_TAG  {
  STREAMID = "rts",
  AUDIOID = "audio",
  VIDEOID = "video",
  SCREENID = "screen",
  CUSTOMID = "custom",
}

class PublishStreamInfo {
  public stramId: string;
  public audioTrackId: string;
  public videoTrackId: string;
  public screenTrackId: string;
  public customTrackId: string;

  constructor() {
    this.stramId = "";
    this.audioTrackId = "";
    this.videoTrackId = "";
    this.screenTrackId = "";
    this.customTrackId = "";
  }
}

protected publishStreamInfo: PublishStreamInfo;

public modifyTrackName() {
    if (this.localStream) {
      this.publishStreamInfo.stramId = this.localStream.mediaStream.id;
      if (this.localStream.audioTrack) {
        this.publishStreamInfo.audioTrackId = this.localStream.audioTrack.id;
      }
      if (this.localStream.videoTrack) {
        this.publishStreamInfo.videoTrackId = this.localStream.videoTrack.id;
      }

      let sdpAudio: boolean = false;
      let sdpVideo: boolean = false;
      for(var i = 0; i < this.sdpLines.length; ++i) {
        let lineStr: string = this.sdpLines[i];
        let index: number = lineStr.indexOf(this.publishStreamInfo.stramId);
        if (this.publishStreamInfo.stramId && index != -1) {
          this.sdpLines[i] = this.sdpLines[i].replace(this.publishStreamInfo.stramId, PUBLISH_TAG.STREAMID)
          if (BrowserUtil.isFirefox) {
            if (sdpAudio) {
              this.sdpLines[i] = this.sdpLines[i].substring(0, index + 4) + PUBLISH_TAG.AUDIOID;
            } else if (sdpVideo) {
              this.sdpLines[i] = this.sdpLines[i].substring(0, index + 4) + PUBLISH_TAG.VIDEOID;
            }
          }
        }
        if (this.publishStreamInfo.audioTrackId && lineStr.indexOf(this.publishStreamInfo.audioTrackId) != -1) {
          this.sdpLines[i] = this.sdpLines[i].replace(this.publishStreamInfo.audioTrackId, PUBLISH_TAG.AUDIOID)
        } else if (this.publishStreamInfo.videoTrackId && lineStr.indexOf(this.publishStreamInfo.videoTrackId) != -1) {
          this.sdpLines[i] = this.sdpLines[i].replace(this.publishStreamInfo.videoTrackId, PUBLISH_TAG.VIDEOID)
        }
        if (lineStr.indexOf("m=audio") != -1) {
          sdpAudio = true;
          sdpVideo = false;
        } else if (lineStr.indexOf("m=video") != -1) {
          sdpVideo = true;
          sdpAudio = false;
        }
        if (lineStr.indexOf("a=ssrc") != -1) {
          index = lineStr.indexOf("cname:");
          if (index !== -1) {
            if (sdpAudio) {
              this.sdpLines[i] = lineStr.substr(0, index+6) + PUBLISH_TAG.AUDIOID;
            } else if (sdpVideo) {
              this.sdpLines[i] = lineStr.substr(0, index+6) + PUBLISH_TAG.VIDEOID;
            }
          }
        }
      }
    }
  }

rts是阿里云webrtc推流sdp的约定。

image.png

6 、http执行交换信令

阿里云 rts-sdk 发送格

推荐阅读