npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

portrait-camera

v0.1.0

Published

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.

Readme

portrait-camera

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.

Seven functions, no dependencies, MIT.

npm install portrait-camera

The problem this solves

Ask a phone browser for the front camera in portrait and two things go wrong.

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.

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.

The measurements, the reproduction and the fixes are public:

  • 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
  • Chromium issue 558254908: https://issues.chromium.org/issues/558254908
  • LiveKit issue 2099, which produced PR 2100 in their client SDK: https://github.com/livekit/client-sdk-js/issues/2099
  • LiveKit issue 2107, which produced PR 2110: https://github.com/livekit/client-sdk-js/issues/2107
  • 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

What it does

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.

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.

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().

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.

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.

Usage

import {
  openFrontCamera,
  waitForFirstFrame,
  planRecordingStream,
  pickMimeType,
  extensionFor,
} from 'portrait-camera'

const stream = await openFrontCamera()
video.srcObject = stream
await video.play()
await waitForFirstFrame(video)

const plan = planRecordingStream(video, stream)
const mime = pickMimeType()
const recorder = new MediaRecorder(plan.stream, mime ? { mimeType: mime } : undefined)

const chunks: BlobPart[] = []
recorder.ondataavailable = (e) => chunks.push(e.data)
recorder.onstop = () => {
  plan.stop()
  const blob = new Blob(chunks, { type: mime })
  const name = `take.${extensionFor(mime)}`
  // save or share the blob
}
recorder.start()

plan.reason tells you which branch it took: raw, canvas-whole or canvas-crop. plan.stop() releases the canvas loop when you are done.

API

| Function | What it returns | |---|---| | waitForFirstFrame(video, timeoutMs?) | true once a frame has painted, false on timeout. Call it before planning or reading dimensions | | openFrontCamera() | A MediaStream from the front camera, requested in the order that gives portrait on both platforms | | measureDrawnFrame(video, reportedW, reportedH) | The drawn frame's { w, h } from a corner probe, or null if the video has not painted yet | | planRecordingStream(video, stream) | { stream, reason, stop }, the stream to hand to MediaRecorder | | pickMimeType() | The best supported video/mp4 or video/webm type, High profile first | | extensionFor(mime) | 'mp4' or 'webm' | | isIOS() | true on iPhone and iPad, including iPadOS with a desktop user agent |

Where it came from

Extracted from web-teleprompter, a browser teleprompter that records the take with the script over the camera: https://github.com/lagudafuadtosin/web-teleprompter

Tested on iPhone 15, iOS 26.6, Safari, in normal and Low Power Mode, and on Android 15, Chrome.

Licence

MIT. Copyright 2026 Fuad Laguda.