@scarlett-player/captions 1.2.0 → 1.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.cjs +87 -32
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +87 -32
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -24,11 +24,17 @@ __export(index_exports, {
|
|
|
24
24
|
default: () => index_default
|
|
25
25
|
});
|
|
26
26
|
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
var HLS_SUBTITLE_TRACKS_UPDATED = "hlsSubtitleTracksUpdated";
|
|
28
|
+
var HLS_INSTANCE_RETRY_MS = 500;
|
|
27
29
|
function createCaptionsPlugin(config = {}) {
|
|
28
30
|
let api = null;
|
|
29
31
|
let video = null;
|
|
30
32
|
let addedTrackElements = [];
|
|
31
33
|
let hlsSubtitleHandler = null;
|
|
34
|
+
let observedTextTracks = null;
|
|
35
|
+
let hlsRetryTimer = null;
|
|
36
|
+
let hlsRetryUsed = false;
|
|
37
|
+
let hasAutoSelected = false;
|
|
32
38
|
const extractFromHLS = config.extractFromHLS !== false;
|
|
33
39
|
const autoSelect = config.autoSelect ?? false;
|
|
34
40
|
const defaultLanguage = config.defaultLanguage ?? "en";
|
|
@@ -87,6 +93,7 @@ function createCaptionsPlugin(config = {}) {
|
|
|
87
93
|
const selectTrack = (trackId) => {
|
|
88
94
|
const videoEl = getVideo();
|
|
89
95
|
if (!videoEl) return;
|
|
96
|
+
hasAutoSelected = true;
|
|
90
97
|
for (let i = 0; i < videoEl.textTracks.length; i++) {
|
|
91
98
|
const track = videoEl.textTracks[i];
|
|
92
99
|
if (track.kind !== "subtitles" && track.kind !== "captions") continue;
|
|
@@ -99,45 +106,90 @@ function createCaptionsPlugin(config = {}) {
|
|
|
99
106
|
}
|
|
100
107
|
syncTracksToState();
|
|
101
108
|
};
|
|
109
|
+
const maybeAutoSelect = () => {
|
|
110
|
+
if (!autoSelect || hasAutoSelected) return;
|
|
111
|
+
if (api?.getState("currentTextTrack")) {
|
|
112
|
+
hasAutoSelected = true;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const tracks = api?.getState("textTracks") || [];
|
|
116
|
+
const match = tracks.find((t) => t.language === defaultLanguage);
|
|
117
|
+
if (!match) return;
|
|
118
|
+
selectTrack(match.id);
|
|
119
|
+
api?.logger.debug("Auto-selected caption track", { language: defaultLanguage, id: match.id });
|
|
120
|
+
};
|
|
121
|
+
const handleTextTracksChanged = () => {
|
|
122
|
+
syncTracksToState();
|
|
123
|
+
maybeAutoSelect();
|
|
124
|
+
};
|
|
125
|
+
const observeTextTracks = () => {
|
|
126
|
+
const videoEl = getVideo();
|
|
127
|
+
if (!videoEl || observedTextTracks === videoEl.textTracks) return;
|
|
128
|
+
unobserveTextTracks();
|
|
129
|
+
const list = videoEl.textTracks;
|
|
130
|
+
if (typeof list?.addEventListener !== "function") return;
|
|
131
|
+
observedTextTracks = list;
|
|
132
|
+
observedTextTracks.addEventListener("addtrack", handleTextTracksChanged);
|
|
133
|
+
observedTextTracks.addEventListener("removetrack", handleTextTracksChanged);
|
|
134
|
+
observedTextTracks.addEventListener("change", handleTextTracksChanged);
|
|
135
|
+
};
|
|
136
|
+
const unobserveTextTracks = () => {
|
|
137
|
+
if (typeof observedTextTracks?.removeEventListener !== "function") {
|
|
138
|
+
observedTextTracks = null;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
observedTextTracks.removeEventListener("addtrack", handleTextTracksChanged);
|
|
142
|
+
observedTextTracks.removeEventListener("removetrack", handleTextTracksChanged);
|
|
143
|
+
observedTextTracks.removeEventListener("change", handleTextTracksChanged);
|
|
144
|
+
observedTextTracks = null;
|
|
145
|
+
};
|
|
102
146
|
const extractHlsSubtitles = () => {
|
|
103
147
|
if (!extractFromHLS || !api) return;
|
|
104
148
|
const hlsPlugin = api.getPlugin("hls-provider");
|
|
105
149
|
if (!hlsPlugin || hlsPlugin.isNativeHLS()) return;
|
|
106
150
|
const hlsInstance = hlsPlugin.getHlsInstance();
|
|
107
151
|
if (!hlsInstance?.subtitleTracks?.length) return;
|
|
108
|
-
api.logger.debug("
|
|
152
|
+
api.logger.debug("Syncing HLS subtitle tracks", {
|
|
109
153
|
count: hlsInstance.subtitleTracks.length
|
|
110
154
|
});
|
|
111
|
-
for (const hlsTrack of hlsInstance.subtitleTracks) {
|
|
112
|
-
addTrackElement({
|
|
113
|
-
language: hlsTrack.lang || "unknown",
|
|
114
|
-
label: hlsTrack.name || `Subtitle ${hlsTrack.id}`,
|
|
115
|
-
src: hlsTrack.url,
|
|
116
|
-
kind: "subtitles"
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
155
|
syncTracksToState();
|
|
120
|
-
|
|
121
|
-
|
|
156
|
+
maybeAutoSelect();
|
|
157
|
+
};
|
|
158
|
+
const unsubscribeFromHls = () => {
|
|
159
|
+
if (hlsRetryTimer) {
|
|
160
|
+
clearTimeout(hlsRetryTimer);
|
|
161
|
+
hlsRetryTimer = null;
|
|
122
162
|
}
|
|
163
|
+
if (!hlsSubtitleHandler) return;
|
|
164
|
+
const hlsInstance = api?.getPlugin("hls-provider")?.getHlsInstance();
|
|
165
|
+
hlsInstance?.off(HLS_SUBTITLE_TRACKS_UPDATED, hlsSubtitleHandler);
|
|
166
|
+
hlsSubtitleHandler = null;
|
|
123
167
|
};
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
if (
|
|
128
|
-
|
|
129
|
-
|
|
168
|
+
const syncFromHls = () => {
|
|
169
|
+
if (!extractFromHLS || !api) return;
|
|
170
|
+
const hlsPlugin = api.getPlugin("hls-provider");
|
|
171
|
+
if (!hlsPlugin || hlsPlugin.isNativeHLS()) return;
|
|
172
|
+
const hlsInstance = hlsPlugin.getHlsInstance();
|
|
173
|
+
if (!hlsInstance) {
|
|
174
|
+
if (!hlsRetryUsed) {
|
|
175
|
+
hlsRetryUsed = true;
|
|
176
|
+
hlsRetryTimer = setTimeout(() => {
|
|
177
|
+
hlsRetryTimer = null;
|
|
178
|
+
syncFromHls();
|
|
179
|
+
}, HLS_INSTANCE_RETRY_MS);
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
130
182
|
}
|
|
183
|
+
unsubscribeFromHls();
|
|
184
|
+
hlsSubtitleHandler = () => extractHlsSubtitles();
|
|
185
|
+
hlsInstance.on(HLS_SUBTITLE_TRACKS_UPDATED, hlsSubtitleHandler);
|
|
186
|
+
extractHlsSubtitles();
|
|
131
187
|
};
|
|
132
188
|
const initSources = () => {
|
|
133
189
|
if (!config.sources?.length) return;
|
|
134
190
|
for (const source of config.sources) {
|
|
135
191
|
addTrackElement(source);
|
|
136
192
|
}
|
|
137
|
-
syncTracksToState();
|
|
138
|
-
if (autoSelect) {
|
|
139
|
-
autoSelectTrack();
|
|
140
|
-
}
|
|
141
193
|
};
|
|
142
194
|
return {
|
|
143
195
|
id: "captions",
|
|
@@ -155,33 +207,36 @@ function createCaptionsPlugin(config = {}) {
|
|
|
155
207
|
});
|
|
156
208
|
const unsubLoaded = api.on("media:loaded", () => {
|
|
157
209
|
video = null;
|
|
210
|
+
hasAutoSelected = false;
|
|
211
|
+
hlsRetryUsed = false;
|
|
158
212
|
cleanupTracks();
|
|
159
213
|
initSources();
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
214
|
+
observeTextTracks();
|
|
215
|
+
syncTracksToState();
|
|
216
|
+
maybeAutoSelect();
|
|
217
|
+
syncFromHls();
|
|
163
218
|
});
|
|
164
219
|
const unsubLoadRequest = api.on("media:load-request", () => {
|
|
165
220
|
video = null;
|
|
221
|
+
hasAutoSelected = false;
|
|
222
|
+
hlsRetryUsed = false;
|
|
223
|
+
unsubscribeFromHls();
|
|
224
|
+
unobserveTextTracks();
|
|
166
225
|
cleanupTracks();
|
|
167
226
|
});
|
|
168
227
|
api.onDestroy(() => {
|
|
169
228
|
unsubTrackText();
|
|
170
229
|
unsubLoaded();
|
|
171
230
|
unsubLoadRequest();
|
|
231
|
+
unsubscribeFromHls();
|
|
232
|
+
unobserveTextTracks();
|
|
172
233
|
cleanupTracks();
|
|
173
|
-
if (hlsSubtitleHandler) {
|
|
174
|
-
const hlsPlugin = api?.getPlugin("hls-provider");
|
|
175
|
-
const hlsInstance = hlsPlugin?.getHlsInstance();
|
|
176
|
-
if (hlsInstance) {
|
|
177
|
-
hlsInstance.off("SUBTITLE_TRACKS_UPDATED", hlsSubtitleHandler);
|
|
178
|
-
}
|
|
179
|
-
hlsSubtitleHandler = null;
|
|
180
|
-
}
|
|
181
234
|
});
|
|
182
235
|
},
|
|
183
236
|
destroy() {
|
|
184
237
|
api?.logger.debug("Captions plugin destroyed");
|
|
238
|
+
unsubscribeFromHls();
|
|
239
|
+
unobserveTextTracks();
|
|
185
240
|
cleanupTracks();
|
|
186
241
|
video = null;
|
|
187
242
|
api = null;
|
package/dist/index.d.cts
CHANGED
|
@@ -33,7 +33,9 @@ interface CaptionsPluginConfig {
|
|
|
33
33
|
*
|
|
34
34
|
* Provides WebVTT subtitle/caption support with:
|
|
35
35
|
* - External WebVTT file loading via <track> elements
|
|
36
|
-
* - HLS.js subtitle track extraction
|
|
36
|
+
* - HLS.js subtitle track extraction, driven by Hls.Events.SUBTITLE_TRACKS_UPDATED
|
|
37
|
+
* (emitted as 'hlsSubtitleTracksUpdated')
|
|
38
|
+
* - Native HLS support (Safari/iOS) by observing the video's TextTrackList
|
|
37
39
|
* - Browser-native rendering (no custom VTT parser)
|
|
38
40
|
* - State sync with core textTracks/currentTextTrack
|
|
39
41
|
* - Automatic cleanup on source change
|
package/dist/index.d.ts
CHANGED
|
@@ -33,7 +33,9 @@ interface CaptionsPluginConfig {
|
|
|
33
33
|
*
|
|
34
34
|
* Provides WebVTT subtitle/caption support with:
|
|
35
35
|
* - External WebVTT file loading via <track> elements
|
|
36
|
-
* - HLS.js subtitle track extraction
|
|
36
|
+
* - HLS.js subtitle track extraction, driven by Hls.Events.SUBTITLE_TRACKS_UPDATED
|
|
37
|
+
* (emitted as 'hlsSubtitleTracksUpdated')
|
|
38
|
+
* - Native HLS support (Safari/iOS) by observing the video's TextTrackList
|
|
37
39
|
* - Browser-native rendering (no custom VTT parser)
|
|
38
40
|
* - State sync with core textTracks/currentTextTrack
|
|
39
41
|
* - Automatic cleanup on source change
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
+
var HLS_SUBTITLE_TRACKS_UPDATED = "hlsSubtitleTracksUpdated";
|
|
3
|
+
var HLS_INSTANCE_RETRY_MS = 500;
|
|
2
4
|
function createCaptionsPlugin(config = {}) {
|
|
3
5
|
let api = null;
|
|
4
6
|
let video = null;
|
|
5
7
|
let addedTrackElements = [];
|
|
6
8
|
let hlsSubtitleHandler = null;
|
|
9
|
+
let observedTextTracks = null;
|
|
10
|
+
let hlsRetryTimer = null;
|
|
11
|
+
let hlsRetryUsed = false;
|
|
12
|
+
let hasAutoSelected = false;
|
|
7
13
|
const extractFromHLS = config.extractFromHLS !== false;
|
|
8
14
|
const autoSelect = config.autoSelect ?? false;
|
|
9
15
|
const defaultLanguage = config.defaultLanguage ?? "en";
|
|
@@ -62,6 +68,7 @@ function createCaptionsPlugin(config = {}) {
|
|
|
62
68
|
const selectTrack = (trackId) => {
|
|
63
69
|
const videoEl = getVideo();
|
|
64
70
|
if (!videoEl) return;
|
|
71
|
+
hasAutoSelected = true;
|
|
65
72
|
for (let i = 0; i < videoEl.textTracks.length; i++) {
|
|
66
73
|
const track = videoEl.textTracks[i];
|
|
67
74
|
if (track.kind !== "subtitles" && track.kind !== "captions") continue;
|
|
@@ -74,45 +81,90 @@ function createCaptionsPlugin(config = {}) {
|
|
|
74
81
|
}
|
|
75
82
|
syncTracksToState();
|
|
76
83
|
};
|
|
84
|
+
const maybeAutoSelect = () => {
|
|
85
|
+
if (!autoSelect || hasAutoSelected) return;
|
|
86
|
+
if (api?.getState("currentTextTrack")) {
|
|
87
|
+
hasAutoSelected = true;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const tracks = api?.getState("textTracks") || [];
|
|
91
|
+
const match = tracks.find((t) => t.language === defaultLanguage);
|
|
92
|
+
if (!match) return;
|
|
93
|
+
selectTrack(match.id);
|
|
94
|
+
api?.logger.debug("Auto-selected caption track", { language: defaultLanguage, id: match.id });
|
|
95
|
+
};
|
|
96
|
+
const handleTextTracksChanged = () => {
|
|
97
|
+
syncTracksToState();
|
|
98
|
+
maybeAutoSelect();
|
|
99
|
+
};
|
|
100
|
+
const observeTextTracks = () => {
|
|
101
|
+
const videoEl = getVideo();
|
|
102
|
+
if (!videoEl || observedTextTracks === videoEl.textTracks) return;
|
|
103
|
+
unobserveTextTracks();
|
|
104
|
+
const list = videoEl.textTracks;
|
|
105
|
+
if (typeof list?.addEventListener !== "function") return;
|
|
106
|
+
observedTextTracks = list;
|
|
107
|
+
observedTextTracks.addEventListener("addtrack", handleTextTracksChanged);
|
|
108
|
+
observedTextTracks.addEventListener("removetrack", handleTextTracksChanged);
|
|
109
|
+
observedTextTracks.addEventListener("change", handleTextTracksChanged);
|
|
110
|
+
};
|
|
111
|
+
const unobserveTextTracks = () => {
|
|
112
|
+
if (typeof observedTextTracks?.removeEventListener !== "function") {
|
|
113
|
+
observedTextTracks = null;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
observedTextTracks.removeEventListener("addtrack", handleTextTracksChanged);
|
|
117
|
+
observedTextTracks.removeEventListener("removetrack", handleTextTracksChanged);
|
|
118
|
+
observedTextTracks.removeEventListener("change", handleTextTracksChanged);
|
|
119
|
+
observedTextTracks = null;
|
|
120
|
+
};
|
|
77
121
|
const extractHlsSubtitles = () => {
|
|
78
122
|
if (!extractFromHLS || !api) return;
|
|
79
123
|
const hlsPlugin = api.getPlugin("hls-provider");
|
|
80
124
|
if (!hlsPlugin || hlsPlugin.isNativeHLS()) return;
|
|
81
125
|
const hlsInstance = hlsPlugin.getHlsInstance();
|
|
82
126
|
if (!hlsInstance?.subtitleTracks?.length) return;
|
|
83
|
-
api.logger.debug("
|
|
127
|
+
api.logger.debug("Syncing HLS subtitle tracks", {
|
|
84
128
|
count: hlsInstance.subtitleTracks.length
|
|
85
129
|
});
|
|
86
|
-
for (const hlsTrack of hlsInstance.subtitleTracks) {
|
|
87
|
-
addTrackElement({
|
|
88
|
-
language: hlsTrack.lang || "unknown",
|
|
89
|
-
label: hlsTrack.name || `Subtitle ${hlsTrack.id}`,
|
|
90
|
-
src: hlsTrack.url,
|
|
91
|
-
kind: "subtitles"
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
130
|
syncTracksToState();
|
|
95
|
-
|
|
96
|
-
|
|
131
|
+
maybeAutoSelect();
|
|
132
|
+
};
|
|
133
|
+
const unsubscribeFromHls = () => {
|
|
134
|
+
if (hlsRetryTimer) {
|
|
135
|
+
clearTimeout(hlsRetryTimer);
|
|
136
|
+
hlsRetryTimer = null;
|
|
97
137
|
}
|
|
138
|
+
if (!hlsSubtitleHandler) return;
|
|
139
|
+
const hlsInstance = api?.getPlugin("hls-provider")?.getHlsInstance();
|
|
140
|
+
hlsInstance?.off(HLS_SUBTITLE_TRACKS_UPDATED, hlsSubtitleHandler);
|
|
141
|
+
hlsSubtitleHandler = null;
|
|
98
142
|
};
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
143
|
+
const syncFromHls = () => {
|
|
144
|
+
if (!extractFromHLS || !api) return;
|
|
145
|
+
const hlsPlugin = api.getPlugin("hls-provider");
|
|
146
|
+
if (!hlsPlugin || hlsPlugin.isNativeHLS()) return;
|
|
147
|
+
const hlsInstance = hlsPlugin.getHlsInstance();
|
|
148
|
+
if (!hlsInstance) {
|
|
149
|
+
if (!hlsRetryUsed) {
|
|
150
|
+
hlsRetryUsed = true;
|
|
151
|
+
hlsRetryTimer = setTimeout(() => {
|
|
152
|
+
hlsRetryTimer = null;
|
|
153
|
+
syncFromHls();
|
|
154
|
+
}, HLS_INSTANCE_RETRY_MS);
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
105
157
|
}
|
|
158
|
+
unsubscribeFromHls();
|
|
159
|
+
hlsSubtitleHandler = () => extractHlsSubtitles();
|
|
160
|
+
hlsInstance.on(HLS_SUBTITLE_TRACKS_UPDATED, hlsSubtitleHandler);
|
|
161
|
+
extractHlsSubtitles();
|
|
106
162
|
};
|
|
107
163
|
const initSources = () => {
|
|
108
164
|
if (!config.sources?.length) return;
|
|
109
165
|
for (const source of config.sources) {
|
|
110
166
|
addTrackElement(source);
|
|
111
167
|
}
|
|
112
|
-
syncTracksToState();
|
|
113
|
-
if (autoSelect) {
|
|
114
|
-
autoSelectTrack();
|
|
115
|
-
}
|
|
116
168
|
};
|
|
117
169
|
return {
|
|
118
170
|
id: "captions",
|
|
@@ -130,33 +182,36 @@ function createCaptionsPlugin(config = {}) {
|
|
|
130
182
|
});
|
|
131
183
|
const unsubLoaded = api.on("media:loaded", () => {
|
|
132
184
|
video = null;
|
|
185
|
+
hasAutoSelected = false;
|
|
186
|
+
hlsRetryUsed = false;
|
|
133
187
|
cleanupTracks();
|
|
134
188
|
initSources();
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
189
|
+
observeTextTracks();
|
|
190
|
+
syncTracksToState();
|
|
191
|
+
maybeAutoSelect();
|
|
192
|
+
syncFromHls();
|
|
138
193
|
});
|
|
139
194
|
const unsubLoadRequest = api.on("media:load-request", () => {
|
|
140
195
|
video = null;
|
|
196
|
+
hasAutoSelected = false;
|
|
197
|
+
hlsRetryUsed = false;
|
|
198
|
+
unsubscribeFromHls();
|
|
199
|
+
unobserveTextTracks();
|
|
141
200
|
cleanupTracks();
|
|
142
201
|
});
|
|
143
202
|
api.onDestroy(() => {
|
|
144
203
|
unsubTrackText();
|
|
145
204
|
unsubLoaded();
|
|
146
205
|
unsubLoadRequest();
|
|
206
|
+
unsubscribeFromHls();
|
|
207
|
+
unobserveTextTracks();
|
|
147
208
|
cleanupTracks();
|
|
148
|
-
if (hlsSubtitleHandler) {
|
|
149
|
-
const hlsPlugin = api?.getPlugin("hls-provider");
|
|
150
|
-
const hlsInstance = hlsPlugin?.getHlsInstance();
|
|
151
|
-
if (hlsInstance) {
|
|
152
|
-
hlsInstance.off("SUBTITLE_TRACKS_UPDATED", hlsSubtitleHandler);
|
|
153
|
-
}
|
|
154
|
-
hlsSubtitleHandler = null;
|
|
155
|
-
}
|
|
156
209
|
});
|
|
157
210
|
},
|
|
158
211
|
destroy() {
|
|
159
212
|
api?.logger.debug("Captions plugin destroyed");
|
|
213
|
+
unsubscribeFromHls();
|
|
214
|
+
unobserveTextTracks();
|
|
160
215
|
cleanupTracks();
|
|
161
216
|
video = null;
|
|
162
217
|
api = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scarlett-player/captions",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "Captions Plugin for Scarlett Player - WebVTT subtitles and closed captions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"tsup": "^8.5.1",
|
|
31
31
|
"typescript": "^5.3.0",
|
|
32
32
|
"vitest": "^1.6.0",
|
|
33
|
-
"@scarlett-player/core": "1.
|
|
33
|
+
"@scarlett-player/core": "1.4.1"
|
|
34
34
|
},
|
|
35
35
|
"keywords": [
|
|
36
36
|
"video",
|