通过SDK进行云手机画面的本地旋转

在云手机使用场景中,用户操作的是运行于云端的虚拟设备,而本地终端仅作为显示与交互的载体。当云手机发生屏幕方向变化(如从竖屏切换至横屏)时,若本地客户端无法同步响应,将导致画面显示异常(如倒置、拉伸、黑边)或交互错位,严重影响用户体验。本文介绍如何在AndroidiOS客户端SDK中实现本地屏幕方向与云端旋转指令的精准同步机制,确保本地界面与视频流始终与云端设备保持一致的屏幕朝向,从而提供沉浸式、无感知的远程操作体验。

背景

传统移动端应用通常依赖设备传感器或系统配置自动响应屏幕旋转。然而在云手机架构下,屏幕方向应由云端虚拟设备的状态驱动,而非本地物理设备的姿态。若直接启用系统自动旋转,会导致以下问题:

  • 本地旋转与云端状态不同步,画面显示错误。

  • 用户无意中转动手机即触发旋转,干扰正常操作。

  • 视频渲染层(如 SurfaceView/TextureView 或 Metal/OpenGL View)未适配新方向,出现裁剪或形变。

因此必须主动接管旋转控制权,由云端下发明确的方向指令,本地客户端据此强制切换界面方向并同步调整视频渲染逻辑。

方案概述

本方案在AndroidiOS平台上分别实现了以下核心能力:

  • 统一的旋转指令接收机制

    • 通过自定义DataChannel(如 wy_vdagent_default_dc)接收来自云端的旋转命令。

    • 使用标准化协议解析旋转参数(如 rotation = 0/1/3 分别对应竖屏、左横屏、右横屏)。

  • 本地界面方向强制同步

    • Android:调用setRequestedOrientation()动态锁定Activity方向。

    • iOS:重写supportedInterfaceOrientations 并结合setNeedsUpdateOfSupportedInterfaceOrientations(iOS 16+)或私有API实现方向切换。

    • 两者均禁用设备自动旋转(shouldAutorotate = false / 禁用传感器响应),确保仅响应云端指令。

  • 视频渲染层精准适配

    • Android:启用TextureView并调用setSurfaceRotation()直接旋转底层纹理,避免画面失真。

    • iOS:通过CGAffineTransformStreamView进行旋转,并动态调整其中心点,确保画面居中且比例正确。

  • 多方向支持

    • 同时支持Portrait(竖屏)、Landscape Left(左横屏)和Landscape Right(右横屏)三种方向。

    • 正确处理Home键位置差异,提升iOS设备在横屏下的交互一致性。

实现步骤

Android SDK本地旋转处理

// 禁止云手机横竖屏自适应
bundle.putBoolean(StreamView.CONFIG_DISABLE_ORIENTATION_CLOUD_CONTROL, true);
// 使用textureView
mStreamView = findViewById(R.id.stream_view);
mStreamView.enableTextureView(true);
//旋转处理
mStreamView.getASPEngineDelegate().addDataChannel(new DataChannel("wy_vdagent_default_dc") {
            @Override
            protected void onReceiveData(byte[] buf) {
                String str = "";
                try {
                    str = new String(buf, "UTF-8");
                } catch (UnsupportedEncodingException e) {
                    str = new String(buf);
                }
                Log.i(TAG, "wy_vdagent_default_dc dc received " + buf.length + " bytes data:" + str);
                CommandUtils.parseCommand(str, new CommandUtils.CommandListener() {
                    @Override
                    public void onCameraAuthorize() {
                        checkStartCpd();
                    }
                    @Override
                    public void onRotation(int rotation) {
                        runOnUiThread(() -> {
                            mStreamView.setSurfaceRotation(rotation);
                            if (rotation == 1) {
                                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
                            } else if (rotation == 3) {
                                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
                            } else {
                                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
                            }
                        });
                    }
                    @Override
                    public void onUnknownCommand(String cmd) {
                        showError("未知命令:" + cmd);
                    }
                });
            }
            @Override
            protected void onConnectStateChanged(DataChannelConnectState state) {
                Log.i(TAG, "wy_vdagent_default_dc dc connection state changed to " + state);
            }
        });

