portrait-camera 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fuad Laguda
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # portrait-camera
2
+
3
+ Open the front camera in portrait on a phone browser, find out whether the track is telling the truth about its orientation, and record a file that plays upright.
4
+
5
+ Seven functions, no dependencies, MIT.
6
+
7
+ ```
8
+ npm install portrait-camera
9
+ ```
10
+
11
+ ## The problem this solves
12
+
13
+ Ask a phone browser for the front camera in portrait and two things go wrong.
14
+
15
+ On iOS 26.6 Safari, `track.getSettings()` reports the sensor's landscape shape, 1920 by 1080, for roughly the first 100 to 400 milliseconds after the stream arrives, then corrects to 1080 by 1920 when the first frame paints. Anything that reads the size inside that window records or publishes the wrong shape. Any test that waits for a painted frame before reading sees nothing wrong, which is why this stayed hidden.
16
+
17
+ On Android Chrome the track reports portrait but the recorded file comes back nearly square, 1088 by 1080, with the top and bottom of the picture missing.
18
+
19
+ The measurements, the reproduction and the fixes are public:
20
+
21
+ - WebKit bug 323550, classified by Apple as a regression in iOS 26.6 on 6 September 2026: https://bugs.webkit.org/show_bug.cgi?id=323550
22
+ - Chromium issue 558254908: https://issues.chromium.org/issues/558254908
23
+ - LiveKit issue 2099, which produced PR 2100 in their client SDK: https://github.com/livekit/client-sdk-js/issues/2099
24
+ - LiveKit issue 2107, which produced PR 2110: https://github.com/livekit/client-sdk-js/issues/2107
25
+ - The write-up: https://dev.to/lagudafuad/the-ios-camera-lies-for-about-a-tenth-of-a-second-and-that-is-why-nobody-noticed-it-5802
26
+
27
+ ## What it does
28
+
29
+ **Wait for the first painted frame before you read anything.** `waitForFirstFrame` resolves when the browser has drawn a frame, using `requestVideoFrameCallback` where it exists. Every dimension the track reports before that moment can be wrong, and LiveKit's own fix for this fault, PR 2100, is the same wait.
30
+
31
+ **Ask for the camera in landscape numbers.** Requesting portrait dimensions makes both browsers return the wide sensor preset unrotated. Requesting landscape lets the phone rotate the picture itself. `openFrontCamera` walks a chain of requests that works on both.
32
+
33
+ **Do not trust the track. Look at the picture.** `measureDrawnFrame` draws one frame of the live video onto a 16 by 16 canvas and checks which corners are painted, so you learn the real orientation from the pixels rather than from `getSettings()`.
34
+
35
+ **Record what is drawn, not what is reported.** `planRecordingStream` compares the drawn frame with the reported track. If they agree, it records the raw track. If the picture is portrait but the track says landscape, it records a canvas stream of the drawn picture at its own size. If the picture is wide on a portrait screen, it records a centred 9 by 16 crop. No upscaling in any branch.
36
+
37
+ **Pick a codec that does not soften the video.** `pickMimeType` puts H.264 High profile ahead of Baseline, because `isTypeSupported` says yes to both and list order decides.
38
+
39
+ ## Usage
40
+
41
+ ```ts
42
+ import {
43
+ openFrontCamera,
44
+ waitForFirstFrame,
45
+ planRecordingStream,
46
+ pickMimeType,
47
+ extensionFor,
48
+ } from 'portrait-camera'
49
+
50
+ const stream = await openFrontCamera()
51
+ video.srcObject = stream
52
+ await video.play()
53
+ await waitForFirstFrame(video)
54
+
55
+ const plan = planRecordingStream(video, stream)
56
+ const mime = pickMimeType()
57
+ const recorder = new MediaRecorder(plan.stream, mime ? { mimeType: mime } : undefined)
58
+
59
+ const chunks: BlobPart[] = []
60
+ recorder.ondataavailable = (e) => chunks.push(e.data)
61
+ recorder.onstop = () => {
62
+ plan.stop()
63
+ const blob = new Blob(chunks, { type: mime })
64
+ const name = `take.${extensionFor(mime)}`
65
+ // save or share the blob
66
+ }
67
+ recorder.start()
68
+ ```
69
+
70
+ `plan.reason` tells you which branch it took: `raw`, `canvas-whole` or `canvas-crop`. `plan.stop()` releases the canvas loop when you are done.
71
+
72
+ ## API
73
+
74
+ | Function | What it returns |
75
+ |---|---|
76
+ | `waitForFirstFrame(video, timeoutMs?)` | `true` once a frame has painted, `false` on timeout. Call it before planning or reading dimensions |
77
+ | `openFrontCamera()` | A `MediaStream` from the front camera, requested in the order that gives portrait on both platforms |
78
+ | `measureDrawnFrame(video, reportedW, reportedH)` | The drawn frame's `{ w, h }` from a corner probe, or `null` if the video has not painted yet |
79
+ | `planRecordingStream(video, stream)` | `{ stream, reason, stop }`, the stream to hand to `MediaRecorder` |
80
+ | `pickMimeType()` | The best supported `video/mp4` or `video/webm` type, High profile first |
81
+ | `extensionFor(mime)` | `'mp4'` or `'webm'` |
82
+ | `isIOS()` | `true` on iPhone and iPad, including iPadOS with a desktop user agent |
83
+
84
+ ## Where it came from
85
+
86
+ Extracted from web-teleprompter, a browser teleprompter that records the take with the script over the camera: https://github.com/lagudafuadtosin/web-teleprompter
87
+
88
+ Tested on iPhone 15, iOS 26.6, Safari, in normal and Low Power Mode, and on Android 15, Chrome.
89
+
90
+ ## Licence
91
+
92
+ MIT. Copyright 2026 Fuad Laguda.
@@ -0,0 +1,17 @@
1
+ export type DrawnFrame = {
2
+ w: number;
3
+ h: number;
4
+ };
5
+ export declare function measureDrawnFrame(video: HTMLVideoElement, reportedW: number, reportedH: number): DrawnFrame | null;
6
+ export declare function pickMimeType(): string | undefined;
7
+ export declare function extensionFor(mime: string | undefined): 'mp4' | 'webm';
8
+ export declare function openFrontCamera(): Promise<MediaStream>;
9
+ export type RecordingPlan = {
10
+ stream: MediaStream;
11
+ stop: () => void;
12
+ reason: 'raw' | 'canvas-whole' | 'canvas-crop';
13
+ };
14
+ export declare function planRecordingStream(liveVideo: HTMLVideoElement, stream: MediaStream): RecordingPlan;
15
+ export declare function isIOS(): boolean;
16
+ export declare function waitForFirstFrame(video: HTMLVideoElement, timeoutMs?: number): Promise<boolean>;
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,UAAU,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAOjD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CA4BlH;AAID,wBAAgB,YAAY,IAAI,MAAM,GAAG,SAAS,CAuBjD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,GAAG,MAAM,CAErE;AAKD,wBAAsB,eAAe,IAAI,OAAO,CAAC,WAAW,CAAC,CAyB5D;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,WAAW,CAAA;IAEnB,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,MAAM,EAAE,KAAK,GAAG,cAAc,GAAG,aAAa,CAAA;CAC/C,CAAA;AAQD,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,gBAAgB,EAAE,MAAM,EAAE,WAAW,GAAG,aAAa,CAkDnG;AAED,wBAAgB,KAAK,IAAI,OAAO,CAG/B;AAYD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,gBAAgB,EAAE,SAAS,SAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAmB7F"}
package/dist/index.js ADDED
@@ -0,0 +1,203 @@
1
+ // Front camera helpers for recording a portrait take in a phone browser.
2
+ // Plain browser APIs, no dependencies. The commented out code is what was
3
+ // tried first and why it was dropped. It is left in on purpose.
4
+ // Which way does the picture actually extend when this <video> is drawn?
5
+ // On iOS 26.6 and on Android Chrome the track says 1920x1080 while the browser draws a portrait
6
+ // frame. Asked for portrait, the recorder gives the 1920x1080 sensor frame back, with a
7
+ // rotation note so it plays upright but wide (WebKit bug 323550). Not the portrait you saw.
8
+ // So we draw one frame onto a 16px canvas and look at where the paint lands.
9
+ export function measureDrawnFrame(video, reportedW, reportedH) {
10
+ // Wrong theory 1: read the rotation from the track. There is nothing to read.
11
+ // const angle = (video.srcObject as MediaStream).getVideoTracks()[0].getSettings().rotation
12
+ // Wrong theory 2: the sensor is square, so width and height are the same thing.
13
+ // if (reportedW === reportedH) return { w: reportedW, h: reportedH }
14
+ try {
15
+ const big = Math.max(reportedW, reportedH);
16
+ const S = 16;
17
+ const c = document.createElement('canvas');
18
+ c.width = S;
19
+ c.height = S;
20
+ const ctx = c.getContext('2d', { willReadFrequently: true });
21
+ if (!ctx)
22
+ return null;
23
+ ctx.clearRect(0, 0, S, S);
24
+ ctx.save();
25
+ ctx.scale(S / big, S / big);
26
+ ctx.drawImage(video, 0, 0);
27
+ ctx.restore();
28
+ const px = ctx.getImageData(0, 0, S, S).data;
29
+ const painted = (x, y) => px[(y * S + x) * 4 + 3] > 0;
30
+ const wide = painted(S - 1, 1) && !painted(1, S - 1);
31
+ const tall = painted(1, S - 1) && !painted(S - 1, 1);
32
+ if (wide)
33
+ return { w: big, h: Math.min(reportedW, reportedH) };
34
+ if (tall)
35
+ return { w: Math.min(reportedW, reportedH), h: big };
36
+ return null;
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ // Best container and codec this browser supports, strongest H.264 profile
43
+ // first. Safari says yes to Baseline and to High, so list order decides.
44
+ export function pickMimeType() {
45
+ if (typeof MediaRecorder === 'undefined')
46
+ return undefined;
47
+ // First version. Baseline was listed first, got picked, and every take
48
+ // came out soft.
49
+ // const candidates = ['video/mp4;codecs=avc1.42E01E,mp4a.40.2', 'video/mp4', 'video/webm']
50
+ const candidates = [
51
+ 'video/mp4;codecs=avc1.640028,mp4a.40.2',
52
+ 'video/mp4;codecs=avc1.64001F,mp4a.40.2',
53
+ 'video/mp4;codecs=avc1.4D401F,mp4a.40.2',
54
+ 'video/mp4;codecs=avc1.42E01E,mp4a.40.2',
55
+ 'video/mp4;codecs=avc1,mp4a.40.2',
56
+ 'video/mp4',
57
+ 'video/webm;codecs=vp9,opus',
58
+ 'video/webm;codecs=vp8,opus',
59
+ 'video/webm',
60
+ ];
61
+ return candidates.find((c) => {
62
+ try {
63
+ return MediaRecorder.isTypeSupported(c);
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ });
69
+ }
70
+ export function extensionFor(mime) {
71
+ return mime && mime.startsWith('video/mp4') ? 'mp4' : 'webm';
72
+ }
73
+ // Open the front camera. Ask in landscape numbers even for a portrait take.
74
+ // The phone matches them to a native preset and rotates the picture itself.
75
+ // Permission errors are thrown at once, anything else tries the next set.
76
+ export async function openFrontCamera() {
77
+ const audio = { echoCancellation: true, noiseSuppression: true };
78
+ // First version. Asking for portrait numbers, exact or ideal, made both iOS
79
+ // Safari and Android Chrome return the wide 1920x1080 preset unrotated.
80
+ // The take landed sideways and zoomed in on one eye.
81
+ // { video: { facingMode: 'user', width: { exact: 1080 }, height: { exact: 1920 } }, audio },
82
+ // { video: { facingMode: 'user', width: { exact: 720 }, height: { exact: 1280 } }, audio },
83
+ // { video: { facingMode: 'user', aspectRatio: { exact: 9 / 16 } }, audio },
84
+ // { video: { facingMode: 'user', width: { ideal: 1080 }, height: { ideal: 1920 } }, audio },
85
+ const attempts = [
86
+ { video: { facingMode: 'user', width: { ideal: 1920 }, height: { ideal: 1080 }, frameRate: { ideal: 30 } }, audio },
87
+ { video: { facingMode: 'user', width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30 } }, audio },
88
+ { video: { facingMode: 'user' }, audio },
89
+ ];
90
+ let lastErr = null;
91
+ for (const c of attempts) {
92
+ try {
93
+ return await navigator.mediaDevices.getUserMedia(c);
94
+ }
95
+ catch (e) {
96
+ lastErr = e;
97
+ const n = e?.name;
98
+ if (n === 'NotAllowedError' || n === 'SecurityError')
99
+ throw e;
100
+ }
101
+ }
102
+ throw lastErr ?? new Error('camera');
103
+ }
104
+ // Decide what to record.
105
+ // raw: portrait picture and the track agrees. Best quality.
106
+ // canvas-whole: portrait picture but the track says landscape (iOS 26.6, Android Chrome).
107
+ // The recorder would write the landscape sensor frame, so draw the picture to a
108
+ // canvas at its own size and record that.
109
+ // canvas-crop: wide picture on a portrait screen. Record the centre 9:16.
110
+ export function planRecordingStream(liveVideo, stream) {
111
+ // First version. Always cropped the centre 9:16 out of the reported size.
112
+ // On iOS 26.6 and Android that cut a 608x1080 slice out of a picture that was already
113
+ // portrait, a 3x zoom of an eye and a nose.
114
+ // const W = Math.round((st.height * 9) / 16)
115
+ // plan = { W, H: st.height, sx: Math.round((st.width - W) / 2), sw: W }
116
+ const raw = { stream, stop: () => { }, reason: 'raw' };
117
+ const vt = stream.getVideoTracks()[0];
118
+ const st = vt?.getSettings();
119
+ const canCanvas = typeof HTMLCanvasElement.prototype.captureStream === 'function';
120
+ if (!st?.width || !st?.height || !canCanvas)
121
+ return raw;
122
+ const portraitScreen = window.innerHeight >= window.innerWidth;
123
+ const drawn = measureDrawnFrame(liveVideo, st.width, st.height) ?? { w: st.width, h: st.height };
124
+ const trackLandscape = st.width > st.height;
125
+ const pictureLandscape = drawn.w > drawn.h;
126
+ let plan = null;
127
+ if (!pictureLandscape && trackLandscape) {
128
+ plan = { W: drawn.w, H: drawn.h, sx: 0, sw: drawn.w, reason: 'canvas-whole' };
129
+ }
130
+ else if (pictureLandscape && portraitScreen) {
131
+ const W = Math.round((drawn.h * 9) / 16);
132
+ plan = { W, H: drawn.h, sx: Math.round((drawn.w - W) / 2), sw: W, reason: 'canvas-crop' };
133
+ }
134
+ if (!plan)
135
+ return raw;
136
+ try {
137
+ const canvas = document.createElement('canvas');
138
+ canvas.width = plan.W;
139
+ canvas.height = plan.H;
140
+ const ctx = canvas.getContext('2d');
141
+ if (!ctx)
142
+ return raw;
143
+ const { W, H, sx, sw } = plan;
144
+ let loop = null;
145
+ const draw = () => {
146
+ ctx.drawImage(liveVideo, sx, 0, sw, H, 0, 0, W, H);
147
+ loop = requestAnimationFrame(draw);
148
+ };
149
+ draw();
150
+ const cs = canvas.captureStream(30);
151
+ const ct = cs.getVideoTracks()[0];
152
+ return {
153
+ stream: new MediaStream([ct, ...stream.getAudioTracks()]),
154
+ stop: () => {
155
+ if (loop)
156
+ cancelAnimationFrame(loop);
157
+ loop = null;
158
+ ct.stop();
159
+ },
160
+ reason: plan.reason,
161
+ };
162
+ }
163
+ catch {
164
+ return raw;
165
+ }
166
+ }
167
+ export function isIOS() {
168
+ const ua = navigator.userAgent;
169
+ return /iPhone|iPad|iPod/.test(ua) || (ua.includes('Mac') && navigator.maxTouchPoints > 1);
170
+ }
171
+ // Wait until the browser has actually painted a frame from this <video>.
172
+ // Measured on iPhone 15, iOS 26.6: the track reports the landscape sensor
173
+ // shape for the first 112 to 139 ms after the stream arrives, then corrects
174
+ // itself at the first painted frame. Reading dimensions inside that window is
175
+ // how a portrait phone publishes as landscape (LiveKit issue 2099, which
176
+ // their PR 2100 fixed with this same wait). requestVideoFrameCallback fires
177
+ // on the first paint, and it fires on a paused element that has never played
178
+ // (measured +143 ms normal, +127 ms Low Power Mode). Where the browser lacks
179
+ // it, poll for a real videoWidth instead. Resolves true when a frame is there,
180
+ // false on timeout, so the caller can decide whether to trust the track.
181
+ export function waitForFirstFrame(video, timeoutMs = 3000) {
182
+ return new Promise((resolve) => {
183
+ let done = false;
184
+ const finish = (ok) => {
185
+ if (done)
186
+ return;
187
+ done = true;
188
+ window.clearTimeout(timer);
189
+ window.clearInterval(poll);
190
+ resolve(ok);
191
+ };
192
+ const timer = window.setTimeout(() => finish(false), timeoutMs);
193
+ const v = video;
194
+ if (typeof v.requestVideoFrameCallback === 'function') {
195
+ v.requestVideoFrameCallback(() => finish(true));
196
+ }
197
+ const poll = window.setInterval(() => {
198
+ if (video.readyState >= 2 && video.videoWidth > 0 && video.videoHeight > 0)
199
+ finish(true);
200
+ }, 50);
201
+ });
202
+ }
203
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "portrait-camera",
3
+ "version": "0.1.0",
4
+ "description": "Open the front camera in portrait on iOS Safari and Android Chrome, detect when the track reports the wrong orientation, and record a file that plays upright.",
5
+ "author": "Fuad Laguda",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "sideEffects": false,
23
+ "keywords": [
24
+ "getusermedia",
25
+ "mediarecorder",
26
+ "camera",
27
+ "portrait",
28
+ "orientation",
29
+ "ios",
30
+ "safari",
31
+ "android",
32
+ "webrtc",
33
+ "front-camera"
34
+ ],
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/lagudafuadtosin/portrait-camera"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/lagudafuadtosin/portrait-camera/issues"
41
+ },
42
+ "homepage": "https://github.com/lagudafuadtosin/portrait-camera#readme",
43
+ "scripts": {
44
+ "build": "tsc -p tsconfig.json",
45
+ "prepublishOnly": "npm run build"
46
+ },
47
+ "devDependencies": {
48
+ "typescript": "~6.0.2"
49
+ }
50
+ }