news 2026/10/3 2:31:52

Pion WebRTC 的 ortc-media 示例:用 ORTC API 构建免 SDP 的媒体传输与自定义信令

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Pion WebRTC 的 ortc-media 示例:用 ORTC API 构建免 SDP 的媒体传输与自定义信令
  • WebRTC
  • 音视频
  • 即时通讯
  • 通信

【免费下载链接】webrtc

Pure Go implementation of the WebRTC API

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

导读

本文围绕 Pion WebRTC 仓库中的 examples/ortc-media 示例 展开,讲解如何绕过传统的 Session Description Protocol(SDP),直接使用 ORTC 风格的底层传输 API(ICE、DTLS、RTP)来搭建一个"文件到文件"的视频推流通道:一端从 IVF 文件读取 VP8/VP9/AV1 视频流,另一端接收并打印收到的 RTP 包。读完本文,你将掌握 ORTC 与 WebRTC 的关系、基于 JSON+Base64 的自定义信令协议设计,以及如何把 IVF 容器与 RTP/RTCP 管线衔接起来,并能完整复现一个可运行的媒体示例。

ORTC 是什么:为什么要绕开 SDP

ORTC(Object Real-Time Communications,见 ortc.org)的核心思想是:不再用 SDP 文本来描述媒体会话,而是直接暴露一组可编程的对象与 API。在examples/ortc-media/README.md中对此有明确表述:

Instead of using the Session Description Protocol to configure and communicate, ORTC provides APIs. Users then can implement signaling with whatever protocol they wish. ORTC can then be used to implement WebRTC. A ORTC implementation can parse/emit Session Description and act as a WebRTC implementation.

翻译过来就是三层含义:

  1. 信令协议完全自由:SDP 只是 WebRTC 生态中一种约定俗成的会话描述格式,ORTC 场景下你可以用 JSON、Protobuf、纯文本甚至自定义二进制协议来交换会话参数。
  2. ORTC 可以反过来实现 WebRTC:一个 ORTC 实现既可以解析/生成 Session Description,也可以扮演完整的 WebRTC 实现——这说明 ORTC 与 WebRTC 并非对立,而是更底层的"积木"。
  3. 媒体能力由对象而非文本承载:编解码器协商、ICE 参数、DTLS 指纹、RTP 发送/接收参数都通过类型化的 Go 结构体传递,编译期即可发现类型错误,这是相对 SDP 字符串解析的一大优势。

在 examples/ortc/README.md 中,同一个 ORTC 思路被用于 DataChannel 场景;而本示例ortc-media则聚焦在媒体传输(RTP)上,两者合起来覆盖了 ORTC 的"数据"与"媒体"两条主线。

示例概览:两个进程 + 一个自定义 JSON 信令协议

ortc-media示例由两个 Go 进程组成,分别扮演 offerer 与 answerer:

角色启动方式职责
offerer(发送方)ortc-media -offer打开output.ivf,创建本地媒体轨(Track),启动 RTP Sender,并开启 HTTP 服务等待接收对方的信令
answerer(接收方)echo <base64> \| ortc-media通过 stdin 接收对方信令,创建 RTP Receiver,接收媒体并打印首个 RTP 包的 SSRC

两端之间交换的会话参数被定义为一个名为Signal的 Go 结构体,见 examples/ortc-media/main.go:

// Signal is used to exchange signaling info. // This is not part of the ORTC spec. You are free // to exchange this information any way you want. type Signal struct { ICECandidates []webrtc.ICECandidate `json:"iceCandidates"` ICEParameters webrtc.ICEParameters `json:"iceParameters"` DTLSParameters webrtc.DTLSParameters `json:"dtlsParameters"` RTPSendParameters webrtc.RTPSendParameters `json:"rtpSendParameters"` }

注意代码注释特别强调:这个结构体并不是 ORTC 规范的一部分,你可以用任何方式交换这些信息。这正是 ORTC 设计哲学的体现——规范只负责定义"会话参数对象",不负责定义"如何传输它们"。

该结构体随后通过json.Marshal再叠加base64.StdEncoding编码成一个字符串(见 main.go 的 encode/decode 函数),方便在命令行、stdin、HTTP body 之间无差别搬运。

逐步实操:从零跑通媒体传输

以下步骤完整继承自 examples/ortc-media/README.md,并补充了源码层面的解释。

第 1 步:用 ffmpeg 生成 IVF 测试文件

示例读取的是名为output.ivf的 IVF(Interchange Video File Format)文件,且必须包含VP8/VP9/AV1其中一种编码的视频轨。生成命令如下:

ffmpeg -i $INPUT_FILE -g 30 -b:v 2M output.ivf

参数含义:

  • -i $INPUT_FILE:任意输入视频文件;
  • -g 30:GOP(关键帧间隔)为 30 帧;
  • -b:v 2M:视频目标码率 2 兆比特每秒。

