Android 4.2开始支持secondary display。 提供media router用于query当前secondary display的状态。 提供Presentation, 由用户负责填充,作为secondary display的输入。
在android source code中使用CONFIG_WIFI_DISPLAY来区分相关代码。
- 从mediaroutner说起
public MediaRouter(Context context) { synchronized (Static.class) {
if (sStatic == null) {
final Context appContext = context.getApplicationContext();
sStatic = new Static(appContext); //创建static
sStatic.startMonitoringRoutes(appContext); //调用 startMonitoringRoutes,
}
}
}
public class MediaRouter
{
static class Static implements DisplayManager.DisplayListener {
void startMonitoringRoutes(Context appContext) {
appContext.registerReceiver(new WifiDisplayStatusChangedReceiver(), //将WifiDisplayStatusChangedReceiver注册进去。 new IntentFilter(DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED));
}
}
}
static class WifiDisplayStatusChangedReceiver extends BroadcastReceiver { @Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED)) {
updateWifiDisplayStatus((WifiDisplayStatus) intent.getParcelableExtra(
DisplayManager.EXTRA_WIFI_DISPLAY_STATUS));
}
}
}
- MediaRouter 将接收DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED消息, 当收到消息时,调用updateWifiDisplayStatus()。根据wifiDisplay 创建display route
static void updateWifiDisplayStatus(WifiDisplayStatus newStatus) { final WifiDisplayStatus oldStatus = sStatic.mLastKnownWifiDisplayStatus;
// TODO Naive implementation. Make this smarter later.
boolean wantScan = false;
boolean blockScan = false;
WifiDisplay[] oldDisplays = oldStatus != null ?
oldStatus.getRememberedDisplays() : new WifiDisplay[0];
WifiDisplay[] newDisplays = newStatus.getRememberedDisplays();
WifiDisplay[] availableDisplays = newStatus.getAvailableDisplays();
WifiDisplay activeDisplay = newStatus.getActiveDisplay(); //获得新的display
for (int i = 0; i < newDisplays.length; i++) {
final WifiDisplay d = newDisplays[i];
final WifiDisplay oldRemembered = findMatchingDisplay(d, oldDisplays);
if (oldRemembered == null) {
addRouteStatic(makeWifiDisplayRoute(d,
findMatchingDisplay(d, availableDisplays) != null)); //新建wifi router
wantScan = true;
} else {
final boolean available = findMatchingDisplay(d, availableDisplays) != null;
final RouteInfo route = findWifiDisplayRoute(d);
updateWifiDisplayRoute(route, d, available, newStatus);
}
if (d.equals(activeDisplay)) {
final RouteInfo activeRoute = findWifiDisplayRoute(d);
if (activeRoute != null) {
selectRouteStatic(activeRoute.getSupportedTypes(), activeRoute);
// Don't scan if we're already connected to a wifi display,
// the scanning process can cause a hiccup with some configurations.
blockScan = true;
}
}
}
for (int i = 0; i < oldDisplays.length; i++) {
final WifiDisplay d = oldDisplays[i];
final WifiDisplay newDisplay = findMatchingDisplay(d, newDisplays);
if (newDisplay == null) {
removeRoute(findWifiDisplayRoute(d));
}
}
if (wantScan && !blockScan) {
sStatic.mDisplayService.scanWifiDisplays();//重要 !会触发discoverPeers
}
sStatic.mLastKnownWifiDisplayStatus = newStatus;
}
- 确认wifidisplay是合法的
private static WifiDisplay findMatchingDisplay(WifiDisplay d, WifiDisplay[] displays) {
for (int i = 0; i < displays.length; i++) {
final WifiDisplay other = displays[i];
if (d.getDeviceAddress().equals(other.getDeviceAddress())) {
return other;
}
}
return null;
}
- 创建router
static RouteInfo makeWifiDisplayRoute(WifiDisplay display, boolean available) { final RouteInfo newRoute = new RouteInfo(sStatic.mSystemCategory);
newRoute.mDeviceAddress = display.getDeviceAddress(); //mac 地址
newRoute.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO;
newRoute.mVolumeHandling = RouteInfo.PLAYBACK_VOLUME_FIXED;
newRoute.mPlaybackType = RouteInfo.PLAYBACK_TYPE_REMOTE;
newRoute.setStatusCode(available ?
RouteInfo.STATUS_AVAILABLE : RouteInfo.STATUS_CONNECTING);
newRoute.mEnabled = available;
newRoute.mName = display.getFriendlyDisplayName();
newRoute.mPresentationDisplay = choosePresentationDisplayForRoute(newRoute,
sStatic.getAllPresentationDisplays());
return newRoute;
}
Connect Wifi Display!
static void selectRouteStatic(int types, RouteInfo route) { final RouteInfo oldRoute = sStatic.mSelectedRoute;
if (oldRoute == route) return;
if ((route.getSupportedTypes() & types) == 0) {
Log.w(TAG, "selectRoute ignored; cannot select route with supported types " +
typesToString(route.getSupportedTypes()) + " into route types " +
typesToString(types));
return;
}
final RouteInfo btRoute = sStatic.mBluetoothA2dpRoute;
if (btRoute != null && (types & ROUTE_TYPE_LIVE_AUDIO) != 0 &&
(route == btRoute || route == sStatic.mDefaultAudioVideo)) {
try {
sStatic.mAudioService.setBluetoothA2dpOn(route == btRoute);
} catch (RemoteException e) {
Log.e(TAG, "Error changing Bluetooth A2DP state", e);
}
}
final WifiDisplay activeDisplay =
sStatic.mDisplayService.getWifiDisplayStatus().getActiveDisplay();
final boolean oldRouteHasAddress = oldRoute != null && oldRoute.mDeviceAddress != null;
final boolean newRouteHasAddress = route != null && route.mDeviceAddress != null;
if (activeDisplay != null || oldRouteHasAddress || newRouteHasAddress) {
if (newRouteHasAddress && !matchesDeviceAddress(activeDisplay, route)) {
sStatic.mDisplayService.connectWifiDisplay(route.mDeviceAddress); //Connect
} else if (activeDisplay != null && !newRouteHasAddress) {
sStatic.mDisplayService.disconnectWifiDisplay();
}
}
if (oldRoute != null) {
// TODO filter types properly
dispatchRouteUnselected(types & oldRoute.getSupportedTypes(), oldRoute);
}
sStatic.mSelectedRoute = route;
if (route != null) {
// TODO filter types properly
dispatchRouteSelected(types & route.getSupportedTypes(), route);
}
}
In DisplayManager.java
public final class DisplayManager {
public void connectWifiDisplay(String deviceAddress) {
mGlobal.connectWifiDisplay(deviceAddress);
}
}
In DisplayManagerGloabal.java
public void connectWifiDisplay(String deviceAddress) { if (deviceAddress == null) {
throw new IllegalArgumentException("deviceAddress must not be null");
}
try {
mDm.connectWifiDisplay(deviceAddress);
} catch (RemoteException ex) {
Log.e(TAG, "Failed to connect to Wifi display " + deviceAddress + ".", ex);
}
}
In DisplayManagerService.java
public void connectWifiDisplay(String address) { if (address == null) {
throw new IllegalArgumentException("address must not be null");
}
final boolean trusted = canCallerConfigureWifiDisplay();
final long token = Binder.clearCallingIdentity();
try {
synchronized (mSyncRoot) {
if (mWifiDisplayAdapter != null) {
mWifiDisplayAdapter.requestConnectLocked(address, trusted);
}
}
} finally {
Binder.restoreCallingIdentity(token);
}
}
Check App是否支持wifidisplay, 需要在manifest中增加android.Manifest.permission.CONFIGURE_WIFI_DISPLAY
private boolean canCallerConfigureWifiDisplay() {
return mContext.checkCallingPermission(android.Manifest.permission.CONFIGURE_WIFI_DISPLAY)
== PackageManager.PERMISSION_GRANTED;
}
In WifiDisplayAdapter.java
public void registerLocked() { super.registerLocked();
updateRememberedDisplaysLocked();
getHandler().post(new Runnable() {
@Override
public void run() {
mDisplayController = new WifiDisplayController(
getContext(), getHandler(), mWifiDisplayListener);
getContext().registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL,
new IntentFilter(ACTION_DISCONNECT), null, mHandler);
}
});
}
public void requestConnectLocked(final String address, final boolean trusted) { if (DEBUG) {
Slog.d(TAG, "requestConnectLocked: address=" + address + ", trusted=" + trusted);
}
if (!trusted) {
synchronized (getSyncRoot()) {
if (!isRememberedDisplayLocked(address)) {
Slog.w(TAG, "Ignoring request by an untrusted client to connect to "
+ "an unknown wifi display: " + address);
return;
}
}
}
getHandler().post(new Runnable() { @Override
public void run() {
if (mDisplayController != null) {
mDisplayController.requestConnect(address);
}
}
});
}
private WifiDisplayController mDisplayController;
In DisplayController.java
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;
}
mDesiredDevice = device;
mConnectionRetriesLeft = CONNECT_MAX_RETRIES;
updateConnection();
}
- 最后调用到最重要的api:updateConnection,wifi display的握手就是在这里完成的
/** * This function is called repeatedly after each asynchronous operation
* until all preconditions for the connection have been satisfied and the
* connection is established (or not).
*/
private void updateConnection() {
// Step 1. Before we try to connect to a new device, tell the system we
// have disconnected from the old one.
if (mRemoteDisplay != null && mConnectedDevice != mDesiredDevice) {
Slog.i(TAG, "Stopped listening for RTSP connection on " + mRemoteDisplayInterface
+ " from Wifi display: " + mConnectedDevice.deviceName);
mRemoteDisplay.dispose();
mRemoteDisplay = null;
mRemoteDisplayInterface = null;
mRemoteDisplayConnected = false;
mHandler.removeCallbacks(mRtspTimeout);
setRemoteSubmixOn(false);
unadvertiseDisplay();
// continue to next step
}
// Step 2. Before we try to connect to a new device, disconnect from the old one.
if (mConnectedDevice != null && mConnectedDevice != mDesiredDevice) {
Slog.i(TAG, "Disconnecting from Wifi display: " + mConnectedDevice.deviceName);
unadvertiseDisplay();
final WifiP2pDevice oldDevice = mConnectedDevice;
mWifiP2pManager.removeGroup(mWifiP2pChannel, new ActionListener() {
@Override
public void onSuccess() {
Slog.i(TAG, "Disconnected from Wifi display: " + oldDevice.deviceName);
next();
}
@Override
public void onFailure(int reason) {
Slog.i(TAG, "Failed to disconnect from Wifi display: "
+ oldDevice.deviceName + ", reason=" + reason);
next();
}
private void next() {
if (mConnectedDevice == oldDevice) {
mConnectedDevice = null;
updateConnection();
}
}
});
return; // wait for asynchronous callback
}
// Step 3. Before we try to connect to a new device, stop trying to connect
// to the old one.
if (mConnectingDevice != null && mConnectingDevice != mDesiredDevice) {
Slog.i(TAG, "Canceling connection to Wifi display: " + mConnectingDevice.deviceName);
unadvertiseDisplay();
mHandler.removeCallbacks(mConnectionTimeout);
final WifiP2pDevice oldDevice = mConnectingDevice;
mWifiP2pManager.cancelConnect(mWifiP2pChannel, new ActionListener() {
@Override
public void onSuccess() {
Slog.i(TAG, "Canceled connection to Wifi display: " + oldDevice.deviceName);
next();
}
@Override
public void onFailure(int reason) {
Slog.i(TAG, "Failed to cancel connection to Wifi display: "
+ oldDevice.deviceName + ", reason=" + reason);
next();
}
private void next() {
if (mConnectingDevice == oldDevice) {
mConnectingDevice = null;
updateConnection();
}
}
});
return; // wait for asynchronous callback
}
// Step 4. If we wanted to disconnect, then mission accomplished.
if (mDesiredDevice == null) {
unadvertiseDisplay();
return; // done
}
// Step 5. Try to connect.
if (mConnectedDevice == null && mConnectingDevice == null) {
Slog.i(TAG, "Connecting to Wifi display: " + mDesiredDevice.deviceName);
mConnectingDevice = mDesiredDevice;
WifiP2pConfig config = new WifiP2pConfig();
config.deviceAddress = mConnectingDevice.deviceAddress;
// Helps with STA & P2P concurrency
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() {
// The connection may not yet be established. We still need to wait
// for WIFI_P2P_CONNECTION_CHANGED_ACTION. However, we might never
// get that broadcast, so we register a timeout.
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; // wait for asynchronous callback
}
// Step 6. Listen for incoming connections.
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
}
setRemoteSubmixOn(true);
final WifiP2pDevice oldDevice = mConnectedDevice; //实际的ip和port
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) {
if (mConnectedDevice == oldDevice && !mRemoteDisplayConnected) {
Slog.i(TAG, "Opened RTSP connection with Wifi display: "
+ mConnectedDevice.deviceName);
mRemoteDisplayConnected = true;
mHandler.removeCallbacks(mRtspTimeout);
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);
mHandler.postDelayed(mRtspTimeout, RTSP_TIMEOUT_SECONDS * 1000);
}
}
- 会用到RemoteDisplay来接收消息,并callback出来,当连接成功的时候。
public final class RemoteDisplay {
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;
}
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");
}
private native int nativeListen(String iface);
static JNINativeMethod gMethods[] = {
{"nativeListen", "(Ljava/lang/String;)I",
(void*)nativeListen },
{"nativeDispose", "(I)V",
(void*)nativeDispose },
};
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);
}
sp<IRemoteDisplay> MediaPlayerService::listenForRemoteDisplay( const sp<IRemoteDisplayClient>& client, const String8& iface) {
if (!checkPermission("android.permission.CONTROL_WIFI_DISPLAY")) {
return NULL;
}
return new RemoteDisplay(client, iface.string()); //创建了一个
}
RemoteDisplay::RemoteDisplay( const sp<IRemoteDisplayClient> &client, const char *iface)
: mLooper(new ALooper),
mNetSession(new ANetworkSession),
mSource(new WifiDisplaySource(mNetSession, client)) { //实现wdf source
mLooper->setName("wfd_looper");
mLooper->registerHandler(mSource);
mNetSession->start();
mLooper->start();
mSource->start(iface);
}
RemoteDisplay::~RemoteDisplay() {
}
status_t RemoteDisplay::dispose() {
mSource->stop();
mLooper->stop();
mNetSession->stop();
return OK;
}
至此,wifi display source握手结束。 这里并没有提到presentation。下次再说。