Skip to content

Wait for the first video frame on iPadOS in waitForDimensions - #2110

Open
1egoman wants to merge 1 commit into
mainfrom
fix/wait-for-dimensions-ipados
Open

1egoman wants to merge 1 commit into
mainfrom
fix/wait-for-dimensions-ipados

Conversation

@1egoman

@1egoman 1egoman commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Updates the check from #2100 to also wait for the first video frame to go through the capture pipeline on iPads (as well as iPhones) as iPads suffer from the same issue.

More info / context can be found in #2107 (comment).

waitForDimensions gated its first-frame wait on getBrowser()?.os === 'iOS'.
browserParser decides iOS versus macOS for Safari by whether the user agent
contains `mobile/`, and iPadOS has sent the desktop Macintosh user agent by
default since iPadOS 13, so iPads took the old path and read getSettings()
immediately. The same device flipped between the two paths depending on the
per-site "Request Mobile Website" setting.

Measured on an iPad 6 running iPadOS 17, front camera, held in portrait: the
track reports 1280x960 for ~600ms and then corrects to 960x1280 at the first
painted frame, in 5 of 5 captures. That is the window waitForDimensions was
reading inside, so a portrait iPad announced itself to the room as landscape.

Adds isIPadOS()/isAppleMobile() and gates on the latter. isIPadOS() keys on
touch capability, which separates the two platforms cleanly: macOS Safari
implements no touch events at all (TouchEvent undefined, maxTouchPoints 0
regardless of any attached touchscreen) while the iPad reports maxTouchPoints
5. It is scoped to Safari on a Macintosh user agent, so no other browser
reaches the check.

Deliberately a separate helper rather than a fix inside browserParser: iPadOS
spoofs the Mac version too, so reporting os: 'iOS' there would leave osVersion
at '10.15.7' and silently break isSafari17Based() and isSafariSvcApi(), which
compare it.

The gate stays narrow rather than running everywhere. On macOS Safari the
first frame takes 487-826ms (measured over 10 captures) and getSettings()
never changes, so waiting there would be pure added publish latency on the
critical path for a bug that platform does not have.

Fixes #2107
@changeset-bot

changeset-bot Bot commented Sep 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ba76fe8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
livekit-client Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size
dist/livekit-client.esm.mjs 111.57 KB (+0.04% 🔺)
dist/livekit-client.umd.js 120.71 KB (+0.04% 🔺)

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Devin Review

@1egoman
1egoman requested a review from davidzhao September 18, 2026 14:55
Comment thread src/room/utils.ts
Comment on lines +303 to +329
/**
* iPadOS has sent the desktop Macintosh user agent by default since iPadOS 13, so `getBrowser()`
* reports `os: 'macOS'` there and every `os === 'iOS'` check misses it. macOS Safari implements no
* touch events at all (`TouchEvent` undefined, `maxTouchPoints` 0) regardless of any touchscreen
* attached, so touch capability separates the two. Measured on an iPad 6 / iPadOS 17
* (`maxTouchPoints` 5) against macOS Safari 26.4 (`maxTouchPoints` 0).
*
* Deliberately a separate helper rather than a fix in `browserParser`: iPadOS spoofs the Mac
* version too, so reporting `os: 'iOS'` there would leave `osVersion` at `10.15.7` and silently
* break {@link isSafari17Based} and {@link isSafariSvcApi}, which compare it.
*/
export function isIPadOS(): boolean {
if (!isWeb()) {
return false;
}
const b = getBrowser();
return b?.name === 'Safari' && b?.os === 'macOS' && navigator.maxTouchPoints > 1;
}

/**
* iPhone, iPad and iOS-hosted browsers — everything running the Apple camera capture pipeline,
* including iPads that present themselves as a Mac.
*/
export function isAppleMobile(): boolean {
return getBrowser()?.os === 'iOS' || isIPadOS();
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the highest risk part of this change.

As a datapoint, I have an older ipad 6 and iphone 11 kicking around along with my mac - I ran the above functions on all three platforms and isIPadOS() returned true only for the ipad, and isAppleMobile() returned true for both the ipad and iphone.

Related issue: #1788. Not 100% sure this handles all cases right though and would like another perspective before merging.

@davidzhao davidzhao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

helpful context. lgtm!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants