不使用 DataChannel 的 WebRTC H.265

背景:WebRTC 用于从我的 Raspberry Pi 摄像头流式传输视频和音频。为了节省网络带宽并满足我的个人兴趣,我想让它支持 H.265。多亏了 Chrome 浏览器,它现在原生支持 H.265 硬件解码器。如果我能通过 WebRTC 传输 H.265 视频流,一切似乎都可行了。

WebRTC 上的 H.265

WebRTC 本身不支持 H.265,互联网上几乎所有的解决方案都是通过数据通道传递多媒体数据。但是,我有一些理由拒绝接受这种解决方案。

  • 我希望在客户端对所有代码使用相同的接收流程。
  • RTP 和 RTCP 有一些 QoS 机制。

第一次尝试: 在 SPD 上支持 H.265 (失败…)

我的第一个问题是:

RTP 支持 H.265,Chrome 浏览器可以播放 H.265。为什么不让 RTP 发送 H.265?

回到 WebRTC 握手阶段,客户端将收集其能力(offer)并将其发送给 WebRTC 服务器,以协商最终的过渡编解码器。offer 中包含 SDP,用于描述所有支持的编解码器,如下所示。

m=audo/m=video 是分隔 SDP 的行,行尾的数字称为 RTP 负载类型。H.265 没有预定义的有效负载类型,H.264 和 VP9 有传统的有效负载类型。

v=0
o=- 266327... 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0 1 2
a=extmap-allow-mixed
a=msid-semantic: WMS
...
m=audio 9 UDP/TLS/RTP/SAVPF 111 63 9 0 8 13 110 126
...
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:9 G722/8000
a=rtpmap:0 PCMU/8000
...
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 35 36 37 38 102 103 104 105 106 107 108 109 127 125 39 40 41 42 43 44 45 46 47 48 112 113 114 115 116 117 118 49
...
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
...
m=application 9 UDP/DTLS/SCTP webrtc-datachannel
...

我的想法是,在客户端 SDP 中手动添加 H.265 有效载荷类型(我任意选择了 110 号),然后只在 Pion 中使用 RegisterCodec 函数添加 H.265 支持,以强制 WebRTC 使用 H.265。

代码如下:

// client

// Because a data channel is used,
// and H.265 support should be added in m=video scope
let s = offer.sdp.split("m=application 9 UDP/DTLS/SCTP webrtc-datachannel")

// add payload type 110 at the end of the line
s[0] = s[0].replace("m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 35 36 37 38 102 103 104 105 106 107 108 109 127 125 39 40 41 42 43 44 45 46 47 48 112 113 114 115 116 117 118 49",
"m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 35 36 37 38 102 103 104 105 106 107 108 109 127 125 39 40 41 42 43 44 45 46 47 48 112 113 114 115 116 117 118 49 110")
// add payload type 110
s[0] = s[0] + "a=rtpmap:110 H265/90000\r\na=rtcp-fb:110 goog-remb\r\na=rtcp-fb:110 transport-cc\r\na=rtcp-fb:110 ccm fir\r\na=rtcp-fb:110 nack\r\na=rtcp-fb:110 nack pli\r\n"
// re-join sdp
offer.sdp = s.join("m=application 9 UDP/DTLS/SCTP webrtc-datachannel")
// server

// not support all codecs, only H.265
// m.RegisterDefaultCodecs()

// use 110 as H.265 payload type
 m.RegisterCodec(webrtc.RTPCodecParameters{
  RTPCodecCapability: webrtc.RTPCodecCapability{
   MimeType:  webrtc.MimeTypeH265,
   ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: []webrtc.RTCPFeedback{{
    Type: "goog-remb", Parameter: "",
   }, {
    Type: "ccm", Parameter: "fir",
   }, {
    Type: "nack", Parameter: "",
   }, {
    Type: "nack", Parameter: "pli",
   }},
  },
  PayloadType: 110,
 }, webrtc.RTPCodecTypeVideo)

