mpegts-vue3 0.3.2 → 0.4.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/dist/index.js CHANGED
@@ -1,18 +1,28 @@
1
- import { computed, createCommentVNode, createElementBlock, createElementVNode, createStaticVNode, defineComponent, normalizeStyle, onMounted, onUnmounted, openBlock, ref, watch, withModifiers } from "vue";
2
- import Mpegts from "mpegts.js";
1
+ import { computed, createCommentVNode, createElementBlock, createElementVNode, defineComponent, normalizeStyle, onMounted, onUnmounted, openBlock, ref, watch, withModifiers } from "vue";
2
+ import Mpegts, { default as Mpegts$1 } from "mpegts.js";
3
3
  //#region src/components/MpegtsPlayer.vue
4
- const _hoisted_1 = { class: "relative w-full h-full bg-black overflow-hidden" };
5
- const _hoisted_2 = {
6
- key: 0,
7
- class: "absolute inset-0 flex flex-col items-center justify-center bg-gray-900/90"
8
- };
9
- const _hoisted_3 = {
10
- key: 1,
11
- class: "absolute inset-0 flex items-center justify-center bg-black/60"
4
+ const _hoisted_1 = {
5
+ style: {
6
+ width: "2.5rem",
7
+ height: "2.5rem",
8
+ marginBottom: "0.75rem",
9
+ color: "#9ca3af"
10
+ },
11
+ fill: "none",
12
+ stroke: "currentColor",
13
+ "stroke-width": "1.5",
14
+ viewBox: "0 0 24 24"
12
15
  };
13
- const _hoisted_4 = {
14
- key: 2,
15
- class: "absolute inset-0 flex items-center justify-center bg-black/60"
16
+ const _hoisted_2 = {
17
+ style: {
18
+ width: "2rem",
19
+ height: "2rem",
20
+ color: "#f87171"
21
+ },
22
+ fill: "none",
23
+ stroke: "currentColor",
24
+ "stroke-width": "1.5",
25
+ viewBox: "0 0 24 24"
16
26
  };
17
27
  const _sfc_main = /* @__PURE__ */ defineComponent({
18
28
  __name: "MpegtsPlayer",
@@ -44,11 +54,33 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
44
54
  },
45
55
  duration: {},
46
56
  filesize: {},
47
- config: { default: () => ({}) }
57
+ showLoading: {
58
+ type: Boolean,
59
+ default: true
60
+ },
61
+ config: { default: () => ({}) },
62
+ autoReconnect: {
63
+ type: Boolean,
64
+ default: true
65
+ },
66
+ reconnect: { default: () => ({
67
+ retries: 5,
68
+ minDelay: 1e3,
69
+ maxDelay: 16e3
70
+ }) }
48
71
  },
49
- emits: ["error", "status"],
72
+ emits: [
73
+ "error",
74
+ "status",
75
+ "statistics",
76
+ "mediaInfo",
77
+ "recovered",
78
+ "ended"
79
+ ],
50
80
  setup(__props, { expose: __expose, emit: __emit }) {
51
81
  const DEFAULT_CONFIG = {
82
+ enableWorker: true,
83
+ reuseRedirectedURL: true,
52
84
  enableStashBuffer: false,
53
85
  liveBufferLatencyChasing: true,
54
86
  liveBufferLatencyChasingOnPaused: true,
@@ -62,15 +94,135 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
62
94
  autoCleanupMinBackwardDuration: 10,
63
95
  fixAudioTimestampGap: true
64
96
  };
97
+ const MSE_REQUIRED_TYPES = [
98
+ "mse",
99
+ "mpegts",
100
+ "m2ts",
101
+ "flv"
102
+ ];
65
103
  const props = __props;
66
104
  const emit = __emit;
67
105
  const videoRef = ref();
68
106
  const status = ref("nosignal");
69
107
  let player = null;
70
- const videoStyle = computed(() => ({ objectFit: props.objectFit ?? "fill" }));
71
- function destroyPlayer() {
108
+ let gen = 0;
109
+ let recreateTimer = null;
110
+ function scheduleRecreate() {
111
+ if (recreateTimer) clearTimeout(recreateTimer);
112
+ recreateTimer = setTimeout(() => {
113
+ recreateTimer = null;
114
+ createPlayer();
115
+ }, 300);
116
+ }
117
+ const RECONNECTABLE_ERRORS = new Set([
118
+ "Exception",
119
+ "ConnectingTimeout",
120
+ "UnrecoverableEarlyEof"
121
+ ]);
122
+ let reconnectAttempts = 0;
123
+ let reconnectTimer = null;
124
+ function markPlaying() {
125
+ reconnectAttempts = 0;
126
+ status.value = "playing";
127
+ emit("status", "playing");
128
+ }
129
+ function scheduleReconnect() {
130
+ const { retries = 5, minDelay = 1e3, maxDelay = 16e3 } = props.reconnect;
131
+ if (reconnectAttempts >= retries) return false;
132
+ const delay = Math.min(maxDelay, minDelay * 2 ** reconnectAttempts);
133
+ reconnectAttempts++;
134
+ status.value = "reconnecting";
135
+ emit("status", "reconnecting");
136
+ reconnectTimer = setTimeout(() => {
137
+ reconnectTimer = null;
138
+ createPlayer();
139
+ }, delay);
140
+ return true;
141
+ }
142
+ const videoStyle = computed(() => ({
143
+ position: "absolute",
144
+ inset: 0,
145
+ width: "100%",
146
+ height: "100%",
147
+ objectFit: props.objectFit ?? "fill"
148
+ }));
149
+ const containerStyle = {
150
+ position: "relative",
151
+ width: "100%",
152
+ height: "100%",
153
+ backgroundColor: "#000",
154
+ overflow: "hidden"
155
+ };
156
+ const overlayBase = {
157
+ position: "absolute",
158
+ inset: 0,
159
+ display: "flex",
160
+ alignItems: "center",
161
+ justifyContent: "center"
162
+ };
163
+ const noSignalOverlay = {
164
+ ...overlayBase,
165
+ flexDirection: "column",
166
+ backgroundColor: "rgb(17 24 39 / 0.9)"
167
+ };
168
+ const maskOverlay = {
169
+ ...overlayBase,
170
+ backgroundColor: "rgb(0 0 0 / 0.6)"
171
+ };
172
+ const connectingInner = {
173
+ display: "flex",
174
+ flexDirection: "column",
175
+ alignItems: "center",
176
+ gap: "0.75rem"
177
+ };
178
+ const errorInner = {
179
+ display: "flex",
180
+ flexDirection: "column",
181
+ alignItems: "center",
182
+ gap: "0.5rem"
183
+ };
184
+ const spinnerStyle = {
185
+ width: "2rem",
186
+ height: "2rem",
187
+ borderRadius: "9999px",
188
+ border: "2px solid #3b82f6",
189
+ borderTopColor: "transparent",
190
+ animation: "mpegts-spin 1s linear infinite"
191
+ };
192
+ const noSignalTextStyle = {
193
+ fontSize: "0.875rem",
194
+ fontWeight: 500,
195
+ color: "#9ca3af",
196
+ letterSpacing: "0.05em",
197
+ textTransform: "uppercase"
198
+ };
199
+ const connectingTextStyle = {
200
+ fontSize: "0.875rem",
201
+ color: "#d1d5db"
202
+ };
203
+ const errorTextStyle = {
204
+ fontSize: "0.875rem",
205
+ color: "#f87171"
206
+ };
207
+ let keyframeInjected = false;
208
+ function ensureKeyframe() {
209
+ if (keyframeInjected || typeof document === "undefined") return;
210
+ keyframeInjected = true;
211
+ const el = document.createElement("style");
212
+ el.textContent = "@keyframes mpegts-spin { to { transform: rotate(360deg) } }";
213
+ document.head.appendChild(el);
214
+ }
215
+ function destroyPlayer(silent = false) {
216
+ gen++;
217
+ if (recreateTimer) {
218
+ clearTimeout(recreateTimer);
219
+ recreateTimer = null;
220
+ }
221
+ if (reconnectTimer) {
222
+ clearTimeout(reconnectTimer);
223
+ reconnectTimer = null;
224
+ }
72
225
  if (!player) return;
73
- status.value = "destroying";
74
226
  try {
75
227
  player.pause();
76
228
  player.unload();
@@ -78,23 +230,25 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
78
230
  player.destroy();
79
231
  } catch {}
80
232
  player = null;
81
- status.value = "nosignal";
233
+ if (!silent) {
234
+ status.value = "nosignal";
235
+ emit("status", "nosignal");
236
+ }
82
237
  }
83
238
  function play() {
84
239
  if (!player) return;
240
+ const myGen = gen;
85
241
  videoRef.value.muted = props.muted;
86
242
  const result = player.play();
87
243
  if (result instanceof Promise) result.then(() => {
88
- status.value = "playing";
89
- emit("status", "playing");
244
+ if (myGen !== gen) return;
245
+ markPlaying();
90
246
  }).catch(() => {
247
+ if (myGen !== gen) return;
91
248
  status.value = "stopped";
92
249
  emit("status", "stopped");
93
250
  });
94
- else {
95
- status.value = "playing";
96
- emit("status", "playing");
97
- }
251
+ else markPlaying();
98
252
  }
99
253
  function pause() {
100
254
  if (!player) return;
@@ -102,9 +256,45 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
102
256
  status.value = "stopped";
103
257
  emit("status", "stopped");
104
258
  }
259
+ function reload() {
260
+ createPlayer();
261
+ }
262
+ function setMuted(muted) {
263
+ if (videoRef.value) videoRef.value.muted = muted;
264
+ }
265
+ function getPlayer() {
266
+ return player;
267
+ }
268
+ function getVolume() {
269
+ return videoRef.value?.volume ?? 0;
270
+ }
271
+ function setVolume(volume) {
272
+ if (videoRef.value) videoRef.value.volume = volume;
273
+ }
274
+ function seek(seconds) {
275
+ if (player) player.currentTime = seconds;
276
+ }
277
+ function getCurrentTime() {
278
+ return videoRef.value?.currentTime ?? 0;
279
+ }
280
+ function getBufferedRanges() {
281
+ return videoRef.value?.buffered ?? null;
282
+ }
283
+ function getStatistics() {
284
+ return player?.statisticsInfo ?? null;
285
+ }
105
286
  __expose({
106
287
  play,
107
- pause
288
+ pause,
289
+ reload,
290
+ setMuted,
291
+ getPlayer,
292
+ getVolume,
293
+ setVolume,
294
+ seek,
295
+ getCurrentTime,
296
+ getBufferedRanges,
297
+ getStatistics
108
298
  });
109
299
  function buildMediaDataSource() {
110
300
  const source = {
@@ -115,17 +305,19 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
115
305
  if (props.cors !== void 0) source.cors = props.cors;
116
306
  if (props.withCredentials !== void 0) source.withCredentials = props.withCredentials;
117
307
  source.hasAudio = props.hasAudio;
118
- if (props.hasVideo !== void 0) source.hasVideo = props.hasVideo;
308
+ source.hasVideo = props.hasVideo;
119
309
  if (props.duration !== void 0) source.duration = props.duration;
120
310
  if (props.filesize !== void 0) source.filesize = props.filesize;
121
311
  return source;
122
312
  }
123
313
  function createPlayer() {
124
- destroyPlayer();
314
+ destroyPlayer(true);
315
+ const myGen = gen;
125
316
  if (!props.url || !videoRef.value) return;
126
- if (!Mpegts.isSupported()) {
317
+ if (MSE_REQUIRED_TYPES.includes(props.type) && !Mpegts$1.isSupported()) {
127
318
  status.value = "error";
128
319
  emit("status", "error");
320
+ emit("error", "NotSupportedError", "MediaSource Extensions (MSE) are not supported by this browser", { type: props.type });
129
321
  return;
130
322
  }
131
323
  status.value = "connecting";
@@ -135,56 +327,66 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
135
327
  ...props.config
136
328
  };
137
329
  const mediaSource = buildMediaDataSource();
138
- player = Mpegts.createPlayer(mediaSource, mergedConfig);
330
+ player = Mpegts$1.createPlayer(mediaSource, mergedConfig);
139
331
  player.attachMediaElement(videoRef.value);
140
- player.on(Mpegts.Events.ERROR, (errorType, errorDetail, errorInfo) => {
332
+ player.on(Mpegts$1.Events.ERROR, (errorType, errorDetail, errorInfo) => {
333
+ if (myGen !== gen) return;
334
+ emit("error", errorType, errorDetail, errorInfo);
335
+ if (props.autoReconnect && errorType === "NetworkError" && RECONNECTABLE_ERRORS.has(errorDetail) && scheduleReconnect()) return;
141
336
  status.value = "error";
142
337
  emit("status", "error");
143
- emit("error", errorType, errorDetail, errorInfo);
338
+ });
339
+ player.on(Mpegts$1.Events.STATISTICS_INFO, (info) => {
340
+ if (myGen !== gen) return;
341
+ emit("statistics", info);
342
+ });
343
+ player.on(Mpegts$1.Events.MEDIA_INFO, (info) => {
344
+ if (myGen !== gen) return;
345
+ emit("mediaInfo", info);
346
+ });
347
+ player.on(Mpegts$1.Events.RECOVERED_EARLY_EOF, () => {
348
+ if (myGen !== gen) return;
349
+ emit("recovered");
350
+ });
351
+ player.on(Mpegts$1.Events.LOADING_COMPLETE, () => {
352
+ if (myGen !== gen) return;
353
+ emit("ended");
144
354
  });
145
355
  player.load();
146
356
  if (props.autoplay) {
147
357
  videoRef.value.muted = props.muted;
148
358
  const result = player.play();
149
359
  if (result instanceof Promise) result.then(() => {
150
- status.value = "playing";
151
- emit("status", "playing");
360
+ if (myGen !== gen) return;
361
+ markPlaying();
152
362
  }).catch(() => {
363
+ if (myGen !== gen) return;
153
364
  if (!props.muted && videoRef.value && player) {
154
365
  videoRef.value.muted = true;
155
366
  const fallbackResult = player.play();
156
367
  if (fallbackResult instanceof Promise) fallbackResult.then(() => {
157
- status.value = "playing";
158
- emit("status", "playing");
368
+ if (myGen !== gen) return;
369
+ markPlaying();
159
370
  }).catch(() => {
371
+ if (myGen !== gen) return;
160
372
  status.value = "stopped";
161
373
  emit("status", "stopped");
162
374
  });
163
- else {
164
- status.value = "playing";
165
- emit("status", "playing");
166
- }
375
+ else markPlaying();
167
376
  } else {
168
377
  status.value = "stopped";
169
378
  emit("status", "stopped");
170
379
  }
171
380
  });
172
- else {
173
- status.value = "playing";
174
- emit("status", "playing");
175
- }
381
+ else markPlaying();
176
382
  }
177
383
  }
178
384
  watch(() => props.url, (newUrl) => {
179
- if (newUrl) createPlayer();
180
- else {
181
- destroyPlayer();
182
- status.value = "nosignal";
183
- emit("status", "nosignal");
184
- }
385
+ if (newUrl) scheduleRecreate();
386
+ else destroyPlayer();
185
387
  });
186
388
  watch(() => props.config, () => {
187
- if (props.url) createPlayer();
389
+ if (props.url) scheduleRecreate();
188
390
  }, { deep: true });
189
391
  watch(() => [
190
392
  props.type,
@@ -196,9 +398,10 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
196
398
  props.duration,
197
399
  props.filesize
198
400
  ], () => {
199
- if (props.url) createPlayer();
401
+ if (props.url) scheduleRecreate();
200
402
  });
201
403
  onMounted(() => {
404
+ ensureKeyframe();
202
405
  if (props.url) createPlayer();
203
406
  });
204
407
  watch(() => props.muted, (val) => {
@@ -208,31 +411,44 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
208
411
  destroyPlayer();
209
412
  });
210
413
  return (_ctx, _cache) => {
211
- return openBlock(), createElementBlock("div", _hoisted_1, [
414
+ return openBlock(), createElementBlock("div", {
415
+ class: "mpegts-player",
416
+ style: containerStyle
417
+ }, [
212
418
  createElementVNode("video", {
213
419
  ref_key: "videoRef",
214
420
  ref: videoRef,
215
- class: "absolute inset-0 w-full h-full",
216
421
  style: normalizeStyle(videoStyle.value),
217
422
  onClick: _cache[0] || (_cache[0] = withModifiers(() => {}, ["prevent"])),
218
423
  onContextmenu: _cache[1] || (_cache[1] = withModifiers(() => {}, ["prevent"]))
219
424
  }, null, 36),
220
- status.value === "nosignal" || !__props.url ? (openBlock(), createElementBlock("div", _hoisted_2, [..._cache[2] || (_cache[2] = [createStaticVNode("<div class=\"mb-3 flex items-center gap-2 text-gray-400\"><svg class=\"size-10\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" viewBox=\"0 0 24 24\"><path d=\"m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></path><path d=\"M18 12 6 12M18 8 6 8M18 16l-12 0\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></path></svg></div><span class=\"text-sm font-medium text-gray-400 tracking-wider uppercase\"> No Signal </span>", 2)])])) : createCommentVNode("v-if", true),
221
- status.value === "connecting" ? (openBlock(), createElementBlock("div", _hoisted_3, [..._cache[3] || (_cache[3] = [createElementVNode("div", { class: "flex flex-col items-center gap-3" }, [createElementVNode("div", { class: "size-8 rounded-full border-2 border-blue-500 border-t-transparent animate-spin" }), createElementVNode("span", { class: "text-sm text-gray-300" }, "Connecting...")], -1)])])) : createCommentVNode("v-if", true),
222
- status.value === "error" && __props.url ? (openBlock(), createElementBlock("div", _hoisted_4, [..._cache[4] || (_cache[4] = [createElementVNode("div", { class: "flex flex-col items-center gap-2" }, [createElementVNode("svg", {
223
- class: "size-8 text-red-400",
224
- fill: "none",
225
- stroke: "currentColor",
226
- "stroke-width": "1.5",
227
- viewBox: "0 0 24 24"
228
- }, [createElementVNode("path", {
425
+ status.value === "nosignal" || !__props.url ? (openBlock(), createElementBlock("div", {
426
+ key: 0,
427
+ style: noSignalOverlay
428
+ }, [(openBlock(), createElementBlock("svg", _hoisted_1, [..._cache[2] || (_cache[2] = [createElementVNode("path", {
429
+ d: "m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z",
430
+ "stroke-linecap": "round",
431
+ "stroke-linejoin": "round"
432
+ }, null, -1), createElementVNode("path", {
433
+ d: "M18 12 6 12M18 8 6 8M18 16l-12 0",
434
+ "stroke-linecap": "round",
435
+ "stroke-linejoin": "round"
436
+ }, null, -1)])])), createElementVNode("span", { style: noSignalTextStyle }, "No Signal")])) : createCommentVNode("v-if", true),
437
+ status.value === "connecting" && __props.showLoading ? (openBlock(), createElementBlock("div", {
438
+ key: 1,
439
+ style: maskOverlay
440
+ }, [createElementVNode("div", { style: connectingInner }, [createElementVNode("div", { style: spinnerStyle }), createElementVNode("span", { style: connectingTextStyle }, "Connecting...")])])) : createCommentVNode("v-if", true),
441
+ status.value === "error" && __props.url ? (openBlock(), createElementBlock("div", {
442
+ key: 2,
443
+ style: maskOverlay
444
+ }, [createElementVNode("div", { style: errorInner }, [(openBlock(), createElementBlock("svg", _hoisted_2, [..._cache[3] || (_cache[3] = [createElementVNode("path", {
229
445
  d: "M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z",
230
446
  "stroke-linecap": "round",
231
447
  "stroke-linejoin": "round"
232
- })]), createElementVNode("span", { class: "text-sm text-red-400" }, "Connection Failed")], -1)])])) : createCommentVNode("v-if", true)
448
+ }, null, -1)])])), createElementVNode("span", { style: errorTextStyle }, "Connection Failed")])])) : createCommentVNode("v-if", true)
233
449
  ]);
234
450
  };
235
451
  }
236
452
  });
237
453
  //#endregion
238
- export { _sfc_main as MpegtsPlayer };
454
+ export { Mpegts, _sfc_main as MpegtsPlayer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mpegts-vue3",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "description": "Vue 3 component for mpegts.js video streaming player",
6
6
  "keywords": [
@@ -37,14 +37,18 @@
37
37
  ],
38
38
  "sideEffects": false,
39
39
  "scripts": {
40
- "build": "tsdown && node -e \"const fs=require('fs'),d='dist';fs.readdirSync(d).filter(f=>f.startsWith('index-')&&f.endsWith('.d.ts')).forEach(f=>fs.renameSync(d+'/'+f,d+'/index.d.ts'));fs.readdirSync(d).filter(f=>f.startsWith('index-')&&f.endsWith('.d.cts')).forEach(f=>fs.renameSync(d+'/'+f,d+'/index.d.cts'))\"",
41
- "dev": "tsdown --watch"
40
+ "build": "tsdown && node ../../scripts/fix-dts.mjs",
41
+ "dev": "tsdown --watch",
42
+ "test": "vitest run",
43
+ "typecheck": "vue-tsc --noEmit"
42
44
  },
43
45
  "peerDependencies": {
44
46
  "mpegts.js": "^1.8.0",
45
47
  "vue": "^3.4.0"
46
48
  },
47
49
  "devDependencies": {
50
+ "@vitejs/plugin-vue": "^5.2.0",
51
+ "@vue/test-utils": "^2.4.11",
48
52
  "mpegts.js": "^1.8.0",
49
53
  "tsdown": "^0.12.0",
50
54
  "typescript": "^5.8.0",