@streamslice/widget 1.0.0

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.
@@ -0,0 +1,1655 @@
1
+ /*!
2
+ * Widget v1.0.0
3
+ * Floating video player with Amazon IVS support
4
+ * Released under the MIT License
5
+ */
6
+ /**
7
+ * StreamSlice API Client
8
+ */
9
+ class ApiClient {
10
+ constructor(baseUrl) {
11
+ this.baseUrl = baseUrl.replace(/\/$/, '');
12
+ }
13
+ /**
14
+ * Get playlist URL for a page link
15
+ */
16
+ async getPlaylist(pageLink) {
17
+ const url = new URL(`${this.baseUrl}/api/event/getPlaylist`);
18
+ url.searchParams.set('link', pageLink);
19
+ try {
20
+ const response = await fetch(url.toString(), {
21
+ method: 'GET',
22
+ headers: {
23
+ 'Content-Type': 'application/json',
24
+ },
25
+ });
26
+ if (!response.ok) {
27
+ throw new Error(`HTTP error! status: ${response.status}`);
28
+ }
29
+ const data = await response.json();
30
+ return data;
31
+ }
32
+ catch (error) {
33
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
34
+ return {
35
+ error: {
36
+ code: 'FETCH_ERROR',
37
+ error_message_message: errorMessage,
38
+ },
39
+ };
40
+ }
41
+ }
42
+ }
43
+
44
+ /**
45
+ * SVG Icons
46
+ */
47
+ const Icons = {
48
+ play: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>`,
49
+ pause: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>`,
50
+ volumeHigh: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>`,
51
+ volumeLow: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.5 12c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM5 9v6h4l5 5V4L9 9H5z"/></svg>`,
52
+ volumeMute: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/></svg>`,
53
+ fullscreen: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>`,
54
+ fullscreenExit: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/></svg>`,
55
+ close: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>`,
56
+ video: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/></svg>`,
57
+ error: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>`,
58
+ chevronDown: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>`,
59
+ };
60
+
61
+ /**
62
+ * Floating Window Component
63
+ */
64
+ class FloatingWindow {
65
+ constructor(options) {
66
+ this.isDragging = false;
67
+ this.isResizing = false;
68
+ this.resizeDirection = null;
69
+ this.dragOffset = { x: 0, y: 0 };
70
+ this.startPos = { x: 0, y: 0 };
71
+ this.startSize = { width: 0, height: 0 };
72
+ this.options = options;
73
+ this.currentPosition = { ...options.position };
74
+ this.currentSize = { ...options.size };
75
+ this.container = this.createContainer();
76
+ this.header = this.createHeader();
77
+ this.videoContainer = this.createVideoContainer();
78
+ this.controlsContainer = this.createControlsContainer();
79
+ this.container.appendChild(this.header);
80
+ this.container.appendChild(this.videoContainer);
81
+ this.container.appendChild(this.controlsContainer);
82
+ this.createResizeHandles();
83
+ this.attachEventListeners();
84
+ document.body.appendChild(this.container);
85
+ // Trigger enter animation
86
+ requestAnimationFrame(() => {
87
+ this.container.classList.add('ss-entering');
88
+ setTimeout(() => this.container.classList.remove('ss-entering'), 300);
89
+ });
90
+ }
91
+ createContainer() {
92
+ const container = document.createElement('div');
93
+ container.className = `ss-widget ss-theme-${this.options.theme}`;
94
+ if (this.options.className) {
95
+ container.classList.add(this.options.className);
96
+ }
97
+ container.style.cssText = `
98
+ left: ${this.currentPosition.x}px;
99
+ top: ${this.currentPosition.y}px;
100
+ width: ${this.currentSize.width}px;
101
+ height: ${this.currentSize.height}px;
102
+ z-index: ${this.options.zIndex};
103
+ `;
104
+ return container;
105
+ }
106
+ createHeader() {
107
+ const header = document.createElement('div');
108
+ header.className = 'ss-header';
109
+ const titleWrapper = document.createElement('div');
110
+ titleWrapper.className = 'ss-header-title';
111
+ const liveBadge = document.createElement('span');
112
+ liveBadge.className = 'ss-live-badge';
113
+ liveBadge.innerHTML = `<span class="ss-live-dot"></span>LIVE`;
114
+ titleWrapper.appendChild(liveBadge);
115
+ const actions = document.createElement('div');
116
+ actions.className = 'ss-header-actions';
117
+ const closeBtn = document.createElement('button');
118
+ closeBtn.className = 'ss-btn ss-btn-close';
119
+ closeBtn.innerHTML = Icons.close;
120
+ closeBtn.title = 'Close';
121
+ closeBtn.addEventListener('click', () => this.close());
122
+ actions.appendChild(closeBtn);
123
+ header.appendChild(titleWrapper);
124
+ header.appendChild(actions);
125
+ return header;
126
+ }
127
+ createVideoContainer() {
128
+ const container = document.createElement('div');
129
+ container.className = 'ss-video-container';
130
+ return container;
131
+ }
132
+ createControlsContainer() {
133
+ const container = document.createElement('div');
134
+ container.className = 'ss-controls';
135
+ return container;
136
+ }
137
+ createResizeHandles() {
138
+ const directions = ['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'];
139
+ directions.forEach(dir => {
140
+ const handle = document.createElement('div');
141
+ handle.className = `ss-resize-handle ss-resize-${dir}`;
142
+ handle.dataset.direction = dir;
143
+ this.container.appendChild(handle);
144
+ });
145
+ }
146
+ attachEventListeners() {
147
+ // Drag handling
148
+ this.header.addEventListener('mousedown', this.onDragStart.bind(this));
149
+ document.addEventListener('mousemove', this.onDrag.bind(this));
150
+ document.addEventListener('mouseup', this.onDragEnd.bind(this));
151
+ // Touch support for drag
152
+ this.header.addEventListener('touchstart', this.onTouchDragStart.bind(this), { passive: false });
153
+ document.addEventListener('touchmove', this.onTouchDrag.bind(this), { passive: false });
154
+ document.addEventListener('touchend', this.onDragEnd.bind(this));
155
+ // Resize handling
156
+ this.container.querySelectorAll('.ss-resize-handle').forEach(handle => {
157
+ handle.addEventListener('mousedown', this.onResizeStart.bind(this));
158
+ });
159
+ document.addEventListener('mousemove', this.onResize.bind(this));
160
+ document.addEventListener('mouseup', this.onResizeEnd.bind(this));
161
+ }
162
+ onDragStart(e) {
163
+ if (e.target.closest('.ss-btn'))
164
+ return;
165
+ this.isDragging = true;
166
+ this.dragOffset = {
167
+ x: e.clientX - this.currentPosition.x,
168
+ y: e.clientY - this.currentPosition.y,
169
+ };
170
+ this.container.style.transition = 'none';
171
+ }
172
+ onTouchDragStart(e) {
173
+ if (e.target.closest('.ss-btn'))
174
+ return;
175
+ const touch = e.touches[0];
176
+ this.isDragging = true;
177
+ this.dragOffset = {
178
+ x: touch.clientX - this.currentPosition.x,
179
+ y: touch.clientY - this.currentPosition.y,
180
+ };
181
+ this.container.style.transition = 'none';
182
+ }
183
+ onDrag(e) {
184
+ if (!this.isDragging)
185
+ return;
186
+ e.preventDefault();
187
+ this.updatePosition(e.clientX - this.dragOffset.x, e.clientY - this.dragOffset.y);
188
+ }
189
+ onTouchDrag(e) {
190
+ if (!this.isDragging)
191
+ return;
192
+ e.preventDefault();
193
+ const touch = e.touches[0];
194
+ this.updatePosition(touch.clientX - this.dragOffset.x, touch.clientY - this.dragOffset.y);
195
+ }
196
+ onDragEnd() {
197
+ if (this.isDragging) {
198
+ this.isDragging = false;
199
+ this.container.style.transition = '';
200
+ this.options.onMove?.(this.currentPosition);
201
+ }
202
+ }
203
+ updatePosition(x, y) {
204
+ // Keep window within viewport
205
+ const maxX = window.innerWidth - this.currentSize.width;
206
+ const maxY = window.innerHeight - this.currentSize.height;
207
+ this.currentPosition = {
208
+ x: Math.max(0, Math.min(x, maxX)),
209
+ y: Math.max(0, Math.min(y, maxY)),
210
+ };
211
+ this.container.style.left = `${this.currentPosition.x}px`;
212
+ this.container.style.top = `${this.currentPosition.y}px`;
213
+ }
214
+ onResizeStart(e) {
215
+ e.preventDefault();
216
+ e.stopPropagation();
217
+ const handle = e.target;
218
+ this.resizeDirection = handle.dataset.direction;
219
+ this.isResizing = true;
220
+ this.startPos = { x: e.clientX, y: e.clientY };
221
+ this.startSize = { ...this.currentSize };
222
+ this.container.style.transition = 'none';
223
+ }
224
+ onResize(e) {
225
+ if (!this.isResizing || !this.resizeDirection)
226
+ return;
227
+ e.preventDefault();
228
+ const deltaX = e.clientX - this.startPos.x;
229
+ const deltaY = e.clientY - this.startPos.y;
230
+ let newWidth = this.startSize.width;
231
+ let newHeight = this.startSize.height;
232
+ let newX = this.currentPosition.x;
233
+ let newY = this.currentPosition.y;
234
+ // Calculate new dimensions based on resize direction
235
+ if (this.resizeDirection.includes('e')) {
236
+ newWidth = this.startSize.width + deltaX;
237
+ }
238
+ if (this.resizeDirection.includes('w')) {
239
+ newWidth = this.startSize.width - deltaX;
240
+ newX = this.currentPosition.x + deltaX;
241
+ }
242
+ if (this.resizeDirection.includes('s')) {
243
+ newHeight = this.startSize.height + deltaY;
244
+ }
245
+ if (this.resizeDirection.includes('n')) {
246
+ newHeight = this.startSize.height - deltaY;
247
+ newY = this.currentPosition.y + deltaY;
248
+ }
249
+ // Apply constraints
250
+ newWidth = Math.max(this.options.minSize.width, Math.min(newWidth, this.options.maxSize.width));
251
+ newHeight = Math.max(this.options.minSize.height, Math.min(newHeight, this.options.maxSize.height));
252
+ // Adjust position if resizing from left or top
253
+ if (this.resizeDirection.includes('w') && newWidth !== this.currentSize.width) {
254
+ newX = this.currentPosition.x + (this.currentSize.width - newWidth);
255
+ }
256
+ if (this.resizeDirection.includes('n') && newHeight !== this.currentSize.height) {
257
+ newY = this.currentPosition.y + (this.currentSize.height - newHeight);
258
+ }
259
+ this.currentSize = { width: newWidth, height: newHeight };
260
+ this.currentPosition = { x: newX, y: newY };
261
+ this.container.style.width = `${newWidth}px`;
262
+ this.container.style.height = `${newHeight}px`;
263
+ this.container.style.left = `${newX}px`;
264
+ this.container.style.top = `${newY}px`;
265
+ }
266
+ onResizeEnd() {
267
+ if (this.isResizing) {
268
+ this.isResizing = false;
269
+ this.resizeDirection = null;
270
+ this.container.style.transition = '';
271
+ this.options.onResize?.(this.currentSize);
272
+ }
273
+ }
274
+ getVideoContainer() {
275
+ return this.videoContainer;
276
+ }
277
+ getControlsContainer() {
278
+ return this.controlsContainer;
279
+ }
280
+ getContainer() {
281
+ return this.container;
282
+ }
283
+ setSize(size) {
284
+ this.currentSize = size;
285
+ this.container.style.width = `${size.width}px`;
286
+ this.container.style.height = `${size.height}px`;
287
+ }
288
+ setPosition(position) {
289
+ this.currentPosition = position;
290
+ this.container.style.left = `${position.x}px`;
291
+ this.container.style.top = `${position.y}px`;
292
+ }
293
+ showLiveBadge(show) {
294
+ const badge = this.header.querySelector('.ss-live-badge');
295
+ if (badge) {
296
+ badge.style.display = show ? 'inline-flex' : 'none';
297
+ }
298
+ }
299
+ close() {
300
+ this.container.classList.add('ss-leaving');
301
+ setTimeout(() => {
302
+ this.destroy();
303
+ this.options.onClose?.();
304
+ }, 200);
305
+ }
306
+ destroy() {
307
+ document.removeEventListener('mousemove', this.onDrag.bind(this));
308
+ document.removeEventListener('mouseup', this.onDragEnd.bind(this));
309
+ document.removeEventListener('mousemove', this.onResize.bind(this));
310
+ document.removeEventListener('mouseup', this.onResizeEnd.bind(this));
311
+ this.container.remove();
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Player Controls Component
317
+ */
318
+ class PlayerControls {
319
+ constructor(container, options) {
320
+ this.container = container;
321
+ this.options = options;
322
+ this.state = {
323
+ isPlaying: false,
324
+ isMuted: false,
325
+ volume: 1,
326
+ isFullscreen: false,
327
+ isLoading: true,
328
+ duration: 0,
329
+ currentTime: 0,
330
+ isLive: true,
331
+ quality: 'auto',
332
+ availableQualities: [],
333
+ };
334
+ this.render();
335
+ }
336
+ render() {
337
+ this.container.innerHTML = '';
338
+ // Left controls
339
+ const leftControls = document.createElement('div');
340
+ leftControls.className = 'ss-controls-left';
341
+ // Play/Pause button
342
+ this.playBtn = this.createButton('ss-control-btn ss-play-btn', Icons.play, 'Play');
343
+ this.playBtn.addEventListener('click', () => this.options.onPlayPause?.());
344
+ leftControls.appendChild(this.playBtn);
345
+ // Volume controls
346
+ const volumeWrapper = document.createElement('div');
347
+ volumeWrapper.className = 'ss-volume-wrapper';
348
+ this.muteBtn = this.createButton('ss-control-btn ss-mute-btn', Icons.volumeHigh, 'Mute');
349
+ this.muteBtn.addEventListener('click', () => this.options.onMuteToggle?.());
350
+ this.volumeSlider = document.createElement('input');
351
+ this.volumeSlider.type = 'range';
352
+ this.volumeSlider.className = 'ss-volume-slider';
353
+ this.volumeSlider.min = '0';
354
+ this.volumeSlider.max = '1';
355
+ this.volumeSlider.step = '0.1';
356
+ this.volumeSlider.value = '1';
357
+ this.volumeSlider.addEventListener('input', (e) => {
358
+ const target = e.target;
359
+ this.options.onVolumeChange?.(parseFloat(target.value));
360
+ });
361
+ volumeWrapper.appendChild(this.muteBtn);
362
+ volumeWrapper.appendChild(this.volumeSlider);
363
+ leftControls.appendChild(volumeWrapper);
364
+ // Right controls
365
+ const rightControls = document.createElement('div');
366
+ rightControls.className = 'ss-controls-right';
367
+ // Quality selector
368
+ const qualityWrapper = document.createElement('div');
369
+ qualityWrapper.className = 'ss-quality-wrapper';
370
+ this.qualityBtn = document.createElement('button');
371
+ this.qualityBtn.className = 'ss-quality-btn';
372
+ this.qualityBtn.innerHTML = `Auto ${Icons.chevronDown}`;
373
+ this.qualityBtn.addEventListener('click', () => this.toggleQualityMenu());
374
+ this.qualityMenu = document.createElement('div');
375
+ this.qualityMenu.className = 'ss-quality-menu';
376
+ qualityWrapper.appendChild(this.qualityBtn);
377
+ qualityWrapper.appendChild(this.qualityMenu);
378
+ rightControls.appendChild(qualityWrapper);
379
+ // Fullscreen button
380
+ this.fullscreenBtn = this.createButton('ss-control-btn ss-fullscreen-btn', Icons.fullscreen, 'Fullscreen');
381
+ this.fullscreenBtn.addEventListener('click', () => this.options.onFullscreenToggle?.());
382
+ rightControls.appendChild(this.fullscreenBtn);
383
+ this.container.appendChild(leftControls);
384
+ this.container.appendChild(rightControls);
385
+ // Close quality menu when clicking outside
386
+ document.addEventListener('click', (e) => {
387
+ if (!qualityWrapper.contains(e.target)) {
388
+ this.qualityMenu.classList.remove('ss-open');
389
+ }
390
+ });
391
+ }
392
+ createButton(className, icon, title) {
393
+ const btn = document.createElement('button');
394
+ btn.className = className;
395
+ btn.innerHTML = icon;
396
+ btn.title = title;
397
+ return btn;
398
+ }
399
+ toggleQualityMenu() {
400
+ this.qualityMenu.classList.toggle('ss-open');
401
+ }
402
+ updateState(newState) {
403
+ this.state = { ...this.state, ...newState };
404
+ this.updateUI();
405
+ }
406
+ updateUI() {
407
+ // Update play button
408
+ this.playBtn.innerHTML = this.state.isPlaying ? Icons.pause : Icons.play;
409
+ this.playBtn.title = this.state.isPlaying ? 'Pause' : 'Play';
410
+ // Update mute button and volume
411
+ if (this.state.isMuted || this.state.volume === 0) {
412
+ this.muteBtn.innerHTML = Icons.volumeMute;
413
+ this.muteBtn.title = 'Unmute';
414
+ }
415
+ else if (this.state.volume < 0.5) {
416
+ this.muteBtn.innerHTML = Icons.volumeLow;
417
+ this.muteBtn.title = 'Mute';
418
+ }
419
+ else {
420
+ this.muteBtn.innerHTML = Icons.volumeHigh;
421
+ this.muteBtn.title = 'Mute';
422
+ }
423
+ this.volumeSlider.value = this.state.isMuted ? '0' : String(this.state.volume);
424
+ // Update fullscreen button
425
+ this.fullscreenBtn.innerHTML = this.state.isFullscreen ? Icons.fullscreenExit : Icons.fullscreen;
426
+ this.fullscreenBtn.title = this.state.isFullscreen ? 'Exit Fullscreen' : 'Fullscreen';
427
+ // Update quality button
428
+ this.qualityBtn.innerHTML = `${this.state.quality} ${Icons.chevronDown}`;
429
+ }
430
+ setAvailableQualities(qualities) {
431
+ this.state.availableQualities = qualities;
432
+ this.renderQualityMenu();
433
+ }
434
+ renderQualityMenu() {
435
+ this.qualityMenu.innerHTML = '';
436
+ this.state.availableQualities.forEach(quality => {
437
+ const option = document.createElement('button');
438
+ option.className = 'ss-quality-option';
439
+ if (quality === this.state.quality) {
440
+ option.classList.add('ss-active');
441
+ }
442
+ option.textContent = quality;
443
+ option.addEventListener('click', () => {
444
+ this.state.quality = quality;
445
+ this.options.onQualityChange?.(quality);
446
+ this.qualityMenu.classList.remove('ss-open');
447
+ this.updateUI();
448
+ this.renderQualityMenu();
449
+ });
450
+ this.qualityMenu.appendChild(option);
451
+ });
452
+ }
453
+ setEnabled(enabled) {
454
+ this.playBtn.disabled = !enabled;
455
+ this.muteBtn.disabled = !enabled;
456
+ this.volumeSlider.disabled = !enabled;
457
+ this.fullscreenBtn.disabled = !enabled;
458
+ this.qualityBtn.disabled = !enabled;
459
+ }
460
+ destroy() {
461
+ this.container.innerHTML = '';
462
+ }
463
+ }
464
+
465
+ /**
466
+ * Amazon IVS Player Wrapper
467
+ */
468
+ class IVSPlayerWrapper {
469
+ constructor(options) {
470
+ this.videoElement = null;
471
+ this.player = null;
472
+ this.isPlayerReady = false;
473
+ this.loadingOverlay = null;
474
+ this.errorOverlay = null;
475
+ this.currentPlaybackUrl = null;
476
+ this.options = options;
477
+ this.container = options.container;
478
+ this.init();
479
+ }
480
+ async init() {
481
+ this.showLoading('Initializing player...');
482
+ try {
483
+ // Check if IVS Player SDK is loaded
484
+ if (!window.IVSPlayer) {
485
+ // Try to load the IVS Player SDK dynamically
486
+ await this.loadIVSPlayerSDK();
487
+ }
488
+ if (!window.IVSPlayer?.isPlayerSupported) {
489
+ throw new Error('IVS Player is not supported in this browser');
490
+ }
491
+ this.createVideoElement();
492
+ this.initializePlayer();
493
+ }
494
+ catch (error) {
495
+ const errorMessage = error instanceof Error ? error.message : 'Failed to initialize player';
496
+ this.showError(errorMessage);
497
+ this.options.onError?.({ code: 'INIT_ERROR', message: errorMessage });
498
+ }
499
+ }
500
+ loadIVSPlayerSDK() {
501
+ return new Promise((resolve, reject) => {
502
+ // Check if already loaded
503
+ if (window.IVSPlayer) {
504
+ resolve();
505
+ return;
506
+ }
507
+ const script = document.createElement('script');
508
+ script.src = 'https://player.live-video.net/1.24.0/amazon-ivs-player.min.js';
509
+ script.async = true;
510
+ script.onload = () => {
511
+ if (window.IVSPlayer) {
512
+ resolve();
513
+ }
514
+ else {
515
+ reject(new Error('IVS Player SDK loaded but not available'));
516
+ }
517
+ };
518
+ script.onerror = () => reject(new Error('Failed to load IVS Player SDK'));
519
+ document.head.appendChild(script);
520
+ });
521
+ }
522
+ createVideoElement() {
523
+ this.videoElement = document.createElement('video');
524
+ this.videoElement.playsInline = true;
525
+ this.videoElement.muted = this.options.muted ?? false;
526
+ this.container.appendChild(this.videoElement);
527
+ }
528
+ initializePlayer() {
529
+ if (!window.IVSPlayer || !this.videoElement)
530
+ return;
531
+ const { create, PlayerState, PlayerEventType } = window.IVSPlayer;
532
+ this.player = create({
533
+ wasmWorker: 'https://player.live-video.net/1.24.0/amazon-ivs-wasmworker.min.js',
534
+ wasmBinary: 'https://player.live-video.net/1.24.0/amazon-ivs-wasmworker.min.wasm',
535
+ });
536
+ this.player.attachHTMLVideoElement(this.videoElement);
537
+ // Set initial volume
538
+ this.player.setVolume(this.options.volume ?? 1);
539
+ this.player.setMuted(this.options.muted ?? false);
540
+ // Event listeners
541
+ this.player.addEventListener(PlayerState.READY, () => {
542
+ this.isPlayerReady = true;
543
+ this.hideLoading();
544
+ this.hideError();
545
+ this.updateQualities();
546
+ this.options.onReady?.();
547
+ this.options.onStateChange?.({ isLoading: false });
548
+ });
549
+ this.player.addEventListener(PlayerState.PLAYING, () => {
550
+ this.hideLoading();
551
+ this.options.onStateChange?.({ isPlaying: true, isLoading: false });
552
+ });
553
+ this.player.addEventListener(PlayerState.ENDED, () => {
554
+ this.options.onStateChange?.({ isPlaying: false });
555
+ });
556
+ this.player.addEventListener(PlayerState.IDLE, () => {
557
+ this.options.onStateChange?.({ isPlaying: false });
558
+ });
559
+ this.player.addEventListener(PlayerState.BUFFERING, () => {
560
+ this.showLoading('Buffering...');
561
+ this.options.onStateChange?.({ isLoading: true });
562
+ });
563
+ this.player.addEventListener(PlayerEventType.ERROR, (error) => {
564
+ const errorMessage = error?.message || 'Playback error';
565
+ this.showError(errorMessage);
566
+ this.options.onError?.({ code: 'PLAYBACK_ERROR', message: errorMessage });
567
+ });
568
+ this.player.addEventListener(PlayerEventType.QUALITY_CHANGED, () => {
569
+ const quality = this.player.getQuality();
570
+ this.options.onStateChange?.({ quality: quality?.name || 'auto' });
571
+ });
572
+ }
573
+ async load(playbackUrl) {
574
+ this.currentPlaybackUrl = playbackUrl;
575
+ this.showLoading('Loading stream...');
576
+ if (!this.player) {
577
+ // Wait for player to initialize
578
+ await new Promise((resolve) => {
579
+ const checkPlayer = setInterval(() => {
580
+ if (this.player) {
581
+ clearInterval(checkPlayer);
582
+ resolve();
583
+ }
584
+ }, 100);
585
+ // Timeout after 10 seconds
586
+ setTimeout(() => {
587
+ clearInterval(checkPlayer);
588
+ resolve();
589
+ }, 10000);
590
+ });
591
+ }
592
+ if (!this.player) {
593
+ this.showError('Player not initialized');
594
+ return;
595
+ }
596
+ try {
597
+ this.player.load(playbackUrl);
598
+ if (this.options.autoPlay) {
599
+ await this.play();
600
+ }
601
+ }
602
+ catch (error) {
603
+ const errorMessage = error instanceof Error ? error.message : 'Failed to load stream';
604
+ this.showError(errorMessage);
605
+ this.options.onError?.({ code: 'LOAD_ERROR', message: errorMessage });
606
+ }
607
+ }
608
+ async play() {
609
+ if (!this.player)
610
+ return;
611
+ try {
612
+ await this.player.play();
613
+ }
614
+ catch (error) {
615
+ // Auto-play might be blocked, try muted
616
+ if (this.player) {
617
+ this.player.setMuted(true);
618
+ await this.player.play();
619
+ this.options.onStateChange?.({ isMuted: true });
620
+ }
621
+ }
622
+ }
623
+ pause() {
624
+ if (!this.player)
625
+ return;
626
+ this.player.pause();
627
+ this.options.onStateChange?.({ isPlaying: false });
628
+ }
629
+ togglePlayPause() {
630
+ if (!this.player)
631
+ return;
632
+ if (this.player.isPaused()) {
633
+ this.play();
634
+ }
635
+ else {
636
+ this.pause();
637
+ }
638
+ }
639
+ setVolume(volume) {
640
+ if (!this.player)
641
+ return;
642
+ this.player.setVolume(Math.max(0, Math.min(1, volume)));
643
+ this.options.onStateChange?.({ volume, isMuted: volume === 0 });
644
+ }
645
+ getVolume() {
646
+ return this.player?.getVolume() ?? 1;
647
+ }
648
+ setMuted(muted) {
649
+ if (!this.player)
650
+ return;
651
+ this.player.setMuted(muted);
652
+ this.options.onStateChange?.({ isMuted: muted });
653
+ }
654
+ toggleMute() {
655
+ if (!this.player)
656
+ return;
657
+ const isMuted = this.player.isMuted();
658
+ this.setMuted(!isMuted);
659
+ }
660
+ setQuality(quality) {
661
+ if (!this.player)
662
+ return;
663
+ const qualities = this.player.getQualities();
664
+ const selectedQuality = qualities.find((q) => q.name === quality);
665
+ if (selectedQuality) {
666
+ this.player.setQuality(selectedQuality);
667
+ }
668
+ else if (quality === 'auto' || quality === 'Auto') {
669
+ this.player.setAutoQualityMode(true);
670
+ }
671
+ }
672
+ getQualities() {
673
+ if (!this.player)
674
+ return ['Auto'];
675
+ const qualities = this.player.getQualities();
676
+ return ['Auto', ...qualities.map((q) => q.name)];
677
+ }
678
+ updateQualities() {
679
+ const qualities = this.getQualities();
680
+ this.options.onStateChange?.({ availableQualities: qualities });
681
+ }
682
+ async enterFullscreen() {
683
+ try {
684
+ if (this.container.requestFullscreen) {
685
+ await this.container.requestFullscreen();
686
+ }
687
+ else if (this.container.webkitRequestFullscreen) {
688
+ await this.container.webkitRequestFullscreen();
689
+ }
690
+ this.options.onStateChange?.({ isFullscreen: true });
691
+ }
692
+ catch (error) {
693
+ console.warn('Fullscreen not supported or denied');
694
+ }
695
+ }
696
+ async exitFullscreen() {
697
+ try {
698
+ if (document.exitFullscreen) {
699
+ await document.exitFullscreen();
700
+ }
701
+ else if (document.webkitExitFullscreen) {
702
+ await document.webkitExitFullscreen();
703
+ }
704
+ this.options.onStateChange?.({ isFullscreen: false });
705
+ }
706
+ catch (error) {
707
+ console.warn('Exit fullscreen failed');
708
+ }
709
+ }
710
+ toggleFullscreen() {
711
+ if (document.fullscreenElement) {
712
+ this.exitFullscreen();
713
+ }
714
+ else {
715
+ this.enterFullscreen();
716
+ }
717
+ }
718
+ isPlaying() {
719
+ return this.player ? !this.player.isPaused() : false;
720
+ }
721
+ isMuted() {
722
+ return this.player?.isMuted() ?? false;
723
+ }
724
+ showLoading(text = 'Loading...') {
725
+ this.hideError();
726
+ if (!this.loadingOverlay) {
727
+ this.loadingOverlay = document.createElement('div');
728
+ this.loadingOverlay.className = 'ss-loading';
729
+ this.container.appendChild(this.loadingOverlay);
730
+ }
731
+ this.loadingOverlay.innerHTML = `
732
+ <div class="ss-spinner"></div>
733
+ <div class="ss-loading-text">${text}</div>
734
+ `;
735
+ this.loadingOverlay.style.display = 'flex';
736
+ }
737
+ hideLoading() {
738
+ if (this.loadingOverlay) {
739
+ this.loadingOverlay.style.display = 'none';
740
+ }
741
+ }
742
+ showError(message) {
743
+ this.hideLoading();
744
+ if (!this.errorOverlay) {
745
+ this.errorOverlay = document.createElement('div');
746
+ this.errorOverlay.className = 'ss-error';
747
+ this.container.appendChild(this.errorOverlay);
748
+ }
749
+ this.errorOverlay.innerHTML = `
750
+ <div class="ss-error-icon">${Icons.error}</div>
751
+ <div class="ss-error-message">${message}</div>
752
+ <button class="ss-error-retry">Retry</button>
753
+ `;
754
+ this.errorOverlay.style.display = 'flex';
755
+ const retryBtn = this.errorOverlay.querySelector('.ss-error-retry');
756
+ retryBtn?.addEventListener('click', () => {
757
+ if (this.currentPlaybackUrl) {
758
+ this.load(this.currentPlaybackUrl);
759
+ }
760
+ });
761
+ }
762
+ hideError() {
763
+ if (this.errorOverlay) {
764
+ this.errorOverlay.style.display = 'none';
765
+ }
766
+ }
767
+ showNoStream() {
768
+ this.hideLoading();
769
+ this.hideError();
770
+ const noStreamOverlay = document.createElement('div');
771
+ noStreamOverlay.className = 'ss-no-stream';
772
+ noStreamOverlay.innerHTML = `
773
+ <div class="ss-no-stream-icon">${Icons.video}</div>
774
+ <div class="ss-no-stream-text">No stream available</div>
775
+ <div class="ss-no-stream-subtext">Stream has not started yet</div>
776
+ `;
777
+ this.container.appendChild(noStreamOverlay);
778
+ }
779
+ destroy() {
780
+ if (this.player) {
781
+ this.player.pause();
782
+ this.player.delete();
783
+ this.player = null;
784
+ }
785
+ if (this.videoElement) {
786
+ this.videoElement.remove();
787
+ this.videoElement = null;
788
+ }
789
+ this.loadingOverlay?.remove();
790
+ this.errorOverlay?.remove();
791
+ }
792
+ }
793
+
794
+ /**
795
+ * StreamSlice Widget Styles
796
+ */
797
+ const STYLES = `
798
+ /* StreamSlice Widget Styles */
799
+ .ss-widget {
800
+ --ss-primary: #6366f1;
801
+ --ss-primary-hover: #4f46e5;
802
+ --ss-bg: #1a1a2e;
803
+ --ss-bg-secondary: #16213e;
804
+ --ss-text: #ffffff;
805
+ --ss-text-secondary: #a0aec0;
806
+ --ss-border: #2d3748;
807
+ --ss-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
808
+ --ss-radius: 12px;
809
+ --ss-transition: all 0.2s ease;
810
+
811
+ position: fixed;
812
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
813
+ background: var(--ss-bg);
814
+ border-radius: var(--ss-radius);
815
+ box-shadow: var(--ss-shadow);
816
+ overflow: hidden;
817
+ user-select: none;
818
+ display: flex;
819
+ flex-direction: column;
820
+ }
821
+
822
+ .ss-widget.ss-theme-light {
823
+ --ss-bg: #ffffff;
824
+ --ss-bg-secondary: #f7fafc;
825
+ --ss-text: #1a202c;
826
+ --ss-text-secondary: #718096;
827
+ --ss-border: #e2e8f0;
828
+ --ss-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.15);
829
+ }
830
+
831
+ /* Header / Drag Handle */
832
+ .ss-header {
833
+ display: flex;
834
+ align-items: center;
835
+ justify-content: space-between;
836
+ padding: 8px 12px;
837
+ background: var(--ss-bg-secondary);
838
+ cursor: grab;
839
+ border-bottom: 1px solid var(--ss-border);
840
+ }
841
+
842
+ .ss-header:active {
843
+ cursor: grabbing;
844
+ }
845
+
846
+ .ss-header-title {
847
+ display: flex;
848
+ align-items: center;
849
+ gap: 8px;
850
+ color: var(--ss-text);
851
+ font-size: 13px;
852
+ font-weight: 600;
853
+ }
854
+
855
+ .ss-live-badge {
856
+ display: inline-flex;
857
+ align-items: center;
858
+ gap: 4px;
859
+ padding: 2px 8px;
860
+ background: #ef4444;
861
+ color: white;
862
+ font-size: 10px;
863
+ font-weight: 700;
864
+ text-transform: uppercase;
865
+ border-radius: 4px;
866
+ animation: ss-pulse 2s infinite;
867
+ }
868
+
869
+ @keyframes ss-pulse {
870
+ 0%, 100% { opacity: 1; }
871
+ 50% { opacity: 0.7; }
872
+ }
873
+
874
+ .ss-live-dot {
875
+ width: 6px;
876
+ height: 6px;
877
+ background: white;
878
+ border-radius: 50%;
879
+ }
880
+
881
+ .ss-header-actions {
882
+ display: flex;
883
+ align-items: center;
884
+ gap: 4px;
885
+ }
886
+
887
+ .ss-btn {
888
+ display: flex;
889
+ align-items: center;
890
+ justify-content: center;
891
+ width: 28px;
892
+ height: 28px;
893
+ padding: 0;
894
+ background: transparent;
895
+ border: none;
896
+ border-radius: 6px;
897
+ color: var(--ss-text-secondary);
898
+ cursor: pointer;
899
+ transition: var(--ss-transition);
900
+ }
901
+
902
+ .ss-btn:hover {
903
+ background: var(--ss-border);
904
+ color: var(--ss-text);
905
+ }
906
+
907
+ .ss-btn-close:hover {
908
+ background: #ef4444;
909
+ color: white;
910
+ }
911
+
912
+ /* Video Container */
913
+ .ss-video-container {
914
+ position: relative;
915
+ flex: 1;
916
+ min-height: 0;
917
+ background: #000;
918
+ overflow: hidden;
919
+ }
920
+
921
+ .ss-video-container video {
922
+ width: 100%;
923
+ height: 100%;
924
+ object-fit: contain;
925
+ }
926
+
927
+ /* Loading Overlay */
928
+ .ss-loading {
929
+ position: absolute;
930
+ inset: 0;
931
+ display: flex;
932
+ flex-direction: column;
933
+ align-items: center;
934
+ justify-content: center;
935
+ background: rgba(0, 0, 0, 0.8);
936
+ color: var(--ss-text);
937
+ gap: 12px;
938
+ }
939
+
940
+ .ss-spinner {
941
+ width: 40px;
942
+ height: 40px;
943
+ border: 3px solid var(--ss-border);
944
+ border-top-color: var(--ss-primary);
945
+ border-radius: 50%;
946
+ animation: ss-spin 1s linear infinite;
947
+ }
948
+
949
+ @keyframes ss-spin {
950
+ to { transform: rotate(360deg); }
951
+ }
952
+
953
+ .ss-loading-text {
954
+ font-size: 13px;
955
+ color: var(--ss-text-secondary);
956
+ }
957
+
958
+ /* Error Overlay */
959
+ .ss-error {
960
+ position: absolute;
961
+ inset: 0;
962
+ display: flex;
963
+ flex-direction: column;
964
+ align-items: center;
965
+ justify-content: center;
966
+ background: rgba(0, 0, 0, 0.9);
967
+ color: var(--ss-text);
968
+ padding: 20px;
969
+ text-align: center;
970
+ gap: 12px;
971
+ }
972
+
973
+ .ss-error-icon {
974
+ width: 48px;
975
+ height: 48px;
976
+ color: #ef4444;
977
+ }
978
+
979
+ .ss-error-message {
980
+ font-size: 14px;
981
+ color: var(--ss-text-secondary);
982
+ max-width: 280px;
983
+ }
984
+
985
+ .ss-error-retry {
986
+ padding: 8px 16px;
987
+ background: var(--ss-primary);
988
+ color: white;
989
+ border: none;
990
+ border-radius: 6px;
991
+ font-size: 13px;
992
+ font-weight: 500;
993
+ cursor: pointer;
994
+ transition: var(--ss-transition);
995
+ }
996
+
997
+ .ss-error-retry:hover {
998
+ background: var(--ss-primary-hover);
999
+ }
1000
+
1001
+ /* Player Controls */
1002
+ .ss-controls {
1003
+ display: flex;
1004
+ align-items: center;
1005
+ gap: 8px;
1006
+ padding: 10px 12px;
1007
+ background: var(--ss-bg-secondary);
1008
+ border-top: 1px solid var(--ss-border);
1009
+ }
1010
+
1011
+ .ss-controls-left {
1012
+ display: flex;
1013
+ align-items: center;
1014
+ gap: 8px;
1015
+ }
1016
+
1017
+ .ss-controls-right {
1018
+ display: flex;
1019
+ align-items: center;
1020
+ gap: 8px;
1021
+ margin-left: auto;
1022
+ }
1023
+
1024
+ .ss-control-btn {
1025
+ display: flex;
1026
+ align-items: center;
1027
+ justify-content: center;
1028
+ width: 32px;
1029
+ height: 32px;
1030
+ padding: 0;
1031
+ background: transparent;
1032
+ border: none;
1033
+ border-radius: 6px;
1034
+ color: var(--ss-text);
1035
+ cursor: pointer;
1036
+ transition: var(--ss-transition);
1037
+ }
1038
+
1039
+ .ss-control-btn:hover {
1040
+ background: var(--ss-border);
1041
+ }
1042
+
1043
+ .ss-control-btn:disabled {
1044
+ opacity: 0.5;
1045
+ cursor: not-allowed;
1046
+ }
1047
+
1048
+ .ss-control-btn svg {
1049
+ width: 18px;
1050
+ height: 18px;
1051
+ }
1052
+
1053
+ /* Volume Control */
1054
+ .ss-volume-wrapper {
1055
+ display: flex;
1056
+ align-items: center;
1057
+ gap: 4px;
1058
+ }
1059
+
1060
+ .ss-volume-slider {
1061
+ width: 60px;
1062
+ height: 4px;
1063
+ -webkit-appearance: none;
1064
+ appearance: none;
1065
+ background: var(--ss-border);
1066
+ border-radius: 2px;
1067
+ cursor: pointer;
1068
+ transition: var(--ss-transition);
1069
+ }
1070
+
1071
+ .ss-volume-slider::-webkit-slider-thumb {
1072
+ -webkit-appearance: none;
1073
+ width: 12px;
1074
+ height: 12px;
1075
+ background: var(--ss-primary);
1076
+ border-radius: 50%;
1077
+ cursor: pointer;
1078
+ transition: var(--ss-transition);
1079
+ }
1080
+
1081
+ .ss-volume-slider::-webkit-slider-thumb:hover {
1082
+ transform: scale(1.2);
1083
+ }
1084
+
1085
+ .ss-volume-slider::-moz-range-thumb {
1086
+ width: 12px;
1087
+ height: 12px;
1088
+ background: var(--ss-primary);
1089
+ border: none;
1090
+ border-radius: 50%;
1091
+ cursor: pointer;
1092
+ }
1093
+
1094
+ /* Quality Selector */
1095
+ .ss-quality-wrapper {
1096
+ position: relative;
1097
+ }
1098
+
1099
+ .ss-quality-btn {
1100
+ display: flex;
1101
+ align-items: center;
1102
+ gap: 4px;
1103
+ padding: 4px 8px;
1104
+ background: var(--ss-border);
1105
+ border: none;
1106
+ border-radius: 4px;
1107
+ color: var(--ss-text);
1108
+ font-size: 11px;
1109
+ font-weight: 500;
1110
+ cursor: pointer;
1111
+ transition: var(--ss-transition);
1112
+ }
1113
+
1114
+ .ss-quality-btn:hover {
1115
+ background: var(--ss-primary);
1116
+ }
1117
+
1118
+ .ss-quality-menu {
1119
+ position: absolute;
1120
+ bottom: 100%;
1121
+ right: 0;
1122
+ margin-bottom: 4px;
1123
+ min-width: 100px;
1124
+ background: var(--ss-bg);
1125
+ border: 1px solid var(--ss-border);
1126
+ border-radius: 6px;
1127
+ box-shadow: var(--ss-shadow);
1128
+ overflow: hidden;
1129
+ opacity: 0;
1130
+ visibility: hidden;
1131
+ transform: translateY(8px);
1132
+ transition: var(--ss-transition);
1133
+ }
1134
+
1135
+ .ss-quality-menu.ss-open {
1136
+ opacity: 1;
1137
+ visibility: visible;
1138
+ transform: translateY(0);
1139
+ }
1140
+
1141
+ .ss-quality-option {
1142
+ display: block;
1143
+ width: 100%;
1144
+ padding: 8px 12px;
1145
+ background: transparent;
1146
+ border: none;
1147
+ color: var(--ss-text);
1148
+ font-size: 12px;
1149
+ text-align: left;
1150
+ cursor: pointer;
1151
+ transition: var(--ss-transition);
1152
+ }
1153
+
1154
+ .ss-quality-option:hover {
1155
+ background: var(--ss-border);
1156
+ }
1157
+
1158
+ .ss-quality-option.ss-active {
1159
+ color: var(--ss-primary);
1160
+ font-weight: 600;
1161
+ }
1162
+
1163
+ /* Resize Handles */
1164
+ .ss-resize-handle {
1165
+ position: absolute;
1166
+ background: transparent;
1167
+ z-index: 10;
1168
+ }
1169
+
1170
+ .ss-resize-n {
1171
+ top: 0;
1172
+ left: 10px;
1173
+ right: 10px;
1174
+ height: 6px;
1175
+ cursor: n-resize;
1176
+ }
1177
+
1178
+ .ss-resize-s {
1179
+ bottom: 0;
1180
+ left: 10px;
1181
+ right: 10px;
1182
+ height: 6px;
1183
+ cursor: s-resize;
1184
+ }
1185
+
1186
+ .ss-resize-e {
1187
+ top: 10px;
1188
+ right: 0;
1189
+ bottom: 10px;
1190
+ width: 6px;
1191
+ cursor: e-resize;
1192
+ }
1193
+
1194
+ .ss-resize-w {
1195
+ top: 10px;
1196
+ left: 0;
1197
+ bottom: 10px;
1198
+ width: 6px;
1199
+ cursor: w-resize;
1200
+ }
1201
+
1202
+ .ss-resize-ne {
1203
+ top: 0;
1204
+ right: 0;
1205
+ width: 12px;
1206
+ height: 12px;
1207
+ cursor: ne-resize;
1208
+ }
1209
+
1210
+ .ss-resize-nw {
1211
+ top: 0;
1212
+ left: 0;
1213
+ width: 12px;
1214
+ height: 12px;
1215
+ cursor: nw-resize;
1216
+ }
1217
+
1218
+ .ss-resize-se {
1219
+ bottom: 0;
1220
+ right: 0;
1221
+ width: 12px;
1222
+ height: 12px;
1223
+ cursor: se-resize;
1224
+ }
1225
+
1226
+ .ss-resize-sw {
1227
+ bottom: 0;
1228
+ left: 0;
1229
+ width: 12px;
1230
+ height: 12px;
1231
+ cursor: sw-resize;
1232
+ }
1233
+
1234
+ /* Animations */
1235
+ .ss-widget.ss-entering {
1236
+ animation: ss-enter 0.3s ease-out;
1237
+ }
1238
+
1239
+ .ss-widget.ss-leaving {
1240
+ animation: ss-leave 0.2s ease-in forwards;
1241
+ }
1242
+
1243
+ @keyframes ss-enter {
1244
+ from {
1245
+ opacity: 0;
1246
+ transform: scale(0.9) translateY(20px);
1247
+ }
1248
+ to {
1249
+ opacity: 1;
1250
+ transform: scale(1) translateY(0);
1251
+ }
1252
+ }
1253
+
1254
+ @keyframes ss-leave {
1255
+ from {
1256
+ opacity: 1;
1257
+ transform: scale(1);
1258
+ }
1259
+ to {
1260
+ opacity: 0;
1261
+ transform: scale(0.9);
1262
+ }
1263
+ }
1264
+
1265
+ /* No Stream State */
1266
+ .ss-no-stream {
1267
+ position: absolute;
1268
+ inset: 0;
1269
+ display: flex;
1270
+ flex-direction: column;
1271
+ align-items: center;
1272
+ justify-content: center;
1273
+ background: var(--ss-bg);
1274
+ color: var(--ss-text);
1275
+ padding: 20px;
1276
+ text-align: center;
1277
+ gap: 8px;
1278
+ }
1279
+
1280
+ .ss-no-stream-icon {
1281
+ width: 48px;
1282
+ height: 48px;
1283
+ color: var(--ss-text-secondary);
1284
+ opacity: 0.5;
1285
+ }
1286
+
1287
+ .ss-no-stream-text {
1288
+ font-size: 14px;
1289
+ font-weight: 500;
1290
+ }
1291
+
1292
+ .ss-no-stream-subtext {
1293
+ font-size: 12px;
1294
+ color: var(--ss-text-secondary);
1295
+ }
1296
+
1297
+ /* Responsive adjustments */
1298
+ @media (max-width: 480px) {
1299
+ .ss-widget {
1300
+ --ss-radius: 0;
1301
+ }
1302
+ }
1303
+ `;
1304
+ function injectStyles() {
1305
+ if (document.getElementById('streamslice-styles')) {
1306
+ return;
1307
+ }
1308
+ const styleElement = document.createElement('style');
1309
+ styleElement.id = 'streamslice-styles';
1310
+ styleElement.textContent = STYLES;
1311
+ document.head.appendChild(styleElement);
1312
+ }
1313
+ function removeStyles() {
1314
+ const styleElement = document.getElementById('streamslice-styles');
1315
+ if (styleElement) {
1316
+ styleElement.remove();
1317
+ }
1318
+ }
1319
+
1320
+ /**
1321
+ * StreamSlice - Main Widget Class
1322
+ */
1323
+ const DEFAULT_CONFIG = {
1324
+ position: { x: 20, y: 20 },
1325
+ size: { width: 400, height: 280 },
1326
+ minSize: { width: 320, height: 220 },
1327
+ maxSize: { width: 800, height: 600 },
1328
+ autoPlay: true,
1329
+ muted: false,
1330
+ volume: 1,
1331
+ showControls: true,
1332
+ zIndex: 999999,
1333
+ theme: 'dark',
1334
+ };
1335
+ class StreamSlice {
1336
+ constructor(config) {
1337
+ this.floatingWindow = null;
1338
+ this.playerControls = null;
1339
+ this.player = null;
1340
+ this.isInitialized = false;
1341
+ this.playlistUrl = null;
1342
+ this.playerState = {
1343
+ isPlaying: false,
1344
+ isMuted: false,
1345
+ volume: 1,
1346
+ isFullscreen: false,
1347
+ isLoading: true,
1348
+ duration: 0,
1349
+ currentTime: 0,
1350
+ isLive: true,
1351
+ quality: 'Auto',
1352
+ availableQualities: ['Auto'],
1353
+ };
1354
+ if (!config.apiUrl) {
1355
+ throw new Error('StreamSlice: apiUrl is required');
1356
+ }
1357
+ this.config = {
1358
+ ...DEFAULT_CONFIG,
1359
+ ...config,
1360
+ onReady: config.onReady,
1361
+ onPlay: config.onPlay,
1362
+ onPause: config.onPause,
1363
+ onError: config.onError,
1364
+ onClose: config.onClose,
1365
+ onResize: config.onResize,
1366
+ onMove: config.onMove,
1367
+ className: config.className,
1368
+ };
1369
+ this.apiClient = new ApiClient(config.apiUrl);
1370
+ // Set initial player state from config
1371
+ this.playerState.volume = this.config.volume;
1372
+ this.playerState.isMuted = this.config.muted;
1373
+ }
1374
+ /**
1375
+ * Initialize and show the widget
1376
+ */
1377
+ async init() {
1378
+ if (this.isInitialized) {
1379
+ console.warn('StreamSlice: Widget is already initialized');
1380
+ return;
1381
+ }
1382
+ // Inject styles
1383
+ injectStyles();
1384
+ // Get current page URL
1385
+ const pageUrl = window.location.href;
1386
+ // Fetch playlist from API
1387
+ try {
1388
+ const response = await this.apiClient.getPlaylist(pageUrl);
1389
+ if (response.error) {
1390
+ this.config.onError?.({
1391
+ code: response.error.code,
1392
+ message: response.error.error_message_message,
1393
+ });
1394
+ // Still show the widget but with error state
1395
+ this.createWidget();
1396
+ this.showNoStreamState();
1397
+ return;
1398
+ }
1399
+ if (response.data?.link) {
1400
+ this.playlistUrl = response.data.link;
1401
+ }
1402
+ this.createWidget();
1403
+ if (this.playlistUrl) {
1404
+ await this.loadStream(this.playlistUrl);
1405
+ }
1406
+ else {
1407
+ this.showNoStreamState();
1408
+ }
1409
+ }
1410
+ catch (error) {
1411
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1412
+ this.config.onError?.({
1413
+ code: 'INIT_ERROR',
1414
+ message: errorMessage,
1415
+ });
1416
+ // Show widget with error
1417
+ this.createWidget();
1418
+ this.showNoStreamState();
1419
+ }
1420
+ this.isInitialized = true;
1421
+ }
1422
+ createWidget() {
1423
+ // Create floating window
1424
+ this.floatingWindow = new FloatingWindow({
1425
+ position: this.config.position,
1426
+ size: this.config.size,
1427
+ minSize: this.config.minSize,
1428
+ maxSize: this.config.maxSize,
1429
+ zIndex: this.config.zIndex,
1430
+ theme: this.config.theme,
1431
+ className: this.config.className,
1432
+ onClose: () => this.handleClose(),
1433
+ onResize: (size) => this.config.onResize?.(size),
1434
+ onMove: (position) => this.config.onMove?.(position),
1435
+ });
1436
+ // Create player controls
1437
+ if (this.config.showControls) {
1438
+ this.playerControls = new PlayerControls(this.floatingWindow.getControlsContainer(), {
1439
+ onPlayPause: () => this.togglePlayPause(),
1440
+ onMuteToggle: () => this.toggleMute(),
1441
+ onVolumeChange: (volume) => this.setVolume(volume),
1442
+ onFullscreenToggle: () => this.toggleFullscreen(),
1443
+ onQualityChange: (quality) => this.setQuality(quality),
1444
+ });
1445
+ // Update controls with initial state
1446
+ this.playerControls.updateState(this.playerState);
1447
+ this.playerControls.setEnabled(false); // Disabled until stream loads
1448
+ }
1449
+ }
1450
+ async loadStream(url) {
1451
+ if (!this.floatingWindow)
1452
+ return;
1453
+ // Create IVS player
1454
+ this.player = new IVSPlayerWrapper({
1455
+ container: this.floatingWindow.getVideoContainer(),
1456
+ autoPlay: this.config.autoPlay,
1457
+ muted: this.config.muted,
1458
+ volume: this.config.volume,
1459
+ onStateChange: (state) => this.handlePlayerStateChange(state),
1460
+ onError: (error) => this.config.onError?.(error),
1461
+ onReady: () => {
1462
+ this.playerControls?.setEnabled(true);
1463
+ this.config.onReady?.();
1464
+ },
1465
+ });
1466
+ // Load the stream
1467
+ await this.player.load(url);
1468
+ }
1469
+ showNoStreamState() {
1470
+ if (!this.floatingWindow)
1471
+ return;
1472
+ this.floatingWindow.showLiveBadge(false);
1473
+ // Show no stream message in video container
1474
+ const videoContainer = this.floatingWindow.getVideoContainer();
1475
+ videoContainer.innerHTML = `
1476
+ <div class="ss-no-stream">
1477
+ <svg class="ss-no-stream-icon" viewBox="0 0 24 24" fill="currentColor">
1478
+ <path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/>
1479
+ </svg>
1480
+ <div class="ss-no-stream-text">No stream available</div>
1481
+ <div class="ss-no-stream-subtext">Stream is not active for this page</div>
1482
+ </div>
1483
+ `;
1484
+ }
1485
+ handlePlayerStateChange(state) {
1486
+ this.playerState = { ...this.playerState, ...state };
1487
+ // Update controls
1488
+ this.playerControls?.updateState(this.playerState);
1489
+ // Update live badge
1490
+ if (state.isLive !== undefined) {
1491
+ this.floatingWindow?.showLiveBadge(state.isLive);
1492
+ }
1493
+ // Update available qualities
1494
+ if (state.availableQualities) {
1495
+ this.playerControls?.setAvailableQualities(state.availableQualities);
1496
+ }
1497
+ // Trigger callbacks
1498
+ if (state.isPlaying !== undefined) {
1499
+ if (state.isPlaying) {
1500
+ this.config.onPlay?.();
1501
+ }
1502
+ else {
1503
+ this.config.onPause?.();
1504
+ }
1505
+ }
1506
+ }
1507
+ handleClose() {
1508
+ this.destroy();
1509
+ this.config.onClose?.();
1510
+ }
1511
+ /**
1512
+ * Play the stream
1513
+ */
1514
+ play() {
1515
+ this.player?.play();
1516
+ }
1517
+ /**
1518
+ * Pause the stream
1519
+ */
1520
+ pause() {
1521
+ this.player?.pause();
1522
+ }
1523
+ /**
1524
+ * Toggle play/pause
1525
+ */
1526
+ togglePlayPause() {
1527
+ this.player?.togglePlayPause();
1528
+ }
1529
+ /**
1530
+ * Set volume (0-1)
1531
+ */
1532
+ setVolume(volume) {
1533
+ this.player?.setVolume(volume);
1534
+ this.playerState.volume = volume;
1535
+ this.playerControls?.updateState({ volume });
1536
+ }
1537
+ /**
1538
+ * Get current volume
1539
+ */
1540
+ getVolume() {
1541
+ return this.player?.getVolume() ?? this.playerState.volume;
1542
+ }
1543
+ /**
1544
+ * Mute the player
1545
+ */
1546
+ mute() {
1547
+ this.player?.setMuted(true);
1548
+ }
1549
+ /**
1550
+ * Unmute the player
1551
+ */
1552
+ unmute() {
1553
+ this.player?.setMuted(false);
1554
+ }
1555
+ /**
1556
+ * Toggle mute
1557
+ */
1558
+ toggleMute() {
1559
+ this.player?.toggleMute();
1560
+ }
1561
+ /**
1562
+ * Set quality
1563
+ */
1564
+ setQuality(quality) {
1565
+ this.player?.setQuality(quality);
1566
+ this.playerState.quality = quality;
1567
+ this.playerControls?.updateState({ quality });
1568
+ }
1569
+ /**
1570
+ * Get available qualities
1571
+ */
1572
+ getQualities() {
1573
+ return this.player?.getQualities() ?? ['Auto'];
1574
+ }
1575
+ /**
1576
+ * Toggle fullscreen
1577
+ */
1578
+ toggleFullscreen() {
1579
+ this.player?.toggleFullscreen();
1580
+ }
1581
+ /**
1582
+ * Set window position
1583
+ */
1584
+ setPosition(position) {
1585
+ this.floatingWindow?.setPosition(position);
1586
+ }
1587
+ /**
1588
+ * Set window size
1589
+ */
1590
+ setSize(size) {
1591
+ this.floatingWindow?.setSize(size);
1592
+ }
1593
+ /**
1594
+ * Check if widget is initialized
1595
+ */
1596
+ isReady() {
1597
+ return this.isInitialized;
1598
+ }
1599
+ /**
1600
+ * Get current player state
1601
+ */
1602
+ getState() {
1603
+ return { ...this.playerState };
1604
+ }
1605
+ /**
1606
+ * Show the widget
1607
+ */
1608
+ show() {
1609
+ if (this.floatingWindow) {
1610
+ this.floatingWindow.getContainer().style.display = 'flex';
1611
+ }
1612
+ }
1613
+ /**
1614
+ * Hide the widget
1615
+ */
1616
+ hide() {
1617
+ if (this.floatingWindow) {
1618
+ this.floatingWindow.getContainer().style.display = 'none';
1619
+ }
1620
+ }
1621
+ /**
1622
+ * Close and destroy the widget
1623
+ */
1624
+ close() {
1625
+ this.floatingWindow?.close();
1626
+ }
1627
+ /**
1628
+ * Destroy the widget and clean up resources
1629
+ */
1630
+ destroy() {
1631
+ this.player?.destroy();
1632
+ this.playerControls?.destroy();
1633
+ this.floatingWindow?.destroy();
1634
+ this.player = null;
1635
+ this.playerControls = null;
1636
+ this.floatingWindow = null;
1637
+ this.isInitialized = false;
1638
+ // Note: We don't remove styles as other instances might use them
1639
+ }
1640
+ /**
1641
+ * Static method to remove all injected styles
1642
+ */
1643
+ static removeStyles() {
1644
+ removeStyles();
1645
+ }
1646
+ }
1647
+
1648
+ /**
1649
+ * StreamSlice Widget Library
1650
+ *
1651
+ * A floating video player widget with Amazon IVS support
1652
+ */
1653
+
1654
+ export { StreamSlice, StreamSlice as default };
1655
+ //# sourceMappingURL=streamslice.esm.js.map