Story 4.2: Implement full AV replacement with WebRTC stream access
- Update FoundryAdapter to properly detect and expose WebRTC stream access - Modify ScryingPoolStrip to create video elements with WebRTC streams - Add video container to roster-strip.hbs template with conditional rendering - Add CSS to hide Foundry's AV dock (#av and .camera-view) - Add CSS styling for video containers and elements - Fix unused variable in FoundryAdapter.buildWebRTCSurface - Add comprehensive test script for stream access implementation Architecture: Full replacement mode where module hides Foundry's AV dock and creates its own video elements using game.webrtc.client.getMediaStreamForUser() Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
@@ -28,9 +28,10 @@ export function resolveTargetState(currentState) {
|
||||
* @param {object} stateStore
|
||||
* @param {object} controller
|
||||
* @param {object} adapter
|
||||
* @param {boolean} hasStreamAccess - Whether stream access is available for video replacement
|
||||
* @returns {Array<object>}
|
||||
*/
|
||||
export function buildParticipantList(userIds, stateStore, controller, adapter) {
|
||||
export function buildParticipantList(userIds, stateStore, controller, adapter, hasStreamAccess = false) {
|
||||
return userIds.map(userId => {
|
||||
const user = adapter.users.get(userId) ?? { name: userId, avatar: null };
|
||||
const state = stateStore.getState(userId) ?? 'active';
|
||||
@@ -42,6 +43,7 @@ export function buildParticipantList(userIds, stateStore, controller, adapter) {
|
||||
stateLabel: _stateLabel(state),
|
||||
hasPendingOp: controller.hasPendingOp ? controller.hasPendingOp(userId) : false,
|
||||
isCurrentUser: adapter.users.current?.()?.id === userId,
|
||||
hasStreamAccess,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -152,11 +154,15 @@ export class ScryingPoolStrip extends _AppBase {
|
||||
? this._adapter.users.all().map(u => u.id)
|
||||
: [];
|
||||
|
||||
// Check if we have stream access for video replacement (full AV replacement mode)
|
||||
const hasStreamAccess = this._adapter.webrtc?.getMediaStreamForUser !== undefined;
|
||||
|
||||
const participants = buildParticipantList(
|
||||
userIds,
|
||||
this._stateStore,
|
||||
this._controller,
|
||||
this._adapter
|
||||
this._adapter,
|
||||
hasStreamAccess
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -164,6 +170,7 @@ export class ScryingPoolStrip extends _AppBase {
|
||||
isExpanded: this._isExpanded,
|
||||
isEmpty: participants.length === 0,
|
||||
showFirstOpenTip,
|
||||
hasStreamAccess,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -207,6 +214,11 @@ export class ScryingPoolStrip extends _AppBase {
|
||||
if (isFirstOpen) {
|
||||
game.user?.setFlag?.('video-view-manager', 'firstStripOpen', true);
|
||||
}
|
||||
|
||||
// Attach video streams if we have stream access (full AV replacement mode)
|
||||
if (this._adapter.webrtc?.getMediaStreamForUser !== undefined) {
|
||||
this._attachVideoStreams(el);
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
@@ -323,6 +335,80 @@ export class ScryingPoolStrip extends _AppBase {
|
||||
const baseRevision = this._controller.getRevision?.(participantId) ?? 0;
|
||||
this._controller.action('strip', participantId, targetState, opId, baseRevision);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches video stream elements for all participants in the strip.
|
||||
* Called from activateListeners() when stream access is available.
|
||||
* @param {HTMLElement} container - The container element (html from activateListeners)
|
||||
*/
|
||||
_attachVideoStreams(container) {
|
||||
const participantItems = container.querySelectorAll('.sp-strip__participant-item');
|
||||
for (const item of participantItems) {
|
||||
const userId = item.querySelector('[data-user-id]')?.dataset.userId;
|
||||
if (userId) {
|
||||
this._attachVideoStream(userId, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches a video element with the WebRTC stream for a specific user.
|
||||
* Creates a video element with srcObject set to the user's MediaStream.
|
||||
* The video element is appended to the video container within the participant item.
|
||||
* @param {string} userId - The user ID to attach video for
|
||||
* @param {HTMLElement} participantItem - The participant list item element
|
||||
*/
|
||||
_attachVideoStream(userId, participantItem) {
|
||||
const stream = this._adapter.webrtc.getMediaStreamForUser(userId);
|
||||
|
||||
// Check if video container exists
|
||||
const videoContainer = participantItem.querySelector('.sp-participant-video');
|
||||
if (!videoContainer) {
|
||||
console.warn('[ScryingPool] No video container found for user:', userId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove any existing video element
|
||||
const existingVideo = videoContainer.querySelector('video');
|
||||
if (existingVideo) {
|
||||
existingVideo.remove();
|
||||
}
|
||||
|
||||
// If no stream available, don't create video element (will show avatar fallback)
|
||||
if (!stream) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new video element
|
||||
const videoElement = document.createElement('video');
|
||||
videoElement.srcObject = stream;
|
||||
videoElement.autoplay = true;
|
||||
videoElement.playsInline = true;
|
||||
videoElement.muted = userId === this._adapter.users.current?.()?.id;
|
||||
|
||||
// Add CSS class for styling
|
||||
videoElement.className = 'sp-participant-video__element';
|
||||
|
||||
// Set up error handling
|
||||
videoElement.addEventListener('error', () => {
|
||||
console.warn('[ScryingPool] Video element error for user:', userId);
|
||||
});
|
||||
|
||||
videoContainer.appendChild(videoElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes all video streams for participants.
|
||||
* Called when stream state changes (user joins/leaves, mute/unmute).
|
||||
*/
|
||||
_refreshVideoStreams() {
|
||||
if (!this._adapter.webrtc?.getMediaStreamForUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Re-render to ensure DOM is up to date
|
||||
this.render(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user