/**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.media.VttTextParser');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.media.TextEngine');
goog.require('shaka.util.Error');
goog.require('shaka.util.StringUtils');
goog.require('shaka.util.TextParser');
/**
* @constructor
* @implements {shakaExtern.TextParser}
*/
shaka.media.VttTextParser = function() { };
/** @override */
shaka.media.VttTextParser.prototype.parseInit = function(data) {
goog.asserts.assert(false, 'VTT does not have init segments');
};
/**
* @override
* @throws {shaka.util.Error}
*/
shaka.media.VttTextParser.prototype.parseMedia = function(data, time) {
var VttTextParser = shaka.media.VttTextParser;
// Get the input as a string. Normalize newlines to \n.
var str = shaka.util.StringUtils.fromUTF8(data);
str = str.replace(/\r\n|\r(?=[^\n]|$)/gm, '\n');
var blocks = str.split(/\n{2,}/m);
if (!/^WEBVTT($|[ \t\n])/m.test(blocks[0])) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.TEXT,
shaka.util.Error.Code.INVALID_TEXT_HEADER);
}
var offset = time.segmentStart;
// Parse X-TIMESTAMP-MAP metadata header if it's present to get
// time offset information.
// https://tools.ietf.org/html/draft-pantos-http-live-streaming-20#section-3.5
if (blocks[0].indexOf('X-TIMESTAMP-MAP') >= 0) {
// 'X-TIMESTAMP-MAP' header is used in HLS to align text with
// the rest of the media.
// The header format is 'X-TIMESTAMP-MAP=MPEGTS:n,LOCAL:m'
// (the attributes can go in any order)
// where n is MPEG-2 time and m is cue time it maps to.
// For example 'X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:900000'
// means an offset of 10 seconds
// 900000/MPEG_TIMESCALE - cue time.
var cueTimeMatch =
blocks[0].match(/LOCAL:((?:(\d{1,}):)?(\d{2}):(\d{2})\.(\d{3}))/m);
var mpegTimeMatch = blocks[0].match(/MPEGTS:(\d+)/m);
if (cueTimeMatch && mpegTimeMatch) {
var parser = new shaka.util.TextParser(cueTimeMatch[1]);
var cueTime = shaka.media.VttTextParser.parseTime_(parser);
var mpegTime = Number(mpegTimeMatch[1]);
var mpegTimescale = shaka.media.VttTextParser.MPEG_TIMESCALE_;
// Apple-encoded HLS content uses absolute timestamps, so assume
// the presence of the map tag means the content uses absolute
// timestamps.
offset = time.periodStart + (mpegTime / mpegTimescale - cueTime);
}
}
var ret = [];
for (var i = 1; i < blocks.length; i++) {
var lines = blocks[i].split('\n');
var cue = VttTextParser.parseCue_(lines, offset);
if (cue)
ret.push(cue);
}
return ret;
};
/**
* Parses a text block into a Cue object.
*
* @param {!Array.<string>} text
* @param {number} timeOffset
* @return {?TextTrackCue}
* @private
*/
shaka.media.VttTextParser.parseCue_ = function(text, timeOffset) {
// Skip empty blocks.
if (text.length == 1 && !text[0])
return null;
// Skip comment blocks.
if (/^NOTE($|[ \t])/.test(text[0]))
return null;
var id = null;
var index = text[0].indexOf('-->');
if (index < 0) {
id = text[0];
text.splice(0, 1);
}
// Parse the times.
var parser = new shaka.util.TextParser(text[0]);
var start = shaka.media.VttTextParser.parseTime_(parser);
var expect = parser.readRegex(/[ \t]+-->[ \t]+/g);
var end = shaka.media.VttTextParser.parseTime_(parser);
if (start == null || expect == null || end == null) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.TEXT,
shaka.util.Error.Code.INVALID_TEXT_CUE);
}
start += timeOffset;
end += timeOffset;
// Get the payload.
var payload = text.slice(1).join('\n').trim();
var cue = shaka.media.TextEngine.makeCue(start, end, payload);
if (!cue)
return null;
// Parse optional settings.
parser.skipWhitespace();
var word = parser.readWord();
while (word) {
if (!shaka.media.VttTextParser.parseSetting(cue, word)) {
shaka.log.warning('VTT parser encountered an invalid VTT setting: ',
word,
' The setting will be ignored.');
}
parser.skipWhitespace();
word = parser.readWord();
}
if (id != null)
cue.id = id;
return cue;
};
/**
* Parses a WebVTT setting from the given word.
*
* @param {!TextTrackCue} cue
* @param {string} word
* @return {boolean} True on success.
*/
shaka.media.VttTextParser.parseSetting = function(cue, word) {
// NOTE: positionAlign and lineAlign settings are not supported by Chrome
// at the moment, so setting them will have no effect.
// The bug on chromium to implement them:
// https://bugs.chromium.org/p/chromium/issues/detail?id=633690
var results = null;
if ((results = /^align:(start|middle|center|end|left|right)$/.exec(word))) {
cue.align = results[1];
if (results[1] == 'center' && cue.align != 'center') {
// Workaround for a Chrome bug http://crbug.com/663797
// Chrome does not support align = 'center'
cue.position = 'auto';
cue.align = 'middle';
}
} else if ((results = /^vertical:(lr|rl)$/.exec(word))) {
cue.vertical = results[1];
} else if ((results = /^size:([\d\.]+)%$/.exec(word))) {
cue.size = Number(results[1]);
} else if ((results =
/^position:([\d\.]+)%(?:,(line-left|line-right|center|start|end))?$/
.exec(word))) {
cue.position = Number(results[1]);
if (results[2])
cue.positionAlign = results[2];
} else if ((results =
/^line:([\d\.]+)%(?:,(start|end|center))?$/.exec(word))) {
cue.snapToLines = false;
cue.line = Number(results[1]);
if (results[2])
cue.lineAlign = results[2];
} else if ((results = /^line:(-?\d+)(?:,(start|end|center))?$/.exec(word))) {
cue.snapToLines = true;
cue.line = Number(results[1]);
if (results[2])
cue.lineAlign = results[2];
} else {
return false;
}
return true;
};
/**
* Parses a WebVTT time from the given parser.
*
* @param {!shaka.util.TextParser} parser
* @return {?number}
* @private
*/
shaka.media.VttTextParser.parseTime_ = function(parser) {
// 00:00.000 or 00:00:00.000 or 0:00:00.000
var results = parser.readRegex(/(?:(\d{1,}):)?(\d{2}):(\d{2})\.(\d{3})/g);
if (results == null)
return null;
// This capture is optional, but will still be in the array as undefined,
// default to 0.
var hours = Number(results[1]) || 0;
var minutes = Number(results[2]);
var seconds = Number(results[3]);
var miliseconds = Number(results[4]);
if (minutes > 59 || seconds > 59)
return null;
return (miliseconds / 1000) + seconds + (minutes * 60) + (hours * 3600);
};
/**
* @const {number}
* @private
*/
shaka.media.VttTextParser.MPEG_TIMESCALE_ = 90000;
shaka.media.TextEngine.registerParser(
'text/vtt',
shaka.media.VttTextParser);
shaka.media.TextEngine.registerParser(
'text/vtt; codecs="vtt"',
shaka.media.VttTextParser);