@webitel/ui-sdk 26.4.78 → 26.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/{contacts-eFuSbv3m.js → contacts-DuiejYmg.js} +1551 -802
- package/dist/{index-CaY0ocAF.js → index-D_Bvqjjy.js} +1 -1
- package/dist/{index-TiiXs7ZO.js → index-ofJZWfBt.js} +1 -1
- package/dist/{install-Bd5HfFTV.js → install-rysuPEjF.js} +38 -38
- package/dist/{isObject-BV09j4E1.js → isObject-C5JdCKnH.js} +1 -1
- package/dist/ui-sdk.css +1 -1
- package/dist/ui-sdk.js +1 -1
- package/dist/ui-sdk.umd.cjs +187 -188
- package/dist/{useVidstackSrc-BGxC2kR5.js → useVidstackSrc-C6hUWIuE.js} +1 -1
- package/dist/{vidstack-Bq6c3Bam-jeTFOXk6.js → vidstack-Bq6c3Bam-D6M5GPTI.js} +3 -3
- package/dist/{vidstack-D2pY00kU-I-LJsKk8.js → vidstack-D2pY00kU-CRo5Kvyt.js} +3 -3
- package/dist/{vidstack-DDXt6fpN-W_Ix9vbH.js → vidstack-DDXt6fpN-QgRuASyL.js} +2 -2
- package/dist/{vidstack-D_-9AA6_-27F2Mv_5.js → vidstack-D_-9AA6_-BQB6UUD-.js} +2 -2
- package/dist/{vidstack-DqAw8m9J-BxfLqlFs.js → vidstack-DqAw8m9J-0B90hhJB.js} +1 -1
- package/dist/{vidstack-audio-CvjDPPuQ.js → vidstack-audio-BML-tyCz.js} +2 -2
- package/dist/{vidstack-dash-rV4ryZh2.js → vidstack-dash-BwVTHD5j.js} +4 -4
- package/dist/{vidstack-google-cast-Cb8t34m4.js → vidstack-google-cast-CvoUE_Qj.js} +4 -4
- package/dist/{vidstack-hls-C2zZ7COz.js → vidstack-hls-BgbWYqKl.js} +4 -4
- package/dist/{vidstack-video-DzItbHtm.js → vidstack-video-D6tVAa-p.js} +3 -3
- package/dist/{vidstack-vimeo-BpKsHwp4.js → vidstack-vimeo-CMino7JB.js} +4 -4
- package/dist/{vidstack-youtube--6zpNJz6.js → vidstack-youtube-DLE3pyNu.js} +3 -3
- package/dist/{wt-action-bar-Df57KUsc.js → wt-action-bar-BwAQUnPp.js} +1 -1
- package/dist/{wt-button-select-ymdOC5NH.js → wt-button-select-D8-D18E4.js} +1 -1
- package/dist/{wt-call-media-action-D4CP3oZl.js → wt-call-media-action-B-AEoxjt.js} +1 -1
- package/dist/{wt-chat-emoji-CUjHxdzV.js → wt-chat-emoji-D_4QMhH7.js} +2 -2
- package/dist/{wt-confirm-dialog-CNwpPdgE.js → wt-confirm-dialog-BQVImEga.js} +1 -1
- package/dist/{wt-context-menu-CToKHamf.js → wt-context-menu-CfvdB5Xl.js} +1 -1
- package/dist/{wt-copy-action-Dd0tVB7g.js → wt-copy-action-CIa3ImHz.js} +1 -1
- package/dist/{wt-datepicker-B7sKwJlh.js → wt-datepicker-CEoYduIo.js} +1 -1
- package/dist/{wt-display-chip-items-IGBh7VMV.js → wt-display-chip-items-CD6E8917.js} +1 -1
- package/dist/{wt-dual-panel-CqN6YB0F.js → wt-dual-panel-Cm2oe8sF.js} +1 -1
- package/dist/{wt-dummy-OrcannPM.js → wt-dummy-DHdJccrG.js} +1 -1
- package/dist/{wt-error-page-BMpoQdQl.js → wt-error-page-Rt-F9oVv.js} +1 -1
- package/dist/{wt-expansion-card-DEDM3Znr.js → wt-expansion-card-v7GFQBoJ.js} +1 -1
- package/dist/{wt-expansion-panel-YCSPf-eq.js → wt-expansion-panel-DSsY-KFR.js} +1 -1
- package/dist/{wt-filters-panel-wrapper-Ck2bF7qg.js → wt-filters-panel-wrapper-Cr9dYYnT.js} +1 -1
- package/dist/{wt-galleria-CNDkujRu.js → wt-galleria-C2aburQk.js} +1 -1
- package/dist/{wt-inline-add-panel-BSEdS7eW.js → wt-inline-add-panel-BaDP2TC1.js} +1 -1
- package/dist/{wt-navigation-menu-igJBYaxO.js → wt-navigation-menu-B_R8-_Bk.js} +1 -1
- package/dist/{wt-notifications-bar-C0M5LC4G.js → wt-notifications-bar-CfoCOojR.js} +2 -2
- package/dist/{wt-pagination-BXDqQ9YV.js → wt-pagination-DW7KL87c.js} +1 -1
- package/dist/{wt-player-A13mN2qk.js → wt-player-Be2YAJcQ.js} +2 -2
- package/dist/{wt-screen-recordings-action-BQPS--RV.js → wt-screen-recordings-action-BJBx9KXM.js} +1 -1
- package/dist/{wt-search-bar-Cvt_urUF.js → wt-search-bar-BT5d_LDB.js} +1 -1
- package/dist/{wt-selection-popup-CcAfc6T6.js → wt-selection-popup-DK6oLjfk.js} +1 -1
- package/dist/{wt-send-message-popup-pN9zzJpT.js → wt-send-message-popup-C8oG1Kxc.js} +3 -3
- package/dist/{wt-start-page-DaotE9Vi.js → wt-start-page-BcV-IdG_.js} +1 -1
- package/dist/{wt-status-select-Dhw-c6FB.js → wt-status-select-BcrdWJ9m.js} +1 -1
- package/dist/{wt-stepper-DyM6hf_l.js → wt-stepper-B_ny-IQN.js} +1 -1
- package/dist/{wt-table-Do6tizZl.js → wt-table-BX5kDXje.js} +1 -1
- package/dist/{wt-table-actions-Stm_KC06.js → wt-table-actions-Cvk2_NUG.js} +1 -1
- package/dist/{wt-table-column-select-DaGMSlL1.js → wt-table-column-select-CPacgsbN.js} +2 -2
- package/dist/{wt-tabs-CTv4GgX8.js → wt-tabs-wcnQfMfj.js} +1 -1
- package/dist/{wt-tags-input-DJhQrEdL.js → wt-tags-input-CPO80nnI.js} +2 -2
- package/dist/{wt-timepicker-98DhU-ME.js → wt-timepicker-DbdcOXYa.js} +1 -1
- package/dist/{wt-tree-CfkLw1SI.js → wt-tree-CLs3S4Av.js} +2 -2
- package/dist/{wt-tree-table-DDyxMFbz.js → wt-tree-table-DtNIE08B.js} +1 -1
- package/dist/{wt-type-extension-value-input-B2CwLd8p.js → wt-type-extension-value-input-CjJ8_xRU.js} +3 -3
- package/dist/{wt-vidstack-player-BduaN7w2.js → wt-vidstack-player-DmD7s58v.js} +900 -882
- package/package.json +10 -2
- package/src/components/wt-vidstack-player/wt-vidstack-player.vue +47 -3
- package/src/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/bridgeCustomElements.ts +32 -0
- package/src/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/copyStyles.ts +18 -0
- package/src/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/domTraversal.ts +16 -0
- package/src/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/index.ts +1 -0
- package/src/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/mediaCollection.ts +13 -0
- package/src/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/mediaPlayback.ts +22 -0
- package/src/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/mediaSnapshot.ts +43 -0
- package/src/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/playbackRetry.ts +81 -0
- package/src/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/useDocumentPiP.ts +146 -0
- package/src/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/usePiPResizeObserver.ts +30 -0
- package/src/modules/CallSession/modules/VideoCall/composables/useReceiverLiveStream.ts +64 -0
- package/src/modules/CallSession/modules/VideoCall/types/types.ts +17 -0
- package/src/modules/CallSession/modules/VideoCall/video-call.vue +170 -18
- package/types/components/wt-vidstack-player/wt-vidstack-player.vue.d.ts +3 -1
- package/types/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/bridgeCustomElements.d.ts +1 -0
- package/types/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/copyStyles.d.ts +1 -0
- package/types/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/domTraversal.d.ts +1 -0
- package/types/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/index.d.ts +1 -0
- package/types/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/mediaCollection.d.ts +2 -0
- package/types/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/mediaPlayback.d.ts +3 -0
- package/types/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/mediaSnapshot.d.ts +16 -0
- package/types/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/playbackRetry.d.ts +24 -0
- package/types/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/useDocumentPiP.d.ts +8 -0
- package/types/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/usePiPResizeObserver.d.ts +6 -0
- package/types/modules/CallSession/modules/VideoCall/composables/useReceiverLiveStream.d.ts +49 -0
- package/types/modules/CallSession/modules/VideoCall/types/types.d.ts +14 -0
- package/types/modules/CallSession/modules/VideoCall/video-call.vue.d.ts +6 -4
package/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webitel/ui-sdk",
|
|
3
|
-
"version": "26.
|
|
3
|
+
"version": "26.6.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"scripts": {
|
|
6
|
-
"make-all": "npm version patch --git-tag-version false && npm run build && (npm run build:types || true) && (npm run biome:format:all || true) && npm run publish-lib",
|
|
7
6
|
"dev": "npm run docs:dev",
|
|
8
7
|
"build": "vite build",
|
|
9
8
|
"test:unit": "vitest run",
|
|
@@ -173,6 +172,15 @@
|
|
|
173
172
|
"npm": "11",
|
|
174
173
|
"node": "v24"
|
|
175
174
|
},
|
|
175
|
+
"repository": {
|
|
176
|
+
"type": "git",
|
|
177
|
+
"url": "https://github.com/webitel/webitel-ui-sdk"
|
|
178
|
+
},
|
|
179
|
+
"keywords": [
|
|
180
|
+
"webitel",
|
|
181
|
+
"ui-sdk"
|
|
182
|
+
],
|
|
183
|
+
"author": "webitel",
|
|
176
184
|
"exports": {
|
|
177
185
|
".": {
|
|
178
186
|
"types": "./types/install.d.ts",
|
|
@@ -96,8 +96,12 @@ const emit = defineEmits<{
|
|
|
96
96
|
];
|
|
97
97
|
}>();
|
|
98
98
|
|
|
99
|
-
// const player = useTemplateRef<MediaPlayerElement>('player');
|
|
100
99
|
const rootEl = useTemplateRef<HTMLElement>('root');
|
|
100
|
+
|
|
101
|
+
defineExpose({
|
|
102
|
+
rootEl,
|
|
103
|
+
});
|
|
104
|
+
|
|
101
105
|
const size = ref(props.size || ComponentSize.SM);
|
|
102
106
|
const fullscreen = ref(false);
|
|
103
107
|
|
|
@@ -138,10 +142,50 @@ const rootClasses = computed(() => [
|
|
|
138
142
|
props.hideBackground && 'wt-vidstack-player--hide-background',
|
|
139
143
|
]);
|
|
140
144
|
|
|
145
|
+
/**
|
|
146
|
+
* @author @Palonnyi Oleksandr
|
|
147
|
+
* Document Picture-in-Picture (PiP) autoplay support.
|
|
148
|
+
* [WTEL-9414](https://webitel.atlassian.net/browse/WTEL-9414)
|
|
149
|
+
*
|
|
150
|
+
* When the player moves into a PiP window, Vidstack re-initialises inside a
|
|
151
|
+
* new Document context. Its internal request manager can then block autoplay
|
|
152
|
+
* even for muted media. To work around this we locate the native `<video>`
|
|
153
|
+
* and call `play()` on it directly — the browser always permits muted autoplay.
|
|
154
|
+
*
|
|
155
|
+
* `findNativeVideoElement` is needed because `querySelector('video')` does not cross
|
|
156
|
+
* shadow-DOM boundaries; Vidstack renders `<video>` inside `<media-provider>`'s
|
|
157
|
+
* shadow root, so we walk the tree manually and pierce each shadow root we meet.
|
|
158
|
+
* Technique: https://developer.mozilla.org/en-US/docs/Web/API/Element/shadowRoot
|
|
159
|
+
*/
|
|
160
|
+
const findNativeVideoElement = (
|
|
161
|
+
root: Element | ShadowRoot,
|
|
162
|
+
): HTMLVideoElement | null => {
|
|
163
|
+
if (root instanceof HTMLVideoElement) return root;
|
|
164
|
+
|
|
165
|
+
const shadowChildren =
|
|
166
|
+
root instanceof Element && root.shadowRoot
|
|
167
|
+
? Array.from(root.shadowRoot.children)
|
|
168
|
+
: [];
|
|
169
|
+
const lightChildren = Array.from(root.children ?? []);
|
|
170
|
+
|
|
171
|
+
for (const child of [
|
|
172
|
+
...shadowChildren,
|
|
173
|
+
...lightChildren,
|
|
174
|
+
]) {
|
|
175
|
+
const found = findNativeVideoElement(child);
|
|
176
|
+
if (found) return found;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return null;
|
|
180
|
+
};
|
|
181
|
+
|
|
141
182
|
const onCanPlay = (ev: Event) => {
|
|
142
183
|
if (!props.autoplay) return;
|
|
143
|
-
|
|
144
|
-
(ev.target as HTMLMediaElement)?.
|
|
184
|
+
const video = findNativeVideoElement(ev.target as Element);
|
|
185
|
+
const playPromise = (video ?? (ev.target as HTMLMediaElement)).play?.();
|
|
186
|
+
if (playPromise && typeof playPromise.catch === 'function') {
|
|
187
|
+
playPromise.catch(() => {});
|
|
188
|
+
}
|
|
145
189
|
};
|
|
146
190
|
</script>
|
|
147
191
|
|
package/src/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/bridgeCustomElements.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @author @PalonnyiOleksandr
|
|
3
|
+
*
|
|
4
|
+
* [WTEL-9414](https://webitel.atlassian.net/browse/WTEL-9414)
|
|
5
|
+
*
|
|
6
|
+
* Registers custom elements from the main window into the Document PiP window.
|
|
7
|
+
* Walks the source DOM tree, collects all hyphenated tag names, and defines
|
|
8
|
+
* any missing constructors in the target window's custom element registry.
|
|
9
|
+
*/
|
|
10
|
+
import { domTraversal } from './domTraversal';
|
|
11
|
+
|
|
12
|
+
export const bridgeCustomElements = (root: Element, targetWindow: Window) => {
|
|
13
|
+
const tags = new Set<string>();
|
|
14
|
+
domTraversal(root, (el) => {
|
|
15
|
+
const tag = el.tagName.toLowerCase();
|
|
16
|
+
if (tag.includes('-')) tags.add(tag);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
for (const tag of tags) {
|
|
20
|
+
const customElementConstructor = window.customElements.get(tag);
|
|
21
|
+
if (!customElementConstructor || targetWindow.customElements.get(tag))
|
|
22
|
+
continue;
|
|
23
|
+
try {
|
|
24
|
+
targetWindow.customElements.define(
|
|
25
|
+
tag,
|
|
26
|
+
customElementConstructor as CustomElementConstructor,
|
|
27
|
+
);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.warn('[document PiP] custom element bridge failed', tag, err);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const copyStyles = (targetWindow: Window) => {
|
|
2
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
3
|
+
try {
|
|
4
|
+
const style = targetWindow.document.createElement('style');
|
|
5
|
+
style.textContent = Array.from(sheet.cssRules)
|
|
6
|
+
.map((r) => r.cssText)
|
|
7
|
+
.join('');
|
|
8
|
+
targetWindow.document.head.appendChild(style);
|
|
9
|
+
} catch {
|
|
10
|
+
if (sheet.href) {
|
|
11
|
+
const link = targetWindow.document.createElement('link');
|
|
12
|
+
link.rel = 'stylesheet';
|
|
13
|
+
link.href = sheet.href;
|
|
14
|
+
targetWindow.document.head.appendChild(link);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const domTraversal = (
|
|
2
|
+
root: Element | ShadowRoot,
|
|
3
|
+
visit: (el: Element) => void,
|
|
4
|
+
) => {
|
|
5
|
+
if (root instanceof Element) {
|
|
6
|
+
visit(root);
|
|
7
|
+
if (root.shadowRoot) {
|
|
8
|
+
for (const child of root.shadowRoot.children) {
|
|
9
|
+
domTraversal(child, visit);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
for (const child of root.children) {
|
|
14
|
+
domTraversal(child, visit);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useDocumentPiP } from './useDocumentPiP';
|
package/src/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/mediaCollection.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { domTraversal } from './domTraversal';
|
|
2
|
+
|
|
3
|
+
export const safePlay = (el: HTMLMediaElement) => {
|
|
4
|
+
void el.play().catch(() => {});
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const collectMedia = (root: Element): HTMLMediaElement[] => {
|
|
8
|
+
const list: HTMLMediaElement[] = [];
|
|
9
|
+
domTraversal(root, (el) => {
|
|
10
|
+
if (el instanceof HTMLMediaElement) list.push(el);
|
|
11
|
+
});
|
|
12
|
+
return list;
|
|
13
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { MediaRemoteControl } from 'vidstack';
|
|
2
|
+
|
|
3
|
+
import type { MediaSnapshot } from '../../types/types';
|
|
4
|
+
import { domTraversal } from './domTraversal';
|
|
5
|
+
import { collectMedia, safePlay } from './mediaCollection';
|
|
6
|
+
import { restoreStreams } from './mediaSnapshot';
|
|
7
|
+
|
|
8
|
+
export const requestVidstackPlayback = (root: Element) => {
|
|
9
|
+
domTraversal(root, (el) => {
|
|
10
|
+
if (el.tagName.toLowerCase() !== 'media-player') return;
|
|
11
|
+
const remote = new MediaRemoteControl();
|
|
12
|
+
remote.setTarget(el);
|
|
13
|
+
remote.mute();
|
|
14
|
+
remote.play();
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const resumePlayback = (root: Element, snapshot: MediaSnapshot[]) => {
|
|
19
|
+
restoreStreams(snapshot);
|
|
20
|
+
collectMedia(root).forEach(safePlay);
|
|
21
|
+
requestVidstackPlayback(root);
|
|
22
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @author @PalonnyiOleksandr
|
|
3
|
+
*
|
|
4
|
+
* [WTEL-9414](https://webitel.atlassian.net/browse/WTEL-9414)
|
|
5
|
+
*
|
|
6
|
+
* Snapshot utilities for media elements inside Document PiP window.
|
|
7
|
+
* `snapshotMedia` captures current muted state and srcObject of all media
|
|
8
|
+
* elements, then forces mute to prevent autoplay policy violations.
|
|
9
|
+
* `restoreMedia` reverts muted state and re-attaches lost streams.
|
|
10
|
+
* `restoreStreams` re-attaches srcObject without touching muted state,
|
|
11
|
+
* used during playback retry polling.
|
|
12
|
+
*/
|
|
13
|
+
import type { MediaSnapshot } from '../../types/types';
|
|
14
|
+
|
|
15
|
+
import { collectMedia } from './mediaCollection';
|
|
16
|
+
|
|
17
|
+
export const snapshotMedia = (root: Element, snapshot: MediaSnapshot[]) => {
|
|
18
|
+
const items = collectMedia(root).map((el) => ({
|
|
19
|
+
el,
|
|
20
|
+
muted: el.muted,
|
|
21
|
+
srcObject: el.srcObject instanceof MediaStream ? el.srcObject : null,
|
|
22
|
+
}));
|
|
23
|
+
snapshot.splice(0, snapshot.length, ...items);
|
|
24
|
+
for (const { el } of snapshot) {
|
|
25
|
+
el.muted = true;
|
|
26
|
+
el.setAttribute('muted', '');
|
|
27
|
+
el.defaultMuted = true;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const restoreMedia = (snapshot: MediaSnapshot[]) => {
|
|
32
|
+
for (const { el, muted, srcObject } of snapshot) {
|
|
33
|
+
el.muted = muted;
|
|
34
|
+
if (!el.srcObject && srcObject) el.srcObject = srcObject;
|
|
35
|
+
}
|
|
36
|
+
snapshot.splice(0);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const restoreStreams = (snapshot: MediaSnapshot[]) => {
|
|
40
|
+
for (const { el, srcObject } of snapshot) {
|
|
41
|
+
if (!el.srcObject && srcObject) el.srcObject = srcObject;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @author @PalonnyiOleksandr
|
|
3
|
+
*
|
|
4
|
+
* [WTEL-9414](https://webitel.atlassian.net/browse/WTEL-9414)
|
|
5
|
+
*
|
|
6
|
+
* Polling retry mechanism for media playback inside Document PiP window.
|
|
7
|
+
* Restores streams from snapshot, mutes and plays all media elements until
|
|
8
|
+
* all are ready or the timeout expires. Triggers vidstack playback after an
|
|
9
|
+
* initial delay and re-kicks it periodically when any element stays stalled,
|
|
10
|
+
* throttled to once per 1500 ms to avoid redundant calls.
|
|
11
|
+
*
|
|
12
|
+
* Background: browsers block autoplay until the user interacts with the page,
|
|
13
|
+
* which caused the video to freeze after opening PiP. Since the video must
|
|
14
|
+
* play immediately without any extra clicks, this retry loop works around that
|
|
15
|
+
* by muting elements (which bypasses the autoplay restriction) and
|
|
16
|
+
* re-attempting playback until all media is running.
|
|
17
|
+
*
|
|
18
|
+
* link to explanation - https://webitel.atlassian.net/browse/WTEL-9414?focusedCommentId=754533
|
|
19
|
+
*/
|
|
20
|
+
import type { MediaSnapshot } from '../../types/types';
|
|
21
|
+
|
|
22
|
+
import { collectMedia, safePlay } from './mediaCollection';
|
|
23
|
+
import { requestVidstackPlayback } from './mediaPlayback';
|
|
24
|
+
import { restoreStreams } from './mediaSnapshot';
|
|
25
|
+
|
|
26
|
+
export const createPlaybackRetry = (snapshot: MediaSnapshot[]) => {
|
|
27
|
+
let retryTimer: number | null = null;
|
|
28
|
+
let vidstackDelayTimer: number | null = null;
|
|
29
|
+
|
|
30
|
+
const stopPlaybackRetry = () => {
|
|
31
|
+
if (retryTimer !== null) {
|
|
32
|
+
clearInterval(retryTimer);
|
|
33
|
+
retryTimer = null;
|
|
34
|
+
}
|
|
35
|
+
if (vidstackDelayTimer !== null) {
|
|
36
|
+
clearTimeout(vidstackDelayTimer);
|
|
37
|
+
vidstackDelayTimer = null;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const retryPlayback = (root: Element, durationMs = 10000) => {
|
|
42
|
+
stopPlaybackRetry();
|
|
43
|
+
const started = performance.now();
|
|
44
|
+
let lastVidstackKick = 0;
|
|
45
|
+
|
|
46
|
+
vidstackDelayTimer = window.setTimeout(() => {
|
|
47
|
+
vidstackDelayTimer = null;
|
|
48
|
+
requestVidstackPlayback(root);
|
|
49
|
+
lastVidstackKick = performance.now();
|
|
50
|
+
}, 800);
|
|
51
|
+
|
|
52
|
+
retryTimer = window.setInterval(() => {
|
|
53
|
+
restoreStreams(snapshot);
|
|
54
|
+
|
|
55
|
+
const media = collectMedia(root);
|
|
56
|
+
if (!media.length) return;
|
|
57
|
+
|
|
58
|
+
for (const el of media) {
|
|
59
|
+
el.muted = true;
|
|
60
|
+
if (el.paused || el.readyState === 0) safePlay(el);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const hasStalled = media.some((el) => el.paused || el.readyState === 0);
|
|
64
|
+
const now = performance.now();
|
|
65
|
+
if (hasStalled && now - lastVidstackKick > 1500) {
|
|
66
|
+
requestVidstackPlayback(root);
|
|
67
|
+
lastVidstackKick = now;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const allReady = media.every((el) => !el.paused && el.readyState > 0);
|
|
71
|
+
if (allReady || now - started > durationMs) {
|
|
72
|
+
stopPlaybackRetry();
|
|
73
|
+
}
|
|
74
|
+
}, 200);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
retryPlayback,
|
|
79
|
+
stopPlaybackRetry,
|
|
80
|
+
};
|
|
81
|
+
};
|
package/src/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/useDocumentPiP.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { computed, onUnmounted, ref } from 'vue';
|
|
2
|
+
|
|
3
|
+
import type { DocumentPiPWindow, MediaSnapshot } from '../../types/types';
|
|
4
|
+
|
|
5
|
+
import { bridgeCustomElements } from './bridgeCustomElements';
|
|
6
|
+
import { copyStyles } from './copyStyles';
|
|
7
|
+
import { collectMedia, safePlay } from './mediaCollection';
|
|
8
|
+
import { resumePlayback } from './mediaPlayback';
|
|
9
|
+
import { restoreMedia, restoreStreams, snapshotMedia } from './mediaSnapshot';
|
|
10
|
+
import { createPlaybackRetry } from './playbackRetry';
|
|
11
|
+
import { usePiPResizeObserver } from './usePiPResizeObserver';
|
|
12
|
+
|
|
13
|
+
export function useDocumentPiP(
|
|
14
|
+
getElement: () => HTMLElement | null | undefined,
|
|
15
|
+
) {
|
|
16
|
+
const isPiP = ref(false);
|
|
17
|
+
const isSupported = computed(() => 'documentPictureInPicture' in window);
|
|
18
|
+
|
|
19
|
+
let pipWindow: Window | null = null;
|
|
20
|
+
let movedEl: HTMLElement | null = null;
|
|
21
|
+
let originalParent: Node | null = null;
|
|
22
|
+
let originalNextSibling: Node | null = null;
|
|
23
|
+
|
|
24
|
+
const mediaSnapshot: MediaSnapshot[] = [];
|
|
25
|
+
const { retryPlayback, stopPlaybackRetry } =
|
|
26
|
+
createPlaybackRetry(mediaSnapshot);
|
|
27
|
+
|
|
28
|
+
const { startPiPResizeObserver, stopPiPResizeObserver, onPiPResize } =
|
|
29
|
+
usePiPResizeObserver();
|
|
30
|
+
|
|
31
|
+
const armFirstGestureResume = (w?: Window) => {
|
|
32
|
+
const target = w ?? pipWindow;
|
|
33
|
+
if (!target || target.closed) return;
|
|
34
|
+
|
|
35
|
+
const kick = () => {
|
|
36
|
+
if (movedEl) resumePlayback(movedEl, mediaSnapshot);
|
|
37
|
+
target.document.removeEventListener('pointerdown', kick, true);
|
|
38
|
+
target.document.removeEventListener('keydown', kick, true);
|
|
39
|
+
};
|
|
40
|
+
target.document.addEventListener('pointerdown', kick, true);
|
|
41
|
+
target.document.addEventListener('keydown', kick, true);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const restoreElement = () => {
|
|
45
|
+
if (!movedEl || !originalParent) {
|
|
46
|
+
mediaSnapshot.splice(0);
|
|
47
|
+
movedEl = null;
|
|
48
|
+
originalParent = null;
|
|
49
|
+
originalNextSibling = null;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (
|
|
54
|
+
originalNextSibling &&
|
|
55
|
+
originalNextSibling.parentNode === originalParent
|
|
56
|
+
) {
|
|
57
|
+
originalParent.insertBefore(movedEl, originalNextSibling);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
restoreMedia(mediaSnapshot);
|
|
61
|
+
resumePlayback(movedEl, mediaSnapshot);
|
|
62
|
+
|
|
63
|
+
movedEl = null;
|
|
64
|
+
originalParent = null;
|
|
65
|
+
originalNextSibling = null;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const onPiPWindowClose = () => {
|
|
69
|
+
stopPlaybackRetry();
|
|
70
|
+
stopPiPResizeObserver();
|
|
71
|
+
restoreElement();
|
|
72
|
+
pipWindow = null;
|
|
73
|
+
isPiP.value = false;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const enterPiP = async (width = 480, height = 320) => {
|
|
77
|
+
if (!isSupported.value || isPiP.value) return;
|
|
78
|
+
|
|
79
|
+
const el = getElement();
|
|
80
|
+
if (!el) return;
|
|
81
|
+
|
|
82
|
+
const api = (window as DocumentPiPWindow).documentPictureInPicture;
|
|
83
|
+
let win: Window;
|
|
84
|
+
try {
|
|
85
|
+
win = await api.requestWindow({
|
|
86
|
+
width,
|
|
87
|
+
height,
|
|
88
|
+
});
|
|
89
|
+
} catch {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
pipWindow = win;
|
|
94
|
+
|
|
95
|
+
copyStyles(win);
|
|
96
|
+
win.document.body.style.cssText =
|
|
97
|
+
'margin:0;overflow:hidden;width:100%;height:100%;';
|
|
98
|
+
bridgeCustomElements(el, win);
|
|
99
|
+
|
|
100
|
+
originalParent = el.parentNode;
|
|
101
|
+
originalNextSibling = el.nextSibling;
|
|
102
|
+
movedEl = el;
|
|
103
|
+
|
|
104
|
+
snapshotMedia(el, mediaSnapshot);
|
|
105
|
+
win.document.body.appendChild(el);
|
|
106
|
+
|
|
107
|
+
restoreStreams(mediaSnapshot);
|
|
108
|
+
collectMedia(el).forEach(safePlay);
|
|
109
|
+
|
|
110
|
+
retryPlayback(el);
|
|
111
|
+
armFirstGestureResume(win);
|
|
112
|
+
|
|
113
|
+
isPiP.value = true;
|
|
114
|
+
win.addEventListener('pagehide', onPiPWindowClose);
|
|
115
|
+
startPiPResizeObserver(el);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const exitPiP = () => {
|
|
119
|
+
if (!isPiP.value) return;
|
|
120
|
+
|
|
121
|
+
stopPlaybackRetry();
|
|
122
|
+
stopPiPResizeObserver();
|
|
123
|
+
restoreElement();
|
|
124
|
+
|
|
125
|
+
if (pipWindow && !pipWindow.closed) {
|
|
126
|
+
pipWindow.removeEventListener('pagehide', onPiPWindowClose);
|
|
127
|
+
pipWindow.close();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
pipWindow = null;
|
|
131
|
+
isPiP.value = false;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
onUnmounted(() => {
|
|
135
|
+
if (isPiP.value) exitPiP();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
isPiP,
|
|
140
|
+
isSupported,
|
|
141
|
+
enterPiP,
|
|
142
|
+
exitPiP,
|
|
143
|
+
armFirstGestureResume,
|
|
144
|
+
onPiPResize,
|
|
145
|
+
};
|
|
146
|
+
}
|
package/src/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/usePiPResizeObserver.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { PiPResizeCallback } from '../../types/types';
|
|
2
|
+
|
|
3
|
+
export function usePiPResizeObserver() {
|
|
4
|
+
const resizeCallbacks = new Set<PiPResizeCallback>();
|
|
5
|
+
let pipResizeObserver: ResizeObserver | null = null;
|
|
6
|
+
|
|
7
|
+
const startPiPResizeObserver = (element: HTMLElement) => {
|
|
8
|
+
pipResizeObserver?.disconnect();
|
|
9
|
+
pipResizeObserver = new ResizeObserver(([entry]) => {
|
|
10
|
+
for (const callback of resizeCallbacks) callback(entry.contentRect);
|
|
11
|
+
});
|
|
12
|
+
pipResizeObserver.observe(element);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const stopPiPResizeObserver = () => {
|
|
16
|
+
pipResizeObserver?.disconnect();
|
|
17
|
+
pipResizeObserver = null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const onPiPResize = (callback: PiPResizeCallback) => {
|
|
21
|
+
resizeCallbacks.add(callback);
|
|
22
|
+
return () => resizeCallbacks.delete(callback);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
startPiPResizeObserver,
|
|
27
|
+
stopPiPResizeObserver,
|
|
28
|
+
onPiPResize,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type ComputedRef, ref, watch } from 'vue';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tracks whether the receiver's stream has a live video track.
|
|
5
|
+
* Reacts to track additions, unmute, and ended events on the stream.
|
|
6
|
+
* Returns null when stream is absent, video is disabled, or call is on hold.
|
|
7
|
+
*/
|
|
8
|
+
export function useReceiverLiveStream(
|
|
9
|
+
receiverStream: ComputedRef<MediaStream | null | undefined>,
|
|
10
|
+
videoEnabled: ComputedRef<boolean | undefined>,
|
|
11
|
+
isOnHold: ComputedRef<boolean>,
|
|
12
|
+
) {
|
|
13
|
+
const liveStream = ref<MediaStream | null>(null);
|
|
14
|
+
|
|
15
|
+
watch(
|
|
16
|
+
[
|
|
17
|
+
receiverStream,
|
|
18
|
+
videoEnabled,
|
|
19
|
+
isOnHold,
|
|
20
|
+
],
|
|
21
|
+
([stream, videoOn, onHold], _, onCleanup) => {
|
|
22
|
+
liveStream.value = null;
|
|
23
|
+
|
|
24
|
+
if (!stream || !videoOn || onHold) return;
|
|
25
|
+
|
|
26
|
+
const refresh = () => {
|
|
27
|
+
const hasLiveVideo = stream
|
|
28
|
+
.getVideoTracks()
|
|
29
|
+
.some((t) => t.readyState === 'live');
|
|
30
|
+
liveStream.value = hasLiveVideo ? stream : null;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const onAddTrack = ({ track }: MediaStreamTrackEvent) => {
|
|
34
|
+
if (track.kind === 'video') {
|
|
35
|
+
track.addEventListener('unmute', refresh);
|
|
36
|
+
track.addEventListener('ended', refresh);
|
|
37
|
+
}
|
|
38
|
+
refresh();
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
stream.getVideoTracks().forEach((t) => {
|
|
42
|
+
t.addEventListener('unmute', refresh);
|
|
43
|
+
t.addEventListener('ended', refresh);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
stream.addEventListener('addtrack', onAddTrack);
|
|
47
|
+
|
|
48
|
+
refresh();
|
|
49
|
+
|
|
50
|
+
onCleanup(() => {
|
|
51
|
+
stream.removeEventListener('addtrack', onAddTrack);
|
|
52
|
+
stream.getVideoTracks().forEach((t) => {
|
|
53
|
+
t.removeEventListener('unmute', refresh);
|
|
54
|
+
t.removeEventListener('ended', refresh);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
immediate: true,
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return liveStream;
|
|
64
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type DocumentPiPWindow = Window &
|
|
2
|
+
typeof globalThis & {
|
|
3
|
+
documentPictureInPicture: {
|
|
4
|
+
requestWindow: (opts: {
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
}) => Promise<Window>;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type MediaSnapshot = {
|
|
12
|
+
el: HTMLMediaElement;
|
|
13
|
+
muted: boolean;
|
|
14
|
+
srcObject: MediaStream | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type PiPResizeCallback = (rect: DOMRectReadOnly) => void;
|