当前位置:网站首页>Miracast技术详解(一):Wi-Fi Display
Miracast技术详解(一):Wi-Fi Display
2022-07-05 07:21:00 【放大的EZ】
Miracast概述
Miracast
Miracast是由Wi-Fi联盟于2012年所制定,以Wi-Fi直连(Wi-Fi Direct)为基础的无线显示标准。支持此标准的消费性电子产品(又称3C设备)可透过无线方式分享视频画面,例如手机可透过Miracast将影片或照片直接在电视或其他设备播放而无需任何连接线,也不需透过无线热点(AP,Access Point)。
Wi-Fi Direct
Wi-Fi直连(英语:Wi-Fi Direct),之前曾被称为Wi-Fi点对点(Wi-Fi Peer-to-Peer),是一套无线网络互连协议,让wifi设备可以不必透过无线网络接入点(Access Point),以点对点的方式,直接与另一个wifi设备连线,进行高速数据传输。这个协议由Wi-Fi联盟发展、支持与授与认证,通过认证的产品将可获得Wi-Fi CERTIFIED Wi-Fi Direct标志。
Wi-Fi Display
Wi-Fi Display是Wi-Fi联盟制定的一个标准协议,它结合了Wi-Fi标准和H.264视频编码技术。利用这种技术,消费者可以从一个移动设备将音视频内容实时镜像到大型屏幕,随时、随地、在各种设备之间可靠地传输和观看内容。
Miracast实际上就是Wi-Fi联盟对支持WiFi Display功能的设备的认证名称,产品通过认证后会打上Miracast标签。
Sink & Source
如下图所示,Miracast可分为发送端与接收端。Source端为Miracast音视频数据发送端,负责音视频数据的采集、编码及发送。而Sink端为Miracast业务的接收端,负责接收Source端的音视频码流并解码显示,其中通过Wi-Fi Direct技术进行连接。
/** * A class representing Wifi Display information for a device * @hide */ public class WifiP2pWfdInfo implements Parcelable { ... public WifiP2pWfdInfo() { } public WifiP2pWfdInfo(int devInfo, int ctrlPort, int maxTput) { mWfdEnabled = true; mDeviceInfo = devInfo; mCtrlPort = ctrlPort; mMaxThroughput = maxTput; } ... }
对于setWFDInfo()
方法,我们可以采用反射的方式进行调用:
private void setWFDInfoInner(WifiP2pWfdInfo wfdInfo) { try { Method method = ReflectUtil.getPrivateMethod(mManager.getClass(), "setWFDInfo", WifiP2pManager.Channel.class, WifiP2pWfdInfo.class, WifiP2pManager.ActionListener.class); method.invoke(mManager, mChannel, wfdInfo, new WifiP2pManager.ActionListener() { @Override public void onSuccess() { Log.d(TAG, "setWFDInfo onSuccess:"); } @Override public void onFailure(int reason) { Log.e(TAG, "setWFDInfo onFailure:" + reason); } }); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } }
初始化WifiP2pWfdInfo
,并进行如下设置,重要的几个字段详见注释:
public void setWfdInfo() { final WifiP2pWfdInfo wfdInfo = new WifiP2pWfdInfo(); // 开启WiFi Display wfdInfo.setWfdEnabled(true); wfdInfo.setSessionAvailable(true); wfdInfo.setCoupledSinkSupportAtSink(false); wfdInfo.setCoupledSinkSupportAtSource(false); // 设置设备模式为SINK端(Miracast接收端) wfdInfo.setDeviceType(WifiP2pWfdInfo.PRIMARY_SINK); wfdInfo.setControlPort(WFD_DEFAULT_PORT); wfdInfo.setMaxThroughput(WFD_MAX_THROUGHPUT); setWFDInfoInner(wfdInfo); }
若希望关闭WiFi Display模式,则直接setWfdEnabled(false)
即可:
public void clearWfdInfo() { final WifiP2pWfdInfo wfdInfo = new WifiP2pWfdInfo(); wfdInfo.setWfdEnabled(false); setWFDInfoInner(wfdInfo); }
完成上述步骤后,在发送端的投屏或者投射功能中,应该能搜索到对应的Miracast设备了。
Wi-Fi P2P 连接
在发送端搜索到Miracast设备,并点击对应设备后,就进入到了连接过程。此时Sink端应该会弹出一个[连接邀请]的授权窗口,可以选择拒绝或者接受。选择接受后,若是第一次连接,则会进入到GO协商的过程。
GO协商(Group Owner Negotiation)
GO协商是一个复杂的过程,共包含三个类型的Action帧:GO Req、GO Resp、GO Confirm,经过这几个帧的交互最终确认是Sink端还是Source端作为Group Owner,因此谁做GO是不确定的。那具体的协商规则是怎样的呢?官方的流程图清晰地给出了答案:
首先通过Group Owner Intent
的值进行协商,值大者为GO。若Intent值相同就需要判断Req帧中Tie breaker
位,置1者为GO。若2台设备都设置了Intent为最大值,都希望能成为GO,则这次协商失败。
那么,如何设置这个Intent值呢?发送端在connect()
的时候,可通过groupOwnerIntent
字段设置GO的优先级的(范围从0-15,0表示最小优先级),方法如下:
WifiP2pConfig config = new WifiP2pConfig(); ... config.groupOwnerIntent = 15; // I want this device to become the owner mManager.connect(mChannel, config, actionListener);
PS: 对GO完整协商过程感兴趣的童鞋可以查看Wi-Fi P2P Technical Specification
文档的3.1.4.2 Group Owner Negotiation
这章
Miracast Sink端的场景为接收端,因此不能通过groupOwnerIntent
字段来设置GO优先级。那么还有其他方式可以让Sink端成为GO吗?毕竟在多台设备通过Miracast投屏的时候,Sink端是必须作为GO才能实现的。答案其实也很简单,就是自己创建一个组,自己成为GO,让其他Client加进来,在连接前直接调用createGroup()
方法即可完成建组操作:
mManager.createGroup(mChannel, new WifiP2pManager.ActionListener() { @Override public void onSuccess() { Log.d(TAG, "createGroup onSuccess"); } @Override public void onFailure(int reason) { Log.d(TAG, "createGroup onFailure:" + reason); } });
建组成功后我们可以通过requestGroupInfo()
方法来查看组的基本信息,以及组内Client的情况:
mManager.requestGroupInfo(mChannel, wifiP2pGroup -> { Log.d(TAG, "onGroupInfoAvailable detail:\n" + wifiP2pGroup.toString()); Collection<WifiP2pDevice> clientList = wifiP2pGroup.getClientList(); if (clientList != null) { int size = clientList.size(); Log.d(TAG, "onGroupInfoAvailable - client count:" + size); // Handle all p2p client devices } });
GO协商完毕,并且Wi-Fi Direct
连接成功的时候,我们将会收到WIFI_P2P_CONNECTION_CHANGED_ACTION
这个广播,此时我们可以调用 requestConnectionInfo()
,并在onConnectionInfoAvailable()
回调中通过isGroupOwner
字段来判断当前设备是Group Owner,还是Peer。通过groupOwnerAddress
,我们可以很方便的获取到Group Owner的IP地址。
@Override public void onConnectionInfoAvailable(WifiP2pInfo wifiP2pInfo) { if (wifiP2pInfo.groupFormed && wifiP2pInfo.isGroupOwner) { Log.d(TAG, "is groupOwner: "); } else if (wifiP2pInfo.groupFormed) { Log.d(TAG, "is peer: "); } String ownerIP = wifiP2pInfo.groupOwnerAddress.getHostAddress(); Log.d(TAG, "onConnectionInfoAvailable ownerIP = " + ownerIP); }
受WiFi P2P API的限制,各设备获取到的MAC和IP地址情况如下图所示:
由于在后续RTSP进行指令通讯的时候,需要通过Socket与Source端建立连接,也就是我们需要先知道Source端的IP地址与端口。根据上图,我们可能出现以下2种情况:
- 情况1:Sink端为Peer,Source端为GO。
这种情况下,Sink端知道Source端(GO)的IP地址,可以直接进行Socket连接。
- 情况2:Sink端为GO,Source端为Peer。
这种情况下,Sink端只知道自己(GO)的IP地址,不知道Source端(Peer)的IP地址,但此时能获取到MAC地址。
通过ARP协议获取对应MAC设备的IP地址
针对上述情况2,我们需要通过MAC地址获取到对应主机的IP地址,以完成与Source端的Socket连接,比较经典的方案是采用解析ARP缓存表的形式进行。
ARP(Address Resolution Protocol),即地址解析协议,是根据IP地址获取物理地址的一个TCP/IP协议。主机发送信息时将包含目标IP地址的ARP请求广播到局域网络上的所有主机,并接收返回消息,以此确定目标的物理地址;收到返回消息后将该IP地址和物理地址存入本机ARP缓存中并保留一定时间,下次请求时直接查询ARP缓存以节约资源。
在Android上,我们可以通过以下指令获取ARP缓存表:
方法1:通过busybox arp指令
dior:/ $ busybox arp ? (192.168.0.108) at f8:ff:c2:10:e7:62 [ether] on wlan0 ? (192.168.0.1) at 9c:a6:15:d6:e8:f4 [ether] on wlan0
方法2:通过cat proc/net/arp命令
dior:/ $ cat proc/net/arp
IP address HW type Flags HW address Mask Device
192.168.0.108 0x1 0x2 f8:ff:c2:10:e7:62 * wlan0
192.168.0.1 0x1 0x2 9c:a6:15:d6:e8:f4 * wlan0
剩下的工作就是采用强大的正则表达式解析返回的字符串,并查找出对应MAC设备的IP地址了。
获取Source端RTSP端口号
经过上面的步骤,我们已经拿到了Source端的IP地址,只剩下端口号了。这一步就比较简单了,通过requestPeers()
方法获取已连接的对等设备WifiP2pDevice
,再获取其中的WifiP2pWfdInfo
即可拿到端口号:
mManager.requestPeers(mChannel, peers -> { Collection<WifiP2pDevice> devices = peers.getDeviceList(); for (WifiP2pDevice device : devices) { boolean isConnected = (WifiP2pDevice.CONNECTED == device.status); if (isConnected) { int port = getDevicePort(device); break; } } });
这里由于WifiP2pDevice
中的wfdInfo
字段为@hide
,因此需要通过反射的方式获取WifiP2pWfdInfo
。最后通过getControlPort()
方法即可拿到Source端RTSP端口号:
public int getDevicePort(WifiP2pDevice device) { int port = WFD_DEFAULT_PORT; try { Field field = ReflectUtil.getPrivateField(device.getClass(), "wfdInfo"); if (field == null) { return port; } WifiP2pWfdInfo wfdInfo = (WifiP2pWfdInfo) field.get(device); if (wfdInfo != null) { port = wfdInfo.getControlPort(); if (port == 0) { Log.w(TAG,"set port to WFD_DEFAULT_PORT"); port = WFD_DEFAULT_PORT; } } } catch (IllegalAccessException e) { e.printStackTrace(); } return port; }
拿到了Source端的IP地址与端口号后,我们就可以建立RTSP连接,建立后续控制指令的通道了,详见下篇博客。
参考
- https://developer.android.com/guide/topics/connectivity/wifip2p
- https://developer.android.com/training/connect-devices-wirelessly/wifi-direct
文章转载:
边栏推荐
- Ros2 - first acquaintance with ros2 (I)
- Rough notes of C language (1)
- Brief description of inux camera (Mipi interface)
- 并发编程 — 如何中断/停止一个运行中的线程?
- golang定时器使用踩的坑:定时器每天执行一次
- Basic series of SHEL script (I) variables
- Unconventional ending disconnected from the target VM, address: '127.0.0.1:62635', transport: 'socket‘
- What does soda ash do?
- Qu'est - ce que l'hydroxyde de sodium?
- GPIO port bit based on Cortex-M3 and M4 with operation macro definition (can be used for bus input and output, STM32, aducm4050, etc.)
猜你喜欢
Powermanagerservice (I) - initialization
CADD课程学习(5)-- 构建靶点已知的化合结构(ChemDraw)
arcgis_ spatialjoin
PowerManagerService(一)— 初始化
Idea to view the source code of jar package and some shortcut keys (necessary for reading the source code)
Rough notes of C language (2) -- constants
【软件测试】04 -- 软件测试与软件开发
Inftnews | drink tea and send virtual stocks? Analysis of Naixue's tea "coin issuance"
Do you choose pandas or SQL for the top 1 of data analysis in your mind?
一文揭开,测试外包公司的真实情况
随机推荐
Netease to B, soft outside, hard in
Matrix and TMB package version issues in R
ModuleNotFoundError: No module named ‘picamera‘
SD_ CMD_ RECEIVE_ SHIFT_ REGISTER
Chapter 2: try to implement a simple bean container
CADD课程学习(6)-- 获得已有的虚拟化合物库(Drugbank、ZINC)
[tf1] save and load parameters
Jenkins reported an error. Illegal character: '\ufeff'. Class, interface or enum are required
2022 PMP project management examination agile knowledge points (7)
Ros2 - function package (VI)
目标检测系列——Faster R-CNN原理详解
When jupyter notebook is encountered, erroe appears in the name and is not output after running, but an empty line of code is added downward, and [] is empty
Executealways of unity is replacing executeineditmode
【obs】x264编码:“buffer_size“
Daily Practice:Codeforces Round #794 (Div. 2)(A~D)
Concurrent programming - deadlock troubleshooting and handling
Ros2 - common command line (IV)
What does soda ash do?
Powermanagerservice (I) - initialization
How to deal with excessive memory occupation of idea and Google browser