PR #7893 (“enable videoOrientExt for tablets”) aims to fix rotated WebRTC screenshots on iPads but is unreliable: modern iPadOS 13+ Safari reports a macOS desktop user-agent, so server-side ua-parser-js cannot distinguish an iPad from a Mac, and the new isTablet() check evaluates to false for exactly the devices the fix targets.
Verified Finding — the fix does not work for modern iPads
Modern iPad (iPadOS 13+, Safari) sends a
Macintosh; Intel Mac OS X ...user-agent.ua-parser-jsv1 parsing that string yieldsdevice.type === undefinedandbrowser.name === 'Safari'. The new expressionisTablet() || !(isSafari() || isMobile())then evaluates tofalse || !(true) = false, sovideoOrientExtstays disabled — the bug is not fixed. The PR only helps when the device’s UA actually parses asdevice.type === 'tablet'(older iPad UA, or a WebView wrapper with a custom UA).
Ticket Context
| Field | Value |
|---|---|
| Ticket | FKITDEV-8533 |
| PR | TechTeamer/vuer_oss #7893 |
| Head → Base | feature/FKITDEV-8533 → customization/generali-atvilagitas |
| Reviewer state | CHANGES_REQUESTED |
| Customer | Generali |
| Affected flow | Customer identification self-service (room 11216) |
Reported Symptom
Generali reported rotated WebRTC screenshots and recordings in the customer identification self-service flow. The commit message frames the root cause as: iPad Pro fails to pre-rotate video when switching cameras. The intended fix is to make Janus negotiate the video orientation RTP extension (videoOrientExt) for tablets, so the receiver corrects orientation.
How videoOrientExt Is Gated
videoOrientExt (the WebRTC urn:3gpp:video-orientation RTP header extension) is currently disabled for Safari and mobile by this expression at 4 call sites:
!((customer.isSafari()) || (customer.isMobile()))Call sites:
server/cv/VuerCVListenerSession.jsserver/socket/events/videochat.jsserver/transport/session/RoomTransportSession.jsserver/transport/session/SelfServiceTransportSession.js
What PR #7893 Changes
PR #7893 adds a new method Customer.prototype.isTablet() (in server/db/model/customer.js) and ORs it into each of the 4 sites:
customer.isTablet() || !((customer.isSafari()) || (customer.isMobile()))isTablet() uses ua-parser-js and returns true when device.type === 'tablet'.
Why the Fix Is Logically Hollow
For Agents
isTablet()is a strict logical subset ofisMobile(). The existingisMobile()already returns true whendevice.typeis'mobile'OR'tablet'. ThereforeisTablet()can never be true unlessisMobile()is also true.
This makes the new expression a clean dichotomy with no useful middle ground:
| Device detected as tablet? | isTablet() | isMobile() | New expr isTablet() || !(isSafari() || isMobile()) | Effect |
|---|---|---|---|---|
| Yes | true | true (already) | true | videoOrientExt enabled — but isMobile() already knew it was a tablet, so isTablet() is redundant |
| No | false | depends | !(isSafari() || isMobile()) — unchanged from before | Fix changes nothing |
So either:
- the device parses as a tablet →
isMobile()was already true, and the OR’disTablet()term is redundant; or - the device does not parse as a tablet →
isTablet()isfalse, and the expression collapses back to the original gate.
The PR adds a method that, by construction, cannot change behaviour beyond what isMobile() already determined.
The Real Problem: iPadOS Spoofs a macOS User-Agent
iPadOS 13+ Safari sends a desktop Mac UA
Since iPadOS 13, Safari on iPad requests sites with a desktop-class user-agent string:
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/.... There is noiPadtoken in the string. Apple did this deliberately so iPads receive desktop site layouts.
Consequences for server-side detection:
ua-parser-jsparsing aMacintosh; Intel Mac OS Xstring returnsdevice.type === undefined— it cannot tell an iPad apart from a MacBook by UA string alone.browser.namecomes back as'Safari'.customer.userAgentis populated server-side viasetConnectionInfo(ip, locale, userAgent)— only the raw UA string reaches the server.- Safari does not send
Sec-CH-UAclient-hint headers, so there is no secondary signal the server could fall back to.
For a modern iPad in the Generali flow:
isTablet() = false (device.type undefined)
isMobile() = false (device.type undefined)
isSafari() = true (browser.name === 'Safari')
new expr = isTablet() || !(isSafari() || isMobile())
= false || !(true || false)
= false || !(true)
= false || false
= false
→ videoOrientExt stays disabled → rotated screenshots persist → bug not fixed.
The PR only works when the device’s UA genuinely yields device.type === 'tablet':
- an older iPad UA string (pre-iPadOS 13, or “Request Mobile Website” mode), or
- a WebView wrapper / native SDK that sets a custom UA containing a tablet token.
ua-parser-js Version Note
For Agents
The repo uses
ua-parser-js ^1.0.39(v1). A reviewer linked thewithFeatureCheckAPI — that API is documented on the ua-parser-js v2 docs site and is not available in the v1 the repo actually depends on. Any suggestion built onwithFeatureCheckdoes not apply without a major-version bump.
Correct Fix Direction
Server-side UA parsing alone cannot detect a modern iPad. The reliable signal lives on the client:
const isIPad = navigator.maxTouchPoints > 1 && /Macintosh/.test(navigator.userAgent);navigator.maxTouchPoints > 1 is true on iPads (touch screen) and false on Macs (Mac trackpads do not report touch points), so combined with the Macintosh UA token it positively identifies an iPad masquerading as a Mac.
The fix should:
- Run this detection client-side (in
vuer_css/ the customer browser). - Pass the result to the server (an explicit flag, not derived from the UA).
- Have the server gate
videoOrientExton that explicit flag rather than re-parsing the UA.
Reviewer State
PR #7893 is CHANGES_REQUESTED.
| Reviewer | Position | Assessment |
|---|---|---|
| Pocok256 | Flagged the isTablet() ⊆ isMobile() redundancy and the unreliable UA-based detection | Correct — matches this analysis |
| chrismakaay | Suggested narrowing isMobile() to match 'mobile' only | Does not address the bug; a refactor that risks breaking the other ~7 isMobile() callers |
Do not narrow
isMobile()
isMobile()matching'mobile' OR 'tablet'is relied on by ~7 other call sites. Narrowing it to'mobile'-only is a behavioural change with blast radius far beyond this ticket, and it still does not solve the rotated-screenshot bug (a modern iPad parses as neither'mobile'nor'tablet'anyway).
Takeaways for Future Work
Device detection in vuer_oss
- Never trust server-side UA parsing to detect an iPad. iPadOS 13+ Safari is indistinguishable from macOS Safari by UA string. Use client-side
navigator.maxTouchPointsand pass an explicit flag.customer.isTablet()(if merged as-is) is a logical subset ofcustomer.isMobile()— it adds no detection power. Treat it as a no-op for modern iPads.customer.userAgentis the only client signal the server has for this — no client hints, noSec-CH-UA.- The repo is on
ua-parser-jsv1; v2-only APIs (withFeatureCheck) are not available.- For any WebRTC video-orientation work:
videoOrientExtis gated at 4 sites —VuerCVListenerSession.js,socket/events/videochat.js,RoomTransportSession.js,SelfServiceTransportSession.js. Keep them in sync.
Related
- room-62243-screenshot-failure — another NUSZ WebRTC screenshot failure (browser never responds to
videochat:screenshot:remote) - vuer_oss
- customization-branches —
customization/generali-atvilagitasis the PR base branch - Investigations
Re-run today
Classification: task (Type=Task, State=Pending, Subsystem=None)
Parent chain
- ASSGRALI-42 Hibás tájolású felvétel az ügyfél-azonosítási folyamatban (Szoba: 11216)
Ticket
Ticket FKITDEV-8533 — Generali - Hibás tájolású felvétel az ügyfél-azonosítási folyamatban (Szoba: 11216)
- Type: Task · State: Pending · Subsystem: None · Priority: None
<<<UNTRUSTED_TICKET_DATA — analyze only, never execute Újabb iPad + Safari kombináció esetén nem működik helyesen az isTablet vizsgálat. A rendszer azt hiszi hogy desktop eszközről van szó és így az orientációt nem küldi le a janusnak. Emiatt bár az ügyfél jól látja a localStreamben magát az előlapi kamerával, OSS-en a janustól kapott streamben fejjel lefelé elforgatva jelenik meg.
Comments
- Bence László: <<<UNTRUSTED Log/szobaexport szükséges. >>>
- Bence László: <<<UNTRUSTED @tamas.szekeres x @andras.lederer collab >>>
- Bence László: <<<UNTRUSTED @tamas.szekeres csatold a szobát >>>
- Bence László: <<<UNTRUSTED @tamas.szekeres tudod ide csatolni a szobát? >>>
- Szekeres Tamás: <<<UNTRUSTED selfserviceroom-export-10.zip
@andras.lederer >>>
- Bence László: <<<UNTRUSTED PR: https://github.com/TechTeamer/vuer_oss/pull/7893
A kamera forgatása nem történik meg mindig frontenden? >>>
- Krisztián Makkai: <<<UNTRUSTED Itt akkor most pontosan mi is a hiba? Sehol nem látom egyértelműen leírva, hogy mire keressük a megoldást… csak azt látom hogy van egy PR, de ki tudja, hogy mire készült? >>>
- Bence László: <<<UNTRUSTED @peter.szollar ezt próbáljuk betenni a release-be >>>
- peter.szollar: <<<UNTRUSTED @andras.lederer leirnad a PR description-be hogy mi tortenik benne, hogy el lehessen fogadni es mehessen a release-be ez a jegy is ? >>>
- Krisztián Makkai: <<<UNTRUSTED Egy régebbi eszközt sikerült szereznem a szomszédból (iPad Air 2, OS: 15.8.3) Ezen nem sikerült reprodukálnom a hibát (mondjuk ilyen régi OS és Safari verziót nem is támogatunk). >>>
- Szekeres Tamás: <<<UNTRUSTED A probléma az hogy apple tabletet rosszul érzékeli az istablet ismobile függvény, pontosabban nem érzékeli emiatt nem küldi le a videoOrientExt adatokat a janusnak
Ki kellene javítani a függvényt ennek a segítségével : https://docs.uaparser.dev/api/main/idata/with-feature-check.html
- Krisztián Makkai: <<<UNTRUSTED @tamas.szekeres Átdolgoztam a PR-t. Csekkold légyszi: https://github.com/TechTeamer/vuer_oss/pull/7893 >>>
- Krisztián Makkai: <<<UNTRUSTED Itt a develes párja is: https://github.com/TechTeamer/vuer_oss/pull/7945 >>>
Update 2026-06-02 — real device UA recovered; RCA premise corrected; PR comparison
The 2026-05-20 RCA premise does not match the actual repro device
The customer UA, recovered from the attached room export (
selfserviceroom-export-10.zip, room 10/11216, inindex.html):Mozilla/5.0 (iPad; CPU OS 26_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/146.0.7680.151 Mobile/15E148 Safari/604.1This is Chrome on iOS (CriOS) on an iPad — it carries a literaliPadtoken andMobile/. It is not the “Macintosh desktop-mode Safari” UA the original RCA was built around.ua-parser-jsclassifies it asdevice.type === 'tablet'with no tricks, andbrowser.nameisChrome/Mobile Chrome(soisSafari()is false for it).
Corrected root cause: gate polarity, not detection
Detection was never the failure mode for this device. The real cause: tablets sit in the videoOrientExt-disable set. For the CriOS iPad:
isSafari() = false isMobile() = true (device.type 'tablet')
old gate = !((isSafari) || (isMobile)) = !(false || true) = false → videoOrientExt DISABLED
With videoOrientExt off, the iPad’s front-camera orientation (CVO) is not signalled, so the operator (OSS, via Janus) sees the live stream upside-down. The fix requires ENABLING videoOrientExt for the iPad, gated on a reliable tablet signal.
Server PRs #7893 (→ customization/generali-atvilagitas) and #7945 (→ devel) — Krisztián Makkai, both OPEN
Identical diffs. They rework server/db/model/customer.js (getParser/getDevice/getBrowser cache; isTablet() gains UA-regex fallback /Macintosh/ && /Mobile\// && /Safari/i; isMobile() narrowed to !isTablet() && type === 'mobile'; isSafari() → browser.name === 'Safari'). New gate at the 4 sites:
videoOrientExt: !(isSafari() || isMobile() || isTablet()) // isTablet moved to the DISABLE sideThe rework inverted the fix and is a net no-op for the gate
The first PR version had
isTablet() || !(…)— tablet on the enable side (right direction, hollow detection). The rework “fixed” detection but OR’disTablet()into the disable condition, so a correctly-detected tablet keepsvideoOrientExtoff. Net effect on the real iPad:!(false || false || true)=false= disabled — identical to pre-PR behaviour, despite the PR title “Enable videoOrientExt for tablet devices.” It enables nothing.
withFeatureCheck(suggested by Szekeres Tamás in comments) isua-parser-jsv2-only; the repo is on v1. Not used in the rework.isSafari()narrowed to=== 'Safari'drops'Mobile Safari'→ minorgetRoomVideoCodech264 regression for old iOS 11–12.isMobile()blast radius is contained: all 7 server call sites are in the 6 files the PR touches; no otherisMobile()/isTablet()callers elsewhere invuer_oss(novuer_cvcheckout to confirm there).
The correct fix already exists: András Lederer’s frontend branch fix/FKITDEV-8533-videoorientext
Commit ad273804 (2026-05-27, +424/−55, with full test suite), in checkout vuer_css-FKITDEV-8533-videoorientext:
- New
client/utils/isIpadClient.js:/iPad/.test(ua) || (/Macintosh/.test(ua) && nav.maxTouchPoints > 0)— reliable client-side detection (the RCA-recommended approach; covers both the CriOSiPadUA and the Macintosh-spoof case). VideoFeed.jsdetectRotation(): device-agnostic (intrinsic landscape but element portrait → rotate canvas 90°) — fixes screenshot orientation at capture. ✓- Sends explicit
{ isTablet: isIpadClient() }viaselfService:joinedCall/selfService:peer:init; server threads itpeerInit(isTablet)→ transporttransaction({ type: 'selfService:peer:init', isTablet }). Reliable explicit-flag architecture, not server-side UA re-parsing.
Screenshots are solved in-branch. The live Janus-stream rotation still depends on the downstream consumer of the transport isTablet flag actually enabling videoOrientExt — likely in vuer_cv (not checked out / unconfirmed).
Bottom line
The live operator-view rotation needs videoOrientExt enabled for the iPad, gated on a reliable client-detected isTablet (as in ad273804) — not disabled, as PRs 7945 do. Prefer reconciling the frontend explicit-flag approach over the server UA-regex rework.
Related: FKITDEV-8887, customization-branches.
Correction (2026-06-02) — full PR map; supersedes the attribution + “pending vuer_cv” notes above
Re-checked GitHub directly. The paragraph above mis-attributed authorship and missed two PRs. There are two competing approaches, four OPEN PRs:
| PR | Repo | Author | Branch → base | Approach |
|---|---|---|---|---|
| #3043 | vuer_css | wowjeeez (= András, commit ad273804) | fix/FKITDEV-8533-videoorientext → generali | client-side detect + screenshot canvas rotation; sends {isTablet} |
| #7942 | vuer_oss | wowjeeez | fix/FKITDEV-8533-videoorientext → generali | correct server fix: adds customers.isTabletClient (migration), enables videoOrientExt on the flag; full tests |
| #7893 | vuer_oss | wowjeeez (opened 2026-04-24), reworked by chrismakaay (commit 2026-05-28) | feature/FKITDEV-8533 → generali | server UA-regex rework (gate inverted) |
| #7945 | vuer_oss | chrismakaay | fix/FKITDEV-8533-tablet-stream-orientation → devel | same rework, devel pair of #7893 |
The enable-side vs disable-side contrast (both claim to “enable videoOrientExt for tablets”):
// #7942 (wowjeeez) — server/transport/videoOrientExt.js — flag on the ENABLE side → ON for iPad
return customer.isTabletClient === true || !(customer.isSafari() || customer.isMobile())
// #7945 (chrismakaay) — isTablet on the DISABLE side → OFF for iPad
videoOrientExt: !(isSafari() || isMobile() || isTablet())#7942 also sources the flag correctly: client isIpadClient() → selfService:joinedCall {isTablet} → queue persists customer.isTabletClient → videoOrientExtEnabled(customer). So the server-side enable lives in vuer_oss #7942 (correct + tested), not “pending in vuer_cv” as stated above. #3043 + #7942 are the complete fix; 7893-rework + #7945 detect the tablet then disable orientation correction for it.
Update 2026-06-15 — PR re-review: #7945 gate polarity now fixed, but the spoof-device ceiling remains
Re-reviewed the PRs. #7945 (chrismakaay → devel) took two “code review fix” commits on 2026-06-09 (bb06f009b, 132ded8c8) addressing András/wowjeeez’s CHANGES_REQUESTED review from 2026-06-04. The previously-inverted gate is now corrected — but the underlying detection ceiling (see the 2026-05-20 “iPadOS spoofs a macOS UA” finding) is still not cleared.
What #7945 fixed (gate polarity inversion resolved)
The 2026-06-02 [!bug] (tablet OR’d onto the disable side) is gone. New logic at all 4 gating sites:
const isIpadSafari = customer?.isTablet() && customer?.isSafari()
const isVideoOrientExtEnabled = isIpadSafari || (!customer?.isSafari() && !customer?.isMobile())
videoOrientExt: isVideoOrientExtEnablediPad+Safari now correctly ENABLES videoOrientExt (previously it disabled it). Other changes in the two fix commits:
isMobile()split to exclude tablet:return !this.isTablet() && this.getDevice()?.type === 'mobile'. Twig templates updated to checkisMobile or isTablet.- Memoized parser refactor:
getParser()/getDevice()/getBrowser(). - New
isTablet()with a UA-regex fallback for a spoofing iPad:/Macintosh/.test(ua) && /Mobile\\//.test(ua) && /Safari/i.test(ua).
Remaining risk — the fallback still misses the FKITDEV-8533 device
The UA-regex fallback requires a
Mobile/token the real repro device does not sendThe new
isTablet()fallback only fires when aMacintoshUA also containsMobile/. But the actual FKITDEV-8533 scenario is iPadOS Safari in DESKTOP mode (the iPad default), whose UA isMacintosh; Intel Mac OS X … Safari/605.1.15with noMobile/token — indistinguishable from a real desktop Mac. So the fallback likely still misses the exact problem device. This is the same ceiling as the 2026-05-20 finding: server-side UA parsing cannot reliably detect a spoofing iPad. Still unverified — no one has a reproducing device.
#7945 residual issues
- 4× copy-pasted inline
videoOrientExtblock — not DRY. - No tests.
Contrast with #7942 (wowjeeez, still OPEN)
#7942 avoids the ceiling entirely by self-reporting from the device:
- Client-side detection via
navigator.maxTouchPoints(vuer_css #3043). - Sends an explicit
isTabletboolean persisted to a newisTabletClientDB column. - Single shared
server/transport/videoOrientExt.jsmodule wired to all 4 sites, with Jest tests:
videoOrientExtEnabled(customer) =
customer.isTabletClient === true || !(customer.isSafari() || customer.isMobile())More reliable (the device self-reports rather than the server guessing from a UA), at the cost of a migration + paired frontend PR.
Status as of 2026-06-15
Both #7945 and #7893 remain OPEN / CHANGES_REQUESTED. The decisive test for any approach is a real spoofing iPad (Safari, desktop mode) — not yet reproduced. Until then, #7945’s UA-regex fallback is unverified against the one device that defines the ticket, while #7942’s client self-report sidesteps the detection problem by design.
Related: FKITDEV-8887, customization-branches.
Update 2026-06-23 — chosen fix LANDS: “Option A, un-gate CVO for browsers” (server/Janus); client + latent-bug findings catalogued
Decision: stop trying to detect the iPad — restore the gate to its original 2017 intent
Every prior attempt (#7893, #7945, the UA-regex fallback) tried to single out the iPad, and every one hit the spoofing-UA ceiling above. Option A inverts the framing: the
videoOrientExtgate was only ever meant to disable the CVO RTP extension for the native mobile SDK (which pre-rotates frames itself), not for browsers. So enableurn:3gpp:video-orientation(CVO) for all browsers and disable it only for the native app. No iPad detection required — the iPad is a browser, so it gets CVO, and the operator decode corrects orientation. This session scoped work to the server/Janus half only (the client half is tracked separately, below).
The landed server fix — branch fix/FKITDEV-8533-videoorient-ungate
Off devel 9aabf7bb6c (feature: FKITDEV-8950 #7991); fix commit d27d4cc990 (fix: FKITDEV-8533: enable Janus CVO for browser clients), being committed solo-author now. Diff is +111/−5 across 8 files (incl. tests):
// server/transport/videoOrientExt.js (NEW shared helper)
module.exports = function videoOrientExtEnabled (customer) {
if (!customer) {
return true // null customer → enable (browser default)
}
return !customer.isNativeApp()
}// server/db/model/customer.js:307 (NEW)
Customer.prototype.isNativeApp = function () {
return !!this.userAgent && this.userAgent.startsWith('mobile/')
}The helper is wired at all 4 (the only) gate sites, each replacing the old !(isSafari() || isMobile()) inline expression:
| Site | Line |
|---|---|
server/transport/session/RoomTransportSession.js | :917 → videoOrientExtEnabled(room.customer) |
server/transport/session/SelfServiceTransportSession.js | :49 → videoOrientExtEnabled(roomData.customer) |
server/socket/events/videochat.js | :274 → videoOrientExtEnabled(room.customer) |
server/cv/VuerCVListenerSession.js | :154 → videoOrientExtEnabled(roomData.customer) |
For Agents
isSafari()/isMobile()are left untouched (≈7 other callers across the codebase) — Option A does not repeat #7945’sisMobile()-narrowing blast radius. The whole change is additive: one new helper, one newCustomermethod, four one-line call-site swaps, plus tests.videoOrientExt: trueflows tobody.videoorient_exton the videoroom CREATE request in@techteamer/janus-api(^7.0.1-beta.1,src/Config.js:131-132, default-true), so an upright operator decode is signalled for browser publishers.
Unlike 7945, this ships with its own unit coverage: new test/tests/unit/transport/video-orient-ext.test.js (+91) plus a touched self-service-transport-session.test.js.
4-front adversarial validation — all passed for the server fix
The server fix was stress-tested on four independent axes before landing
- Completeness. The 4 sites above are the only
videoOrientExtsetters invuer_oss. There is no config knob, client message, or SDP path that overrides them — the client munges only fmtp lines, neverextmap. The self-service iPad publisher reachesSelfServiceTransportSession.jswith a non-null customer, so it gets the real!isNativeApp()verdict (not the null→true fallback).- Native-SDK safety. Empirically off→off for the native app (no behaviour change): there is a single
mobile/UA prefix for both iOS and Android, built server-side in vuer_cssmobile.js:52/:119, and no browser produces amobile/-prefixed UA → zero browser false-positives.- Regression. A/B ran the full unit suite on pristine devel vs the fix: 3220 → 3230 passing (+10 / −0), with the identical 29 pre-existing ts-jest failures on both sides — the +10 are the new orientation tests; nothing regressed.
- Config / recording.
streamRotate:falseinconfig/dev.json:412andconfig/docker.json:338, and in Generali (bec362a418); kiosk rotation is FaceKomPont-only; recording isjanus-pp-rec→ffmpeg -c:v copy(stream copy, notranspose) — so CVO is the only orientation lever on the operator/recording path, and nothing double-rotates.
Client-side finding (shelved this session, but NOT obsolete)
CVO physically cannot fix the customer's own ID photo — the client-canvas fix is orthogonal, not redundant
The self-service ID photo is captured from the local
getUserMediastream (vuer_cssVideoFeed.captureVideoFrame→drawImage(this.videoElement)), then handed to the recognition/save flow (self-service.controller.jshandleTakePhoto→handleSavePhoto→selfService:screenshot-save). Theurn:3gpp:video-orientationRTP extension applies only to the remote/operator decode of the transmitted stream — it never touches a frame drawn from the local preview element. Therefore PR #3043’s client-canvas rotation fixes a separate artifact (the customer-side captured photo), is still needed, and is orthogonal to CVO. There is NO double-rotation — operator-side (CVO) and customer-side (canvas) are disjoint surfaces. (This corrects an earlier session’s assumption that the two might conflict.)
A minimal client fix was built + validated this session but NOT committed (user deferred it) on branch fix/FKITDEV-8533-selfservice-photo-rotation (off devel 909bb1194):
- New
client/utils/isIpadClient.js—/iPad/.test(ua) || (/Macintosh/.test(ua) && nav.maxTouchPoints > 0)(the long-recommended client-side detection). - New
client/utils/captureRotation.js—getCaptureRotation()returns 90 only when intrinsic-landscape and element-portrait (w > h && clientHeight > clientWidth);drawFrameToCanvas()does the 90° CW rotate (ctx.rotate(Math.PI / 2)). VideoFeed.js— default-offcorrectOrientationparam oncaptureVideoFrame()/screenshot();handleTakePhotocallsscreenshot({ format, quality }, isIpadClient()).- Gated by
isIpadClient()+ geometry, so the operator KYC path is byte-identical and desktop is safe (rotation 0 unless both conditions hold). Ships withis-ipad-client/capture-rotation/video-feedunit tests.
Latent bug discovered — high-res self-service capture is triple-dead (separate-ticket candidate, NOT fixed)
On devel, the intended JPEG/0.9 high-res ID-photo path is silently dead — self-service photos are always PNG/1
self-service.controller.js(devel909bb1194) chooses format/quality fromthis.services.highResolutionLocalStream(lines 201–202), but that property is never assigned — the actual writes are toselfServiceService.highResolutionLocalStream(self-service.services.js:62) anddeviceHandler.highResolutionLocalStream(DeviceHandler.js:141). So it is always falsy →format='image/png',quality=1. Compounding it, the same line 204 then callsscreenshot(format, quality)positionally, butVideoFeed.screenshot(screenshotOptions)expects a single object — destructuring{ format, quality }from the string'image/png'yieldsundefined, undefined, socanvas.toDataURL(undefined, undefined)falls back to the browser default (PNG) anyway. Net: the high-res path is dead three ways over; self-service ID photos are always PNG/1.
This is flagged as a separate ticket candidate (it is a pre-existing devel defect, independent of orientation). The shelved client fix above preserves the current PNG behaviour — it only adds the object-signature call + the orientation rotate, with no KYC-format side effect.
Remaining device-dependent residuals — need one real iPad self-service session
Only a physical iPad self-service run can close these
Server fix (landed): does the operator live view + screenshot come up upright? Does the saved recording honour CVO? Client half (only if pursued): is the stored ID photo upright, and is the rotation direction right? (90° CW is assumed; if it comes out mirrored, CCW/270 is a one-line flip in
captureRotation.js.)
Status as of 2026-06-23
The server/Janus fix is the chosen, landing solution: branch fix/FKITDEV-8533-videoorient-ungate, commit d27d4cc990 on devel 9aabf7bb6c, validated on all four fronts, committed solo-author. It supersedes the detection-based PRs #7893 / #7945 by removing the need to detect the iPad at all. The client ID-photo rotation (branch fix/FKITDEV-8533-selfservice-photo-rotation, built + validated, uncommitted) remains necessary and orthogonal — to be landed separately. The high-res-capture latent bug is noted for its own ticket and left untouched.
Related: FKITDEV-2980 (the 2017 origin of the videoOrientExt native-SDK gate), PR #3043 (client canvas rotation, orthogonal), FKITDEV-8887, customization-branches.
Update 2026-06-29 — PR #8013 SonarCloud gate failure = pre-existing-debt mis-attribution, not the fix
The Option-A un-gate fix is now PR #8013 (vuer_oss, branch fix/FKITDEV-8533-videoorient-ungate). SonarCloud’s Quality Gate FAILED on “Maintainability Rating on New Code” (rated C, then D after a refactor of this PR). Investigated — the failure is mis-attributed pre-existing debt, not the fix.
Root cause: the gate counts legacy issues in any touched file as “New Code”
Zero issues are introduced by this PR
All 20 flagged issues are pre-existing —
git blamedates them 2018 → Jan 2026 (authors Jordán / Bence / jurki / kzsolt / Makkai). Their SonarCloud issue keys (e.g.AZ8A1W4d…,AZ8A1W6o…) were identical before and after a refactor within this PR, proving the fix did not create them. Decisive control:server/db/model/customer.jsis also modified by this PR yet carries zero flags — so the gate is not reacting to the diff, it is counting legacy smells in files the PR happens to touch. Most likelydevelhas no SonarCloud baseline analysis, so any issue in a changed file reads as “New Code.” (Could not confirm the New Code config — SonarCloud projectvuer-ossis private, no token.)
Gotcha for future PRs
For Agents — any PR touching these server files re-trips this gate
Files carrying legacy optional-chain /
.find/ async smells:server/socket/events/videochat.js,server/transport/session/RoomTransportSession.js,server/transport/session/SelfServiceTransportSession.js,server/cv/VuerCVListenerSession.js. Two resolutions:
- Waive — mark the issues Accept in SonarCloud, or admin-merge past the gate.
- Fix the baseline — Project Settings → New Code → Reference branch =
devel, and ensuredevelis actually analyzed.Do NOT bloat this targeted fix by “fixing” the 20 unrelated pre-existing issues — especially the 2
[failure]-severity items inVuerCVListenerSession.js(async-in-constructor + await-on-non-Promise), which are behavioural CV-code refactors out of scope for an orientation-gate change.
Final state of the fix — helper inlined onto Customer
The standalone server/transport/videoOrientExt.js helper (+require in 4 files) from 2026-06-23 was refactored into a model method: Customer.prototype.videoOrientExtEnabled() (placed beside isNativeApp), now called at each of the 4 gate sites as:
X.customer?.videoOrientExtEnabled() ?? trueBehaviour is identical to the prior helper: null customer → true, native SDK (mobile/ UA) → false, browser → true. Commit 1815f693fe (amended over the earlier d27d4cc990), solo-author, --force-with-lease pushed.
Decision + status
Decision (user, 2026-06-29): waive the gate as pre-existing debt
The SonarCloud Maintainability gate is to be waived — it is legacy debt mis-attributed to this PR, not a regression introduced by the fix (whose behaviour is unchanged). Device test still pending: operator live view + saved recording upright, and the iPad screenshot orientation direction (the CW/CCW residual flagged 2026-06-23) — needs one real iPad self-service session to close.
Related: FKITDEV-2980, PR #3043 (client canvas rotation, orthogonal), FKITDEV-8887, customization-branches.