Android WifiDisplay分析二:Wifi display连接过程
Posted 魏长志
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android WifiDisplay分析二:Wifi display连接过程相关的知识,希望对你有一定的参考价值。
版权声明:本文为博主原创文章,未经博主允许不得转载。
这一章中我们来看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)
- Slog.d(TAG, "Received group info: " + describeWifiP2pGroup(info));
- if (mConnectingDevice != null && !info.contains(mConnectingDevice))
- Slog.i(TAG, "Aborting connection to Wifi display because "
- + "the current P2P group does not contain the device "
- + "we expected to find: " + mConnectingDevice.deviceName
- + ", group info was: " + describeWifiP2pGroup(info));
- handleConnectionFailure(false);
- return;
- if (mDesiredDevice != null && !info.contains(mDesiredDevice))
- disconnect();
- return;
- if (mConnectingDevice != null && mConnectingDevice == mDesiredDevice)
- Slog.i(TAG, "Connected to Wifi display: "
- + mConnectingDevice.deviceName);
- mHandler.removeCallbacks(mConnectionTimeout);
- mConnectedDeviceGroupInfo = info;
- mConnectedDevice = mConnectingDevice;
- mConnectingDevice = null;
- updateConnection();
- );
当WifiDisplayController收到WIFI_P2P_CONNECTION_CHANGED_ACTION广播后,会调用handleConnectionChanged来获取当前P2P Group相关的信息,如果获取到的P2P Group信息里面没有mConnectingDevice或者mDesiredDevice的信息,则表示连接出错了,直接退出。如果当前连接信息与前面设置的mConnectingDevice一直,则表示连接P2P成功,这里首先会移除前面设置的连接timeout的callback,然后设置mConnectedDevice为当前连接的设备,并设置mConnectingDevice为空,最后调用updateConnection来更新连接状态信息。我们又回到updateConnection这个函数了,但这次进入的分支与之前连接请求的分支又不同了,我们来看代码:
[java] view plain copy
- private void updateConnection()
- // 更新是否需要scan或者停止scan
- updateScanState();
- // 如果有连接上的RemoteDisplay,这里先断开
- if (mRemoteDisplay != null && mConnectedDevice != mDesiredDevice)
- // 接着上面的一步,先断开之前连接的设备
- 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)
- // 当断开之前的连接或者匿名GO时,这里就结束了
- if (mDesiredDevice == null)
- // 如果有连接请求,则进入此
- if (mConnectedDevice == null && mConnectingDevice == null)
- // 当连接上P2P后,就进入到此
- if (mConnectedDevice != null && mRemoteDisplay == null)
- Inet4Address addr = getInterfaceAddress(mConnectedDeviceGroupInfo);
- if (addr == null)
- Slog.i(TAG, "Failed to get local interface address for communicating "
- + "with Wifi display: " + mConnectedDevice.deviceName);
- handleConnectionFailure(false);
- return; // done
- mWifiP2pManager.setMiracastMode(WifiP2pManager.MIRACAST_SOURCE);
- final WifiP2pDevice oldDevice = mConnectedDevice;
- final int port = getPortNumber(mConnectedDevice);
- final String iface = addr.getHostAddress() + ":" + port;
- mRemoteDisplayInterface = iface;
- Slog.i(TAG, "Listening for RTSP connection on " + iface
- + " from Wifi display: " + mConnectedDevice.deviceName);
- mRemoteDisplay = RemoteDisplay.listen(iface, new RemoteDisplay.Listener()
- @Override
- public void onDisplayConnected(Surface surface,
- int width, int height, int flags, int session)
- if (mConnectedDevice == oldDevice && !mRemoteDisplayConnected)
- Slog.i(TAG, "Opened RTSP connection with Wifi display: "
- + mConnectedDevice.deviceName);
- mRemoteDisplayConnected = true;
- mHandler.removeCallbacks(mRtspTimeout);
- if (mWifiDisplayCertMode)
- mListener.onDisplaySessionInfo(
- getSessionInfo(mConnectedDeviceGroupInfo, session));
- final WifiDisplay display = createWifiDisplay(mConnectedDevice);
- advertiseDisplay(display, surface, width, height, flags);
- @Override
- public void onDisplayDisconnected()
- if (mConnectedDevice == oldDevice)
- Slog.i(TAG, "Closed RTSP connection with Wifi display: "
- + mConnectedDevice.deviceName);
- mHandler.removeCallbacks(mRtspTimeout);
- disconnect();
- @Override
- public void onDisplayError(int error)
- if (mConnectedDevice == oldDevice)
- Slog.i(TAG, "Lost RTSP connection with Wifi display due to error "
- + error + ": " + mConnectedDevice.deviceName);
- mHandler.removeCallbacks(mRtspTimeout);
- handleConnectionFailure(false);
- , mHandler);
- // Use extended timeout value for certification, as some tests require user inputs
- int rtspTimeout = mWifiDisplayCertMode ?
- RTSP_TIMEOUT_SECONDS_CERT_MODE : RTSP_TIMEOUT_SECONDS;
- mHandler.postDelayed(mRtspTimeout, rtspTimeout * 1000);
到这里P2P的连接就算建立成功了,接下来就是RTSP的部分了
WifiDisplay之RTSP server的创建
这里首先设置MiracastMode,博主认为这部分应该放在enable WifiDisplay时,不知道Google为什么放在这里? 然后从GroupInfo中取出对方设备的IP地址,利用默认的CONTROL PORT构建mRemoteDisplayInterface,接着调用RemoteDisplay的listen方法去listen指定的IP和端口上面的TCP连接请求。最后会设置Rtsp的连接请求的timeout,当用于Miracast认证时是120秒,正常的使用中是30秒,如果在这么长的时间内没有收到Sink的TCP请求,则表示失败了。下面来看RemoteDisplay的listen的实现:
[java] view plain copy
- public static RemoteDisplay listen(String iface, Listener listener, Handler handler)
- if (iface == null)
- throw new IllegalArgumentException("iface must not be null");
- if (listener == null)
- throw new IllegalArgumentException("listener must not be null");
- if (handler == null)
- throw new IllegalArgumentException("handler must not be null");
- RemoteDisplay display = new RemoteDisplay(listener, handler);
- display.startListening(iface);
- return display;
这里首先进行参数的检查,然后创建一个RemoteDisplay对象(这里不能直接创建RemoteDisplay对象,因为它的构造函数是private的),接着调用RemoteDisplay的startListening方法:
[java] view plain copy
- private void startListening(String iface)
- mPtr = nativeListen(iface);
- if (mPtr == 0)
- throw new IllegalStateException("Could not start listening for "
- + "remote display connection on \\"" + iface + "\\"");
- mGuard.open("dispose");
nativeListen会调用JNI中的实现,相关代码在android_media_RemoteDisplay.cpp中。注意上面的mGuard是CloseGuard对象,是一种用于显示释放一些资源的机制。
[java] view plain copy
- static jint nativeListen(JNIEnv* env, jobject remoteDisplayObj, jstring ifaceStr)
- ScopedUtfChars iface(env, ifaceStr);
- sp<IServiceManager> sm = defaultServiceManager();
- sp<IMediaPlayerService> service = interface_cast<IMediaPlayerService>(
- sm->getService(String16("media.player")));
- if (service == NULL)
- ALOGE("Could not obtain IMediaPlayerService from service manager");
- return 0;
- sp<NativeRemoteDisplayClient> client(new NativeRemoteDisplayClient(env, remoteDisplayObj));
- sp<IRemoteDisplay> display = service->listenForRemoteDisplay(
- client, String8(iface.c_str()));
- if (display == NULL)
- ALOGE("Media player service rejected request to listen for remote display '%s'.",
- iface.c_str());
- return 0;
- NativeRemoteDisplay* wrapper = new NativeRemoteDisplay(display, client);
- return reinterpret_cast<jint>(wrapper);
上面的代码中先从ServiceManager中获取MediaPlayerService的Bpbinder引用,然后由传入的第二个参数remoteDisplayObj,也就是RemoteDisplay对象构造一个NativeRemoteDisplayClient,在framework中,我们经常看到像这样的用法,类似于设计模式中的包装模式,例如在framework中对Java层的BnBinder也是做了一层封装JavaBBinder。在NativeRemoteDisplayClient中通过JNI的反向调用,就可以直接回调RemoteDisplay中的一些函数,实现回调方法了,下面来看它的实现:
[java] view plain copy
- class NativeRemoteDisplayClient : public BnRemoteDisplayClient
- public:
- NativeRemoteDisplayClient(JNIEnv* env, jobject remoteDisplayObj) :
- mRemoteDisplayObjGlobal(env->NewGlobalRef(remoteDisplayObj))
- protected:
- ~NativeRemoteDisplayClient()
- JNIEnv* env = AndroidRuntime::getJNIEnv();
- env->DeleteGlobalRef(mRemoteDisplayObjGlobal);
- public:
- virtual void onDisplayConnected(const sp<IGraphicBufferProducer>& bufferProducer,
- uint32_t width, uint32_t height, uint32_t flags, uint32_t session)
- env->CallVoidMethod(mRemoteDisplayObjGlobal,
- gRemoteDisplayClassInfo.notifyDisplayConnected,
- surfaceObj, width, height, flags, session);
- virtual void onDisplayDisconnected()
- virtual void onDisplayError(int32_t error)
- private:
- jobject mRemoteDisplayObjGlobal;
- static void checkAndClearExceptionFromCallback(JNIEnv* env, const char* methodName)
- ;
在NativeRemoteDisplayClient的构造函数中,把RemoteDisplay对象先保存到mRemoteDisplayObjGlobal中,可以看到上面主要实现了三个回调函数,onDisplayConnected、onDisplayDisconnected、onDisplayError,这三个回调函数对应到RemoteDisplay类的notifyDisplayConnected、notifyDisplayDisconnected和notifyDisplayError三个方法。接着回到nativeListen中,接着会调用MediaPlayerService的listenForRemoteDisplay方法去监听socket连接,这个方法是返回一个RemoteDisplay对象,当然经过binder的调用,最终返回到nativeListen的是BpRemoteDisplay对象,然后会由这个BpRemoteDisplay对象构造一个NativeRemoteDisplay对象并把它的指针地址返回给上层RemoteDisplay使用。
[java] view plain copy
- class NativeRemoteDisplay
- public:
- NativeRemoteDisplay(const sp<IRemoteDisplay>& display,
- const sp<NativeRemoteDisplayClient>& client) :
- mDisplay(display), mClient(client)
- ~NativeRemoteDisplay()
- mDisplay->dispose();
- void pause()
- mDisplay->pause();
- void resume()
- mDisplay->resume();
- private:
- sp<IRemoteDisplay> mDisplay;
- sp<NativeRemoteDisplayClient> mClient;
- ;
来看一下这时Java层的RemoteDisplay和Native层RemoteDisplay之间的关系:
WifiDisplayController通过左边的一条线路关系去控制WifiDisplaySource,而WifiDisplaySource又通过右边一条线路关系去回调WifiDisplayController的一些方法。
接着来看MediaPlayerService的listenForRemoteDisplay方法:
[java] view plain copy
- sp<IRemoteDisplay> MediaPlayerService::listenForRemoteDisplay(
- const sp<IRemoteDisplayClient>& client, const String8& iface)
- if (!checkPermission("android.permission.CONTROL_WIFI_DISPLAY"))
- return NULL;
-
以上是关于Android WifiDisplay分析二:Wifi display连接过程的主要内容,如果未能解决你的问题,请参考以下文章
android4.2 wifidisplay远程显示修改为保存文件
android4.2 WifiDisplay远程显示修改为保存文件