版权声明:本文为博主原创文章,未经博主允许不得转载。
这一章中我们来看Wifi Display连接过程的建立,包含P2P的部分和RTSP的部分,首先来大致看一下Wifi Display规范相关的东西。
HIDC: Human Interface Device Class (遵循HID标准的设备类)
UIBC: User Input Back Channel (UIBC分为两种,一种是Generic,包含鼠标、键盘等;另一种是HIDC,HID是一个规范,只有遵循HID的标准,都可以叫做HID设备,包含USB鼠标、键盘、蓝牙、红外等)
PES: Packetized Elementary Stream (数字电视基本码流)
HDCP: High-bandwidth Digital Content Protection (加密方式,用于加密传输的MPEG2-TS流)
MPEG2-TS: Moving Picture Experts Group 2 Transport Stream (Wifi display之间传输的是MPEG2-TS流)
RTSP: Real-Time Streaming Protocol (Wifi display通过RTSP协议来交互两边的能力)
RTP: Real-time Transport Protocol (Wifi display通过RTP来传输MPEG2-TS流)
Wi-Fi P2P: Wi-Fi Direct
TDLS: Tunneled Direct Link Setup (另一种方式建立两台设备之间的直连,与P2P类似,但要借助一台AP)
另一种比较重要的概念是在Wifi Display中分为Source和Sink两种角色,如下图。Source是用于encode并输出TS流;Sink用于decode并显示TS流。相当于Server/Client架构中,Source就是Server,用于提供服务;Sink就是Client。当然,我们这篇文章主要介绍在Android上Wifi display Source的流程。
从上面的架构图我们可以看到,Wifi display是建立在TCP/UDP上面的应用层协议,L2链路层是通过P2P和TDLS两种方式建立,TDLS是optional的。在L2层建立连接后,Source就会在一个特定的port上listen,等待client的TCP连接。当与Client建立了TCP连接后,就会有M1~M7七个消息的交互,用户获取对方设备的能力,包括视频编码能力、Audio输出能力、是否支持HDCP加密等等。在获取这些能力之后,Source就会选择一种视频编码格式以及Audio格式用于这次会话当中。当一个RTSP会话建立后,双方就会决定出用于传输TS流的RTP port,RTP协议是基于UDP的。当这些都准备好后,Sink设备就会发送M7消息,也就是Play给Source,双方就可以开始传输数据了。
关于M1~M7是什么,我们后面再来介绍。首先我们来介绍在Android WifiDisplay中如何建立P2P的连接。
WifiDisplay之P2P的建立
通过我们之间关于Wifi display的service启动以及enable的分析,我们知道当扫描到可用的设备后,就会显示在WifiDisplaySettings这个页面上,当我们选择其中一个后,就会开始P2P的建立了,首先到WifiDisplaySettings中的代码分析:
[java] view plain copy
- private void pairWifiDisplay(WifiDisplay display) {
- if (display.canConnect()) {
- mDisplayManager.connectWifiDisplay(display.getDeviceAddress());
- }
- }
WifiDisplaySettings通过AIDL调用到DisplayManagerService的connectWifiDisplay方法,关于AIDL的调用过程这里不讲了,直接到DisplayManagerService的connectWifiDisplay方法来看:
[java] view plain copy
- public void connectWifiDisplay(String address) {
- if (address == null) {
- throw new IllegalArgumentException("address must not be null");
- }
- mContext.enforceCallingOrSelfPermission(Manifest.permission.CONFIGURE_WIFI_DISPLAY,
- "Permission required to connect to a wifi display");
- final long token = Binder.clearCallingIdentity();
- try {
- synchronized (mSyncRoot) {
- if (mWifiDisplayAdapter != null) {
- mWifiDisplayAdapter.requestConnectLocked(address);
- }
- }
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
首先做参数的检查,即MAC地址不能为空,然后做权限检查,调用这个方法的application必须要在manifest中声明有CONFIGURE_WIFI_DISPLAY权限,最后直接调用WifiDisplayAdapter的requestConnectLocked方法:
[java] view plain copy
- public void requestConnectLocked(final String address) {
- if (DEBUG) {
- Slog.d(TAG, "requestConnectLocked: address=" + address);
- }
- getHandler().post(new Runnable() {
- @Override
- public void run() {
- if (mDisplayController != null) {
- mDisplayController.requestConnect(address);
- }
- }
- });
- }
这里比较简单,直接调用WifiDisplayController的requestConnect方法。前面都是直接的调用,最终做事情的还是WifiDisplayController。
[java] view plain copy
- public void requestConnect(String address) {
- for (WifiP2pDevice device : mAvailableWifiDisplayPeers) {
- if (device.deviceAddress.equals(address)) {
- connect(device);
- }
- }
- }
- private void connect(final WifiP2pDevice device) {
- if (mDesiredDevice != null
- && !mDesiredDevice.deviceAddress.equals(device.deviceAddress)) {
- if (DEBUG) {
- Slog.d(TAG, "connect: nothing to do, already connecting to "
- + describeWifiP2pDevice(device));
- }
- return;
- }
- if (mConnectedDevice != null
- && !mConnectedDevice.deviceAddress.equals(device.deviceAddress)
- && mDesiredDevice == null) {
- if (DEBUG) {
- Slog.d(TAG, "connect: nothing to do, already connected to "
- + describeWifiP2pDevice(device) + " and not part way through "
- + "connecting to a different device.");
- }
- return;
- }
- if (!mWfdEnabled) {
- Slog.i(TAG, "Ignoring request to connect to Wifi display because the "
- +" feature is currently disabled: " + device.deviceName);
- return;
- }
- mDesiredDevice = device;
- mConnectionRetriesLeft = CONNECT_MAX_RETRIES;
- updateConnection();
- }
requestConnect先从mAvaiableWifiDsiplayPeers中通过Mac地址找到所有连接的WifiP2pDevice,然后调用connect方法,在connect方法中会做一系列的判断,看首先是否有正在连接中或者断开中的设备,如果有就直接返回;再看有没有已经连接上的设备,如果有,也直接返回,然后赋值mDesiredDevice为这次要连接的设备,最后调用updateConnection来更新连接状态并发起连接。updateConnection的代码比较长,我们分段来分析:
[java] view plain copy
- private void updateConnection() {
- n style="white-space:pre"> </span>//更新是否需要scan或者停止scan
- updateScanState();
- n style="white-space:pre"> </span>//如果有已经连接上的RemoteDisplay,先断开。这里先不看
- if (mRemoteDisplay != null && mConnectedDevice != mDesiredDevice) {
- }
- // 接上面的一步,段开这个group
- if (mDisconnectingDevice != null) {
- return; // wait for asynchronous callback
- }
- if (mConnectedDevice != null && mConnectedDevice != mDesiredDevice) {
- }
- // 如果有正在连接的设备,先停止连接之前的设备
- if (mCancelingDevice != null) {
- return; // wait for asynchronous callback
- }
- if (mConnectingDevice != null && mConnectingDevice != mDesiredDevice) {
- }
- // 当断开之前的连接或者启动匿名GROUP时,这里就结束了
- if (mDesiredDevice == null) {
- }
- // 开始连接,这是我们要看的重点
- if (mConnectedDevice == null && mConnectingDevice == null) {
- Slog.i(TAG, "Connecting to Wifi display: " + mDesiredDevice.deviceName);
- mConnectingDevice = mDesiredDevice;
- WifiP2pConfig config = new WifiP2pConfig();
- WpsInfo wps = new WpsInfo();
- if (mWifiDisplayWpsConfig != WpsInfo.INVALID) {
- wps.setup = mWifiDisplayWpsConfig;
- } else if (mConnectingDevice.wpsPbcSupported()) {
- wps.setup = WpsInfo.PBC;
- } else if (mConnectingDevice.wpsDisplaySupported()) {
- wps.setup = WpsInfo.KEYPAD;
- } else {
- wps.setup = WpsInfo.DISPLAY;
- }
- config.wps = wps;
- config.deviceAddress = mConnectingDevice.deviceAddress;
- config.groupOwnerIntent = WifiP2pConfig.MIN_GROUP_OWNER_INTENT;
- WifiDisplay display = createWifiDisplay(mConnectingDevice);
- advertiseDisplay(display, null, 0, 0, 0);
- final WifiP2pDevice newDevice = mDesiredDevice;
- mWifiP2pManager.connect(mWifiP2pChannel, config, new ActionListener() {
- @Override
- public void onSuccess() {
- Slog.i(TAG, "Initiated connection to Wifi display: " + newDevice.deviceName);
- mHandler.postDelayed(mConnectionTimeout, CONNECTION_TIMEOUT_SECONDS * 1000);
- }
- @Override
- public void onFailure(int reason) {
- if (mConnectingDevice == newDevice) {
- Slog.i(TAG, "Failed to initiate connection to Wifi display: "
- + newDevice.deviceName + ", reason=" + reason);
- mConnectingDevice = null;
- handleConnectionFailure(false);
- }
- }
- });
- return;
- }<span style="font-family: Arial, Helvetica, sans-serif;"> </span>
这段函数比较长,我们先看我们需要的,剩下的后面再来分析。首先赋值给mConnectingDevice表示当前正在连接的设备,然后构造一个WifiP2pConfig对象,这个对象包含这次连接的设备的Mac地址、wps方式以及我们自己的GROUP_OWNER intent值,然后调用advertieseDisplay方法来通知WifiDisplayAdapter相关状态的改变,WifiDisplayAdapter会发送相应的broadcast出来,这是WifiDisplaySettings可以接收这些broadcast,然后在UI上更新相应的状态。关于advertieseDisplay的实现,我们后面再来分析。
接着看updateConnection,调用WifiP2pManager的connect方法去实现两台设备的P2P连接,具体过程可以参考前面介绍的P2P连接的文章。这里的onSuccess()并不是表示P2P已经建立成功,而只是表示这个发送命令到wpa_supplicant成功,所以在这里设置了一个连接超时的timeout,为30秒。当连接成功后,会发送WIFI_P2P_CONNECTION_CHANGED_ACTION的广播出来,接着回到WifiDisplayController看如何处理连接成功的broadcast:
[java] view plain copy
- } else if (action.equals(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)) {
- NetworkInfo networkInfo = (NetworkInfo)intent.getParcelableExtra(
- WifiP2pManager.EXTRA_NETWORK_INFO);
- if (DEBUG) {
- Slog.d(TAG, "Received WIFI_P2P_CONNECTION_CHANGED_ACTION: networkInfo="
- + networkInfo);
- }
- handleConnectionChanged(networkInfo);
- private void handleConnectionChanged(NetworkInfo networkInfo) {
- mNetworkInfo = networkInfo;
- if (mWfdEnabled && networkInfo.isConnected()) {
- if (mDesiredDevice != null || mWifiDisplayCertMode) {
- mWifiP2pManager.requestGroupInfo(mWifiP2pChannel, new GroupInfoListener() {
- @Override
- public void onGroupInfoAvailable(WifiP2pGroup info) {
- if (DEBUG) {