mikuru 1.0.29 → 1.0.31
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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.31 - 2026-05-16
|
|
4
|
+
|
|
5
|
+
- Exported `MikuruVideoPlayer` media events so parent components can listen for playback, timing, seeking, volume, and playback-rate changes with typed media state payloads.
|
|
6
|
+
- Exported matching `MikuruAudioPlayer` media events and documented the shared media player event payload.
|
|
7
|
+
|
|
8
|
+
## 1.0.30 - 2026-05-15
|
|
9
|
+
|
|
10
|
+
- Hardened `MikuruVideoPlayer` controls so stop, mute, playback speed, seeking, and modal close operations do not trigger recursive updates while browser media events are firing.
|
|
11
|
+
- Kept media button UI stable by avoiding reactive branch swaps for player control icons and deferring non-play media operations out of the click event stack.
|
|
12
|
+
|
|
3
13
|
## 1.0.29 - 2026-05-15
|
|
4
14
|
|
|
5
15
|
- Fixed `MikuruVideoPlayer` and `MikuruAudioPlayer` unmount handling so media events fired while a modal or conditional branch is being removed no longer update component refs recursively.
|
|
@@ -4,12 +4,15 @@
|
|
|
4
4
|
ref="mediaEl"
|
|
5
5
|
:src="src"
|
|
6
6
|
:preload="preload"
|
|
7
|
-
@loadedmetadata="
|
|
8
|
-
@timeupdate="
|
|
9
|
-
@durationchange="
|
|
7
|
+
@loadedmetadata="handleLoadedMetadata"
|
|
8
|
+
@timeupdate="handleTimeUpdate"
|
|
9
|
+
@durationchange="handleDurationChange"
|
|
10
10
|
@play="markPlaying"
|
|
11
11
|
@pause="markPaused"
|
|
12
|
-
@ended="
|
|
12
|
+
@ended="markEnded"
|
|
13
|
+
@seeked="handleSeeked"
|
|
14
|
+
@volumechange="handleVolumeChange"
|
|
15
|
+
@ratechange="handleRateChange"
|
|
13
16
|
></audio>
|
|
14
17
|
|
|
15
18
|
<div class="art">
|
|
@@ -31,13 +34,11 @@
|
|
|
31
34
|
<div class="controls">
|
|
32
35
|
<button type="button" @click="skipBackward">-10</button>
|
|
33
36
|
<button class="primary" type="button" @click="togglePlayback" :aria-label="playLabel">
|
|
34
|
-
<span
|
|
35
|
-
<span m-else>Play</span>
|
|
37
|
+
<span>{{ playText }}</span>
|
|
36
38
|
</button>
|
|
37
39
|
<button type="button" @click="skipForward">+10</button>
|
|
38
40
|
<button type="button" @click="toggleMute" :aria-label="muteLabel">
|
|
39
|
-
<span
|
|
40
|
-
<span m-else>Sound</span>
|
|
41
|
+
<span>Sound</span>
|
|
41
42
|
</button>
|
|
42
43
|
<label class="volume">
|
|
43
44
|
<span>Volume</span>
|
|
@@ -63,6 +64,18 @@ const {
|
|
|
63
64
|
preload: String
|
|
64
65
|
});
|
|
65
66
|
|
|
67
|
+
const emit = defineEmits([
|
|
68
|
+
"loadedmetadata",
|
|
69
|
+
"timeupdate",
|
|
70
|
+
"durationchange",
|
|
71
|
+
"play",
|
|
72
|
+
"pause",
|
|
73
|
+
"ended",
|
|
74
|
+
"seeked",
|
|
75
|
+
"volumechange",
|
|
76
|
+
"ratechange"
|
|
77
|
+
]);
|
|
78
|
+
|
|
66
79
|
const mediaEl = ref(null);
|
|
67
80
|
const currentTime = ref(0);
|
|
68
81
|
const duration = ref(0);
|
|
@@ -72,11 +85,13 @@ const isPlaying = ref(false);
|
|
|
72
85
|
const safeDuration = computed(() => duration.value > 0 ? duration.value : 0);
|
|
73
86
|
const playLabel = computed(() => isPlaying.value ? "Pause audio" : "Play audio");
|
|
74
87
|
const muteLabel = computed(() => muted.value ? "Unmute audio" : "Mute audio");
|
|
88
|
+
const playText = computed(() => isPlaying.value ? "Pause" : "Play");
|
|
75
89
|
const initials = computed(() => {
|
|
76
90
|
const words = title.value.split(" ").filter(Boolean);
|
|
77
91
|
return words.slice(0, 2).map((word) => word[0]).join("").toUpperCase() || "M";
|
|
78
92
|
});
|
|
79
93
|
let isDisposed = false;
|
|
94
|
+
let ignoreMediaEventsUntil = 0;
|
|
80
95
|
|
|
81
96
|
onMounted(() => {
|
|
82
97
|
applyAudioSettings();
|
|
@@ -91,14 +106,72 @@ function getMedia() {
|
|
|
91
106
|
return mediaEl.value;
|
|
92
107
|
}
|
|
93
108
|
|
|
109
|
+
function ignoreMediaEventsFor(ms) {
|
|
110
|
+
ignoreMediaEventsUntil = performance.now() + ms;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function shouldIgnoreMediaEvent() {
|
|
114
|
+
return performance.now() < ignoreMediaEventsUntil;
|
|
115
|
+
}
|
|
116
|
+
|
|
94
117
|
function syncMedia() {
|
|
95
|
-
if (isDisposed) return;
|
|
118
|
+
if (isDisposed || shouldIgnoreMediaEvent()) return;
|
|
96
119
|
const media = getMedia();
|
|
97
120
|
if (!media) return;
|
|
98
121
|
currentTime.value = media.currentTime || 0;
|
|
99
122
|
duration.value = Number.isFinite(media.duration) ? media.duration : 0;
|
|
100
|
-
|
|
101
|
-
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function createMediaPayload(event) {
|
|
126
|
+
const media = getMedia();
|
|
127
|
+
if (!media) return null;
|
|
128
|
+
return {
|
|
129
|
+
currentTime: media.currentTime || 0,
|
|
130
|
+
duration: Number.isFinite(media.duration) ? media.duration : 0,
|
|
131
|
+
paused: media.paused,
|
|
132
|
+
ended: media.ended,
|
|
133
|
+
muted: media.muted,
|
|
134
|
+
volume: media.volume,
|
|
135
|
+
playbackRate: media.playbackRate,
|
|
136
|
+
nativeEvent: event
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function emitMediaPayload(event, dispatch) {
|
|
141
|
+
const payload = createMediaPayload(event);
|
|
142
|
+
if (payload) {
|
|
143
|
+
dispatch(payload);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function handleLoadedMetadata(event) {
|
|
148
|
+
syncMedia();
|
|
149
|
+
emitMediaPayload(event, (payload) => emit("loadedmetadata", payload));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function handleTimeUpdate(event) {
|
|
153
|
+
syncMedia();
|
|
154
|
+
emitMediaPayload(event, (payload) => emit("timeupdate", payload));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function handleDurationChange(event) {
|
|
158
|
+
syncMedia();
|
|
159
|
+
emitMediaPayload(event, (payload) => emit("durationchange", payload));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function handleSeeked(event) {
|
|
163
|
+
syncMedia();
|
|
164
|
+
emitMediaPayload(event, (payload) => emit("seeked", payload));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function handleVolumeChange(event) {
|
|
168
|
+
syncMedia();
|
|
169
|
+
emitMediaPayload(event, (payload) => emit("volumechange", payload));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function handleRateChange(event) {
|
|
173
|
+
syncMedia();
|
|
174
|
+
emitMediaPayload(event, (payload) => emit("ratechange", payload));
|
|
102
175
|
}
|
|
103
176
|
|
|
104
177
|
function applyAudioSettings() {
|
|
@@ -109,14 +182,22 @@ function applyAudioSettings() {
|
|
|
109
182
|
media.muted = muted.value;
|
|
110
183
|
}
|
|
111
184
|
|
|
112
|
-
function markPlaying() {
|
|
113
|
-
if (isDisposed) return;
|
|
185
|
+
function markPlaying(event) {
|
|
186
|
+
if (isDisposed || shouldIgnoreMediaEvent()) return;
|
|
114
187
|
isPlaying.value = true;
|
|
188
|
+
emitMediaPayload(event, (payload) => emit("play", payload));
|
|
115
189
|
}
|
|
116
190
|
|
|
117
|
-
function markPaused() {
|
|
118
|
-
if (isDisposed) return;
|
|
191
|
+
function markPaused(event) {
|
|
192
|
+
if (isDisposed || shouldIgnoreMediaEvent()) return;
|
|
193
|
+
isPlaying.value = false;
|
|
194
|
+
emitMediaPayload(event, (payload) => emit("pause", payload));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function markEnded(event) {
|
|
198
|
+
if (isDisposed || shouldIgnoreMediaEvent()) return;
|
|
119
199
|
isPlaying.value = false;
|
|
200
|
+
emitMediaPayload(event, (payload) => emit("ended", payload));
|
|
120
201
|
}
|
|
121
202
|
|
|
122
203
|
async function togglePlayback() {
|
|
@@ -126,7 +207,7 @@ async function togglePlayback() {
|
|
|
126
207
|
try {
|
|
127
208
|
await media.play();
|
|
128
209
|
} catch (error) {
|
|
129
|
-
if (
|
|
210
|
+
if (error?.name !== "AbortError") {
|
|
130
211
|
throw error;
|
|
131
212
|
}
|
|
132
213
|
}
|
|
@@ -148,15 +229,26 @@ function seek(event) {
|
|
|
148
229
|
function setVolume(event) {
|
|
149
230
|
if (isDisposed) return;
|
|
150
231
|
const nextVolume = Number(event.target.value);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
232
|
+
window.setTimeout(() => {
|
|
233
|
+
if (isDisposed) return;
|
|
234
|
+
const media = getMedia();
|
|
235
|
+
if (!media) return;
|
|
236
|
+
media.volume = nextVolume;
|
|
237
|
+
media.muted = nextVolume === 0;
|
|
238
|
+
}, 0);
|
|
154
239
|
}
|
|
155
240
|
|
|
156
|
-
function toggleMute() {
|
|
241
|
+
function toggleMute(event) {
|
|
157
242
|
if (isDisposed) return;
|
|
158
|
-
|
|
159
|
-
|
|
243
|
+
const button = event.currentTarget;
|
|
244
|
+
window.setTimeout(() => {
|
|
245
|
+
if (isDisposed) return;
|
|
246
|
+
const media = getMedia();
|
|
247
|
+
if (!media) return;
|
|
248
|
+
media.muted = !media.muted;
|
|
249
|
+
button.textContent = media.muted ? "Muted" : "Sound";
|
|
250
|
+
button.setAttribute("aria-label", media.muted ? "Unmute audio" : "Mute audio");
|
|
251
|
+
}, 0);
|
|
160
252
|
}
|
|
161
253
|
|
|
162
254
|
function skipBackward() {
|
|
@@ -172,8 +264,12 @@ function skipBy(offset) {
|
|
|
172
264
|
const media = getMedia();
|
|
173
265
|
if (!media) return;
|
|
174
266
|
const nextTime = Math.min(Math.max(media.currentTime + offset, 0), safeDuration.value || media.currentTime + offset);
|
|
175
|
-
|
|
176
|
-
|
|
267
|
+
ignoreMediaEventsFor(100);
|
|
268
|
+
window.setTimeout(() => {
|
|
269
|
+
if (isDisposed) return;
|
|
270
|
+
media.currentTime = nextTime;
|
|
271
|
+
currentTime.value = nextTime;
|
|
272
|
+
}, 0);
|
|
177
273
|
}
|
|
178
274
|
|
|
179
275
|
function formatTime(seconds) {
|
|
@@ -17,12 +17,15 @@
|
|
|
17
17
|
:poster="poster"
|
|
18
18
|
:preload="preload"
|
|
19
19
|
playsinline
|
|
20
|
-
@loadedmetadata="
|
|
21
|
-
@timeupdate="
|
|
22
|
-
@durationchange="
|
|
20
|
+
@loadedmetadata="handleLoadedMetadata"
|
|
21
|
+
@timeupdate="handleTimeUpdate"
|
|
22
|
+
@durationchange="handleDurationChange"
|
|
23
23
|
@play="markPlaying"
|
|
24
24
|
@pause="markPaused"
|
|
25
|
-
@ended="
|
|
25
|
+
@ended="markEnded"
|
|
26
|
+
@seeked="handleSeeked"
|
|
27
|
+
@volumechange="handleVolumeChange"
|
|
28
|
+
@ratechange="handleRateChange"
|
|
26
29
|
@click="togglePlayback"
|
|
27
30
|
></video>
|
|
28
31
|
|
|
@@ -32,8 +35,7 @@
|
|
|
32
35
|
</div>
|
|
33
36
|
|
|
34
37
|
<button class="center-toggle" type="button" @click="togglePlayback" :aria-label="playLabel">
|
|
35
|
-
<span
|
|
36
|
-
<span m-else class="fa-icon icon-play large" aria-hidden="true"></span>
|
|
38
|
+
<span class="fa-icon large" :class="playIconClass" aria-hidden="true"></span>
|
|
37
39
|
</button>
|
|
38
40
|
|
|
39
41
|
<div class="control-shelf">
|
|
@@ -61,15 +63,13 @@
|
|
|
61
63
|
<div class="control-row">
|
|
62
64
|
<div class="left-controls">
|
|
63
65
|
<button class="icon-button" type="button" @click="togglePlayback" :aria-label="playLabel">
|
|
64
|
-
<span
|
|
65
|
-
<span m-else class="fa-icon icon-play" aria-hidden="true"></span>
|
|
66
|
+
<span class="fa-icon" :class="playIconClass" aria-hidden="true"></span>
|
|
66
67
|
</button>
|
|
67
68
|
<button class="icon-button" type="button" @click="stopPlayback" aria-label="Stop video">
|
|
68
69
|
<span class="fa-icon icon-stop" aria-hidden="true"></span>
|
|
69
70
|
</button>
|
|
70
71
|
<button class="icon-button" type="button" @click="toggleMute" :aria-label="muteLabel">
|
|
71
|
-
<span
|
|
72
|
-
<span m-else class="fa-icon icon-volume" aria-hidden="true"></span>
|
|
72
|
+
<span class="fa-icon icon-volume" aria-hidden="true"></span>
|
|
73
73
|
</button>
|
|
74
74
|
<label class="volume">
|
|
75
75
|
<span>Volume</span>
|
|
@@ -79,10 +79,9 @@
|
|
|
79
79
|
</div>
|
|
80
80
|
|
|
81
81
|
<div class="right-controls">
|
|
82
|
-
<button class="pill-button" type="button" @click="cycleRate"
|
|
82
|
+
<button class="pill-button" type="button" @click="cycleRate" aria-label="Change playback speed">1x</button>
|
|
83
83
|
<button class="icon-button fullscreen-button" type="button" @click="toggleFullscreen" aria-label="Fullscreen">
|
|
84
|
-
<span
|
|
85
|
-
<span m-else class="fa-icon icon-maximize" aria-hidden="true"></span>
|
|
84
|
+
<span class="fa-icon" :class="fullscreenIconClass" aria-hidden="true"></span>
|
|
86
85
|
</button>
|
|
87
86
|
</div>
|
|
88
87
|
</div>
|
|
@@ -108,6 +107,18 @@ const {
|
|
|
108
107
|
preload: String
|
|
109
108
|
});
|
|
110
109
|
|
|
110
|
+
const emit = defineEmits([
|
|
111
|
+
"loadedmetadata",
|
|
112
|
+
"timeupdate",
|
|
113
|
+
"durationchange",
|
|
114
|
+
"play",
|
|
115
|
+
"pause",
|
|
116
|
+
"ended",
|
|
117
|
+
"seeked",
|
|
118
|
+
"volumechange",
|
|
119
|
+
"ratechange"
|
|
120
|
+
]);
|
|
121
|
+
|
|
111
122
|
const mediaEl = ref(null);
|
|
112
123
|
const screenEl = ref(null);
|
|
113
124
|
const currentTime = ref(0);
|
|
@@ -119,7 +130,6 @@ const isFullscreen = ref(false);
|
|
|
119
130
|
const isSeeking = ref(false);
|
|
120
131
|
const pointerInside = ref(false);
|
|
121
132
|
const controlsVisible = ref(true);
|
|
122
|
-
const playbackRate = ref(1);
|
|
123
133
|
const safeDuration = computed(() => duration.value > 0 ? duration.value : 0);
|
|
124
134
|
const seekProgress = computed(() => {
|
|
125
135
|
if (safeDuration.value <= 0) return 0;
|
|
@@ -129,8 +139,13 @@ const seekProgress = computed(() => {
|
|
|
129
139
|
const seekStyle = computed(() => "--progress: " + seekProgress.value + "%");
|
|
130
140
|
const playLabel = computed(() => isPlaying.value ? "Pause video" : "Play video");
|
|
131
141
|
const muteLabel = computed(() => muted.value ? "Unmute video" : "Mute video");
|
|
142
|
+
const playIconClass = computed(() => isPlaying.value ? "icon-pause" : "icon-play");
|
|
143
|
+
const fullscreenIconClass = computed(() => isFullscreen.value ? "icon-minimize" : "icon-maximize");
|
|
132
144
|
const rates = [1, 1.25, 1.5, 2, 0.75];
|
|
145
|
+
let playbackRate = 1;
|
|
146
|
+
let rateIndex = 0;
|
|
133
147
|
let isDisposed = false;
|
|
148
|
+
let ignoreMediaEventsUntil = 0;
|
|
134
149
|
|
|
135
150
|
onMounted(() => {
|
|
136
151
|
applyAudioSettings();
|
|
@@ -152,15 +167,72 @@ function getMedia() {
|
|
|
152
167
|
return mediaEl.value;
|
|
153
168
|
}
|
|
154
169
|
|
|
170
|
+
function ignoreMediaEventsFor(ms) {
|
|
171
|
+
ignoreMediaEventsUntil = performance.now() + ms;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function shouldIgnoreMediaEvent() {
|
|
175
|
+
return performance.now() < ignoreMediaEventsUntil;
|
|
176
|
+
}
|
|
177
|
+
|
|
155
178
|
function syncMedia() {
|
|
156
|
-
if (isDisposed) return;
|
|
179
|
+
if (isDisposed || shouldIgnoreMediaEvent()) return;
|
|
157
180
|
const media = getMedia();
|
|
158
181
|
if (!media) return;
|
|
159
182
|
currentTime.value = media.currentTime || 0;
|
|
160
183
|
duration.value = Number.isFinite(media.duration) ? media.duration : 0;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function createMediaPayload(event) {
|
|
187
|
+
const media = getMedia();
|
|
188
|
+
if (!media) return null;
|
|
189
|
+
return {
|
|
190
|
+
currentTime: media.currentTime || 0,
|
|
191
|
+
duration: Number.isFinite(media.duration) ? media.duration : 0,
|
|
192
|
+
paused: media.paused,
|
|
193
|
+
ended: media.ended,
|
|
194
|
+
muted: media.muted,
|
|
195
|
+
volume: media.volume,
|
|
196
|
+
playbackRate: media.playbackRate,
|
|
197
|
+
nativeEvent: event
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function emitMediaPayload(event, dispatch) {
|
|
202
|
+
const payload = createMediaPayload(event);
|
|
203
|
+
if (payload) {
|
|
204
|
+
dispatch(payload);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function handleLoadedMetadata(event) {
|
|
209
|
+
syncMedia();
|
|
210
|
+
emitMediaPayload(event, (payload) => emit("loadedmetadata", payload));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function handleTimeUpdate(event) {
|
|
214
|
+
syncMedia();
|
|
215
|
+
emitMediaPayload(event, (payload) => emit("timeupdate", payload));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function handleDurationChange(event) {
|
|
219
|
+
syncMedia();
|
|
220
|
+
emitMediaPayload(event, (payload) => emit("durationchange", payload));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function handleSeeked(event) {
|
|
224
|
+
syncMedia();
|
|
225
|
+
emitMediaPayload(event, (payload) => emit("seeked", payload));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function handleVolumeChange(event) {
|
|
229
|
+
syncMedia();
|
|
230
|
+
emitMediaPayload(event, (payload) => emit("volumechange", payload));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function handleRateChange(event) {
|
|
234
|
+
syncMedia();
|
|
235
|
+
emitMediaPayload(event, (payload) => emit("ratechange", payload));
|
|
164
236
|
}
|
|
165
237
|
|
|
166
238
|
function applyAudioSettings() {
|
|
@@ -169,19 +241,28 @@ function applyAudioSettings() {
|
|
|
169
241
|
if (!media) return;
|
|
170
242
|
media.volume = volume.value;
|
|
171
243
|
media.muted = muted.value;
|
|
172
|
-
media.playbackRate = playbackRate
|
|
244
|
+
media.playbackRate = playbackRate;
|
|
173
245
|
}
|
|
174
246
|
|
|
175
|
-
function markPlaying() {
|
|
176
|
-
if (isDisposed) return;
|
|
247
|
+
function markPlaying(event) {
|
|
248
|
+
if (isDisposed || shouldIgnoreMediaEvent()) return;
|
|
177
249
|
isPlaying.value = true;
|
|
178
250
|
controlsVisible.value = pointerInside.value;
|
|
251
|
+
emitMediaPayload(event, (payload) => emit("play", payload));
|
|
179
252
|
}
|
|
180
253
|
|
|
181
|
-
function markPaused() {
|
|
182
|
-
if (isDisposed) return;
|
|
254
|
+
function markPaused(event) {
|
|
255
|
+
if (isDisposed || shouldIgnoreMediaEvent()) return;
|
|
183
256
|
isPlaying.value = false;
|
|
184
257
|
controlsVisible.value = true;
|
|
258
|
+
emitMediaPayload(event, (payload) => emit("pause", payload));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function markEnded(event) {
|
|
262
|
+
if (isDisposed || shouldIgnoreMediaEvent()) return;
|
|
263
|
+
isPlaying.value = false;
|
|
264
|
+
controlsVisible.value = true;
|
|
265
|
+
emitMediaPayload(event, (payload) => emit("ended", payload));
|
|
185
266
|
}
|
|
186
267
|
|
|
187
268
|
function updateFullscreen() {
|
|
@@ -196,7 +277,7 @@ async function togglePlayback() {
|
|
|
196
277
|
try {
|
|
197
278
|
await media.play();
|
|
198
279
|
} catch (error) {
|
|
199
|
-
if (
|
|
280
|
+
if (error?.name !== "AbortError") {
|
|
200
281
|
throw error;
|
|
201
282
|
}
|
|
202
283
|
}
|
|
@@ -283,33 +364,56 @@ function seekWithKeyboard(event) {
|
|
|
283
364
|
|
|
284
365
|
function stopPlayback() {
|
|
285
366
|
if (isDisposed) return;
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
367
|
+
window.setTimeout(() => {
|
|
368
|
+
if (isDisposed) return;
|
|
369
|
+
const media = getMedia();
|
|
370
|
+
if (!media) return;
|
|
371
|
+
ignoreMediaEventsFor(300);
|
|
372
|
+
media.pause();
|
|
373
|
+
media.currentTime = 0;
|
|
374
|
+
}, 0);
|
|
292
375
|
}
|
|
293
376
|
|
|
294
377
|
function setVolume(event) {
|
|
295
378
|
if (isDisposed) return;
|
|
296
379
|
const nextVolume = Number(event.target.value);
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
380
|
+
window.setTimeout(() => {
|
|
381
|
+
if (isDisposed) return;
|
|
382
|
+
const media = getMedia();
|
|
383
|
+
if (!media) return;
|
|
384
|
+
media.volume = nextVolume;
|
|
385
|
+
media.muted = nextVolume === 0;
|
|
386
|
+
}, 0);
|
|
300
387
|
}
|
|
301
388
|
|
|
302
|
-
function toggleMute() {
|
|
389
|
+
function toggleMute(event) {
|
|
303
390
|
if (isDisposed) return;
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
391
|
+
const button = event.currentTarget;
|
|
392
|
+
window.setTimeout(() => {
|
|
393
|
+
if (isDisposed) return;
|
|
394
|
+
const media = getMedia();
|
|
395
|
+
if (!media) return;
|
|
396
|
+
media.muted = !media.muted;
|
|
397
|
+
const icon = button.querySelector(".fa-icon");
|
|
398
|
+
icon?.classList.toggle("icon-volume", !media.muted);
|
|
399
|
+
icon?.classList.toggle("icon-mute", media.muted);
|
|
400
|
+
button.setAttribute("aria-label", media.muted ? "Unmute video" : "Mute video");
|
|
401
|
+
}, 0);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function cycleRate(event) {
|
|
309
405
|
if (isDisposed) return;
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
406
|
+
const button = event.currentTarget;
|
|
407
|
+
window.setTimeout(() => {
|
|
408
|
+
if (isDisposed) return;
|
|
409
|
+
rateIndex = (rateIndex + 1) % rates.length;
|
|
410
|
+
playbackRate = rates[rateIndex];
|
|
411
|
+
const media = getMedia();
|
|
412
|
+
if (media) {
|
|
413
|
+
media.playbackRate = playbackRate;
|
|
414
|
+
}
|
|
415
|
+
button.textContent = playbackRate + "x";
|
|
416
|
+
}, 0);
|
|
313
417
|
}
|
|
314
418
|
|
|
315
419
|
function toggleFullscreen() {
|
package/package.json
CHANGED
|
@@ -1,11 +1,34 @@
|
|
|
1
1
|
import type { MikuruComponent } from "../env";
|
|
2
2
|
|
|
3
|
+
export type MikuruAudioPlayerEventPayload = {
|
|
4
|
+
currentTime: number;
|
|
5
|
+
duration: number;
|
|
6
|
+
paused: boolean;
|
|
7
|
+
ended: boolean;
|
|
8
|
+
muted: boolean;
|
|
9
|
+
volume: number;
|
|
10
|
+
playbackRate: number;
|
|
11
|
+
nativeEvent?: Event;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type MikuruAudioPlayerEvents = {
|
|
15
|
+
onLoadedmetadata?: (payload: MikuruAudioPlayerEventPayload) => void;
|
|
16
|
+
onTimeupdate?: (payload: MikuruAudioPlayerEventPayload) => void;
|
|
17
|
+
onDurationchange?: (payload: MikuruAudioPlayerEventPayload) => void;
|
|
18
|
+
onPlay?: (payload: MikuruAudioPlayerEventPayload) => void;
|
|
19
|
+
onPause?: (payload: MikuruAudioPlayerEventPayload) => void;
|
|
20
|
+
onEnded?: (payload: MikuruAudioPlayerEventPayload) => void;
|
|
21
|
+
onSeeked?: (payload: MikuruAudioPlayerEventPayload) => void;
|
|
22
|
+
onVolumechange?: (payload: MikuruAudioPlayerEventPayload) => void;
|
|
23
|
+
onRatechange?: (payload: MikuruAudioPlayerEventPayload) => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
3
26
|
export type MikuruAudioPlayerProps = {
|
|
4
27
|
src: string;
|
|
5
28
|
title?: string;
|
|
6
29
|
artist?: string;
|
|
7
30
|
preload?: string;
|
|
8
|
-
};
|
|
31
|
+
} & MikuruAudioPlayerEvents;
|
|
9
32
|
|
|
10
33
|
declare const component: MikuruComponent<MikuruAudioPlayerProps>;
|
|
11
34
|
export default component;
|
|
@@ -1,12 +1,35 @@
|
|
|
1
1
|
import type { MikuruComponent } from "../env";
|
|
2
2
|
|
|
3
|
+
export type MikuruVideoPlayerEventPayload = {
|
|
4
|
+
currentTime: number;
|
|
5
|
+
duration: number;
|
|
6
|
+
paused: boolean;
|
|
7
|
+
ended: boolean;
|
|
8
|
+
muted: boolean;
|
|
9
|
+
volume: number;
|
|
10
|
+
playbackRate: number;
|
|
11
|
+
nativeEvent?: Event;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type MikuruVideoPlayerEvents = {
|
|
15
|
+
onLoadedmetadata?: (payload: MikuruVideoPlayerEventPayload) => void;
|
|
16
|
+
onTimeupdate?: (payload: MikuruVideoPlayerEventPayload) => void;
|
|
17
|
+
onDurationchange?: (payload: MikuruVideoPlayerEventPayload) => void;
|
|
18
|
+
onPlay?: (payload: MikuruVideoPlayerEventPayload) => void;
|
|
19
|
+
onPause?: (payload: MikuruVideoPlayerEventPayload) => void;
|
|
20
|
+
onEnded?: (payload: MikuruVideoPlayerEventPayload) => void;
|
|
21
|
+
onSeeked?: (payload: MikuruVideoPlayerEventPayload) => void;
|
|
22
|
+
onVolumechange?: (payload: MikuruVideoPlayerEventPayload) => void;
|
|
23
|
+
onRatechange?: (payload: MikuruVideoPlayerEventPayload) => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
3
26
|
export type MikuruVideoPlayerProps = {
|
|
4
27
|
src: string;
|
|
5
28
|
poster?: string;
|
|
6
29
|
title?: string;
|
|
7
30
|
subtitle?: string;
|
|
8
31
|
preload?: string;
|
|
9
|
-
};
|
|
32
|
+
} & MikuruVideoPlayerEvents;
|
|
10
33
|
|
|
11
34
|
declare const component: MikuruComponent<MikuruVideoPlayerProps>;
|
|
12
35
|
export default component;
|