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. 情况1:Sink端为Peer,Source端为GO。

    这种情况下,Sink端知道Source端(GO)的IP地址,可以直接进行Socket连接。

  2. 情况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入门

Miracast技术详解(一):Wi-Fi Display的更多相关文章

  1. 架构设计:远程调用服务架构设计及zookeeper技术详解(下篇)

    一.下篇开头的废话 终于开写下篇了,这也是我写远程调用框架的第三篇文章,前两篇都被博客园作为[编辑推荐]的文章,很兴奋哦,嘿嘿~~~~,本人是个很臭美的人,一定得要截图为证: 今天是2014年的第一天 ...

  2. 「视频直播技术详解」系列之七:直播云 SDK 性能测试模型

    ​关于直播的技术文章不少,成体系的不多.我们将用七篇文章,更系统化地介绍当下大热的视频直播各环节的关键技术,帮助视频直播创业者们更全面.深入地了解视频直播技术,更好地技术选型. 本系列文章大纲如下: ...

  3. 手游录屏直播技术详解 | 直播 SDK 性能优化实践

    在上期<直播推流端弱网优化策略 >中,我们介绍了直播推流端是如何优化的.本期,将介绍手游直播中录屏的实现方式. 直播经过一年左右的快速发展,衍生出越来越丰富的业务形式,也覆盖越来越广的应用 ...

  4. 《CDN技术详解》 - CDN知多少?

    开发时间久了,就会接触到性能和并发方面的问题,如果说,在自己还是菜鸟的时候完全不用理会这种问题或者说有其他的高手去处理这类问题,那么,随着经验的丰富起来,自己必须要独立去处理了.或者,知道思路也行,毕 ...

  5. Comet技术详解:基于HTTP长连接的Web端实时通信技术

    前言 一般来说,Web端即时通讯技术因受限于浏览器的设计限制,一直以来实现起来并不容易,主流的Web端即时通讯方案大致有4种:传统Ajax短轮询.Comet技术.WebSocket技术.SSE(Ser ...

  6. SSE技术详解:一种全新的HTML5服务器推送事件技术

    前言 一般来说,Web端即时通讯技术因受限于浏览器的设计限制,一直以来实现起来并不容易,主流的Web端即时通讯方案大致有4种:传统Ajax短轮询.Comet技术.WebSocket技术.SSE(Ser ...

  7. Protocol Buffer技术详解(数据编码)

    Protocol Buffer技术详解(数据编码) 之前已经发了三篇有关Protocol Buffer的技术博客,其中第一篇介绍了Protocol Buffer的语言规范,而后两篇则分别基于C++和J ...

  8. Protocol Buffer技术详解(Java实例)

    Protocol Buffer技术详解(Java实例) 该篇Blog和上一篇(C++实例)基本相同,只是面向于我们团队中的Java工程师,毕竟我们项目的前端部分是基于Android开发的,而且我们研发 ...

  9. Protocol Buffer技术详解(C++实例)

    Protocol Buffer技术详解(C++实例) 这篇Blog仍然是以Google的官方文档为主线,代码实例则完全取自于我们正在开发的一个Demo项目,通过前一段时间的尝试,感觉这种结合的方式比较 ...

  10. Protocol Buffer技术详解(语言规范)

    Protocol Buffer技术详解(语言规范) 该系列Blog的内容主体主要源自于Protocol Buffer的官方文档,而代码示例则抽取于当前正在开发的一个公司内部项目的Demo.这样做的目的 ...

随机推荐

  1. 制作包含最新更新的Windows 10 LTSC 2021 ISO

    介绍 在制作桌面云windows 模板的时候,一般需要安装最新的更新.更新安装过程非常耗时,并且安装更新会导致桌面模板的磁盘空间膨胀.制作出的模板会占用很大的磁盘空间.如果不安装更新,模板大小约5G. ...

  2. XmlDocument 解决 Clone、CloneNode、ImportNode 等节点克隆后的标签自闭合问题

    前言: 这两天在对 Taurus.Mvc 做 html 加载性能优化时,发现存在这个问题. 具体优化的是 CYQ.Data 组件的 XHtmlAction 相关类. 问题过程: 之前 XmlDocum ...

  3. Delphi批量替换工具Cnpack

    操,delphi官方 没有 批量替换工具,需要用到cnpack才可以,

  4. Kettle如何连接SQL Server和问题处理

    简介 Kettle(也称为 Pentaho Data Integration)是一款开源的 ETL(Extract, Transform, Load)工具,由 Pentaho 开发.ETL 是指从一个 ...

  5. java 从零开始手写 RPC (06) reflect 反射实现通用调用之客户端

    通用调用 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 java 从零开始手写 RPC (03) 如何 ...

  6. OCR 02: Tesseract-OCR

    Catalog OCR 01: EasyOCR OCR 02: Tesseract-OCR OCR 03: PaddleOCR Project Host And Brief Official Site ...

  7. Java8接口中抽象方法和default和static方法的区别和使用

    Java接口说明 传统的理解是接口只能是抽象方法.但是程序员们在使用中,发现很不方便,实现接口必须重写所有方法,很麻烦.所以java设计者妥协了,在java8中,支持default和static方法, ...

  8. java使用Timer定时器在指定时间执行程序

    下面是一个利用Timer定时器在每天指定时间执行批处理程序的例子. 有关 java.util.Timer 详细知识请参考API. 值得注意的一点是Timer是单线程顺序执行多个任务的. package ...

  9. 【Android逆向】破解看雪 test1.apk

    1. 获取apk,并安装至手机 apk 获取地址: https://www.kanxue.com/work-task_read-800624.htm adb install -t test1.apk ...

  10. 项目实战:C#上位机+arduino下位机+控制点亮LED灯

    前言   当前比较流行的arduino开发,联动做一个Demo.   应用构架   上位机:C#上位机通过串口发送接收控制协议,来控制下位机:  下位机:arduino下位机主控,接受上位机串口协议控 ...