更新时间:2026-04-15 GMT+08:00
分享

快速开始

本章节以开发Android客户端为例,帮助用户理解基础的Android客户端开发流程以及如何集成KooPhone Android端SDK,主要包含2两个基本功能,发起云手机连接与断开云手机连接。本章节所有开发示例均为展示基本功能的实现,仅供参考。

准备工作

准备开发Android客户端的环境/工具。

  • 开发工具:Android Studio (推荐最新版本)
  • JDK版本:>= 1.8
  • 最小SDK:API 23或更高

步骤一 :创建工程

  1. 创建新工程。

    1. 打开Android Studio,单击“New Project”。
    2. 选择“Empty Views Activity”模板,单击“Next”。
    3. 配置以下参数:
      • Name:CloudPhoneController
      • Package name:com.example. cloudphonecontroller
      • Language:选择Java
      • Minimum SDK:选择API 24或更高
    4. 配置完成后单击“Finish”,等待项目初始化完成。

  2. 集成aar SDK(预置在指定目录)。

    1. 通过下载KooPhone端aar SDK包并解压缩,获取KooPhone端aar SDK。
    2. 创建libs文件夹:如果项目根目录下没有libs文件夹,请在“Project”视图模式下,右键单击app模块,选择“New > Directory”,输入“libs”并单击“确认”。
    3. 复制AAR文件:将下载好的aar SDK文件复制粘贴到2.b创建的app/libs目录下。

  3. 配置项目依赖。

    1. 打开app模块下的build.gradle文件(Module: app)。
    2. 在android代码块中,添加sourceSets配置,确保Gradle能识别libs目录下的本地依赖。
      android {
          // ... 其他配置
      
      
          sourceSets {
              getByName("main") {
                  jniLibs.srcDirs("libs")
              }
          }
    3. 在dependenciesaar代码块中,添加对aar文件的引用。
      dependencies {
          
          implementation fileTree(dir: 'libs', include: '*.aar') //原有arr的依赖
      
          // 其他依赖
          implementation 'androidx.appcompat:appcompat:1.6.1'
          // ...
      }
    4. 单击编辑器右上角的 “Sync Now” 同步项目。

  4. 准备基础配置。

    1. 配置基础参数类。

      项目中通过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也必须提前配置,否则后续无法获取实例连接信息。

    2. 在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顺序实现:

图1 客户端启动串流
  1. 准备请求与响应模型。

    为了让Retrofit能正常请求并解析数据,需要先准备请求体和响应体模型。

    1. 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;
          }
      }
    2. 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 +
                      '}';
          }
      }
    3. 业务接口响应模型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

  2. 定义接口服务。定义接口服务后,项目就具备了请求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用于获取指定实例的连接信息和串流参数。

  3. 获取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;
    }

  4. 在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();
        }

    拉起鉴权流程的执行顺序如下:

    1. 调用IAM接口获取Token。
    2. 校验实例ID是否为空。
    3. 使用IAM Token获取设备连接信息。
    4. 接口返回成功后,需要校验关键字段,并进入串流启动流程。
    5. 校验成功进入串流流程。

    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();
}
  1. 初始化SDK(初始化SDK后,客户端已经具备调用SDK能力,但还未开始串流)。

    private void initCloudPhoneSDK() {
        CloudPhoneClient.init(this, null, true);
        CloudPhoneClient.playerForceTextureView(false);
    }
    • CloudPhoneClient.init(...):初始化SDK。
    • playerForceTextureView(false):设置播放器渲染模式。

  2. 初始化页面承载容器。

    串流画面需要有承载容器,本示例使用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与页面承载组件

    如果缺少初始化页面承载容器这步操作,后续即使鉴权成功、串流启动成功,也无法正常显示画面。

  3. 发起云手机连接。

    1. 构建串流启动参数。

      在调用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时,至少要保证以上字段都被正确组装。

    2. 启动串流。

      客户端在获取鉴权信息后,可以调用以下方法启动串流。

      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);
          }

  4. 关闭云手机连接。示例代码如下:

    private void disconnectCloudPhone() {
            if (!isConnected) {
                showToast("当前未连接");
                return;
            }
            // 停止串流
            CloudPhoneClient.stop();
            isConnected = false;
            tvStatus.setText("状态:未连接");
            showToast("已断开连接");
            blackOverlay.setVisibility(View.VISIBLE); // 显示黑色遮罩
            tvStatus.setVisibility(View.VISIBLE);
        }

客户端demo功能演示

  • 初始化云机。
    1. 在MainActivity的onCreate()中初始化SDK和UI。
    2. 创建并绑定PlayerFragment作为画面容器。
    图2为初始化后的画面。
    图2 初始化云机
  • 连接云手机。在云手机中单击“连接云手机”,连接云手机后的界面如图3所示。

    连接云手机后,后台会执行如下操作:

    1. 调用IAM服务获取Token。
    2. 使用Token获取云手机连接信息。
    3. 构建串流参数Bundle。
    4. 调用SDK的start()方法启动串流。
    5. 隐藏遮罩层,显示云手机画面。
    图3 连接云手机
  • 断开云手机的连接。在云手机中单击“断开连接”,连接云手机后的界面如图4所示。

    断开连接云手机后,后台会执行如下操作:

    1. 调用SDK的stop()方法停止串流。
    2. 显示遮罩层。
    3. 更新连接状态。
    图4 断开连接
  • 状态管理。
    • 通过TextView实时显示连接状态。
    • 使用Toast提示操作结果。
    • 通过遮罩层控制画面显示。

相关文档