Skip to content

Commit 7c11911

Browse files
ldayanandaforbesjo
authored andcommitted
feat(captions): write in-band captions from DASH fmp4 segments to the textTrack API (#108)
1 parent 1f7a4ab commit 7c11911

7 files changed

Lines changed: 532 additions & 6 deletions

src/master-playlist-controller.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export class MasterPlaylistController extends videojs.EventTarget {
106106
}, false).track;
107107

108108
this.decrypter_ = new Decrypter();
109+
this.inbandTextTracks_ = {};
109110

110111
const segmentLoaderSettings = {
111112
hls: this.hls_,
@@ -119,7 +120,8 @@ export class MasterPlaylistController extends videojs.EventTarget {
119120
bandwidth,
120121
syncController: this.syncController_,
121122
decrypter: this.decrypter_,
122-
sourceType: this.sourceType_
123+
sourceType: this.sourceType_,
124+
inbandTextTracks: this.inbandTextTracks_
123125
};
124126

125127
this.masterPlaylistLoader_ = this.sourceType_ === 'dash' ?

src/media-segment-request.js

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import videojs from 'video.js';
22
import { createTransferableMessage } from './bin-utils';
3+
import mp4probe from 'mux.js/lib/mp4/probe';
34

45
export const REQUEST_ERRORS = {
56
FAILURE: 2,
@@ -171,7 +172,7 @@ const handleKeyResponse = (segment, finishProcessingFn) => (error, request) => {
171172
* @param {Function} finishProcessingFn - a callback to execute to continue processing
172173
* this request
173174
*/
174-
const handleInitSegmentResponse = (segment, finishProcessingFn) => (error, request) => {
175+
const handleInitSegmentResponse = (segment, captionParser, finishProcessingFn) => (error, request) => {
175176
const response = request.response;
176177
const errorObj = handleErrors(error, request);
177178

@@ -190,6 +191,15 @@ const handleInitSegmentResponse = (segment, finishProcessingFn) => (error, reque
190191
}
191192

192193
segment.map.bytes = new Uint8Array(request.response);
194+
195+
// Initialize CaptionParser if it hasn't been yet
196+
if (!captionParser.isInitialized()) {
197+
captionParser.init();
198+
}
199+
200+
segment.map.timescales = mp4probe.timescale(segment.map.bytes);
201+
segment.map.videoTrackIds = mp4probe.videoTrackIds(segment.map.bytes);
202+
193203
return finishProcessingFn(null, segment);
194204
};
195205

@@ -203,9 +213,10 @@ const handleInitSegmentResponse = (segment, finishProcessingFn) => (error, reque
203213
* @param {Function} finishProcessingFn - a callback to execute to continue processing
204214
* this request
205215
*/
206-
const handleSegmentResponse = (segment, finishProcessingFn) => (error, request) => {
216+
const handleSegmentResponse = (segment, captionParser, finishProcessingFn) => (error, request) => {
207217
const response = request.response;
208218
const errorObj = handleErrors(error, request);
219+
let parsed;
209220

210221
if (errorObj) {
211222
return finishProcessingFn(errorObj, segment);
@@ -229,6 +240,25 @@ const handleSegmentResponse = (segment, finishProcessingFn) => (error, request)
229240
segment.bytes = new Uint8Array(request.response);
230241
}
231242

243+
// This is likely an FMP4 and has the init segment.
244+
// Run through the CaptionParser in case there are captions.
245+
if (segment.map && segment.map.bytes) {
246+
// Initialize CaptionParser if it hasn't been yet
247+
if (!captionParser.isInitialized()) {
248+
captionParser.init();
249+
}
250+
251+
parsed = captionParser.parse(
252+
segment.bytes,
253+
segment.map.videoTrackIds,
254+
segment.map.timescales);
255+
256+
if (parsed && parsed.captions) {
257+
segment.captionStreams = parsed.captionStreams;
258+
segment.fmp4Captions = parsed.captions;
259+
}
260+
}
261+
232262
return finishProcessingFn(null, segment);
233263
};
234264

@@ -393,6 +423,7 @@ const handleProgress = (segment, progressFn) => (event) => {
393423
export const mediaSegmentRequest = (xhr,
394424
xhrOptions,
395425
decryptionWorker,
426+
captionParser,
396427
segment,
397428
progressFn,
398429
doneFn) => {
@@ -420,6 +451,7 @@ export const mediaSegmentRequest = (xhr,
420451
headers: segmentXhrHeaders(segment.map)
421452
});
422453
const initSegmentRequestCallback = handleInitSegmentResponse(segment,
454+
captionParser,
423455
finishProcessingFn);
424456
const initSegmentXhr = xhr(initSegmentOptions, initSegmentRequestCallback);
425457

@@ -431,7 +463,9 @@ export const mediaSegmentRequest = (xhr,
431463
responseType: 'arraybuffer',
432464
headers: segmentXhrHeaders(segment)
433465
});
434-
const segmentRequestCallback = handleSegmentResponse(segment, finishProcessingFn);
466+
const segmentRequestCallback = handleSegmentResponse(segment,
467+
captionParser,
468+
finishProcessingFn);
435469
const segmentXhr = xhr(segmentRequestOptions, segmentRequestCallback);
436470

437471
segmentXhr.addEventListener('progress', handleProgress(segment, progressFn));

src/segment-loader.js

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { initSegmentId } from './bin-utils';
1111
import { mediaSegmentRequest, REQUEST_ERRORS } from './media-segment-request';
1212
import { TIME_FUDGE_FACTOR, timeUntilRebuffer as timeUntilRebuffer_ } from './ranges';
1313
import { minRebufferMaxBandwidthSelector } from './playlist-selectors';
14+
import { addCaptionData, createCaptionsTrackIfNotExists } from './util/text-tracks';
15+
import { CaptionParser } from 'mux.js/lib/mp4';
1416
import logger from './util/logger';
1517

1618
// in ms
@@ -166,6 +168,7 @@ export default class SegmentLoader extends videojs.EventTarget {
166168
this.segmentMetadataTrack_ = settings.segmentMetadataTrack;
167169
this.goalBufferLength_ = settings.goalBufferLength;
168170
this.sourceType_ = settings.sourceType;
171+
this.inbandTextTracks_ = settings.inbandTextTracks;
169172
this.state_ = 'INIT';
170173

171174
// private instance variables
@@ -180,6 +183,8 @@ export default class SegmentLoader extends videojs.EventTarget {
180183
// Fragmented mp4 playback
181184
this.activeInitSegmentId_ = null;
182185
this.initSegments_ = {};
186+
// Fmp4 CaptionParser
187+
this.captionParser_ = new CaptionParser();
183188

184189
this.decrypter_ = settings.decrypter;
185190

@@ -240,6 +245,7 @@ export default class SegmentLoader extends videojs.EventTarget {
240245
this.sourceUpdater_.dispose();
241246
}
242247
this.resetStats_();
248+
this.captionParser_.reset();
243249
}
244250

245251
/**
@@ -340,7 +346,9 @@ export default class SegmentLoader extends videojs.EventTarget {
340346
this.initSegments_[id] = storedMap = {
341347
resolvedUri: map.resolvedUri,
342348
byterange: map.byterange,
343-
bytes: map.bytes
349+
bytes: map.bytes,
350+
timescales: map.timescales,
351+
videoTrackIds: map.videoTrackIds
344352
};
345353
}
346354

@@ -544,6 +552,8 @@ export default class SegmentLoader extends videojs.EventTarget {
544552
this.ended_ = false;
545553
this.resetLoader();
546554
this.remove(0, this.duration_());
555+
// clears fmp4 captions
556+
this.captionParser_.clearAllCaptions();
547557
this.trigger('reseteverything');
548558
}
549559

@@ -578,6 +588,12 @@ export default class SegmentLoader extends videojs.EventTarget {
578588
this.sourceUpdater_.remove(start, end);
579589
}
580590
removeCuesFromTrack(start, end, this.segmentMetadataTrack_);
591+
592+
if (this.inbandTextTracks_) {
593+
for (let id in this.inbandTextTracks_) {
594+
removeCuesFromTrack(start, end, this.inbandTextTracks_[id]);
595+
}
596+
}
581597
}
582598

583599
/**
@@ -666,11 +682,13 @@ export default class SegmentLoader extends videojs.EventTarget {
666682
// (we are crossing a discontinuity somehow)
667683
// - The "timestampOffset" for the start of this segment is less than
668684
// the currently set timestampOffset
685+
// Also, clear captions if we are crossing a discontinuity boundary
669686
if (segmentInfo.timeline !== this.currentTimeline_ ||
670687
((segmentInfo.startOfSegment !== null) &&
671688
segmentInfo.startOfSegment < this.sourceUpdater_.timestampOffset())) {
672689
this.syncController_.reset();
673690
segmentInfo.timestampOffset = segmentInfo.startOfSegment;
691+
this.captionParser_.clearAllCaptions();
674692
}
675693

676694
this.loadSegment_(segmentInfo);
@@ -952,6 +970,7 @@ export default class SegmentLoader extends videojs.EventTarget {
952970
segmentInfo.abortRequests = mediaSegmentRequest(this.hls_.xhr,
953971
this.xhrOptions_,
954972
this.decrypter_,
973+
this.captionParser_,
955974
this.createSimplifiedSegmentObj_(segmentInfo),
956975
// progress callback
957976
this.handleProgress_.bind(this),
@@ -1112,6 +1131,24 @@ export default class SegmentLoader extends videojs.EventTarget {
11121131
}
11131132

11141133
segmentInfo.endOfAllRequests = simpleSegment.endOfAllRequests;
1134+
1135+
// This has fmp4 captions, add them to text tracks
1136+
if (simpleSegment.fmp4Captions) {
1137+
createCaptionsTrackIfNotExists(
1138+
this.inbandTextTracks_,
1139+
this.hls_.tech_,
1140+
simpleSegment.captionStreams);
1141+
addCaptionData({
1142+
inbandTextTracks: this.inbandTextTracks_,
1143+
captionArray: simpleSegment.fmp4Captions,
1144+
// fmp4s will not have a timestamp offset
1145+
timestampOffset: 0
1146+
});
1147+
// Reset stored captions since we added parsed
1148+
// captions to a text track at this point
1149+
this.captionParser_.clearParsedCaptions();
1150+
}
1151+
11151152
this.handleSegment_();
11161153
}
11171154

src/util/text-tracks.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Create captions text tracks on video.js if they do not exist
3+
*
4+
* @param {Object} inbandTextTracks a reference to current inbandTextTracks
5+
* @param {Object} tech the video.js tech
6+
* @param {Object} captionStreams the caption streams to create
7+
* @private
8+
*/
9+
export const createCaptionsTrackIfNotExists = function(inbandTextTracks, tech, captionStreams) {
10+
for (let trackId in captionStreams) {
11+
if (!inbandTextTracks[trackId]) {
12+
tech.trigger({type: 'usage', name: 'hls-608'});
13+
let track = tech.textTracks().getTrackById(trackId);
14+
15+
if (track) {
16+
// Resuse an existing track with a CC# id because this was
17+
// very likely created by videojs-contrib-hls from information
18+
// in the m3u8 for us to use
19+
inbandTextTracks[trackId] = track;
20+
} else {
21+
// Otherwise, create a track with the default `CC#` label and
22+
// without a language
23+
inbandTextTracks[trackId] = tech.addRemoteTextTrack({
24+
kind: 'captions',
25+
id: trackId,
26+
label: trackId
27+
}, false).track;
28+
}
29+
}
30+
}
31+
};
32+
33+
export const addCaptionData = function({
34+
inbandTextTracks,
35+
captionArray,
36+
timestampOffset
37+
}) {
38+
if (!captionArray) {
39+
return;
40+
}
41+
42+
const Cue = window.WebKitDataCue || window.VTTCue;
43+
44+
captionArray.forEach((caption) => {
45+
const track = caption.stream;
46+
let startTime = caption.startTime;
47+
let endTime = caption.endTime;
48+
49+
if (!inbandTextTracks[track]) {
50+
return;
51+
}
52+
53+
startTime += timestampOffset;
54+
endTime += timestampOffset;
55+
56+
inbandTextTracks[track].addCue(
57+
new Cue(
58+
startTime,
59+
endTime,
60+
caption.text
61+
));
62+
});
63+
};

0 commit comments

Comments
 (0)