快速开始
本章节以开发Android客户端为例,帮助用户理解基础的Android客户端开发流程以及如何集成KooPhone Android端SDK,主要包含2两个基本功能,发起云手机连接与断开云手机连接。本章节所有开发示例均为展示基本功能的实现,仅供参考。
准备工作
准备开发Android客户端的环境/工具。
- 开发工具:Android Studio (推荐最新版本)
- JDK版本:>= 1.8
- 最小SDK:API 23或更高
步骤一 :创建工程
- 创建新工程。
- 打开Android Studio,单击“New Project”。
- 选择“Empty Views Activity”模板,单击“Next”。
- 配置以下参数:
- Name:CloudPhoneController
- Package name:com.example. cloudphonecontroller
- Language:选择Java
- Minimum SDK:选择API 24或更高
- 配置完成后单击“Finish”,等待项目初始化完成。
- 集成aar SDK(预置在指定目录)。
- 通过下载KooPhone端aar SDK包并解压缩,获取KooPhone端aar SDK。
- 创建libs文件夹:如果项目根目录下没有libs文件夹,请在“Project”视图模式下,右键单击app模块,选择“New > Directory”,输入“libs”并单击“确认”。
- 复制AAR文件:将下载好的aar SDK文件复制粘贴到2.b创建的app/libs目录下。
- 配置项目依赖。
- 打开app模块下的build.gradle文件(Module: app)。
- 在android代码块中,添加sourceSets配置,确保Gradle能识别libs目录下的本地依赖。
android { // ... 其他配置 sourceSets { getByName("main") { jniLibs.srcDirs("libs") } } - 在dependenciesaar代码块中,添加对aar文件的引用。
dependencies { implementation fileTree(dir: 'libs', include: '*.aar') //原有arr的依赖 // 其他依赖 implementation 'androidx.appcompat:appcompat:1.6.1' // ... } - 单击编辑器右上角的 “Sync Now” 同步项目。
- 准备基础配置。
- 配置基础参数类。
项目中通过BuildConfig和Constant统一管理关键参数,开发者至少需要准备以下参数:
- NSTANCE_ID:实例ID,用于获取实例连接信息。
- USER_ID:串流启动时使用的用户标识。
- SCREEN_WIDTH / SCREEN_HEIGHT:目标分辨率。
- AUDIO_SAMPLE_RATE / AUDIO_CHANNEL_COUNT:音频播放参数。
- USER_NAME / USER_PASSWORD / DOMAIN_NAME / PROJECT_NAME:IAM鉴权信息。
- IAM_BASE_URL / API_BASE_URL:接口地址。
Config.java示例代码如下:
public class Config { public static final String USER_ID = BuildConfig.USER_ID; public static final int SCREEN_WIDTH = BuildConfig.SCREEN_WIDTH; public static final int SCREEN_HEIGHT = BuildConfig.SCREEN_HEIGHT; public static final int AUDIO_SAMPLE_RATE = BuildConfig.AUDIO_SAMPLE_RATE; public static final int AUDIO_CHANNEL_COUNT = BuildConfig.AUDIO_CHANNEL_COUNT; }Constant.java示例代码如下:
public class Constant { public final static String INSTANCE_ID = com.example.cloudphonecontroller.BuildConfig.INSTANCE_ID; // 请求IAM接口相关参数 public final static String IAM_BASE_URL = ""; public final static String USER_NAME = ""; public final static String USER_PASSWORD = ""; public final static String DOMAIN_NAME = ""; public final static String PROJECT_NAME = ""; // 请求业务接口相关参数 public final static String API_BASE_URL = ""; }USER_NAME、USER_PASSWORD、DOMAIN_NAME、PROJECT_NAME需要开发者替换为自己的租户账号信息。 INSTANCE_ID也必须提前配置,否则后续无法获取实例连接信息。
- 在Gradle中配置BuildConfig字段。
由于Config和Constant中会读取BuildConfig字段,因此还需要在app/build.gradle中补充如下配置:
android { defaultConfig { buildConfigField "String", "INSTANCE_ID", "\"你的实例ID\"" buildConfigField "String", "USER_ID", "\"43740902807\"" buildConfigField "int", "SCREEN_WIDTH", "1920" buildConfigField "int", "SCREEN_HEIGHT", "1080" buildConfigField "int", "AUDIO_SAMPLE_RATE", "48000" buildConfigField "int", "AUDIO_CHANNEL_COUNT", "2" } }
- 配置基础参数类。
步骤二:获取鉴权信息
请求鉴权信息之前,需要做如下准备:
- 已购买KooPhone云手机产品。
- 已获取IAM租户鉴权信息,可参考认证鉴权。
注意:IAM租户鉴权信息需严格遵循官网指导文档开发意见获取,不可频繁调用,避免产生不必要的问题。
- 参考租户实例串流前获取设备的device_token,获取串流所需参数:
- device_token:鉴权Token
- device_id:设备ID
- signaling_url:信令服务器地址
建议开发者严格按图1顺序实现:
- 准备请求与响应模型。
为了让Retrofit能正常请求并解析数据,需要先准备请求体和响应体模型。
- IAM请求模型AuthRequest。
package com.example.cloudphonecontroller.model.request; public class AuthRequest { public Auth auth; public static class Auth { public Identity identity; public Scope scope; } public static class Identity { public String[] methods = {"password"}; public Password password; } public static class Password { public User user; } public static class User { public Domain domain; public String name; public String password; } public static class Domain { public String name; } public static class Scope { public Project project; } public static class Project { public String name; } } - IAM响应模型AuthResponse。
package com.example.cloudphonecontroller.model.response; public class AuthResponse { public Token token; public static class Token { public String id; // 其他token字段 } @Override public String toString() { return "AuthResponse{" + "token=" + token + '}'; } } - 业务接口响应模型ApiResponse。
package com.example.cloudphonecontroller.model.response; public class ApiResponse { private Data data; private String error_code; private String error_msg; @Override public String toString() { return "ApiResponse{" + "data=" + data + ", error_code='" + error_code + '\'' + ", error_msg='" + error_msg + '\'' + '}'; } public static class Data { private Resource resource; private String device_token; public static class Resource { private Sdk sdk; private Rtc rtc; private String device_id; private String kp_id; public static class Sdk { private Internal internal; private External external; @Override public String toString() { return "Sdk{" + "internal=" + internal + ", external=" + external + '}'; } public static class Internal { private String address; private Integer aport; private Integer atype; private String address_ipv6; // Getters and Setters public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public Integer getAport() { return aport; } public void setAport(Integer aport) { this.aport = aport; } public Integer getAtype() { return atype; } public void setAtype(Integer atype) { this.atype = atype; } public String getAddress_ipv6() { return address_ipv6; } public void setAddress_ipv6(String address_ipv6) { this.address_ipv6 = address_ipv6; } @Override public String toString() { return "Internal{" + "address='" + address + '\'' + ", aport=" + aport + ", atype=" + atype + ", address_ipv6='" + address_ipv6 + '\'' + '}'; } } public static class External { private String address; private Integer aport; private Integer atype; private String address_ipv6; private String address_adn; // Getters and Setters public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public Integer getAport() { return aport; } public void setAport(Integer aport) { this.aport = aport; } public Integer getAtype() { return atype; } public void setAtype(Integer atype) { this.atype = atype; } public String getAddress_ipv6() { return address_ipv6; } public void setAddress_ipv6(String address_ipv6) { this.address_ipv6 = address_ipv6; } public String getAddress_adn() { return address_adn; } public void setAddress_adn(String address_adn) { this.address_adn = address_adn; } @Override public String toString() { return "External{" + "address='" + address + '\'' + ", aport=" + aport + ", atype=" + atype + ", address_ipv6='" + address_ipv6 + '\'' + '}'; } } // Getters and Setters public Internal getInternal() { return internal; } public void setInternal(Internal internal) { this.internal = internal; } public External getExternal() { return external; } public void setExternal(External external) { this.external = external; } } public static class Rtc { private IceSignaling ice_signaling; @Override public String toString() { return "Rtc{" + "ice_signaling=" + ice_signaling + '}'; } public static class IceSignaling { private String signaling_url; private String expired_time; private List<Object> ice_servers; // Getters and Setters public String getSignaling_url() { return signaling_url; } public void setSignaling_url(String signaling_url) { this.signaling_url = signaling_url; } public String getExpired_time() { return expired_time; } public void setExpired_time(String expired_time) { this.expired_time = expired_time; } public List<Object> getIce_servers() { return ice_servers; } public void setIce_servers(List<Object> ice_servers) { this.ice_servers = ice_servers; } @Override public String toString() { return "IceSignaling{" + "signaling_url='" + signaling_url + '\'' + ", expired_time='" + expired_time + '\'' + ", ice_servers=" + ice_servers + '}'; } } // Getters and Setters public IceSignaling getIce_signaling() { return ice_signaling; } public void setIce_signaling(IceSignaling ice_signaling) { this.ice_signaling = ice_signaling; } } // Getters and Setters public Sdk getSdk() { return sdk; } public void setSdk(Sdk sdk) { this.sdk = sdk; } public Rtc getRtc() { return rtc; } public void setRtc(Rtc rtc) { this.rtc = rtc; } public String getDevice_id() { return device_id; } public void setDevice_id(String device_id) { this.device_id = device_id; } public String getKp_id() { return kp_id; } public void setKp_id(String kp_id) { this.kp_id = kp_id; } @Override public String toString() { return "Resource{" + "sdk=" + sdk + ", rtc=" + rtc + ", device_id='" + device_id + '\'' + ", kp_id='" + kp_id + '\'' + '}'; } } // Getters and Setters public Resource getResource() { return resource; } public void setResource(Resource resource) { this.resource = resource; } public String getDevice_token() { return device_token; } public void setDevice_token(String device_token) { this.device_token = device_token; } @Override public String toString() { return "Data{" + "resource=" + resource + ", device_token='" + device_token + '\'' + '}'; } } // Getters and Setters public Data getData() { return data; } public void setData(Data data) { this.data = data; } public String getError_code() { return error_code; } public void setError_code(String error_code) { this.error_code = error_code; } public String getError_msg() { return error_msg; } public void setError_msg(String error_msg) { this.error_msg = error_msg; } }该类用于接收实例连接信息,开发者至少需要保证以下字段可解析成功:
- device_token
- external.address
- external.aport
- ice_signaling.signaling_url
- IAM请求模型AuthRequest。
- 定义接口服务。定义接口服务后,项目就具备了请求IAM接口和业务接口的能力。
ApiService.java
import io.reactivex.Observable; import retrofit2.Response; import retrofit2.http.Body; import retrofit2.http.Headers; import retrofit2.http.POST; import retrofit2.http.Path; import retrofit2.http.Query; public interface ApiService { /** * 获取华为云鉴权token * * @return */ @POST("v3/auth/tokens") @Headers("Content-Type: application/json") public Observable<Response<ResponseBean<AuthResponse>>> authenticate( @Body AuthRequest authRequest, @Query("nocatalog") boolean noCatalog ); /** * 获取串流信息 * * @return */ @POST("v1/instances/{instance_id}/auth") public Observable<ApiResponse> getDeviceInfo( @Path("instance_id") String id ); }- v3/auth/tokens用于用户身份认证,向IAM服务获取访问令牌(Token)。
- v1/instances/{instance_id}/auth用于获取指定实例的连接信息和串流参数。
- 获取IAM Token。
项目中通过TokenManager.fetchToken()获取IAM Token(IAM的调用面向管理者, token获取需要进行缓存)。
public static synchronized String fetchToken() throws IOException, RuntimeException { AuthRequest authRequest = new AuthRequest(); authRequest.auth = new AuthRequest.Auth(); authRequest.auth.identity = new AuthRequest.Identity(); authRequest.auth.identity.password = new AuthRequest.Password(); authRequest.auth.identity.password.user = new AuthRequest.User(); authRequest.auth.identity.password.user.domain = new AuthRequest.Domain(); authRequest.auth.identity.password.user.domain.name = Constant.DOMAIN_NAME; authRequest.auth.identity.password.user.name = Constant.USER_NAME; authRequest.auth.identity.password.user.password = Constant.USER_PASSWORD; authRequest.auth.scope = new AuthRequest.Scope(); authRequest.auth.scope.project = new AuthRequest.Project(); authRequest.auth.scope.project.name = Constant.PROJECT_NAME; Response<ResponseBean<AuthResponse>> responseBeanResponse = IAMServiceManager.createService(ApiService.class) .authenticate(authRequest, true) .blockingFirst(); currentToken = responseBeanResponse.headers().get("X-Subject-Token"); return currentToken; } - 在MainActivity中拉起鉴权流程。
获取IAM Token后,就可以在MainActivity的connectCloudPhone()方法中拉起完整鉴权流程并校验关键字段,进入串流启动流程。
private void connectCloudPhone() { // 检查连接状态 if (isConnected) { showToast("已经连接"); return; } tvStatus.setText("状态:正在获取鉴权信息..."); // 在子线程中进行网络请求 new Thread(() -> { try { // 1.获取IAM Token String token = TokenManager.fetchToken(); Log.info(TAG,"get token is : " + token); //2.检查实例ID是否为空 if(Constant.INSTANCE_ID.isEmpty()){ this.runOnUiThread(() -> Toast.makeText(getApplicationContext(), "请在gradle.properties里填入 INSTANCE_ID", Toast.LENGTH_LONG).show()); return; } // 3.使用IAM Token获取设备连接信息 Observable<ApiResponse> deviceInfo = KooPhoneServiceManager .createService(ApiService.class, token) .getDeviceInfo(Constant.INSTANCE_ID); // 4.切换到主线程处理结果 runOnUiThread(() -> { deviceInfo.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new DataSubcriber<ApiResponse>() { @Override public void onNext(ApiResponse response) { Log.info(TAG,"ApiResponse " + response.toString()); if (response == null || response.getData() == null || response.getData().getDevice_token() == null) { tvStatus.setText("状态:鉴权失败"); showToast("获取的Token为null,请检查实例状态"); return; } // 5.启动串流 startStream(response); tvStatus.setText("状态:已连接"); isConnected = true; blackOverlay.setVisibility(View.GONE); tvStatus.setVisibility(View.GONE); } @Override public void onError(Throwable e) { tvStatus.setText("状态:鉴权失败"); showToast("获取Token失败: " + e.getMessage()); } }); }); } catch (Exception e) { runOnUiThread(() -> { tvStatus.setText("状态:连接失败"); showToast("连接异常: " + e.getMessage()); }); } }).start(); }拉起鉴权流程的执行顺序如下:
- 调用IAM接口获取Token。
- 校验实例ID是否为空。
- 使用IAM Token获取设备连接信息。
- 接口返回成功后,需要校验关键字段,并进入串流启动流程。
- 校验成功进入串流流程。
MainActivity中拉起鉴权后,客户端可以正常获取IAM Token、获取实例连接信息、在业务响应有效时进入串流启动流程。
步骤三:开发Demo及启动串流
开发MainActivity可以实现一个最小客户端的基础初始化,使应用具备正常启动页面、初始化SDK、初始化UI的能力。
onCreate是MainActivity创建时执行的核心方法,用于完成初始化工作。示例如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化SDK
initCloudPhoneSDK();
// 初始化UI
initUI();
} - 初始化SDK(初始化SDK后,客户端已经具备调用SDK能力,但还未开始串流)。
private void initCloudPhoneSDK() { CloudPhoneClient.init(this, null, true); CloudPhoneClient.playerForceTextureView(false); }- CloudPhoneClient.init(...):初始化SDK。
- playerForceTextureView(false):设置播放器渲染模式。
- 初始化页面承载容器。
串流画面需要有承载容器,本示例使用PlayerFragment作为串流承载对象,并在initUI()中完成绑定。初始化页面时,需要创建PlayerFragment并绑定到SDK,用于承载后续串流画面。
private void initUI() { //设置基础组件遮罩,状态信息,串流/断流按钮等; blackOverlay = findViewById(R.id.black_overlay); tvStatus = findViewById(R.id.tv_status); btnConnect = findViewById(R.id.btn_connect); btnDisconnect = findViewById(R.id.btn_disconnect); // 初始化PlayerFragment if (mFragment == null) { mFragment = new PlayerFragment(); getFragmentManager() .beginTransaction() .add(R.id.containerf, mFragment, "player_fragment") .commit(); CloudPhoneClient.bindFragment(mFragment); } // 设置按钮监听 btnConnect.setOnClickListener(v -> connectCloudPhone()); btnDisconnect.setOnClickListener(v -> disconnectCloudPhone()); }初始化页面承载容器时,可实现如下操作:
- 设置基础组件按钮
- 创建PlayerFragment
- 将PlayerFragment添加到页面的containerf容器中
- 调用CloudPhoneClient.bindFragment(m_fragment) ,绑定SDK与页面承载组件
如果缺少初始化页面承载容器这步操作,后续即使鉴权成功、串流启动成功,也无法正常显示画面。
- 发起云手机连接。
- 构建串流启动参数。
在调用SDK启动串流前,需要构建连接所需参数(可自定义传入,参数在步骤二:获取鉴权信息中获取)。 这些参数通常包括:
- 会话Token
- 远端服务地址
- 服务端口
- 视频编码配置
- 用户标识
- 分辨率配置
- 各类业务开关
构建串流启动参数示例代码如下:
private Bundle createBundle(ApiResponse authResponse) { Bundle bundle = new Bundle(); if (authResponse != null && null != authResponse.getData()) { ApiResponse.Data data = authResponse.getData(); // 会话Token bundle.putString(LAUNCH_KEY_TOKEN, TextUtils.isEmpty(data.getDevice_token()) ? "" : data.getDevice_token()); ApiResponse.Data.Resource resource = data.getResource(); if (null != resource) { ApiResponse.Data.Resource.Sdk sdk = resource.getSdk(); if (null != sdk) { ApiResponse.Data.Resource.Sdk.External external = sdk.getExternal(); if (null != external) { //服务地址 bundle.putString(LAUNCH_KEY_ADDRESS, external.getAddress()); //服务端口 bundle.putInt(LAUNCH_KEY_PORT, external.getAport()); } } } } // 用户标识 bundle.putString("userId", Config.USER_ID); // 自适应分辨率 bundle.putBoolean("free_aspect", true); // 目标分辨率 bundle.putInt("screen_width", Config.SCREEN_WIDTH);//任意分辨率宽 bundle.putInt("screen_height", Config.SCREEN_HEIGHT);//任意分辨率高 return bundle; }关键字段说明:
- LAUNCH_KEY_TOKEN:接口返回的device_token
- LAUNCH_KEY_ADDRESS:实例外网地址
- LAUNCH_KEY_PORT:实例连接端口
- userId:用户标识
- screen_width / screen_height:分辨率
- free_aspect:是否开启自由比例
开发者在搭建demo时,至少要保证以上字段都被正确组装。
- 启动串流。
private void startStream(ApiResponse response) { Bundle bundle = createBundle(response); CloudPhoneCallBack callBack = CloudPhoneCallBack.getInstance(); EmulationTransport.init(this); // 设置回调 CloudPhoneClient.setPlayerCallback(callBack); CloudPhoneClient.setPlayerType(HMTP_PLAYER); // 启动串流 CloudPhoneClient.start(mFragment, bundle); }
- 构建串流启动参数。
- 关闭云手机连接。示例代码如下:
private void disconnectCloudPhone() { if (!isConnected) { showToast("当前未连接"); return; } // 停止串流 CloudPhoneClient.stop(); isConnected = false; tvStatus.setText("状态:未连接"); showToast("已断开连接"); blackOverlay.setVisibility(View.VISIBLE); // 显示黑色遮罩 tvStatus.setVisibility(View.VISIBLE); }
客户端demo功能演示
- 初始化云机。
- 在MainActivity的onCreate()中初始化SDK和UI。
- 创建并绑定PlayerFragment作为画面容器。
图2为初始化后的画面。



