// Copyright 2014 Globo.com Player authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
import {uniqueId, currentScriptUrl} from '../base/utils'
import BaseObject from '../base/base_object'
import Events from '../base/events'
import Browser from './browser'
import CoreFactory from './core_factory'
import Loader from './loader'
import PlayerInfo from './player_info'
import $ from 'clappr-zepto'
const baseUrl = currentScriptUrl().replace(/\/[^\/]+$/, '')
/**
* @class Player
* @constructor
* @extends BaseObject
* @module components
* @example
* ### Using the Player
*
* Add the following script on your HTML:
* ```html
* <head>
* <script type="text/javascript" src="http://cdn.clappr.io/latest/clappr.min.js"></script>
* </head>
* ```
* Now, create the player:
* ```html
* <body>
* <div id="player"></div>
* <script>
* var player = new Clappr.Player({source: "http://your.video/here.mp4", parentId: "#player"});
* </script>
* </body>
* ```
*/
export default class Player extends BaseObject {
set loader(loader) { this._loader = loader }
get loader() {
if (!this._loader) {
this._loader = new Loader(this.options.plugins || {}, this.options.playerId)
}
return this._loader
}
/**
* Determine if the playback has ended.
* @property ended
* @type Boolean
*/
get ended() {
return this.core.mediaControl.container.ended
}
/**
* Determine if the playback is having to buffer in order for
* playback to be smooth.
* (i.e if a live stream is playing smoothly, this will be false)
* @property buffering
* @type Boolean
*/
get buffering() {
return this.core.mediaControl.container.buffering
}
/*
* determine if the player is ready.
* @property isReady
* @type {Boolean} `true` if the player is ready. ie PLAYER_READY event has fired
*/
get isReady() {
return !!this._ready
}
/**
* An events map that allows the user to add custom callbacks in player's options.
* @property eventsMapping
* @type {Object}
*/
get eventsMapping() {
return {
onReady: Events.PLAYER_READY,
onResize: Events.PLAYER_RESIZE,
onPlay: Events.PLAYER_PLAY,
onPause: Events.PLAYER_PAUSE,
onStop: Events.PLAYER_STOP,
onEnded: Events.PLAYER_ENDED,
onSeek: Events.PLAYER_SEEK,
onError: Events.PLAYER_ERROR,
onTimeUpdate: Events.PLAYER_TIMEUPDATE,
onVolumeUpdate: Events.PLAYER_VOLUMEUPDATE,
onTextTrackLoaded: Events.PLAYER_TEXTTRACKLOADED
}
}
/**
* ## Player's constructor
*
* You might pass the options object to build the player.
* ```javascript
* var options = {source: "http://example.com/video.mp4", param1: "val1"};
* var player = new Clappr.Player(options);
* ```
*
* @method constructor
* @param {Object} options Data
* options to build a player instance
* @param {Number} [options.width]
* player's width **default**: `640`
* @param {Number} [options.height]
* player's height **default**: `360`
* @param {String} [options.parentId]
* the id of the element on the page that the player should be inserted into
* @param {Object} [options.parent]
* a reference to a dom element that the player should be inserted into
* @param {String} [options.source]
* The media source URL, or {source: <<source URL>>, mimeType: <<source mime type>>}
* @param {Object} [options.sources]
* An array of media source URL's, or an array of {source: <<source URL>>, mimeType: <<source mime type>>}
* @param {Boolean} [options.autoPlay]
* automatically play after page load **default**: `false`
* @param {Boolean} [options.loop]
* automatically replay after it ends **default**: `false`
* @param {Boolean} [options.chromeless]
* player acts in chromeless mode **default**: `false`
* @param {Boolean} [options.allowUserInteraction]
* whether or not the player should handle click events when in chromeless mode **default**: `false` on desktops browsers, `true` on mobile.
* @param {Boolean} [options.disableKeyboardShortcuts]
* disable keyboard shortcuts. **default**: `false`. `true` if `allowUserInteraction` is `false`.
* @param {Boolean} [options.muted]
* start the video muted **default**: `false`
* @param {String} [options.mimeType]
* add `mimeType: "application/vnd.apple.mpegurl"` if you need to use a url without extension.
* @param {String} [options.actualLiveTime]
* show duration and seek time relative to actual time.
* @param {String} [options.actualLiveServerTime]
* specify server time as a string, format: "2015/11/26 06:01:03". This option is meant to be used with actualLiveTime.
* @param {Boolean} [options.persistConfig]
* persist player's settings (volume) through the same domain **default**: `true`
* @param {String} [options.preload]
* video will be preloaded according to `preload` attribute options **default**: `'metadata'`
* @param {Number} [options.maxBufferLength]
* the default behavior for the **HLS playback** is to keep buffering indefinitely, even on VoD.
* This replicates the behavior for progressive download, which continues buffering when pausing the video, thus making the video available for playback even on slow networks.
* To change this behavior use `maxBufferLength` where **value is in seconds**.
* @param {String} [options.gaAccount]
* enable Google Analytics events dispatch **(play/pause/stop/buffering/etc)** by adding your `gaAccount`
* @param {String} [options.gaTrackerName]
* besides `gaAccount` you can optionally, pass your favorite trackerName as `gaTrackerName`
* @param {Object} [options.mediacontrol]
* customize control bar colors, example: `mediacontrol: {seekbar: "#E113D3", buttons: "#66B2FF"}`
* @param {Boolean} [options.hideMediaControl]
* control media control auto hide **default**: `true`
* @param {Boolean} [options.hideVolumeBar]
* when embedded with width less than 320, volume bar will hide. You can force this behavior for all sizes by adding `true` **default**: `false`
* @param {String} [options.watermark]
* put `watermark: 'http://url/img.png'` on your embed parameters to automatically add watermark on your video.
* You can customize corner position by defining position parameter. Positions can be `bottom-left`, `bottom-right`, `top-left` and `top-right`.
* @param {String} [options.watermarkLink]
* `watermarkLink: 'http://example.net/'` - define URL to open when the watermark is clicked. If not provided watermark will not be clickable.
* @param {Boolean} [options.disableVideoTagContextMenu]
* disables the context menu (right click) on the video element if a HTML5Video playback is used.
* @param {Boolean} [options.autoSeekFromUrl]
* Automatically seek to the seconds provided in the url (e.g example.com?t=100) **default**: `true`
* @param {Boolean} [options.exitFullscreenOnEnd]
* Automatically exit full screen when the media finishes. **default**: `true`
* @param {String} [options.poster]
* define a poster by adding its address `poster: 'http://url/img.png'`. It will appear after video embed, disappear on play and go back when user stops the video.
* @param {String} [options.playbackNotSupportedMessage]
* define a custom message to be displayed when a playback is not supported.
* @param {Object} [options.events]
* Specify listeners which will be registered with their corresponding player events.
* E.g. onReady -> "PLAYER_READY", onTimeUpdate -> "PLAYER_TIMEUPDATE"
*/
constructor(options) {
super(options)
const defaultOptions = {playerId: uniqueId(''), persistConfig: true, width: 640, height: 360, baseUrl: baseUrl, allowUserInteraction: Browser.isMobile}
this._options = $.extend(defaultOptions, options)
this.options.sources = this._normalizeSources(options)
if (!this.options.chromeless) {
// "allowUserInteraction" cannot be false if not in chromeless mode.
this.options.allowUserInteraction = true
}
if (!this.options.allowUserInteraction) {
// if user iteraction is not allowed ensure keyboard shortcuts are disabled
this.options.disableKeyboardShortcuts = true
}
this._registerOptionEventListeners()
this._coreFactory = new CoreFactory(this)
this.playerInfo = PlayerInfo.getInstance(this.options.playerId)
this.playerInfo.currentSize = {width: options.width, height: options.height}
this.playerInfo.options = this.options
if (this.options.parentId) {
this.setParentId(this.options.parentId)
}
else if (this.options.parent) {
this.attachTo(this.options.parent)
}
}
/**
* Specify a `parentId` to the player.
* @method setParentId
* @param {String} parentId the element parent id.
* @return {Player} itself
*/
setParentId(parentId) {
const el = document.querySelector(parentId)
if (el) {
this.attachTo(el)
}
return this
}
/**
* You can use this method to attach the player to a given element. You don't need to do this when you specify it during the player instantiation passing the `parentId` param.
* @method attachTo
* @param {Object} element a given element.
* @return {Player} itself
*/
attachTo(element) {
this.options.parentElement = element
this.core = this._coreFactory.create()
this._addEventListeners()
return this
}
_addEventListeners() {
if (!this.core.isReady) {
this.listenToOnce(this.core, Events.CORE_READY, this._onReady)
} else {
this._onReady()
}
this.listenTo(this.core.mediaControl, Events.MEDIACONTROL_CONTAINERCHANGED, this._containerChanged)
this.listenTo(this.core, Events.CORE_FULLSCREEN, this._onFullscreenChange)
return this
}
_addContainerEventListeners() {
const container = this.core.mediaControl.container
if (container) {
this.listenTo(container, Events.CONTAINER_PLAY, this._onPlay)
this.listenTo(container, Events.CONTAINER_PAUSE, this._onPause)
this.listenTo(container, Events.CONTAINER_STOP, this._onStop)
this.listenTo(container, Events.CONTAINER_ENDED, this._onEnded)
this.listenTo(container, Events.CONTAINER_SEEK, this._onSeek)
this.listenTo(container, Events.CONTAINER_ERROR, this._onError)
this.listenTo(container, Events.CONTAINER_TIMEUPDATE, this._onTimeUpdate)
this.listenTo(container, Events.CONTAINER_VOLUME, this._onVolumeUpdate)
this.listenTo(container, Events.CONTAINER_LOADEDTEXTTRACK, this._onTextTrackLoaded)
}
return this
}
_registerOptionEventListeners() {
const userEvents = this.options.events || {}
Object.keys(userEvents).forEach((userEvent) => {
const eventType = this.eventsMapping[userEvent]
if (eventType) {
let eventFunction = userEvents[userEvent]
eventFunction = typeof eventFunction === 'function' && eventFunction
eventFunction && this.on(eventType, eventFunction)
}
})
return this
}
_containerChanged() {
this.stopListening()
this._addEventListeners()
}
_onReady() {
this._ready = true
this._addContainerEventListeners()
this.trigger(Events.PLAYER_READY)
}
_onFullscreenChange(fullscreen) {
this.trigger(Events.PLAYER_FULLSCREEN, fullscreen)
}
_onVolumeUpdate(volume) {
this.trigger(Events.PLAYER_VOLUMEUPDATE, volume)
}
_onTextTrackLoaded(evt, data) {
this.trigger(Events.PLAYER_TEXTTRACKLOADED, evt, data)
}
_onPlay() {
this.trigger(Events.PLAYER_PLAY)
}
_onPause() {
this.trigger(Events.PLAYER_PAUSE)
}
_onStop() {
this.trigger(Events.PLAYER_STOP, this.getCurrentTime())
}
_onEnded() {
this.trigger(Events.PLAYER_ENDED)
}
_onSeek(time) {
this.trigger(Events.PLAYER_SEEK, time)
}
_onTimeUpdate(timeProgress) {
this.trigger(Events.PLAYER_TIMEUPDATE, timeProgress)
}
_onError(error) {
this.trigger(Events.PLAYER_ERROR, error)
}
_normalizeSources(options) {
const sources = options.sources || (options.source !== undefined? [options.source] : [])
return sources.length === 0 ? [{source:'', mimeType:''}] : sources
}
/**
* resizes the current player canvas.
* @method resize
* @param {Object} size should be a literal object with `height` and `width`.
* @return {Player} itself
* @example
* ```javascript
* player.resize({height: 360, width: 640})
* ```
*/
resize(size) {
this.core.resize(size)
return this
}
/**
* loads a new source.
* @method load
* @param {Array|String} sources source or sources of video.
* An array item can be a string or {source: <<source URL>>, mimeType: <<source mime type>>}
* @param {String} mimeType a mime type, example: `'application/vnd.apple.mpegurl'`
* @param {Boolean} [autoPlay=false] whether playing should be started immediately
* @return {Player} itself
*/
load(sources, mimeType, autoPlay) {
if (autoPlay !== undefined) {
this.configure({autoPlay: !!autoPlay})
}
this.core.load(sources, mimeType)
return this
}
/**
* destroys the current player and removes it from the DOM.
* @method destroy
* @return {Player} itself
*/
destroy() {
this.core.destroy()
return this
}
/**
* Gives user consent to playback. Required by mobile device after a click event before Player.load().
* @method consent
* @return {Player} itself
*/
consent() {
this.core.getCurrentPlayback().consent()
return this
}
/**
* plays the current video (`source`).
* @method play
* @return {Player} itself
*/
play() {
this.core.mediaControl.container.play()
return this
}
/**
* pauses the current video (`source`).
* @method pause
* @return {Player} itself
*/
pause() {
this.core.mediaControl.container.pause()
return this
}
/**
* stops the current video (`source`).
* @method stop
* @return {Player} itself
*/
stop() {
this.core.mediaControl.container.stop()
return this
}
/**
* seeks the current video (`source`). For example, `player.seek(120)` will seek to second 120 (2minutes) of the current video.
* @method seek
* @param {Number} time should be a number between 0 and the video duration.
* @return {Player} itself
*/
seek(time) {
this.core.mediaControl.container.seek(time)
return this
}
/**
* seeks the current video (`source`). For example, `player.seek(50)` will seek to the middle of the current video.
* @method seekPercentage
* @param {Number} time should be a number between 0 and 100.
* @return {Player} itself
*/
seekPercentage(percentage) {
this.core.mediaControl.container.seekPercentage(percentage)
return this
}
/**
* Set the volume for the current video (`source`).
* @method setVolume
* @param {Number} volume should be a number between 0 and 100, 0 being mute and 100 the max volume.
* @return {Player} itself
*/
setVolume(volume) {
if (this.core && this.core.mediaControl) {
this.core.mediaControl.setVolume(volume)
}
return this
}
/**
* Get the volume for the current video
* @method getVolume
* @return {Number} volume should be a number between 0 and 100, 0 being mute and 100 the max volume.
*/
getVolume() {
return this.core && this.core.mediaControl ? this.core.mediaControl.volume : 0
}
/**
* mutes the current video (`source`).
* @method mute
* @return {Player} itself
*/
mute() {
this._mutedVolume = this.getVolume()
this.setVolume(0)
return this
}
/**
* unmutes the current video (`source`).
* @method unmute
* @return {Player} itself
*/
unmute() {
this.setVolume(typeof this._mutedVolume === 'number' ? this._mutedVolume : 100)
this._mutedVolume = null
return this
}
/**
* checks if the player is playing.
* @method isPlaying
* @return {Boolean} `true` if the current source is playing, otherwise `false`
*/
isPlaying() {
return this.core.mediaControl.container.isPlaying()
}
/**
* returns `true` if DVR is enable otherwise `false`.
* @method isDvrEnabled
* @return {Boolean}
*/
isDvrEnabled() {
return this.core.mediaControl.container.isDvrEnabled()
}
/**
* returns `true` if DVR is in use otherwise `false`.
* @method isDvrInUse
* @return {Boolean}
*/
isDvrInUse() {
return this.core.mediaControl.container.isDvrInUse()
}
/**
* enables to configure a player after its creation
* @method configure
* @param {Object} options all the options to change in form of a javascript object
* @return {Player} itself
*/
configure(options) {
this.core.configure(options)
return this
}
/**
* get a plugin by its name.
* @method getPlugin
* @param {String} name of the plugin.
* @return {Object} the plugin instance
* @example
* ```javascript
* var poster = player.getPlugin('poster');
* poster.hidePlayButton();
* ```
*/
getPlugin(name) {
const plugins = this.core.plugins.concat(this.core.mediaControl.container.plugins)
return plugins.filter(plugin => plugin.name === name)[0]
}
/**
* the current time in seconds.
* @method getCurrentTime
* @return {Number} current time (in seconds) of the current source
*/
getCurrentTime() {
return this.core.mediaControl.container.getCurrentTime()
}
/**
* The time that "0" now represents relative to when playback started.
* For a stream with a sliding window this will increase as content is
* removed from the beginning.
* @method getStartTimeOffset
* @return {Number} time (in seconds) that time "0" represents.
*/
getStartTimeOffset() {
return this.core.mediaControl.container.getStartTimeOffset()
}
/**
* the duration time in seconds.
* @method getDuration
* @return {Number} duration time (in seconds) of the current source
*/
getDuration() {
return this.core.mediaControl.container.getDuration()
}
}