UNPKG

8.53 kBJavaScriptView Raw
1/**
2 * @file text-tracks.js
3 */
4import window from 'global/window';
5import videojs from 'video.js';
6
7/**
8 * Create captions text tracks on video.js if they do not exist
9 *
10 * @param {Object} inbandTextTracks a reference to current inbandTextTracks
11 * @param {Object} tech the video.js tech
12 * @param {Object} captionStream the caption stream to create
13 * @private
14 */
15export const createCaptionsTrackIfNotExists = function(inbandTextTracks, tech, captionStream) {
16 if (!inbandTextTracks[captionStream]) {
17 tech.trigger({type: 'usage', name: 'vhs-608'});
18 tech.trigger({type: 'usage', name: 'hls-608'});
19
20 let instreamId = captionStream;
21
22 // we need to translate SERVICEn for 708 to how mux.js currently labels them
23 if (/^cc708_/.test(captionStream)) {
24 instreamId = 'SERVICE' + captionStream.split('_')[1];
25 }
26
27 const track = tech.textTracks().getTrackById(instreamId);
28
29 if (track) {
30 // Resuse an existing track with a CC# id because this was
31 // very likely created by videojs-contrib-hls from information
32 // in the m3u8 for us to use
33 inbandTextTracks[captionStream] = track;
34 } else {
35 // This section gets called when we have caption services that aren't specified in the manifest.
36 // Manifest level caption services are handled in media-groups.js under CLOSED-CAPTIONS.
37 const captionServices = tech.options_.vhs && tech.options_.vhs.captionServices || {};
38 let label = captionStream;
39 let language = captionStream;
40 let def = false;
41 const captionService = captionServices[instreamId];
42
43 if (captionService) {
44 label = captionService.label;
45 language = captionService.language;
46 def = captionService.default;
47 }
48
49 // Otherwise, create a track with the default `CC#` label and
50 // without a language
51 inbandTextTracks[captionStream] = tech.addRemoteTextTrack({
52 kind: 'captions',
53 id: instreamId,
54 // TODO: investigate why this doesn't seem to turn the caption on by default
55 default: def,
56 label,
57 language
58 }, false).track;
59 }
60 }
61};
62
63/**
64 * Add caption text track data to a source handler given an array of captions
65 *
66 * @param {Object}
67 * @param {Object} inbandTextTracks the inband text tracks
68 * @param {number} timestampOffset the timestamp offset of the source buffer
69 * @param {Array} captionArray an array of caption data
70 * @private
71 */
72export const addCaptionData = function({
73 inbandTextTracks,
74 captionArray,
75 timestampOffset
76}) {
77 if (!captionArray) {
78 return;
79 }
80
81 const Cue = window.WebKitDataCue || window.VTTCue;
82
83 captionArray.forEach((caption) => {
84 const track = caption.stream;
85
86 inbandTextTracks[track].addCue(new Cue(
87 caption.startTime + timestampOffset,
88 caption.endTime + timestampOffset,
89 caption.text
90 ));
91 });
92};
93
94/**
95 * Define properties on a cue for backwards compatability,
96 * but warn the user that the way that they are using it
97 * is depricated and will be removed at a later date.
98 *
99 * @param {Cue} cue the cue to add the properties on
100 * @private
101 */
102const deprecateOldCue = function(cue) {
103 Object.defineProperties(cue.frame, {
104 id: {
105 get() {
106 videojs.log.warn('cue.frame.id is deprecated. Use cue.value.key instead.');
107 return cue.value.key;
108 }
109 },
110 value: {
111 get() {
112 videojs.log.warn('cue.frame.value is deprecated. Use cue.value.data instead.');
113 return cue.value.data;
114 }
115 },
116 privateData: {
117 get() {
118 videojs.log.warn('cue.frame.privateData is deprecated. Use cue.value.data instead.');
119 return cue.value.data;
120 }
121 }
122 });
123};
124
125/**
126 * Add metadata text track data to a source handler given an array of metadata
127 *
128 * @param {Object}
129 * @param {Object} inbandTextTracks the inband text tracks
130 * @param {Array} metadataArray an array of meta data
131 * @param {number} timestampOffset the timestamp offset of the source buffer
132 * @param {number} videoDuration the duration of the video
133 * @private
134 */
135export const addMetadata = ({
136 inbandTextTracks,
137 metadataArray,
138 timestampOffset,
139 videoDuration
140}) => {
141 if (!metadataArray) {
142 return;
143 }
144
145 const Cue = window.WebKitDataCue || window.VTTCue;
146 const metadataTrack = inbandTextTracks.metadataTrack_;
147
148 if (!metadataTrack) {
149 return;
150 }
151
152 metadataArray.forEach((metadata) => {
153 const time = metadata.cueTime + timestampOffset;
154
155 // if time isn't a finite number between 0 and Infinity, like NaN,
156 // ignore this bit of metadata.
157 // This likely occurs when you have an non-timed ID3 tag like TIT2,
158 // which is the "Title/Songname/Content description" frame
159 if (typeof time !== 'number' || window.isNaN(time) || time < 0 || !(time < Infinity)) {
160 return;
161 }
162
163 metadata.frames.forEach((frame) => {
164 const cue = new Cue(
165 time,
166 time,
167 frame.value || frame.url || frame.data || ''
168 );
169
170 cue.frame = frame;
171 cue.value = frame;
172 deprecateOldCue(cue);
173
174 metadataTrack.addCue(cue);
175 });
176 });
177
178 if (!metadataTrack.cues || !metadataTrack.cues.length) {
179 return;
180 }
181
182 // Updating the metadeta cues so that
183 // the endTime of each cue is the startTime of the next cue
184 // the endTime of last cue is the duration of the video
185 const cues = metadataTrack.cues;
186 const cuesArray = [];
187
188 // Create a copy of the TextTrackCueList...
189 // ...disregarding cues with a falsey value
190 for (let i = 0; i < cues.length; i++) {
191 if (cues[i]) {
192 cuesArray.push(cues[i]);
193 }
194 }
195
196 // Group cues by their startTime value
197 const cuesGroupedByStartTime = cuesArray.reduce((obj, cue) => {
198 const timeSlot = obj[cue.startTime] || [];
199
200 timeSlot.push(cue);
201 obj[cue.startTime] = timeSlot;
202
203 return obj;
204 }, {});
205
206 // Sort startTimes by ascending order
207 const sortedStartTimes = Object.keys(cuesGroupedByStartTime)
208 .sort((a, b) => Number(a) - Number(b));
209
210 // Map each cue group's endTime to the next group's startTime
211 sortedStartTimes.forEach((startTime, idx) => {
212 const cueGroup = cuesGroupedByStartTime[startTime];
213 const nextTime = Number(sortedStartTimes[idx + 1]) || videoDuration;
214
215 // Map each cue's endTime the next group's startTime
216 cueGroup.forEach((cue) => {
217 cue.endTime = nextTime;
218 });
219 });
220};
221
222/**
223 * Create metadata text track on video.js if it does not exist
224 *
225 * @param {Object} inbandTextTracks a reference to current inbandTextTracks
226 * @param {string} dispatchType the inband metadata track dispatch type
227 * @param {Object} tech the video.js tech
228 * @private
229 */
230export const createMetadataTrackIfNotExists = (inbandTextTracks, dispatchType, tech) => {
231 if (inbandTextTracks.metadataTrack_) {
232 return;
233 }
234
235 inbandTextTracks.metadataTrack_ = tech.addRemoteTextTrack({
236 kind: 'metadata',
237 label: 'Timed Metadata'
238 }, false).track;
239
240 inbandTextTracks.metadataTrack_.inBandMetadataTrackDispatchType = dispatchType;
241};
242
243/**
244 * Remove cues from a track on video.js.
245 *
246 * @param {Double} start start of where we should remove the cue
247 * @param {Double} end end of where the we should remove the cue
248 * @param {Object} track the text track to remove the cues from
249 * @private
250 */
251export const removeCuesFromTrack = function(start, end, track) {
252 let i;
253 let cue;
254
255 if (!track) {
256 return;
257 }
258
259 if (!track.cues) {
260 return;
261 }
262
263 i = track.cues.length;
264
265 while (i--) {
266 cue = track.cues[i];
267
268 // Remove any cue within the provided start and end time
269 if (cue.startTime >= start && cue.endTime <= end) {
270 track.removeCue(cue);
271 }
272 }
273};
274
275/**
276 * Remove duplicate cues from a track on video.js (a cue is considered a
277 * duplicate if it has the same time interval and text as another)
278 *
279 * @param {Object} track the text track to remove the duplicate cues from
280 * @private
281 */
282export const removeDuplicateCuesFromTrack = function(track) {
283 const cues = track.cues;
284
285 if (!cues) {
286 return;
287 }
288
289 for (let i = 0; i < cues.length; i++) {
290 const duplicates = [];
291 let occurrences = 0;
292
293 for (let j = 0; j < cues.length; j++) {
294 if (
295 cues[i].startTime === cues[j].startTime &&
296 cues[i].endTime === cues[j].endTime &&
297 cues[i].text === cues[j].text
298 ) {
299 occurrences++;
300
301 if (occurrences > 1) {
302 duplicates.push(cues[j]);
303 }
304 }
305 }
306
307 if (duplicates.length) {
308 duplicates.forEach(dupe => track.removeCue(dupe));
309 }
310 }
311};