This commit is contained in:
@@ -177,7 +177,8 @@ export class ScryingPoolStrip extends _AppBase {
|
||||
/** @inheritdoc */
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
const el = html instanceof HTMLElement ? html : html[0];
|
||||
const el = html instanceof HTMLElement ? html : html?.[0];
|
||||
if (!el) return;
|
||||
|
||||
el.querySelectorAll('[data-action="open-popover"]').forEach(btn => {
|
||||
const userId = btn.dataset.userId;
|
||||
@@ -227,6 +228,10 @@ export class ScryingPoolStrip extends _AppBase {
|
||||
this._activePopover.close('superseded');
|
||||
this._activePopover = null;
|
||||
}
|
||||
|
||||
// Clean up video elements and streams when closing
|
||||
this._cleanupVideoStreams();
|
||||
|
||||
if (typeof game !== 'undefined') {
|
||||
game.user?.setFlag?.('video-view-manager', 'stripState', {
|
||||
left: this.position?.left,
|
||||
@@ -238,6 +243,24 @@ export class ScryingPoolStrip extends _AppBase {
|
||||
return super.close(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up all video elements and their associated streams.
|
||||
* Called when the strip is closed to prevent memory leaks.
|
||||
*/
|
||||
_cleanupVideoStreams() {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
const videoElements = document.querySelectorAll?.('.sp-participant-video__element') ?? [];
|
||||
videoElements.forEach(videoEl => {
|
||||
// Stop all tracks in the stream
|
||||
if (videoEl.srcObject instanceof MediaStream) {
|
||||
videoEl.srcObject.getTracks().forEach(track => track.stop());
|
||||
}
|
||||
videoEl.srcObject = null;
|
||||
videoEl.remove();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the expanded/collapsed state of the strip.
|
||||
*/
|
||||
@@ -342,9 +365,12 @@ export class ScryingPoolStrip extends _AppBase {
|
||||
* @param {HTMLElement} container - The container element (html from activateListeners)
|
||||
*/
|
||||
_attachVideoStreams(container) {
|
||||
const participantItems = container.querySelectorAll('.sp-strip__participant-item');
|
||||
if (!container) return;
|
||||
|
||||
const participantItems = container.querySelectorAll?.('.sp-strip__participant-item') ?? [];
|
||||
for (const item of participantItems) {
|
||||
const userId = item.querySelector('[data-user-id]')?.dataset.userId;
|
||||
if (!item) continue;
|
||||
const userId = item.querySelector?.('[data-user-id]')?.dataset?.userId;
|
||||
if (userId) {
|
||||
this._attachVideoStream(userId, item);
|
||||
}
|
||||
@@ -359,17 +385,28 @@ export class ScryingPoolStrip extends _AppBase {
|
||||
* @param {HTMLElement} participantItem - The participant list item element
|
||||
*/
|
||||
_attachVideoStream(userId, participantItem) {
|
||||
// Guard: check webrtc is available
|
||||
if (!this._adapter?.webrtc?.getMediaStreamForUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Guard: check participantItem is valid
|
||||
if (!participantItem) {
|
||||
console.warn('[ScryingPool] _attachVideoStream: participantItem is null/undefined');
|
||||
return;
|
||||
}
|
||||
|
||||
const stream = this._adapter.webrtc.getMediaStreamForUser(userId);
|
||||
|
||||
// Check if video container exists
|
||||
const videoContainer = participantItem.querySelector('.sp-participant-video');
|
||||
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');
|
||||
const existingVideo = videoContainer.querySelector?.('video');
|
||||
if (existingVideo) {
|
||||
existingVideo.remove();
|
||||
}
|
||||
@@ -379,12 +416,27 @@ export class ScryingPoolStrip extends _AppBase {
|
||||
return;
|
||||
}
|
||||
|
||||
// Guard: ensure we're in a browser environment
|
||||
if (typeof document === 'undefined' || !document.createElement) {
|
||||
console.warn('[ScryingPool] _attachVideoStream: document or createElement not available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate stream is a MediaStream
|
||||
if (!(stream instanceof MediaStream)) {
|
||||
console.warn('[ScryingPool] _attachVideoStream: stream is not a MediaStream:', typeof 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;
|
||||
|
||||
// Guard: ensure current user check is safe
|
||||
const currentUserId = this._adapter?.users?.current?.()?.id;
|
||||
videoElement.muted = userId === currentUserId;
|
||||
|
||||
// Add CSS class for styling
|
||||
videoElement.className = 'sp-participant-video__element';
|
||||
@@ -406,8 +458,14 @@ export class ScryingPoolStrip extends _AppBase {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove existing video elements before re-rendering to prevent duplicates
|
||||
const existingVideos = this.element?.querySelectorAll('.sp-participant-video__element') ?? [];
|
||||
existingVideos.forEach(v => v.remove());
|
||||
|
||||
// Re-render to ensure DOM is up to date
|
||||
this.render(false);
|
||||
if (typeof this.render === 'function') {
|
||||
this.render(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user