Skip to content

Commit 0d3c5bd

Browse files
committed
Normalize Android WebRTC frame orientation
1 parent 1af44e4 commit 0d3c5bd

1 file changed

Lines changed: 90 additions & 14 deletions

File tree

server/src/android.rs

Lines changed: 90 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ pub struct AndroidFrame {
5858

5959
pub struct AndroidGrpcFrameStream {
6060
inner: tonic::Streaming<grpc::Image>,
61+
target_size: Option<(u32, u32)>,
6162
}
6263

6364
pub fn is_android_id(id: &str) -> bool {
@@ -516,21 +517,25 @@ impl AndroidBridge {
516517
display: 0,
517518
transport: None,
518519
};
519-
if let Ok(serial) = self.resolve_serial(&avd_name) {
520-
if let Ok((width, height)) = self.screen_size_for_serial(&serial) {
521-
let mut target_width = width;
522-
let mut target_height = height;
523-
if let Some(max_edge) = max_edge {
524-
let max_edge = max_edge.clamp(240, 2400) as f64;
525-
let largest = width.max(height);
526-
if largest > max_edge {
527-
let scale = max_edge / largest;
528-
target_width = (width * scale).round().max(1.0);
529-
target_height = (height * scale).round().max(1.0);
530-
}
520+
let target_size = self
521+
.resolve_serial(&avd_name)
522+
.ok()
523+
.and_then(|serial| self.screen_size_for_serial(&serial).ok())
524+
.map(|(width, height)| {
525+
(
526+
width.round().max(1.0) as u32,
527+
height.round().max(1.0) as u32,
528+
)
529+
});
530+
if let (Some(max_edge), Some((width, height))) = (max_edge, target_size) {
531+
let max_edge = max_edge.clamp(240, 2400);
532+
let largest = width.max(height);
533+
if largest > max_edge {
534+
if width >= height {
535+
format.width = max_edge;
536+
} else {
537+
format.height = max_edge;
531538
}
532-
format.width = target_width.round().max(1.0) as u32;
533-
format.height = target_height.round().max(1.0) as u32;
534539
}
535540
}
536541

@@ -568,6 +573,7 @@ impl AndroidBridge {
568573
})?;
569574
Ok(AndroidGrpcFrameStream {
570575
inner: response.into_inner(),
576+
target_size,
571577
})
572578
}
573579

@@ -965,6 +971,8 @@ impl AndroidGrpcFrameStream {
965971
grpc::image_format::ImgFormat::try_from(format.format)
966972
.unwrap_or(grpc::image_format::ImgFormat::Rgba8888),
967973
)?;
974+
let (width, height, rgba) =
975+
normalize_android_frame_orientation(width, height, rgba, self.target_size);
968976
Ok(Some(AndroidFrame {
969977
width,
970978
height,
@@ -974,6 +982,42 @@ impl AndroidGrpcFrameStream {
974982
}
975983
}
976984

985+
fn normalize_android_frame_orientation(
986+
width: u32,
987+
height: u32,
988+
rgba: Vec<u8>,
989+
target_size: Option<(u32, u32)>,
990+
) -> (u32, u32, Vec<u8>) {
991+
let Some((target_width, target_height)) = target_size else {
992+
return (width, height, rgba);
993+
};
994+
if width == 0
995+
|| height == 0
996+
|| target_width == 0
997+
|| target_height == 0
998+
|| (width > height) == (target_width > target_height)
999+
{
1000+
return (width, height, rgba);
1001+
}
1002+
(height, width, rotate_rgba_clockwise(&rgba, width, height))
1003+
}
1004+
1005+
fn rotate_rgba_clockwise(rgba: &[u8], width: u32, height: u32) -> Vec<u8> {
1006+
let width = width as usize;
1007+
let height = height as usize;
1008+
let mut out = vec![0; rgba.len()];
1009+
for y in 0..height {
1010+
for x in 0..width {
1011+
let src = (y * width + x) * 4;
1012+
let dst_x = height - 1 - y;
1013+
let dst_y = x;
1014+
let dst = (dst_y * height + dst_x) * 4;
1015+
out[dst..dst + 4].copy_from_slice(&rgba[src..src + 4]);
1016+
}
1017+
}
1018+
out
1019+
}
1020+
9771021
fn run_command_text<const N: usize>(program: PathBuf, args: [&str; N]) -> Result<String, AppError> {
9781022
let output = run_command(program, args)?;
9791023
String::from_utf8(output)
@@ -1633,6 +1677,38 @@ mod tests {
16331677
})
16341678
);
16351679
}
1680+
1681+
#[test]
1682+
fn android_frame_orientation_rotates_when_stream_aspect_is_swapped() {
1683+
let rgba = vec![
1684+
1, 0, 0, 255, 2, 0, 0, 255, 3, 0, 0, 255, //
1685+
4, 0, 0, 255, 5, 0, 0, 255, 6, 0, 0, 255,
1686+
];
1687+
1688+
let (width, height, rotated) =
1689+
normalize_android_frame_orientation(3, 2, rgba, Some((2, 3)));
1690+
1691+
assert_eq!((width, height), (2, 3));
1692+
assert_eq!(
1693+
rotated,
1694+
vec![
1695+
4, 0, 0, 255, 1, 0, 0, 255, //
1696+
5, 0, 0, 255, 2, 0, 0, 255, //
1697+
6, 0, 0, 255, 3, 0, 0, 255,
1698+
]
1699+
);
1700+
}
1701+
1702+
#[test]
1703+
fn android_frame_orientation_keeps_matching_aspect() {
1704+
let rgba = vec![1, 2, 3, 4, 5, 6, 7, 8];
1705+
1706+
let (width, height, out) =
1707+
normalize_android_frame_orientation(1, 2, rgba.clone(), Some((1080, 2400)));
1708+
1709+
assert_eq!((width, height), (1, 2));
1710+
assert_eq!(out, rgba);
1711+
}
16361712
}
16371713

16381714
mod grpc {

0 commit comments

Comments
 (0)