Skip to content

Commit b100d2f

Browse files
committed
Fix shared Go Live camera capture race
1 parent 8ed1e8e commit b100d2f

6 files changed

Lines changed: 43 additions & 12 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,7 @@ Ask first:
506506
- hardcoded fallback reader/test fixtures such as inline `Ready` chunks, fake word models, or synthetic UI state embedded directly in tests when the same behavior can be exercised through shared script fixtures, builders, or production-owned constants
507507
- agent-started local servers taking shared user ports or using ports outside the reserved `5050-5070` agent range
508508
- brittle selectors without dedicated test attributes
509+
- any assumption that local Playwright or MCP browser automation must use Chrome; prefer `msedge` when a browser flag is needed because the user's local workflow is Edge-first
509510
- progress updates that imply a fix is done before there is concrete implementation and verification evidence; keep status factual and let the user verify final behavior personally
510511
- slow repo-wide serialized test runs as the default for small UI fixes when targeted component/browser suites already cover the changed slice; prefer the fastest relevant proof first and only escalate to the slow full solution path when the user explicitly asks for it
511512
- long-running local import or conversion flows that leave the shell looking frozen; file import must expose a visible in-app busy/progress state and a clear completion or failure transition

src/PrompterOne.Shared/GoLive/Pages/GoLivePage.CameraLifecycle.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
using Microsoft.JSInterop;
2-
31
namespace PrompterOne.Shared.Pages;
42

53
public partial class GoLivePage

src/PrompterOne.Shared/GoLive/Pages/GoLivePage.Notifications.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using Microsoft.AspNetCore.Components;
21
using Microsoft.AspNetCore.Components.Routing;
32
using PrompterOne.Shared.Contracts;
43

src/PrompterOne.Shared/wwwroot/media/browser-media.js

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
const defaultDeviceId = "default";
77
const microphoneMonitorLevelMultiplier = 2800;
88
const monitorMap = new Map();
9+
const pendingCameraCaptureMap = new Map();
910
const remoteCaptureMap = new Map();
1011
const remoteStreamMap = new Map();
1112
const appleVendorFragment = "Apple";
@@ -396,17 +397,43 @@
396397
}
397398
}
398399

400+
function createCameraCapture(deviceId) {
401+
const liveKitClient = getLiveKitClient();
402+
return liveKitClient.createLocalVideoTrack(getTrackCaptureOptions(deviceId)).then(track => ({
403+
refCount: 0,
404+
track
405+
}));
406+
}
407+
408+
async function getOrCreateCameraCapture(captureKey, deviceId) {
409+
const existingCapture = cameraCaptureMap.get(captureKey);
410+
if (existingCapture) {
411+
return existingCapture;
412+
}
413+
414+
let pendingCapture = pendingCameraCaptureMap.get(captureKey);
415+
if (!pendingCapture) {
416+
pendingCapture = createCameraCapture(deviceId);
417+
pendingCameraCaptureMap.set(captureKey, pendingCapture);
418+
}
419+
420+
try {
421+
const resolvedCapture = await pendingCapture;
422+
if (!cameraCaptureMap.has(captureKey)) {
423+
cameraCaptureMap.set(captureKey, resolvedCapture);
424+
}
425+
426+
return cameraCaptureMap.get(captureKey);
427+
} finally {
428+
if (pendingCameraCaptureMap.get(captureKey) === pendingCapture) {
429+
pendingCameraCaptureMap.delete(captureKey);
430+
}
431+
}
432+
}
433+
399434
async function acquireCameraCapture(deviceId) {
400435
const captureKey = getCaptureKey(deviceId);
401-
let capture = cameraCaptureMap.get(captureKey);
402-
if (!capture) {
403-
const liveKitClient = getLiveKitClient();
404-
capture = {
405-
refCount: 0,
406-
track: await liveKitClient.createLocalVideoTrack(getTrackCaptureOptions(deviceId))
407-
};
408-
cameraCaptureMap.set(captureKey, capture);
409-
}
436+
const capture = await getOrCreateCameraCapture(captureKey, deviceId);
410437

411438
capture.refCount += 1;
412439
const stream = new MediaStream([capture.track.mediaStreamTrack]);

tests/PrompterOne.Web.UITests.Studio/GoLive/GoLiveMediaCleanupLifecycleTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,10 @@ await page.WaitForFunctionAsync(
210210

211211
var activeVideoTrackCount = await page.EvaluateAsync<int>(BrowserTestConstants.Media.GetActiveVideoTrackCountScript);
212212
await Assert.That(activeVideoTrackCount).IsGreaterThan(0);
213+
var activePrimaryCameraTrackCount = await page.EvaluateAsync<int>(
214+
BrowserTestConstants.Media.GetActiveVideoTrackCountForDeviceScript,
215+
BrowserTestConstants.Media.PrimaryCameraId);
216+
await Assert.That(activePrimaryCameraTrackCount).IsEqualTo(BrowserTestConstants.Media.ExpectedVideoTrackCount);
213217

214218
await page.GetByTestId(UiTestIds.GoLive.Back).ClickAsync();
215219
await page.WaitForURLAsync(BrowserTestConstants.Routes.Pattern(BrowserTestConstants.Routes.Library));

tests/PrompterOne.Web.UITests/Media/BrowserTestConstants.Media.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ public static class Media
5858
$$"""testId => { const element = document.querySelector(`[data-test="${testId}"]`); return window["{{HarnessGlobal}}"].getElementState(element?.id ?? ""); }""";
5959
public static string GetActiveVideoTrackCountScript =>
6060
$$"""() => window["{{HarnessGlobal}}"].getActiveTrackCount({ kind: "video" })""";
61+
public static string GetActiveVideoTrackCountForDeviceScript =>
62+
$$"""deviceId => window["{{HarnessGlobal}}"].getActiveTrackCount({ kind: "video", deviceId })""";
6163
public static string GetActiveVideoTracksScript =>
6264
$$"""() => window["{{HarnessGlobal}}"].getActiveTracks({ kind: "video" })""";
6365
public static string HasNoActiveVideoTracksScript =>

0 commit comments

Comments
 (0)