news 2026/10/3 2:13:36

使用 Pion WebRTC 实现 WHIP/WHEP 直播推拉流服务:从浏览器与 OBS 发布到亚秒级观看

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用 Pion WebRTC 实现 WHIP/WHEP 直播推拉流服务:从浏览器与 OBS 发布到亚秒级观看
  • WebRTC
  • 音视频
  • 即时通讯
  • 通信

【免费下载链接】webrtc

Pure Go implementation of the WebRTC API

项目地址:https://gitcode.com/gh_mirrors/we/webrtc
点击查看免费下载

WHIP(WebRTC-HTTP Ingestion Protocol)与 WHEP(WebRTC-HTTP Egress Protocol)是建立在 HTTP 之上的标准化 WebRTC 信令协议,它们将"Offer 通过 HTTP 上传、服务器返回 Answer"这一强 API 契约固定下来,从而让 OBS、GStreamer 等专业推流工具可以直接对接 WebRTC 媒体服务器。本文以 Pion WebRTC 仓库中的 whip-whep 示例 为主体,完整讲解该示例的部署运行、浏览器/OBS 双端推拉流操作,并逐段剖析 main.go 的服务端实现与 index.html 的浏览器端实现,帮助读者掌握用纯 Go 构建一套"推流—中转—订阅"WebRTC 直播链路的完整技术方案。

WHIP/WHEP 解决什么问题

传统 WebRTC 应用的信令(SDP 协商)通常由双方自行约定,而 WHIP/WHEP 把信令固化为两个 HTTP 端点:

  • WHIP(推流/上行):发布端(OBS、GStreamer 或浏览器)通过POST把 SDP Offer 上传到/whip,服务器返回携带完整 ICE Candidate 的 SDP Answer;
  • WHEP(拉流/下行):订阅端通过POST把 SDP Offer 上传到/whep,服务器在 Answer 中携带媒体轨道信息,订阅端即可消费同一份媒体流。

正是这种强契约让 OBS、GStreamer 这类原本不原生支持 WebRTC 的工具,无需了解信令细节即可完成 WebRTC 推流。在 whip-whep 示例 中,WHIP 会话接收的 RTP 包会被转发到 WHEP 会话,实现"一端推流、多端亚秒级观看",甚至可以把 WHEP 流再拉回 OBS 做二次处理。

快速运行示例

该示例因为需要对外提供静态 HTML 页面(index.html),所以必须克隆仓库后从示例目录运行:

git clone https://github.com/pion/webrtc.git cd webrtc/examples/whip-whep go run *.go

启动成功后终端会打印Open http://localhost:8080 to access this demo,监听端口为8080(见 main.go 中的http.ListenAndServe(":8080", nil))。

通过浏览器发布

打开 http://localhost:8080,点击页面上的Publish按钮。浏览器会调用getUserMedia采集摄像头与麦克风,生成 Offer 后以application/sdp的 Content-TypePOST到/whip,拿到 Answer 即完成协商(见 index.html 的doWHIP函数)。

通过 OBS 发布

在 OBS 中配置推流:

配置项取值
Service(服务)WHIP
Server(服务器)http://localhost:8080/whip
Bearer Token任意值(示例未校验)

OBS 会把编码后的音视频以 WHIP 协议推送到该端点。由于 WHIP/WHEP 是标准化信令,GStreamer 等同样支持 WHIP 的推流工具也可按相同方式接入。

订阅观看

保持推流进行中,再次打开 http://localhost:8080 并点击Subscribe。浏览器会添加recvonly音视频 Transceiver,将 OfferPOST到/whep,拿到 Answer 后即可在页面视频区看到刚才推送的画面(见 index.html 的doWHEP函数)。同样,OBS、GStreamer 也可以通过 WHEP 把这条流拉回去。

服务端源码剖析

媒体轨道与 ICE 配置

服务端在main()中先创建两条本地轨道,作为 WHIP→WHEP 的媒体中转载体(main.go):

videoTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{ MimeType: webrtc.MimeTypeH264, }, "video", "pion") audioTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{ MimeType: webrtc.MimeTypeOpus, }, "audio", "pion")

