All Products
Search
Document Center

Cloud Phone:Rotate the cloud phone screen locally using the SDK

Last Updated:Apr 01, 2026

In a cloud phone setup, the cloud virtual device controls screen orientation — not the local device's sensors. When the cloud rotates from portrait to landscape, the local client must follow in sync. Without this synchronization, the video stream appears cropped, stretched, or rotated incorrectly, and touch input coordinates no longer map to the correct screen regions.

This topic explains how to implement cloud-driven screen rotation on Android and iOS using the ECP SDK.

How it works

Synchronizing screen rotation requires handling two independent concerns:

  1. UI orientation — forcing the local Activity (Android) or ViewController (iOS) to switch orientation to match the cloud device

  2. Video frame rotation — adjusting the rendering layer so the video stream fills the screen correctly without distortion

Both must happen together. If you update the UI orientation without rotating the video frame (or vice versa), you get a misaligned or distorted display.

The cloud sends a rotation value over a data channel named wy_vdagent_default_dc. The local client receives it, switches the UI orientation, and rotates the rendering layer accordingly.

Rotation valueOrientation
0Portrait
1Landscape left
3Landscape right

Commands are parsed using CommandUtils.parseCommand(), which triggers an onRotation callback with the rotation value.

Prerequisites

Before you begin, ensure that you have:

  • The ECP Android or iOS SDK integrated into your project

  • A cloud phone instance running and reachable from your local client

Handle local rotation on Android

The Android implementation uses TextureView for rendering and a DataChannel to receive rotation commands from the cloud.

Step 1: Configure the StreamView

Disable automatic cloud orientation control and switch the rendering view to TextureView:

// Disable cloud-side automatic orientation control
bundle.putBoolean(StreamView.CONFIG_DISABLE_ORIENTATION_CLOUD_CONTROL, true);

// Use TextureView instead of SurfaceView for rotation support
mStreamView = findViewById(R.id.stream_view);
mStreamView.enableTextureView(true);

Step 2: Receive rotation commands and sync orientation

Register a DataChannel listener on wy_vdagent_default_dc. When onRotation fires, the handler performs two actions on the UI thread:

  • Step 2a — Rotate the video frame: Call setSurfaceRotation() to rotate the underlying texture to match the cloud orientation.

  • Step 2b — Lock the Activity orientation: Call setRequestedOrientation() to prevent sensor-driven rotation from overriding the cloud state.

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(() -> {
                    // Step 2a: Rotate the video texture to match the cloud orientation
                    mStreamView.setSurfaceRotation(rotation);
                    // Step 2b: Lock the Activity orientation to prevent sensor-driven 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);
    }
});

Both setSurfaceRotation() and setRequestedOrientation() run on the UI thread via runOnUiThread().

Handle local rotation on iOS

The iOS implementation splits the work across two classes: BaseViewController manages orientation state, and DemoEDSAgentChannel receives cloud commands and applies the rotation transform to StreamView.

Step 1: Configure supported orientations in Info.plist

In Info.plist, declare support for portrait, landscape left, and landscape right:

<?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>

Step 2: Implement BaseViewController

BaseViewController holds the current rotation state and forces the orientation switch when the cloud sends a new value. Setting shouldAutorotate to false disables sensor-driven rotation — only cloud commands drive orientation changes.

@implementation BaseViewController {
    NSInteger mRoration;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
}

- (void)switchRoration:(NSInteger)roration {
    mRoration = roration;
    // Force orientation switch (recommended for iOS 16+)
    if (@available(iOS 16.0, *)) {
        [self setNeedsUpdateOfSupportedInterfaceOrientations];
    } else {
        // Legacy method (deprecated, but compatible with iOS 15 and earlier)
        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; // Disable sensor-driven rotation; only cloud commands control orientation
}

// Optional: Specify the preferred orientation for presentation
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
    switch (mRoration) {
        case 1:  return UIInterfaceOrientationLandscapeLeft;
        case 3:  return UIInterfaceOrientationLandscapeRight;
        default: return UIInterfaceOrientationPortrait;
    }
}

@end

Step 3: Set up the data channel

In your view controller, initialize DemoEDSAgentChannel and add it to the StreamView:

self.esdAgent = [[DemoEDSAgentChannel alloc] initWithParameter:DATA_CHANNEL_NAME];
self.esdAgent.streamView = self.streamView;
self.esdAgent.viewController = self;
[self.streamView addDataChannel:self.esdAgent];

Step 4: Handle rotation commands in DemoEDSAgentChannel

DemoEDSAgentChannel decodes incoming data, parses the rotation command, and applies two changes on the main thread:

  • Step 4a — Switch UI orientation: Call switchRoration: on the view controller.

  • Step 4b — Rotate the video frame: Apply a CGAffineTransform to the StreamView.

For landscape orientations, the StreamView center coordinates are swapped (X and Y) to keep the video centered after the 90-degree transform.

@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(), ^{
        // Step 4a: Switch UI orientation
        [self.viewController switchRoration:value];
        // Step 4b: Rotate the StreamView to match the video frame orientation
        if (value == 1 || value == 3) {
            // Swap X and Y center coordinates to keep the view centered in landscape
            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;
        }
    });
}

@end