@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.
Files changed (89) hide show
  1. package/README.md +10 -0
  2. package/dist/{contacts-eFuSbv3m.js → contacts-DuiejYmg.js} +1551 -802
  3. package/dist/{index-CaY0ocAF.js → index-D_Bvqjjy.js} +1 -1
  4. package/dist/{index-TiiXs7ZO.js → index-ofJZWfBt.js} +1 -1
  5. package/dist/{install-Bd5HfFTV.js → install-rysuPEjF.js} +38 -38
  6. package/dist/{isObject-BV09j4E1.js → isObject-C5JdCKnH.js} +1 -1
  7. package/dist/ui-sdk.css +1 -1
  8. package/dist/ui-sdk.js +1 -1
  9. package/dist/ui-sdk.umd.cjs +187 -188
  10. package/dist/{useVidstackSrc-BGxC2kR5.js → useVidstackSrc-C6hUWIuE.js} +1 -1
  11. package/dist/{vidstack-Bq6c3Bam-jeTFOXk6.js → vidstack-Bq6c3Bam-D6M5GPTI.js} +3 -3
  12. package/dist/{vidstack-D2pY00kU-I-LJsKk8.js → vidstack-D2pY00kU-CRo5Kvyt.js} +3 -3
  13. package/dist/{vidstack-DDXt6fpN-W_Ix9vbH.js → vidstack-DDXt6fpN-QgRuASyL.js} +2 -2
  14. package/dist/{vidstack-D_-9AA6_-27F2Mv_5.js → vidstack-D_-9AA6_-BQB6UUD-.js} +2 -2
  15. package/dist/{vidstack-DqAw8m9J-BxfLqlFs.js → vidstack-DqAw8m9J-0B90hhJB.js} +1 -1
  16. package/dist/{vidstack-audio-CvjDPPuQ.js → vidstack-audio-BML-tyCz.js} +2 -2
  17. package/dist/{vidstack-dash-rV4ryZh2.js → vidstack-dash-BwVTHD5j.js} +4 -4
  18. package/dist/{vidstack-google-cast-Cb8t34m4.js → vidstack-google-cast-CvoUE_Qj.js} +4 -4
  19. package/dist/{vidstack-hls-C2zZ7COz.js → vidstack-hls-BgbWYqKl.js} +4 -4
  20. package/dist/{vidstack-video-DzItbHtm.js → vidstack-video-D6tVAa-p.js} +3 -3
  21. package/dist/{vidstack-vimeo-BpKsHwp4.js → vidstack-vimeo-CMino7JB.js} +4 -4
  22. package/dist/{vidstack-youtube--6zpNJz6.js → vidstack-youtube-DLE3pyNu.js} +3 -3
  23. package/dist/{wt-action-bar-Df57KUsc.js → wt-action-bar-BwAQUnPp.js} +1 -1
  24. package/dist/{wt-button-select-ymdOC5NH.js → wt-button-select-D8-D18E4.js} +1 -1
  25. package/dist/{wt-call-media-action-D4CP3oZl.js → wt-call-media-action-B-AEoxjt.js} +1 -1
  26. package/dist/{wt-chat-emoji-CUjHxdzV.js → wt-chat-emoji-D_4QMhH7.js} +2 -2
  27. package/dist/{wt-confirm-dialog-CNwpPdgE.js → wt-confirm-dialog-BQVImEga.js} +1 -1
  28. package/dist/{wt-context-menu-CToKHamf.js → wt-context-menu-CfvdB5Xl.js} +1 -1
  29. package/dist/{wt-copy-action-Dd0tVB7g.js → wt-copy-action-CIa3ImHz.js} +1 -1
  30. package/dist/{wt-datepicker-B7sKwJlh.js → wt-datepicker-CEoYduIo.js} +1 -1
  31. package/dist/{wt-display-chip-items-IGBh7VMV.js → wt-display-chip-items-CD6E8917.js} +1 -1
  32. package/dist/{wt-dual-panel-CqN6YB0F.js → wt-dual-panel-Cm2oe8sF.js} +1 -1
  33. package/dist/{wt-dummy-OrcannPM.js → wt-dummy-DHdJccrG.js} +1 -1
  34. package/dist/{wt-error-page-BMpoQdQl.js → wt-error-page-Rt-F9oVv.js} +1 -1
  35. package/dist/{wt-expansion-card-DEDM3Znr.js → wt-expansion-card-v7GFQBoJ.js} +1 -1
  36. package/dist/{wt-expansion-panel-YCSPf-eq.js → wt-expansion-panel-DSsY-KFR.js} +1 -1
  37. package/dist/{wt-filters-panel-wrapper-Ck2bF7qg.js → wt-filters-panel-wrapper-Cr9dYYnT.js} +1 -1
  38. package/dist/{wt-galleria-CNDkujRu.js → wt-galleria-C2aburQk.js} +1 -1
  39. package/dist/{wt-inline-add-panel-BSEdS7eW.js → wt-inline-add-panel-BaDP2TC1.js} +1 -1
  40. package/dist/{wt-navigation-menu-igJBYaxO.js → wt-navigation-menu-B_R8-_Bk.js} +1 -1
  41. package/dist/{wt-notifications-bar-C0M5LC4G.js → wt-notifications-bar-CfoCOojR.js} +2 -2
  42. package/dist/{wt-pagination-BXDqQ9YV.js → wt-pagination-DW7KL87c.js} +1 -1
  43. package/dist/{wt-player-A13mN2qk.js → wt-player-Be2YAJcQ.js} +2 -2
  44. package/dist/{wt-screen-recordings-action-BQPS--RV.js → wt-screen-recordings-action-BJBx9KXM.js} +1 -1
  45. package/dist/{wt-search-bar-Cvt_urUF.js → wt-search-bar-BT5d_LDB.js} +1 -1
  46. package/dist/{wt-selection-popup-CcAfc6T6.js → wt-selection-popup-DK6oLjfk.js} +1 -1
  47. package/dist/{wt-send-message-popup-pN9zzJpT.js → wt-send-message-popup-C8oG1Kxc.js} +3 -3
  48. package/dist/{wt-start-page-DaotE9Vi.js → wt-start-page-BcV-IdG_.js} +1 -1
  49. package/dist/{wt-status-select-Dhw-c6FB.js → wt-status-select-BcrdWJ9m.js} +1 -1
  50. package/dist/{wt-stepper-DyM6hf_l.js → wt-stepper-B_ny-IQN.js} +1 -1
  51. package/dist/{wt-table-Do6tizZl.js → wt-table-BX5kDXje.js} +1 -1
  52. package/dist/{wt-table-actions-Stm_KC06.js → wt-table-actions-Cvk2_NUG.js} +1 -1
  53. package/dist/{wt-table-column-select-DaGMSlL1.js → wt-table-column-select-CPacgsbN.js} +2 -2
  54. package/dist/{wt-tabs-CTv4GgX8.js → wt-tabs-wcnQfMfj.js} +1 -1
  55. package/dist/{wt-tags-input-DJhQrEdL.js → wt-tags-input-CPO80nnI.js} +2 -2
  56. package/dist/{wt-timepicker-98DhU-ME.js → wt-timepicker-DbdcOXYa.js} +1 -1
  57. package/dist/{wt-tree-CfkLw1SI.js → wt-tree-CLs3S4Av.js} +2 -2
  58. package/dist/{wt-tree-table-DDyxMFbz.js → wt-tree-table-DtNIE08B.js} +1 -1
  59. package/dist/{wt-type-extension-value-input-B2CwLd8p.js → wt-type-extension-value-input-CjJ8_xRU.js} +3 -3
  60. package/dist/{wt-vidstack-player-BduaN7w2.js → wt-vidstack-player-DmD7s58v.js} +900 -882
  61. package/package.json +10 -2
  62. package/src/components/wt-vidstack-player/wt-vidstack-player.vue +47 -3
  63. package/src/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/bridgeCustomElements.ts +32 -0
  64. package/src/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/copyStyles.ts +18 -0
  65. package/src/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/domTraversal.ts +16 -0
  66. package/src/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/index.ts +1 -0
  67. package/src/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/mediaCollection.ts +13 -0
  68. package/src/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/mediaPlayback.ts +22 -0
  69. package/src/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/mediaSnapshot.ts +43 -0
  70. package/src/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/playbackRetry.ts +81 -0
  71. package/src/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/useDocumentPiP.ts +146 -0
  72. package/src/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/usePiPResizeObserver.ts +30 -0
  73. package/src/modules/CallSession/modules/VideoCall/composables/useReceiverLiveStream.ts +64 -0
  74. package/src/modules/CallSession/modules/VideoCall/types/types.ts +17 -0
  75. package/src/modules/CallSession/modules/VideoCall/video-call.vue +170 -18
  76. package/types/components/wt-vidstack-player/wt-vidstack-player.vue.d.ts +3 -1
  77. package/types/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/bridgeCustomElements.d.ts +1 -0
  78. package/types/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/copyStyles.d.ts +1 -0
  79. package/types/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/domTraversal.d.ts +1 -0
  80. package/types/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/index.d.ts +1 -0
  81. package/types/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/mediaCollection.d.ts +2 -0
  82. package/types/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/mediaPlayback.d.ts +3 -0
  83. package/types/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/mediaSnapshot.d.ts +16 -0
  84. package/types/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/playbackRetry.d.ts +24 -0
  85. package/types/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/useDocumentPiP.d.ts +8 -0
  86. package/types/modules/CallSession/modules/VideoCall/composables/useDocumentPiP/usePiPResizeObserver.d.ts +6 -0
  87. package/types/modules/CallSession/modules/VideoCall/composables/useReceiverLiveStream.d.ts +49 -0
  88. package/types/modules/CallSession/modules/VideoCall/types/types.d.ts +14 -0
  89. 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.4.78",
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)?.play();
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
 
@@ -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';
@@ -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
+ };
@@ -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
+ }
@@ -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;