すべてのプロダクト
Search
ドキュメントセンター

Cloud Phone:SDK を使用したクラウドフォン画面のローカルでの回転

最終更新日:Dec 17, 2025

クラウドフォンのシナリオでは、ユーザーはクラウドで実行される仮想デバイスを操作します。ローカルクライアントは、表示とインタラクションのための中間媒体としてのみ機能します。クラウドフォンの画面の向きが縦向きから横向きなどに変更された場合、ローカルクライアントが同期して応答しないと、ユーザーエクスペリエンスが著しく損なわれます。これにより、画像の反転、引き伸ばし、黒帯などの表示上の問題や、操作の不一致が発生する可能性があります。このトピックでは、Android および iOS クライアント SDK で正確な同期メカニズムを実装する方法について説明します。このメカニズムは、ローカル画面の向きをクラウドからの回転命令と同期させます。これにより、ローカルのインターフェイスとビデオストリームが、常にクラウドデバイスの画面の向きと一致するようになります。これにより、没入感のあるシームレスなリモート操作体験が提供されます。

背景情報

従来のモバイルアプリケーションは通常、デバイスセンサーやシステム構成に依存して、画面の回転に自動的に応答します。しかし、クラウドフォンアーキテクチャでは、画面の向きはローカルの物理デバイスの向きではなく、クラウドの仮想デバイスの状態によって駆動される必要があります。システムの自動回転を直接有効にすると、次の問題が発生します:

  • ローカルの回転がクラウドの状態と同期されず、画面表示が不正確になります。

  • ユーザーが誤ってスマートフォンを回転させると回転がトリガーされ、通常の操作が妨げられます。

  • SurfaceView、TextureView、または Metal/OpenGL View などのビデオレンダリングレイヤーが、新しい向きに適応しません。これにより、トリミングや歪みが発生します。

したがって、回転を制御する必要があります。クラウドが特定の向きの命令を送信し、ローカルクライアントがそれに応じてインターフェイスの向きの切り替えを強制し、ビデオレンダリングのロジックを調整します。

ソリューション概要

このソリューションは、Android および iOS プラットフォームで次のコア機能を実装します:

  • 回転命令を受信するための統一されたメカニズム

    • wy_vdagent_default_dc などのカスタムデータチャンネルを介して、クラウドから回転コマンドを受信します。

    • 標準化されたプロトコルを使用して回転パラメーターを解析します。たとえば、rotation = 0/1/3 は、それぞれ縦向き、左横向き、右横向きに対応します。

  • ローカルインターフェイスの向きの強制的な同期

    • Android: setRequestedOrientation() を呼び出して、アクティビティの向きを動的にロックします。

    • iOS: supportedInterfaceOrientations をオーバーライドし、setNeedsUpdateOfSupportedInterfaceOrientations (iOS 16 以降) または非公開 API を使用して向きを切り替えます。

    • 両プラットフォームとも、クラウドからの命令にのみ応答するように、デバイスの自動回転 (shouldAutorotate = false またはセンサー応答の無効化) を無効にします。

  • ビデオレンダリングレイヤーの正確な適応

    • Android: TextureView を使用し、setSurfaceRotation() を呼び出して基盤となるテクスチャを直接回転させます。これにより、画像の歪みを回避します。

    • iOS: CGAffineTransform を使用して StreamView を回転させ、その重心を動的に調整します。これにより、画像が中央に配置され、正しいアスペクト比が維持されます。

  • 複数の向きのサポート

    • 縦向き、左横向き、右横向きの 3 つの向きをサポートします。

    • 横向きモードでの 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("Unknown command: " + cmd);
                    }
                });
            }
            @Override
            protected void onConnectStateChanged(DataChannelConnectState state) {
                Log.i(TAG, "wy_vdagent_default_dc dc connection state changed to " + state);
            }
        });

iOS SDK でのローカル回転の処理

  1. Info.plistSupported 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; // 自動回転を無効にします。
    }
    
    // オプション:表示に推奨される向きを指定します。
    - (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) {
            // データを送信します
        }
    }
        
    - (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;
            }
        });
    }