Em um ambiente de cloud phone, o dispositivo virtual na nuvem controla a orientação da tela, e não os sensores do dispositivo local. Quando a nuvem alterna de retrato para paisagem, o cliente local deve acompanhar essa mudança de forma sincronizada. Sem essa sincronização, o fluxo de vídeo pode aparecer cortado, esticado ou rotacionado incorretamente, e as coordenadas de toque deixam de corresponder às regiões corretas da tela.
Este tópico explica como implementar a rotação de tela controlada pela nuvem no Android e iOS usando o ECP SDK.
Como funciona
A sincronização da rotação da tela exige o tratamento de dois aspectos independentes:
Orientação da UI: force a Activity local (Android) ou o ViewController (iOS) a alternar a orientação para corresponder ao dispositivo na nuvem.
Rotação do quadro de vídeo: ajuste a camada de renderização para que o fluxo de vídeo preencha a tela corretamente, sem distorções.
Ambos os processos devem ocorrer simultaneamente. Se você atualizar a orientação da UI sem rotacionar o quadro de vídeo (ou vice-versa), a exibição ficará desalinhada ou distorcida.
A nuvem envia um valor de rotação por meio de um canal de dados chamado wy_vdagent_default_dc. O cliente local recebe esse valor, alterna a orientação da UI e rotaciona a camada de renderização adequadamente.
|
Valor de rotação |
Orientação |
|
|
Retrato |
|
|
Paisagem esquerda |
|
|
Paisagem direita |
O sistema analisa os comandos usando CommandUtils.parseCommand(), que aciona um callback onRotation com o valor de rotação.
Pré-requisitos
Antes de começar, verifique se você possui:
O ECP SDK para Android ou iOS integrado ao seu projeto.
Uma instância de cloud phone em execução e acessível a partir do seu cliente local.
Gerenciar a rotação local no Android
A implementação para Android utiliza TextureView para renderização e um DataChannel para receber comandos de rotação da nuvem.
Etapa 1: Configurar o StreamView
Desative o controle automático de orientação na nuvem e alterne a visualização de renderização para 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);
Etapa 2: Receber comandos de rotação e sincronizar a orientação
Registre um listener de DataChannel em wy_vdagent_default_dc. Quando onRotation é disparado, o handler executa duas ações na thread da UI:
Etapa 2a — Rotacionar o quadro de vídeo: chame
setSurfaceRotation()para girar a textura subjacente e corresponder à orientação da nuvem.Etapa 2b — Bloquear a orientação da Activity: chame
setRequestedOrientation()para impedir que a rotação baseada em sensores substitua o estado definido pela nuvem.
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);
}
});
Tanto setSurfaceRotation() quanto setRequestedOrientation() são executados na thread da UI por meio de runOnUiThread().
Gerenciar a rotação local no iOS
A implementação para iOS divide o trabalho entre duas classes: BaseViewController gerencia o estado de orientação, enquanto DemoEDSAgentChannel recebe os comandos da nuvem e aplica a transformação de rotação ao StreamView.
Etapa 1: Configurar as orientações suportadas no Info.plist
No arquivo Info.plist, declare suporte para retrato, paisagem esquerda e paisagem direita:
<?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>
Etapa 2: Implementar o BaseViewController
O BaseViewController armazena o estado atual de rotação e força a alteração de orientação quando a nuvem envia um novo valor. Definir shouldAutorotate como false desativa a rotação baseada em sensores. Assim, apenas os comandos da nuvem controlarão as mudanças de orientação.
@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
Etapa 3: Configurar o canal de dados
No seu view controller, inicialize o DemoEDSAgentChannel e adicione-o ao StreamView:
self.esdAgent = [[DemoEDSAgentChannel alloc] initWithParameter:DATA_CHANNEL_NAME];
self.esdAgent.streamView = self.streamView;
self.esdAgent.viewController = self;
[self.streamView addDataChannel:self.esdAgent];
Etapa 4: Processar comandos de rotação no DemoEDSAgentChannel
O DemoEDSAgentChannel decodifica os dados recebidos, analisa o comando de rotação e aplica duas alterações na thread principal:
Etapa 4a — Alternar a orientação da UI: chame
switchRoration:no view controller.Etapa 4b — Rotacionar o quadro de vídeo: aplique uma
CGAffineTransformaoStreamView.
Para orientações em paisagem, as coordenadas centrais do StreamView (X e Y) são trocadas para manter o vídeo centralizado após a transformação de 90 graus.
@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