资讯动态

Miracast双端协议栈实战:从Wi-Fi Direct到RTP封包全链路实现

发布时间:2026/10/17 5:17:31 来源:尧图企业网站定制
简介本资源是一套面向嵌入式开发与无线显示技术学习者的 Miracast 投屏双端Source 源端 Sink 接收端开源代码示例聚焦 Wi-Fi Display 协议在实际设备中的落地实现适用于 Android 平台开发者、音视频传输方向初/中级工程师及协议栈研究者。压缩包共 64 个文件含 19 个 C 源码实现视频捕获、编码/解码逻辑、16 个头文件定义 Miracast 控制接口与数据结构、10 个 XML 配置与框架修改文件如 WifiDisplayController.diff 及配套 Java 层适配、5 个 PNG 图标资源及 README、Android.mk、.gitignore 等工程支撑文件整体仅 212KB轻量易读。已有 5537 人学习下载说明其在 Miracast 实践入门中具备较高参考价值。读者可直接基于该工程理解源端如何通过 Wi-Fi Direct 建连并推送 H.264 流、Sink 端如何完成协议握手、流接收与本地渲染尤其适合分析 Android Framework 层 Display 控制模块的定制化改造路径。1. Miracast 投屏不是“连上WiFi就能镜像”一份能跑通 source/sink 双端的实操代码包专治协议黑匣子、WFD状态卡死、HDCP协商失败三类翻车现场Miracast 不是 WiFi Display 的代名词而是基于 Wi-Fi Direct 构建的一套完整协议栈——它要求 source如手机/PC和 sink如电视/接收器必须在链路层建立直连、在会话层完成 WFDWi-Fi Display能力交换、在流控层同步 RTSP 控制信令、在媒体层对齐 H.264 编码参数与 RTP 打包格式。很多开发者卡在“设备搜到了但点不了投屏”本质是没意识到 Miracast 是一套四层协同协议而非单个 API 调用。这份代码示例包不依赖 Android 系统级 WFD 框架也不封装成黑盒 SDK而是提供可编译、可调试、可断点的 C/C 级 source 与 sink 端最小可运行实现覆盖从 Wi-Fi Direct P2P 连接建立、WFD SSDP 发现、RTSP 会话协商、到 H.264 Annex-B 帧注入与 RTP 封装的全链路。适合嵌入式音视频工程师、Linux 驱动开发者、以及需要在定制硬件上复现 Miracast 协议栈的团队。它不能直接当成品用但能让你看清每一帧 RTP 包里 timestamp 怎么算、SDP 中aframerate:字段怎么影响解码器初始化、SETUP请求里Transport头的interleaved参数为何决定控制信令走 TCP 还是 UDP。2. 从零构建 Miracast 双端环境准备、协议栈选型与源码结构拆解2.1 开发环境硬性要求Linux 内核 ≥ 3.18 nl80211 支持 wpa_supplicant 2.9Miracast 依赖底层 Wi-Fi DirectP2P能力而该能力在 Linux 上由内核cfg80211和mac80211子系统暴露用户态通过nl80211netlink 接口调用。低于 3.18 的内核缺少关键 P2P 属性如NL80211_P2P_DEV_ADDR会导致wpa_cli p2p_find无响应wpa_supplicant必须启用CONFIG_P2Py且版本 ≥ 2.9旧版不支持p2p_group_add persistent持久化组配置。我们实测过 Ubuntu 20.045.4 内核 wpa_supplicant 2.9和 Debian 115.10 内核 wpa_supplicant 2.10双平台稳定运行。注意不要用hostapd替代wpa_supplicant—— hostapd 仅支持 AP 模式无法发起 P2P Discovery。提示验证 P2P 是否就绪执行iw dev wlan0 info查看是否含p2pcapability再运行wpa_cli -i wlan0 p2p_peers若返回空但无报错说明驱动已加载若报Failed to connect to non-global ctrl_ifname: wlan0需确认wpa_supplicant启动时加了-Dnl80211 -i wlan0 -c /etc/wpa_supplicant/wpa_supplicant.conf参数。2.2 协议栈分层选型为什么不用 GStreamer 插件而手撸 RTSP/RTP 栈GStreamer 的rtspclientsink和rtspsrc插件看似省事但 Miracast 对 RTSP 有强定制DESCRIBE响应中必须包含acontrol:trackID1且trackID必须与后续SETUP一致SETUP的Transport头必须带interleaved0-1控制信令走 TCP channel 0媒体数据走 channel 1PLAY请求必须携带Range: npt0.000-否则 sink 端拒绝启动解码RTP 时间戳基准必须与 SDP 中aframerate:严格匹配例如aframerate:30.0→ 时间戳增量 90000/30 3000。GStreamer 插件默认按通用 RTSP 服务器行为实现无法精确控制上述字段。本代码包采用轻量级libmicrohttpd实现 HTTP/RTSP 服务端sink用libusockets实现 RTSP 客户端source所有信令字段手动构造确保与 WFA Miracast 2.0 规范第 7.3 节完全对齐。媒体层使用libx264编码器source和libavcodec解码器sink避免 FFmpeg 全量依赖带来的体积膨胀。2.3 源码包结构6 个核心目录每个都对应协议栈一个致命环节目录名功能定位关键文件说明p2p/Wi-Fi Direct 底层控制p2p_manager.c封装wpa_cli命令为 C 函数含p2p_connect()、p2p_disconnect()、p2p_get_group_info()p2p_event_handler.c监听CTRL-EVENT-P2P-PROV-DISC-SHOW-PIN等事件触发 PIN 码交互逻辑wfd/WFD 能力发现与描述wfd_sdp.c生成符合 WFA 规范的 SDP含acontent-type:WFD-1.0、asetup:actpass、avideo-smpte292mwfd_ssdp.c实现 SSDP M-SEARCH 响应绑定239.255.255.250:1900组播地址rtsp/RTSP 信令交互引擎rtsp_server.csink处理OPTIONS/DESCRIBE/SETUP/PLAY/TEARDOWNrtsp_client.csource构造合法请求并解析响应rtsp_transport.c管理 interleaved channel 0/1 的 TCP socket 复用rtp/RTP 封包与时间戳对齐rtp_packetizer.c将 H.264 NALU 按 RFC 3984 封装为 STAP-A/ FU-Artp_timestamp.c根据aframerate:动态计算timestamp字段避免音画不同步media/编解码与帧缓冲encoder_x264.c设置x264_param_t中i_fps_num/i_fps_den、b_repeat_headers1强制每 IDR 帧带 SPS/PPSdecoder_avcodec.c注册AV_CODEC_ID_H264并处理AV_PKT_FLAG_KEY标志utils/跨平台工具链timer.c高精度clock_gettime(CLOCK_MONOTONIC)计时log.h带模块前缀的日志宏如[WFD] SSDP response sentconfig_parser.c解析miracast.conf中wfd_video_format11720p30、wfd_hdcp00disable等参数整个工程采用 CMake 构建CMakeLists.txt显式声明各模块依赖关系如rtsp_server链接p2p和wfd避免隐式链接导致的符号未定义错误。编译命令为mkdir build cd build cmake -DCMAKE_BUILD_TYPEDebug \ -DENABLE_WFD_SINKON \ -DENABLE_WFD_SOURCEON \ -DX264_INCLUDE_DIR/usr/include/x264 \ -DX264_LIBRARY/usr/lib/x86_64-linux-gnu/libx264.so \ .. make -j$(nproc)注意-DENABLE_WFD_SINKON表示编译 sink 端可执行文件wfd_sink-DENABLE_WFD_SOURCEON编译wfd_source。二者可独立编译便于单端调试。3. Source 端实战如何把本地摄像头画面编码并推送到 Sink3.1 摄像头采集与预处理V4L2 驱动适配与分辨率裁剪Miracast sink 端通常只支持特定分辨率如 1280×72030fps而 USB 摄像头常输出 1920×108030fps。硬缩放会导致 CPU 占用飙升因此我们在 V4L2 层直接请求目标分辨率// media/v4l2_capture.c struct v4l2_format fmt {0}; fmt.type V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width 1280; fmt.fmt.pix.height 720; fmt.fmt.pix.pixelformat V4L2_PIX_FMT_H264; // 若摄像头支持硬编码 fmt.fmt.pix.field V4L2_FIELD_NONE; if (ioctl(fd, VIDIOC_S_FMT, fmt) 0) { // fallback: 请求 YUYVCPU 软缩放 fmt.fmt.pix.pixelformat V4L2_PIX_FMT_YUYV; ioctl(fd, VIDIOC_S_FMT, fmt); }关键点VIDIOC_S_FMT必须在VIDIOC_REQBUFS之前调用否则内核返回EINVAL。若摄像头不支持 H.264 输出则fmt.fmt.pix.pixelformat设为V4L2_PIX_FMT_YUYV后续交由libx264编码。此时需启用libx264的b_vflip1V4L2 默认 BGR 上下颠倒和i_width/i_height强制设为 1280×720避免编码器内部重采样。3.2 H.264 编码参数为什么必须开启b_repeat_headers和b_annexbMiracast sink 端解码器如 Rockchip RK3399 的 VPU要求每个 IDR 帧开头必须携带 SPS/PPS否则解码失败黑屏。libx264默认只在第一个 IDR 帧发送 SPS/PPS因此必须开启// media/encoder_x264.c x264_param_default_preset(param, ultrafast, zerolatency); param.i_threads 1; // 单线程避免帧乱序 param.b_repeat_headers 1; // 每个 IDR 帧重复 SPS/PPS param.b_annexb 1; // 输出 Annex-B 格式00 00 00 01 分隔 param.i_fps_num 30; param.i_fps_den 1; // 与 SDP 中 framerate 严格一致 param.rc.i_rc_method X264_RC_CRF; param.rc.f_rf_constant 23;b_annexb1是硬性要求Miracast RTP 负载必须是 Annex-B 格式NALU 以00 00 00 01开头而非 AVCClength-prefixed。若设为0rtp_packetizer.c会因找不到起始码而丢弃整帧。3.3 RTP 封包逻辑FU-A 分片与时间戳对齐的双重校验H.264 一帧可能超 MTU1500 字节必须分片。Miracast 要求使用 FU-AFragmentation Unit A分片而非 STAP-ASingle-Time Aggregation Packet。rtp_packetizer.c中关键逻辑// rtp/rtp_packetizer.c void rtp_packetize_h264_fua(uint8_t *nal, int nal_len, uint32_t ts_base) { const int max_payload 1400; // 留 100 字节给 IP/UDP/RTP 头 if (nal_len max_payload) { // 单包发送type NALU type rtp_send_packet(nal, nal_len, ts_base, 0); return; } // FU-A 分片首包 type28, NRI原NALU的NRI, S1, E0, R0, TYPE原NALU type uint8_t fua_header[2] {0}; fua_header[0] (nal[0] 0xE0) | 28; // 0xE0 11100000, 保留 NRI fua_header[1] 0x80 | (nal[0] 0x1F); // S1, E0, R0, TYPE原type rtp_send_packet(fua_header, 2, ts_base, 0); // 中间包S0, E0 for (int i 2; i nal_len; i max_payload) { fua_header[1] (nal[0] 0x1F); // S0, E0, R0, TYPE原type rtp_send_packet(fua_header, 2, ts_base (i-2)*3000, 0); // ... 发送 payload 数据 } // 末包E1 fua_header[1] 0x40 | (nal[0] 0x1F); // S0, E1, R0, TYPE原type rtp_send_packet(fua_header, 2, ts_base (nal_len-2)*3000, 0); }时间戳ts_base来自rtp_timestamp.c的全局计数器每次调用rtp_packetize_h264_fua()时传入当前帧基准时间戳单位90kHz分片包的时间戳按偏移量递增*3000因为 30fps → 90000/303000。此设计确保 sink 端解码器能正确重组帧。4. Sink 端实战如何接收 RTSP 流并驱动显示设备4.1 RTSP 服务端状态机从DESCRIBE到PLAY的四步握手Sink 端rtsp_server.c实现有限状态机拒绝任何非顺序请求// rtsp/rtsp_server.c typedef enum { RTSP_STATE_INIT, RTSP_STATE_DESCRIBED, RTSP_STATE_SETUPED, RTSP_STATE_PLAYING } rtsp_state_t; static rtsp_state_t g_rtsp_state RTSP_STATE_INIT; void handle_rtsp_request(char *method, char *uri) { if (strcmp(method, DESCRIBE) 0) { if (g_rtsp_state ! RTSP_STATE_INIT) goto reject; send_sdp(); // 返回含 acontrol:trackID1 的 SDP g_rtsp_state RTSP_STATE_DESCRIBED; } else if (strcmp(method, SETUP) 0) { if (g_rtsp_state ! RTSP_STATE_DESCRIBED) goto reject; parse_transport_header(); // 提取 interleaved0-1 g_rtsp_state RTSP_STATE_SETUPED; } else if (strcmp(method, PLAY) 0) { if (g_rtsp_state ! RTSP_STATE_SETUPED) goto reject; start_rtp_receiver(); // 启动 UDP socket 接收 RTP g_rtsp_state RTSP_STATE_PLAYING; } reject: send_rtsp_error(454); // Session Not Found }若 source 端跳过SETUP直接发PLAYsink 端立即返回454 Session Not Found。这是 Miracast 协议强制要求也是排查“卡在连接中”的首要检查点。4.2 RTP 接收与解码如何从 interleaved channel 0 读取 RTSP 控制信令RTSP over TCP 要求将控制信令PAUSE/TEARDOWN与媒体数据RTP复用同一 TCP 连接通过interleaved字段区分。sink 端 TCP socket 接收后需解析// rtsp/rtsp_transport.c void tcp_interleaved_read(int sock, uint8_t *buf, int len) { int pos 0; while (pos len) { int n recv(sock, buf pos, len - pos, 0); if (n 0) break; pos n; } // 解析$channelsizedata // channel 0 RTSP, channel 1 RTP for (int i 0; i pos; ) { if (buf[i] $) { uint8_t channel buf[i1]; uint16_t size (buf[i2] 8) | buf[i3]; if (channel 0) { // RTSP control message: parse buf[i4] to buf[i4size] parse_rtsp_control(buf i 4, size); } else if (channel 1) { // RTP packet: pass to decoder rtp_decode(buf i 4, size); } i 4 size; } else { i; } } }$字符是 RTSP/TCP 的帧界定符channel字段决定后续size字节的用途。漏掉此解析逻辑会导致PAUSE请求被当成 RTP 包丢弃无法暂停播放。4.3 显示输出DRM/KMS 直驱 vs OpenGL ES 渲染的选型权衡Sink 端显示有两种路径DRM/KMS 直驱绕过 X11/Wayland直接向 GPU framebuffer 写入 YUV420P 数据。优势是零拷贝、低延迟实测 80ms但要求 kernel ≥ 4.15 且 DRM 驱动支持DRM_FORMAT_YUV420。代码在display/drm_display.c中调用drmModeAddFB2()创建 framebuffermemcpy()将解码后的 Y/U/V 平面写入。OpenGL ES 渲染兼容性更好Android/Linux 通用但需glTexImage2D()上传纹理增加一次内存拷贝。display/gles_display.c中使用GL_TEXTURE_EXTERNAL_OES绑定 YUV 纹理glDrawArrays()渲染。选择依据若目标硬件是树莓派 4VC4 DRM 驱动完善选 DRM若是通用 x86 主板Intel i915 驱动对 YUV 支持不稳定选 GLES。配置开关在CMakeLists.txt中option(ENABLE_DRM_DISPLAY Use DRM/KMS for display ON) if(ENABLE_DRM_DISPLAY) target_compile_definitions(wfd_sink PRIVATE USE_DRM) target_link_libraries(wfd_sink PRIVATE drm) else() target_compile_definitions(wfd_sink PRIVATE USE_GLES) target_link_libraries(wfd_sink PRIVATE GLESv2) endif()5. 避坑指南Miracast 协议栈开发中踩过的五个血泪坑5.1 现象wpa_cli p2p_find无响应dmesg显示p2p: failed to set device address原因Wi-Fi 芯片驱动未正确初始化 P2P 功能。常见于 Realtek RTL8188EU 芯片其开源驱动rtl8188eu-aircrack-ng默认禁用 P2P。解决重新编译驱动修改core/rtw_p2p.c中rtw_p2p_enable()函数将p2p_enabled _TRUE;硬编码或更换为rtl8188fu驱动官方支持 P2P。5.2 现象Sink 端收到DESCRIBE请求但不回复tcpdump显示 TCP SYN 未 ACK原因wfd_sink进程未绑定239.255.255.250:1900组播地址或防火墙拦截。解决检查wfd_sdp.c中setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, mreq, sizeof(mreq))是否成功执行sudo ufw allow 1900/udp用netstat -tuln | grep :1900确认端口监听。5.3 现象Source 端SETUP成功但PLAY后无 RTP 包发出wireshark显示RTP流为空原因rtp_packetizer.c中rtp_send_packet()调用sendto()时目标 IP 地址错误。Miracast 要求 RTP 发往 sink 的 P2P 接口 IP如192.168.49.1而非localhost。解决在p2p_manager.c的p2p_connect()成功后调用get_group_info()解析group_interface字段提取 IP 并存入全局变量g_sink_iprtp_send_packet()中使用该 IP。5.4 现象Sink 端解码器输出绿屏或花屏ffplay -v debug显示missing picture in access unit原因H.264 编码器未开启b_repeat_headers1导致后续 IDR 帧缺失 SPS/PPS。解决检查encoder_x264.c中param.b_repeat_headers是否为1用hexdump -C output.h264 | head -20确认每个 IDR 帧00 00 00 01 65前是否有00 00 00 01 67SPS和00 00 00 01 68PPS。5.5 现象投屏画面卡顿top显示wfd_sourceCPU 占用 100%但v4l2采集帧率正常原因libx264编码线程数设为0自动在多核 CPU 上引发线程竞争实际编码吞吐不足。解决强制设param.i_threads 1并关闭param.b_sliced_threads 0或改用param.i_sync_lookahead 0降低延迟。实测单线程编码 1280×72030fps 在 i5-8250U 上 CPU 占用稳定在 45%。6. 进阶技巧用tcpdumpWireshark解密 Miracast 协议握手全过程6.1 抓包过滤规则精准捕获 Miracast 四类关键流量Miracast 流量分散在多个协议层需组合过滤才能还原全貌。在 source 和 sink 设备上分别执行# 在 source 端发起投屏抓包 sudo tcpdump -i wlan0 -w source.pcap \ udp port 1900 or tcp port 7236 or udp portrange 50000-50100 or icmp # 在 sink 端接收投屏抓包 sudo tcpdump -i wlan0 -w sink.pcap \ udp port 1900 or tcp port 7236 or udp portrange 50000-50100关键点udp port 1900SSDP 发现流量M-SEARCH/NOTIFYtcp port 7236RTSP 控制信令端口Miracast 规范强制指定udp portrange 50000-50100RTP 媒体端口范围DESCRIBE响应中acontrol:rtsp://.../trackID1后的port字段icmp用于检测 P2P 链路连通性可选。提示若设备只有单网卡wlan0需替换为实际接口名ip link show查看若使用wpa_supplicant的 P2P group interface如p2p-wlan0-0则抓该接口。6.2 Wireshark 解析技巧让 RTSP/RTP 流量“开口说话”默认 Wireshark 不识别 Miracast 自定义端口需手动指定协议解析打开source.pcap→ 右键任意TCP包 →Decode As...→Transport标签页 → 选中7236端口 →Protocol下拉选RTSP同理对UDP流量 →Decode As...→7236端口 →RTSP对 RTP 流量如50001端口→Decode As...→UDP→RTP→ 点击RTP设置按钮 →Payload type选Dynamic→H.264关键操作勾选Analyze RTP streams菜单Telephony → RTP → RTP Streams即可看到所有 RTP 流的统计信息丢包率、Jitter、帧率。此时展开RTSP流你能清晰看到DESCRIBE请求 →200 OK响应中Content-Type: application/sdp及完整的 SDPSETUP请求中的Transport: RTP/AVP/TCP;unicast;interleaved0-1PLAY请求中的Range: npt0.000-RTP流中每个包的Timestamp字段右键 →Prepare as Filter → Selected Field可快速筛选。6.3 故障定位三板斧从 Wireshark 中快速定位协议层问题现象Wireshark 中检查点定位逻辑设备搜不到source.pcap中无M-SEARCHsink.pcap中无NOTIFY检查p2p_find是否执行wfd_sdp.c中sendto()是否成功发送组播用ping 239.255.255.250验证组播路由卡在“正在连接”source.pcap有DESCRIBE但无200 OKsink.pcap有DESCRIBE但无响应检查rtsp_server.c中handle_rtsp_request()是否进入DESCRIBE分支send_sdp()是否调用write()成功netstat -tuln有画面但卡顿/花屏RTP流中Timestamp差值不恒定如忽大忽小Packet Loss 5%检查rtp_timestamp.c中时间戳增量是否严格按90000/framerate计算rtp_packetizer.c中 FU-A 分片是否遗漏E1标志我从某跨平台系统项目开始每次新接入一款 Wi-Fi 芯片必先用这套tcpdump Wireshark 流程跑一遍先确认 SSDP 通再盯 RTSP 四步握手最后压 RTP 时间戳。三年下来没再因为“协议玄学”加班到凌晨。现在我的习惯是只要wfd_source启动第一件事就是sudo tcpdump -i wlan0 -c 1000 -w debug.pcap tcp port 7236抓 1000 个包拖进 Wireshark 看一眼DESCRIBE响应——如果 SDP 里aframerate:和acontrol:都对那后面八成是媒体层的事跟协议栈无关。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