开发指导
本章节通过一个典型客户端应用的接入示例,介绍如何集成SDK并完成最小可运行环境搭建。示例包含初始化SDK、获取鉴权信息、启动主界面串流以及基础配置等内容,帮助开发者完成快速接入。
前提条件
开发者在阅读本章节前,默认已经完成快速开始中的步骤一~步骤三,包含以下内容:创建Android工程、集成aar SDK包、配置Gradle依赖、配置BuildConfig参数、准备Config.java、Constant.java等基础参数、准备IAM鉴权参数、创建基础页面与MainActivity、验证基础串流启动能力。默认开发者已具备“快速开始”中的最小接入能力。
步骤一:准备样式
在实际示例工程中,除了完成串流能力接入,还需要准备一个适合展示和交互的页面结构。该页面不仅用于承载串流画面,还需要满足应用墙展示、系统控制按钮操作以及状态过渡展示等需求。
- 设计页面布局目标。示例页面建议包含以下几个区域:
- 串流承载区域:用于放置PlayerFragment,承载远端画面显示。
- 遮罩层:在未连接、连接中或应用墙展示阶段,可通过半透明黑色遮罩控制页面层级和视觉状态。
- 应用墙区域:用于展示已安装应用列表,开发者可采用按钮、列表或宫格形式。
- 按键操作区域:用于提供返回主屏、关闭应用、退出等基础控制能力。
- 最简布局示例。
以下示例为本开发示例中使用的页面布局,该布局相较于快速开始的页面,增加了应用墙、任务管理和关闭应用等个性化功能区域,删除了断连和连接按钮,首次进入应用墙默认发起连接,进入应用会发起音视频通道连接,再次返回应用墙会切换回数据通道连接。
该布局相较于快速开始的最小示例,主要体现以下个性化设计:
<?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并启动串流
该步骤描述示例工程中如何组织主页面启动流程,并在应用启动后自动完成串流初始化。本示例的启动流程如下:
- 启动MainActivity组织方式。
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 初始化SDK initCloudPhoneSDK(); // 初始化UI initUI(); // 开始鉴权并启动串流 fetchTokenAndStartSteam(); }initCloudPhoneSDK()和initUI()的基础能力可参见快速开始的内容,本示例保留相同入口结构,但在启动后直接进入鉴权和串流流程,而不是等待用户手动单击连接按钮。
- 发起鉴权流程。
获取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); } }); } - 获取实例连接信息。
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()); } }); }本示例的重点是说明如何在页面启动阶段直接串联“鉴权--串流启动”。
- 构建个性化串流启动参数。
快速开始中已经说明了最小必需的串流参数: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; } - 注册回调并启动串流。
在快速开始中,仅演示了最小串流启动方式。本示例中,除了播放器回调外,还同时注册应用回调,以支持后续应用墙、应用启动、应用关闭等能力。
- 回调对象示例:
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) { // 串流建立失败 } } - 启动串流。
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跑通之后,可以根据需求补齐基础应用管理能力。
- 进入app应用墙(查看已安装应用列表)。
- 当串流真正启动后,在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; } - 处理应用列表结果:
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(); }完成这一步串流建立成功后,客户端会自动获取应用列表,页面按钮显示应用名称,用户可以单击按钮启动对应应用。
- 当串流真正启动后,在onStreamStarted()回调中调用getInstalledApps()获取应用列表,然后在页面中显示为应用墙。获取应用列表:
- 进入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 应用示例
- 关闭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)可以关闭当前应用,关闭应用后,页面会重新回到应用墙状态。
- 查看正在运行中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 调整界面显示
- 针对低算力场景的端侧串流初始化配置。
- 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执行模式。
- 当配置为1时表示使用Ignition执行器(IGNITION_EXECUTOR),任务会通过Ignition调度执行。Ignition内部通过高精度定时器 + 任务队列 + 线程池的方式进行任务调度,定时器周期触发Update(),检查需要执行的任务,再将任务提交到线程池中执行,从而统一管理异步任务、延时任务和周期任务,减少线程创建开销并提升系统稳定性。
- 补帧策略:用于在用户操作时提升画面流畅度,通过参数CLOUD_APP_HIGH_PERFORMANCE控制。端侧通过Bundle传入一个boolean值来配置补帧行为。
- true:开启高性能模式,手指按下和抬起都会触发补帧,可以更快更新画面,操作响应更流畅。
bundle.putBoolean(CLOUD_APP_HIGH_PERFORMANCE, true); // 按下和抬起都补帧
- false:关闭高性能模式,只在手指抬起时触发补帧,减少补帧次数,从而降低性能消耗。
bundle.putBoolean(CLOUD_APP_HIGH_PERFORMANCE, false); // 仅抬起补帧
如果未配置该参数,则按照默认打开策略执行。
- true:开启高性能模式,手指按下和抬起都会触发补帧,可以更快更新画面,操作响应更流畅。
- Ignition策略:用于控制底层任务的执行方式,通过参数CLOUD_APP_EVENT_EXECUTOR进行配置。端侧通过Bundle传入一个int值来指定任务执行器类型。