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="syncMedia"
8
- @timeupdate="syncMedia"
9
- @durationchange="syncMedia"
7
+ @loadedmetadata="handleLoadedMetadata"
8
+ @timeupdate="handleTimeUpdate"
9
+ @durationchange="handleDurationChange"
10
10
  @play="markPlaying"
11
11
  @pause="markPaused"
12
- @ended="markPaused"
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 m-if="isPlaying">Pause</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 m-if="muted">Muted</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
- volume.value = media.volume;
101
- muted.value = media.muted;
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 (!isDisposed && error?.name !== "AbortError") {
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
- volume.value = nextVolume;
152
- muted.value = nextVolume === 0;
153
- applyAudioSettings();
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
- muted.value = !muted.value;
159
- applyAudioSettings();
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
- media.currentTime = nextTime;
176
- currentTime.value = nextTime;
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="syncMedia"
21
- @timeupdate="syncMedia"
22
- @durationchange="syncMedia"
20
+ @loadedmetadata="handleLoadedMetadata"
21
+ @timeupdate="handleTimeUpdate"
22
+ @durationchange="handleDurationChange"
23
23
  @play="markPlaying"
24
24
  @pause="markPaused"
25
- @ended="markPaused"
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 m-if="isPlaying" class="fa-icon icon-pause large" aria-hidden="true"></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 m-if="isPlaying" class="fa-icon icon-pause" aria-hidden="true"></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 m-if="muted" class="fa-icon icon-mute" aria-hidden="true"></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">{{ playbackRate }}x</button>
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 m-if="isFullscreen" class="fa-icon icon-minimize" aria-hidden="true"></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
- volume.value = media.volume;
162
- muted.value = media.muted;
163
- playbackRate.value = media.playbackRate;
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.value;
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 (!isDisposed && error?.name !== "AbortError") {
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
- const media = getMedia();
287
- if (!media) return;
288
- media.pause();
289
- media.currentTime = 0;
290
- currentTime.value = 0;
291
- isPlaying.value = false;
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
- volume.value = nextVolume;
298
- muted.value = nextVolume === 0;
299
- applyAudioSettings();
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
- muted.value = !muted.value;
305
- applyAudioSettings();
306
- }
307
-
308
- function cycleRate() {
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 currentIndex = rates.indexOf(playbackRate.value);
311
- playbackRate.value = rates[(currentIndex + 1) % rates.length];
312
- applyAudioSettings();
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,6 +1,6 @@
1
1
  {
2
2
  "name": "mikuru",
3
- "version": "1.0.29",
3
+ "version": "1.0.31",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "A compile-first JavaScript framework with Vue-like authoring and Svelte-like generated DOM updates.",
@@ -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;