README 特别提示:-b:v 2M只是一个"够用就好"的默认值,用来换取不错的画质。如果遇到丢帧等问题,可以调低这个值。更完整的取值语法请查阅 ffmpeg 官方文档的 Options 一节。

值得强调的是,示例并不限定编码格式——源码 fourCCToTrack 函数 会根据 IVF 文件头部的 FourCC 字段自动选择对应的 MIME 类型:

switch fourCC { case "AV01": trackCodec = webrtc.MimeTypeAV1 case "VP90": trackCodec = webrtc.MimeTypeVP9 case "VP80": trackCodec = webrtc.MimeTypeVP8 default: panic(fmt.Sprintf("Unable to handle FourCC %s", fourCC)) }

也就是说,你生成的output.ivf是 VP8、VP9 还是 AV1,程序都能自动适配,无需修改代码。

第 2 步:安装 ortc-media 命令

go install github.com/pion/webrtc/v4/examples/ortc-media@latest

这会利用 Go 的模块工具链把示例编译成可执行文件安装到$GOBIN。当前仓库的模块声明为github.com/pion/webrtc/v4(见 go.mod),因此@latest会拉取最新发布版本。

第 3 步:启动 offerer,拿到第一段 Base64 信令

ortc-media -offer

程序会打印一段 Base64 字符串,把它复制到剪贴板。这段字符串内部就是前面Signal结构体的 JSON 编码:本地 ICE 候选、ICE 参数、DTLS 参数以及 RTP 发送参数。

结合源码看 offerer 端的启动流程(main.go):

  1. 用os.Open打开output.ivf;
  2. 用ivfreader.NewWith解析 IVF 文件头;
  3. 根据header.FourCC创建对应的TrackLocalStaticSample本地媒体轨;
  4. 通过api.NewRTPSender(trackLocal, dtls)创建 RTP Sender;
  5. 调用rtpSender.Send(rtpSendParameters)开始发送;
  6. 启动 goroutinewriteFileToTrack把文件帧写入轨道。

第 4 步:启动 answerer,回传第二段 Base64 信令

把刚才复制的消息通过 stdin 传给第二个进程:

echo BASE64_MESSAGE_YOU_COPIED | ortc-media

answerer 会打印另一段 Base64 字符串,同样复制它。answerer 端的流程对应 main.go:

if rtpReceiver, err = api.NewRTPReceiver(webrtc.RTPCodecTypeVideo, dtls); err != nil { panic(err) }

它只创建了一个面向视频的RTPReceiver,不打开任何文件——因为它只负责"收"。

第 5 步:用 curl 把第二段信令送回 offerer

curl localhost:8080 -d "BASE64_MESSAGE_YOU_COPIED"

localhost:8080正是 offerer 内部启动的 HTTP 服务器,对应源码中的httpSDPServer(*port)(main.go)。这个 HTTP 服务非常简单:把请求 body 原样塞进一个 channel,然后由主流程取出来解码。

默认监听端口是 8080,可通过命令行参数覆盖:ortc-media -offer -port 9090(对应 main.go 的 flag 定义)。

第 6 步:观察结果

接收方(answerer)在拿到首个 RTP 包后会打印类似这样的信息:

Got RTP Packet with SSRC 3097857772

每次运行的 SSRC 都不同,因为 SSRC 是会话建立时随机生成的。媒体包会持续流动,直到文件所有帧都被读取完毕,发送方打印All video frames parsed and sent后进程退出(见 writeFileToTrack)。

源码深挖:一次完整的 ORTC 媒体会话是如何搭起来的

1. 自底向上的传输对象组装

与使用PeerConnection的"一站式"用法不同,ORTC 风格要求你手动组装传输栈。main.go中这段代码完整呈现了从 ICE 到 RTP 的每一层:

// Prepare ICE gathering options iceOptions := webrtc.ICEGatherOptions{ ICEServers: []webrtc.ICEServer{ {URLs: []string{"stun:stun.l.google.com:19302"}}, }, } // Use default Codecs mediaEngine := &webrtc.MediaEngine{} if err := mediaEngine.RegisterDefaultCodecs(); err != nil { panic(err) } // Create an API object api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine)) // Create the ICE gatherer gatherer, err := api.NewICEGatherer(iceOptions) // Construct the ICE transport ice := api.NewICETransport(gatherer) // Construct the DTLS transport dtls, err := api.NewDTLSTransport(ice, nil)

逐层说明:

  • ICEGatherer(icegatherer.go 的api.NewICEGatherer):负责收集本地 ICE 候选。示例使用了一个公开的 Google STUN 服务器stun:stun.l.google.com:19302来获取公网候选地址。注意这里通过WithMediaEngine定制了 API,默认NewAPI()也会自动注册默认编解码器(见 api.go 的NewAPI实现),此处显式传入是为了强调 MediaEngine 在 ORTC 中的可配置地位。
  • ICETransport:承载候选对(candidate pair)的连通性检查,负责 NAT 穿透。
  • DTLSTransport:在 ICE 之上建立 DTLS 加密通道,第二个参数nil表示使用自动生成的证书(可通过传入[]Certificate自定义)。

