@banou/media-player 0.8.7 → 0.8.9
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/engine/index.js +1 -1
- package/dist/{engine-B6flRjLd.js → engine-BwK7ycv0.js} +8 -3
- package/dist/index.js +24 -6
- package/package.json +1 -1
- package/src/lib/engine/playback.ts +28 -1
- package/src/lib/react/components/control-bar.tsx +5 -0
- package/src/lib/react/components/settings.tsx +2 -0
- package/src/lib/react/components/sound.tsx +4 -0
- package/src/lib/react/components/subtitles.tsx +8 -1
- package/src/lib/react/hooks/use-playback.ts +18 -5
package/dist/engine/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as e, c as t, i as n, l as r, n as i, o as a, r as o, s, t as c, u as l } from "../engine-
|
|
1
|
+
import { a as e, c as t, i as n, l as r, n as i, o as a, r as o, s, t as c, u as l } from "../engine-BwK7ycv0.js";
|
|
2
2
|
export { n as DEFAULT_BUFFER_SIZE, s as SUBTITLES_OFF, c as createPictureInPicture, t as createSubtitleRenderer, o as createThumbnailGenerator, r as getTimeRanges, i as pictureInPictureMode, e as startPlayback, a as terminateRemuxer, l as updateSourceBuffer };
|
|
@@ -201,7 +201,12 @@ var a = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
|
|
|
201
201
|
let c = [e.info.output.videoMimeType, e.info.output.audioMimeType].filter(Boolean).join(","), l = `video/mp4; codecs="${c}"`;
|
|
202
202
|
if (!MediaSource.isTypeSupported(l)) throw Error(`This browser cannot play the codecs in this file: ${c}`);
|
|
203
203
|
let C = new MediaSource(), w = URL.createObjectURL(C);
|
|
204
|
-
N.push(() => URL.revokeObjectURL(w)), n.src = w
|
|
204
|
+
N.push(() => URL.revokeObjectURL(w)), n.src = w, N.push(() => {
|
|
205
|
+
try {
|
|
206
|
+
n.pause();
|
|
207
|
+
} catch {}
|
|
208
|
+
n.removeAttribute("src"), n.load();
|
|
209
|
+
});
|
|
205
210
|
let T = await new Promise((t, n) => {
|
|
206
211
|
let r = setTimeout(() => {
|
|
207
212
|
i(), n(/* @__PURE__ */ Error("The MediaSource never opened"));
|
|
@@ -248,8 +253,8 @@ var a = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
|
|
|
248
253
|
V = !0, G = null, K = 0, te = e;
|
|
249
254
|
let t = ++W;
|
|
250
255
|
try {
|
|
251
|
-
let { data: n, pts: r, subtitles: i } = await M.seek(e);
|
|
252
|
-
if (P || t !== W || (i?.length && o.pushFragments(i), await R(r), P || t !== W)) return;
|
|
256
|
+
let { data: n, pts: r, subtitles: i, finished: a } = await M.seek(e);
|
|
257
|
+
if (P || t !== W || (a && (H = !0), i?.length && o.pushFragments(i), await R(r), P || t !== W)) return;
|
|
253
258
|
n.byteLength && (await I(n), Q());
|
|
254
259
|
} catch (e) {
|
|
255
260
|
Y(e, P || t !== W);
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as e, c as t, n, r, s as i, t as a } from "./engine-
|
|
1
|
+
import { a as e, c as t, n, r, s as i, t as a } from "./engine-BwK7ycv0.js";
|
|
2
2
|
import { Children as o, Fragment as s, isValidElement as c, useCallback as l, useEffect as u, useMemo as d, useRef as f, useState as p } from "react";
|
|
3
3
|
import { css as m, keyframes as h } from "@emotion/react";
|
|
4
4
|
import { createPlayer as g, useContainerAttach as _, useMediaAttach as v } from "@videojs/react";
|
|
@@ -61,7 +61,7 @@ var re = {
|
|
|
61
61
|
id: e.streamIndex,
|
|
62
62
|
label: t
|
|
63
63
|
})), oe = (t, n, r) => {
|
|
64
|
-
let i = F(), { read: a, size: o, publicPath: s = "", libavWorkerUrl: c = "", jassubWorkerUrl: d = "", jassubWasmUrl: m = "", jassubLegacyWasmUrl: h, defaultFontUrl: g, bufferSize: _, autoplay: v = !1 } = r ?? {}, [y, b] = p(void 0), x = f(null), S = f(
|
|
64
|
+
let i = F(), { read: a, size: o, publicPath: s = "", libavWorkerUrl: c = "", jassubWorkerUrl: d = "", jassubWasmUrl: m = "", jassubLegacyWasmUrl: h, defaultFontUrl: g, bufferSize: _, autoplay: v = !1 } = r ?? {}, [y, b] = p(void 0), x = f(null), S = f(null), C = f(!1), w = f(a);
|
|
65
65
|
w.current = a;
|
|
66
66
|
let T = f(r?.onSeek);
|
|
67
67
|
T.current = r?.onSeek;
|
|
@@ -108,7 +108,10 @@ var re = {
|
|
|
108
108
|
bufferSize: _,
|
|
109
109
|
audioStreamIndex: y,
|
|
110
110
|
onReady: () => {
|
|
111
|
-
|
|
111
|
+
if (r) return;
|
|
112
|
+
i.setSourceState({ ready: !0 });
|
|
113
|
+
let e = S.current;
|
|
114
|
+
S.current = null, e && e.size === o && e.time > 0 && (t.currentTime = e.time), v && t.play().catch(() => {});
|
|
112
115
|
},
|
|
113
116
|
onError: l,
|
|
114
117
|
onRecovered: () => {
|
|
@@ -139,7 +142,10 @@ var re = {
|
|
|
139
142
|
l(e);
|
|
140
143
|
}
|
|
141
144
|
})(), () => {
|
|
142
|
-
r = !0, S.current =
|
|
145
|
+
r = !0, S.current = {
|
|
146
|
+
time: t.currentTime,
|
|
147
|
+
size: o
|
|
148
|
+
}, x.current?.destroy(), x.current = null, i.setSourceState({ ready: !1 });
|
|
143
149
|
};
|
|
144
150
|
}, [
|
|
145
151
|
i,
|
|
@@ -1088,6 +1094,8 @@ ${Y}
|
|
|
1088
1094
|
className: "settings",
|
|
1089
1095
|
type: "button",
|
|
1090
1096
|
onClick: c,
|
|
1097
|
+
"aria-label": "Settings",
|
|
1098
|
+
"aria-expanded": s,
|
|
1091
1099
|
children: /* @__PURE__ */ S(j, {})
|
|
1092
1100
|
}),
|
|
1093
1101
|
toolTipText: /* @__PURE__ */ S("span", { children: "Settings" })
|
|
@@ -1170,7 +1178,9 @@ position: static;
|
|
|
1170
1178
|
${Y}
|
|
1171
1179
|
`, Oe = () => {
|
|
1172
1180
|
let e = F((e) => e.subtitleTracks), t = F((e) => e.selectedSubtitleTrack), n = F((e) => e.selectSubtitleTrack), r = F((e) => e.subtitleOffLabel), { open: i, toggle: a, containerRef: o, pending: s, failed: c, runSelect: l } = Z();
|
|
1173
|
-
|
|
1181
|
+
if (e.length === 0) return null;
|
|
1182
|
+
let u = t !== void 0;
|
|
1183
|
+
return /* @__PURE__ */ C("div", {
|
|
1174
1184
|
css: De,
|
|
1175
1185
|
ref: o,
|
|
1176
1186
|
children: [/* @__PURE__ */ S(K, {
|
|
@@ -1180,7 +1190,10 @@ ${Y}
|
|
|
1180
1190
|
className: "subtitles",
|
|
1181
1191
|
type: "button",
|
|
1182
1192
|
onClick: a,
|
|
1183
|
-
|
|
1193
|
+
"aria-label": "Subtitles",
|
|
1194
|
+
"aria-expanded": i,
|
|
1195
|
+
"aria-pressed": u,
|
|
1196
|
+
children: u ? /* @__PURE__ */ S(be, { className: "captions" }) : /* @__PURE__ */ S(xe, { className: "captions-off" })
|
|
1184
1197
|
}),
|
|
1185
1198
|
toolTipText: /* @__PURE__ */ S("span", { children: "Subtitles" })
|
|
1186
1199
|
}), i && /* @__PURE__ */ S(X, {
|
|
@@ -1356,6 +1369,8 @@ ${Y}
|
|
|
1356
1369
|
type: "button",
|
|
1357
1370
|
onClick: () => t.toggleMuted(),
|
|
1358
1371
|
ref: e,
|
|
1372
|
+
"aria-label": "Mute",
|
|
1373
|
+
"aria-pressed": r || n === 0,
|
|
1359
1374
|
children: S(r || n === 0 ? te : n <= .5 ? M : ee, {
|
|
1360
1375
|
size: 18,
|
|
1361
1376
|
color: "#fff"
|
|
@@ -1583,6 +1598,7 @@ ${Y}
|
|
|
1583
1598
|
className: "play",
|
|
1584
1599
|
type: "button",
|
|
1585
1600
|
onClick: () => e.togglePaused(),
|
|
1601
|
+
"aria-label": _ ? "Replay" : t ? "Play" : "Pause",
|
|
1586
1602
|
children: S(_ ? A : t ? k : O, {})
|
|
1587
1603
|
}),
|
|
1588
1604
|
toolTipText: /* @__PURE__ */ S("span", { children: _ ? "Replay (k)" : t ? "Play (k)" : "Pause (k)" })
|
|
@@ -1631,6 +1647,8 @@ ${Y}
|
|
|
1631
1647
|
className: "full-screen",
|
|
1632
1648
|
type: "button",
|
|
1633
1649
|
onClick: () => e.toggleFullscreen(),
|
|
1650
|
+
"aria-label": "Full screen",
|
|
1651
|
+
"aria-pressed": i,
|
|
1634
1652
|
children: S(i ? D : E, {})
|
|
1635
1653
|
}),
|
|
1636
1654
|
toolTipText: /* @__PURE__ */ S("span", { children: i ? "Exit full screen (f)" : "Full screen (f)" })
|
package/package.json
CHANGED
|
@@ -130,6 +130,28 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
|
|
|
130
130
|
const mediaSourceUrl = URL.createObjectURL(mediaSource)
|
|
131
131
|
teardown.push(() => URL.revokeObjectURL(mediaSourceUrl))
|
|
132
132
|
videoElement.src = mediaSourceUrl
|
|
133
|
+
/**
|
|
134
|
+
* Give the element back, before the url it is holding is revoked.
|
|
135
|
+
*
|
|
136
|
+
* Revoking alone detaches nothing: `src` still names the dead MediaSource, the old SourceBuffer
|
|
137
|
+
* is still attached to it, and the decoder keeps whatever state it was in. That matters because
|
|
138
|
+
* this pipeline is REBUILT in place, on the same element, whenever the audio track changes, and
|
|
139
|
+
* the rebuild awaits a wasm load plus a header read before it assigns a new `src`. On a torrent
|
|
140
|
+
* that is seconds of the element sitting on a corpse.
|
|
141
|
+
*
|
|
142
|
+
* `load()` is the only thing that actually resets it: the media element load algorithm aborts
|
|
143
|
+
* the current resource and clears the decoder. Without it the element goes from an ended source
|
|
144
|
+
* straight into fresh appends with no seek in between, which is exactly the sequence that makes
|
|
145
|
+
* firefox hand a real packet to a drained decoder (`avcodec_send_packet error: End of file`).
|
|
146
|
+
* Pushed AFTER the revoke so the reversed teardown runs it FIRST.
|
|
147
|
+
*/
|
|
148
|
+
teardown.push(() => {
|
|
149
|
+
try { videoElement.pause() } catch {}
|
|
150
|
+
videoElement.removeAttribute('src')
|
|
151
|
+
// no src and no <source> children means the load algorithm goes straight to NETWORK_EMPTY
|
|
152
|
+
// without firing `error`, so this cannot manufacture the failure it exists to prevent
|
|
153
|
+
videoElement.load()
|
|
154
|
+
})
|
|
133
155
|
|
|
134
156
|
const sourceBuffer = await new Promise<SourceBuffer>((resolve, reject) => {
|
|
135
157
|
const timeout = setTimeout(() => { cleanup(); reject(new Error('The MediaSource never opened')) }, SOURCE_OPEN_TIMEOUT)
|
|
@@ -230,8 +252,13 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
|
|
|
230
252
|
lastSeekPosition = time
|
|
231
253
|
const generation = ++seekGeneration
|
|
232
254
|
try {
|
|
233
|
-
|
|
255
|
+
// `seek` ends by reading, and that read can hit the end of the file, which finalizes the
|
|
256
|
+
// muxer: libav writes the mp4 trailer and cannot be read from again. Dropping the flag left
|
|
257
|
+
// this side believing the stream was still open, so the next pump read straight back into a
|
|
258
|
+
// context that had already been torn down.
|
|
259
|
+
const { data, pts, subtitles: fragments, finished: done } = await remuxer.seek(time)
|
|
234
260
|
if (destroyed || generation !== seekGeneration) return
|
|
261
|
+
if (done) finished = true
|
|
235
262
|
if (fragments?.length) subtitles.pushFragments(fragments)
|
|
236
263
|
await updateTimestampOffset(pts)
|
|
237
264
|
if (destroyed || generation !== seekGeneration) return
|
|
@@ -276,6 +276,9 @@ export const ControlBar = () => {
|
|
|
276
276
|
className='play'
|
|
277
277
|
type='button'
|
|
278
278
|
onClick={() => player.togglePaused()}
|
|
279
|
+
// This one genuinely changes what it does, so the name changes with it rather than
|
|
280
|
+
// carrying a pressed state. Replay is a third action, not an on or off.
|
|
281
|
+
aria-label={ended ? 'Replay' : paused ? 'Play' : 'Pause'}
|
|
279
282
|
>
|
|
280
283
|
{
|
|
281
284
|
ended
|
|
@@ -363,6 +366,8 @@ export const ControlBar = () => {
|
|
|
363
366
|
className='full-screen'
|
|
364
367
|
type='button'
|
|
365
368
|
onClick={() => player.toggleFullscreen()}
|
|
369
|
+
aria-label='Full screen'
|
|
370
|
+
aria-pressed={fullscreen}
|
|
366
371
|
>
|
|
367
372
|
{
|
|
368
373
|
fullscreen
|
|
@@ -105,6 +105,10 @@ export const Sound = ({ ref }: SoundProps) => {
|
|
|
105
105
|
type='button'
|
|
106
106
|
onClick={() => player.toggleMuted()}
|
|
107
107
|
ref={ref}
|
|
108
|
+
// A static name with the state on `aria-pressed`. Renaming a control when it is activated
|
|
109
|
+
// announces the state twice and breaks the expectation that a control keeps its name.
|
|
110
|
+
aria-label='Mute'
|
|
111
|
+
aria-pressed={muted || volume === 0}
|
|
108
112
|
>
|
|
109
113
|
{muted || volume === 0
|
|
110
114
|
? <VolumeX size={18} color='#fff' />
|
|
@@ -54,7 +54,14 @@ export const SubtitlesAction = () => {
|
|
|
54
54
|
id='subtitles'
|
|
55
55
|
disabled={open}
|
|
56
56
|
text={
|
|
57
|
-
<button
|
|
57
|
+
<button
|
|
58
|
+
className='subtitles'
|
|
59
|
+
type='button'
|
|
60
|
+
onClick={toggle}
|
|
61
|
+
aria-label='Subtitles'
|
|
62
|
+
aria-expanded={open}
|
|
63
|
+
aria-pressed={on}
|
|
64
|
+
>
|
|
58
65
|
{on ? <Captions className='captions' /> : <CaptionsOff className='captions-off' />}
|
|
59
66
|
</button>
|
|
60
67
|
}
|
|
@@ -30,7 +30,18 @@ export const usePlayback = (
|
|
|
30
30
|
const [audioStreamIndex, setAudioStreamIndex] = useState<number | undefined>(undefined)
|
|
31
31
|
|
|
32
32
|
const controllerRef = useRef<PlaybackController | null>(null)
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Where to pick playback back up, and WHICH media that position belongs to.
|
|
35
|
+
*
|
|
36
|
+
* The position exists so a rebuild of the pipeline is invisible: switching audio track tears the
|
|
37
|
+
* whole thing down and starts again, and dropping the viewer back at zero for that would be absurd.
|
|
38
|
+
* But the same rebuild happens when the media itself changes, and then the position is meaningless:
|
|
39
|
+
* dropping a second file in while the first was playing used to open it 40 minutes in.
|
|
40
|
+
*
|
|
41
|
+
* `size` is the identity, because it is the one piece of the source in the effect's deps. `read` is
|
|
42
|
+
* deliberately not, since a streaming consumer passes a fresh closure several times a second.
|
|
43
|
+
*/
|
|
44
|
+
const resumeRef = useRef<{ time: number, size: number } | null>(null)
|
|
34
45
|
// The renderer turns the first track on by itself, so the menu has to mirror that or it shows
|
|
35
46
|
// "Disable" ticked over subtitles that are visibly on screen.
|
|
36
47
|
const subtitleChoiceMade = useRef(false)
|
|
@@ -94,9 +105,11 @@ export const usePlayback = (
|
|
|
94
105
|
onReady: () => {
|
|
95
106
|
if (cancelled) return
|
|
96
107
|
player.setSourceState({ ready: true })
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
108
|
+
const resume = resumeRef.current
|
|
109
|
+
resumeRef.current = null
|
|
110
|
+
// only a position belonging to THIS media, or a new file opens where the last one stopped
|
|
111
|
+
if (resume && resume.size === size && resume.time > 0) {
|
|
112
|
+
video.currentTime = resume.time
|
|
100
113
|
}
|
|
101
114
|
if (autoplay) video.play().catch(() => {})
|
|
102
115
|
},
|
|
@@ -130,7 +143,7 @@ export const usePlayback = (
|
|
|
130
143
|
})()
|
|
131
144
|
return () => {
|
|
132
145
|
cancelled = true
|
|
133
|
-
|
|
146
|
+
resumeRef.current = { time: video.currentTime, size }
|
|
134
147
|
controllerRef.current?.destroy()
|
|
135
148
|
controllerRef.current = null
|
|
136
149
|
player.setSourceState({ ready: false })
|