diff --git a/.changeset/dull-numbers-roll.md b/.changeset/dull-numbers-roll.md new file mode 100644 index 000000000..f09189c79 --- /dev/null +++ b/.changeset/dull-numbers-roll.md @@ -0,0 +1,5 @@ +--- +'@signalwire/js': patch +--- + +Fix to allow the JS SDK to be used in the Shadow DOM. diff --git a/packages/js/src/features/mediaElements/mediaElementsSagas.ts b/packages/js/src/features/mediaElements/mediaElementsSagas.ts index 33ef52160..08eeac762 100644 --- a/packages/js/src/features/mediaElements/mediaElementsSagas.ts +++ b/packages/js/src/features/mediaElements/mediaElementsSagas.ts @@ -9,10 +9,11 @@ import { setMediaElementSinkId } from '@signalwire/webrtc' import { buildVideo, cleanupElement, - makeDisplayChangeFn, makeLayoutChangedHandler, setVideoMediaTrack, waitForVideoReady, + LocalOverlay, + addSDKPrefix, } from '../../utils/videoElement' import { setAudioMediaTrack } from '../../utils/audioElement' import { audioSetSpeakerAction } from '../actions' @@ -30,14 +31,50 @@ export const makeVideoElementSaga = ({ runSaga, }: CustomSagaParams): SagaIterator { try { - const layerMap = new Map() + const layerMap = new Map() const videoEl = buildVideo() + + /** + * We used this `LocalOverlay` interface to interact with the localVideo + * overlay DOM element in here and in the `layoutChangedHandler`. + * The idea is to avoid APIs like `document.getElementById` because it + * won't work if the SDK is used within a Shadow DOM tree. + * Instead of querying the `document`, let's use our `layerMap`. + */ + const localOverlay: LocalOverlay = { + get id() { + return addSDKPrefix(room.memberId) + }, + get domElement() { + return layerMap.get(this.id) + }, + set domElement(element: HTMLDivElement | undefined) { + if (element) { + getLogger().debug('Set localOverlay', element) + layerMap.set(this.id, element) + } else { + getLogger().debug('Remove localOverlay') + layerMap.delete(this.id) + } + }, + hide() { + if (!this.domElement) { + return getLogger().warn('Missing localOverlay to hide') + } + this.domElement.style.display = 'none' + }, + show() { + if (!this.domElement) { + return getLogger().warn('Missing localOverlay to show') + } + this.domElement.style.display = 'block' + }, + } + const layoutChangedHandler = makeLayoutChangedHandler({ rootElement, - layerMap, + localOverlay, }) - const hideOverlay = makeDisplayChangeFn('none') - const showOverlay = makeDisplayChangeFn('block') room.on('layout.changed', (params) => { getLogger().debug('Received layout.changed') @@ -55,7 +92,7 @@ export const makeVideoElementSaga = ({ try { const { member } = params if (member.id === room.memberId && 'video_muted' in member) { - member.video_muted ? hideOverlay(member.id) : showOverlay(member.id) + member.video_muted ? localOverlay.hide() : localOverlay.show() } } catch (error) { getLogger().error('Error handling video_muted', error) diff --git a/packages/js/src/utils/videoElement.ts b/packages/js/src/utils/videoElement.ts index 8536bc979..792007d13 100644 --- a/packages/js/src/utils/videoElement.ts +++ b/packages/js/src/utils/videoElement.ts @@ -4,7 +4,7 @@ import { InternalVideoLayout, } from '@signalwire/core' -const _addSDKPrefix = (input: string) => { +const addSDKPrefix = (input: string) => { return `sw-sdk-${input}` } @@ -57,6 +57,18 @@ const _buildLayer = ({ location }: { location: InternalVideoLayoutLayer }) => { return layer } +export interface LocalOverlay { + readonly id: string + domElement: HTMLDivElement | undefined + hide(): void + show(): void +} + +interface MakeLayoutChangedHandlerParams { + localOverlay: LocalOverlay + rootElement: HTMLElement +} + interface LayoutChangedHandlerParams { layout: InternalVideoLayout myMemberId: string @@ -64,26 +76,19 @@ interface LayoutChangedHandlerParams { } const makeLayoutChangedHandler = - ({ - layerMap, - rootElement, - }: { - layerMap: Map - rootElement: HTMLElement - }) => + ({ localOverlay, rootElement }: MakeLayoutChangedHandlerParams) => async ({ layout, myMemberId, localStream }: LayoutChangedHandlerParams) => { getLogger().debug('Process layout.changed') try { const { layers = [] } = layout const location = layers.find(({ member_id }) => member_id === myMemberId) - const myLayerKey = _addSDKPrefix(myMemberId) - let myLayer = layerMap.get(myLayerKey) + let myLayer = localOverlay.domElement if (!location) { getLogger().debug('Location not found') if (myLayer) { getLogger().debug('Current layer not visible') - myLayer.style.display = 'none' + localOverlay.hide() } return @@ -92,7 +97,7 @@ const makeLayoutChangedHandler = if (!myLayer) { getLogger().debug('Build myLayer') myLayer = _buildLayer({ location }) - myLayer.id = myLayerKey + myLayer.id = localOverlay.id const localVideo = buildVideo() localVideo.srcObject = localStream @@ -102,10 +107,10 @@ const makeLayoutChangedHandler = myLayer.appendChild(localVideo) const mcuLayers = rootElement.querySelector('.mcuLayers') - const exists = document.getElementById(myLayerKey) + const exists = mcuLayers?.querySelector(`#${myLayer.id}`) if (mcuLayers && !exists) { mcuLayers.appendChild(myLayer) - layerMap.set(myLayerKey, myLayer) + localOverlay.domElement = myLayer } return @@ -128,15 +133,6 @@ const makeLayoutChangedHandler = } } -const makeDisplayChangeFn = (display: 'block' | 'none') => { - return (domId: string) => { - const el = document.getElementById(_addSDKPrefix(domId)) - if (el) { - el.style.display = display - } - } -} - const cleanupElement = (rootElement: HTMLElement) => { while (rootElement.firstChild) { rootElement.removeChild(rootElement.firstChild) @@ -162,7 +158,7 @@ export { buildVideo, cleanupElement, makeLayoutChangedHandler, - makeDisplayChangeFn, setVideoMediaTrack, waitForVideoReady, + addSDKPrefix, }