@joewinke/jatui 0.2.23 → 0.2.25
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/dist/actions/floating.js +11 -1
- package/dist/meet/PreJoinLobby.svelte +82 -23
- package/package.json +1 -1
package/dist/actions/floating.js
CHANGED
|
@@ -75,6 +75,17 @@ export function floating(node, options) {
|
|
|
75
75
|
function applyPosition() {
|
|
76
76
|
if (!opts.anchor)
|
|
77
77
|
return;
|
|
78
|
+
// Anchor the element synchronously BEFORE the async computePosition
|
|
79
|
+
// refines it. computePosition resolves on a microtask, and until then a
|
|
80
|
+
// position:fixed element with auto left/top renders at its static flow
|
|
81
|
+
// position — observed in prod as a context menu stuck at the container's
|
|
82
|
+
// top-left instead of the cursor. The raw anchor rect is the correct
|
|
83
|
+
// coarse position (exact for virtual/cursor anchors); flip/shift then
|
|
84
|
+
// refine it when the promise lands.
|
|
85
|
+
const rect = opts.anchor.getBoundingClientRect();
|
|
86
|
+
node.style.position = 'fixed';
|
|
87
|
+
node.style.left = `${rect.left}px`;
|
|
88
|
+
node.style.top = `${rect.bottom + (opts.offset ?? 0)}px`;
|
|
78
89
|
const middlewares = [];
|
|
79
90
|
if (opts.offset != null)
|
|
80
91
|
middlewares.push(fuOffset(opts.offset));
|
|
@@ -87,7 +98,6 @@ export function floating(node, options) {
|
|
|
87
98
|
strategy: 'fixed',
|
|
88
99
|
middleware: middlewares,
|
|
89
100
|
}).then(({ x, y }) => {
|
|
90
|
-
node.style.position = 'fixed';
|
|
91
101
|
node.style.left = `${x}px`;
|
|
92
102
|
node.style.top = `${y}px`;
|
|
93
103
|
});
|
|
@@ -78,42 +78,93 @@
|
|
|
78
78
|
if (videoEl) videoEl.srcObject = null;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// Serialize acquisitions: toggles/device switches re-run acquire() while a
|
|
82
|
+
// previous getUserMedia may still be pending, and a stale resolution used to
|
|
83
|
+
// stop the live stream or attach a dead one (black preview). Latest-wins:
|
|
84
|
+
// stale acquisitions stop their own tracks and change nothing.
|
|
85
|
+
let acquireSeq = 0;
|
|
86
|
+
|
|
87
|
+
function videoConstraint(): MediaStreamConstraints['video'] {
|
|
88
|
+
if (!cameraEnabled) return false;
|
|
89
|
+
return videoDeviceId ? { deviceId: { exact: videoDeviceId } } : true;
|
|
90
|
+
}
|
|
91
|
+
function audioConstraint(): MediaStreamConstraints['audio'] {
|
|
92
|
+
if (!micEnabled) return false;
|
|
93
|
+
return audioDeviceId ? { deviceId: { exact: audioDeviceId } } : true;
|
|
94
|
+
}
|
|
95
|
+
|
|
81
96
|
async function acquire() {
|
|
82
97
|
if (typeof navigator === 'undefined' || !navigator.mediaDevices?.getUserMedia) {
|
|
83
98
|
permissionError = 'This browser does not support camera/microphone access.';
|
|
84
99
|
return;
|
|
85
100
|
}
|
|
101
|
+
const seq = ++acquireSeq;
|
|
86
102
|
stopStream();
|
|
87
103
|
permissionError = null;
|
|
88
104
|
if (!cameraEnabled && !micEnabled) return;
|
|
105
|
+
|
|
106
|
+
// One combined call first (single permission prompt). If it fails with
|
|
107
|
+
// both kinds requested, fall back to acquiring each kind separately so
|
|
108
|
+
// one bad device (camera held by another app, unplugged mic) doesn't
|
|
109
|
+
// take the other down with it — that failure mode left the lobby with a
|
|
110
|
+
// dead preview tile even though the mic was fine.
|
|
111
|
+
let next: MediaStream | null = null;
|
|
112
|
+
let failedKinds: string[] = [];
|
|
113
|
+
let blocked = false;
|
|
89
114
|
try {
|
|
90
|
-
|
|
91
|
-
video:
|
|
92
|
-
|
|
93
|
-
? { deviceId: { exact: videoDeviceId } }
|
|
94
|
-
: true
|
|
95
|
-
: false,
|
|
96
|
-
audio: micEnabled
|
|
97
|
-
? audioDeviceId
|
|
98
|
-
? { deviceId: { exact: audioDeviceId } }
|
|
99
|
-
: true
|
|
100
|
-
: false
|
|
115
|
+
next = await navigator.mediaDevices.getUserMedia({
|
|
116
|
+
video: videoConstraint(),
|
|
117
|
+
audio: audioConstraint()
|
|
101
118
|
});
|
|
102
|
-
stream = next;
|
|
103
|
-
if (videoEl && cameraEnabled) {
|
|
104
|
-
videoEl.srcObject = next;
|
|
105
|
-
void videoEl.play().catch(() => {});
|
|
106
|
-
}
|
|
107
|
-
// Enumerate labelled devices now that permission is granted.
|
|
108
|
-
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
109
|
-
videoDevices = devices.filter((d) => d.kind === 'videoinput');
|
|
110
|
-
audioDevices = devices.filter((d) => d.kind === 'audioinput');
|
|
111
119
|
} catch (err) {
|
|
120
|
+
if (err instanceof DOMException && err.name === 'NotAllowedError') blocked = true;
|
|
121
|
+
else if (cameraEnabled && micEnabled) {
|
|
122
|
+
for (const kind of ['audio', 'video'] as const) {
|
|
123
|
+
try {
|
|
124
|
+
const partial = await navigator.mediaDevices.getUserMedia(
|
|
125
|
+
kind === 'video'
|
|
126
|
+
? { video: videoConstraint(), audio: false }
|
|
127
|
+
: { video: false, audio: audioConstraint() }
|
|
128
|
+
);
|
|
129
|
+
if (next) partial.getTracks().forEach((t) => next!.addTrack(t));
|
|
130
|
+
else next = partial;
|
|
131
|
+
} catch {
|
|
132
|
+
failedKinds.push(kind === 'video' ? 'camera' : 'microphone');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
failedKinds.push(cameraEnabled ? 'camera' : 'microphone');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (seq !== acquireSeq) {
|
|
141
|
+
// A newer acquisition superseded this one while we awaited.
|
|
142
|
+
next?.getTracks().forEach((t) => t.stop());
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (blocked) {
|
|
112
147
|
permissionError =
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
: 'Could not access your camera or microphone.';
|
|
148
|
+
'Camera/microphone access was blocked. Enable it in your browser to preview.';
|
|
149
|
+
return;
|
|
116
150
|
}
|
|
151
|
+
if (failedKinds.length) {
|
|
152
|
+
permissionError = `Could not access your ${failedKinds.join(' or ')}${
|
|
153
|
+
failedKinds.includes('camera') ? ' — it may be in use by another app' : ''
|
|
154
|
+
}.`;
|
|
155
|
+
}
|
|
156
|
+
if (!next) return;
|
|
157
|
+
|
|
158
|
+
stream = next;
|
|
159
|
+
if (videoEl && cameraEnabled && next.getVideoTracks().length) {
|
|
160
|
+
videoEl.srcObject = next;
|
|
161
|
+
void videoEl.play().catch(() => {});
|
|
162
|
+
}
|
|
163
|
+
// Enumerate labelled devices now that permission is granted.
|
|
164
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
165
|
+
if (seq !== acquireSeq) return;
|
|
166
|
+
videoDevices = devices.filter((d) => d.kind === 'videoinput');
|
|
167
|
+
audioDevices = devices.filter((d) => d.kind === 'audioinput');
|
|
117
168
|
}
|
|
118
169
|
|
|
119
170
|
// (Re)acquire whenever the enabled toggles or selected devices change.
|
|
@@ -169,6 +220,14 @@
|
|
|
169
220
|
</svg>
|
|
170
221
|
<span class="text-sm">Camera is off</span>
|
|
171
222
|
</div>
|
|
223
|
+
{:else if !stream && !permissionError}
|
|
224
|
+
<!-- Camera requested but no stream yet: the browser permission prompt is
|
|
225
|
+
(or is about to be) up. Without this state the tile is a blank black
|
|
226
|
+
box while the user decides — observed on iOS Safari. -->
|
|
227
|
+
<div class="absolute inset-0 flex flex-col items-center justify-center gap-3 px-6 text-center text-base-content/60">
|
|
228
|
+
<span class="loading loading-ring loading-lg" aria-hidden="true"></span>
|
|
229
|
+
<span class="text-sm">Allow camera & microphone access to see your preview</span>
|
|
230
|
+
</div>
|
|
172
231
|
{/if}
|
|
173
232
|
|
|
174
233
|
<!-- Preview device toggles -->
|