WebRTC 直播整体链路
直播的采集发送端一般 obs,app和浏览器。obs是c++的桌面端软件,app客户端 这两个很好理解,可以直接调用底层C++能力。不过浏览器怎么做直播呢?这个就需要用到webrtc技术。
上图是阿里云的直播架构,阿里云是支持RTC终端进行直播的。此文章解析阿里云webrtc sdk是怎么进行推流的。
获取本地音视频流
browserdevicemanager
浏览器驱动管理包是用来获取麦克风、摄像头和屏幕共享图像的。它提供了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)
});
结果如下,输出是一个数组,数组里面包含了驱动设备的对象。
获取麦克风同样的原理
获取到驱动后需要用驱动deviceId,传入到getVideoTrack/geAudioTrack获取对应的视频流/音频流。
deviceManager.getVideoTrack({deviceId: ""}).then((videotrack) => {
console.log(videotrack);
}).catch((err) => {
console.log("get video track error:", err);
});
videotrack 视频流轨道返回如下:
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()的获取音视频轨道的方法都是适用的。
{: width="100px" height="100px"}
videotrack 视频流轨道返回如下:
audiotrack 音频轨道返回如下:
视频轨道和音频轨道不同之处在 kind 一个是 video,一个是 audio。
注意: video.captureStream 在移动端不被支持,移动端需要使用 canvas.captureStream
MediaStream
媒体流对象 MediaStream 是浏览器自带的,可在浏览器调试栏中测试。 此对象是一个构造函数,因此使用它需要进行new
> MediaStream
ƒ MediaStream() { [native code] }
> new MediaStream()
有了媒体流,我们可以向媒体流中加入音视频轨道。此处的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 连接 如下:
1、设置音视频流
const pc = new RTCPeerConnection();
pc.addTrack(audio Track/video Track, mediaStream);
console.log(pc);
通过以上设置,我们可以通过打印 pc 查看peer上的信息。
2、sdp内容解析
pc.createOffer({offerToReceiveAudio: true, offerToReceiveVideo: true}).then(offer => {
// 返回的offer如下图
})
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:trickleclient 一边收集 candidate 一边发送给对端并开始连通性检查,可以缩短 ICE 建立连接的时间。a=ice-options:renomination允许 ICE controlling 一方动态重新提名新的 candidate ,默认情况 Offer 一方为controlling 角色,answer 一方为 controlled 角色;同时 Lite 一方只能为 controlled 角色。
a=fingerprintDTLS 通信开始前上方都需要校验证书是否被篡改,检验的依据就是协商阶段的证书指纹信息。常见的指纹校验算法有: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 的媒体信息。
- opus
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/VTR51etgjT7cname是必须的label对应MediaStreamTrack IDmslabel对应MediaStream IDmsid将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
- Codec Parameters
- 连接信息
- 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
- ICE Candidate
- 其他 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对比:
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对比:
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的约定。
6 、http执行交换信令
阿里云 rts-sdk 发送格式:
阿里云 rts-sdk 接收格式:
项目难点
推流播放兼容问题
opus音频无法播放问题
当然完成了P2P连接以后就可以进行推流了,不过在播放端用 video 播放会出现没有音频的情况。因为一般播放侧支持 flv/hls 进行直播,它的音频格式并不支持opus。因此需要在云端进行转码变成 flv 进行播放。
iOS15.1 推流导致页面刷新
this.sdpLines = new Array<string>();
pc.createOffer({offerToReceiveAudio: true, offerToReceiveVideo: true}).then(offer => {
// 示例
this.sdpLines = String(offer.sdp).split('\r\n');
})
// iOS15.1 推流会导致页面刷新,Webkit bug:https://bugs.webkit.org/show_bug.cgi?id=231505 (H264 encoder 的问题)。
// workaround: answer sdp 增加 extmap,内容为 urn:3gpp:video-orientation`)。
// 推流成功后,disableVideo 仍然会导致页面刷新,目前没有解法:https://bugs.webkit.org/show_bug.cgi?id=232006
// 目前这段代码仍然无法解决问题,以下只是示例,实际应用中并不能解决问题
public ios15PushHack() {
let hasVideoField = false;
let attrFieldIndex = -1;
this.sdpLines.forEach((line, index) => {
// 查找 video 媒体层
if (line.includes('m=video')) {
hasVideoField = true;
}
if (hasVideoField && attrFieldIndex === -1) {
// 在第一个 attr field 前插入
if (line.includes('a=')) {
attrFieldIndex = index;
}
}
})
if (attrFieldIndex !== -1) {
// 新增唯一的 extmap identifier
const extmapList = this.getExtmapIds();
const newExtmapId = Math.max(...extmapList) + 1;
this.sdpLines.splice(attrFieldIndex, 0, `a=extmap:${newExtmapId} urn:3gpp:video-orientation`);
}
}
private getExtmapIds () {
const extmapList = this.sdpLines.reduce((acc: number[], line) => {
if (line.includes('a=extmap')) {
const id = Number(line.replace('a=extmap:', '').split(' ')[0]);
return [ ...acc, id ]
}
return acc;
}, [])
return extmapList.filter(v => v);
}
音视频流畅度性能问题
之前Linky进行NACK优化:包括RecycleFramesUntilKeyFrame函数、NACK其他参数优化等,但是出现一个问题:建立通话后有7~15s左右时间黑屏现象。
防止谷歌CPU过度使用问题
const pc = new RTCPeerConnection({
googCpuOveruseDetection: false,
})
马赛克
黑屏
卡顿
参考
- LATM 参考:Application-Bulletin_AAC-Transport-Format
- 音频编码参考:csclub.uwaterloo.ca/~ehashman/I…
- AAC 协议族:www.indexcom.com/wp-content/…
- SDP 协商:www.rfc-editor.org/rfc/rfc4566