Android实现Miracast Sink端:从Wi-Fi P2P到H.264解码渲染的完整实战

发布时间:2026/9/21 14:46:53
Android实现Miracast Sink端:从Wi-Fi P2P到H.264解码渲染的完整实战 这项目我做完了跑了几个月中间踩了不少坑。坦白说Miracast Sink端在Android上实现网上资料大多只讲了Wi-Fi P2P的配对真正把RTSP协商、RTP收流、H.264解码渲染这条完整链路讲透的几乎没有。这篇就实操记录为主把整个流程和我能想到的细节都写出来代码可以直接参考至少能让你少走几个星期的弯路。1. 项目整体设计与技术架构拆解先说清楚Miracast Sink端到底是个什么东西。Miracast是Wi-Fi Alliance定义的无线投屏协议核心是让Source端发流端通常是手机通过网络把屏幕内容实时传给Sink端收流显示端比如电视、盒子、车机。Sink端就是接收显示的那一方。协议栈底层强制使用Wi-Fi P2P建立直连链路上层又分成了RTSP信令控制、RTP音视频传输、HDCP内容保护这几层。Android系统从4.0开始就内置了Wi-Fi P2P API但官方SDK从来没有暴露过Miracast Sink端的接口因为谷歌那套方案主要给Miracast Source用的。用手机当Sink有些人觉得没必要但在很多场景下非常实用比如老旧电视没有投屏接收功能手上的Android设备当接收屏车里把后排娱乐屏当作Sink显示导航开发测试时需要抓取手机投出的音视频流做分析。跑通了这套流程后面做投射测试工具、投屏协议分析仪都有基础。整体架构上我拆成了四层链路层、信令层、传输层和应用层。链路层走Wi-Fi P2P建立Source和Sink的直连两边协商好IP后在TCP 7236端口上做RTSP会话。信令层是Miracast的控制面通过RTSP M1-M6消息协商音视频参数。传输层基于RTP承载H.264视频流和AAC音频流。应用层就是解码、渲染、播放通常用MediaCodec SurfaceView实现。这四层之间是环环相扣的P2P连不上后面全免谈P2P参数协商失败音频视频也出不来。调试的时候建议一层一层确认好了再往下走。为了让整体流程更清晰画一张简单的流程表阶段关键技术点核心任务链路建立Wi-Fi P2P、Supplicant发现、配对、连接、获取IP信令协商RTSP、WFD IE能力交换、参数协商、RTSP会话建立音视频传输RTP、H.264、AAC收流、拆包、组帧、时间戳同步解码显示MediaCodec、Surface硬解码、渲染、AudioTrack播放Wi-Fi P2P是物理链路基础RTSP协商是双方“互相摸底”的关键RTP承载真正的音视频数据MediaCodec负责将H.264流解码成帧画面。2. 核心关键点Wi-Fi P2P连接建立全流程Wi-Fi P2P也就是Wi-Fi Direct是整套流程里最琐碎也最容易出问题的一环。Android系统提供的WifiP2pManager封装了大部分底层细节但从调用API到真正能跑TCP中间的状态变化比想象中要多。2.1 初始化与广播注册WifiP2pManager的调用方式在不同Android版本上差异较大API 29以上的初始化写法如下WifiP2pManager manager (WifiP2pManager) context.getSystemService(Context.WIFI_P2P_SERVICE); Channel channel manager.initialize(context, Looper.getMainLooper(), null);需要注意的是initialize必须在主线程调用而且要在Looper准备好之后。紧接着要注册系统的WIFI_P2P状态变化广播这是整个P2P流程的关键节点。广播接收器要监听WIFI_P2P_STATE_CHANGED_ACTION、WIFI_P2P_PEERS_CHANGED_ACTION、WIFI_P2P_CONNECTION_CHANGED_ACTION、WIFI_P2P_THIS_DEVICE_CHANGED_ACTION这几类action。最常踩的坑是很多人只监听前两个漏了连接状态变化导致Group建好之后不知道什么时候可以起服务白白等超时。在Android 13及以上版本动态广播需要加上RECEIVER_NOT_EXPORTEDflag。此外Android 10以下需要在Manifest里声明这些权限ACCESS_WIFI_STATE、CHANGE_WIFI_STATE、ACCESS_FINE_LOCATION、CHANGE_NETWORK_STATE、INTERNET。Android 12及以上还要额外注意附近WLAN设备的权限否则扫描阶段会直接空白。2.2 设备发现与回调处理Sink端作为Group Owner需要主动发起设备发现。调用manager.discoverPeers(channel, actionListener)后回调里的onSuccess只代表系统开始扫描不等于找到了设备。真正的结果要等广播里收到WIFI_P2P_PEERS_CHANGED_ACTION之后再去通过requestPeers拿设备列表。设备发现回调的细节很容易被忽略。很多设备第一次扫描结果为空需要间隔3-5秒再触发一次不然经常扫不到。我遇到过一批手机必须连续触发3-5次才能发现相邻设备可能是驱动的扫描通道切换时序问题。做了个简单的定时器重试机制把间隔控制在3000毫秒比较稳。扫描到设备后还要看设备的wpsP2pSupported字段低端设备上这个标志位经常是false配对时就要用DisplayPin而不是PushButton方式不然会卡在配对环节。设备类型过滤也要提前想好。严格来说Source端设备通常显示为10-0050F204-5Network Infrastructure但不同厂商的wfdDeviceType可能写得不规范不能把这个字段作为唯一过滤依据多判断一轮设备名称会更稳。2.3 连接与Group Owner协商connect是Wi-Fi P2P里最关键的一步因为这一步背后是四路握手、WPS配对、Group协商的完整流程。调用示例WifiP2pConfig config new WifiP2pConfig(); config.deviceAddress device.deviceAddress; config.wps.setup WpsInfo.PBC; manager.connect(channel, config, actionListener);流程走到这里设备往往会在系统层弹一个配对框用户要确认。我们做的是Sink端同一设备上会跑Service和UI如果产品形态是无屏幕的盒子就要处理自动接受配对的方案否则只能在有输入设备的场景下用。这里的建议是系统应用直接使用WifiP2pConfig中的groupOwnerIntent字段来影响协商结果数值范围0-15Sink端设置为15可以极大提升成为Group Owner的概率。P2P连接建立之后WIFI_P2P_CONNECTION_CHANGED_ACTION广播会携带最新的WifiP2pInfo其中包含isGroupOwner、groupOwnerAddress以及groupFormed标记。拿到这三个字段说明P2P真连上了不是仅仅“扫描到了”而已。2.4 IP分配与链路就绪判定Miracast规范里P2P GroupOwner要开启DHCP服务给Client提供IP地址。Sink端在这个阶段承担GroupOwner职责但Android设备本身通常没有内置DHCP Server。有两种方案直接在Group Owner上静态配置一个IP比如192.168.49.1这是Android P2P的默认网段或者自己实现一个简化版DHCP Server。第一种方式在Wi-Fi Direct场景里更常用、也更稳。实际操作是在连接成功的回调里通过WifiManager去更新网络配置或者使用ConnectivityManager来绑定P2P网络。小米、华为部分机型的驱动在P2P连接后会延迟几秒才分配IP所以在连接成功的回调里不要立刻去建TCP服务端先循环探测一下确认链路通了再起RTSP Server能规避掉大多数兼容性问题。注意在Android 10以上使用P2P网络需要为本地Socket绑定Network对象否则流量走的是蜂窝或普通Wi-FiSource端根本连不上Sink端的TCP端口。这一步能过滤掉很多人卡了一周但完全没思路的问题。3. RTSP信令协商全解析P2P连通之后才是Miracast真正发力的地方。Source端和Sink端要靠RTSP完成能力交换、参数协商、媒体建立这三个阶段。RTSP包基于TCP传输默认端口7236。3.1 Miracast RTSP消息结构Miracast RTSP基于RFC 2326但加入了WFDWi-Fi Display私有扩展。Sink端启动后要监听7236端口等Source端先发M1请求。常见的M1长这样OPTIONS rtsp://192.168.49.1:7236 RTSP/1.0 CSeq: 0 User-Agent: stagefright/1.2 (Linux;Android 9)收到M1后Sink要回一个200响应同时把自己的能力带在Supported头里。从这一开始就要注意CSeq的正确回传必须严格沿用请求里的CSeq值一旦对不上Source端立刻断链而且不会给任何日志提示。再看M2Source会要求Sink提供能力集响应里的wfd_video_formats是最关键的一个字段要按WFD规范编码。能显示1080p就声明1080p能解H.264就声明H.264这段声明会决定后续实际传输的分辨率和帧率。3.2 M3-M4参数协商的隐藏坑M3消息是Source端用来请求建立会话的里面会带wfd_presentation_url。响应时要带上协商结果wfd_video_formats: 01 01 02 00000020 00000000 00000000 00000000 00000000 00000000 00000000。这个字段不能随便填每个参数组都严格对应分辨率、帧率、色深、HDCP标志位。M4是Source端最终选定参数的确认收到后Sink要回200表示接受Source的选择。M4里Source可能自己选了某种格式如果Sink不支持要回603 Decline绝对不能回200之后再拒绝接收否则Source端会把RTP流已经开始发送但你无法解码显示连上就花屏。就在这个环节上挣扎过好几天后来逐字节比对wfd_fmt字段才把问题定位清楚。3.3 M5-M6会话建立的最终握手M5请求里一般带着wfd_trig_method、wfd_presentation_url、wfd_rtsp_ports。这里要留出的坑是Sink端声明的RTSP端口要和实际监听的Socket完全一致。我第一版监听的是7236响应里居然还写着默认的rtsp://192.168.49.1:7236而M5里Source声明自己接收RTP的端口这个端口来自wfd_rtsp_portsRTP/AVP/UDP就按UDP处理RTP/AVP/TCP就按TCP处理这个协商值在后面收流时要用到。M6是最后一个触发指令Source发完M6就等Sink回200回完立刻开始推流。所以M6回完之后到第一帧画面出来的时间窗口非常短正常情况下应在200ms以内如果Sink端的解码器没有提前配置好第一帧画面几乎必定丢失这是很多“黑屏但链路正常”问题的根源。4. RTP音视频流接收、解码与渲染实现信令协商完成后真正考验功底的是RTP收流、解码、渲染这条流水线。Source端会把H.264视频和AAC音频分别封装成RTP包发过来Sink要按序穿越UDP端口、拆解RTP包、重组帧再喂给MediaCodec硬解。4.1 视频接收与H.264组帧视频RTP包格式是比较标准的12字节RTP头 RTP载荷。H.264载荷一般情况下是单NAL单元模式也就是一个IP包承载一个NAL类型的数据。要干的活儿就是从RTP负载中解出NAL单元去掉起始码再交给解码器。难点在于RTP包可能丢失。如果Source端设备性能一般Wi-Fi环境存在干扰UDP丢包率就上来了。丢包会导致NAL不完整如果直接喂给MediaCodec轻则花屏重则解码器卡死后续所有帧全被丢弃。一种稳妥方案是标记丢包点如果检测到连续RTP序号不连续就把当前帧的bufferInfo标记为不完整直到下一个IDR帧再喂给解码器。经验值是缓冲区间在5-10毫秒内尽量不等等得越久画面延迟越大。H.264的SPS/PPS处理也很关键。虽然没有看到代码正文但这块确实是投屏开发的高频坑。Source端的SPS/PPS有可能通过RTSP M4消息传也可能带在RTP流里。实现时要做一套状态机存好SPS/PPS如果MediaCodec要求csd-0/csd-1就把SPS/PPS在configure阶段传给解码器如果流中途SPS/PPS发生变化要动态更新。4.2 MediaCodec硬解码配置Android的MediaCodec对H.264硬解支持很成熟但要配置好输入格式。按开发经验常见配置是video/avc、KEY_FRAME_RATE、KEY_I_FRAME_INTERVAL、KEY_COLOR_FORMAT。要想让延迟尽可能低可以考虑KEY_LOW_LATENCY但这是一个可选键部分芯片不实现需要判断返回值再做降级。从BufferQueue拿到outputBuffer后直接交给SurfaceView显示这就要注意最后releaseOutputBuffer(bufferIndex, true)这行true表示render到Surface上。很多新手漏了这个参数结果就是一直在解码、一直黑屏。另一点是分辨率变化。投屏过程中用户可能旋转手机屏幕分辨率从1080p横屏切到竖屏MediaCodec的OutputFormat会变。要在onOutputFormatChanged回调中重新设置Surface的尺寸或者让SurfaceView自适配否则画面会变形、裁切。4.3 音频接收与AAC解码Miracast音频通常使用AAC-LC封装在RTP里协议约定是按RFC 3640的格式打包。其中有效载荷包含AU-headers每个AU-headers的长度字段是13bit表示Access Unit的长度。这一步必须耐心解析搞错一个字节音频就完全不出声。音频的采样率、声道数、profile也可以在RTSP M4协商里拿到比如wfd_audio_codecs: AAC。实际传输时有时候音频RTP包没有提供采样率这时候要根据RTP时间戳的增量来推断默认按44100Hz处理。解码时把ADTS头拼好MediaCodec的KEY_IS_ADTS设为1再喂给audio/mp4a-latm类型的解码器。声音延迟和画面对齐也是关键。投屏场景要音画同步以视频时间为基准音频早到就略微延时、晚到就稍微追帧。我做了一套简单的缓冲控制音频缓冲目标设定在100ms以内当RTP包到达的抖动超过200ms时丢弃一部分音频帧换取同步。4.4 端到端延迟调优Miracast链路天生就有延迟常见优化手段解码前缓冲不要贪多很多人为了平稳播放在解码前搞了一大段缓存结果画质没提升延迟蹭蹭上到好几秒。我的经验是视频缓冲控制在50-100ms音频缓冲控制在100-200ms宁可偶尔卡一下投屏体验也远好于像看卫星转播一样慢半拍。Surface渲染用双缓冲使用SurfaceView时SurfaceFlinger本身有vsync机制配合硬解可以直接渲染比TextureView少一道纹理拷贝明显降低延迟。关掉不必要的高分辨率如果Sink端解码能力有限强制Source端协商到较大分辨率反而会拉高延迟。Sink端在M3-M4协商时选择自己能轻松扛住的分辨率更靠谱。5. 常见问题与排查技巧实录整个项目做完整理了这份问题排查清单实际上比正儿八经的功能实现还要值钱。现象可能原因排查方法Source端搜不到SinkP2P扫描未触发或权限缺失确认动态广播注册了PEERS_CHANGED重试3-5次不断提示配对失败WPS方式不匹配切换PIN或PBC方式查看设备WPS能力P2P已连接但TCP连不上本地Socket未绑定P2P网络Android 10使用Network.bindSocketM1-M2正常但M3无响应参数协商失败抓RTSP包比对wfd_video_formats和wfd_audio_codecs有画面但很花SPS/PPS配置错乱或丢包检查是否传递了正确的csd-0/csd-1断流时等待IDR帧音频无声或杂音AAC AU-headers解析错误按RFC 3640规范逐字段解析AU-headers延迟特别高缓冲过大或渲染方式不当缩到100ms缓存选择SurfaceView投屏后源端卡顿Sink端压力大没及时收包确认接收线程没有阻塞增大UDP接收缓冲区HDCP也是投屏绕不开的话题。Miracast规范规定强制支持HDCP但很多Sink端设备并没有真正的HDCP证书。如果Source端要求HDCP保护而Sink端没有对应机制Source就会直接拒绝输出现象是“连接建立后屏幕一直是黑的没有任何数据”。常见解决思路是在M4协商时声明支持HDCP但实际只传未加密流不过这种方案只适合内部测试如果做正式产品必须正确走完HDCP保护流程。还有一类问题是“虽然成功了但是不稳定”。Wi-Fi P2P本身是点对点直连但2.4GHz频段在环境里特别容易受干扰。手机投屏时如果旁边蓝牙设备多、路由器信道拥挤P2P链路就会抖动RTP丢包暴涨。实际项目里可以提示用户把投屏环境挪到5GHz频段或者手动把Wi-Fi P2P工作频段设为5GHz很多小厂设备都支持通过系统属性或者驱动配置来切换。调试工具方面手机上最有价值的就是抓包日志了。RTSP是基于TCP的tcpdump抓包就能看信令全过程RTP是UDP抓包时要兼顾时序和端口。如果手机没root可以用adb shell tcpdump不过要确认P2P接口名通常是p2p0。抓包后优先看四件事RTSP每个请求是否如期返回200、M4协商的参数是否合法、RTP序号是否连续、SSRC有没有中途变化。SSRC变化很隐蔽Source端重启推流后SSRC会变解码端没用新SSRC做状态重置就会一直解码失败。最后再分享一个后续扩展的比较成熟的方向把Sink端跑通之后同一套代码可以继续做屏幕录制和低延迟转发。核心思路是不需要额外申请屏幕录制权限因为RTP流已经拿到解码前的数据了在MediaCodec解码前复制一份完整的H.264流存成mp4文件或者推到流媒体服务器。基于这套逻辑开发一个无线投屏电视盒也顺理成章毕竟是Android系统硬件成本不高Sink端代码却可以复用一个底子。关于Miracast后续要不要接着讲评论区聊聊想看哪块我再单独写。