/**
* @function addSubtitlesToVideo
* @summary
* Create subtitles from a standard .vtt file, and add to the input container Movie Clip.
* @description
* The .vtt file must be in the directory /src/sounds
* Center the container Movie Clip at the desired location of the subtitles, and set
* the alpha of the shape in the container to zero.
*
* Set options.showWhenMuted to true if you only want the subtitles displayed
* when the video is muted. This cannot detect if the entire browser tab is muted.
*
* Set optional styling parameters for the text and background shape.
*
* Required parameters:
* @param {Object} options
* @param {string} options.vtFileName
* @param {createjs.MovieClip} options.container
*
* Optional parameters:
* @param {boolean=} options.allowMutedPlayWithSubtitles (default true)
* @param {boolean=} options.showWhenMuted (default false)
* @param {number=} options.fontSize (default 24) units px
* @param {string=} options.fontFamily (default "Arial")
* @param {string=} options.fontColor (default "#CCC")
* @param {number=} options.fontAlpha (default 0.8)
* @param {number=} options.fontStyle (default "normal", allowable "italic" and "oblique")
* @param {number|string=} options.fontWeight (default "normal", allowable 100, 200, ...900)
* @param {string=} options.backgroundColor (default "#333")
* @param {number=} options.backgroundAlpha (default 0.3)
* @param {number=} options.padding (default 10) units px
* @param {number=} options.lineFactor (default 1.25) lineHeight = lineFactor * fontSize
*/
function addSubtitlesToVideo (options) {
if (typeof options !== 'object') {
console.error('addSubtitlesToVideo must be passed an options object')
return
}
if (typeof options.vtFileName !== 'string') {
console.error('addSubtitlestoVideo must be passed options.vtFileName')
return
}
var createjs = window.createjs
if (!(options.container instanceof createjs.MovieClip)) {
console.error('options.container must be a movie clip')
return
}
window.$b.allowMutedPlayWithSubtitles = options.allowMutedPlayWithSubtitles !== false
if (window.$b.allowMutedPlayWithSubtitles && !window.$b.playVideoWithFallback) {
console.error('Upgrade blink version to allow muted autoplay with subtitles')
}
var container = options.container
var containerCenter = {
x: container.nominalBounds.x + container.nominalBounds.width / 2,
y: container.nominalBounds.y + container.nominalBounds.height / 2
}
var showWhenMuted = !!options.showWhenMuted
console.log('showWhenMuted = ' + showWhenMuted)
var mcOptions = {
mode: 'independent',
startPosition: 0,
loop: false,
ignoreGlobalPause: true,
paused: false,
labels: { start: 0 }
}
var mc = new createjs.MovieClip(mcOptions)
options.container.addChild(mc)
mc.x = containerCenter.x
mc.y = containerCenter.y
var timeline = new createjs.Timeline({ useTicks: false })
mc.textTimeline = timeline
var video = window.$b._videoSlot
loadVtt(options)
.then(function (result) {
var frames = parseVtt({ text: result })
if (frames instanceof Array) {
var addFramesAndPlay = function () {
addFrames(Object.assign({}, options, {
mc: mc,
frames: frames,
timeline: timeline,
duration: video.duration
}))
mc.textTimeline.gotoAndStop()
/*
* Sometimes the browser will be stopping autoplay before changing video.paused to false,
* so add small delay before playing subtitles
*/
setTimeout(function () {
playSubtitles()
}, 10)
}
/*
* Cannot add the subtitles until the video duration is known
*/
if (video.readyState > 0) {
addFramesAndPlay()
} else {
video.addEventListener('canplay', function (evt) {
addFramesAndPlay()
})
}
}
})
function onMute () {
if (window.$b.getAdVolume() === 0) {
mc.alpha = 1
} else {
mc.alpha = 0
}
window.$b.stage.update()
}
function playSubtitles (evt) {
if (!video.paused) {
var currentTime = video.currentTime
mc.textTimeline.gotoAndPlay(currentTime * 1000)
mc.alpha = 1
showWhenMuted && onMute()
}
}
function pauseSubtitles (evt) {
mc.textTimeline.gotoAndStop()
mc.alpha = 0
}
video.addEventListener('play', playSubtitles)
video.addEventListener('playing', playSubtitles)
video.addEventListener('seeked', playSubtitles)
video.addEventListener('pause', pauseSubtitles)
if (showWhenMuted) {
onMute()
window.$b.on('AdVolumeChange', onMute)
}
}
function isNumber (x) {
return typeof x === 'number'
}
function addFrames (options) {
var defaultFontSize = 24
var defaultFontWeight = 'bold'
var defaultFontFamily = 'Arial'
var defaultLineFactor = 1.25
var defaultFontColor = '#333'
var defaultBackgroundColor = '#CCC'
var defaultPadding = 10
var frames = options.frames
var timeline = options.timeline
var mc = options.mc
/*
* Set font & shape options
*/
var fontSize = isNumber(options.fontSize) ? options.fontSize : defaultFontSize
var fontWeight
if (!options.fontWeight) {
fontWeight = defaultFontWeight
} else if (isNumber(options.fontWeight)) {
if ([100, 200, 300, 400, 500, 600, 700, 800, 900].includes(options.fontWeight)) {
fontWeight = options.fontWeight
} else {
console.error('Numeric allowable font weigths are 100, 200, ... 900')
fontWeight = defaultFontWeight
}
} else if (['normal', 'bold', 'bolder', 'lighter'].includes(options.fontWeight)) {
fontWeight = options.fontWeight
} else {
console.error('Named allowable font weights are normal, bold, bolder, or lighter')
fontWeight = defaultFontWeight
}
var fontFamily = defaultFontFamily
if (options.fontFamily) {
var fonts = [
'Arial',
'Arial Black',
'Helvetica',
'Times New Roman',
'Times',
'Courier New',
'Courier',
'Verdana',
'Georgia',
'Palatino',
'Garamond',
'Bookman',
'Comic Sans MS',
'Trebuchet MS',
'Impact'
]
if (!fonts.includes(options.fontFamily)) {
console.error('Font Family must be one of ' + fonts.join(', '))
} else {
fontFamily = options.fontFamily
}
}
var fontStyle = 'normal'
var fontStyles = ['normal', 'italic', 'oblique']
if (options.fontStyle) {
if (fontStyles.includes(options.fontStyle)) {
fontStyle = options.fontStyle
} else {
console.error('font style must be one of ' + fontStyles.join(', '))
}
}
var lineHeight = isNumber(options.lineFactor)
? options.lineFactor * fontSize : defaultLineFactor * fontSize
var fontColor = options.fontColor || defaultFontColor
var fontAlpha = isNumber(options.fontAlpha) ? options.fontAlpha : 0.8
var backgroundColor = options.backgroundColor || defaultBackgroundColor
var backgroundAlpha = isNumber(options.backgroundAlpha) ? options.backgroundAlpha : 0.3
var padding = isNumber(options.padding) ? options.padding : defaultPadding
var font = fontStyle + ' ' + fontWeight + ' ' + fontSize + 'px ' + fontFamily
frames.forEach(function (frame) {
if (typeof frame !== 'object') {
console.error('each frame must be an object')
} else {
frame.text = typeof frame.text === 'string' ? frame.text : ''
var textObj = new createjs.Text(frame.text, font, fontColor)
textObj.textAlign = 'center'
textObj.alpha = 0
textObj.lineHeight = lineHeight
var textBounds = textObj.getBounds()
var shapeBounds = {
x: textBounds.x - padding,
y: -padding,
width: textBounds.width + padding * 2,
height: textBounds.height + padding * 2
}
var shape = new createjs.Shape()
shape
.graphics
.beginFill(backgroundColor)
.drawRect(shapeBounds.x, shapeBounds.y, shapeBounds.width, shapeBounds.height)
shape.alpha = 0
frame.items = [
{ object: textObj, alpha: fontAlpha },
{ object: shape, alpha: backgroundAlpha }
]
applyTween({
timeline: timeline,
duration: options.duration,
frame: frame
})
mc.addChild(shape)
mc.addChild(textObj)
}
})
}
function applyTween (options) {
var timeline = options.timeline
var frame = options.frame
var start = frame.start * 1000
/*
* Prevent tween at time 0 from displaying at end of video
*/
start = Math.max(start, 100)
var end = frame.end * 1000
end = Math.min(end, options.duration * 1000)
if (start >= end) return
var delta = end - start
var remainder = options.duration * 1000 - end
frame.items.forEach(function (item) {
var tween =
createjs.Tween.get(item.object, { override: true, ignoreGlobalPause: true, loop: true })
.to({ alpha: 0 })
.wait(start)
.to({ alpha: item.alpha })
.wait(delta)
.to({ alpha: 0 })
.wait(remainder)
timeline.addTween(tween)
})
console.log('start, end', start, end)
}
function loadVtt (options) {
var url = window.$b.baseUrl + 'sounds/' + options.vtFileName
return window.$.ajax({
url: url,
dataType: 'text'
})
}
function parseVtt (options) {
var text = options.text.split('\n')
if (!/WEBVTT/i.test(text[0])) {
console.error('captions file must have WEBVTT in first line')
return
}
text.shift()
text = removeCues(text)
text = removeNotes(text)
var frames = []
while (getNextFrame(text, frames)) {}
return frames
function removeCues (text) {
var textWithoutCues = []
for (var i = 0; i < text.length - 1; i++) {
/*
* A cue is a non-blank line preceding a time period
*/
if (!(text[i] && (text[i + 1].indexOf('-->') > -1))) {
textWithoutCues.push(text[i])
}
}
textWithoutCues.push(text[text.length - 1])
return textWithoutCues
}
/**
* A Note must be:
* 1. preceded by a blank line
* 2. start with NOTE in caps
* 3. includes all text until a blank line followed by a time period
* @param {Array} text
* @returns {Array}
*/
function removeNotes (text) {
var textWithoutNotes = []
var i = 0
var isNote
while (i < text.length - 1) {
isNote = false
if (!text[i] && (text[i + 1].indexOf('NOTE') === 0)) {
isNote = true
/*
* A NOTE can be more than one line long
*/
while ((i < text.length - 1) && (text[i + 1].indexOf('-->') < 0)) {
i += 1
}
}
/*
* Check if NOTE is in the last block of the file
*/
if (!(isNote && (i === text.length - 1))) {
textWithoutNotes.push(text[i])
}
i += 1
}
/*
* Check for a one-line NOTE
*/
if ((i < text.length) && (text[i].indexOf('NOTE') !== 0)) {
textWithoutNotes.push(text[i])
}
return textWithoutNotes
}
function getNextFrame (text, frames) {
var frame = {}
var found
do {
found = getTimes(text, frame)
} while (!found)
if (!text.length) return false
getText(text, frame)
frames.push(frame)
return true
}
function getTimes (text, frame) {
if (!text.length) return true
var line = text.shift()
var words = line.split(/\s*-->\s*/)
if (words.length < 2) return false
var start = parseTime(words[0])
if (start === false) return false
var end = parseTime(words[1])
if (end === false) return false
frame.start = start
frame.end = end
return true
}
function parseTime (word) {
word = word.replace(/(\d)\s.*$/, '$1')
var parts = word.split(':')
var time = 0
var seconds = parseFloat(parts.pop())
if (isNaN(seconds)) return false
time = seconds
if (!parts.length) return time
var minutes = parseInt(parts.pop())
if (isNaN(minutes)) return time
time += minutes * 60
if (!parts.length) return time
var hours = parseInt(parts.pop())
if (isNaN(hours)) return time
time += 3600 * hours
return time
}
function getText (text, frame) {
frame.text = ''
while (text.length && text[0].indexOf('-->') < 0) {
if (frame.text && (text.length > 1) && (text[1].indexOf('-->') < 0)) {
frame.text += '\n'
}
/*
* Remove HTML tags
*/
frame.text += (text.shift())
.replace(/<.*>/g, '')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/&/g, '&')
}
}
}
window.$b = window.$b || {}
window.$b.snippets = window.$b.snippets || {}
var snippets = window.$b.snippets
snippets.addSubtitlesToVideo = addSubtitlesToVideo
if (window.module) {
window.module.exports = addSubtitlesToVideo
}