1
0
mirror of https://github.com/videojs/video.js.git synced 2025-03-31 22:22:09 +02:00
video.js/src/js/tech/tech.js

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;