Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

camera: restore landscape constraint + EXIF-6 rotation; bypass SW for bios/disk

Research from webrtcHacks, Snap Camera Kit, and addpipe confirmed two
things:

1. iOS Safari (and modern Android Chrome) hand drawImage(video) the raw
landscape sensor buffer — they do NOT pre-rotate to match device
orientation. The CSS-rotated <video> display is decoupled from the
pixel buffer that drawImage / MediaRecorder receive.
2. The robust pattern is to always request landscape constraints (the
sensor's native orientation), and rotate manually in canvas — iPhone
tags portrait captures EXIF orientation 6 (= rotate 90° CW for
display), so canvas.rotate(+PI/2) is the correct default.

Restored from my earlier (mistaken) "let the browser auto-rotate"
attempt:

bios.mjs:
- getDevice(): re-add the width/height swap on mobile portrait so
getUserMedia gets a landscape request. Otherwise iOS may stick the
stream at whatever orientation getUserMedia was first called in.
- Default rotationAngle is now Math.PI / 2 (CW) for both rear and front
cameras. The selfie mirror still flips X separately. The
?camrot=0|cw|180|ccw URL override stays available for on-device
bisecting.

sw.js:
- Bump CACHE_NAME → ac-modules-v8 so existing v7 SWs reactivate and
drop their stale entries.
- Pull bios.mjs and lib/disk.mjs out of PRECACHE_MODULES and add them
to NEVER_CACHE while we iterate. They're at the heart of the
rotation/telemetry feedback loop and the stale-while-revalidate
pattern is what kept the iPhone running pre-fix code for hours
after deploys. Will revert this once the rotation default is
locked in.

+38 -22
+28 -18
system/public/aesthetic.computer/bios.mjs
··· 19862 19862 async function getDevice(facingModeChoice) { 19863 19863 // Use local copies so we don't mutate the outer cWidth/cHeight 19864 19864 // across repeated calls (camera swap, resize, etc.). 19865 - const reqWidth = cWidth, 19865 + let reqWidth = cWidth, 19866 19866 reqHeight = cHeight; 19867 19867 19868 19868 const constraints = { ··· 19870 19870 frameRate: { ideal: 30 }, 19871 19871 }; 19872 19872 19873 - // Ask for what we actually want. On mobile portrait, iOS Safari 19874 - // and modern Android Chrome already present the stream oriented 19875 - // to match the device (auto-rotated for the <video> element and 19876 - // drawImage()), so we do NOT swap width/height to force a 19877 - // landscape request. If the browser gives us a landscape stream 19878 - // anyway, the detection path in process() will rotate it to fit. 19873 + // Mobile camera sensors are physically landscape. Per Snap and 19874 + // webrtcHacks: always request landscape constraints, even on a 19875 + // portrait device — otherwise iOS Safari may refuse the request 19876 + // or hand back a stream stuck at the orientation present at 19877 + // getUserMedia time. We then rotate the canvas in process() 19878 + // below to fit the portrait buffer. 19879 + if ( 19880 + (iOS || Android) && 19881 + typeof window !== "undefined" && 19882 + window.matchMedia?.("(orientation: portrait)")?.matches && 19883 + (facingModeChoice === "environment" || 19884 + facingModeChoice === "user") 19885 + ) { 19886 + const tmp = reqWidth; 19887 + reqWidth = reqHeight; 19888 + reqHeight = tmp; 19889 + } 19890 + 19879 19891 constraints.width = { ideal: reqWidth }; 19880 19892 constraints.height = { ideal: reqHeight }; 19881 19893 ··· 20158 20170 // Mirror the front camera for selfie framing. Desktop webcams are 20159 20171 // conventionally mirrored too. Mobile rear camera stays unmirrored. 20160 20172 const needsMirror = facingMode === "user" || (!iOS && !Android); 20161 - // Rotation direction for sensor→buffer orientation mismatch. 20162 - // Front cameras are mounted mirrored relative to rear cameras, so the 20163 - // rotation that lands the subject upright is opposite: front = +90° CW, 20164 - // rear = -90° CCW. Desktop webcams behave like front cameras. 20165 - // 🧪 URL override for camera rotation while we bisect the correct 20166 - // angle on device: ?camrot=0|cw|180|ccw (0 = no rotation). Also 20167 - // ?camforce=1 forces the rotation path even when dims say no. 20168 - let rotationAngle = 20169 - facingMode === "user" || (!iOS && !Android) 20170 - ? Math.PI / 2 20171 - : -Math.PI / 2; 20173 + // EXIF orientation 6 — the convention iPhone (and most Android 20174 + // devices) tag portrait camera captures with — means "rotate 90° 20175 + // CW for display". The raw landscape sensor buffer has the 20176 + // subject's "up" direction at column 0, and rotating +PI/2 CW 20177 + // brings column 0 to the top of the rendered frame. 20178 + // 🧪 URL override while we bisect on device: 20179 + // ?camrot=0|cw|180|ccw manually pick the angle 20180 + // ?camforce=1 force rotation even when dims say no 20181 + let rotationAngle = Math.PI / 2; 20172 20182 let forceRotation = false; 20173 20183 if (typeof location !== "undefined" && location.search) { 20174 20184 const camParams = new URLSearchParams(location.search);
+10 -4
system/public/sw.js
··· 1 1 // Aesthetic Computer Service Worker 2 2 // Caches JavaScript modules for faster subsequent loads 3 3 4 - const CACHE_NAME = 'ac-modules-v7'; // Bump to force fresh udp.mjs with robust respond() parsing (fixes arena:snap drops) 4 + const CACHE_NAME = 'ac-modules-v8'; // Bump: pull bios.mjs/disk.mjs out of the cache pipeline while we iterate on camera + telemetry 5 5 const CACHE_DURATION = 60 * 60 * 1000; // 1 hour in ms (dev-friendly) 6 6 7 - // Critical modules to precache on install 7 + // Critical modules to precache on install. NOTE: bios.mjs and lib/disk.mjs 8 + // are intentionally absent — they sit at the heart of the rotation/telemetry 9 + // loop and need to ship updates within seconds, not stale-while-revalidate 10 + // cycles. NEVER_CACHE below also bypasses runtime caching for them. 8 11 const PRECACHE_MODULES = [ 9 12 '/aesthetic.computer/boot.mjs', 10 - '/aesthetic.computer/bios.mjs', 11 13 '/aesthetic.computer/lib/parse.mjs', 12 - '/aesthetic.computer/lib/disk.mjs', 13 14 '/aesthetic.computer/lib/graph.mjs', 14 15 '/aesthetic.computer/lib/num.mjs', 15 16 '/aesthetic.computer/lib/help.mjs', ··· 44 45 /\/disks\/.*\.mjs$/, // User pieces should always be fresh 45 46 /\?v=/, // Cache-busted URLs 46 47 /localhost:8889/, // Session server 48 + // 🚧 Pinned-to-network while iterating on camera rotation + piece-runs 49 + // telemetry. Restore caching for these once the rotation default is 50 + // locked in. 51 + /\/aesthetic\.computer\/bios\.mjs$/, 52 + /\/aesthetic\.computer\/lib\/disk\.mjs$/, 47 53 ]; 48 54 49 55 self.addEventListener('install', (event) => {