2. 媒体方向的分叉:Sender 与 Receiver

组装完传输层后,程序按角色分叉(main.go):

  • offerer创建RTPSender:把本地TrackLocalStaticSample绑定到 DTLS 通道上发送;
  • answerer创建RTPReceiver:用RTPCodecTypeVideo指定接收视频流。

之后双方都要执行候选收集与参数导出(main.go):

gatherFinished := make(chan struct{}) gatherer.OnLocalCandidate(func(candidate *webrtc.ICECandidate) { if candidate == nil { close(gatherFinished) } }) if err = gatherer.Gather(); err != nil { ... } <-gatherFinished iceCandidates, err := gatherer.GetLocalCandidates() iceParams, err := gatherer.GetLocalParameters() dtlsParams, err := dtls.GetLocalParameters()

这里OnLocalCandidate回调中candidate == nil表示收集结束(gathering complete),随后统一取出本地候选、ICE 参数与 DTLS 参数,打包进Signal发送给对方。

3. 角色与 ICE 控制方协商

值得注意的一个细节是 ICE 角色的选择(main.go):

iceRole := webrtc.ICERoleControlled if *isOffer { iceRole = webrtc.ICERoleControlling }

默认先假设自己是Controlled方,如果是 offerer 再切换为Controlling。这是因为在 ICE 标准中必须一方 controlling、一方 controlled,而本示例用-offer标志来消解这个歧义。

4. 连接启动:ICE → DTLS → RTP

信息交换完成后,双方向启动连接(main.go):

if err = ice.SetRemoteCandidates(remoteSignal.ICECandidates); err != nil { ... } if err = ice.Start(nil, remoteSignal.ICEParameters, &iceRole); err != nil { ... } if err = dtls.Start(remoteSignal.DTLSParameters); err != nil { ... }

answerer 还会用远程的 RTP 发送参数配置自己的接收端,并从rtpReceiver.Track()取出TrackRemote读取 RTP 包:

if err = rtpReceiver.Receive(webrtc.RTPReceiveParameters{ Encodings: []webrtc.RTPDecodingParameters{ { RTPCodingParameters: remoteSignal.RTPSendParameters.Encodings[0].RTPCodingParameters, }, }, }); err != nil { ... } remoteTrack := rtpReceiver.Track() pkt, _, err := remoteTrack.ReadRTP() fmt.Printf("Got RTP Packet with SSRC %d \n", pkt.SSRC)

RTPReceiver.Receive是接收侧的关键入口(见 rtpreceiver.go),它先configureReceive再startReceive,把TrackRemote挂到接收管线里;Track()方法在只有一个轨道时直接返回该轨道(rtpreceiver.go)。

5. IVF 文件是怎么变成 RTP 包的

发送端 goroutinewriteFileToTrack(main.go)承担了"文件 → 媒体采样"的搬运工作:

ticker := time.NewTicker( time.Millisecond * time.Duration((float32(header.TimebaseNumerator)/float32(header.TimebaseDenominator))*1000), ) for ; true; <-ticker.C { frame, _, err := ivf.ParseNextFrame() if errors.Is(err, io.EOF) { fmt.Printf("All video frames parsed and sent") os.Exit(0) } if err = track.WriteSample(media.Sample{Data: frame, Duration: time.Second}); err != nil { ... } }

它按照 IVF 头部声明的时间基准(timebase)构造一个time.Ticker,以接近实时播放的节奏逐帧读取并写入媒体轨,模拟真实推流行为。

底层的解析逻辑位于 pkg/media/ivfreader/ivfreader.go:

  • 文件头:32 字节,以DKIF魔数开头(ivfFileHeaderSignature = "DKIF"),包含 FourCC、宽高、timebase 分子/分母、帧数等信息(对应IVFFileHeader结构体,ivfreader.go);
  • 帧头:每帧 12 字节,含FrameSize与时间戳(IVFFrameHeader);
  • NewWith解析文件头并校验签名、版本与 timebase 合法性(ivfreader.go);
  • ParseNextFrame逐帧读取负载,读到文件末尾返回io.EOF并给出空结果(ivfreader.go)。

而TrackLocalStaticSample.WriteSample会把media.Sample交给内部 Payloader 打包成 RTP 包再经 SRTP 加密发送,这正是TrackLocalStaticSample类(见 track_local_static.go 的注释:If you wish to send a media.Sample use TrackLocalStaticSample)与TrackLocalStaticRTP的区别所在——前者面向媒体采样,后者直接接受现成的 RTP 包。

