更新时间:2026-05-22 GMT+08:00
分享

开发指导

本章节通过一个典型客户端应用的接入示例,介绍如何集成SDK并完成最小可运行环境搭建。示例包含初始化SDK、获取鉴权信息、启动主界面串流以及基础配置等内容,帮助开发者完成快速接入。

前提条件

开发者在阅读本章节前,默认已经完成快速开始中的步骤一~步骤三,包含以下内容:创建Android工程、集成aar SDK包、配置Gradle依赖、配置BuildConfig参数、准备Config.java、Constant.java等基础参数、准备IAM鉴权参数、创建基础页面与MainActivity、验证基础串流启动能力。默认开发者已具备“快速开始”中的最小接入能力。

步骤一:准备样式

在实际示例工程中,除了完成串流能力接入,还需要准备一个适合展示和交互的页面结构。该页面不仅用于承载串流画面,还需要满足应用墙展示、系统控制按钮操作以及状态过渡展示等需求。

  1. 设计页面布局目标。示例页面建议包含以下几个区域:

    • 串流承载区域:用于放置PlayerFragment,承载远端画面显示。
    • 遮罩层:在未连接、连接中或应用墙展示阶段,可通过半透明黑色遮罩控制页面层级和视觉状态。
    • 应用墙区域:用于展示已安装应用列表,开发者可采用按钮、列表或宫格形式。
    • 按键操作区域:用于提供返回主屏、关闭应用、退出等基础控制能力。

  2. 最简布局示例。

    以下示例为本开发示例中使用的页面布局,该布局相较于快速开始的页面,增加了应用墙、任务管理和关闭应用等个性化功能区域,删除了断连和连接按钮,首次进入应用墙默认发起连接,进入应用会发起音视频通道连接,再次返回应用墙会切换回数据通道连接。

    该布局相较于快速开始的最小示例,主要体现以下个性化设计:

    <?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 业务页面

步骤二:初始化SDK并启动串流

该步骤描述示例工程中如何组织主页面启动流程,并在应用启动后自动完成串流初始化。本示例的启动流程如下:

图2 初始化SDK并启动串流流程
  • 快速开始中已完整说明初始化基础SDK、IAM鉴权模型、接口定义及基础串流启动流程,因此本章仅保留示例工程中的组织方式和增强配置。
  • 快速开始的区别在于,快速开始强调“手动连接/断开”的最小流程,本示例强调“页面启动后自动拉起串流,并为后续应用管理能力做准备”。
  1. 启动MainActivity组织方式。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    
    
        // 初始化SDK
        initCloudPhoneSDK();
    
    
        // 初始化UI
        initUI();
    
    
        // 开始鉴权并启动串流
        fetchTokenAndStartSteam();
    }

    initCloudPhoneSDK()和initUI()的基础能力可参见快速开始的内容,本示例保留相同入口结构,但在启动后直接进入鉴权和串流流程,而不是等待用户手动单击连接按钮。

  2. 发起鉴权流程。

    获取IAM Token、实例连接信息请求、ApiService定义以及AuthRequest、AuthResponse、ApiResponse等模型,均已在快速开始中完整说明。本示例直接复用已有能力,仅保留在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. 构建个性化串流启动参数。

    快速开始中已经说明了最小必需的串流参数: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. 注册回调并启动串流。

    快速开始中,仅演示了最小串流启动方式。本示例中,除了播放器回调外,还同时注册应用回调,以支持后续应用墙、应用启动、应用关闭等能力。

    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 应用示例

  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 调整界面显示

  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); // 仅抬起补帧

      如果未配置该参数,则按照默认打开策略执行。

相关文档