dictrigyn-video-player
Version:
Contains Video player library components powered by angular. These components are designed to be used in sunbird consumption platforms *(mobile app, web portal, offline desktop app)* to drive reusability, maintainability hence reducing the redundant devel
419 lines (388 loc) • 14.5 kB
JavaScript
/*! videojs-transcript - v1.0.0
* (c) 2015 Matthew Walsh, changes (c) 2020 Tom Byrer; Licensed MIT */
(function (window, videojs) {
'use strict';
// Global settings
var my = {};
my.settings = {};
my.prefix = 'transcript';
my.player = this;
// Defaults
var defaults = {
autoscroll: true,
clickArea: 'text',
showTitle: true,
showTrackSelector: true,
followPlayerTrack: true,
stopScrollWhenInUse: true,
};
/*global my*/
var utils = (function (plugin) {
return {
secondsToTime: function (timeInSeconds) {
var hour = Math.floor(timeInSeconds / 3600);
var min = Math.floor(timeInSeconds % 3600 / 60);
var sec = Math.floor(timeInSeconds % 60);
sec = (sec < 10) ? '0' + sec : sec;
min = (hour > 0 && min < 10) ? '0' + min : min;
if (hour > 0) {
return hour + ':' + min + ':' + sec;
}
return min + ':' + sec;
},
localize: function (string) {
return string; // TODO: do something here;
},
createEl: function (elementName, classSuffix) {
classSuffix = classSuffix || '';
var el = document.createElement(elementName);
el.className = plugin.prefix + classSuffix;
return el;
},
extend: function(obj) {
var type = typeof obj;
if (!(type === 'function' || type === 'object' && !!obj)) {
return obj;
}
var source, prop;
for (var i = 1, length = arguments.length; i < length; i++) {
source = arguments[i];
for (prop in source) {
obj[prop] = source[prop];
}
}
return obj;
}
};
}(my));
var eventEmitter = {
handlers_: [],
on: function on (object, eventtype, callback) {
if (typeof callback === 'function') {
this.handlers_.push([object, eventtype, callback]);
} else {
throw new TypeError('Callback is not a function.');
}
},
trigger: function trigger (object, eventtype) {
this.handlers_.forEach( function(h) {
if (h[0] === object &&
h[1] === eventtype) {
h[2].apply();
}
});
}
};
var scrollerProto = function(plugin) {
var initHandlers = function (el) {
var self = this;
// The scroll event. We want to keep track of when the user is scrolling the transcript.
el.addEventListener('scroll', function () {
if (self.isAutoScrolling) {
// If isAutoScrolling was set to true, we can set it to false and then ignore this event.
// It wasn't the user.
self.isAutoScrolling = false; // event handled
} else {
// We only care about when the user scrolls. Set userIsScrolling to true and add a nice class.
self.userIsScrolling = true;
el.classList.add('is-inuse');
}
});
// The mouseover event.
el.addEventListener('mouseenter', function () {
self.mouseIsOverTranscript = true;
});
el.addEventListener('mouseleave', function () {
self.mouseIsOverTranscript = false;
// Have a small delay before deciding user as done interacting.
setTimeout(function () {
// Make sure the user didn't move the pointer back in.
if (!self.mouseIsOverTranscript) {
self.userIsScrolling = false;
el.classList.remove('is-inuse');
}
}, 1000);
});
};
// Init instance variables
var init = function (element, plugin) {
this.element = element;
this.userIsScrolling = false;
//default to true in case user isn't using a mouse;
this.mouseIsOverTranscript = true;
this.isAutoScrolling = true;
initHandlers.call(this, this.element);
return this;
};
// Easing function for smoothness.
var easeOut = function (time, start, change, duration) {
return start + change * Math.sin(Math.min(1, time / duration) * (Math.PI / 2));
};
// Animate the scrolling.
var scrollTo = function (element, newPos, duration) {
var startTime = Date.now();
var startPos = element.scrollTop;
var self = this;
// Don't try to scroll beyond the limits. You won't get there and this will loop forever.
newPos = Math.max(0, newPos);
newPos = Math.min(element.scrollHeight - element.clientHeight, newPos);
var change = newPos - startPos;
// This inner function is called until the elements scrollTop reaches newPos.
var updateScroll = function () {
var now = Date.now();
var time = now - startTime;
self.isAutoScrolling = true;
element.scrollTop = easeOut(time, startPos, change, duration);
if (element.scrollTop !== newPos) {
requestAnimationFrame(updateScroll, element);
}
};
requestAnimationFrame(updateScroll, element);
};
// Scroll an element's parent so the element is brought into view.
var scrollToElement = function (element) {
if (this.canScroll()) {
var parent = element.parentElement;
var parentOffsetBottom = parent.offsetTop + parent.clientHeight;
var elementOffsetBottom = element.offsetTop + element.clientHeight;
var relTop = element.offsetTop - parent.offsetTop;
var relBottom = (element.offsetTop + element.clientHeight) - parent.offsetTop;
var newPos;
// If the top of the line is above the top of the parent view, were scrolling up,
// so we want to move the top of the element downwards to match the top of the parent.
if (relTop < parent.scrollTop) {
newPos = element.offsetTop - parent.offsetTop;
// If the bottom of the line is below the parent view, we're scrolling down, so we want the
// bottom edge of the line to move up to meet the bottom edge of the parent.
} else if (relBottom > (parent.scrollTop + parent.clientHeight)) {
newPos = elementOffsetBottom - parentOffsetBottom;
}
// Don't try to scroll if we haven't set a new position. If we didn't
// set a new position the line is already in view (i.e. It's not above
// or below the view)
// And don't try to scroll when the element is already in position.
if (newPos !== undefined && parent.scrollTop !== newPos) {
scrollTo.call(this, parent, newPos, 400);
}
}
};
// Return whether the element is scrollable.
var canScroll = function () {
var el = this.element;
return el.scrollHeight > el.offsetHeight;
};
// Return whether the user is interacting with the transcript.
var inUse = function () {
return this.userIsScrolling;
};
return {
init: init,
to : scrollToElement,
canScroll : canScroll,
inUse : inUse
}
}(my);
var scroller = function(element) {
return Object.create(scrollerProto).init(element);
};
/*global my*/
var trackList = function (plugin) {
var activeTrack;
return {
get: function () {
var validTracks = [];
var i, track;
my.tracks = my.player.textTracks();
for (i = 0; i < my.tracks.length; i++) {
track = my.tracks[i];
if (track.kind === 'captions' || track.kind === 'subtitles') {
validTracks.push(track);
}
}
return validTracks;
},
active: function (tracks) {
var i, track;
for (i = 0; i < my.tracks.length; i++) {
track = my.tracks[i];
if (track.mode === 'showing') {
activeTrack = track;
return track;
}
}
// fallback to first track
return activeTrack || tracks[0];
},
};
}(my);
/*globals utils, eventEmitter, my, scrollable*/
var widget = function (plugin) {
var my = {};
my.element = {};
my.body = {};
var on = function (event, callback) {
eventEmitter.on(this, event, callback);
};
var trigger = function (event) {
eventEmitter.trigger(this, event);
};
var createTitle = function () {
var header = utils.createEl('header', '-header');
header.textContent = utils.localize('Transcript');
return header;
};
var createSelector = function (){
var selector = utils.createEl('select', '-selector');
plugin.validTracks.forEach(function (track, i) {
var option = document.createElement('option');
option.value = i;
option.textContent = track.label + ' (' + track.language + ')';
selector.appendChild(option);
});
selector.addEventListener('change', function (e) {
setTrack(document.querySelector('#' + plugin.prefix + '-' + plugin.player.id() + ' option:checked').value);
trigger('trackchanged');
});
return selector;
};
var clickToSeekHandler = function (event) {
var clickedClasses = event.target.classList;
var clickedTime = event.target.getAttribute('data-begin') || event.target.parentElement.getAttribute('data-begin');
if (clickedTime !== undefined && clickedTime !== null) { // can be zero
if ((plugin.settings.clickArea === 'line') || // clickArea: 'line' activates on all elements
(plugin.settings.clickArea === 'timestamp' && clickedClasses.contains(plugin.prefix + '-timestamp')) ||
(plugin.settings.clickArea === 'text' && clickedClasses.contains(plugin.prefix + '-text'))) {
plugin.player.currentTime(clickedTime);
}
}
};
var createLine = function (cue) {
var line = utils.createEl('div', '-line');
var timestamp = utils.createEl('span', '-timestamp');
var text = utils.createEl('span', '-text');
line.setAttribute('data-begin', cue.startTime);
timestamp.textContent = utils.secondsToTime(cue.startTime);
text.innerHTML = cue.text;
line.appendChild(timestamp);
line.appendChild(text);
return line;
};
var createTranscriptBody = function (track) {
if (typeof track !== 'object') {
track = plugin.player.textTracks()[track];
}
var body = utils.createEl('div', '-body');
var line, i;
var fragment = document.createDocumentFragment();
// activeCues returns null when the track isn't loaded (for now?)
if (!track.activeCues) {
// If cues aren't loaded, set mode to hidden, wait, and try again.
// But don't hide an active track. In that case, just wait and try again.
if (track.mode !== 'showing') {
track.mode = 'hidden';
}
window.setTimeout(function() {
createTranscriptBody(track);
}, 100);
} else {
var cues = track.cues;
for (i = 0; i < cues.length; i++) {
line = createLine(cues[i]);
fragment.appendChild(line);
}
body.innerHTML = '';
body.appendChild(fragment);
body.setAttribute('lang', track.language);
body.scroll = scroller(body);
body.addEventListener('click', clickToSeekHandler);
my.element.replaceChild(body, my.body);
my.body = body;
}
};
var create = function () {
var el = document.createElement('div');
my.element = el;
el.setAttribute('id', plugin.prefix + '-' + plugin.player.id());
if (plugin.settings.showTitle) {
var title = createTitle();
el.appendChild(title);
}
if (plugin.settings.showTrackSelector) {
var selector = createSelector();
el.appendChild(selector);
}
my.body = utils.createEl('div', '-body');
el.appendChild(my.body);
setTrack(plugin.currentTrack);
return this;
};
var setTrack = function (track, trackCreated) {
createTranscriptBody(track, trackCreated);
};
var setCue = function (time) {
var active, i, line, begin, end;
var lines = my.body.children;
for (i = 0; i < lines.length; i++) {
line = lines[i];
begin = line.getAttribute('data-begin');
if (i < lines.length - 1) {
end = lines[i + 1].getAttribute('data-begin');
} else {
end = plugin.player.duration() || Infinity;
}
if (time > begin && time < end) {
if (!line.classList.contains('is-active')) { // don't update if it hasn't changed
line.classList.add('is-active');
if (plugin.settings.autoscroll && !(plugin.settings.stopScrollWhenInUse && my.body.scroll.inUse())) {
my.body.scroll.to(line);
}
}
} else {
line.classList.remove('is-active');
}
}
};
var el = function () {
return my.element;
};
return {
create: create,
setTrack: setTrack,
setCue: setCue,
el : el,
on: on,
trigger: trigger,
};
}(my);
var transcript = function (options) {
my.player = this;
my.validTracks = trackList.get();
my.currentTrack = trackList.active(my.validTracks);
my.settings = videojs.mergeOptions(defaults, options);
my.widget = widget.create();
var timeUpdate = function () {
my.widget.setCue(my.player.currentTime());
};
var updateTrack = function () {
my.currentTrack = trackList.active(my.validTracks);
my.widget.setTrack(my.currentTrack);
};
if (my.validTracks.length > 0) {
updateTrack();
my.player.on('timeupdate', timeUpdate);
if (my.settings.followPlayerTrack) {
my.player.on('captionstrackchange', updateTrack);
my.player.on('subtitlestrackchange', updateTrack);
}
} else {
throw new Error('videojs-transcript: No tracks found!');
}
return {
el: function () {
return my.widget.el();
},
setTrack: my.widget.setTrack
};
};
videojs.registerPlugin('transcript', transcript);
}(window, videojs));