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