mirror of
https://github.com/videojs/video.js.git
synced 2025-03-31 22:22:09 +02:00
598 lines
16 KiB
JavaScript
598 lines
16 KiB
JavaScript
/**
|
|
* @file tech.js
|
|
* Media Technology Controller - Base class for media playback
|
|
* technology controllers like Flash and HTML5
|
|
*/
|
|
|
|
import Component from '../component';
|
|
import TextTrack from '../tracks/text-track';
|
|
import TextTrackList from '../tracks/text-track-list';
|
|
import * as Fn from '../utils/fn.js';
|
|
import log from '../utils/log.js';
|
|
import { createTimeRange } from '../utils/time-ranges.js';
|
|
import { bufferedPercent } from '../utils/buffer.js';
|
|
import window from 'global/window';
|
|
import document from 'global/document';
|
|
|
|
/**
|
|
* Base class for media (HTML5 Video, Flash) controllers
|
|
*
|
|
* @param {Object=} options Options object
|
|
* @param {Function=} ready Ready callback function
|
|
* @extends Component
|
|
* @class Tech
|
|
*/
|
|
class Tech extends Component {
|
|
|
|
constructor(options={}, ready=function(){}){
|
|
// we don't want the tech to report user activity automatically.
|
|
// This is done manually in addControlsListeners
|
|
options.reportTouchActivity = false;
|
|
super(null, options, ready);
|
|
|
|
// keep track of whether the current source has played at all to
|
|
// implement a very limited played()
|
|
this.hasStarted_ = false;
|
|
this.on('playing', function() {
|
|
this.hasStarted_ = true;
|
|
});
|
|
this.on('loadstart', function() {
|
|
this.hasStarted_ = false;
|
|
});
|
|
|
|
this.textTracks_ = options.textTracks;
|
|
|
|
// Manually track progress in cases where the browser/flash player doesn't report it.
|
|
if (!this.featuresProgressEvents) {
|
|
this.manualProgressOn();
|
|
}
|
|
|
|
// Manually track timeupdates in cases where the browser/flash player doesn't report it.
|
|
if (!this.featuresTimeupdateEvents) {
|
|
this.manualTimeUpdatesOn();
|
|
}
|
|
|
|
this.initControlsListeners();
|
|
|
|
if (options.nativeCaptions === false || options.nativeTextTracks === false) {
|
|
this.featuresNativeTextTracks = false;
|
|
}
|
|
|
|
if (!this.featuresNativeTextTracks) {
|
|
this.emulateTextTracks();
|
|
}
|
|
|
|
this.initTextTrackListeners();
|
|
|
|
// Turn on component tap events
|
|
this.emitTapEvents();
|
|
}
|
|
|
|
/**
|
|
* Set up click and touch listeners for the playback element
|
|
* On desktops, a click on the video itself will toggle playback,
|
|
* on a mobile device a click on the video toggles controls.
|
|
* (toggling controls is done by toggling the user state between active and
|
|
* inactive)
|
|
* A tap can signal that a user has become active, or has become inactive
|
|
* e.g. a quick tap on an iPhone movie should reveal the controls. Another
|
|
* quick tap should hide them again (signaling the user is in an inactive
|
|
* viewing state)
|
|
* In addition to this, we still want the user to be considered inactive after
|
|
* a few seconds of inactivity.
|
|
* Note: the only part of iOS interaction we can't mimic with this setup
|
|
* is a touch and hold on the video element counting as activity in order to
|
|
* keep the controls showing, but that shouldn't be an issue. A touch and hold on
|
|
* any controls will still keep the user active
|
|
*
|
|
* @method initControlsListeners
|
|
*/
|
|
initControlsListeners() {
|
|
// if we're loading the playback object after it has started loading or playing the
|
|
// video (often with autoplay on) then the loadstart event has already fired and we
|
|
// need to fire it manually because many things rely on it.
|
|
// Long term we might consider how we would do this for other events like 'canplay'
|
|
// that may also have fired.
|
|
this.ready(function(){
|
|
if (this.networkState && this.networkState() > 0) {
|
|
this.trigger('loadstart');
|
|
}
|
|
// Allow the tech ready event to handle synchronisity
|
|
}, true);
|
|
}
|
|
|
|
/* Fallbacks for unsupported event types
|
|
================================================================================ */
|
|
// Manually trigger progress events based on changes to the buffered amount
|
|
// Many flash players and older HTML5 browsers don't send progress or progress-like events
|
|
/**
|
|
* Turn on progress events
|
|
*
|
|
* @method manualProgressOn
|
|
*/
|
|
manualProgressOn() {
|
|
this.on('durationchange', this.onDurationChange);
|
|
|
|
this.manualProgress = true;
|
|
|
|
// Trigger progress watching when a source begins loading
|
|
this.one('ready', this.trackProgress);
|
|
}
|
|
|
|
/**
|
|
* Turn off progress events
|
|
*
|
|
* @method manualProgressOff
|
|
*/
|
|
manualProgressOff() {
|
|
this.manualProgress = false;
|
|
this.stopTrackingProgress();
|
|
|
|
this.off('durationchange', this.onDurationChange);
|
|
}
|
|
|
|
/**
|
|
* Track progress
|
|
*
|
|
* @method trackProgress
|
|
*/
|
|
trackProgress() {
|
|
this.stopTrackingProgress();
|
|
this.progressInterval = this.setInterval(Fn.bind(this, function(){
|
|
// Don't trigger unless buffered amount is greater than last time
|
|
|
|
let numBufferedPercent = this.bufferedPercent();
|
|
|
|
if (this.bufferedPercent_ !== numBufferedPercent) {
|
|
this.trigger('progress');
|
|
}
|
|
|
|
this.bufferedPercent_ = numBufferedPercent;
|
|
|
|
if (numBufferedPercent === 1) {
|
|
this.stopTrackingProgress();
|
|
}
|
|
}), 500);
|
|
}
|
|
|
|
/**
|
|
* Update duration
|
|
*
|
|
* @method onDurationChange
|
|
*/
|
|
onDurationChange() {
|
|
this.duration_ = this.duration();
|
|
}
|
|
|
|
/**
|
|
* Create and get TimeRange object for buffering
|
|
*
|
|
* @return {TimeRangeObject}
|
|
* @method buffered
|
|
*/
|
|
buffered() {
|
|
return createTimeRange(0, 0);
|
|
}
|
|
|
|
/**
|
|
* Get buffered percent
|
|
*
|
|
* @return {Number}
|
|
* @method bufferedPercent
|
|
*/
|
|
bufferedPercent() {
|
|
return bufferedPercent(this.buffered(), this.duration_);
|
|
}
|
|
|
|
/**
|
|
* Stops tracking progress by clearing progress interval
|
|
*
|
|
* @method stopTrackingProgress
|
|
*/
|
|
stopTrackingProgress() {
|
|
this.clearInterval(this.progressInterval);
|
|
}
|
|
|
|
/*! Time Tracking -------------------------------------------------------------- */
|
|
/**
|
|
* Set event listeners for on play and pause and tracking current time
|
|
*
|
|
* @method manualTimeUpdatesOn
|
|
*/
|
|
manualTimeUpdatesOn() {
|
|
this.manualTimeUpdates = true;
|
|
|
|
this.on('play', this.trackCurrentTime);
|
|
this.on('pause', this.stopTrackingCurrentTime);
|
|
}
|
|
|
|
/**
|
|
* Remove event listeners for on play and pause and tracking current time
|
|
*
|
|
* @method manualTimeUpdatesOff
|
|
*/
|
|
manualTimeUpdatesOff() {
|
|
this.manualTimeUpdates = false;
|
|
this.stopTrackingCurrentTime();
|
|
this.off('play', this.trackCurrentTime);
|
|
this.off('pause', this.stopTrackingCurrentTime);
|
|
}
|
|
|
|
/**
|
|
* Tracks current time
|
|
*
|
|
* @method trackCurrentTime
|
|
*/
|
|
trackCurrentTime() {
|
|
if (this.currentTimeInterval) { this.stopTrackingCurrentTime(); }
|
|
this.currentTimeInterval = this.setInterval(function(){
|
|
this.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true });
|
|
}, 250); // 42 = 24 fps // 250 is what Webkit uses // FF uses 15
|
|
}
|
|
|
|
/**
|
|
* Turn off play progress tracking (when paused or dragging)
|
|
*
|
|
* @method stopTrackingCurrentTime
|
|
*/
|
|
stopTrackingCurrentTime() {
|
|
this.clearInterval(this.currentTimeInterval);
|
|
|
|
// #1002 - if the video ends right before the next timeupdate would happen,
|
|
// the progress bar won't make it all the way to the end
|
|
this.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true });
|
|
}
|
|
|
|
/**
|
|
* Turn off any manual progress or timeupdate tracking
|
|
*
|
|
* @method dispose
|
|
*/
|
|
dispose() {
|
|
// Turn off any manual progress or timeupdate tracking
|
|
if (this.manualProgress) { this.manualProgressOff(); }
|
|
|
|
if (this.manualTimeUpdates) { this.manualTimeUpdatesOff(); }
|
|
|
|
super.dispose();
|
|
}
|
|
|
|
/**
|
|
* Return the time ranges that have been played through for the
|
|
* current source. This implementation is incomplete. It does not
|
|
* track the played time ranges, only whether the source has played
|
|
* at all or not.
|
|
* @return {TimeRangeObject} a single time range if this video has
|
|
* played or an empty set of ranges if not.
|
|
* @method played
|
|
*/
|
|
played() {
|
|
if (this.hasStarted_) {
|
|
return createTimeRange(0, 0);
|
|
}
|
|
return createTimeRange();
|
|
}
|
|
|
|
/**
|
|
* Set current time
|
|
*
|
|
* @method setCurrentTime
|
|
*/
|
|
setCurrentTime() {
|
|
// improve the accuracy of manual timeupdates
|
|
if (this.manualTimeUpdates) { this.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true }); }
|
|
}
|
|
|
|
/**
|
|
* Initialize texttrack listeners
|
|
*
|
|
* @method initTextTrackListeners
|
|
*/
|
|
initTextTrackListeners() {
|
|
let textTrackListChanges = Fn.bind(this, function() {
|
|
this.trigger('texttrackchange');
|
|
});
|
|
|
|
let tracks = this.textTracks();
|
|
|
|
if (!tracks) return;
|
|
|
|
tracks.addEventListener('removetrack', textTrackListChanges);
|
|
tracks.addEventListener('addtrack', textTrackListChanges);
|
|
|
|
this.on('dispose', Fn.bind(this, function() {
|
|
tracks.removeEventListener('removetrack', textTrackListChanges);
|
|
tracks.removeEventListener('addtrack', textTrackListChanges);
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Emulate texttracks
|
|
*
|
|
* @method emulateTextTracks
|
|
*/
|
|
emulateTextTracks() {
|
|
if (!window['WebVTT'] && this.el().parentNode != null) {
|
|
let script = document.createElement('script');
|
|
script.src = this.options_['vtt.js'] || '../node_modules/vtt.js/dist/vtt.js';
|
|
this.el().parentNode.appendChild(script);
|
|
window['WebVTT'] = true;
|
|
}
|
|
|
|
let tracks = this.textTracks();
|
|
if (!tracks) {
|
|
return;
|
|
}
|
|
|
|
let textTracksChanges = Fn.bind(this, function() {
|
|
let updateDisplay = () => this.trigger('texttrackchange');
|
|
|
|
updateDisplay();
|
|
|
|
for (let i = 0; i < tracks.length; i++) {
|
|
let track = tracks[i];
|
|
track.removeEventListener('cuechange', updateDisplay);
|
|
if (track.mode === 'showing') {
|
|
track.addEventListener('cuechange', updateDisplay);
|
|
}
|
|
}
|
|
});
|
|
|
|
tracks.addEventListener('change', textTracksChanges);
|
|
|
|
this.on('dispose', function() {
|
|
tracks.removeEventListener('change', textTracksChanges);
|
|
});
|
|
}
|
|
|
|
/*
|
|
* Provide default methods for text tracks.
|
|
*
|
|
* Html5 tech overrides these.
|
|
*/
|
|
|
|
/**
|
|
* Get texttracks
|
|
*
|
|
* @returns {TextTrackList}
|
|
* @method textTracks
|
|
*/
|
|
textTracks() {
|
|
this.textTracks_ = this.textTracks_ || new TextTrackList();
|
|
return this.textTracks_;
|
|
}
|
|
|
|
/**
|
|
* Get remote texttracks
|
|
*
|
|
* @returns {TextTrackList}
|
|
* @method remoteTextTracks
|
|
*/
|
|
remoteTextTracks() {
|
|
this.remoteTextTracks_ = this.remoteTextTracks_ || new TextTrackList();
|
|
return this.remoteTextTracks_;
|
|
}
|
|
|
|
/**
|
|
* Creates and returns a remote text track object
|
|
*
|
|
* @param {String} kind Text track kind (subtitles, captions, descriptions
|
|
* chapters and metadata)
|
|
* @param {String=} label Label to identify the text track
|
|
* @param {String=} language Two letter language abbreviation
|
|
* @return {TextTrackObject}
|
|
* @method addTextTrack
|
|
*/
|
|
addTextTrack(kind, label, language) {
|
|
if (!kind) {
|
|
throw new Error('TextTrack kind is required but was not provided');
|
|
}
|
|
|
|
return createTrackHelper(this, kind, label, language);
|
|
}
|
|
|
|
/**
|
|
* Creates and returns a remote text track object
|
|
*
|
|
* @param {Object} options The object should contain values for
|
|
* kind, language, label and src (location of the WebVTT file)
|
|
* @return {TextTrackObject}
|
|
* @method addRemoteTextTrack
|
|
*/
|
|
addRemoteTextTrack(options) {
|
|
let track = createTrackHelper(this, options.kind, options.label, options.language, options);
|
|
this.remoteTextTracks().addTrack_(track);
|
|
return {
|
|
track: track
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Remove remote texttrack
|
|
*
|
|
* @param {TextTrackObject} track Texttrack to remove
|
|
* @method removeRemoteTextTrack
|
|
*/
|
|
removeRemoteTextTrack(track) {
|
|
this.textTracks().removeTrack_(track);
|
|
this.remoteTextTracks().removeTrack_(track);
|
|
}
|
|
|
|
/**
|
|
* Provide a default setPoster method for techs
|
|
* Poster support for techs should be optional, so we don't want techs to
|
|
* break if they don't have a way to set a poster.
|
|
*
|
|
* @method setPoster
|
|
*/
|
|
setPoster() {}
|
|
|
|
}
|
|
|
|
/*
|
|
* List of associated text tracks
|
|
*
|
|
* @type {Array}
|
|
* @private
|
|
*/
|
|
Tech.prototype.textTracks_;
|
|
|
|
var createTrackHelper = function(self, kind, label, language, options={}) {
|
|
let tracks = self.textTracks();
|
|
|
|
options.kind = kind;
|
|
|
|
if (label) {
|
|
options.label = label;
|
|
}
|
|
if (language) {
|
|
options.language = language;
|
|
}
|
|
options.tech = self;
|
|
|
|
let track = new TextTrack(options);
|
|
tracks.addTrack_(track);
|
|
|
|
return track;
|
|
};
|
|
|
|
Tech.prototype.featuresVolumeControl = true;
|
|
|
|
// Resizing plugins using request fullscreen reloads the plugin
|
|
Tech.prototype.featuresFullscreenResize = false;
|
|
Tech.prototype.featuresPlaybackRate = false;
|
|
|
|
// Optional events that we can manually mimic with timers
|
|
// currently not triggered by video-js-swf
|
|
Tech.prototype.featuresProgressEvents = false;
|
|
Tech.prototype.featuresTimeupdateEvents = false;
|
|
|
|
Tech.prototype.featuresNativeTextTracks = false;
|
|
|
|
/*
|
|
* A functional mixin for techs that want to use the Source Handler pattern.
|
|
*
|
|
* ##### EXAMPLE:
|
|
*
|
|
* Tech.withSourceHandlers.call(MyTech);
|
|
*
|
|
*/
|
|
Tech.withSourceHandlers = function(_Tech){
|
|
/*
|
|
* Register a source handler
|
|
* Source handlers are scripts for handling specific formats.
|
|
* The source handler pattern is used for adaptive formats (HLS, DASH) that
|
|
* manually load video data and feed it into a Source Buffer (Media Source Extensions)
|
|
* @param {Function} handler The source handler
|
|
* @param {Boolean} first Register it before any existing handlers
|
|
*/
|
|
_Tech.registerSourceHandler = function(handler, index){
|
|
let handlers = _Tech.sourceHandlers;
|
|
|
|
if (!handlers) {
|
|
handlers = _Tech.sourceHandlers = [];
|
|
}
|
|
|
|
if (index === undefined) {
|
|
// add to the end of the list
|
|
index = handlers.length;
|
|
}
|
|
|
|
handlers.splice(index, 0, handler);
|
|
};
|
|
|
|
/*
|
|
* Return the first source handler that supports the source
|
|
* TODO: Answer question: should 'probably' be prioritized over 'maybe'
|
|
* @param {Object} source The source object
|
|
* @returns {Object} The first source handler that supports the source
|
|
* @returns {null} Null if no source handler is found
|
|
*/
|
|
_Tech.selectSourceHandler = function(source){
|
|
let handlers = _Tech.sourceHandlers || [];
|
|
let can;
|
|
|
|
for (let i = 0; i < handlers.length; i++) {
|
|
can = handlers[i].canHandleSource(source);
|
|
|
|
if (can) {
|
|
return handlers[i];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
/*
|
|
* Check if the tech can support the given source
|
|
* @param {Object} srcObj The source object
|
|
* @return {String} 'probably', 'maybe', or '' (empty string)
|
|
*/
|
|
_Tech.canPlaySource = function(srcObj){
|
|
let sh = _Tech.selectSourceHandler(srcObj);
|
|
|
|
if (sh) {
|
|
return sh.canHandleSource(srcObj);
|
|
}
|
|
|
|
return '';
|
|
};
|
|
|
|
/*
|
|
* Create a function for setting the source using a source object
|
|
* and source handlers.
|
|
* Should never be called unless a source handler was found.
|
|
* @param {Object} source A source object with src and type keys
|
|
* @return {Tech} self
|
|
*/
|
|
_Tech.prototype.setSource = function(source){
|
|
let sh = _Tech.selectSourceHandler(source);
|
|
|
|
if (!sh) {
|
|
// Fall back to a native source hander when unsupported sources are
|
|
// deliberately set
|
|
if (_Tech.nativeSourceHandler) {
|
|
sh = _Tech.nativeSourceHandler;
|
|
} else {
|
|
log.error('No source hander found for the current source.');
|
|
}
|
|
}
|
|
|
|
// Dispose any existing source handler
|
|
this.disposeSourceHandler();
|
|
this.off('dispose', this.disposeSourceHandler);
|
|
|
|
this.currentSource_ = source;
|
|
this.sourceHandler_ = sh.handleSource(source, this);
|
|
this.on('dispose', this.disposeSourceHandler);
|
|
|
|
this.originalSeekable_ = this.seekable;
|
|
// when a source handler is registered, prefer its implementation of
|
|
// seekable when present.
|
|
this.seekable = function() {
|
|
if (this.sourceHandler_ && this.sourceHandler_.seekable) {
|
|
return this.sourceHandler_.seekable();
|
|
}
|
|
return this.originalSeekable_.call(this);
|
|
};
|
|
|
|
return this;
|
|
};
|
|
|
|
/*
|
|
* Clean up any existing source handler
|
|
*/
|
|
_Tech.prototype.disposeSourceHandler = function(){
|
|
if (this.sourceHandler_ && this.sourceHandler_.dispose) {
|
|
this.sourceHandler_.dispose();
|
|
this.seekable = this.originalSeekable_;
|
|
}
|
|
};
|
|
|
|
};
|
|
|
|
Component.registerComponent('Tech', Tech);
|
|
// Old name for Tech
|
|
Component.registerComponent('MediaTechController', Tech);
|
|
export default Tech;
|