UNPKG

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
/*! 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));