Miracast技术详解(一):Wi-Fi Display
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技术进行连接。
Android上Wi-Fi Direct的实现
上面的概述里面也说到,Miracast是基于Wi-Fi Direct技术来实现连接与数据传输。那么要实现Miracast技术,首先就得研究下Android平台下的Wi-Fi Direct技术。
Wi-Fi P2P 简介
Wi-Fi Direct(在Android平台上也称Wi-Fi P2P),可以让具备相应硬件的Android 4.0(API 级别 14)或更高版本设备在没有AP的情况下,通过WLAN进行直接互联,使用这些 API,可以实现支持 WiFi P2P 的设备间相互发现和连接,从而获得比蓝牙连接更远距离的高速连接通信效果。
为了实现一个基础的WiFiP2P,大致分为如下部分:
- 权限申请
- 初始化WiFiP2P的相关对象
- 定义监听WiFiP2P的广播接收器
- 连接设备
关于WiFiP2P中的群组,大致分为如下部分:
- 创建群组
- 连接群组
- 移除群组
- 权限申请
权限申请
首先,在AndroidManifest.xml中,对WiFi相关权限进行静态申请:
<uses-sdk android:minSdkVersion="14" />
<!-- WiFi相关权限 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
若后续还需要读写权限则添加:
<!-- 读写权限 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
然后,在 Android 6.0 及更高版本中,部分危险权限(Dangerous Permissions)权限需要在运行时请求用户批准(动态申请):
private void checkPermission() {
String[] permissions = new String[]{
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.ACCESS_FINE_LOCATION
};
for (String permission : permissions) {
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
Log.i(TAG, permission + " granted.");
} else {
ActivityCompat.requestPermissions(this, permissions, 0);
Log.w(TAG, permission + " not granted.");
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == 0) {
for (int result : grantResults) {
if (result == PackageManager.PERMISSION_GRANTED) {
continue;
} else {
Toast.makeText(this, "权限未获取", Toast.LENGTH_SHORT).show();
}
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
初始化
首先,需要创建:
- WifiP2pManager 对象
- WifiP2pManager.Channel 对象
- WiFiDirectBroadcastReceiver 对象(稍后介绍该广播接收器的定义)
- IntentFilter 对象
private WifiP2pManager mManager;
private WifiP2pManager.Channel mChannel;
private WiFiDirectBroadcastReceiver mReceiver;
private IntentFilter mIntentFilter;
private void initWifip2pHelper() {
// 创建 WifiP2pManager 对象
mManager = (WifiP2pManager) getSystemService(WIFI_P2P_SERVICE);
// 创建 WifiP2pManager.Channel 对象
mChannel = mManager.initialize(this, Looper.getMainLooper(), new WifiP2pManager.ChannelListener() {
@Override
public void onChannelDisconnected() {
Log.i(TAG, "onChannelDisconnected: ");
}
});
// 创建 WiFiDirectBroadcastReceiver 对象
mReceiver = new WiFiDirectBroadcastReceiver(mManager, mChannel, this);
// 创建 IntentFilter 对象
mIntentFilter = new IntentFilter();
mIntentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);
mIntentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);
mIntentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
mIntentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
mIntentFilter.addAction(WifiP2pManager.WIFI_P2P_DISCOVERY_CHANGED_ACTION);
}
其中, WiFiDirectBroadcastReceiver 对象想要监听的广播,与 IntentFilter 对象添加的action相同。
然后,在 Activity 的onResume()方法中注册广播接收器,在 Activity 的onPause()方法中取消注册该广播接收器:
/* register the broadcast receiver with the intent values to be matched */
@Override
protected void onResume() {
super.onResume();
registerReceiver(mReceiver, mIntentFilter);
}
/* unregister the broadcast receiver */
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(mReceiver);
}
定义监听WiFiP2P的广播接收器
监听WiFiP2P的广播接收器 WiFiDirectBroadcastReceiver 类具体定义如下:
public class WiFiDirectBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "WiFiDirectBroadcastReceiver";
private WifiP2pManager mManager;
private WifiP2pManager.Channel mChannel;
private Wifip2pActivity mActivity;
private List<WifiP2pDevice> mWifiP2pDeviceList = new ArrayList<>();
WifiP2pManager.PeerListListener mPeerListListener = new WifiP2pManager.PeerListListener() {
@Override
public void onPeersAvailable(WifiP2pDeviceList wifiP2pDeviceList) {
mWifiP2pDeviceList.clear();
mWifiP2pDeviceList.addAll(wifiP2pDeviceList.getDeviceList());
}
};
/**
* 构造方法
*
* @param manager WifiP2pManager对象
* @param channel WifiP2pManager.Channel对象
* @param activity Wifip2pActivity 对象
*/
public WiFiDirectBroadcastReceiver(WifiP2pManager manager, WifiP2pManager.Channel channel, Wifip2pActivity activity) {
super();
this.mManager = manager;
this.mChannel = channel;
this.mActivity = activity;
}
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION:
// Check to see if Wi-Fi is enabled and notify appropriate activity
Log.i(TAG, "onReceive: WIFI_P2P_STATE_CHANGED_ACTION");
int state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1);
if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) {
// Wifi P2P is enabled
Log.i(TAG, "onReceive: Wifi P2P is enabled");
mManager.discoverPeers(mChannel, new WifiP2pManager.ActionListener() {
@Override
public void onSuccess() {
Log.d(TAG, "onSuccess: ");
}
@Override
public void onFailure(int i) {
Log.d(TAG, "onFailure: ");
}
});
} else {
// Wi-Fi P2P is not enabled
Log.i(TAG, "onReceive: Wi-Fi P2P is not enabled");
}
break;
case WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION:
// Call WifiP2pManager.requestPeers() to get a list of current peers
Log.i(TAG, "onReceive: WIFI_P2P_PEERS_CHANGED_ACTION");
if (mManager == null) {
return;
}
mManager.requestPeers(mChannel, mPeerListListener);
break;
case WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION:
// Respond to new connection or disconnections
Log.i(TAG, "onReceive: WIFI_P2P_CONNECTION_CHANGED_ACTION");
// NetworkInfo
NetworkInfo networkInfo = intent.getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO);
// WifiP2pInfo
WifiP2pInfo wifiP2pInfo = intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO);
// WifiP2pGroup
WifiP2pGroup wifiP2pGroup = intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP);
if (networkInfo.isConnected()) {
if (wifiP2pInfo.isGroupOwner) {
Toast.makeText(mActivity, "设备连接,本设备为GO", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(mActivity, "设备连接,本设备非GO", Toast.LENGTH_LONG).show();
}
} else {
Toast.makeText(mActivity, "设备断开", Toast.LENGTH_LONG).show();
}
break;
case WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION:
// Respond to this device's wifi state changing
Log.i(TAG, "onReceive: WIFI_P2P_THIS_DEVICE_CHANGED_ACTION");
WifiP2pDevice device = intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_DEVICE);
Log.d(TAG, "onReceive: " +device.deviceAddress);
break;
case WifiP2pManager.WIFI_P2P_DISCOVERY_CHANGED_ACTION:
Log.i(TAG, "onReceive: WIFI_P2P_DISCOVERY_CHANGED_ACTION");
break;
}
}
}
上述广播接收器用于监听系统关于WiFiP2P相关的广播。通常在onReceive()方法中,通过intent.getAction()方法获取到action,并根据action去匹配不同的关于WiFiP2P相关的广播,分别为:
- WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION
- WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION
- WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION
- WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION
- WifiP2pManager.WIFI_P2P_DISCOVERY_CHANGED_ACTION
WIFI_P2P_STATE_CHANGED_ACTION:WiFiP2P状态发生改变时的广播
WiFiP2P具体有两个状态:
- WifiP2pManager.WIFI_P2P_STATE_ENABLED:可用
- WifiP2pManager.WIFI_P2P_STATE_DISABLED:不可用
而该状态的获取是由:
int state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1);
当WiFiP2P状态为可用时,调用discoverPeers()方法开始搜索附近WiFiP2P设备:
mManager.discoverPeers(mChannel, new WifiP2pManager.ActionListener() {
@Override
public void onSuccess() {
Log.d(TAG, "onSuccess: ");
}
@Override
public void onFailure(int i) {
Log.d(TAG, "onFailure: ");
}
});
WIFI_P2P_PEERS_CHANGED_ACTION:发现附近WiFiP2P设备时的广播
当搜索发现附近存在WiFiP2P设备时,调用requestPeers()方法开始获取附近WiFiP2P设备列表:
mManager.requestPeers(mChannel, mPeerListListener);
当成功获取附近WiFiP2P设备列表后,会回调侦听器 WifiP2pManager.PeerListListener 中的onPeersAvailable()方法,并传递一个 WifiP2pDeviceList 对象作为参数,可以用一个 List
WifiP2pManager.PeerListListener mPeerListListener = new WifiP2pManager.PeerListListener() {
@Override
public void onPeersAvailable(WifiP2pDeviceList wifiP2pDeviceList) {
mWifiP2pDeviceList.clear();
mWifiP2pDeviceList.addAll(wifiP2pDeviceList.getDeviceList());
}
};
WIFI_P2P_CONNECTION_CHANGED_ACTION:连接状态发生改变时的广播
当连接状态发生改变时(如连接了一个设备,断开了一个设备),都会接收到该广播。当接收到该广播后,可以使用intent.getParcelableExtra()方法分别获取到 NetworkInfo , WifiP2pInfo , WifiP2pGroup 对象:
// 获取 NetworkInfo 对象
NetworkInfo networkInfo = intent.getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO);
// 获取 WifiP2pInfo 对象
WifiP2pInfo wifiP2pInfo = intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO);
// 获取 WifiP2pGroup 对象
WifiP2pGroup wifiP2pGroup = intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP);
获取到上述对象后,可以使用networkInfo.isConnected()方法来判断连接状态具体是“设备连接”还是“设备断开”,还可以根据wifiP2pInfo.isGroupOwner的值来判断设备是否为GroupOwner:
if (networkInfo.isConnected()) {
if (wifiP2pInfo.isGroupOwner) {
Toast.makeText(mActivity, "设备连接,本设备为GroupOwner", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(mActivity, "设备连接,本设备非GroupOwner", Toast.LENGTH_LONG).show();
}
} else {
Toast.makeText(mActivity, "设备断开", Toast.LENGTH_LONG).show();
}
WIFI_P2P_THIS_DEVICE_CHANGED_ACTION:当前设备状态发生改变时的广播
通常可以在这个广播中获取到当前设备的信息:
WifiP2pDevice device = intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_DEVICE);
WifiP2pManager.WIFI_P2P_DISCOVERY_CHANGED_ACTION:搜索状态发生改变时的广播
启动搜索:
mManager.discoverPeers(mChannel, new WifiP2pManager.ActionListener() {
@Override
public void onSuccess() {
Log.d(TAG, "onSuccess: ");
}
@Override
public void onFailure(int i) {
Log.d(TAG, "onFailure: ");
}
});
停止搜索:
mManager.stopPeerDiscovery(mChannel, new WifiP2pManager.ActionListener() {
@Override
public void onSuccess() {
Log.d(TAG, "onSuccess: ");
}
@Override
public void onFailure(int i) {
Log.d(TAG, "onFailure: ");
}
});
连接设备
首先,选择一个需要连接的设备,并获取到该设备的 WifiP2pDevice 对象。然后,判断该设备的状态,设备状态通常为三种:
- WifiP2pDevice.AVAILABLE:可连接
- WifiP2pDevice.CONNECTED:已连接
- WifiP2pDevice.INVITED:已请求连接
根据不同的设备状态,进行不同的具体逻辑:
@Override
public void onClick(View view) {
// 获取到该设备的 WifiP2pDevice 对象
WifiP2pDevice wifiP2pDevice = mWifiP2pDeviceList.get(viewHolder.getAdapterPosition());
// 判断该设备的状态
switch (wifiP2pDevice.status) {
case WifiP2pDevice.AVAILABLE:
// 请求连接
WifiP2pConfig config = new WifiP2pConfig();
config.deviceAddress = wifiP2pDevice.deviceAddress;
mManager.connect(mChannel, config, new WifiP2pManager.ActionListener() {
@Override
public void onSuccess() {
Log.i(TAG, "connect success.");
}
@Override
public void onFailure(int i) {
Log.i(TAG, "connect failed.");
}
});
break;
case WifiP2pDevice.CONNECTED:
// 断开连接
mManager.removeGroup(mChannel, new WifiP2pManager.ActionListener() {
@Override
public void onSuccess() {
Log.i(TAG, "removeGroup success.");
}
@Override
public void onFailure(int i) {
Log.i(TAG, "removeGroup failed.");
}
});
break;
case WifiP2pDevice.INVITED:
// 关闭连接请求
mManager.cancelConnect(mChannel, new WifiP2pManager.ActionListener() {
@Override
public void onSuccess() {
Log.i(TAG, "cancelConnect success.");
}
@Override
public void onFailure(int i) {
Log.i(TAG, "cancelConnect failed.");
}
});
break;
}
}
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);
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连接,建立后续控制指令的通道了,详见下篇博客。
参考文档
WLAN 直连(对等连接或 P2P)概览
通过 Wi-Fi 直连创建点对点连接
Android WiFi P2P开发实践笔记
wifi直连(Android)Wifi-Direct
「Android」WiFiP2P入门