// 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.
var Playback = require('../../base/playback')
var JST = require('../../base/jst')
var Styler = require('../../base/styler')
var Browser = require('../../components/browser')
var seekStringToSeconds = require('../../base/utils').seekStringToSeconds
var Events = require('../../base/events')
var find = require('lodash.find')
/**
* @class HTML5Video
* @constructor
* @extends Playback
* @module playback
* @example
* var video = new HTML5Video({src: 'http://example.com/example.mpd'})
* //starts to play the video
* video.play()
*/
class HTML5Video extends Playback {
/**
* component name
* @property name
* @type {String}
* @readOnly
*/
get name() { return 'html5_video' }
/**
* tag name
* @property tagName
* @type {String}
*/
get tagName() { return 'video' }
/**
* template for the component
* @property template
* @type {Object}
*/
get template() { return JST.html5_video }
/**
* data attributes (k,v) for the component
* @property attributes
* @type {Object}
*/
get attributes() {
return {
'data-html5-video': ''
}
}
/**
* events binding
* @property events
* @type {Object}
*/
get events() {
return {
'timeupdate': 'timeUpdated',
'progress': 'progress',
'ended': 'ended',
'stalled': 'stalled',
'waiting': 'waiting',
'canplaythrough': 'bufferFull',
'loadedmetadata': 'loadedMetadata',
'canplay': 'ready',
'durationchange': 'durationChange',
'error': 'error',
'playing': 'playing',
'pause': 'paused'
}
}
/**
* constructor.
*
* @method constructor
* @param {Object} general options
*/
constructor(options) {
super(options)
this.options = options
this.src = options.src
this.el.src = options.src
this.el.loop = options.loop
this.firstBuffer = true
this.settings = {default: ['seekbar']}
if (Browser.isSafari) {
this.setupSafari()
} else {
this.el.preload = options.preload ? options.preload: 'metadata'
this.settings.seekEnabled = true
}
this.settings.left = ["playpause", "position", "duration"]
this.settings.right = ["fullscreen", "volume"]
}
/**
* Safari needs to be initialized with `preload` set to 'auto'
*
* @method setupSafari
* @private
*/
setupSafari() {
this.el.preload = 'auto'
}
/**
* This is called when loadedmetadata is fired by tag video.
*
* @method loadedMetadata
* @param {Object} e An event handler
* @return {Boolean} Returns true on success
*/
loadedMetadata(e) {
this.durationChange()
this.trigger(Events.PLAYBACK_LOADEDMETADATA, e.target.duration)
this.checkInitialSeek()
}
durationChange() {
// we can't figure out if hls resource is VoD or not until it is being loaded or duration has changed.
// that's why we check it again and update media control accordingly.
if (this.getPlaybackType() === 'vod') {
this.settings.left = ["playpause", "position", "duration"]
} else {
this.settings.left = ["playstop"]
}
this.settings.seekEnabled = isFinite(this.getDuration())
this.trigger(Events.PLAYBACK_SETTINGSUPDATE)
}
getPlaybackType() {
return [0, undefined, Infinity].indexOf(this.el.duration) >= 0 ? 'live' : 'vod'
}
isHighDefinitionInUse() {
return false
}
play() {
this.el.play()
}
pause() {
this.el.pause()
}
stop() {
this.pause()
if (this.el.readyState !== 0) {
this.el.currentTime = 0
}
}
volume(value) {
this.el.volume = value / 100
}
mute() {
this.el.volume = 0
}
unmute() {
this.el.volume = 1
}
isMuted() {
return !!this.el.volume
}
isPlaying() {
return !this.el.paused && !this.el.ended
}
playing() {
this.trigger(Events.PLAYBACK_PLAY);
}
paused() {
this.trigger(Events.PLAYBACK_PAUSE);
}
ended() {
this.trigger(Events.PLAYBACK_BUFFERFULL, this.name)
this.trigger(Events.PLAYBACK_ENDED, this.name)
this.trigger(Events.PLAYBACK_TIMEUPDATE, 0, this.el.duration, this.name)
}
stalled() {
if (this.getPlaybackType() === 'vod' && this.el.readyState < this.el.HAVE_FUTURE_DATA) {
this.trigger(Events.PLAYBACK_BUFFERING, this.name)
}
}
waiting() {
if(this.el.readyState < this.el.HAVE_FUTURE_DATA) {
this.trigger(Events.PLAYBACK_BUFFERING, this.name)
}
}
bufferFull() {
if (this.options.poster && this.firstBuffer) {
this.firstBuffer = false
if (!this.isPlaying()) {
this.el.poster = this.options.poster
}
} else {
this.el.poster = ''
}
this.trigger(Events.PLAYBACK_BUFFERFULL, this.name)
}
error(event) {
this.trigger(Events.PLAYBACK_ERROR, this.el.error, this.name)
}
destroy() {
this.stop()
this.el.src = ''
this.$el.remove()
}
seek(seekBarValue) {
var time = this.el.duration * (seekBarValue / 100)
this.seekSeconds(time)
}
seekSeconds(time) {
this.el.currentTime = time
}
checkInitialSeek() {
var seekTime = seekStringToSeconds(window.location.href)
this.seekSeconds(seekTime)
}
getCurrentTime() {
return this.el.currentTime
}
getDuration() {
return this.el.duration
}
timeUpdated() {
if (this.getPlaybackType() === 'live') {
this.trigger(Events.PLAYBACK_TIMEUPDATE, 1, 1, this.name)
} else {
this.trigger(Events.PLAYBACK_TIMEUPDATE, this.el.currentTime, this.el.duration, this.name)
}
}
progress() {
if (!this.el.buffered.length) return
var bufferedPos = 0
for (var i = 0; i < this.el.buffered.length; i++) {
if (this.el.currentTime >= this.el.buffered.start(i) && this.el.currentTime <= this.el.buffered.end(i)) {
bufferedPos = i
break
}
}
this.checkBufferState(this.el.buffered.end(bufferedPos))
this.trigger(Events.PLAYBACK_PROGRESS, this.el.buffered.start(bufferedPos), this.el.buffered.end(bufferedPos), this.el.duration, this.name)
}
checkBufferState(bufferedPos) {
var playbackPos = this.el.currentTime + 0.05; // 50 ms threshold
if (this.isPlaying() && playbackPos >= bufferedPos) {
this.trigger(Events.PLAYBACK_BUFFERING, this.name)
this.buffering = true
} else if (this.buffering) {
this.trigger(Events.PLAYBACK_BUFFERFULL, this.name)
this.buffering = false
}
}
typeFor(src) {
return (src.indexOf('.m3u8') > 0) ? 'application/vnd.apple.mpegurl' : 'video/mp4'
}
ready() {
this.trigger(Events.PLAYBACK_READY, this.name)
}
render() {
var style = Styler.getStyleFor(this.name)
this.$el.html(this.template({ src: this.src, type: this.typeFor(this.src) }))
if (this.options.useVideoTagDefaultControls) {
this.$el.attr('controls', 'controls')
}
this.$el.append(style)
process.nextTick(() => this.options.autoPlay && this.play())
if (this.el.readyState === this.el.HAVE_ENOUGH_DATA) {
this.ready()
}
return this
}
}
HTML5Video.canPlay = function(resource, mimeType) {
var mimetypes = {
'mp4': ["avc1.42E01E", "avc1.58A01E", "avc1.4D401E", "avc1.64001E", "mp4v.20.8", "mp4v.20.240", "mp4a.40.2"].map(
(codec) => { return 'video/mp4; codecs="' + codec + ', mp4a.40.2"'}),
'ogg': ['video/ogg; codecs="theora, vorbis"', 'video/ogg; codecs="dirac"', 'video/ogg; codecs="theora, speex"'],
'3gpp': ['video/3gpp; codecs="mp4v.20.8, samr"'],
'webm': ['video/webm; codecs="vp8, vorbis"'],
'mkv': ['video/x-matroska; codecs="theora, vorbis"'],
'm3u8': ['application/x-mpegURL']
}
mimetypes['ogv'] = mimetypes['ogg']
mimetypes['3gp'] = mimetypes['3gpp']
var resourceParts = resource.split('?')[0].match(/.*\.(.*)$/) || []
if ((resourceParts.length > 1) && (mimetypes[resourceParts[1]] !== undefined)) {
var v = document.createElement('video')
return !!find(mimetypes[resourceParts[1]], (ext) => { return !!v.canPlayType(ext).replace(/no/, '') })
} else if (mimeType) {
var v = document.createElement('video')
return !!v.canPlayType(mimeType).replace(/no/, '')
}
return false
}
module.exports = HTML5Video