信令交换链路全景

把整个流程串起来,两端的信令交互是这样的:

offerer: Gather → 导出 Signal#1(ICE/DTLS/RTP参数) → 打印 Base64#1 answerer: 读取 Base64#1 → Gather → 导出 Signal#2 → 打印 Base64#2 offerer: curl POST Base64#2 → 解码 → SetRemoteCandidates → ICE.Start → DTLS.Start answerer: SetRemoteCandidates → ICE.Start → DTLS.Start → RTPReceiver.Receive → ReadRTP

两个方向分别走不同的信令通道:

  • offerer → answerer:通过终端复制粘贴(stdin);
  • answerer → offerer:通过 offerer 上运行的 HTTP 服务(默认 8080 端口)。

这种"两头不对称"的设计恰好演示了 ORTC 的优势:信令通道完全由你定义。你可以把两段 Base64 换成 WebSocket、消息队列、甚至纸条,都不影响媒体面的正常工作。

与 DataChannel 版 ortc 示例的对照

仓库中还有 examples/ortc 示例,两者共享同一套 ORTC 组装思路(NewICEGatherer → NewICETransport → NewDTLSTransport),区别在于:

  • ortc(数据面):在 DTLS 之上继续叠加NewSCTPTransport,通过api.NewDataChannel创建 DataChannel,信令载荷为SCTPCapabilities,媒体内容为随机生成的文本消息;
  • ortc-media(媒体面):在 DTLS 之上直接创建RTPSender/RTPReceiver,信令载荷为RTPSendParameters,媒体内容为 IVF 文件中的视频帧。

对照两个 main.go 可以看到,ORTC 把"传输栈"和"会话参数"拆成了清晰的层次,无论是跑数据通道还是媒体通道,ICE/DTLS 部分的代码几乎可以原样复用——这正是 ORTC 面向工程复用的价值所在。

小结与下一步

ortc-media示例证明了:在 Pion WebRTC 中,你可以完全不依赖 SDP,仅凭类型化的 ORTC 对象(ICEGatherer、ICETransport、DTLSTransport、RTPSender/RTPReceiver)就搭建出一条完整的视频传输链路,并且信令协议可以按需自定义。对希望把 WebRTC 能力嵌入自有系统、需要精细控制传输栈或希望规避 SDP 解析复杂性的开发者来说,这是一个理想的起点。

如果你想进一步探索:

  • 阅读 examples/ortc,看同样的 ORTC 栈如何承载 DataChannel;
  • 阅读 pkg/media/ivfreader/ivfreader.go 与 pkg/media/ivfwriter,了解 IVF 容器读写的完整实现;
  • 参考 examples/ortc-media/main.go 的完整源码,尝试把信令通道替换为 WebSocket 或 MQTT,体会"信令与媒体解耦"的设计自由。
  • WebRTC
  • 音视频
  • 即时通讯
  • 通信

【免费下载链接】webrtc

Pure Go implementation of the WebRTC API

项目地址:https://gitcode.com/gh_mirrors/we/webrtc
点击查看免费下载
上一篇:DBX 开源指南:25 MB 轻量级跨平台数据库客户端,内置 AI 助手与 MCP Server 全解析
下一篇:chezmoi 实战:在 `chezmoi init` 阶段用 `hooks.read-source-state.pre` 自动安装密码管理器

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

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

Go 内存对齐与数据结构性能:字段顺序影响 50% 内存占用

Go 内存对齐与数据结构性能&#xff1a;字段顺序影响 50% 内存占用struct 字段顺序变了&#xff0c;内存占用也变了。写 Go 服务必须懂内存对齐规则&#xff0c;能让数据结构更紧凑。一、对齐基础 CPU 读内存一般按 8 字节对齐。如果 struct 内部字段顺序错乱&#xff0c;编译时…

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

蓝牙芯片驱动开发-第6章第5题-如何确保ACL数据包的正确识别与处理

蓝牙面试题解析:如何确保 ACL 数据包的正确识别与处理? 难度:⭐⭐⭐⭐ 较难 | 场景:社招二面/三面、蓝牙驱动开发 | 高频:🔥🔥🔥🔥 标准答案 ACL 数据包的识别与处理通过 类型标识 + 句柄匹配 + 完整性校验 + 缓冲管理 四层保障: ① 包类型识别(H4 传输层) …

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

京东云老用户主机续费省钱攻略:优惠活动全拆解

云主机续费这件事&#xff0c;一聊起来基本全是眼泪。新用户首年三折五折拿主机&#xff0c;看着挺香&#xff0c;可一到第二年续费&#xff0c;价格“duang”一下回到原价&#xff0c;账单直接翻倍甚至翻两倍。很多人就是被这一刀“劝退”的&#xff0c;干脆换一家重新“上车”…

作者头像 李华