@streamscloud/kit 0.0.1-1770903095775 → 0.0.1-1770982902053

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.
@@ -1,4 +1,5 @@
1
1
  export declare const createGQLClient: (data: {
2
2
  url: string;
3
+ headers?: HeadersInit;
3
4
  customFetch?: typeof fetch;
4
5
  }) => import("@urql/core").Client;
@@ -3,7 +3,8 @@ export const createGQLClient = (data) => createClient({
3
3
  url: data.url,
4
4
  requestPolicy: 'network-only',
5
5
  fetchOptions: {
6
- credentials: 'include'
6
+ credentials: 'include',
7
+ headers: data.headers
7
8
  },
8
9
  fetch: data.customFetch ?? fetch,
9
10
  exchanges: [fetchExchange]
@@ -0,0 +1 @@
1
+ export declare const preloadImage: (src: string, throwOnError?: boolean) => Promise<void>;
@@ -0,0 +1,10 @@
1
+ export const preloadImage = (src, throwOnError = false) => {
2
+ return new Promise((resolve, reject) => {
3
+ const img = new Image();
4
+ img.decoding = 'async';
5
+ img.loading = 'eager';
6
+ img.onload = () => resolve();
7
+ img.onerror = (e) => (throwOnError ? reject(e) : resolve());
8
+ img.src = src;
9
+ });
10
+ };
@@ -6,6 +6,7 @@ export * from './date-helper';
6
6
  export * from './dom-helper';
7
7
  export * from './href-validator';
8
8
  export * from './html-helper';
9
+ export * from './image-preloader';
9
10
  export * from './lazy-init';
10
11
  export * from './number-helper';
11
12
  export * from './string-generator';
@@ -6,6 +6,7 @@ export * from './date-helper';
6
6
  export * from './dom-helper';
7
7
  export * from './href-validator';
8
8
  export * from './html-helper';
9
+ export * from './image-preloader';
9
10
  export * from './lazy-init';
10
11
  export * from './number-helper';
11
12
  export * from './string-generator';
@@ -0,0 +1,2 @@
1
+ export { MediaVolumeManager } from './volume-manager.svelte';
2
+ export { PlaybackManager } from './playback-manager.svelte';
@@ -0,0 +1,2 @@
1
+ export { MediaVolumeManager } from './volume-manager.svelte';
2
+ export { PlaybackManager } from './playback-manager.svelte';
@@ -0,0 +1,25 @@
1
+ type MediaCallbacks = {
2
+ onPlay: () => void;
3
+ onPause: () => void;
4
+ onStop: (inRespectToNewlyActivatedVideo: boolean) => void;
5
+ onToggle?: () => void;
6
+ onVolumeChange?: (volume: number) => void;
7
+ onMute?: () => void;
8
+ onUnmute?: () => void;
9
+ };
10
+ declare class PlaybackManagerInstance {
11
+ private _mountedPlayableComponents;
12
+ private _playingComponentId;
13
+ registerMountedPlayer: (componentId: string, callbacks: MediaCallbacks) => void;
14
+ unregisterPlayer: (componentId: string) => void;
15
+ setPlayingComponent: (componentId: string | null) => void;
16
+ play: (componentId: string | null) => void;
17
+ pause: (componentId?: string) => void;
18
+ stop: (componentId: string) => void;
19
+ toggle: (componentId: string) => void;
20
+ setVolume: (componentId: string, volume: number) => void;
21
+ mute: (componentId: string) => void;
22
+ unmute: (componentId: string) => void;
23
+ }
24
+ export declare const PlaybackManager: PlaybackManagerInstance;
25
+ export {};
@@ -0,0 +1,62 @@
1
+ class PlaybackManagerInstance {
2
+ _mountedPlayableComponents = new Map();
3
+ _playingComponentId = null;
4
+ registerMountedPlayer = (componentId, callbacks) => {
5
+ this._mountedPlayableComponents.set(componentId, callbacks);
6
+ };
7
+ unregisterPlayer = (componentId) => {
8
+ this._mountedPlayableComponents.delete(componentId);
9
+ if (this._playingComponentId === componentId) {
10
+ this._playingComponentId = null;
11
+ }
12
+ };
13
+ setPlayingComponent = (componentId) => {
14
+ if (this._playingComponentId === componentId) {
15
+ return;
16
+ }
17
+ if (this._playingComponentId) {
18
+ const callbacks = this._mountedPlayableComponents.get(this._playingComponentId);
19
+ callbacks?.onStop(true);
20
+ }
21
+ this._playingComponentId = componentId;
22
+ };
23
+ play = (componentId) => {
24
+ this.setPlayingComponent(componentId);
25
+ if (componentId) {
26
+ const callbacks = this._mountedPlayableComponents.get(componentId);
27
+ callbacks?.onPlay();
28
+ }
29
+ };
30
+ pause = (componentId) => {
31
+ const id = componentId || this._playingComponentId;
32
+ if (!id) {
33
+ return;
34
+ }
35
+ const callbacks = this._mountedPlayableComponents.get(id);
36
+ callbacks?.onPause();
37
+ };
38
+ stop = (componentId) => {
39
+ const callbacks = this._mountedPlayableComponents.get(componentId);
40
+ callbacks?.onStop(false);
41
+ if (this._playingComponentId === componentId) {
42
+ this._playingComponentId = null;
43
+ }
44
+ };
45
+ toggle = (componentId) => {
46
+ const callbacks = this._mountedPlayableComponents.get(componentId);
47
+ callbacks?.onToggle?.();
48
+ };
49
+ setVolume = (componentId, volume) => {
50
+ const callbacks = this._mountedPlayableComponents.get(componentId);
51
+ callbacks?.onVolumeChange?.(volume);
52
+ };
53
+ mute = (componentId) => {
54
+ const callbacks = this._mountedPlayableComponents.get(componentId);
55
+ callbacks?.onMute?.();
56
+ };
57
+ unmute = (componentId) => {
58
+ const callbacks = this._mountedPlayableComponents.get(componentId);
59
+ callbacks?.onUnmute?.();
60
+ };
61
+ }
62
+ export const PlaybackManager = new PlaybackManagerInstance();
@@ -0,0 +1,10 @@
1
+ declare class VolumeManagerInstance {
2
+ private _volumeLevel;
3
+ private _isMuted;
4
+ get volumeLevel(): number;
5
+ get isMuted(): boolean;
6
+ set volumeLevel(level: number);
7
+ set isMuted(state: boolean);
8
+ }
9
+ export declare const MediaVolumeManager: VolumeManagerInstance;
10
+ export {};
@@ -0,0 +1,25 @@
1
+ class VolumeManagerInstance {
2
+ _volumeLevel = $state(1);
3
+ _isMuted = $state(false);
4
+ get volumeLevel() {
5
+ return this._volumeLevel;
6
+ }
7
+ get isMuted() {
8
+ return this._isMuted;
9
+ }
10
+ set volumeLevel(level) {
11
+ if (level < 0) {
12
+ this._volumeLevel = 0;
13
+ }
14
+ else if (level > 1) {
15
+ this._volumeLevel = 1;
16
+ }
17
+ else {
18
+ this._volumeLevel = level;
19
+ }
20
+ }
21
+ set isMuted(state) {
22
+ this._isMuted = state;
23
+ }
24
+ }
25
+ export const MediaVolumeManager = new VolumeManagerInstance();
@@ -0,0 +1,371 @@
1
+ <script lang="ts">import { randomNanoid } from '../../core/utils';
2
+ import { Icon } from '../icon';
3
+ import { MediaVolumeManager, PlaybackManager } from '../media-playback';
4
+ import { SeekBar } from '../seek-bar';
5
+ import IconPause from '@fluentui/svg-icons/icons/pause_20_regular.svg?raw';
6
+ import IconPlay from '@fluentui/svg-icons/icons/play_20_regular.svg?raw';
7
+ import IconSpeaker from '@fluentui/svg-icons/icons/speaker_2_20_regular.svg?raw';
8
+ import IconSpeakerMute from '@fluentui/svg-icons/icons/speaker_mute_20_regular.svg?raw';
9
+ import { untrack } from 'svelte';
10
+ import { fade } from 'svelte/transition';
11
+ let { src, poster, id = randomNanoid(), controls = true, autoplay = false, loop = false, inert = false, allowPreloading = false, hideSpeaker = false, hidePlayButton = false, intersectionContainer, scrubberPosition = 'bottom', on } = $props();
12
+ let video = $state(null);
13
+ let videoContainerRef = $state(null);
14
+ let showControlsOnHover = $state(false);
15
+ let isVideoPaused = $state(true);
16
+ let percentageCompleted = $state(0);
17
+ let everActivated = $state(false);
18
+ let autoplayState = $state(untrack(() => autoplay));
19
+ $effect(() => {
20
+ void src;
21
+ untrack(() => {
22
+ if (video?.src) {
23
+ video.src = src;
24
+ }
25
+ });
26
+ });
27
+ // Register player instance in global playback manager
28
+ $effect(() => untrack(() => {
29
+ PlaybackManager.registerMountedPlayer(id, {
30
+ onPlay: () => {
31
+ void play();
32
+ },
33
+ onPause: () => {
34
+ pause();
35
+ },
36
+ onStop: (inRespectToNewlyActivatedVideo) => {
37
+ if (autoplayState === 'on-appearance' && inRespectToNewlyActivatedVideo) {
38
+ return;
39
+ }
40
+ stop();
41
+ },
42
+ onToggle: () => {
43
+ togglePlay();
44
+ },
45
+ onVolumeChange: (volume) => {
46
+ setVolume(volume);
47
+ },
48
+ onMute: () => {
49
+ setMuted(true);
50
+ },
51
+ onUnmute: () => {
52
+ setMuted(false);
53
+ }
54
+ });
55
+ on?.loaded?.({ id, src });
56
+ return () => {
57
+ PlaybackManager.unregisterPlayer(id);
58
+ };
59
+ }));
60
+ // Intersection observer for lazy loading and autoplay-on-appearance
61
+ $effect(() => {
62
+ if (!videoContainerRef) {
63
+ return;
64
+ }
65
+ const container = videoContainerRef;
66
+ return untrack(() => {
67
+ const observer = new IntersectionObserver((entries) => {
68
+ if (!video) {
69
+ return;
70
+ }
71
+ const [entry] = entries;
72
+ if ((entry.isIntersecting || allowPreloading) && !video.src) {
73
+ video.src = src;
74
+ video.load();
75
+ if (autoplayState === true) {
76
+ const handleCanPlay = () => {
77
+ if (entry.isIntersecting) {
78
+ void play();
79
+ autoplayState = false;
80
+ }
81
+ };
82
+ video.addEventListener('canplay', handleCanPlay, { once: true });
83
+ return;
84
+ }
85
+ }
86
+ if (autoplayState !== 'on-appearance') {
87
+ return;
88
+ }
89
+ if (!entry.isIntersecting) {
90
+ stop();
91
+ return;
92
+ }
93
+ if (entry.intersectionRatio < 0.4) {
94
+ pause();
95
+ }
96
+ if (entry.intersectionRatio >= 0.6) {
97
+ const handleCanPlay = () => {
98
+ if (entry.intersectionRatio >= 0.6) {
99
+ void play();
100
+ }
101
+ };
102
+ if (video.readyState >= 3) {
103
+ void play();
104
+ }
105
+ else {
106
+ video.addEventListener('canplay', handleCanPlay, { once: true });
107
+ }
108
+ }
109
+ }, {
110
+ root: intersectionContainer,
111
+ rootMargin: '0px',
112
+ threshold: [0.1, 0.4, 0.6]
113
+ });
114
+ observer.observe(container);
115
+ return () => {
116
+ observer.disconnect();
117
+ };
118
+ });
119
+ });
120
+ const play = async () => {
121
+ if (!video) {
122
+ return;
123
+ }
124
+ try {
125
+ await video.play();
126
+ }
127
+ catch {
128
+ // can't play video without interaction with document, just ignoring the error
129
+ }
130
+ finally {
131
+ if (video.paused) {
132
+ video.muted = true;
133
+ void video.play();
134
+ }
135
+ everActivated = true;
136
+ }
137
+ on?.started?.({ id, src });
138
+ };
139
+ const pause = () => {
140
+ video?.pause();
141
+ };
142
+ const stop = () => {
143
+ if (!video) {
144
+ return;
145
+ }
146
+ video.pause();
147
+ video.currentTime = 0;
148
+ video.load();
149
+ onPause();
150
+ };
151
+ const togglePlay = (e) => {
152
+ e?.stopPropagation();
153
+ if (!video) {
154
+ return;
155
+ }
156
+ if (video.paused) {
157
+ void play();
158
+ }
159
+ else {
160
+ pause();
161
+ }
162
+ };
163
+ const toggleMute = (e) => {
164
+ e?.stopPropagation();
165
+ setMuted(!video?.muted);
166
+ };
167
+ const onVolumeChange = () => {
168
+ if (!video) {
169
+ return;
170
+ }
171
+ MediaVolumeManager.volumeLevel = video.volume;
172
+ MediaVolumeManager.isMuted = video.muted;
173
+ };
174
+ const onTimeUpdate = () => {
175
+ if (!video) {
176
+ return;
177
+ }
178
+ percentageCompleted = video.currentTime / video.duration || 0;
179
+ notifyProgress();
180
+ };
181
+ const onLoaded = () => {
182
+ setVolume(MediaVolumeManager.volumeLevel);
183
+ setMuted(MediaVolumeManager.isMuted);
184
+ };
185
+ const onEnded = () => {
186
+ percentageCompleted = 1;
187
+ notifyProgress();
188
+ on?.ended?.({ id, src });
189
+ if (!loop) {
190
+ everActivated = false;
191
+ }
192
+ };
193
+ const onPlay = () => {
194
+ isVideoPaused = false;
195
+ PlaybackManager.setPlayingComponent(id);
196
+ };
197
+ const onPause = () => {
198
+ isVideoPaused = true;
199
+ };
200
+ const setVolume = (level) => {
201
+ MediaVolumeManager.volumeLevel = level;
202
+ if (video) {
203
+ video.volume = level;
204
+ }
205
+ };
206
+ const setMuted = (state) => {
207
+ MediaVolumeManager.isMuted = state;
208
+ };
209
+ $effect(() => {
210
+ if (video) {
211
+ video.muted = MediaVolumeManager.isMuted;
212
+ }
213
+ });
214
+ const notifyProgress = () => {
215
+ on?.progress?.(percentageCompleted);
216
+ };
217
+ const handleSeek = (percent) => {
218
+ if (video) {
219
+ video.currentTime = video.duration * percent;
220
+ percentageCompleted = percent;
221
+ notifyProgress();
222
+ }
223
+ };
224
+ </script>
225
+
226
+ <div class="video" role="none" inert={inert} bind:this={videoContainerRef}>
227
+ <video
228
+ class="video__video"
229
+ class:video__video--not-activated={!everActivated}
230
+ width="100%"
231
+ controls={controls && everActivated}
232
+ poster={poster}
233
+ loop={loop}
234
+ preload="metadata"
235
+ onvolumechange={onVolumeChange}
236
+ ontimeupdate={onTimeUpdate}
237
+ onloadeddata={onLoaded}
238
+ onended={onEnded}
239
+ onplay={onPlay}
240
+ onpause={onPause}
241
+ playsinline
242
+ bind:this={video}>
243
+ <track src="" kind="captions" />
244
+ </video>
245
+ {#if !everActivated && poster}
246
+ <img class="video__poster" src={poster} alt="" />
247
+ {/if}
248
+ {#if !controls || !everActivated}
249
+ <div class="video__overlay" onclick={togglePlay} onkeydown={() => ({})} role="none">
250
+ {#if isVideoPaused && !hidePlayButton}
251
+ <button type="button" aria-label="play" class="video__playback-button" onclick={togglePlay} onkeydown={() => ({})}>
252
+ <Icon src={IconPlay} color="white" />
253
+ </button>
254
+ {:else if showControlsOnHover && !hidePlayButton}
255
+ <button type="button" aria-label="pause" class="video__playback-button video__playback-button--pause" onclick={togglePlay} onkeydown={() => ({})}>
256
+ <Icon src={IconPause} color="white" />
257
+ </button>
258
+ {/if}
259
+ {#if (showControlsOnHover || MediaVolumeManager.isMuted) && !hideSpeaker}
260
+ <button type="button" aria-label={MediaVolumeManager.isMuted ? 'mute' : 'unmute'} class="video__mute-button" onclick={toggleMute}>
261
+ {#if MediaVolumeManager.isMuted}
262
+ <Icon src={IconSpeakerMute} color="white" />
263
+ {:else}
264
+ <Icon src={IconSpeaker} color="white" />
265
+ {/if}
266
+ </button>
267
+ {/if}
268
+
269
+ {#if everActivated}
270
+ <div
271
+ class="video__progress-container"
272
+ class:video__progress-container--top={scrubberPosition === 'top'}
273
+ class:video__progress-container--bottom={scrubberPosition === 'bottom'}
274
+ onmouseenter={() => (showControlsOnHover = true)}
275
+ onmouseleave={() => (showControlsOnHover = false)}
276
+ role="none">
277
+ {#if showControlsOnHover || (!showControlsOnHover && isVideoPaused)}
278
+ <div
279
+ class="video__seek-bar"
280
+ transition:fade={{ duration: isVideoPaused ? 0 : 300 }}
281
+ onclick={(e) => e.stopPropagation()}
282
+ onkeydown={() => ({})}
283
+ role="none">
284
+ <SeekBar value={percentageCompleted} on={{ seek: handleSeek }} />
285
+ </div>
286
+ {/if}
287
+ </div>
288
+ {/if}
289
+ </div>
290
+ {/if}
291
+ </div>
292
+
293
+ <style>.video {
294
+ --_video--background-color: var(--video--background-color, #000000);
295
+ --_video--border-radius: var(--video--border-radius, 0);
296
+ --_video--media-fit: var(--video--media-fit, contain);
297
+ --_video--poster--media-fit: var(--video--poster--media-fit, cover);
298
+ height: 100%;
299
+ min-height: 100%;
300
+ max-height: 100%;
301
+ width: 100%;
302
+ min-width: 100%;
303
+ max-width: 100%;
304
+ cursor: pointer;
305
+ position: relative;
306
+ overflow: hidden;
307
+ border-radius: var(--_video--border-radius);
308
+ background: var(--_video--background-color);
309
+ }
310
+ .video__playback-button {
311
+ --icon--filter: drop-shadow(1px 1px #000000);
312
+ position: absolute;
313
+ top: 50%;
314
+ left: 50%;
315
+ transform: translate(-50%, -50%);
316
+ font-size: 2em;
317
+ }
318
+ .video__playback-button--pause {
319
+ /* Set 'container-type: inline-size;' to reference container*/
320
+ }
321
+ @container (width < 576px) {
322
+ .video__playback-button--pause {
323
+ display: none;
324
+ }
325
+ }
326
+ .video__mute-button {
327
+ position: absolute;
328
+ top: 0.625em;
329
+ right: 0.625em;
330
+ font-size: 1em;
331
+ z-index: 1;
332
+ }
333
+ .video__poster {
334
+ object-fit: var(--_video--poster--media-fit);
335
+ min-width: 100%;
336
+ min-height: 100%;
337
+ max-width: 100%;
338
+ max-height: 100%;
339
+ }
340
+ .video__video {
341
+ object-fit: var(--_video--media-fit);
342
+ min-width: 100%;
343
+ min-height: 100%;
344
+ max-width: 100%;
345
+ max-height: 100%;
346
+ }
347
+ .video__video--not-activated {
348
+ visibility: hidden;
349
+ height: 0;
350
+ min-height: 0;
351
+ }
352
+ .video__overlay {
353
+ position: absolute;
354
+ inset: 0;
355
+ background-color: transparent;
356
+ }
357
+ .video__progress-container {
358
+ position: absolute;
359
+ left: 0;
360
+ right: 0;
361
+ z-index: 1;
362
+ padding-inline: 0.25rem;
363
+ }
364
+ .video__progress-container--top {
365
+ top: 0;
366
+ padding-bottom: 0.9375rem;
367
+ }
368
+ .video__progress-container--bottom {
369
+ bottom: 0;
370
+ padding-top: 0.9375rem;
371
+ }</style>
@@ -0,0 +1,33 @@
1
+ import type { ScrubberPosition } from './types';
2
+ type Props = {
3
+ src: string;
4
+ poster: string | null | undefined;
5
+ id?: string;
6
+ controls?: boolean;
7
+ autoplay?: true | false | 'on-appearance';
8
+ loop?: boolean;
9
+ intersectionContainer?: HTMLElement;
10
+ inert?: boolean;
11
+ allowPreloading?: boolean;
12
+ hideSpeaker?: boolean;
13
+ hidePlayButton?: boolean;
14
+ scrubberPosition?: ScrubberPosition;
15
+ on?: {
16
+ started?: (data: {
17
+ id: string;
18
+ src: string;
19
+ }) => void;
20
+ loaded?: (data: {
21
+ id: string;
22
+ src: string;
23
+ }) => void;
24
+ progress?: (value: number) => void;
25
+ ended?: (data: {
26
+ id: string;
27
+ src: string;
28
+ }) => void;
29
+ };
30
+ };
31
+ declare const Cmp: import("svelte").Component<Props, {}, "">;
32
+ type Cmp = ReturnType<typeof Cmp>;
33
+ export default Cmp;
@@ -0,0 +1,2 @@
1
+ export { default as Video } from './cmp.video.svelte';
2
+ export type { ScrubberPosition } from './types';
@@ -0,0 +1 @@
1
+ export { default as Video } from './cmp.video.svelte';
@@ -0,0 +1 @@
1
+ export type ScrubberPosition = 'top' | 'bottom';
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamscloud/kit",
3
- "version": "0.0.1-1770903095775",
3
+ "version": "0.0.1-1770982902053",
4
4
  "author": "StreamsCloud",
5
5
  "repository": {
6
6
  "type": "git",
@@ -124,6 +124,10 @@
124
124
  "types": "./dist/ui/loading/index.d.ts",
125
125
  "svelte": "./dist/ui/loading/index.js"
126
126
  },
127
+ "./ui/media-playback": {
128
+ "types": "./dist/ui/media-playback/index.d.ts",
129
+ "svelte": "./dist/ui/media-playback/index.js"
130
+ },
127
131
  "./ui/progress": {
128
132
  "types": "./dist/ui/progress/index.d.ts",
129
133
  "svelte": "./dist/ui/progress/index.js"
@@ -144,6 +148,10 @@
144
148
  "types": "./dist/ui/time-ago/index.d.ts",
145
149
  "svelte": "./dist/ui/time-ago/index.js"
146
150
  },
151
+ "./ui/video": {
152
+ "types": "./dist/ui/video/index.d.ts",
153
+ "svelte": "./dist/ui/video/index.js"
154
+ },
147
155
  "./styles/base": {
148
156
  "sass": "./dist/styles/_index.scss"
149
157
  },