结果

协商成功,WebRTC 尝试使用 H.265 有效负载类型!

由于 Pion 缺乏 H.265 有效负载,因此会显示一条错误消息 Set local description failed: the requested codec does not have a payloader.虽然 H.265 有效负载器已提交 PR,但它任然是开放的。

在客户端,Chrome 浏览器会显示无法识别 H.265 编解码器的错误信息:Uncaught (in promise) DOMException: Failed to execute 'setLocalDescription' on 'RTCPeerConnection': Failed to set local offer sdp: Failed to set local video description recv parameters for m-section with mid='1'

更改 SDP 并使用自定义的 H.265 有效负载类型是不可行的。

第二次尝试:在 H.264 中打包 H.265

第一次尝试之后,我开始阅读 H.264 payloader的代码

我注意到 Payload 函数中有两个不同的分支。第一个分支处理包含 SPS 和 PPS 的关键帧,第二个分支处理 P 和 B 帧。通过查找起始代码(00 00 00 01 或 00 00 01)并检查其 NALU 类型来区分帧类型。

最后,Single NALU 部分引起了我的注意。

func (p *H264Payloader) Payload(mtu uint16, payload []byte) [][]byte {
  ...
 emitNalus(payload, func(nalu []byte) {
  if len(nalu) == 0 {
   return
  }

  naluType := nalu[0] & naluTypeBitmask
  naluRefIdc := nalu[0] & naluRefIdcBitmask

  switch {
  case naluType == audNALUType || naluType == fillerNALUType:
   return
  case naluType == spsNALUType:
   p.spsNalu = nalu
   return
  case naluType == ppsNALUType:
   p.ppsNalu = nalu
   return
  case p.spsNalu != nil && p.ppsNalu != nil:
   // Pack current NALU with SPS and PPS as STAP-A
    ...
  }

  // Single NALU
  if len(nalu) <= int(mtu) {
   out := make([]byte, len(nalu))
   copy(out, nalu)
   payloads = append(payloads, out)
   return
  }
 }
  ...
 return payloads
}

如果我让一个 H.265 帧假装是单个 NALU H.264 帧,会发生什么情况?

H.265 视频编解码器与 H.264 共享相同的起始码,但 NALU 类型枚举不同。单个 NALU 帧的函数Payload仅检查单个起始代码模式。

因此,将所有 H.265 帧直接发送到 H.264 payloader 并不是正确的方法。包含 VPS、SPS 和 PPS 的关键帧在发送前需要进行一些转换,否则 VSP/SPS/PPS 会因不同的 NALU 类型枚举而被忽略。

应用假 NALU 类型

不使用 DataChannel 的 WebRTC H.265
在关键帧上附加假标头

在上图中,我添加了一个假标头(绿色块),并将所有原始起始代码(蓝色块中的灰色块)更改为 00 00 00 00。任意的 NALU 类型 99 00 是识别帧是否被重新打包的标记。

为了能够恢复帧,我将起始码位置字符串 0_28_28_37_65_13_78_49089 改为[vps start index]_[vps size]_[sps start index]_[sps size]_[pps start index]_[pps size]_[frame start index]_[frame size]。为了读取字符串,我还将字符串大小放在字符串之前的 2 个字节中。

就这样!!

结果

在建立 WebRTC 连接时,我始终使用 H.264,并将 H.265 帧发送到 H.264 payloader。关键帧会附加假标题,而 P 帧则不会有任何变化,因为一帧中只有一个起始代码模式。开销的大小取决于关键帧的间隔时间,通常为 1 到 10 秒或更长。这种开销通常很小。

在下一篇文章中,我将向您展示如何在 Chrome 浏览器上显示 H.265 帧。

作者:Angcar
原文:https://medium.com/@yangcar/h-265-on-webrtc-without-using-datachannel-1-2-a74649eeadae

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论