Android 13 Miracast 投屏适配总结
文章目录
- [Android 13 Miracast 投屏适配总结](#Android 13 Miracast 投屏适配总结)
-
- 一、前言
- 二、适配的代码
- 三、其他
-
- 1、小结
- 2、RK方案修改分辨率参考
- [2、Miracast 使能判断逻辑代码](#2、Miracast 使能判断逻辑代码)
- [3、Android 无线投屏相关知识介绍](#3、Android 无线投屏相关知识介绍)
一、前言
Android 自研设备使用Miracast协议好像可以投屏部分Tv设备但是无法投屏到笔记本设备。
但是国内的手机都是可以正常投屏的,说明Android系统是支持投屏的,估计需要一定的适配。
查看Google手机也是无法投屏到部分Tv设备和笔记本设备;说明Google源码就是要另外适配才可以投屏的。
这里简单分享一下适配的内容。
这里主要分析解决投屏不到window设备的解决。
投屏不到Tv设备,有可能是Tv设备校验Miracast的信息有差异,有的校验数据比较多。
二、适配的代码
1、设置Android设备支持Miracast投屏适配
Android原生和Google手机都是未默认开启Miracast的Display投屏的;
需要适配属性支持投屏,如果是debug版本可以临时设置一个pro属性,设置支持Display投屏。
代码中:
1、设置支持Display:
方式一:修改 persist.debug.wfd.enable true
方式二:修改 framework/base/core/res/res/values/config.xml
<bool name="config_enableWifiDisplay">true</bool>
2、设置打开Display 扫描,一般不需要也可以
settings global 属性 wifi_display_on 设置成 1
这个是设置投屏界面,右上角会显示的,打开都是开始扫描,但是有些系统没有这个按钮就要手动设置。
cmd窗口查询和设置:
adb shell
获取:
settings get global wifi_display_on
getprop persist.debug.wfd.enable
设置:
settings put global wifi_display_on 1
setprop persist.debug.wfd.enable 1 //设置后重启才能生效
如果能扫描到Miracast设备,说明设备是支持Miracast投屏的;
可能是 config_enableWifiDisplay 的属性已经是 true 。
如果扫描不到Miracast设备,就设置 persist.debug.wfd.enable 和 global wifi_display_on 然后重启再试试。
能扫描到之后,但是无法投屏到笔记本设备就要另外适配了。
2、投屏到Window电脑适配
分析过程:
Windows 端已经作为 GO 存在(autonomous GO),Android 端需要以 P2P client 身份 join 到这个已有的 Group。日志显示 Android 端确实在尝试 associate 到 DIRECT-VTDKDNJOBGms8L,但每次都被 ASSOC-REJECT status_code=12 拒绝。
修改:WifiP2pServiceImpl.java 和 WifiDisplayController.java
修改后还有问题:
错误从 ASSOC-REJECT status_code=12 变成了 4-Way Handshake failed。
跳过 Provision Discovery 后,join 时 wpa_supplicant 没有正确的 PSK。
需要确保在 join 之前完成 WPS PBC 交换来获取 PSK。
修改方案:不跳过 ProvisionDiscovery,而是修改 ProvisionDiscovery 的行为
之前的方案 2 是跳过 ProvisionDiscovery 直接 join,这导致了 PSK 问题。
正确的做法是:仍然走 ProvisionDiscoveryState,但确保 Provision Discovery 使用正确的方式,
并且在 p2pConnectWithPinDisplay 中使用 join 方式。
最终的修改:
wifi/base/services/core/java/com/android/server/display下的:
WifiDisplayAdapter.java、WifiDisplayController.java
WifiDisplayAdapter.java :
+++ b/release/frameworks/base/services/core/java/com/android/server/display/WifiDisplayAdapter.java
@@ -651,10 +651,12 @@ final class WifiDisplayAdapter extends DisplayAdapter {
mInfo.flags = mFlags;
mInfo.type = Display.TYPE_WIFI;
mInfo.address = mAddress;
- mInfo.touch = DisplayDeviceInfo.TOUCH_EXTERNAL;
+ //mInfo.touch = DisplayDeviceInfo.TOUCH_EXTERNAL;
+ mInfo.touch = DisplayDeviceInfo.TOUCH_NONE;
mInfo.setAssumedDensityForExternalDisplay(mWidth, mHeight);
// The display is trusted since it is created by system.
- mInfo.flags |= DisplayDeviceInfo.FLAG_TRUSTED;
+ // mInfo.flags |= DisplayDeviceInfo.FLAG_TRUSTED; play meida something wrong. change by skg
+ mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP;
}
return mInfo;
}
SY-3000-RK3588T-K8HC-E-Board/release/frameworks$
WifiDisplayController.java :
+++ b/release/frameworks/base/services/core/java/com/android/server/display/WifiDisplayController.java
@@ -69,7 +69,7 @@ import java.util.Objects;
*/
final class WifiDisplayController implements DumpUtils.Dump {
private static final String TAG = "WifiDisplayController";
- private static final boolean DEBUG = false;
+ private static final boolean DEBUG = true;
private static final int DEFAULT_CONTROL_PORT = 7236;
private static final int MAX_THROUGHPUT = 50;
@@ -285,6 +285,8 @@ final class WifiDisplayController implements DumpUtils.Dump {
}
private void updateWfdEnableState() {
+ Slog.d(TAG, "updateWfdEnableState mWifiDisplayOnSetting = " + mWifiDisplayOnSetting + ". mWifiP2pEnabled = " + mWifiP2pEnabled);
+ Slog.d(TAG, "updateWfdEnableState mWfdEnabled = " + mWfdEnabled + ". mWfdEnabling = " + mWfdEnabling);
if (mWifiDisplayOnSetting && mWifiP2pEnabled) {
// WFD should be enabled.
if (!mWfdEnabled && !mWfdEnabling) {
@@ -695,6 +697,24 @@ final class WifiDisplayController implements DumpUtils.Dump {
config.deviceAddress = mConnectingDevice.deviceAddress;
// Helps with STA & P2P concurrency
config.groupOwnerIntent = WifiP2pConfig.GROUP_OWNER_INTENT_MIN;
+ Slog.i(TAG, "Target device config.groupOwnerIntent = " + config.groupOwnerIntent + " , config.wps.setup = " + config.wps.setup + ", WpsInfo.PBC = " + WpsInfo.PBC);
+ // When target device is already a Group Owner (e.g. Windows Miracast sink),
+ // force PBC mode and low GO intent to ensure we join as client.
+ if (mConnectingDevice.isGroupOwner()) {
+ Slog.i(TAG, "Target device is GO, force PBC and low GO intent for join");
+ config.wps.setup = WpsInfo.PBC;
+ config.groupOwnerIntent = WifiP2pConfig.GROUP_OWNER_INTENT_MIN;
+ } else {
+ // Target is not GO (e.g. KShare Miracast sink on Android).
+ // Use high GO intent so that we (Source) become the Group Owner.
+ // This ensures the Source has a well-known IP (typically 192.168.49.1)
+ // that the Sink can easily connect to for RTSP, improving compatibility
+ // with third-party Miracast sink implementations.
+ Slog.i(TAG, "Target device is not GO, use high GO intent to become GO");
+ config.groupOwnerIntent = WifiP2pConfig.GROUP_OWNER_INTENT_MAX;
+ }
+ Slog.i(TAG, "Final config: groupOwnerIntent=" + config.groupOwnerIntent
+ + ", wps.setup=" + config.wps.setup);
WifiDisplay display = createWifiDisplay(mConnectingDevice);
advertiseDisplay(display, null, 0, 0, 0);
@@ -734,6 +754,22 @@ final class WifiDisplayController implements DumpUtils.Dump {
return; // done
}
+ // Log P2P group role and Sink WFD info for debugging
+ final boolean isGroupOwner = mConnectedDeviceGroupInfo.isGroupOwner();
+ Slog.i(TAG, "P2P group role: " + (isGroupOwner ? "GO" : "Client")
+ + ", interface: " + mConnectedDeviceGroupInfo.getInterface()
+ + ", frequency: " + mConnectedDeviceGroupInfo.getFrequency());
+
+ // Get the Sink device's RTSP control port from its WFD IE
+ int sinkRtspPort = DEFAULT_CONTROL_PORT;
+ WifiP2pWfdInfo sinkWfdInfo = mConnectedDevice.getWfdInfo();
+ if (sinkWfdInfo != null && sinkWfdInfo.isEnabled()) {
+ sinkRtspPort = sinkWfdInfo.getControlPort();
+ Slog.i(TAG, "Sink WFD info - DeviceType: " + sinkWfdInfo.getDeviceType()
+ + ", CtrlPort: " + sinkRtspPort
+ + ", MaxThroughput: " + sinkWfdInfo.getMaxThroughput());
+ }
+
mWifiP2pManager.setMiracastMode(WifiP2pManager.MIRACAST_SOURCE);
final WifiP2pDevice oldDevice = mConnectedDevice;
@@ -928,7 +964,13 @@ final class WifiDisplayController implements DumpUtils.Dump {
&& mRemoteDisplay != null && !mRemoteDisplayConnected) {
Slog.i(TAG, "Timed out waiting for Wifi display RTSP connection after "
+ RTSP_TIMEOUT_SECONDS + " seconds: "
- + mConnectedDevice.deviceName);
+ + mConnectedDevice.deviceName
+ + ", listening on: " + mRemoteDisplayInterface);
+ if (mConnectedDeviceGroupInfo != null) {
+ Slog.i(TAG, "RTSP timeout debug - isGO: "
+ + mConnectedDeviceGroupInfo.isGroupOwner()
+ + ", interface: " + mConnectedDeviceGroupInfo.getInterface());
+ }
handleConnectionFailure(true);
}
}
@@ -1038,6 +1080,12 @@ final class WifiDisplayController implements DumpUtils.Dump {
// These dongles ignore the port we broadcast in our WFD IE.
return 8554;
}
+ // Use the control port from the Sink device's WFD IE if available,
+ // otherwise fall back to the default port.
+ WifiP2pWfdInfo wfdInfo = device.getWfdInfo();
+ if (wfdInfo != null && wfdInfo.isEnabled() && wfdInfo.getControlPort() > 0) {
+ return wfdInfo.getControlPort();
+ }
return DEFAULT_CONTROL_PORT;
}
SY-3000-RK3588T-K8HC-E-Board/release/frameworks$
WifiP2pServiceImpl.java:
+++ b/release/packages/modules/Wifi/service/java/com/android/server/wifi/p2p/WifiP2pServiceImpl.java
@@ -2817,6 +2817,18 @@ public class WifiP2pServiceImpl extends IWifiP2pManager.Stub {
config, GroupEvent.GROUP_UNKNOWN);
transitionTo(mGroupNegotiationState);
} else {
+ // When target device is already a GO (e.g. Windows
+ // Miracast sink as autonomous GO), mark join flag
+ // so that p2pConnectWithPinDisplay uses JOIN_GROUP.
+ // Still go through ProvisionDiscovery to complete
+ // WPS exchange and obtain the correct PSK.
+ WifiP2pDevice targetDev =
+ mPeers.get(config.deviceAddress);
+ if (targetDev != null && targetDev.isGroupOwner()) {
+ Log.i(TAG, "Target device is GO, set join flag "
+ + "before entering ProvisionDiscoveryState");
+ mJoinExistingGroup = true;
+ }
mWifiP2pMetrics.startConnectionEvent(
P2pConnectionEvent.CONNECTION_FRESH,
config, GroupEvent.GROUP_UNKNOWN);
@@ -3372,6 +3384,12 @@ public class WifiP2pServiceImpl extends IWifiP2pManager.Stub {
@Override
public void enter() {
if (isVerboseLoggingEnabled()) logd(getName());
+ // Log whether target is GO for debugging Miracast connection issues
+ WifiP2pDevice targetDev = mPeers.get(mSavedPeerConfig.deviceAddress);
+ if (targetDev != null && targetDev.isGroupOwner()) {
+ Log.i(TAG, "ProvisionDiscovery: target " + targetDev.deviceName
+ + " is GO, prov disc may timeout and fallback to direct join");
+ }
mWifiNative.p2pProvisionDiscovery(mSavedPeerConfig);
}
@@ -3453,6 +3471,23 @@ public class WifiP2pServiceImpl extends IWifiP2pManager.Stub {
case WifiP2pMonitor.P2P_PROV_DISC_FAILURE_EVENT:
loge("provision discovery failed status: " + message.arg1);
+ // When target is an autonomous GO (e.g. Windows Miracast sink),
+ // it may not respond to Provision Discovery requests.
+ // In this case, fallback to direct PBC join instead of failing.
+ WifiP2pDevice targetGo =
+ mPeers.get(mSavedPeerConfig.deviceAddress);
+ if (targetGo != null && targetGo.isGroupOwner()) {
+ Log.i(TAG, "Prov disc failed but target is autonomous GO,"
+ + " fallback to direct PBC join: "
+ + targetGo.deviceName);
+ mSavedPeerConfig.wps.setup = WpsInfo.PBC;
+ mJoinExistingGroup = true;
+ p2pConnectWithPinDisplay(mSavedPeerConfig,
+ P2P_CONNECT_TRIGGER_OTHER);
+ transitionTo(mGroupNegotiationState);
+ break;
+ }
+
// Saved peer information is used in handleGroupCreationFailure().
if (!handleProvDiscFailure(
(WifiP2pProvDiscEvent) message.obj, false)) {
@@ -5087,6 +5122,17 @@ public class WifiP2pServiceImpl extends IWifiP2pManager.Stub {
netId = mGroups.getNetworkId(dev.deviceAddress);
}
if (netId >= 0) {
+ // For autonomous GO (e.g. Windows Miracast sink), the passphrase
+ // may change after each disconnection. Using a stale persistent
+ // group credential will cause 4-Way Handshake failure.
+ // Skip reinvoke and let the caller fall through to
+ // ProvisionDiscovery to obtain a fresh PSK via WPS.
+ if (dev.isGroupOwner() && !isInvited) {
+ Log.i(TAG, "Target is autonomous GO, skip reinvoke with "
+ + "persistent netId=" + netId
+ + " to avoid stale PSK, will do fresh WPS");
+ return false;
+ }
// Skip WPS and start 4way handshake immediately.
if (!mWifiNative.p2pGroupAdd(netId)) {
return false;
SY-3000-RK3588T-K8HC-E-Board/release/packages/modules/Wifi$
上面三个文件的修改其实是ai帮我修改的:
修改了多次,最后一次的修改思路:
WifiDisplayController.java
--- Step 5 中当目标设备是 GO 时,强制使用 PBC 模式并添加日志
WifiP2pServiceImpl.java
--- InactiveState 的 CONNECT 处理中,当目标是 GO 且无持久化组时,设置 mJoinExistingGroup = true,然后仍然进入 ProvisionDiscoveryState 完成 WPS 交换
WifiP2pServiceImpl.java
--- ProvisionDiscoveryState 中两处修改:
enter() 增加 GO 目标的调试日志
P2P_PROV_DISC_FAILURE_EVENT 处理中,当目标是 autonomous GO 且 prov disc 失败时,fallback 到 PBC join(调用 p2pConnectWithPinDisplay 走 JOIN_GROUP 路径),而不是直接宣告连接失败
核心思路是:先尝试正常的 Provision Discovery 获取 PSK,如果 GO 不响应导致失败,则 fallback 用 p2p_connect pbc join 让 wpa_supplicant 在 join 过程中自动完成 WPS PBC 交换来获取正确的 PSK,从而解决 4-Way Handshake 失败的问题。
这些代码修改仅供参考,对比看了下不同Android版本的代码差异还是蛮大的;
三、其他
1、小结
(1)Android 设备需要支持Miracast投屏,需要两边都适配;
(2)手机端,作为Source端,需要系统设置支持Display;
(3)接收端,作为Sink端,可以是Tv、window,某些Android设备;
(4)Sink端,必须是有专门的应用在接收设备;不管是window还是android要打开接收的应用界面;
(5)投屏失败分析解决:需要投屏到多个设备对比查看是Source端还是Sink端的问题;
(6)上面修改Source端代码,仅供参考,不同版本的代码,估计要理解具体过程再做修改;
(6)Android Miracast投屏一定不能打开热点,不管是Source端还是Sink端,但是热点功能正常。
RK方案和AML方案的源码里面都有接收应用的代码,但是默认是不编译的,需要自己手动编译安装。
大概位置:
aml:
release/vendor/amlogic/common/apps/DisplayController
rk:
release/vendor/rockchip/common/apps/wifipisplay
编译报错解决:把报错的编译换成 peakRefreshRate;
运行崩溃,解决:需要把apk放到system目录并且把so放到system/lib64目录
aml和rk的投屏接收应用都是可以接收手机端的投屏镜像界面;具体实现没有细看。
2、RK方案修改分辨率参考
RK那边默认投出帧率是24帧,我们开发的sink端只支持720p或者1080p ,也会导致Sink端直接断开。
+++ b/release/frameworks/av/media/libstagefright/wifi-display/source/WifiDisplaySource.cpp
@@ -74,7 +74,7 @@ WifiDisplaySource::WifiDisplaySource(
mSupportedSourceVideoFormats.disableAll();
mSupportedSourceVideoFormats.setNativeResolution(
- VideoFormats::RESOLUTION_CEA, 15); // 1280x720 p23
+ VideoFormats::RESOLUTION_CEA, 7); // 1280x720 p23, (15->7)change by skg
// Enable all resolutions up to 1280x720 p23
mSupportedSourceVideoFormats.enableResolutionUpto(
这个也是Source端的一个修改参考情况。
2、Miracast 使能判断逻辑代码
framework/base/services/core/java/com/android/server/display/DisplayManagerService.java
public final class DisplayManagerService extends SystemService {
private static final String TAG = "DisplayManagerService";
private static final boolean DEBUG = false;
// When this system property is set to 0, WFD is forcibly disabled on boot.
// When this system property is set to 1, WFD is forcibly enabled on boot.
// Otherwise WFD is enabled according to the value of config_enableWifiDisplay.
private static final String FORCE_WIFI_DISPLAY_ENABLE = "persist.debug.wfd.enable";
private void registerWifiDisplayAdapterLocked() {
if (mContext.getResources().getBoolean(
com.android.internal.R.bool.config_enableWifiDisplay)
|| SystemProperties.getInt(FORCE_WIFI_DISPLAY_ENABLE, -1) == 1) {
mWifiDisplayAdapter = new WifiDisplayAdapter(
mSyncRoot, mContext, mHandler, mDisplayDeviceRepo,
mPersistentDataStore);
registerDisplayAdapterLocked(mWifiDisplayAdapter);
}
}
...
}
上面 R.bool.config_enableWifiDisplay 和 FORCE_WIFI_DISPLAY_ENABLE 就是判断Miracast是否能用的代码;
registerWifiDisplayAdapterLocked 方法就是开机的时候调用的,这也是为啥修改了prop属性后要重启才生效。
如果设置了使能,并且开启扫描后,还是扫描不到Miracast设备,估计系统底层有问题。
3、Android 无线投屏相关知识介绍
无线 AirPlay 投屏是苹果设备的,这里只介绍Android相关的投屏;
Android设备可以投屏到其他电视机、Window设备,以及某些在Android系统特殊适配的设备。
Android 设备投屏主要是有 Miracast(WFD)、Google Cast、DLNA 等协议。
具体内容:
https://blog.csdn.net/wenzhi20102321/article/details/159887898