
# 开发指导
本章节通过一个典型客户端应用的接入示例，介绍如何集成SDK并完成最小可运行环境搭建。示例包含初始化SDK、获取鉴权信息、启动主界面串流以及基础配置等内容，帮助开发者完成快速接入。
#### 前提条件
开发者在阅读本章节前，默认已经完成[快速开始](https://support.huaweicloud.com/devg-koophone/kp-00_00004.html)中的[步骤一](https://support.huaweicloud.com/devg-koophone/kp-00_00004.html#kp-00_00004__section1096011721310)\~[步骤三](https://support.huaweicloud.com/devg-koophone/kp-00_00004.html#kp-00_00004__section19173513121613)，包含以下内容：创建Android工程、集成aar SDK包、配置Gradle依赖、配置BuildConfig参数、准备Config.java、Constant.java等基础参数、准备IAM鉴权参数、创建基础页面与MainActivity、验证基础串流启动能力。默认开发者已具备"快速开始"中的最小接入能力。
#### 步骤一：准备样式
在实际示例工程中，除了完成串流能力接入，还需要准备一个适合展示和交互的页面结构。该页面不仅用于承载串流画面，还需要满足应用墙展示、系统控制按钮操作以及状态过渡展示等需求。
1. 设计页面布局目标。示例页面建议包含以下几个区域： 
   - 串流承载区域：用于放置PlayerFragment，承载远端画面显示。
   
   - 遮罩层：在未连接、连接中或应用墙展示阶段，可通过半透明黑色遮罩控制页面层级和视觉状态。
   
   - 应用墙区域：用于展示已安装应用列表，开发者可采用按钮、列表或宫格形式。
   
   - 按键操作区域：用于提供返回主屏、关闭应用、退出等基础控制能力。
   
   
   
   
2. 最简布局示例。 
   以下示例为本开发示例中使用的页面布局，该布局相较于[快速开始](https://support.huaweicloud.com/devg-koophone/kp-00_00004.html)的页面，增加了应用墙、任务管理和关闭应用等个性化功能区域，删除了断连和连接按钮，首次进入应用墙默认发起连接，进入应用会发起音视频通道连接，再次返回应用墙会切换回数据通道连接。
   该布局相较于[快速开始](https://support.huaweicloud.com/devg-koophone/kp-00_00004.html)的最小示例，主要体现以下个性化设计：
   ```
   <?xml version="1.0" encoding="utf-8"?>
   <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:app="http://schemas.android.com/apk/res-auto"
       xmlns:tools="http://schemas.android.com/tools"
       android:id="@+id/main"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       tools:context=".MainActivity"
       >
       <FrameLayout
           android:id="@+id/containerf"
           android:layout_width="match_parent"
           android:layout_height="wrap_content" />
       <View
           android:id="@+id/black_overlay"
           android:layout_width="match_parent"
           android:layout_height="match_parent"
           android:background="#FF000000"
           android:alpha="1" />
       <Button
           android:id="@+id/btn_top"
           android:layout_width="0dp"
           android:layout_height="60dp"
           android:layout_marginBottom="16dp"
           android:text="顶部留空"
           android:textSize="18sp"
           android:textColor="@android:color/white"
           app:layout_constraintTop_toTopOf="parent"
           app:layout_constraintStart_toStartOf="parent"
           app:layout_constraintEnd_toEndOf="parent" />
       <androidx.constraintlayout.helper.widget.Flow
           android:id="@+id/flow"
           android:layout_width="0dp"
           android:layout_height="wrap_content"
           android:layout_margin="8dp"
           app:constraint_referenced_ids="btnStartApp1,btnStartApp2,btnStartApp3,btnStartApp4,btnStartApp5,
           btnStartApp6,btnStartApp7,btnStartApp8,btnStartApp9,btnStartApp10,btnStartApp11,btnStartApp12,
           btnStartApp13,btnStartApp14,btnStartApp15,btnStartApp16,btnStartApp17,btnStartApp18,btnStartApp19,btnStartApp20,btnStartApp21"
           app:flow_wrapMode="chain"
           app:flow_maxElementsWrap="7"
           app:flow_horizontalGap="2dp"
           app:flow_verticalGap="2dp"
           app:flow_horizontalStyle="packed"
           app:flow_horizontalBias="0.5"
           app:flow_horizontalAlign="center"
           app:layout_constraintTop_toBottomOf="@+id/btn_top"
           app:layout_constraintStart_toStartOf="parent"
           app:layout_constraintEnd_toEndOf="parent" />
       <Button
           android:id="@+id/btnTaskMng"
           android:layout_width="77dp"
           android:layout_height="55dp"
           android:layout_marginTop="0dp"
           android:layout_marginStart="0dp"
           android:text="任务管理"
           android:gravity="center"
           app:layout_constraintTop_toBottomOf="@+id/flow"
           app:layout_constraintEnd_toEndOf="parent"
           />
       <Button
           android:id="@+id/btnExit"
           android:layout_width="77dp"
           android:layout_height="55dp"
           android:layout_marginTop="0dp"
           android:layout_marginStart="0dp"
           android:text="返回主屏"
           android:gravity="center"
           app:layout_constraintTop_toBottomOf="@+id/btnTaskMng"
           app:layout_constraintEnd_toEndOf="parent"
           />
       <Button
           android:id="@+id/btnCloseApp"
           android:layout_width="40dp"
           android:layout_height="40dp"
           android:layout_marginTop="10dp"
           android:layout_marginEnd="10dp"
           android:text="X"
           android:textColor="#FFFFFF"
           android:textSize="18sp"
           android:textStyle="bold"
           android:gravity="center"
           android:backgroundTint="@null"
           android:background="#E53935"
           android:padding="0dp"
           android:elevation="20dp"
           app:layout_constraintTop_toTopOf="parent"
           app:layout_constraintEnd_toEndOf="parent"/>
       <Button
           android:id="@+id/btnStartApp1"
           android:layout_width="0dp"
           android:layout_height="72dp"
           android:text="按钮1"
           android:minWidth="60dp"
           android:minHeight="38dp"
           android:maxWidth="120dp"
           app:layout_constrainedWidth="true" />
       <Button
           android:id="@+id/btnStartApp2"
           android:layout_width="0dp"
           android:layout_height="72dp"
           android:text="按钮2"
           android:minWidth="60dp"
           android:minHeight="38dp"
           android:maxWidth="120dp"
           app:layout_constrainedWidth="true" />
   </androidx.constraintlayout.widget.ConstraintLayout>
   ```
   - containerf：继续作为串流承载容器。
   
   - black_overlay：用于串流前、切换中和返回应用墙阶段的遮罩控制。
   
   - Flow + 多个应用按钮：用于展示应用墙。
   
   - btnTaskMng：进入任务管理场景。
   
   - btnExit：返回主屏。
   
   - btnCloseApp：关闭当前应用。
   
   - 顶部预留区域：可用于显示状态信息、标题或后续扩展控件。
    
   本示例重点在于将最小串流页面扩展为一个可操作、可演示的业务页面。
   图1业务页面   
   ![](https://support.huaweicloud.com/devg-koophone/zh-cn_image_0000002531936382.png "点击放大")
   
   
 
#### 步骤二：初始化SDK并启动串流
该步骤描述示例工程中如何组织主页面启动流程，并在应用启动后自动完成串流初始化。本示例的启动流程如下：
图2初始化SDK并启动串流流程   
![](https://support.huaweicloud.com/devg-koophone/zh-cn_image_0000002531745060.png)
![](https://support.huaweicloud.com/devg-koophone/public_sys-resources/note_3.0-zh-cn.png)
- [快速开始](https://support.huaweicloud.com/devg-koophone/kp-00_00004.html)中已完整说明初始化基础SDK、IAM鉴权模型、接口定义及基础串流启动流程，因此本章仅保留示例工程中的组织方式和增强配置。
- 与[快速开始](https://support.huaweicloud.com/devg-koophone/kp-00_00004.html)的区别在于，[快速开始](https://support.huaweicloud.com/devg-koophone/kp-00_00004.html)强调"手动连接/断开"的最小流程，本示例强调"页面启动后自动拉起串流，并为后续应用管理能力做准备"。
 
1. 启动MainActivity组织方式。 
   ```
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       // 初始化SDK
       initCloudPhoneSDK();
       // 初始化UI
       initUI();
       // 开始鉴权并启动串流
       fetchTokenAndStartSteam();
   }
   ```
   initCloudPhoneSDK()和initUI()的基础能力可参见[快速开始](https://support.huaweicloud.com/devg-koophone/kp-00_00004.html)的内容，本示例保留相同入口结构，但在启动后直接进入鉴权和串流流程，而不是等待用户手动单击连接按钮。
   
   
2. 发起鉴权流程。 
   获取IAM Token、实例连接信息请求、ApiService定义以及AuthRequest、AuthResponse、ApiResponse等模型，均已在[快速开始](https://support.huaweicloud.com/devg-koophone/kp-00_00004.html)中完整说明。本示例直接复用已有能力，仅保留在MainActivity中的调用方式。
   ```
   private void fetchTokenAndStartSteam() {
       threadPool.execute(() -> {
           try {
               if (Constant.INSTANCE_ID.isEmpty()) {
                   this.runOnUiThread(() -> Toast.makeText(
                           getApplicationContext(),
                           "请在gradle.properties里填入 INSTANCE_ID",
                           Toast.LENGTH_LONG
                   ).show());
                   return;
               }
               String result = TokenManager.fetchToken();
               Observable<ApiResponse> deviceInfo =
                       KooPhoneServiceManager
                               .createService(ApiService.class, result)
                               .getDeviceInfo(Constant.INSTANCE_ID);
               executeAuthTokenAndStartStream(deviceInfo);
           } catch (Exception e) {
               Log.error(TAG, "Token initialization failed", e);
           }
       });
   }
   ```
   
   
3. 获取实例连接信息。 
   ```
   private void executeAuthTokenAndStartStream(Observable<ApiResponse> deviceInfo) {
       deviceInfo.subscribeOn(Schedulers.io())
               .observeOn(AndroidSchedulers.mainThread())
               .subscribe(new DataSubcriber<ApiResponse>() {
                   @Override
                   public void onNext(ApiResponse authResponse) {
                       if (authResponse.getData() == null
                               || authResponse.getData().getDevice_token() == null
                               || authResponse.getData().getResource() == null
                               || authResponse.getData().getResource().getRtc().getIce_signaling().getSignaling_url() == null) {
                           MainActivity.this.runOnUiThread(() -> Toast.makeText(
                                   getApplicationContext(),
                                   "获取的Token等信息为null 请检查实例状态",
                                   Toast.LENGTH_LONG
                           ).show());
                           return;
                       }
                       startStream(authResponse);
                   }
                   @Override
                   public void onError(Throwable e) {
                       MainActivity.this.runOnUiThread(() -> Toast.makeText(
                               getApplicationContext(),
                               "获取Token失败 请检查网络配置及实例ID是否正确 " + e,
                               Toast.LENGTH_LONG
                       ).show());
                   }
               });
   }
   ```
   本示例的重点是说明如何在页面启动阶段直接串联"鉴权--串流启动"。
   
   
4. 构建个性化串流启动参数。 
   [快速开始](https://support.huaweicloud.com/devg-koophone/kp-00_00004.html)中已经说明了最小必需的串流参数：device_token、地址、端口、用户标识和分辨率。本示例在此基础上增加了若干更贴近实际场景的配置项，例如：连接类型参数、视频编码类型、编码性能参数、启动时隐藏画面、音频保活，示例代码如下：
   ```
   private Bundle createBundle(ApiResponse authResponse) {
       Bundle bundle = new Bundle();
       if (authResponse != null && authResponse.getData() != null) {
           ApiResponse.Data data = authResponse.getData();
           bundle.putString(LAUNCH_KEY_TOKEN, data.getDevice_token());
           ApiResponse.Data.Resource resource = data.getResource();
           if (resource != null) {
               ApiResponse.Data.Resource.Sdk sdk = resource.getSdk();
               if (sdk != null) {
                   ApiResponse.Data.Resource.Sdk.External external = sdk.getExternal();
                   if (external != null) {
                       bundle.putString(LAUNCH_KEY_ADDRESS, external.getAddress());
                       bundle.putInt(LAUNCH_KEY_PORT, external.getAport());
                       
                   }
               }
           }
       }
     
       bundle.putString("userId", "43740902807");
       bundle.putBoolean(CLOUD_APP_HIDE_STREAM_AT_START_UP, true);
       return bundle;
   }
   ```
   
   
5. 注册回调并启动串流。 
   在[快速开始](https://support.huaweicloud.com/devg-koophone/kp-00_00004.html)中，仅演示了最小串流启动方式。本示例中，除了播放器回调外，还同时注册应用回调，以支持后续应用墙、应用启动、应用关闭等能力。
   1. 回调对象示例：
      ```
      public class CloudPhoneCallBack implements CloudPhoneClient.Callback,
              CloudPhoneClient.Callback.AppCallback {
          private static volatile CloudPhoneCallBack instance;
          private CloudPhoneCallBack() {
          }
          public static CloudPhoneCallBack getInstance() {
              if (instance == null) {
                  synchronized (CloudPhoneCallBack.class) {
                      if (instance == null) {
                          instance = new CloudPhoneCallBack();
                      }
                  }
              }
              return instance;
          }
          @Override
          public void onSuccess() {
              // 串流建立成功
          }
          @Override
          public void onFailure(int code, String msg) {
              // 串流建立失败
          }
      }
      ```
      
   
   2. 启动串流。
      ```
      public void startStream(ApiResponse authResponseResponseBean) {
          Bundle bundle = createBundle(authResponseResponseBean);
          CloudPhoneCallBack callBack = CloudPhoneCallBack.getInstance();
          CloudPhoneClient.setPlayerType(HMTP_PLAYER);
          CloudPhoneClient.enableAudioKeeping(true);
          CloudPhoneClient.setPlayerCallback(callBack);
          CloudPhoneClient.start(m_fragment, bundle);
      }
      ```
      enableAudioKeeping(true)：用于保持音频能力。
      
   
   
   
   
 
#### 个性化接口调用示例
以下几个常见的个性化接口调用示例，帮助开发者在最小demo跑通之后，可以根据需求补齐基础应用管理能力。
1. 进入app应用墙（查看已安装应用列表）。 
   1. 当串流真正启动后，在onStreamStarted()回调中调用getInstalledApps()获取应用列表，然后在页面中显示为应用墙。获取应用列表：
      ```
      public void setStreamStarted(){
          if(!streamStarted){
              Log.info(TAG, "首次streamStarted 调getInstalledApps尝试获取应用列表");
              GetAppsReq getAppsReq = new GetAppsReq();
              getAppsReq.setAppType(AppTypeEnum.THIRD_APP);
              getAppsReq.setNeedIcon(false);
              getAppsReq.setPageNum(1);
              getAppsReq.setPageSize(20);
              getAppsReq.setQuality(85);
              CloudPhoneClient.getInstalledApps(getAppsReq);
          }
          streamStarted = true;
      }
      ```
      
   
   2. 处理应用列表结果：
      ```
      public void updateAppList(AppOperateResponse<GetAppsRsp> appOperateResponse) {
          Log.info(TAG, "处理已安装应用列表 显示按钮");
          int count = appOperateResponse.getData().getTotalCount();
          for(int i = 0; i < count; i++) {
              AppInfo ai = appOperateResponse.getData().getAppList().get(i);
              Log.info(TAG, "包名及应用名  " + ai.getPackageName() + "  " + ai.getAppName());
              appButtonList.get(i).setText(ai.getAppName());
              appButtonMap.put(appButtonList.get(i), ai.getPackageName());
          }
          this.setAppButtonClick();
          showAppWall();
      }
      ```
      完成这一步串流建立成功后，客户端会自动获取应用列表，页面按钮显示应用名称，用户可以单击按钮启动对应应用。
      
   
   
   
   
2. 进入APP。应用墙展示完成后，开发者可以通过包名启动指定应用。
   
   ```
   private void launchApp(String packageName) {
       Log.info(TAG, "调用SDK 启动应用<" + packageName + ">");
       CloudPhoneClient.startApp(packageName);
       currentRunningApp = packageName;
       startAudio();
       CloudPhoneClient.switchStreamType(CloudPhoneConst.CLOUD_APP_SWITCH_STREAM_TYPE.BOTH);
       showFragment();
   }
   ```
   客户端在实际接入过程中，在启动应用、关闭应用、获取应用列表时候需要根据业务场景动态切换串流通道类型，例如：
   - 进入应用时，需要同时打开音频和视频通道，如：[2]中示例launchApp中在调用startApp后还需要调用switchStreamType(both)。
   
   - 返回应用墙时，只保留音频或数据通道。
   
   - 某些场景下只显示控制界面，不显示视频画面。
   
   - 某些场景下只保留数据交互，减少不必要的视频渲染。
   
   
   为满足上述需求，SDK提供了CloudPhoneClient.switchStreamType(type)接口，用于切换当前串流的通道类型。建议根据业务行为明确切换时机：
   
   | 场景           | 推荐流类型      |
   |:---|:---|
   | 进入应用/首次进入应用墙 | BOTH       |
   | 应用正常运行       | BOTH       |
   | 返回应用墙        | AUDIO或DATA |
   | 只做后台控制       | DATA       |
   | 只播放声音        | AUDIO      |
   | 只显示画面        | VIDEO      |
      
   开发者只需要保证packageName来源正确，即可实现单击应用墙进入应用。
   图3应用示例   
   ![](https://support.huaweicloud.com/devg-koophone/zh-cn_image_0000002531976446.png "点击放大")
   
   
3. 关闭APP。开发者可以通过当前运行应用的包名关闭应用。 
   ```
   btnCloseApp.setOnClickListener(new View.OnClickListener() {
       @Override
       public void onClick(View v) {
           Log.info(TAG,"关闭应用 按钮被单击了 " + currentRunningApp);
           CloudPhoneClient.switchStreamType(CloudPhoneConst.CLOUD_APP_SWITCH_STREAM_TYPE.DATA);
           if(!currentRunningApp.isEmpty()) {
               CloudPhoneClient.closeApp(currentRunningApp);
           } else {
               Log.error(TAG,"currentRunningApp isEmpty ");
           }
           showAppWall();
       }
   });
   ```
   调用CloudPhoneClient.closeApp(currentRunningApp)可以关闭当前应用，关闭应用后，页面会重新回到应用墙状态。
   
   
4. 查看正在运行中APP列表。 
   客户端除了可以查看已安装应用列表外，还可以进一步查询当前实例中正在运行的应用列表。建议在页面初始化阶段调用查询逻辑，例如在initData()中完成回调注册和查询请求发送。
   ```
   private void initData() {
       CloudPhoneCallBack callBack = CloudPhoneCallBack.getInstance();
       callBack.setRunningAppHandler(this::onGetRunningApps);
       callBack.setCloseAppHandler(this::onCloseApp);
       CloudInterfaceClient.setAppCallback(callBack);
       GetAppsReq getAppsReq = new GetAppsReq();
       getAppsReq.setAppType(AppTypeEnum.THIRD_APP);
       getAppsReq.setNeedIcon(true);
       getAppsReq.setPageNum(1);
       getAppsReq.setPageSize(20);
       getAppsReq.setQuality(70);
       CloudPhoneClient.getRunningApps(getAppsReq);
   }
   ```
   - 查询运行中应用列表时，主要通过GetAppsReq指定查询条件。
   
   - setAppType(AppTypeEnum.THIRD_APP)：查询第三方应用。
   
   - setNeedIcon(true)：返回应用图标，便于界面展示。
   
   - setPageNum(1)：查询第一页数据。
   
   - setPageSize(20)：单次最多返回20个应用。
   
   - setQuality(70)：图标质量参数。
   
   
   开发者可根据实际界面需要调整分页大小、是否返回图标以及图标质量。
   图4调整界面显示   
   ![](https://support.huaweicloud.com/devg-koophone/zh-cn_image_0000002562976353.png "点击放大")
   
   
5. 针对低算力场景的端侧串流初始化配置。 
   - **Ignition策略** ：用于控制底层任务的执行方式，通过参数CLOUD_APP_EVENT_EXECUTOR进行配置。端侧通过Bundle传入一个int值来指定任务执行器类型。
     - 当配置为1时表示使用Ignition执行器（IGNITION_EXECUTOR），任务会通过Ignition调度执行。Ignition内部通过高精度定时器 + 任务队列 + 线程池的方式进行任务调度，定时器周期触发Update()，检查需要执行的任务，再将任务提交到线程池中执行，从而统一管理异步任务、延时任务和周期任务，减少线程创建开销并提升系统稳定性。
       ```
       bundle.putInt(CLOUD_APP_EVENT_EXECUTOR, 1); //使用 Ignition 执行器
       ```
       
     
     - 当配置为0时表示使用Loop执行器（LOOP_EXECUTOR），任务通过传统的Loop方式执行，不使用Ignition调度框架。
       ```
       bundle.putInt(CLOUD_APP_EVENT_EXECUTOR, 0);//使用 Loop 执行器
       ```
       
     
     
     如果端侧未配置该参数，则默认使用Ignition执行模式。
     
   
   - **补帧策略** ：用于在用户操作时提升画面流畅度，通过参数CLOUD_APP_HIGH_PERFORMANCE控制。端侧通过Bundle传入一个boolean值来配置补帧行为。
     - true：开启高性能模式，手指按下和抬起都会触发补帧，可以更快更新画面，操作响应更流畅。
       ```
       bundle.putBoolean(CLOUD_APP_HIGH_PERFORMANCE, true);  // 按下和抬起都补帧
       ```
       
     
     - false：关闭高性能模式，只在手指抬起时触发补帧，减少补帧次数，从而降低性能消耗。
       ```
       bundle.putBoolean(CLOUD_APP_HIGH_PERFORMANCE, false); // 仅抬起补帧
       ```
       
     
     
     如果未配置该参数，则按照默认打开策略执行。
     
   
   
   
   
 
