
1. 项目概述为什么“媒体能力协商”是WebRTC稳定通话的隐形地基你有没有遇到过这样的情况两个设备明明都支持高清视频但一打开视频通话画面却自动降成360p帧率卡在15fps连对方眨眼都看不清或者更糟——双方设备列表里明明都有H.264编码器可连接建立后日志里却赫然写着no common codec found直接黑屏失败。这不是网络问题也不是信令故障而是WebRTC最底层、最沉默却最关键的环节出了岔子媒体能力协商Media Capabilities Negotiation。它不像SDP交换那样显眼也不像ICE连通性检查那样有明确日志但它决定了整个会话的“画质天花板”和“功能下限”。我带过的几个团队在做跨终端兼容性优化时80%以上的音视频异常最终都回溯到这个环节——不是协议没跑通而是两边根本没谈拢“能干什么”。这个标题里的“WebRTC源码研究29”说明它是一系列深度源码剖析中的第29篇意味着读者已经具备一定基础知道PeerConnection是什么能看懂SDP结构甚至可能调试过信令流程。但“媒体能力协商”恰恰是很多开发者跳过的盲区——大家习惯把SDP当黑盒copy-paste模板改几行artcp-fb就上线。可一旦遇到某款国产安卓平板死活播不出音频或是某款新发布的MacBook Pro在Safari里无法启用硬件编码这种“模板思维”立刻崩盘。真正的问题藏在RTCRtpTransceiver的setCodecPreferences()调用之后在PeerConnection::CreateOffer()内部触发的MediaSessionDescriptionFactory::CreateMediaDescription()深处在VideoEncoderFactory与AudioEncoderFactory的联合筛选逻辑里。它不暴露在API层却主宰着每一帧数据的命运。所以这篇内容不是教你怎么写一个Hello World的WebRTC应用而是带你钻进Chromium 120版本的WebRTC C源码腹地看清媒体能力从“设备声明”到“会话共识”的完整炼金术。你会明白为什么Chrome默认禁用VP9而Firefox偏爱它为什么iOS Safari至今不支持AV1为什么同一个RTCPeerConnection实例在createOffer()和createAnswer()时协商出的编解码器列表可能完全不同以及最关键的一点——当你想强制启用某个编码器比如让老旧Android设备也用H.264 baseline profile该在源码哪一行打patch又该规避哪些坑。这不仅是技术细节更是构建高可用音视频系统的底层认知框架。2. 媒体能力协商的整体设计与思路拆解2.1 协商不是“选一个”而是“建一张能力交集图”很多人误以为媒体能力协商就是“客户端A说我要用H.264客户端B说我也支持H.264OK成交”。这是对WebRTC设计哲学的根本性误解。真实过程远比这复杂它不是简单的布尔匹配而是一场多维度、分层次、带优先级的能力集合求交集运算。这个“集合”包含至少五个正交维度编解码器类型Codec Typeaudio/video这是最外层过滤。具体编码器Codec NameH264、VP8、VP9、AV1、OPUS、ISAC等每个名称背后对应一套RFC标准。编码参数Codec ParametersH264的profile-level-id如42e01f代表Baseline Profile Level 3.1、VP9的profile0/1/2、OPUS的stereo和sprop-stereo等。这些参数决定了硬件是否能加速、网络能否承受。传输能力Transport Capability是否支持RTX重传、FEC前向纠错、NACK负反馈这些依赖底层传输栈支持。RTP头扩展RTP Header Extension如abs-send-time、transport-wide-cc-01影响拥塞控制精度但需要两端都声明支持才能启用。WebRTC的协商引擎核心在webrtc/sdk/objc/Framework/Classes/PeerConnection/RTCMediaConstraints.m和C层的webrtc/media/engine/webrtc_media_engine.cc会将这些维度全部展开生成一张“能力矩阵”。例如一个现代Chrome浏览器上报的能力可能包含H264:profile-level-id42e01f,level-asymmetry-allowed1,packetization-mode1VP9:profile0,level3.1,frame-rate30AV1:profile0,level3.1,tier0OPUS:stereo1,sprop-stereo1,maxplaybackrate48000而一台老款Android 7设备可能只上报H264:profile-level-id42001f,packetization-mode0OPUS:stereo0协商引擎会逐项比对profile-level-id42e01fvs42001f——前者要求Baseline Level 3.1后者仅支持Level 1.3不匹配packetization-mode1vs0——前者支持分片打包后者只支持单NALU不匹配。最终交集只剩H264降级到42001f和OPUS降级到stereo0。这个过程不是靠运气而是由webrtc/media/base/media_constants.cc中预定义的严格兼容规则驱动。提示profile-level-id的十六进制值不是随意写的。它按ITU-T H.264 Annex A规则编码前6位是Profile ID42Baseline,64Main,6eHigh后18位是Level001fLevel 1.3,e01fLevel 3.1。源码里H264ProfileLevelId::IsSameProfile()函数会做位运算解析而非字符串比较。2.2 为什么必须分“Offer/Answer”两阶段单次协商不行吗这是WebRTC区别于传统SIP的关键设计。很多初学者试图用createOffer()生成一个“全能SDP”再发给对方期望对方无脑接受。这在WebRTC里注定失败因为Offer和Answer承载着完全不同的协商语义Offer发起方表达“我能提供什么”。它基于本地RTCPeerConnection的addTransceiver()配置、setCodecPreferences()显式设置、以及RTCRtpCapabilities的默认能力。注意Offer不承诺“我一定会用”只是亮出底牌。例如你调用pc.addTransceiver(video, {direction: sendrecv})Chrome会把所有已知视频编码器都放进Offer哪怕你实际只打算发一路1080p。Answer应答方表达“我愿意接受并使用什么”。它必须是Offer中能力的子集且要根据自身策略做裁剪。例如Firefox收到含AV1的Offer但其AV1解码器仅支持软件解码CPU占用高它可能主动剔除AV1只保留VP9和H264以保障用户体验。这个裁剪逻辑在webrtc/p2p/base/session_description.cc的JsepSessionDescription::Initialize()中执行。这种分离设计解决了两个核心矛盾异步能力发现设备A在生成Offer时可能还不知道设备B的实时负载如GPU温度过高需降级编码Answer阶段给了B动态决策的空间。策略隔离企业级应用常需服务端策略干预如禁止AV1以节省带宽。服务端可在转发Answer前安全地移除特定编码器而不影响Offer的完整性。我曾在一个跨国会议系统中踩过坑前端工程师为追求画质在Offer里硬编码了afmtp:100 profile-level-id640032High Profile Level 5.0结果导致大量低端手机无法生成Answer因为它们的H264解码器最高只支持Level 3.1。后来我们改为在Answer生成后由信令服务器注入aextmap:1 urn:ietf:params:rtp-hdrext:sdes:mid再由客户端SDK动态降级profile-level-id问题才彻底解决。这印证了WebRTC的设计智慧Offer是能力声明Answer才是能力契约。2.3 源码架构从JS API到底层C的四层穿透要真正理解协商过程必须看清WebRTC的分层架构。它不是单体代码而是典型的“胶水层引擎层平台层”设计层级位置职责关键文件JS Binding层webrtc/sdk/js将C对象映射为JS API处理createOffer()等调用rtc_peerconnection.cc,rtc_rtp_transceiver.ccSession层webrtc/p2p/base/管理SDP生命周期解析/序列化执行Offer/Answer语义校验session_description.cc,jsep_session_description.ccMedia Engine层webrtc/media/engine/核心协商引擎根据本地能力与远端Offer生成Answer的媒体描述webrtc_media_engine.cc,media_session_description_factory.ccPlatform Adapter层webrtc/sdk/android/,webrtc/sdk/ios/适配不同OS的硬件编解码器能力查询如Android MediaCodec, iOS VideoToolboxandroid_video_encoder.cc,video_toolbox_video_encoder.cc整个协商链路是JS调用createOffer()→ JS Binding层转为CPeerConnectionInterface::CreateOffer()→ Session层创建空JsepSessionDescription→ Media Engine层调用MediaSessionDescriptionFactory::CreateMediaDescription()此时它会从VideoEncoderFactory获取本地所有视频编码器能力GetSupportedFormats()从AudioEncoderFactory获取所有音频编码器能力解析远端Offer中的m行提取对方声明的能力对每个m行执行FindMatchingCodec()算法计算交集按codec_preferences或默认优先级排序生成Answer的m行这个过程里FindMatchingCodec()是真正的“大脑”。它不在一个函数里完成而是分散在webrtc/media/engine/webrtc_media_engine.cc的WebRtcMediaEngine::FindMatchingVideoCodec()和WebRtcMediaEngine::FindMatchingAudioCodec()中。它们会遍历所有候选编码器对每个参数做精确匹配或向下兼容判断。例如对H264的level-asymmetry-allowed如果Offer设为1Answer可设为0允许不对称但反之不行。3. 核心细节解析与实操要点3.1 编解码器能力的“三重来源”与优先级冲突一个设备能支持哪些编解码器并非铁板一块而是来自三个动态叠加的来源且存在明确的优先级覆盖关系。忽略这一点是导致协商结果不符合预期的最常见原因。第一重平台原生能力Lowest Priority这是最底层的基础由操作系统和硬件决定。WebRTC在启动时会调用平台适配器扫描可用编解码器Android通过MediaCodecList查询video/avc、video/hevc等MIME类型解析getCapabilitiesForType()返回的CodecCapabilities提取profileLevels数组。iOS调用VTCompressionSessionCreate()和VTDecompressionSessionCreate()尝试创建不同kVTCompressionPropertyKey_ProfileLevel的会话成功即表示支持。Windows枚举DirectX Video Acceleration (DXVA) 或 Windows Media Foundation (WMF) 支持的GUID。这部分能力写死在webrtc/sdk/android/src/jni/jni_generator.cc或webrtc/sdk/ios/src/ios_video_toolbox.mm中生成VideoEncoderFactory::GetSupportedFormats()的初始列表。特点是只读、不可控、但最真实。如果你的Android设备芯片不支持H265这里永远不会有HEVC条目。第二重运行时配置Medium Priority通过RTCPeerConnection构造时的configuration对象或setConfiguration()动态注入const pc new RTCPeerConnection({ encodedInsertableStreams: true, // 启用E2EE bundlePolicy: max-bundle, // 影响m行合并 rtcpMuxPolicy: require // 强制RTCP复用 }); // 或运行时 pc.setConfiguration({ sdpSemantics: unified-plan, // 必须否则不走新协商流程 iceTransportPolicy: relay // 影响candidate类型 });这些配置不直接添加编解码器但会过滤掉不兼容的选项。例如iceTransportPolicy: relay会使协商引擎自动剔除需要hostcandidate的编码器某些旧版VP8实现。第三重显式偏好设置Highest Priority这是开发者可控的最强力手段通过RTCRtpTransceiver.setCodecPreferences()或RTCPeerConnection.createOffer({offerToReceiveVideo: true})的voiceActivityDetection等参数指定const transceiver pc.addTransceiver(video, { direction: sendrecv, streams: [stream] }); // 强制只用H264且必须是Constrained Baseline transceiver.setCodecPreferences([ { mimeType: video/H264, clockRate: 90000, parameters: { profile-level-id: 42c01f, level-asymmetry-allowed: 1 } } ]);这个调用会直接修改Transceiver内部的preferred_codecs_列表覆盖所有其他来源。关键点在于setCodecPreferences()必须在createOffer()之前调用且对每个Transceiver独立生效。我曾见过团队在createOffer()后才调用它结果协商完全不受影响——因为Offer生成时preferred_codecs_还是空的。注意setCodecPreferences()的参数是RTCRtpCodecCapability对象其parameters字段必须是合法的SDP fmtp参数。profile-level-id值必须是6位十六进制字符串且符合H.264 Annex A规范。传入42e01fLevel 3.1给只支持Level 1.3的设备会导致协商失败而非自动降级。3.2 SDP中的“隐性协商”RTP头扩展与FEC的连锁反应媒体能力协商不仅关乎编解码器还深度耦合RTP传输层特性。一个常被忽视的细节是RTP头扩展Header Extension和FEC前向纠错的启用会反向影响编解码器的选择。这是因为它们共享同一套RTP payload type空间且协商逻辑相互依赖。以abs-send-time绝对发送时间为例它在SDP中表示为aextmap:1 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01这个extmap声明告诉对方“我支持transport-wide CC扩展ID为1”。但它的启用有两个前提双方都声明了该extmap如果Offer有aextmap:1 ...Answer里没有此扩展即失效。所选编解码器必须支持该扩展并非所有编码器都兼容transport-wide-cc-01。例如VP8的RFC 7741明确支持但某些定制H264封装可能未实现。更复杂的是FEC。WebRTC支持两种FEC机制ULPFECUDP-Lite FEC通过afmtp:126 apt120;useinbandfec1声明其中apt120指向基础编码器如VP8 payload type 120。FlexFECFlexible FEC通过artpmap:127 flexfec-03/90000独立声明。协商时引擎会检查如果Offer中VP8pt120启用了useinbandfec1那么Answer中若选择VP8就必须保持useinbandfec1否则FEC包无法解码。但如果Answer想选H264而H264的FEC实现不成熟Chrome 115前H264-FEC有严重丢包引擎会主动放弃H264降级回VP8以保障FEC可用性。这个逻辑在webrtc/media/engine/webrtc_media_engine.cc的WebRtcMediaEngine::FindMatchingVideoCodec()中通过codec.has_fec标志位控制。实操中我建议在高丢包网络如4G下显式禁用FEC以换取更多编解码器选择空间// 创建Offer时禁用FEC const offerOptions { offerToReceiveAudio: true, offerToReceiveVideo: true, voiceActivityDetection: false // 关键禁用VAD减少FEC依赖 }; pc.createOffer(offerOptions).then(offer { // 手动移除SDP中的FEC相关行 offer.sdp offer.sdp.replace(/afmtp:\d.*useinbandfec1/g, ); offer.sdp offer.sdp.replace(/artpmap:\d flexfec-\d/g, ); return pc.setLocalDescription(offer); });这招在某海外直播App中救了急他们的CDN节点不支持FlexFEC导致大量Android端黑屏。手动清理FEC后协商自动切到VP8ULPFEC画质虽略降但100%可用。3.3 “动态降级”机制当协商失败时的保底策略WebRTC不会因一次协商失败就放弃。它内置了一套精妙的“动态降级Dynamic Fallback”机制确保在极端情况下仍有基础通信能力。理解这套机制是设计高鲁棒性系统的基石。降级触发条件有三类硬性失败Offer/Answer语法错误如m行缺失c连接地址或artpmap参数格式错误。此时setLocalDescription()直接reject。软性失败能力交集为空如Offer含AV1Answer设备无AV1解码器。此时WebRTC不会报错而是静默降级到下一个可用编解码器。运行时失败连接建立后检测到编码器初始化失败如VideoEncoder::InitEncode()返回WEBRTC_VIDEO_CODEC_ERR_PARAMETER。此时触发OnEncoderError()回调PeerConnection会尝试切换到fallback_encoder。源码中软性降级的核心逻辑在webrtc/media/engine/webrtc_media_engine.cc的WebRtcMediaEngine::FindMatchingVideoCodec()函数末尾// 如果首选编码器不匹配尝试列表中下一个 for (size_t i 0; i supported_formats.size(); i) { const auto format supported_formats[i]; if (MatchCodec(format, remote_codec)) { return format; // 找到第一个匹配的即返回 } } // 全部不匹配返回空触发上层降级 return absl::nullopt;注意它返回的是第一个匹配的而非最优的。这意味着supported_formats的顺序至关重要。Chrome的默认顺序是VP8 H264 VP9 AV1视频OPUS ISAC G722音频。所以即使AV1画质更好只要VP8匹配就绝不会选AV1。要利用好降级必须掌握两个技巧控制supported_formats顺序通过setCodecPreferences()显式排序。例如强制H264优先于VP8transceiver.setCodecPreferences([ { mimeType: video/H264, ... }, { mimeType: video/VP8, ... } ]);提供“兜底编码器”在addTransceiver()时为sendEncodings配置多个scaleResolutionDownBy让编码器能在分辨率/帧率上动态缩放。例如pc.addTransceiver(video, { sendEncodings: [ { scaleResolutionDownBy: 1 }, // 1080p { scaleResolutionDownBy: 2 }, // 540p { scaleResolutionDownBy: 4 } // 270p保底 ] });当网络拥塞时VideoStreamEncoder会自动切换到更低分辨率的编码路径无需重新协商SDP。我曾在一个教育App中部署此策略主讲人用MacBook Pro开1080p学生用千元安卓机协商后自动降为270p15fps但语音始终流畅。用户感知是“画质一般”而非“卡顿黑屏”体验差距巨大。4. 实操过程与核心环节实现4.1 从零开始在Chrome DevTools中追踪协商全过程脱离调试工具谈源码是纸上谈兵。以下是在Chrome 120中实时监控媒体能力协商的完整步骤。这不是理论而是我每天都在用的工作流。第一步开启WebRTC Internals面板访问chrome://webrtc-internals点击右上角“Start Recording”录制图标在你的WebRTC应用中触发createOffer()如点击“开始通话”第二步定位关键日志流在webrtc-internals页面你会看到多个TabPeerConnections显示所有RTCPeerConnection实例点击目标连接的pc_id。Stats实时统计但协商细节在此处不明显。Event Log核心这里记录了每一步SDP操作。查找关键词SetLocalDescription本地Offer/Answer被设置SetRemoteDescription远端Offer/Answer被接收NegotiationNeeded触发协商事件IceConnectionStateChangeICE状态变化常与协商并发第三步深挖SDP内容与能力匹配在Event Log中找到SetLocalDescription事件点击右侧的▶展开。你会看到完整的SDP文本。重点分析mvideo行后的artpmap列出所有候选编码器及其payload typeptafmtp行每个编码器的具体参数如afmtp:100 level-asymmetry-allowed1;packetization-mode1;profile-level-id42e01fartcp-fb反馈机制支持如artcp-fb:100 nack表示支持NACK此时切换到StatsTab筛选outbound-rtp类型找到对应trackId的统计项。关键字段codecId当前实际使用的编码器ID如video-100framesEncoded已编码帧数为0说明编码未启动keyFramesEncoded关键帧数若为0且framesEncoded0说明编码器卡在I帧第四步源码级断点验证若需深入C层启动Chrome with debug flags# macOS open -a Google Chrome --args \ --remote-debugging-port9222 \ --enable-loggingstderr \ --v1 \ --vmodule*/webrtc/media/engine/*2,*/webrtc/p2p/base/*2然后访问chrome://version查看Command Line确认flag生效。在chrome://webrtc-internals的Event Log中点击任意事件旁的Source链接可跳转到Chromium源码对应的行需提前下载源码并配置Source Map。我常用的一个断点位置是webrtc/media/engine/webrtc_media_engine.cc的WebRtcMediaEngine::FindMatchingVideoCodec()函数入口。当它被调用时观察remote_codec远端Offer中的编码器和supported_formats本地支持列表的值就能100%确认协商逻辑是否按预期执行。有一次我发现supported_formats里H264的profile-level-id被错误解析为42001fLevel 1.3而Offer要求42e01fLevel 3.1但函数返回了absl::nullopt证明降级未触发。追查发现是Android NDK版本过低MediaCodecList返回的profileLevels数组被截断。升级NDK后问题解决。4.2 修改源码为老旧设备强制启用H264 Baseline当标准协商无法满足业务需求时修改WebRTC源码是终极方案。以下是在Chromium 120源码中为Android平台强制启用H264 Baseline Profile的完整patch已在某千万级教育App中稳定运行18个月。问题场景某款搭载联发科MT6735芯片的Android 5.1平板MediaCodecList只报告profileLevels[0] {profile1, level1}Baseline Level 1但实际硬件支持Level 3.1。Chrome默认信任MediaCodecList导致协商时拒绝profile-level-id42e01f的Offer。解决方案在webrtc/sdk/android/src/jni/android_video_encoder.cc中绕过MediaCodecList的静态检查动态注入Level 3.1支持。Patch步骤定位到AndroidVideoEncoder::GetSupportedFormats()函数约第120行在std::vectorVideoCodecInfo formats;声明后插入以下代码// 【新增】强制为MT6735芯片添加H264 Baseline Level 3.1支持 std::string device_model GetDeviceModel(); // 需先实现GetDeviceModel() if (device_model MT6735) { VideoCodecInfo h264_baseline_31; h264_baseline_31.name H264; h264_baseline_31.codec_type kVideoCodecH264; h264_baseline_31.max_width 1280; h264_baseline_31.max_height 720; h264_baseline_31.max_fps 30; // 关键硬编码profile-level-id为42e01f h264_baseline_31.parameters[profile-level-id] 42e01f; h264_baseline_31.parameters[level-asymmetry-allowed] 1; h264_baseline_31.parameters[packetization-mode] 1; formats.push_back(h264_baseline_31); }实现GetDeviceModel()在同文件底部std::string GetDeviceModel() { JNIEnv* env AttachCurrentThread(); jclass build_class env-FindClass(android/os/Build); jfieldID model_id env-GetStaticFieldID(build_class, MODEL, Ljava/lang/String;); jstring model_jstr (jstring) env-GetStaticObjectField(build_class, model_id); const char* model_cstr env-GetStringUTFChars(model_jstr, nullptr); std::string model(model_cstr); env-ReleaseStringUTFChars(model_jstr, model_cstr); env-DeleteLocalRef(model_jstr); env-DeleteLocalRef(build_class); DetachCurrentThread(); return model; }重新编译WebRTC for Android需完整GN构建环境效果验证Patch前Offer含afmtp:100 profile-level-id42e01fAnswer中无H264条目协商失败。Patch后GetSupportedFormats()返回的formats列表包含profile-level-id42e01fFindMatchingVideoCodec()成功匹配Answer中出现afmtp:100 profile-level-id42e01f1080p视频正常播放。注意此patch仅影响编码器能力声明解码器仍需AndroidVideoDecoder同步patch。且必须严格限定设备型号避免误伤其他芯片。我们通过Build.MODEL和Build.BOARD双重校验确保精准命中。4.3 构建自定义协商引擎脱离SDP的轻量级方案对于IoT设备或资源受限终端如内存128MB的嵌入式摄像头标准WebRTC的SDP协商过于重量级。我们曾为某安防摄像头SDK开发了一个极简协商引擎仅200行C代码完美替代createOffer/createAnswer。设计原则去SDP化不生成完整SDP只交换核心能力哈希二进制协议用Protocol Buffers序列化体积比文本SDP小80%单次握手Offer/Answer合并为一个NegotiateRequest消息核心数据结构Protobuf定义message NegotiateRequest { string device_id 1; // 设备唯一标识 repeated CodecPreference codecs 2; // 编码器偏好列表 bool supports_fec 3; // 是否支持FEC int32 max_bitrate_kbps 4; // 最大码率 } message CodecPreference { enum CodecType { H264 0; VP8 1; OPUS 2; } CodecType type 1; uint32 profile_level_id 2; // H264: 0x42e01f, VP8: 0 (无) uint32 clock_rate 3; // 音频采样率 }协商流程摄像头端发送NegotiateRequest含codecs[{type:H264, profile_level_id:0x42e01f}]手机APP端收到后遍历本地解码器能力表预存于ROMstruct DecoderCapability { CodecType type; uint32 min_profile_level_id; // 支持的最低Level uint32 max_profile_level_id; // 支持的最高Level }; static const DecoderCapability kDecoderCaps[] { {H264, 0x42001f, 0x640032}, // 支持Baseline Level 1.3 到 High Level 5.0 };匹配逻辑request.profile_level_id cap.min_profile_level_id request.profile_level_id cap.max_profile_level_id若匹配回复NegotiateResponse含选定的codec和selected_profile_level_id双方据此配置RTP通道跳过SDP解析优势启动时间从1200ms降至180ms免去SDP tokenizer内存占用从8MB降至1.2MB无SDP字符串缓存兼容性更高不依赖WebRTC版本纯C实现我们在某款海思Hi3516DV300摄像头中部署此方案与标准WebRTC手机端互通延迟稳定在300ms内。这证明理解协商本质远比套用框架更重要。5. 常见问题与排查技巧实录5.1 “No common codecs”错误的七种根因与速查表Failed to set remote offer: no common codecs是WebRTC协商失败最经典的报错。但它的背后可能是七个完全不同的技术问题。以下是我在一线支持中整理的速查表按发生频率排序排查序号根本原因典型现象快速验证方法解决方案1编解码器名称大小写不匹配Offer写video/h264Answer写video/H264在chrome://webrtc-internals中对比artpmap行统一使用大写RFC 4855规定2H264profile-level-id位宽错误Offer42e01fAnswer设备只支持42001fLevel 1.3解析afmtp参数用在线工具如https://tools.ietf.org/html/rfc6184#section-8.1验证Answer端降级profile-level-id或Offer端提高兼容性3VP9profile参数缺失Offer含afmtp:98 profile2Answer设备VP9只支持profile0检查afmtp行是否有profile无则默认为0在Offer中显式添加profile0或Answer端忽略profile检查4音频采样率不匹配Offerartpmap:111 opus/48000/2Answer设备OPUS只支持44100查看artpmap的第三字段采样率Offer中添加artp