Skip to content

Commit 292f8b1

Browse files
committed
Fix Android stream edge alpha
1 parent 80b3e9a commit 292f8b1

3 files changed

Lines changed: 94 additions & 6 deletions

File tree

client/src/app/AppShell.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1301,9 +1301,7 @@ export function AppShell({
13011301
: null;
13021302
const screenOnlyStyle =
13031303
!viewportChromeProfile && isAndroidViewport
1304-
? ({
1305-
borderRadius: "16px",
1306-
} satisfies CSSProperties)
1304+
? null
13071305
: !viewportChromeProfile && chromeProfile && chromeProfile.screenWidth > 0
13081306
? ({
13091307
borderRadius: `${Math.min(

client/src/styles/components.css

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1504,8 +1504,23 @@
15041504
}
15051505

15061506
.device-screen.android-screen {
1507+
--android-screen-radius: 18px;
1508+
--android-stream-overscan: 2px;
1509+
15071510
background: transparent;
1508-
border-radius: 16px;
1511+
border-radius: var(--android-screen-radius);
1512+
clip-path: inset(0 round var(--android-screen-radius));
1513+
}
1514+
1515+
.device-screen.android-screen .stream-canvas,
1516+
.device-screen.android-screen .stream-video {
1517+
inset: calc(-1 * var(--android-stream-overscan));
1518+
width: calc(
1519+
100% + var(--android-stream-overscan) + var(--android-stream-overscan)
1520+
);
1521+
height: calc(
1522+
100% + var(--android-stream-overscan) + var(--android-stream-overscan)
1523+
);
15091524
}
15101525

15111526
.stream-canvas {

server/src/android.rs

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -977,8 +977,9 @@ impl AndroidGrpcFrameStream {
977977
grpc::image_format::ImgFormat::try_from(format.format)
978978
.unwrap_or(grpc::image_format::ImgFormat::Rgba8888),
979979
)?;
980-
let (width, height, rgba) =
980+
let (width, height, mut rgba) =
981981
normalize_android_frame_orientation(width, height, rgba, self.target);
982+
flatten_android_frame_alpha(&mut rgba, width, height);
982983
Ok(Some(AndroidFrame {
983984
width,
984985
height,
@@ -1041,6 +1042,59 @@ fn rotate_rgba_180_in_place(rgba: &mut [u8], width: u32, height: u32) {
10411042
}
10421043
}
10431044

1045+
fn flatten_android_frame_alpha(rgba: &mut [u8], width: u32, height: u32) {
1046+
if !rgba.chunks_exact(4).any(|pixel| pixel[3] != 255) {
1047+
return;
1048+
}
1049+
1050+
let width = width as usize;
1051+
let height = height as usize;
1052+
let Some(default_fill) = first_opaque_rgb(rgba) else {
1053+
for pixel in rgba.chunks_exact_mut(4) {
1054+
pixel[3] = 255;
1055+
}
1056+
return;
1057+
};
1058+
1059+
for y in 0..height {
1060+
let row_start = y * width * 4;
1061+
let row = &mut rgba[row_start..row_start + width * 4];
1062+
let mut fill = first_opaque_rgb(row).unwrap_or(default_fill);
1063+
for pixel in row.chunks_exact_mut(4) {
1064+
if pixel[3] == 255 {
1065+
fill = [pixel[0], pixel[1], pixel[2]];
1066+
continue;
1067+
}
1068+
composite_pixel_over_rgb(pixel, fill);
1069+
}
1070+
}
1071+
}
1072+
1073+
fn first_opaque_rgb(rgba: &[u8]) -> Option<[u8; 3]> {
1074+
rgba.chunks_exact(4)
1075+
.find(|pixel| pixel[3] == 255)
1076+
.map(|pixel| [pixel[0], pixel[1], pixel[2]])
1077+
}
1078+
1079+
fn composite_pixel_over_rgb(pixel: &mut [u8], background: [u8; 3]) {
1080+
let alpha = u32::from(pixel[3]);
1081+
if alpha == 0 {
1082+
pixel[0] = background[0];
1083+
pixel[1] = background[1];
1084+
pixel[2] = background[2];
1085+
pixel[3] = 255;
1086+
return;
1087+
}
1088+
1089+
for channel in 0..3 {
1090+
pixel[channel] = ((u32::from(pixel[channel]) * alpha
1091+
+ u32::from(background[channel]) * (255 - alpha)
1092+
+ 127)
1093+
/ 255) as u8;
1094+
}
1095+
pixel[3] = 255;
1096+
}
1097+
10441098
fn run_command_text<const N: usize>(program: PathBuf, args: [&str; N]) -> Result<String, AppError> {
10451099
let output = run_command(program, args)?;
10461100
String::from_utf8(output)
@@ -1732,7 +1786,7 @@ mod tests {
17321786

17331787
#[test]
17341788
fn android_frame_orientation_keeps_matching_aspect() {
1735-
let rgba = vec![1, 2, 3, 4, 5, 6, 7, 8];
1789+
let rgba = vec![1, 2, 3, 255, 5, 6, 7, 255];
17361790

17371791
let (width, height, out) = normalize_android_frame_orientation(
17381792
1,
@@ -1749,6 +1803,27 @@ mod tests {
17491803
assert_eq!(out, rgba);
17501804
}
17511805

1806+
#[test]
1807+
fn android_frame_alpha_flattens_transparent_edges_to_nearest_row_pixel() {
1808+
let mut rgba = vec![0, 0, 0, 0, 10, 20, 30, 255, 40, 50, 60, 255, 0, 0, 0, 0];
1809+
1810+
flatten_android_frame_alpha(&mut rgba, 4, 1);
1811+
1812+
assert_eq!(
1813+
rgba,
1814+
vec![10, 20, 30, 255, 10, 20, 30, 255, 40, 50, 60, 255, 40, 50, 60, 255,]
1815+
);
1816+
}
1817+
1818+
#[test]
1819+
fn android_frame_alpha_composites_partial_alpha() {
1820+
let mut rgba = vec![100, 50, 0, 255, 200, 200, 200, 128];
1821+
1822+
flatten_android_frame_alpha(&mut rgba, 2, 1);
1823+
1824+
assert_eq!(rgba, vec![100, 50, 0, 255, 150, 125, 100, 255]);
1825+
}
1826+
17521827
#[test]
17531828
fn android_frame_orientation_rotates_reverse_orientations_180() {
17541829
let rgba = vec![

0 commit comments

Comments
 (0)