Android 13 Miracast 投屏代码适配总结

Android 13 Miracast 投屏适配总结

文章目录

一、前言

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

相关推荐
希望永不加班2 小时前
SpringBoot 整合 Elasticsearch 实现全文检索
java·spring boot·后端·elasticsearch·全文检索
摸鱼的春哥2 小时前
Agent教程22:AI大模型兼容,踩坑到崩溃
前端·javascript·后端
幸福在路上wellbeing2 小时前
Android Compose UI 控件
android·ui
bKYP953cL2 小时前
Flask - 常见应用部署方案
后端·python·flask
希望永不加班2 小时前
SpringBoot 多模块项目搭建:service/dao/web分层设计
java·前端·spring boot·后端·spring
cch89182 小时前
Laravel1.x:初代PHP框架的起点
android
Victor3562 小时前
MongoDB(86)如何使用MongoDB存储大文件?
后端
cch89182 小时前
ThinkPHP3.x核心特性全解析
开发语言·后端·golang
Victor3562 小时前
MongoDB(85)如何实现全文搜索?
后端