Skip to content

Add video support to demo instance#179

Merged
streamer45 merged 11 commits intomainfrom
demo
Mar 22, 2026
Merged

Add video support to demo instance#179
streamer45 merged 11 commits intomainfrom
demo

Conversation

@streamer45
Copy link
Owner

@streamer45 streamer45 commented Mar 21, 2026

Summary

Some misc fixes to get video working properly on our demo instance.


Staging: Open in Devin

@streamer45 streamer45 self-assigned this Mar 21, 2026
Copy link
Contributor

@staging-devin-ai-integration staging-devin-ai-integration bot left a comment

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.

Staging: Open in Devin
Debug

Playground

Copy link
Contributor

@staging-devin-ai-integration staging-devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 3 new potential issues.

View 6 additional findings in Devin Review.

Staging: Open in Devin
Debug

Playground

Comment on lines +134 to +143
const entry = await Promise.race([
announcements.next(),
new Promise<null>((r) => setTimeout(() => r(null), remaining)),
]);
if (!entry) break; // timeout
if (entry.active && entry.path.toString() === broadcastName) {
logger.info(`Broadcast '${broadcastName}' announced`);
return;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🚩 waitForBroadcastAnnouncement accesses entry.active and entry.path — verify against Hang SDK types

The new waitForBroadcastAnnouncement function at ui/src/stores/streamStoreHelpers.ts:128-149 calls announcements.next() and then accesses entry.active and entry.path. If conn.announced() returns a standard AsyncIterableIterator, then .next() would return { value: T, done: boolean }, meaning entry.active would be undefined and the broadcast would never be matched (always timing out). This depends entirely on the Hang SDK's API contract for announced(). If it returns a custom iterator where .next() resolves to the raw announcement object (not wrapped in IteratorResult), the code is correct. This should be verified against the SDK type definitions.

Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

staging-devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Contributor

@staging-devin-ai-integration staging-devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

View 9 additional findings in Devin Review.

Staging: Open in Devin
Debug

Playground

Comment on lines +165 to +168
clipPath:
layer.cropShape === 'circle'
? `circle(${Math.min(layer.width, layer.height) / 2}px at 50% 50%)`
: undefined,
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 clip-path: circle(...) clips resize handles, making circle-crop layers non-resizable via drag

The change from borderRadius: '50%' to clipPath: circle(...) on the LayerBox causes all child elements — including the ResizeHandles component — to be clipped to the circle boundary. border-radius is purely cosmetic and does not clip children, but clip-path creates a hard clipping region that hides everything outside. The corner resize handles (nw, ne, se, sw) are located at distance ~√2 × radius from center, well outside the inscribed circle, so they're fully clipped. Edge handles (n, s, e, w) are right at the circle boundary and partially/fully clipped depending on rect aspect ratio. This makes it impossible to resize a crop_shape: circle layer using the mouse handles.

Affected component structure

The resize handles are children of the LayerBox that receives the clip-path:

<LayerBox style={{ clipPath: `circle(${...}px at 50% 50%)` }}>
  <LayerLabel />
  <LayerDimensions />
  {isSelected && <ResizeHandles />}  // ← clipped!
</LayerBox>

A fix would be to move the clip-path to an inner wrapper element that doesn't contain the resize handles, or use a pseudo-element / separate overlay for the circle visual.

Prompt for agents
In ui/src/components/compositorCanvasLayers.tsx, the clipPath style on the LayerBox (lines 165-168) clips all child content including the ResizeHandles component, making it impossible to resize circle-crop layers via mouse handles.

The fix should separate the circle visual indicator from the interactive layer box. Two approaches:

1. Move the clipPath to an inner wrapper div that contains only the visual content (LayerLabel, LayerDimensions) but NOT the ResizeHandles. The ResizeHandles should remain as children of the outer LayerBox (which has no clipPath).

2. Alternatively, revert to using a visual-only approach like the old borderRadius but with a CSS outline/border technique that renders a circle without clipping children. For example, add a positioned pseudo-element (via a styled wrapper) with the circle clip-path and pointer-events: none, layered behind the resize handles.

The key requirement is that resize handles must remain visible and interactive outside the circle boundary.
Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Comment on lines +389 to +406
// Wait for the video encoder to produce a catalog entry before returning.
if (needsVideo) {
logger.info('Step 5b: Waiting for video catalog...');
try {
await waitForSignalValue(
publish.video.catalog,
(v) => v !== undefined,
10_000,
'Video encoder failed to initialize'
);
} catch (e) {
publish.close();
shutdownMediaSource(camera);
shutdownMediaSource(microphone);
throw e;
}
logger.info('Step 5b: Video catalog ready');
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🚩 Publish-before-watch reordering may produce misleading error on slow connections

In performConnect (lines 519-565), the publish path is now set up BEFORE waitForSignalValue(connection.established). If needsVideo is true, setupPublishPath waits for publish.video.catalog with a 10s timeout (streamStoreHelpers.ts:393-398). If the MoQ connection takes >10s to establish, the video catalog wait would timeout first with "Video encoder failed to initialize" — masking the real issue (slow/failed connection). The subsequent 12s connection wait at line 537 would never be reached. In practice this is unlikely since connection establishment is async and typically completes in <1s, but on degraded networks the error message could be confusing.

Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

staging-devin-ai-integration bot and others added 2 commits March 22, 2026 07:52
* fix: align e2e tests with recent UI logic changes

- Update crop shape test assertions from borderRadius to clipPath
  (compositor now uses clip-path: circle(...) instead of border-radius)

- Add isExternalRelay flag to stream store to conditionally skip
  waitForBroadcastAnnouncement in gateway mode (only needed for
  external relay pipelines with separate pub/sub nodes)

- Fix loadSamples auto-select to apply MoQ peer settings to stream
  store (pipelineNeedsVideo, serverUrl, etc.) so the store matches
  the selected template even when Radix RadioGroup doesn't fire
  onValueChange for an already-selected item

- Add --use-fake-ui-for-media-stream to Playwright config so
  getUserMedia works for video capture in headless Chromium

Co-Authored-By: Claudio Costa <cstcld91@gmail.com>

* fix: reset isExternalRelay when switching to Direct Connect mode

Direct Connect mode connects to a relay without a skit pipeline,
so there is no external relay announcement to wait for. Without
this reset, a stale isExternalRelay=true from a previous template
selection would cause an unnecessary 15s wait in performConnect.

Co-Authored-By: Claudio Costa <cstcld91@gmail.com>

* fix: restore template settings on Session mode switch and fix stale serverUrl in auto-select

- Re-apply selected template's MoQ settings (isExternalRelay, pipelineMediaTypes,
  pipelineOutputTypes) when switching back to Session mode from Direct Connect
- Use useStreamStore.getState().serverUrl in loadSamples auto-select instead of
  stale closure-captured value that is always '' on mount

Co-Authored-By: Claudio Costa <cstcld91@gmail.com>

---------

Co-authored-by: StreamKit Devin <devin@streamkit.dev>
Co-authored-by: Claudio Costa <cstcld91@gmail.com>
@streamer45 streamer45 merged commit 6492b2d into main Mar 22, 2026
16 checks passed
@streamer45 streamer45 deleted the demo branch March 22, 2026 17:22
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.

1 participant