@livepeer-frameworks/player-svelte 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/Player.svelte +12 -4
- package/dist/PlayerControls.svelte +51 -18
- package/dist/controls/SettingsMenu.svelte +7 -1
- package/dist/stores/playerController.d.ts +8 -0
- package/dist/stores/playerController.js +13 -0
- package/package.json +5 -5
- package/src/Player.svelte +12 -4
- package/src/PlayerControls.svelte +51 -18
- package/src/controls/SettingsMenu.svelte +7 -1
- package/src/stores/playerController.ts +24 -0
package/README.md
CHANGED
|
@@ -16,14 +16,15 @@ npm i @livepeer-frameworks/player-svelte
|
|
|
16
16
|
|
|
17
17
|
```svelte
|
|
18
18
|
<script lang="ts">
|
|
19
|
-
import { Player } from
|
|
19
|
+
import { Player } from "@livepeer-frameworks/player-svelte";
|
|
20
20
|
</script>
|
|
21
21
|
|
|
22
22
|
<div style="width: 100%; height: 500px;">
|
|
23
23
|
<Player
|
|
24
24
|
contentType="live"
|
|
25
|
-
contentId="pk_..."
|
|
26
|
-
|
|
25
|
+
contentId="pk_..."
|
|
26
|
+
// playbackId
|
|
27
|
+
options={{ gatewayUrl: "https://your-bridge/graphql" }}
|
|
27
28
|
/>
|
|
28
29
|
</div>
|
|
29
30
|
```
|
package/dist/Player.svelte
CHANGED
|
@@ -90,8 +90,12 @@
|
|
|
90
90
|
let isDevPanelOpen = $state(false);
|
|
91
91
|
let skipDirection: SkipDirection = $state(null);
|
|
92
92
|
|
|
93
|
-
let activeTheme = $state<FwThemePreset>(
|
|
94
|
-
let activeLocale = $state<FwLocale>(
|
|
93
|
+
let activeTheme = $state<FwThemePreset>("default");
|
|
94
|
+
let activeLocale = $state<FwLocale>("en");
|
|
95
|
+
$effect(() => {
|
|
96
|
+
if (options.theme) activeTheme = options.theme;
|
|
97
|
+
if (options.locale) activeLocale = options.locale;
|
|
98
|
+
});
|
|
95
99
|
|
|
96
100
|
// Sync locale state to i18n store and provide translator context
|
|
97
101
|
$effect(() => {
|
|
@@ -221,6 +225,10 @@
|
|
|
221
225
|
playbackQuality: null as any,
|
|
222
226
|
subtitlesEnabled: false,
|
|
223
227
|
toast: null as { message: string; timestamp: number } | null,
|
|
228
|
+
controllerSeekableStart: 0,
|
|
229
|
+
controllerLiveEdge: 0,
|
|
230
|
+
controllerCanSeek: false,
|
|
231
|
+
controllerHasAudio: true,
|
|
224
232
|
});
|
|
225
233
|
|
|
226
234
|
// Track if we've already attached to prevent double-attach race
|
|
@@ -667,8 +675,8 @@
|
|
|
667
675
|
onStatsToggle={() => (isStatsOpen = !isStatsOpen)}
|
|
668
676
|
isContentLive={storeState.isEffectivelyLive}
|
|
669
677
|
onJumpToLive={() => playerStore?.getController()?.jumpToLive()}
|
|
670
|
-
controllerSeekableStart={
|
|
671
|
-
controllerLiveEdge={
|
|
678
|
+
controllerSeekableStart={storeState?.controllerSeekableStart ?? 0}
|
|
679
|
+
controllerLiveEdge={storeState?.controllerLiveEdge ?? 0}
|
|
672
680
|
{activeLocale}
|
|
673
681
|
onLocaleChange={(l) => {
|
|
674
682
|
activeLocale = l;
|
|
@@ -149,40 +149,77 @@
|
|
|
149
149
|
|
|
150
150
|
// Audio detection: trust MistServer metadata first, then DOM fallback
|
|
151
151
|
$effect(() => {
|
|
152
|
-
// Primary: trust MistServer stream metadata (matches ddvtech embed approach)
|
|
153
|
-
if (mistStreamInfo?.hasAudio !== undefined) {
|
|
154
|
-
hasAudio = mistStreamInfo.hasAudio;
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
152
|
if (!video) {
|
|
159
153
|
hasAudio = true;
|
|
160
154
|
return;
|
|
161
155
|
}
|
|
156
|
+
const currentVideo = video;
|
|
157
|
+
|
|
158
|
+
let boundStream: MediaStream | null = null;
|
|
159
|
+
let streamListener: (() => void) | null = null;
|
|
160
|
+
|
|
161
|
+
const unbindStream = () => {
|
|
162
|
+
if (boundStream && streamListener) {
|
|
163
|
+
boundStream.removeEventListener("addtrack", streamListener);
|
|
164
|
+
boundStream.removeEventListener("removetrack", streamListener);
|
|
165
|
+
}
|
|
166
|
+
boundStream = null;
|
|
167
|
+
streamListener = null;
|
|
168
|
+
};
|
|
162
169
|
|
|
163
170
|
const checkAudio = () => {
|
|
164
|
-
|
|
165
|
-
|
|
171
|
+
// Prefer actual MediaStream tracks over metadata to avoid phantom controls.
|
|
172
|
+
if (currentVideo.srcObject instanceof MediaStream) {
|
|
173
|
+
const stream = currentVideo.srcObject;
|
|
174
|
+
if (stream !== boundStream) {
|
|
175
|
+
unbindStream();
|
|
176
|
+
boundStream = stream;
|
|
177
|
+
streamListener = () => {
|
|
178
|
+
hasAudio = stream.getAudioTracks().length > 0;
|
|
179
|
+
};
|
|
180
|
+
stream.addEventListener("addtrack", streamListener);
|
|
181
|
+
stream.addEventListener("removetrack", streamListener);
|
|
182
|
+
}
|
|
183
|
+
hasAudio = stream.getAudioTracks().length > 0;
|
|
166
184
|
return;
|
|
167
185
|
}
|
|
168
|
-
|
|
186
|
+
|
|
187
|
+
unbindStream();
|
|
188
|
+
|
|
189
|
+
// Fallback: metadata for non-MediaStream sources.
|
|
190
|
+
if (mistStreamInfo?.hasAudio !== undefined) {
|
|
191
|
+
hasAudio = mistStreamInfo.hasAudio;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const videoAny = currentVideo as any;
|
|
169
196
|
if (videoAny.audioTracks && videoAny.audioTracks.length !== undefined) {
|
|
170
197
|
hasAudio = videoAny.audioTracks.length > 0;
|
|
171
198
|
return;
|
|
172
199
|
}
|
|
200
|
+
|
|
173
201
|
hasAudio = true;
|
|
174
202
|
};
|
|
203
|
+
|
|
175
204
|
checkAudio();
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
205
|
+
currentVideo.addEventListener("loadedmetadata", checkAudio);
|
|
206
|
+
currentVideo.addEventListener("loadeddata", checkAudio);
|
|
207
|
+
currentVideo.addEventListener("durationchange", checkAudio);
|
|
208
|
+
|
|
209
|
+
const audioTracks = (currentVideo as any).audioTracks;
|
|
179
210
|
if (audioTracks?.addEventListener) {
|
|
180
211
|
audioTracks.addEventListener("addtrack", checkAudio);
|
|
212
|
+
audioTracks.addEventListener("removetrack", checkAudio);
|
|
181
213
|
}
|
|
214
|
+
|
|
182
215
|
return () => {
|
|
183
|
-
|
|
216
|
+
unbindStream();
|
|
217
|
+
currentVideo.removeEventListener("loadedmetadata", checkAudio);
|
|
218
|
+
currentVideo.removeEventListener("loadeddata", checkAudio);
|
|
219
|
+
currentVideo.removeEventListener("durationchange", checkAudio);
|
|
184
220
|
if (audioTracks?.removeEventListener) {
|
|
185
221
|
audioTracks.removeEventListener("addtrack", checkAudio);
|
|
222
|
+
audioTracks.removeEventListener("removetrack", checkAudio);
|
|
186
223
|
}
|
|
187
224
|
};
|
|
188
225
|
});
|
|
@@ -263,11 +300,7 @@
|
|
|
263
300
|
);
|
|
264
301
|
|
|
265
302
|
let allowMediaStreamDvr = $derived(
|
|
266
|
-
isMediaStreamSource(video) &&
|
|
267
|
-
bufferWindowMs !== undefined &&
|
|
268
|
-
bufferWindowMs > 0 &&
|
|
269
|
-
sourceType !== "whep" &&
|
|
270
|
-
sourceType !== "webrtc"
|
|
303
|
+
isMediaStreamSource(video) && bufferWindowMs !== undefined && bufferWindowMs > 0
|
|
271
304
|
);
|
|
272
305
|
|
|
273
306
|
// Seekable range: prefer controller-derived values (same pattern as React/WC)
|
|
@@ -101,7 +101,13 @@
|
|
|
101
101
|
|
|
102
102
|
{#if isOpen}
|
|
103
103
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
104
|
-
<div
|
|
104
|
+
<div
|
|
105
|
+
class="fw-settings-menu"
|
|
106
|
+
role="menu"
|
|
107
|
+
aria-label={t("settings")}
|
|
108
|
+
onkeydown={handleKeyDown}
|
|
109
|
+
tabindex="-1"
|
|
110
|
+
>
|
|
105
111
|
{#if showModeSelector && onModeChange}
|
|
106
112
|
<div class="fw-settings-section">
|
|
107
113
|
<div class="fw-settings-label">{t("mode")}</div>
|
|
@@ -78,6 +78,14 @@ export interface PlayerControllerState {
|
|
|
78
78
|
message: string;
|
|
79
79
|
timestamp: number;
|
|
80
80
|
} | null;
|
|
81
|
+
/** Controller-derived seekable start (ms) — reactively updated via seekingStateChange */
|
|
82
|
+
controllerSeekableStart: number;
|
|
83
|
+
/** Controller-derived live edge (ms) — reactively updated via seekingStateChange */
|
|
84
|
+
controllerLiveEdge: number;
|
|
85
|
+
/** Controller-derived canSeek — reactively updated via seekingStateChange */
|
|
86
|
+
controllerCanSeek: boolean;
|
|
87
|
+
/** Controller-derived hasAudio — reactively updated via seekingStateChange */
|
|
88
|
+
controllerHasAudio: boolean;
|
|
81
89
|
}
|
|
82
90
|
export interface PlayerControllerStore extends Readable<PlayerControllerState> {
|
|
83
91
|
/** Get controller instance */
|
|
@@ -38,6 +38,10 @@ const initialState = {
|
|
|
38
38
|
playbackQuality: null,
|
|
39
39
|
subtitlesEnabled: false,
|
|
40
40
|
toast: null,
|
|
41
|
+
controllerSeekableStart: 0,
|
|
42
|
+
controllerLiveEdge: 0,
|
|
43
|
+
controllerCanSeek: false,
|
|
44
|
+
controllerHasAudio: true,
|
|
41
45
|
};
|
|
42
46
|
// ============================================================================
|
|
43
47
|
// Store Factory
|
|
@@ -209,6 +213,15 @@ export function createPlayerControllerStore(config) {
|
|
|
209
213
|
unsubscribers.push(controller.on("captionsChange", ({ enabled }) => {
|
|
210
214
|
store.update((prev) => ({ ...prev, subtitlesEnabled: enabled }));
|
|
211
215
|
}));
|
|
216
|
+
unsubscribers.push(controller.on("seekingStateChange", (data) => {
|
|
217
|
+
store.update((prev) => ({
|
|
218
|
+
...prev,
|
|
219
|
+
controllerSeekableStart: data.seekableStart,
|
|
220
|
+
controllerLiveEdge: data.liveEdge,
|
|
221
|
+
controllerCanSeek: data.canSeek,
|
|
222
|
+
controllerHasAudio: data.hasAudio,
|
|
223
|
+
}));
|
|
224
|
+
}));
|
|
212
225
|
// Error handling events - show toasts/modals
|
|
213
226
|
unsubscribers.push(controller.on("protocolSwapped", (data) => {
|
|
214
227
|
const message = `Switched to ${data.toProtocol}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livepeer-frameworks/player-svelte",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Svelte 5 components for FrameWorks streaming player",
|
|
6
6
|
"svelte": "./dist/index.js",
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"./player.css": "./src/player.css"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"bits-ui": "^2.
|
|
25
|
-
"@livepeer-frameworks/player-core": "0.2.
|
|
24
|
+
"bits-ui": "^2.16.2",
|
|
25
|
+
"@livepeer-frameworks/player-core": "0.2.5"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"svelte": "^5.0.0"
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"@testing-library/svelte": "^5.3.1",
|
|
34
34
|
"@vitest/coverage-v8": "^4.0.18",
|
|
35
35
|
"jsdom": "^28.1.0",
|
|
36
|
-
"svelte": "^5.
|
|
37
|
-
"svelte-check": "^4.4.
|
|
36
|
+
"svelte": "^5.53.6",
|
|
37
|
+
"svelte-check": "^4.4.4",
|
|
38
38
|
"typescript": "^5.9.3",
|
|
39
39
|
"vite": "^7.3.1",
|
|
40
40
|
"vitest": "^4.0.18"
|
package/src/Player.svelte
CHANGED
|
@@ -90,8 +90,12 @@
|
|
|
90
90
|
let isDevPanelOpen = $state(false);
|
|
91
91
|
let skipDirection: SkipDirection = $state(null);
|
|
92
92
|
|
|
93
|
-
let activeTheme = $state<FwThemePreset>(
|
|
94
|
-
let activeLocale = $state<FwLocale>(
|
|
93
|
+
let activeTheme = $state<FwThemePreset>("default");
|
|
94
|
+
let activeLocale = $state<FwLocale>("en");
|
|
95
|
+
$effect(() => {
|
|
96
|
+
if (options.theme) activeTheme = options.theme;
|
|
97
|
+
if (options.locale) activeLocale = options.locale;
|
|
98
|
+
});
|
|
95
99
|
|
|
96
100
|
// Sync locale state to i18n store and provide translator context
|
|
97
101
|
$effect(() => {
|
|
@@ -221,6 +225,10 @@
|
|
|
221
225
|
playbackQuality: null as any,
|
|
222
226
|
subtitlesEnabled: false,
|
|
223
227
|
toast: null as { message: string; timestamp: number } | null,
|
|
228
|
+
controllerSeekableStart: 0,
|
|
229
|
+
controllerLiveEdge: 0,
|
|
230
|
+
controllerCanSeek: false,
|
|
231
|
+
controllerHasAudio: true,
|
|
224
232
|
});
|
|
225
233
|
|
|
226
234
|
// Track if we've already attached to prevent double-attach race
|
|
@@ -667,8 +675,8 @@
|
|
|
667
675
|
onStatsToggle={() => (isStatsOpen = !isStatsOpen)}
|
|
668
676
|
isContentLive={storeState.isEffectivelyLive}
|
|
669
677
|
onJumpToLive={() => playerStore?.getController()?.jumpToLive()}
|
|
670
|
-
controllerSeekableStart={
|
|
671
|
-
controllerLiveEdge={
|
|
678
|
+
controllerSeekableStart={storeState?.controllerSeekableStart ?? 0}
|
|
679
|
+
controllerLiveEdge={storeState?.controllerLiveEdge ?? 0}
|
|
672
680
|
{activeLocale}
|
|
673
681
|
onLocaleChange={(l) => {
|
|
674
682
|
activeLocale = l;
|
|
@@ -149,40 +149,77 @@
|
|
|
149
149
|
|
|
150
150
|
// Audio detection: trust MistServer metadata first, then DOM fallback
|
|
151
151
|
$effect(() => {
|
|
152
|
-
// Primary: trust MistServer stream metadata (matches ddvtech embed approach)
|
|
153
|
-
if (mistStreamInfo?.hasAudio !== undefined) {
|
|
154
|
-
hasAudio = mistStreamInfo.hasAudio;
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
152
|
if (!video) {
|
|
159
153
|
hasAudio = true;
|
|
160
154
|
return;
|
|
161
155
|
}
|
|
156
|
+
const currentVideo = video;
|
|
157
|
+
|
|
158
|
+
let boundStream: MediaStream | null = null;
|
|
159
|
+
let streamListener: (() => void) | null = null;
|
|
160
|
+
|
|
161
|
+
const unbindStream = () => {
|
|
162
|
+
if (boundStream && streamListener) {
|
|
163
|
+
boundStream.removeEventListener("addtrack", streamListener);
|
|
164
|
+
boundStream.removeEventListener("removetrack", streamListener);
|
|
165
|
+
}
|
|
166
|
+
boundStream = null;
|
|
167
|
+
streamListener = null;
|
|
168
|
+
};
|
|
162
169
|
|
|
163
170
|
const checkAudio = () => {
|
|
164
|
-
|
|
165
|
-
|
|
171
|
+
// Prefer actual MediaStream tracks over metadata to avoid phantom controls.
|
|
172
|
+
if (currentVideo.srcObject instanceof MediaStream) {
|
|
173
|
+
const stream = currentVideo.srcObject;
|
|
174
|
+
if (stream !== boundStream) {
|
|
175
|
+
unbindStream();
|
|
176
|
+
boundStream = stream;
|
|
177
|
+
streamListener = () => {
|
|
178
|
+
hasAudio = stream.getAudioTracks().length > 0;
|
|
179
|
+
};
|
|
180
|
+
stream.addEventListener("addtrack", streamListener);
|
|
181
|
+
stream.addEventListener("removetrack", streamListener);
|
|
182
|
+
}
|
|
183
|
+
hasAudio = stream.getAudioTracks().length > 0;
|
|
166
184
|
return;
|
|
167
185
|
}
|
|
168
|
-
|
|
186
|
+
|
|
187
|
+
unbindStream();
|
|
188
|
+
|
|
189
|
+
// Fallback: metadata for non-MediaStream sources.
|
|
190
|
+
if (mistStreamInfo?.hasAudio !== undefined) {
|
|
191
|
+
hasAudio = mistStreamInfo.hasAudio;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const videoAny = currentVideo as any;
|
|
169
196
|
if (videoAny.audioTracks && videoAny.audioTracks.length !== undefined) {
|
|
170
197
|
hasAudio = videoAny.audioTracks.length > 0;
|
|
171
198
|
return;
|
|
172
199
|
}
|
|
200
|
+
|
|
173
201
|
hasAudio = true;
|
|
174
202
|
};
|
|
203
|
+
|
|
175
204
|
checkAudio();
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
205
|
+
currentVideo.addEventListener("loadedmetadata", checkAudio);
|
|
206
|
+
currentVideo.addEventListener("loadeddata", checkAudio);
|
|
207
|
+
currentVideo.addEventListener("durationchange", checkAudio);
|
|
208
|
+
|
|
209
|
+
const audioTracks = (currentVideo as any).audioTracks;
|
|
179
210
|
if (audioTracks?.addEventListener) {
|
|
180
211
|
audioTracks.addEventListener("addtrack", checkAudio);
|
|
212
|
+
audioTracks.addEventListener("removetrack", checkAudio);
|
|
181
213
|
}
|
|
214
|
+
|
|
182
215
|
return () => {
|
|
183
|
-
|
|
216
|
+
unbindStream();
|
|
217
|
+
currentVideo.removeEventListener("loadedmetadata", checkAudio);
|
|
218
|
+
currentVideo.removeEventListener("loadeddata", checkAudio);
|
|
219
|
+
currentVideo.removeEventListener("durationchange", checkAudio);
|
|
184
220
|
if (audioTracks?.removeEventListener) {
|
|
185
221
|
audioTracks.removeEventListener("addtrack", checkAudio);
|
|
222
|
+
audioTracks.removeEventListener("removetrack", checkAudio);
|
|
186
223
|
}
|
|
187
224
|
};
|
|
188
225
|
});
|
|
@@ -263,11 +300,7 @@
|
|
|
263
300
|
);
|
|
264
301
|
|
|
265
302
|
let allowMediaStreamDvr = $derived(
|
|
266
|
-
isMediaStreamSource(video) &&
|
|
267
|
-
bufferWindowMs !== undefined &&
|
|
268
|
-
bufferWindowMs > 0 &&
|
|
269
|
-
sourceType !== "whep" &&
|
|
270
|
-
sourceType !== "webrtc"
|
|
303
|
+
isMediaStreamSource(video) && bufferWindowMs !== undefined && bufferWindowMs > 0
|
|
271
304
|
);
|
|
272
305
|
|
|
273
306
|
// Seekable range: prefer controller-derived values (same pattern as React/WC)
|
|
@@ -101,7 +101,13 @@
|
|
|
101
101
|
|
|
102
102
|
{#if isOpen}
|
|
103
103
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
104
|
-
<div
|
|
104
|
+
<div
|
|
105
|
+
class="fw-settings-menu"
|
|
106
|
+
role="menu"
|
|
107
|
+
aria-label={t("settings")}
|
|
108
|
+
onkeydown={handleKeyDown}
|
|
109
|
+
tabindex="-1"
|
|
110
|
+
>
|
|
105
111
|
{#if showModeSelector && onModeChange}
|
|
106
112
|
<div class="fw-settings-section">
|
|
107
113
|
<div class="fw-settings-label">{t("mode")}</div>
|
|
@@ -85,6 +85,14 @@ export interface PlayerControllerState {
|
|
|
85
85
|
subtitlesEnabled: boolean;
|
|
86
86
|
/** Toast message to display (auto-dismisses) */
|
|
87
87
|
toast: { message: string; timestamp: number } | null;
|
|
88
|
+
/** Controller-derived seekable start (ms) — reactively updated via seekingStateChange */
|
|
89
|
+
controllerSeekableStart: number;
|
|
90
|
+
/** Controller-derived live edge (ms) — reactively updated via seekingStateChange */
|
|
91
|
+
controllerLiveEdge: number;
|
|
92
|
+
/** Controller-derived canSeek — reactively updated via seekingStateChange */
|
|
93
|
+
controllerCanSeek: boolean;
|
|
94
|
+
/** Controller-derived hasAudio — reactively updated via seekingStateChange */
|
|
95
|
+
controllerHasAudio: boolean;
|
|
88
96
|
}
|
|
89
97
|
|
|
90
98
|
export interface PlayerControllerStore extends Readable<PlayerControllerState> {
|
|
@@ -184,6 +192,10 @@ const initialState: PlayerControllerState = {
|
|
|
184
192
|
playbackQuality: null,
|
|
185
193
|
subtitlesEnabled: false,
|
|
186
194
|
toast: null,
|
|
195
|
+
controllerSeekableStart: 0,
|
|
196
|
+
controllerLiveEdge: 0,
|
|
197
|
+
controllerCanSeek: false,
|
|
198
|
+
controllerHasAudio: true,
|
|
187
199
|
};
|
|
188
200
|
|
|
189
201
|
// ============================================================================
|
|
@@ -412,6 +424,18 @@ export function createPlayerControllerStore(
|
|
|
412
424
|
})
|
|
413
425
|
);
|
|
414
426
|
|
|
427
|
+
unsubscribers.push(
|
|
428
|
+
controller.on("seekingStateChange", (data) => {
|
|
429
|
+
store.update((prev) => ({
|
|
430
|
+
...prev,
|
|
431
|
+
controllerSeekableStart: data.seekableStart,
|
|
432
|
+
controllerLiveEdge: data.liveEdge,
|
|
433
|
+
controllerCanSeek: data.canSeek,
|
|
434
|
+
controllerHasAudio: data.hasAudio,
|
|
435
|
+
}));
|
|
436
|
+
})
|
|
437
|
+
);
|
|
438
|
+
|
|
415
439
|
// Error handling events - show toasts/modals
|
|
416
440
|
unsubscribers.push(
|
|
417
441
|
controller.on("protocolSwapped", (data) => {
|