全部產品
Search
文件中心

Cloud Phone:通過SDK進行雲手機畫面的本地旋轉

更新時間:Dec 16, 2025

在雲手機使用情境中,使用者操作的是運行於雲端的虛擬設備,而本地終端僅作為顯示與互動的載體。當雲手機發生螢幕方向變化(如從豎屏切換至橫屏)時,若本地用戶端無法同步響應,將導致畫面顯示異常(如倒置、展開、黑邊)或互動錯位,嚴重影響使用者體驗。本文介紹如何在Android與iOS用戶端SDK中實現本地螢幕方向與雲端旋轉指令的精準同步機制,確保本地介面與視頻流始終與雲端裝置保持一致的螢幕朝向,從而提供沈浸式、無感知的遠程操作體驗。

背景

傳統移動端應用通常依賴裝置感應器或系統配置自動響應旋轉螢幕。然而在雲手機架構下,螢幕方嚮應由雲端虛擬設備的狀態驅動,而非本地物理裝置的姿態。若直接啟用系統自動旋轉,會導致以下問題:

  • 本地旋轉與雲端狀態不同步,畫面顯示錯誤。

  • 使用者無意中轉動手機即觸發旋轉,幹擾正常操作。

  • 視頻渲染層(如 SurfaceView/TextureView 或 Metal/OpenGL View)未適配新方向,出現裁剪或形變。

因此必須主動接管旋轉控制權,由雲端下發明確的方向指令,本地用戶端據此強制切換介面方向並同步調整視頻渲染邏輯。

方案概述

本方案在Android與iOS平台上分別實現了以下核心能力:

  • 統一的旋轉指令接收機制

    • 通過自訂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;
            }
        });
    }