NewTrackLocalStaticRTP 返回一个"已预置编解码、直接接收 RTP 包"的本地轨道:视频为 H264、音频为 Opus,轨道 ID 与流 ID 均为pion。还声明了全局的 PeerConnection 配置,使用 Google 公共 STUN 服务器辅助 NAT 穿透(main.go):

peerConnectionConfiguration = webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ {URLs: []string{"stun:stun.l.google.com:19302"}}, }, }

WHIP 推流端点(whipHandler)

whipHandler 是整个推流链路的入口,处理流程如下:

  1. CORS 预检:响应头允许任意跨域来源、POST方法与Authorization头;OPTIONS请求直接返回(main.go);
  2. 读取 Offer:io.ReadAll(req.Body)取出请求体中的 SDP;
  3. 配置 MediaEngine:注册 H264(PayloadType 96、时钟 90000Hz)与 Opus(PayloadType 97、时钟 48000Hz、双声道)两种编解码(main.go);
  4. 组装拦截器:先注册intervalpli.NewReceiverInterceptor()——该拦截器每 3 秒向推流端发送一次 PLI 请求,促使编码器生成关键帧,使视频"可 seek"且抗丢包,代价是画质与码率开销;随后调用RegisterDefaultInterceptors补齐 NACK、RTCP 收发报告、TWCC 发送端等默认拦截器(main.go)。从 RegisterDefaultInterceptors 的实现 可以看到,默认集合依次配置了 NACK 生成/应答、RTCP 收发报告、Simulcast 头扩展、统计拦截器与 TWCC 发送端;
  5. 创建 API 与 PeerConnection:webrtc.NewAPI(webrtc.WithMediaEngine(...), webrtc.WithInterceptorRegistry(...))以自定义 MediaEngine 与拦截器注册表构造 API 实例,再据此创建 PeerConnection(main.go)。注意:如果手动管理 MediaEngine,就必须为每个 PeerConnection 显式创建 InterceptorRegistry,而不能依赖webrtc.NewPeerConnection的默认行为;
  6. 声明收发能力:AddTransceiverFromKind为视频和音频各添加一个接收 Transceiver,表示允许接收一条视频轨道与一条音频轨道(main.go);
  7. 转发媒体:在OnTrack回调中启动两个 goroutine——一个持续调用receiver.ReadRTCP()消费推流端的 RTCP(读到io.EOF即会话结束);另一个持续调用track.ReadRTP()读取 RTP 包,并剥离 WHIP 扩展头(pkt.Header.Extensions = nil; pkt.Header.Extension = false)后按轨道类型写入videoTrack或audioTrack,从而完成向所有 WHEP 订阅者的广播(main.go);
  8. 回送 Answer:调用writeAnswer通过 HTTP 响应返回携带 ICE Candidate 的 SDP。

WHEP 订阅端点(whepHandler)

whepHandler 与推流端点的差异集中在两点:

  • 拦截器仅使用默认集合,并额外调用ConfigureTWCCHeaderExtensionSender(media, ir)为音视频轨道注册 Transport-CC(sdp.TransportCCURI)头扩展并挂载 TWCC 头扩展拦截器(main.go),目的是在订阅端支持 TWCC(Transport-wide Congestion Control)时提供带宽估计所需的传输级拥塞控制反馈。从源码看,该函数会向 MediaEngine 注册视频、音频两条头扩展并添加twcc.NewHeaderExtensionInterceptor()(interceptor.go);
  • 通过peerConnection.AddTrack(videoTrack/audioTrack)把 WHIP 会话写入的全局轨道绑定到订阅连接,并为音视频各启动一个循环读取rtpSender.Read(rtcpBuf)的 goroutine,持续消费订阅端回传的 RTCP 包——这一步对 NACK 等反馈机制的生效是必需的(main.go)。

统一的 Answer 生成(writeAnswer)

writeAnswer 是 WHIP/WHEP 两端共用的核心逻辑,体现了该协议的关键约束——只能交换一次信令消息,因此必须禁用 Trickle ICE:

peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { if connectionState == webrtc.ICEConnectionStateFailed { _ = peerConnection.Close() } }) peerConnection.SetRemoteDescription(webrtc.SessionDescription{ Type: webrtc.SDPTypeOffer, SDP: string(offer), }) gatherComplete := webrtc.GatheringCompletePromise(peerConnection) answer, err := peerConnection.CreateAnswer(nil) peerConnection.SetLocalDescription(answer) <-gatherComplete // 阻塞直至 ICE 收集完成 res.Header().Add("Location", path) res.WriteHeader(http.StatusCreated) // WHIP/WHEP 要求 201 状态码与 Location 头 fmt.Fprint(res, peerConnection.LocalDescription().SDP)

