mpegts-vue3 0.3.0 → 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",
@@ -34,18 +44,43 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
34
44
  type: { default: "mse" },
35
45
  cors: { type: Boolean },
36
46
  withCredentials: { type: Boolean },
37
- hasAudio: { type: Boolean },
47
+ hasAudio: {
48
+ type: Boolean,
49
+ default: true
50
+ },
38
51
  hasVideo: {
39
52
  type: Boolean,
40
53
  default: true
41
54
  },
42
55
  duration: {},
43
56
  filesize: {},
44
- 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
+ }) }
45
71
  },
46
- emits: ["error", "status"],
72
+ emits: [
73
+ "error",
74
+ "status",
75
+ "statistics",
76
+ "mediaInfo",
77
+ "recovered",
78
+ "ended"
79
+ ],
47
80
  setup(__props, { expose: __expose, emit: __emit }) {
48
81
  const DEFAULT_CONFIG = {
82
+ enableWorker: true,
83
+ reuseRedirectedURL: true,
49
84
  enableStashBuffer: false,
50
85
  liveBufferLatencyChasing: true,
51
86
  liveBufferLatencyChasingOnPaused: true,
@@ -59,15 +94,135 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
59
94
  autoCleanupMinBackwardDuration: 10,
60
95
  fixAudioTimestampGap: true
61
96
  };
97
+ const MSE_REQUIRED_TYPES = [
98
+ "mse",
99
+ "mpegts",
100
+ "m2ts",
101
+ "flv"
102
+ ];
62
103
  const props = __props;
63
104
  const emit = __emit;
64
105
  const videoRef = ref();
65
106
  const status = ref("nosignal");
66
107
  let player = null;
67
- const videoStyle = computed(() => ({ objectFit: props.objectFit ?? "fill" }));
68
- 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
+ }
69
225
  if (!player) return;
70
- status.value = "destroying";
71
226
  try {
72
227
  player.pause();
73
228
  player.unload();
@@ -75,23 +230,25 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
75
230
  player.destroy();
76
231
  } catch {}
77
232
  player = null;
78
- status.value = "nosignal";
233
+ if (!silent) {
234
+ status.value = "nosignal";
235
+ emit("status", "nosignal");
236
+ }
79
237
  }
80
238
  function play() {
81
239
  if (!player) return;
240
+ const myGen = gen;
82
241
  videoRef.value.muted = props.muted;
83
242
  const result = player.play();
84
243
  if (result instanceof Promise) result.then(() => {
85
- status.value = "playing";
86
- emit("status", "playing");
244
+ if (myGen !== gen) return;
245
+ markPlaying();
87
246
  }).catch(() => {
247
+ if (myGen !== gen) return;
88
248
  status.value = "stopped";
89
249
  emit("status", "stopped");
90
250
  });
91
- else {
92
- status.value = "playing";
93
- emit("status", "playing");
94
- }
251
+ else markPlaying();
95
252
  }
96
253
  function pause() {
97
254
  if (!player) return;
@@ -99,9 +256,45 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
99
256
  status.value = "stopped";
100
257
  emit("status", "stopped");
101
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
+ }
102
286
  __expose({
103
287
  play,
104
- pause
288
+ pause,
289
+ reload,
290
+ setMuted,
291
+ getPlayer,
292
+ getVolume,
293
+ setVolume,
294
+ seek,
295
+ getCurrentTime,
296
+ getBufferedRanges,
297
+ getStatistics
105
298
  });
106
299
  function buildMediaDataSource() {
107
300
  const source = {
@@ -111,18 +304,20 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
111
304
  };
112
305
  if (props.cors !== void 0) source.cors = props.cors;
113
306
  if (props.withCredentials !== void 0) source.withCredentials = props.withCredentials;
114
- if (props.hasAudio !== void 0) source.hasAudio = props.hasAudio;
115
- if (props.hasVideo !== void 0) source.hasVideo = props.hasVideo;
307
+ source.hasAudio = props.hasAudio;
308
+ source.hasVideo = props.hasVideo;
116
309
  if (props.duration !== void 0) source.duration = props.duration;
117
310
  if (props.filesize !== void 0) source.filesize = props.filesize;
118
311
  return source;
119
312
  }
