+
+
+
\ No newline at end of file
diff --git a/samples/captioning/external-captions.vtt b/samples/captioning/external-captions.vtt
new file mode 100644
index 0000000000..b299738e4d
--- /dev/null
+++ b/samples/captioning/external-captions.vtt
@@ -0,0 +1,10 @@
+WEBVTT
+
+00:00:00.500 --> 00:00:04.000
+These are example captions in the VTT format
+
+00:00:04.500 --> 00:00:08.300
+They are not listed in the manifest file
+
+00:00:010.000 --> 00:00:14.000
+This is the last caption. Enjoy!
\ No newline at end of file
diff --git a/samples/samples.json b/samples/samples.json
index 4f21963df9..197c7cc311 100644
--- a/samples/samples.json
+++ b/samples/samples.json
@@ -154,6 +154,11 @@
"title": "TTML EBU timed text tracks",
"description": "Example showing content with TTML EBU timed text tracks.",
"href": "captioning/ttml-ebutt-sample.html"
+ },
+ {
+ "title": "Load external VTT captions",
+ "description": "Example showing how to load external VTT captions.",
+ "href": "captioning/external-caption-vtt.html"
}
]
},
diff --git a/src/core/errors/Errors.js b/src/core/errors/Errors.js
index c3485b5496..a43f0a7532 100644
--- a/src/core/errors/Errors.js
+++ b/src/core/errors/Errors.js
@@ -112,6 +112,9 @@ class Errors extends ErrorsBase {
this.REMOVE_ERROR_MESSAGE = 'buffer is not defined';
this.DATA_UPDATE_FAILED_ERROR_MESSAGE = 'Data update failed';
+ this.CAPTIONS_LOADER_PARSING_FAILURE_ERROR_MESSAGE = 'caption parsing failed for ';
+ this.CAPTIONS_LOADER_LOADING_FAILURE_ERROR_MESSAGE = 'Failed loading captions: ';
+
this.CAPABILITY_MEDIASOURCE_ERROR_MESSAGE = 'mediasource is not supported';
this.CAPABILITY_MEDIAKEYS_ERROR_MESSAGE = 'mediakeys is not supported';
this.TIMED_TEXT_ERROR_MESSAGE_PARSE = 'parsing error :';
diff --git a/src/core/events/CoreEvents.js b/src/core/events/CoreEvents.js
index 2579f06f80..f2f4f7eb76 100644
--- a/src/core/events/CoreEvents.js
+++ b/src/core/events/CoreEvents.js
@@ -64,6 +64,7 @@ class CoreEvents extends EventsBase {
this.MANIFEST_UPDATED = 'manifestUpdated';
this.MEDIA_FRAGMENT_LOADED = 'mediaFragmentLoaded';
this.MEDIA_FRAGMENT_NEEDED = 'mediaFragmentNeeded';
+ this.EXTERNAL_CAPTIONS_LOADED = 'externalCaptionsLoaded';
this.QUOTA_EXCEEDED = 'quotaExceeded';
this.REPRESENTATION_UPDATE_STARTED = 'representationUpdateStarted';
this.REPRESENTATION_UPDATE_COMPLETED = 'representationUpdateCompleted';
diff --git a/src/streaming/CaptionsLoader.js b/src/streaming/CaptionsLoader.js
new file mode 100644
index 0000000000..c42b0d4349
--- /dev/null
+++ b/src/streaming/CaptionsLoader.js
@@ -0,0 +1,178 @@
+/**
+ * The copyright in this software is being made available under the BSD License,
+ * included below. This software may be subject to other third party and contributor
+ * rights, including patent rights, and no such rights are granted under this license.
+ *
+ * Copyright (c) 2013, Dash Industry Forum.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ * * Neither the name of Dash Industry Forum nor the names of its
+ * contributors may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+import Constants from './constants/Constants';
+import DashConstants from '../dash/constants/DashConstants';
+import URLLoader from './net/URLLoader';
+import URLUtils from './utils/URLUtils';
+import TextRequest from './vo/TextRequest';
+import DashJSError from './vo/DashJSError';
+import {HTTPRequest} from './vo/metrics/HTTPRequest';
+import EventBus from '../core/EventBus';
+import Events from '../core/events/Events';
+import Errors from '../core/errors/Errors';
+import FactoryMaker from '../core/FactoryMaker';
+import VTTParser from './utils/VTTParser';
+
+function CaptionsLoader(config) {
+
+ config = config || {};
+ const context = this.context;
+ const debug = config.debug;
+ const eventBus = EventBus(context).getInstance();
+ const urlUtils = URLUtils(context).getInstance();
+
+ let instance,
+ logger,
+ urlLoader,
+ parser;
+
+ let mssHandler = config.mssHandler;
+ let errHandler = config.errHandler;
+
+ function setup() {
+ logger = debug.getLogger(instance);
+
+ urlLoader = URLLoader(context).create({
+ errHandler: config.errHandler,
+ dashMetrics: config.dashMetrics,
+ mediaPlayerModel: config.mediaPlayerModel,
+ requestModifier: config.requestModifier,
+ useFetch: config.settings.get().streaming.lowLatencyEnabled,
+ urlUtils: urlUtils,
+ constants: Constants,
+ dashConstants: DashConstants,
+ errors: Errors
+ });
+ }
+
+ function createParser(data) {
+ if (data.indexOf('WEBVTT') > -1) {
+ return VTTParser(context).getInstance();
+ }
+ return null;
+ }
+
+ function load(url, mediaInfo) {
+
+ const request = new TextRequest(url, HTTPRequest.MEDIA_SEGMENT_TYPE);
+
+ urlLoader.load({
+ request: request,
+ success: function (data, textStatus, responseURL) {
+ let actualUrl,
+ captions;
+
+ // Handle redirects for the MPD - as per RFC3986 Section 5.1.3
+ // also handily resolves relative MPD URLs to absolute
+ if (responseURL && responseURL !== url) {
+ actualUrl = responseURL;
+ } else {
+ // usually this case will be caught and resolved by
+ // responseURL above but it is not available for IE11 and Edge/12 and Edge/13
+ if (urlUtils.isRelative(url)) {
+ url = urlUtils.resolve(url, window.location.href);
+ }
+ }
+
+ // A response of no content implies in-memory is properly up to date
+ if (textStatus == 'No Content') {
+ return;
+ }
+
+ // Create parser according to captions type
+ parser = createParser(data);
+
+ if (parser === null) {
+ eventBus.trigger(Events.EXTERNAL_CAPTIONS_LOADED, {
+ captions: null,
+ error: new DashJSError(
+ Errors.CAPTIONS_LOADER_PARSING_FAILURE_ERROR_CODE,
+ Errors.CAPTIONS_LOADER_PARSING_FAILURE_ERROR_MESSAGE + `${url}`
+ )
+ });
+ return;
+ }
+
+ try {
+ captions = parser.parse(data, 0);
+ } catch (e) {
+ errHandler.error(new DashJSError(Errors.TIMED_TEXT_ERROR_ID_PARSE_CODE, Errors.TIMED_TEXT_ERROR_MESSAGE_PARSE + e.message, data));
+ }
+
+ if (captions) {
+ eventBus.trigger(Events.EXTERNAL_CAPTIONS_LOADED, { captions: captions, mediaInfo: mediaInfo });
+ } else {
+ eventBus.trigger(Events.EXTERNAL_CAPTIONS_LOADED, {
+ captions: null,
+ error: new DashJSError(
+ Errors.CAPTIONS_LOADER_PARSING_FAILURE_ERROR_CODE,
+ Errors.CAPTIONS_LOADER_PARSING_FAILURE_ERROR_MESSAGE + `${url}`
+ )
+ });
+ }
+ },
+ error: function (request, statusText, errorText) {
+ eventBus.trigger(Events.EXTERNAL_CAPTIONS_LOADED, {
+ captions: null,
+ error: new DashJSError(
+ Errors.CAPTIONS_LOADER_LOADING_FAILURE_ERROR_CODE,
+ Errors.CAPTIONS_LOADER_LOADING_FAILURE_ERROR_MESSAGE + `${url}, ${errorText}`
+ )
+ });
+ }
+ });
+ }
+
+ function reset() {
+ if (urlLoader) {
+ urlLoader.abort();
+ urlLoader = null;
+ }
+
+ if (mssHandler) {
+ mssHandler.reset();
+ }
+ }
+
+ instance = {
+ load: load,
+ reset: reset
+ };
+
+ setup();
+
+ return instance;
+}
+
+CaptionsLoader.__dashjs_factory_name = 'CaptionsLoader';
+
+const factory = FactoryMaker.getClassFactory(CaptionsLoader);
+export default factory;
diff --git a/src/streaming/MediaPlayer.js b/src/streaming/MediaPlayer.js
index b73cf1dc2c..8599bbe284 100644
--- a/src/streaming/MediaPlayer.js
+++ b/src/streaming/MediaPlayer.js
@@ -38,6 +38,7 @@ import GapController from './controllers/GapController';
import MediaController from './controllers/MediaController';
import BaseURLController from './controllers/BaseURLController';
import ManifestLoader from './ManifestLoader';
+import CaptionsLoader from './CaptionsLoader';
import ErrorHandler from './utils/ErrorHandler';
import Capabilities from './utils/Capabilities';
import CapabilitiesFilter from './utils/CapabilitiesFilter';
@@ -1288,6 +1289,33 @@ function MediaPlayer() {
return textController.isTextEnabled();
}
+ /**
+ * Use this method to add a captions from an external URL (for captions not listed in the manifest).
+ * @param {string} url - URL of the captions file to load. Use module:MediaPlayer#dashjs.MediaPlayer.events.TEXT_TRACK_ADDED.
+ * @param {MediaInfo} mediaInfo - A MediaInfo object with details about the text track.
+ * @see {@link MediaPlayerEvents#event:TEXT_TRACK_ADDED dashjs.MediaPlayer.events.TEXT_TRACK_ADDED}
+ * @throws {@link module:MediaPlayer~PLAYBACK_NOT_INITIALIZED_ERROR PLAYBACK_NOT_INITIALIZED_ERROR} if called before initializePlayback function
+ * @memberof module:MediaPlayer
+ * @instance
+ */
+ function addTextTrack(url, mediaInfo) {
+ if (!streamingInitialized) {
+ throw STREAMING_NOT_INITIALIZED_ERROR;
+ }
+
+ if (textController === undefined) {
+ textController = TextController(context).getInstance();
+ }
+
+ let captionsLoader = createCaptionsLoader();
+
+ uriFragmentModel.initialize(url);
+
+ // The event `EXTERNAL_CAPTIONS_LOADED` is handled by TextSourceBuffer
+ // and does the required work to add the track once it's loaded.
+ captionsLoader.load(url, mediaInfo);
+ }
+
/**
* Use this method to change the current text track for both external time text files and fragmented text tracks. There is no need to
* set the track mode on the video object to switch a track when using this method.
@@ -2159,6 +2187,18 @@ function MediaPlayer() {
});
}
+ function createCaptionsLoader() {
+ return CaptionsLoader(context).create({
+ debug: debug,
+ errHandler: errHandler,
+ dashMetrics: dashMetrics,
+ mediaPlayerModel: mediaPlayerModel,
+ requestModifier: RequestModifier(context).getInstance(),
+ mssHandler: mssHandler,
+ settings: settings
+ });
+ }
+
function detectProtection() {
if (protectionController) {
return protectionController;
@@ -2405,6 +2445,7 @@ function MediaPlayer() {
enableText: enableText,
enableForcedTextStreaming: enableForcedTextStreaming,
isTextEnabled: isTextEnabled,
+ addTextTrack: addTextTrack,
setTextTrack: setTextTrack,
getBitrateInfoListFor: getBitrateInfoListFor,
getStreamsFromManifest: getStreamsFromManifest,
diff --git a/src/streaming/text/TextSourceBuffer.js b/src/streaming/text/TextSourceBuffer.js
index e6084d7ab3..04cea8a025 100644
--- a/src/streaming/text/TextSourceBuffer.js
+++ b/src/streaming/text/TextSourceBuffer.js
@@ -81,6 +81,8 @@ function TextSourceBuffer() {
logger = Debug(context).getInstance().getLogger(instance);
resetInitialSettings();
+
+ eventBus.on(Events.EXTERNAL_CAPTIONS_LOADED, onExternalCaptionsLoaded, instance);
}
function resetFragmented () {
@@ -265,6 +267,18 @@ function TextSourceBuffer() {
currFragmentedTrackIdx = idx;
}
+
+ function onExternalCaptionsLoaded(e) {
+ if (!e.error) {
+ // Extend `mediaInfos`-array to ensure `totalNrTracks` is increased.
+ mediaInfos = mediaInfos.concat([e.mediaInfo]);
+ createTextTrackFromMediaInfo(e.captions, e.mediaInfo);
+ } else {
+ logger.error('Unable to load external captions: ' + e.error.message);
+ }
+ eventBus.off(Events.EXTERNAL_CAPTIONS_LOADED, onExternalCaptionsLoaded, this);
+ }
+
function createTextTrackFromMediaInfo(captionData, mediaInfo) {
const textTrackInfo = new TextTrackInfo();
const trackKindMap = { subtitle: 'subtitles', caption: 'captions' }; //Dash Spec has no "s" on end of KIND but HTML needs plural.