@outbook/webcomponents-player 1.4.7 → 1.5.1
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/_lib/is-valid-track-index.js +18 -0
- package/_lib/setup-media-session.js +66 -7
- package/package.json +3 -3
- package/player.js +231 -72
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function isValidTrackIndex({ index, playlist }) {
|
|
2
|
+
if (
|
|
3
|
+
!Number.isInteger(index) ||
|
|
4
|
+
!Array.isArray(playlist) ||
|
|
5
|
+
index < 0 ||
|
|
6
|
+
index >= playlist.length
|
|
7
|
+
) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const track = playlist[index];
|
|
12
|
+
return (
|
|
13
|
+
track &&
|
|
14
|
+
typeof track === 'object' &&
|
|
15
|
+
typeof track.file === 'string' &&
|
|
16
|
+
track.file.trim().length > 0
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -1,9 +1,51 @@
|
|
|
1
1
|
import { getCover } from './get-cover.js';
|
|
2
|
+
import { isValidTrackIndex } from './is-valid-track-index.js';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
const MEDIA_SESSION_ACTIONS = [
|
|
5
|
+
'play',
|
|
6
|
+
'pause',
|
|
7
|
+
'stop',
|
|
8
|
+
'nexttrack',
|
|
9
|
+
'previoustrack'
|
|
10
|
+
];
|
|
11
|
+
let mediaSessionOwner = null;
|
|
12
|
+
|
|
13
|
+
function getMediaSession() {
|
|
14
|
+
return navigator.mediaSession || null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isMediaSessionOwner({ owner }) {
|
|
18
|
+
return owner !== null && owner !== undefined && mediaSessionOwner === owner;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function clearMediaSession({ owner }) {
|
|
22
|
+
const mediaSession = getMediaSession();
|
|
23
|
+
if (!mediaSession || !isMediaSessionOwner({ owner })) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
mediaSession.metadata = null;
|
|
28
|
+
MEDIA_SESSION_ACTIONS.forEach(action => {
|
|
29
|
+
mediaSession.setActionHandler(action, null);
|
|
30
|
+
});
|
|
31
|
+
mediaSessionOwner = null;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function setupMediaSession({ owner, index, playlist, handleAudio }) {
|
|
36
|
+
const mediaSession = getMediaSession();
|
|
37
|
+
if (!mediaSession || owner === null || owner === undefined) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!isValidTrackIndex({ index, playlist })) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
mediaSessionOwner = owner;
|
|
4
46
|
const metadata = playlist[index];
|
|
5
47
|
const type = 'image/jpeg';
|
|
6
|
-
|
|
48
|
+
mediaSession.metadata = new MediaMetadata({
|
|
7
49
|
title: metadata.title,
|
|
8
50
|
artist: metadata.artist,
|
|
9
51
|
album: metadata.album,
|
|
@@ -15,15 +57,32 @@ export function setupMediaSession({ index, playlist, handleAudio }) {
|
|
|
15
57
|
};
|
|
16
58
|
})
|
|
17
59
|
});
|
|
18
|
-
|
|
60
|
+
mediaSession.setActionHandler(
|
|
19
61
|
'nexttrack',
|
|
20
62
|
index < playlist.length - 1 ? handleAudio.next : null
|
|
21
63
|
);
|
|
22
|
-
|
|
64
|
+
mediaSession.setActionHandler(
|
|
23
65
|
'previoustrack',
|
|
24
66
|
index > 0 ? handleAudio.previous : null
|
|
25
67
|
);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
68
|
+
mediaSession.setActionHandler('play', handleAudio.play);
|
|
69
|
+
mediaSession.setActionHandler('pause', handleAudio.pause);
|
|
70
|
+
mediaSession.setActionHandler('stop', handleAudio.stop);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function syncMediaSession({ owner, index, playlist, handleAudio }) {
|
|
75
|
+
if (!getMediaSession()) {
|
|
76
|
+
return () => {};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (index !== null) {
|
|
80
|
+
setupMediaSession({ owner, index, playlist, handleAudio });
|
|
81
|
+
} else {
|
|
82
|
+
clearMediaSession({ owner });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return () => {
|
|
86
|
+
clearMediaSession({ owner });
|
|
87
|
+
};
|
|
29
88
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outbook/webcomponents-player",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"main": "player.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -31,12 +31,12 @@
|
|
|
31
31
|
"@outbook/a11y": ">=1.2.1",
|
|
32
32
|
"color.js": "1.2.0",
|
|
33
33
|
"custom-event-conjurer": "1.0.0",
|
|
34
|
-
"@outbook/format-time": "1.0.
|
|
34
|
+
"@outbook/format-time": "1.0.5",
|
|
35
35
|
"@outbook/webcomponents-scrollable": ">=1.0.2",
|
|
36
36
|
"@outbook/webcomponents-type-icon": ">=1.1.4",
|
|
37
37
|
"@outbook/webcomponents-badge-file-extension": ">=1.2.2",
|
|
38
38
|
"@outbook/webcomponents-player-artwork": ">=1.1.3",
|
|
39
|
-
"@outbook/webcomponents-player-timeline": ">=1.0.
|
|
39
|
+
"@outbook/webcomponents-player-timeline": ">=1.0.6",
|
|
40
40
|
"@outbook/icons": ">=1.3.1",
|
|
41
41
|
"@outbook/webcomponents-player-audio-decoy": ">=1.0.0"
|
|
42
42
|
},
|
package/player.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { adoptStyles, html, nothing } from 'lit';
|
|
2
2
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
3
|
-
import { component, useEffect, useState } from 'haunted';
|
|
3
|
+
import { component, useEffect, useRef, useState } from 'haunted';
|
|
4
4
|
import { customEventConjurer } from 'custom-event-conjurer';
|
|
5
5
|
import styleComponent from './_style/player.style.js';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
clearMediaSession,
|
|
8
|
+
isMediaSessionOwner,
|
|
9
|
+
syncMediaSession
|
|
10
|
+
} from './_lib/setup-media-session.js';
|
|
11
|
+
import { isValidTrackIndex } from './_lib/is-valid-track-index.js';
|
|
7
12
|
import { emptyBackground, getBackgroundColor } from './_lib/background.js';
|
|
8
13
|
import { getCover } from './_lib/get-cover.js';
|
|
9
14
|
import { classMap } from 'lit/directives/class-map.js';
|
|
@@ -58,6 +63,57 @@ function ComponentPlayer(element) {
|
|
|
58
63
|
const [prominentColors, setProminentColors] = useState('');
|
|
59
64
|
const [hasProminentColors, setHasProminentColors] = useState(false);
|
|
60
65
|
const [coverUrl, setCoverUrl] = useState(null);
|
|
66
|
+
const [playbackRequestId, setPlaybackRequestId] = useState(0);
|
|
67
|
+
const playbackRequestRef = useRef({
|
|
68
|
+
id: 0,
|
|
69
|
+
started: true,
|
|
70
|
+
removeListeners: null
|
|
71
|
+
});
|
|
72
|
+
const selectedTrackRef = useRef(null);
|
|
73
|
+
const mediaSessionOwner = useRef(Symbol('outbook-player-media-session'));
|
|
74
|
+
const playlistMediaSessionSignature = playlist
|
|
75
|
+
.map(track =>
|
|
76
|
+
[
|
|
77
|
+
track?.file,
|
|
78
|
+
track?.title,
|
|
79
|
+
track?.artist,
|
|
80
|
+
track?.album,
|
|
81
|
+
track?.cover?.path,
|
|
82
|
+
track?.cover?.name,
|
|
83
|
+
track?.cover?.extension
|
|
84
|
+
].join('\u0000')
|
|
85
|
+
)
|
|
86
|
+
.join('\u0001');
|
|
87
|
+
|
|
88
|
+
function setMediaSessionPlaybackState(playbackState) {
|
|
89
|
+
if (
|
|
90
|
+
navigator.mediaSession &&
|
|
91
|
+
isMediaSessionOwner({ owner: mediaSessionOwner.current })
|
|
92
|
+
) {
|
|
93
|
+
navigator.mediaSession.playbackState = playbackState;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function cancelPendingInitialPlayback() {
|
|
98
|
+
const request = playbackRequestRef.current;
|
|
99
|
+
request.removeListeners?.();
|
|
100
|
+
playbackRequestRef.current = {
|
|
101
|
+
id: request.id + 1,
|
|
102
|
+
started: true,
|
|
103
|
+
removeListeners: null
|
|
104
|
+
};
|
|
105
|
+
return playbackRequestRef.current.id;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function requestInitialPlayback() {
|
|
109
|
+
const requestId = cancelPendingInitialPlayback();
|
|
110
|
+
playbackRequestRef.current = {
|
|
111
|
+
id: requestId,
|
|
112
|
+
started: false,
|
|
113
|
+
removeListeners: null
|
|
114
|
+
};
|
|
115
|
+
setPlaybackRequestId(requestId);
|
|
116
|
+
}
|
|
61
117
|
|
|
62
118
|
useEffect(() => {
|
|
63
119
|
let isActive = true;
|
|
@@ -94,30 +150,101 @@ function ComponentPlayer(element) {
|
|
|
94
150
|
}, []);
|
|
95
151
|
|
|
96
152
|
useEffect(() => {
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
153
|
+
if (!Number.isInteger(indexSelected)) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const request = playbackRequestRef.current;
|
|
158
|
+
if (request.id !== playbackRequestId || request.started) {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const mediaElement = getMediaElement();
|
|
163
|
+
if (!mediaElement) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const playInitial = () => {
|
|
168
|
+
if (playbackRequestRef.current !== request || request.started) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
request.started = true;
|
|
173
|
+
request.removeListeners?.();
|
|
174
|
+
request.removeListeners = null;
|
|
175
|
+
mediaElement.currentTime = 0;
|
|
176
|
+
setCurrentTime(0);
|
|
177
|
+
playAudio(mediaElement);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
if (mediaElement.readyState > 0) {
|
|
181
|
+
playInitial();
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const removeListeners = () => {
|
|
186
|
+
mediaElement.removeEventListener('loadedmetadata', playInitial);
|
|
187
|
+
mediaElement.removeEventListener('canplay', playInitial);
|
|
188
|
+
};
|
|
189
|
+
request.removeListeners = removeListeners;
|
|
190
|
+
mediaElement.addEventListener('loadedmetadata', playInitial, {
|
|
191
|
+
once: true
|
|
192
|
+
});
|
|
193
|
+
mediaElement.addEventListener('canplay', playInitial, { once: true });
|
|
194
|
+
|
|
195
|
+
return () => {
|
|
196
|
+
if (playbackRequestRef.current === request) {
|
|
197
|
+
removeListeners();
|
|
198
|
+
request.removeListeners = null;
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}, [indexSelected, playbackRequestId]);
|
|
202
|
+
|
|
203
|
+
useEffect(() => () => cancelPendingInitialPlayback(), []);
|
|
204
|
+
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
if (!Number.isInteger(indexSelected)) {
|
|
207
|
+
selectedTrackRef.current = null;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const selectedTrack = playlist[indexSelected];
|
|
212
|
+
if (
|
|
213
|
+
!isValidTrackIndex({ index: indexSelected, playlist }) ||
|
|
214
|
+
selectedTrackRef.current?.file !== selectedTrack.file
|
|
215
|
+
) {
|
|
216
|
+
handleSessionStop();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
selectedTrackRef.current = selectedTrack;
|
|
221
|
+
setCoverUrl(
|
|
222
|
+
selectedTrack.cover?.name ? getCover(selectedTrack.cover) : null
|
|
223
|
+
);
|
|
224
|
+
}, [indexSelected, playlist, playlistMediaSessionSignature]);
|
|
225
|
+
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
if (
|
|
228
|
+
indexSelected !== null &&
|
|
229
|
+
(!isValidTrackIndex({ index: indexSelected, playlist }) ||
|
|
230
|
+
selectedTrackRef.current?.file !== playlist[indexSelected].file)
|
|
231
|
+
) {
|
|
232
|
+
return undefined;
|
|
119
233
|
}
|
|
120
|
-
|
|
234
|
+
|
|
235
|
+
return syncMediaSession({
|
|
236
|
+
owner: mediaSessionOwner.current,
|
|
237
|
+
index: indexSelected,
|
|
238
|
+
playlist,
|
|
239
|
+
handleAudio: {
|
|
240
|
+
stop: handleAudio.stop,
|
|
241
|
+
play: handleAudio.play,
|
|
242
|
+
pause: handleSessionPause,
|
|
243
|
+
next: handleSessionNext,
|
|
244
|
+
previous: handleSessionPrevious
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}, [indexSelected, playlist, playlistMediaSessionSignature]);
|
|
121
248
|
|
|
122
249
|
useEffect(() => {
|
|
123
250
|
let isMounted = true;
|
|
@@ -142,25 +269,54 @@ function ComponentPlayer(element) {
|
|
|
142
269
|
}
|
|
143
270
|
|
|
144
271
|
function handleSessionPause() {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
272
|
+
cancelPendingInitialPlayback();
|
|
273
|
+
const media = getMediaElement();
|
|
274
|
+
if (!media) {
|
|
275
|
+
return;
|
|
148
276
|
}
|
|
277
|
+
media.pause();
|
|
149
278
|
setIsPlaying(false);
|
|
279
|
+
setMediaSessionPlaybackState('paused');
|
|
150
280
|
}
|
|
151
281
|
|
|
152
|
-
function
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
282
|
+
async function playAudio(mediaElement) {
|
|
283
|
+
try {
|
|
284
|
+
await mediaElement.play();
|
|
285
|
+
} catch (err) {
|
|
286
|
+
console.error('Playback failed:', err);
|
|
287
|
+
setIsPlaying(false);
|
|
288
|
+
setMediaSessionPlaybackState('paused');
|
|
157
289
|
}
|
|
158
290
|
}
|
|
159
291
|
|
|
160
|
-
function
|
|
161
|
-
|
|
162
|
-
|
|
292
|
+
async function handleSessionPlay() {
|
|
293
|
+
const media = getMediaElement();
|
|
294
|
+
if (!media) {
|
|
295
|
+
return;
|
|
163
296
|
}
|
|
297
|
+
await playAudio(media);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function handleAudioNativePlay() {
|
|
301
|
+
setIsPlaying(true);
|
|
302
|
+
setMediaSessionPlaybackState('playing');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function handleAudioNativePause() {
|
|
306
|
+
setIsPlaying(false);
|
|
307
|
+
setMediaSessionPlaybackState('paused');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function handleAudioNativeError(ev) {
|
|
311
|
+
cancelPendingInitialPlayback();
|
|
312
|
+
console.error('Audio error:', ev.currentTarget?.error);
|
|
313
|
+
setIsPlaying(false);
|
|
314
|
+
setMediaSessionPlaybackState('paused');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function handleSessionStop() {
|
|
318
|
+
cancelPendingInitialPlayback();
|
|
319
|
+
setMediaSessionPlaybackState('none');
|
|
164
320
|
|
|
165
321
|
const media = getMediaElement();
|
|
166
322
|
|
|
@@ -173,12 +329,10 @@ function ComponentPlayer(element) {
|
|
|
173
329
|
setCurrentTime(0); // Set zero on stop
|
|
174
330
|
setHasProminentColors(false);
|
|
175
331
|
setIsPlaying(false);
|
|
176
|
-
|
|
177
|
-
if (navigator.mediaSession) {
|
|
178
|
-
navigator.mediaSession.metadata = null;
|
|
179
|
-
}
|
|
332
|
+
clearMediaSession({ owner: mediaSessionOwner.current });
|
|
180
333
|
|
|
181
334
|
setIndexSelected(null);
|
|
335
|
+
selectedTrackRef.current = null;
|
|
182
336
|
setCoverUrl(null);
|
|
183
337
|
}
|
|
184
338
|
|
|
@@ -198,43 +352,31 @@ function ComponentPlayer(element) {
|
|
|
198
352
|
next: handleSessionNext,
|
|
199
353
|
previous: handleSessionPrevious,
|
|
200
354
|
setTrack({ index, fromControls = false }) {
|
|
355
|
+
if (!isValidTrackIndex({ index, playlist })) {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const media = getMediaElement();
|
|
360
|
+
if (media) {
|
|
361
|
+
media.pause();
|
|
362
|
+
media.currentTime = 0;
|
|
363
|
+
}
|
|
364
|
+
setTotalTime(0);
|
|
365
|
+
setCurrentTime(0);
|
|
201
366
|
setIndexSelected(index);
|
|
367
|
+
selectedTrackRef.current = playlist[index];
|
|
202
368
|
if (fromControls) {
|
|
203
369
|
handleScroll({ index });
|
|
204
370
|
}
|
|
205
371
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
setTimeout(() => {
|
|
212
|
-
this.playInitial();
|
|
213
|
-
}, 100);
|
|
372
|
+
setCoverUrl(
|
|
373
|
+
playlist[index].cover?.name ? getCover(playlist[index].cover) : null
|
|
374
|
+
);
|
|
375
|
+
requestInitialPlayback();
|
|
376
|
+
return true;
|
|
214
377
|
},
|
|
215
378
|
stop: handleSessionStop,
|
|
216
379
|
play: handleSessionPlay,
|
|
217
|
-
playInitial() {
|
|
218
|
-
const mediaElement = getMediaElement();
|
|
219
|
-
if (!mediaElement) {
|
|
220
|
-
console.warn('Audio element not ready yet, retrying...');
|
|
221
|
-
setTimeout(() => this.playInitial(), 50);
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
mediaElement.currentTime = 0;
|
|
225
|
-
setCurrentTime(0); // Set zero initial time
|
|
226
|
-
|
|
227
|
-
mediaElement
|
|
228
|
-
.play()
|
|
229
|
-
.then(function () {
|
|
230
|
-
setTotalTime(mediaElement.duration);
|
|
231
|
-
if (navigator.mediaSession) {
|
|
232
|
-
navigator.mediaSession.playbackState = 'playing';
|
|
233
|
-
}
|
|
234
|
-
setIsPlaying(true);
|
|
235
|
-
})
|
|
236
|
-
.catch(err => console.error('Autoplay blocked:', err));
|
|
237
|
-
},
|
|
238
380
|
pause: handleSessionPause
|
|
239
381
|
};
|
|
240
382
|
|
|
@@ -251,13 +393,25 @@ function ComponentPlayer(element) {
|
|
|
251
393
|
setCurrentTime(media.currentTime);
|
|
252
394
|
}
|
|
253
395
|
|
|
396
|
+
function handleDurationChange(ev) {
|
|
397
|
+
const duration = ev.currentTarget.duration;
|
|
398
|
+
setTotalTime(Number.isFinite(duration) && duration > 0 ? duration : 0);
|
|
399
|
+
}
|
|
400
|
+
|
|
254
401
|
function handleReposition(ev) {
|
|
255
402
|
const newTime = ev.detail.currentTime;
|
|
256
403
|
const media = getMediaElement();
|
|
257
|
-
if (
|
|
258
|
-
media
|
|
404
|
+
if (
|
|
405
|
+
!media ||
|
|
406
|
+
!Number.isFinite(totalTime) ||
|
|
407
|
+
totalTime <= 0 ||
|
|
408
|
+
!Number.isFinite(newTime)
|
|
409
|
+
) {
|
|
410
|
+
return;
|
|
259
411
|
}
|
|
260
|
-
|
|
412
|
+
const safeTime = Math.max(0, Math.min(newTime, totalTime));
|
|
413
|
+
media.currentTime = safeTime;
|
|
414
|
+
setCurrentTime(safeTime);
|
|
261
415
|
}
|
|
262
416
|
|
|
263
417
|
const mainClasses = classMap({
|
|
@@ -319,6 +473,11 @@ function ComponentPlayer(element) {
|
|
|
319
473
|
id="${mediaId}"
|
|
320
474
|
@ended="${handleAudioEnd}"
|
|
321
475
|
@timeupdate="${handleTimeUpdate}"
|
|
476
|
+
@loadedmetadata="${handleDurationChange}"
|
|
477
|
+
@durationchange="${handleDurationChange}"
|
|
478
|
+
@play="${handleAudioNativePlay}"
|
|
479
|
+
@pause="${handleAudioNativePause}"
|
|
480
|
+
@error="${handleAudioNativeError}"
|
|
322
481
|
></audio>
|
|
323
482
|
`
|
|
324
483
|
: nothing}
|