120
313
  function createPlayer() {
121
- destroyPlayer();
314
+ destroyPlayer(true);
315
+ const myGen = gen;
122
316
  if (!props.url || !videoRef.value) return;
123
- if (!Mpegts.isSupported()) {
317
+ if (MSE_REQUIRED_TYPES.includes(props.type) && !Mpegts$1.isSupported()) {
124
318
  status.value = "error";
125
319
  emit("status", "error");
320
+ emit("error", "NotSupportedError", "MediaSource Extensions (MSE) are not supported by this browser", { type: props.type });
126
321
  return;
127
322
  }
128
323
  status.value = "connecting";
@@ -132,40 +327,66 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
132
327
  ...props.config
133
328
  };
134
329
  const mediaSource = buildMediaDataSource();
135
- player = Mpegts.createPlayer(mediaSource, mergedConfig);
330
+ player = Mpegts$1.createPlayer(mediaSource, mergedConfig);
136
331
  player.attachMediaElement(videoRef.value);
137
- 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;
138
336
  status.value = "error";
139
337
  emit("status", "error");
140
- 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");
141
354
  });
142
355
  player.load();
143
356
  if (props.autoplay) {
144
357
  videoRef.value.muted = props.muted;
145
358
  const result = player.play();
146
359
  if (result instanceof Promise) result.then(() => {
147
- status.value = "playing";
148
- emit("status", "playing");
360
+ if (myGen !== gen) return;
361
+ markPlaying();
149
362
  }).catch(() => {
150
- status.value = "stopped";
151
- emit("status", "stopped");
363
+ if (myGen !== gen) return;
364
+ if (!props.muted && videoRef.value && player) {
365
+ videoRef.value.muted = true;
366
+ const fallbackResult = player.play();
367
+ if (fallbackResult instanceof Promise) fallbackResult.then(() => {
368
+ if (myGen !== gen) return;
369
+ markPlaying();
370
+ }).catch(() => {
371
+ if (myGen !== gen) return;
372
+ status.value = "stopped";
373
+ emit("status", "stopped");
374
+ });
375
+ else markPlaying();
376
+ } else {
377
+ status.value = "stopped";
378
+ emit("status", "stopped");
379
+ }
152
380
  });
153
- else {
154
- status.value = "playing";
155
- emit("status", "playing");
156
- }
381
+ else markPlaying();
157
382
  }
158
383
  }
159
384
  watch(() => props.url, (newUrl) => {
160
- if (newUrl) createPlayer();
161
- else {
162
- destroyPlayer();
163
- status.value = "nosignal";
164
- emit("status", "nosignal");
165
- }
385
+ if (newUrl) scheduleRecreate();
386
+ else destroyPlayer();
166
387
  });
167
388
  watch(() => props.config, () => {
168
- if (props.url) createPlayer();
389
+ if (props.url) scheduleRecreate();
169
390
  }, { deep: true });
170
391
  watch(() => [
171
392
  props.type,
@@ -177,9 +398,10 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
177
398
  props.duration,
178
399
  props.filesize
179
400
  ], () => {
180
- if (props.url) createPlayer();
401
+ if (props.url) scheduleRecreate();
181
402
  });
182
403
  onMounted(() => {
404
+ ensureKeyframe();
183
405
  if (props.url) createPlayer();
184
406
  });
185
407
  watch(() => props.muted, (val) => {
@@ -189,31 +411,44 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
189
411
  destroyPlayer();
190
412
  });
191
413
  return (_ctx, _cache) => {
192
- return openBlock(), createElementBlock("div", _hoisted_1, [
414
+ return openBlock(), createElementBlock("div", {
415
+ class: "mpegts-player",
416
+ style: containerStyle
417
+ }, [
193
418
  createElementVNode("video", {
194
419
  ref_key: "videoRef",
195
420
  ref: videoRef,
196
- class: "absolute inset-0 w-full h-full",
197
421
  style: normalizeStyle(videoStyle.value),
198
422
  onClick: _cache[0] || (_cache[0] = withModifiers(() => {}, ["prevent"])),
199
423
  onContextmenu: _cache[1] || (_cache[1] = withModifiers(() => {}, ["prevent"]))
200
424
  }, null, 36),
201
- 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),
202
- 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),
203
- 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", {
204
- class: "size-8 text-red-400",
205
- fill: "none",
206
- stroke: "currentColor",
207
- "stroke-width": "1.5",
208
- viewBox: "0 0 24 24"
209
- }, [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", {
210
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",
211
446
  "stroke-linecap": "round",
212
447
  "stroke-linejoin": "round"
213
- })]), 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)
214
449
  ]);
215
450
  };
216
451
  }
217
452
  });
218
453
  //#endregion
219
- 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.0",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "description": "Vue 3 component for mpegts.js video streaming player",
6
6
  "keywords": [
@@ -15,7 +15,7 @@
15
15
  "author": "",
16
16
  "repository": {
17
17
  "type": "git",
18
- "url": "https://github.com/huangzida/mpegts-vue3"
18
+ "url": "git+https://github.com/huangzida/mpegts-vue3.git"
19
19
  },
20
20
  "main": "./dist/index.cjs",
21
21
  "module": "./dist/index.js",
@@ -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",