UNPKG

@l5i/dashjs

Version:

A reference client implementation for the playback of MPEG DASH via Javascript and compliant browsers.

457 lines (390 loc) 15.4 kB
/** * The copyright in this software is being made available under the BSD License, * included below. This software may be subject to other third party and contributor * rights, including patent rights, and no such rights are granted under this license. * * Copyright (c) 2013, Dash Industry Forum. * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation and/or * other materials provided with the distribution. * * Neither the name of Dash Industry Forum nor the names of its * contributors may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ import FactoryMaker from '../../core/FactoryMaker'; import EventBus from '../../core/EventBus'; import Events from '../../core/events/Events'; import Debug from '../../core/Debug'; function VideoModel() { let instance, logger, element, TTMLRenderingDiv, videoContainer, previousPlaybackRate; const VIDEO_MODEL_WRONG_ELEMENT_TYPE = 'element is not video or audio DOM type!'; const context = this.context; const eventBus = EventBus(context).getInstance(); const stalledStreams = []; function setup() { logger = Debug(context).getInstance().getLogger(instance); } function initialize() { eventBus.on(Events.PLAYBACK_PLAYING, onPlaying, this); } function reset() { eventBus.off(Events.PLAYBACK_PLAYING, onPlaying, this); } function onPlaybackCanPlay() { if (element) { element.playbackRate = previousPlaybackRate || 1; element.removeEventListener('canplay', onPlaybackCanPlay); } } function setPlaybackRate(value) { if (!element) return; if (element.readyState <= 2 && value > 0) { // If media element hasn't loaded enough data to play yet, wait until it has element.addEventListener('canplay', onPlaybackCanPlay); } else { element.playbackRate = value; } } //TODO Move the DVR window calculations from MediaPlayer to Here. function setCurrentTime(currentTime, stickToBuffered) { if (element) { //_currentTime = currentTime; // We don't set the same currentTime because it can cause firing unexpected Pause event in IE11 // providing playbackRate property equals to zero. if (element.currentTime == currentTime) return; // TODO Despite the fact that MediaSource 'open' event has been fired IE11 cannot set videoElement.currentTime // immediately (it throws InvalidStateError). It seems that this is related to videoElement.readyState property // Initially it is 0, but soon after 'open' event it goes to 1 and setting currentTime is allowed. Chrome allows to // set currentTime even if readyState = 0. // setTimeout is used to workaround InvalidStateError in IE11 try { currentTime = stickToBuffered ? stickTimeToBuffered(currentTime) : currentTime; element.currentTime = currentTime; } catch (e) { if (element.readyState === 0 && e.code === e.INVALID_STATE_ERR) { setTimeout(function () { element.currentTime = currentTime; }, 400); } } } } function stickTimeToBuffered(time) { const buffered = getBufferRange(); let closestTime = time; let closestDistance = 9999999999; if (buffered) { for (let i = 0; i < buffered.length; i++) { const start = buffered.start(i); const end = buffered.end(i); const distanceToStart = Math.abs(start - time); const distanceToEnd = Math.abs(end - time); if (time >= start && time <= end) { return time; } if (distanceToStart < closestDistance) { closestDistance = distanceToStart; closestTime = start; } if (distanceToEnd < closestDistance) { closestDistance = distanceToEnd; closestTime = end; } } } return closestTime; } function getElement() { return element; } function setElement(value) { //add check of value type if (value === null || value === undefined || (value && (/^(VIDEO|AUDIO)$/i).test(value.nodeName))) { element = value; // Workaround to force Firefox to fire the canplay event. if (element) { element.preload = 'auto'; } } else { throw VIDEO_MODEL_WRONG_ELEMENT_TYPE; } } function setSource(source) { if (element) { if (source) { element.src = source; } else { element.removeAttribute('src'); element.load(); } } } function getSource() { return element ? element.src : null; } function getVideoContainer() { return videoContainer; } function setVideoContainer(value) { videoContainer = value; } function getTTMLRenderingDiv() { return TTMLRenderingDiv; } function setTTMLRenderingDiv(div) { TTMLRenderingDiv = div; // The styling will allow the captions to match the video window size and position. TTMLRenderingDiv.style.position = 'absolute'; TTMLRenderingDiv.style.display = 'flex'; TTMLRenderingDiv.style.overflow = 'hidden'; TTMLRenderingDiv.style.pointerEvents = 'none'; TTMLRenderingDiv.style.top = 0; TTMLRenderingDiv.style.left = 0; } function setStallState(type, state) { stallStream(type, state); } function isStalled() { return (stalledStreams.length > 0); } function addStalledStream(type) { let event; if (type === null || element.seeking || stalledStreams.indexOf(type) !== -1) { return; } stalledStreams.push(type); if (element && stalledStreams.length === 1) { // Halt playback until nothing is stalled. event = document.createEvent('Event'); event.initEvent('waiting', true, false); previousPlaybackRate = element.playbackRate; setPlaybackRate(0); element.dispatchEvent(event); } } function removeStalledStream(type) { let index = stalledStreams.indexOf(type); let event; if (type === null) { return; } if (index !== -1) { stalledStreams.splice(index, 1); } // If nothing is stalled resume playback. if (element && isStalled() === false && element.playbackRate === 0) { setPlaybackRate(previousPlaybackRate || 1); if (!element.paused) { event = document.createEvent('Event'); event.initEvent('playing', true, false); element.dispatchEvent(event); } } } function stallStream(type, isStalled) { if (isStalled) { addStalledStream(type); } else { removeStalledStream(type); } } //Calling play on the element will emit playing - even if the stream is stalled. If the stream is stalled, emit a waiting event. function onPlaying() { if (element && isStalled() && element.playbackRate === 0) { const event = document.createEvent('Event'); event.initEvent('waiting', true, false); element.dispatchEvent(event); } } function getPlaybackQuality() { if (!element) { return null; } let hasWebKit = ('webkitDroppedFrameCount' in element) && ('webkitDecodedFrameCount' in element); let hasQuality = ('getVideoPlaybackQuality' in element); let result = null; if (hasQuality) { result = element.getVideoPlaybackQuality(); } else if (hasWebKit) { result = { droppedVideoFrames: element.webkitDroppedFrameCount, totalVideoFrames: element.webkitDroppedFrameCount + element.webkitDecodedFrameCount, creationTime: new Date() }; } return result; } function play() { if (element) { element.autoplay = true; const p = element.play(); if (p && (typeof Promise !== 'undefined') && (p instanceof Promise)) { p.catch((e) => { if (e.name === 'NotAllowedError') { eventBus.trigger(Events.PLAYBACK_NOT_ALLOWED); } logger.warn(`Caught pending play exception - continuing (${e})`); }); } } } function isPaused() { return element ? element.paused : null; } function pause() { if (element) { element.pause(); element.autoplay = false; } } function isSeeking() { return element ? element.seeking : null; } function getTime() { return element ? element.currentTime : null; } function getPlaybackRate() { return element ? element.playbackRate : null; } function getPlayedRanges() { return element ? element.played : null; } function getEnded() { return element ? element.ended : null; } function addEventListener(eventName, eventCallBack) { if (element) { element.addEventListener(eventName, eventCallBack); } } function removeEventListener(eventName, eventCallBack) { if (element) { element.removeEventListener(eventName, eventCallBack); } } function getReadyState() { return element ? element.readyState : NaN; } function getBufferRange() { return element ? element.buffered : null; } function getClientWidth() { return element ? element.clientWidth : NaN; } function getClientHeight() { return element ? element.clientHeight : NaN; } function getVideoWidth() { return element ? element.videoWidth : NaN; } function getVideoHeight() { return element ? element.videoHeight : NaN; } function getVideoRelativeOffsetTop() { return element && element.parentNode ? element.getBoundingClientRect().top - element.parentNode.getBoundingClientRect().top : NaN; } function getVideoRelativeOffsetLeft() { return element && element.parentNode ? element.getBoundingClientRect().left - element.parentNode.getBoundingClientRect().left : NaN; } function getTextTracks() { return element ? element.textTracks : []; } function getTextTrack(kind, label, lang, isTTML, isEmbedded) { if (element) { for (var i = 0; i < element.textTracks.length; i++) { //label parameter could be a number (due to adaptationSet), but label, the attribute of textTrack, is a string => to modify... //label could also be undefined (due to adaptationSet) if (element.textTracks[i].kind === kind && (label ? element.textTracks[i].label == label : true) && element.textTracks[i].language === lang && element.textTracks[i].isTTML === isTTML && element.textTracks[i].isEmbedded === isEmbedded) { return element.textTracks[i]; } } } return null; } function addTextTrack(kind, label, lang) { if (element) { return element.addTextTrack(kind, label, lang); } return null; } function appendChild(childElement) { if (element) { element.appendChild(childElement); //in Chrome, we need to differenciate textTrack with same lang, kind and label but different format (vtt, ttml, etc...) if (childElement.isTTML !== undefined) { element.textTracks[element.textTracks.length - 1].isTTML = childElement.isTTML; element.textTracks[element.textTracks.length - 1].isEmbedded = childElement.isEmbedded; } } } function removeChild(childElement) { if (element) { element.removeChild(childElement); } } instance = { initialize: initialize, setCurrentTime: setCurrentTime, play: play, isPaused: isPaused, pause: pause, isSeeking: isSeeking, getTime: getTime, getPlaybackRate: getPlaybackRate, setPlaybackRate: setPlaybackRate, getPlayedRanges: getPlayedRanges, getEnded: getEnded, setStallState: setStallState, getElement: getElement, setElement: setElement, setSource: setSource, getSource: getSource, getVideoContainer: getVideoContainer, setVideoContainer: setVideoContainer, getTTMLRenderingDiv: getTTMLRenderingDiv, setTTMLRenderingDiv: setTTMLRenderingDiv, getPlaybackQuality: getPlaybackQuality, addEventListener: addEventListener, removeEventListener: removeEventListener, getReadyState: getReadyState, getBufferRange: getBufferRange, getClientWidth: getClientWidth, getClientHeight: getClientHeight, getTextTracks: getTextTracks, getTextTrack: getTextTrack, addTextTrack: addTextTrack, appendChild: appendChild, removeChild: removeChild, getVideoWidth: getVideoWidth, getVideoHeight: getVideoHeight, getVideoRelativeOffsetTop: getVideoRelativeOffsetTop, getVideoRelativeOffsetLeft: getVideoRelativeOffsetLeft, reset: reset }; setup(); return instance; } VideoModel.__dashjs_factory_name = 'VideoModel'; export default FactoryMaker.getSingletonFactory(VideoModel);