其中GatheringCompletePromise返回一个"ICE 收集完成即关闭"的 channel(gathering_complete_promise.go),并处理了 promise 创建晚于收集完成导致永久阻塞的边界情况。<-gatherComplete确保 Answer 中已包含全部 ICE Candidate,这是"单次 HTTP 往返完成协商"的前提。注释也明确指出:生产环境中应通过OnICECandidate回调流式交换候选以缩短建连时间。协议层面,WHIP/WHEP 规范要求响应状态码为201 Created并携带Location头,示例分别回填/whip与/whep。

浏览器端:一次 HTTP POST 完成协商

doWHIP 与 doWHEP 展示了浏览器侧的对称实现:先createOffer并setLocalDescription,随后用fetch把offer.sdp以application/sdp类型POST到对应端点,响应体即为 SDP Answer,直接setRemoteDescription({ sdp: answer, type: 'answer' })。两处的差异仅在于:

  • Publish(doWHIP):addTrack把采集到的音视频轨加入连接,方向为发送;
  • Subscribe(doWHEP):addTransceiver('video'/'audio', { direction: 'recvonly' })声明只收不发,并在ontrack中把收到的流挂到<video>元素播放。

页面还通过oniceconnectionstatechange实时把 ICE 连接状态(new/checking/connected 等)追加到页面下方,便于观察协商与建连过程。

生产化与扩展方向

示例代码在核心信令流程之外,还有几个值得在生产中补强的点(均为源码注释与实现明确提及):

  1. Bearer Token 鉴权:index.html发送Authorization: Bearer none,但服务端未校验;真实部署应在whipHandler/whepHandler中解析并验证该头,防止任意客户端占用推流通道;
  2. RTCP 反馈转发:当前方案由intervalpli固定每 3 秒请求一次 PLI;源码注释(main.go)建议真实应用应解析订阅端的 RTCP 包并转发给推流端,实现按需关键帧与基于反馈的自适应码率;
  3. ICE 候选流式交换:GatheringCompletePromise会带来更长的启动等待时间(其文档注释明确说明),生产环境建议改用 Trickle ICE 逐条发送候选;
  4. 媒体消费:OnTrack中当前直接把 RTP 转发给 WHEP 订阅者,若要落盘录制或转码,可在读取 RTP 的分支中加入处理逻辑(仓库中的 save-to-disk 示例展示了如何把 RTP 写入 IVF 文件);
  5. 编码协商扩展:MediaEngine 目前仅注册 H264 + Opus,需要支持 VP8/VP9/AV1 时,可在两处 handler 中参照相同模式追加注册。

至此,你已经用 Pion WebRTC 搭建起一条"OBS/浏览器推流 → Go 服务中转 → 浏览器/工具亚秒级订阅"的完整 WHIP/WHEP 直播链路,可以基于此继续扩展鉴权、录制、多订阅与码率自适应等能力。

  • WebRTC
  • 音视频
  • 即时通讯
  • 通信

【免费下载链接】webrtc

Pure Go implementation of the WebRTC API

项目地址:https://gitcode.com/gh_mirrors/we/webrtc
点击查看免费下载

相关推荐

上一篇:抖音无水印下载终极指南:免费批量采集工具完整使用教程
下一篇:抖音内容采集自动化:douyin-downloader 开源工具的技术解析与应用指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 2:10:22

智能血压计DIY全攻略:51/STM32方案与示波法算法详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 2:07:28

Pixhawk固定翼TECS调参指南:从能量控制原理到实战日志分析

1. 为什么固定翼飞控绕不开TECS&#xff1a;从一次惨痛炸机说起先讲个我早年的真实案例。那时候我刚从多旋翼转固定翼&#xff0c;第一次用Pixhawk飞一套自组的1.4米塞斯纳&#xff0c;当时对TECS完全没有概念&#xff0c;只按多旋翼那套思路调PID。结果起飞后一加油门&#xf…

作者头像 李华