iOS SDK本地旋转处理

  1. list.info配置Supported interface orientations,启用应用对竖屏、左横屏、右横屏的支持。

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <array>
    	<string>UIInterfaceOrientationPortrait</string>
    	<string>UIInterfaceOrientationLandscapeLeft</string>
    	<string>UIInterfaceOrientationLandscapeRight</string>
    </array>
    </plist>
    
  2. 创建BaseViewController实现旋转相关逻辑。

    @implementation BaseViewController {
        NSInteger mRoration;
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.view.backgroundColor = [UIColor whiteColor];
    }
    
    - (void)switchRoration:(NSInteger)roration {
        mRoration = roration;
        // 强制旋转到横屏(iOS 16+ 推荐方式)
        if (@available(iOS 16.0, *)) {
            [self setNeedsUpdateOfSupportedInterfaceOrientations];
        } else {
            // 旧方法(已废弃,但兼容)
            NSNumber *value = @([self supportedInterfaceOrientations]);
            [[UIDevice currentDevice] setValue:value forKey:@"orientation"];
        }
    }
    
    - (UIInterfaceOrientationMask)supportedInterfaceOrientations {
        switch (mRoration) {
            case 1:
                return UIInterfaceOrientationMaskLandscapeLeft;
            case 3:
                return UIInterfaceOrientationMaskLandscapeRight;
            default:
                return UIInterfaceOrientationMaskPortrait;
        }
    }
    
    - (BOOL)shouldAutorotate {
        return false; // 允许自动旋转到 supported 的方向
    }
    
    // 可选:指定首选方向(用于 presentation)
    - (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
        switch (mRoration) {
            case 1:  return UIInterfaceOrientationLandscapeLeft;
            case 3:  return UIInterfaceOrientationLandscapeRight;
            default: return UIInterfaceOrientationPortrait;
        }
    }
    
    @end
  3. 建立从云端到本地UI与渲染层的指令响应通路。

    self.esdAgent = [[DemoEDSAgentChannel alloc] initWithParameter:DATA_CHANNEL_NAME];
    self.esdAgent.streamView = self.streamView;
    self.esdAgent.viewController = self;
    [self.streamView addDataChannel:self.esdAgent];
  4. 监听并响应云手机旋转指令。

    @interface DemoEDSAgentChannel() <CommandListener>
    
    @property (nonatomic, assign) CGRect rect;
    
    @end
    
    @implementation DemoEDSAgentChannel
    
    - (void)setViewController:(BaseViewController *)viewController {
        self.rect = viewController.view.bounds;
        _viewController = viewController;
    }
    
    - (void)onConnectStateChanged:(ASPDCConnectState)state {
        NSLog(@"[DemoEDSAgentChannel] onConnectStateChanged %ld", state);
        if (state == OPEN) {
            // to send data
        }
    }
        
    - (void)onReceiveData:(NSData * _Nonnull)buf {
        NSString *string = [[NSString alloc] initWithData:buf encoding:NSUTF8StringEncoding];
        NSLog(@"[DemoEDSAgentChannel] onConnectStateChanged %@", string);
        [CommandUtils parseCommand:string listener:self];
    }
    
    #pragma mark - CommandListener
    - (void)onRotation:(NSInteger)value {
        NSLog(@"[DemoEDSAgentChannel] onRotation %ld", value);
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.viewController switchRoration:value];
            if (value == 1 || value == 3) {
                self.streamView.center = CGPointMake(CGRectGetMidY(self.viewController.view.bounds),
                                                     CGRectGetMidX(self.viewController.view.bounds));
                self.streamView.transform = CGAffineTransformMakeRotation(-M_PI_2*value) ;
            } else {
                self.streamView.center = CGPointMake(CGRectGetMidX(self.rect),
                                                     CGRectGetMidY(self.rect));
                self.streamView.transform = CGAffineTransformIdentity;
            }
        });
    }