2020-08-16 20:24:45 +02:00
import browser from '../../scripts/browser' ;
2020-09-08 02:05:02 -04:00
import { ConnectionManager , Events } from 'jellyfin-apiclient' ;
2020-08-16 20:24:45 +02:00
import { appHost } from '../../components/apphost' ;
2020-08-14 08:46:34 +02:00
import loading from '../../components/loading/loading' ;
2020-08-16 20:24:45 +02:00
import dom from '../../scripts/dom' ;
import { playbackManager } from '../../components/playback/playbackmanager' ;
import { appRouter } from '../../components/appRouter' ;
2020-07-26 14:18:34 +02:00
import {
bindEventsToHlsPlayer ,
destroyHlsPlayer ,
destroyFlvPlayer ,
destroyCastPlayer ,
getCrossOriginValue ,
enableHlsJsPlayer ,
applySrc ,
playWithPromise ,
onEndedInternal ,
saveVolume ,
seekOnPlaybackStart ,
onErrorInternal ,
handleHlsJsMediaError ,
getSavedVolume ,
isValidDuration ,
getBufferedRanges
2020-08-16 20:24:45 +02:00
} from '../../components/htmlMediaHelper' ;
import itemHelper from '../../components/itemHelper' ;
import Screenfull from 'screenfull' ;
import globalize from '../../scripts/globalize' ;
2020-07-19 16:15:11 +02:00
2020-04-15 12:27:04 +02:00
/* eslint-disable indent */
2018-10-23 01:05:09 +03:00
2020-04-15 12:27:04 +02:00
function tryRemoveElement ( elem ) {
const parentNode = elem . parentNode ;
2019-01-10 15:39:37 +03:00
if ( parentNode ) {
// Seeing crashes in edge webview
try {
parentNode . removeChild ( elem );
} catch ( err ) {
2020-07-22 22:59:27 +02:00
console . error ( `error removing dialog element: ${ err } ` );
2019-01-10 15:39:37 +03:00
}
2018-10-23 01:05:09 +03:00
}
}
function enableNativeTrackSupport ( currentSrc , track ) {
if ( track ) {
2019-01-10 15:39:37 +03:00
if ( track . DeliveryMethod === 'Embed' ) {
return true ;
}
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
if ( browser . firefox ) {
2020-04-19 23:23:46 +02:00
if (( currentSrc || '' ). toLowerCase (). includes ( '.m3u8' )) {
2019-01-10 15:39:37 +03:00
return false ;
}
}
if ( browser . ps4 ) {
return false ;
}
if ( browser . web0s ) {
return false ;
}
// Edge is randomly not rendering subtitles
if ( browser . edge ) {
return false ;
}
if ( browser . iOS ) {
// works in the browser but not the native app
if (( browser . iosVersion || 10 ) < 10 ) {
return false ;
}
}
if ( track ) {
2020-07-21 22:22:16 +02:00
const format = ( track . Codec || '' ). toLowerCase ();
2019-01-10 15:39:37 +03:00
if ( format === 'ssa' || format === 'ass' ) {
return false ;
}
}
return true ;
2018-10-23 01:05:09 +03:00
}
function requireHlsPlayer ( callback ) {
2020-08-16 20:24:45 +02:00
import ( 'hls.js' ). then (({ default : hls }) => {
2019-01-10 15:39:37 +03:00
window . Hls = hls ;
callback ();
});
2018-10-23 01:05:09 +03:00
}
function getMediaStreamAudioTracks ( mediaSource ) {
2019-01-10 15:39:37 +03:00
return mediaSource . MediaStreams . filter ( function ( s ) {
return s . Type === 'Audio' ;
});
2018-10-23 01:05:09 +03:00
}
function getMediaStreamTextTracks ( mediaSource ) {
2019-01-10 15:39:37 +03:00
return mediaSource . MediaStreams . filter ( function ( s ) {
return s . Type === 'Subtitle' ;
});
2018-10-23 01:05:09 +03:00
}
2020-05-10 01:08:08 +02:00
function hidePrePlaybackPage () {
2020-07-16 22:19:09 +02:00
const animatedPage = document . querySelector ( '.page:not(.hide)' );
2020-05-10 01:08:08 +02:00
animatedPage . classList . add ( 'hide' );
2020-05-14 14:18:50 +02:00
// At this point, we must hide the scrollbar placeholder, so it's not being displayed while the item is being loaded
2020-05-17 00:11:37 +02:00
document . body . classList . remove ( 'force-scroll' );
2020-05-10 01:08:08 +02:00
}
2018-10-23 01:05:09 +03:00
function zoomIn ( elem ) {
2020-07-27 19:58:57 +02:00
return new Promise ( resolve => {
2020-04-15 12:27:04 +02:00
const duration = 240 ;
2020-07-22 22:42:26 +02:00
elem . style . animation = `htmlvideoplayer-zoomin ${ duration } ms ease-in normal` ;
2019-01-10 15:39:37 +03:00
dom . addEventListener ( elem , dom . whichAnimationEvent (), resolve , {
once : true
});
});
2018-10-23 01:05:09 +03:00
}
2020-04-09 02:28:07 +03:00
function normalizeTrackEventText ( text , useHtml ) {
2020-07-21 22:22:16 +02:00
const result = text . replace ( /\\N/gi , '\n' ). replace ( /\r/gi , '' );
2020-04-09 02:28:07 +03:00
return useHtml ? result . replace ( /\n/gi , '<br>' ) : result ;
2018-10-23 01:05:09 +03:00
}
function getTextTrackUrl ( track , item , format ) {
2019-01-10 15:39:37 +03:00
if ( itemHelper . isLocalItem ( item ) && track . Path ) {
return track . Path ;
}
2020-04-15 12:27:04 +02:00
let url = playbackManager . getSubtitleUrl ( track , item . ServerId );
2019-01-10 15:39:37 +03:00
if ( format ) {
url = url . replace ( '.vtt' , format );
}
return url ;
2018-10-23 01:05:09 +03:00
}
function getDefaultProfile () {
2020-08-16 20:24:45 +02:00
return import ( '../../scripts/browserDeviceProfile' ). then (({ default : profileBuilder }) => {
2020-04-15 12:27:04 +02:00
return profileBuilder ({});
2019-01-10 15:39:37 +03:00
});
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
export class HtmlVideoPlayer {
2020-07-22 22:42:26 +02:00
/**
* @type {string}
*/
2020-08-01 04:07:00 +02:00
name ;
2020-07-22 22:42:26 +02:00
/**
* @type {string}
*/
type = 'mediaplayer' ;
/**
* @type {string}
*/
id = 'htmlvideoplayer' ;
/**
* Let any players created by plugins take priority
*
* @type {number}
*/
priority = 1 ;
/**
* @type {boolean}
*/
isFetching = false ;
2018-10-23 01:05:09 +03:00
2020-07-22 22:42:26 +02:00
/**
* @type {HTMLDivElement | null | undefined}
*/
# videoDialog ;
/**
* @type {number | undefined}
*/
# subtitleTrackIndexToSetOnPlaying ;
/**
* @type {number | null}
*/
# audioTrackIndexToSetOnPlaying ;
/**
* @type {null | undefined}
*/
# currentClock ;
/**
* @type {any | null | undefined}
*/
# currentSubtitlesOctopus ;
/**
* @type {null | undefined}
*/
# currentAssRenderer ;
/**
* @type {number | undefined}
*/
# customTrackIndex ;
/**
* @type {boolean | undefined}
*/
# showTrackOffset ;
/**
* @type {number | undefined}
*/
# currentTrackOffset ;
/**
* @type {HTMLElement | null | undefined}
*/
# videoSubtitlesElem ;
/**
* @type {any | null | undefined}
*/
# currentTrackEvents ;
/**
* @type {string[] | undefined}
*/
# supportedFeatures ;
/**
* @type {HTMLVideoElement | null | undefined}
*/
# mediaElement ;
/**
* @type {number}
*/
# fetchQueue = 0 ;
/**
* @type {string | undefined}
*/
# currentSrc ;
/**
* @type {boolean | undefined}
*/
# started ;
/**
* @type {boolean | undefined}
*/
# timeUpdated ;
/**
* @type {number | null | undefined}
*/
# currentTime ;
/**
* @type {any | undefined}
*/
# flvPlayer ;
/**
* @private (used in other files)
* @type {any | undefined}
*/
_hlsPlayer ;
/**
* @private (used in other files)
* @type {any | null | undefined}
*/
_castPlayer ;
/**
* @private (used in other files)
* @type {any | undefined}
*/
_currentPlayOptions ;
/**
* @type {any | undefined}
*/
# lastProfile ;
/**
* @type {MutationObserver | IntersectionObserver | undefined} (Unclear observer typing)
*/
# resizeObserver ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
constructor () {
if ( browser . edgeUwp ) {
this . name = 'Windows Video Player' ;
} else {
this . name = 'Html Video Player' ;
2019-09-12 21:24:16 +02:00
}
}
2020-04-15 12:27:04 +02:00
currentSrc () {
2020-07-22 22:42:26 +02:00
return this . # currentSrc ;
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
/**
* @private
*/
incrementFetchQueue () {
2020-07-22 22:42:26 +02:00
if ( this . # fetchQueue <= 0 ) {
2020-04-15 12:27:04 +02:00
this . isFetching = true ;
2020-09-08 02:05:02 -04:00
Events . trigger ( this , 'beginFetch' );
2020-04-15 12:27:04 +02:00
}
2020-07-22 22:42:26 +02:00
this . # fetchQueue ++ ;
2020-04-15 12:27:04 +02:00
}
/**
* @private
*/
decrementFetchQueue () {
2020-07-22 22:42:26 +02:00
this . # fetchQueue -- ;
2020-04-15 12:27:04 +02:00
2020-07-22 22:42:26 +02:00
if ( this . # fetchQueue <= 0 ) {
2020-04-15 12:27:04 +02:00
this . isFetching = false ;
2020-09-08 02:05:02 -04:00
Events . trigger ( this , 'endFetch' );
2019-09-12 21:24:16 +02:00
}
}
2020-04-15 12:27:04 +02:00
/**
* @private
*/
updateVideoUrl ( streamInfo ) {
2020-07-21 22:22:16 +02:00
const isHls = streamInfo . url . toLowerCase (). includes ( '.m3u8' );
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
const mediaSource = streamInfo . mediaSource ;
const item = streamInfo . item ;
2019-01-10 15:39:37 +03:00
// Huge hack alert. Safari doesn't seem to like if the segments aren't available right away when playback starts
// This will start the transcoding process before actually feeding the video url into the player
// Edit: Also seeing stalls from hls.js
if ( mediaSource && item && ! mediaSource . RunTimeTicks && isHls && streamInfo . playMethod === 'Transcode' && ( browser . iOS || browser . osx )) {
2020-07-21 22:22:16 +02:00
const hlsPlaylistUrl = streamInfo . url . replace ( 'master.m3u8' , 'live.m3u8' );
2019-01-10 15:39:37 +03:00
loading . show ();
2020-07-22 22:42:26 +02:00
console . debug ( `prefetching hls playlist: ${ hlsPlaylistUrl } ` );
2019-01-10 15:39:37 +03:00
2020-08-16 20:24:45 +02:00
return ConnectionManager . getApiClient ( item . ServerId ). ajax ({
2019-01-10 15:39:37 +03:00
type : 'GET' ,
url : hlsPlaylistUrl
}). then ( function () {
2020-07-22 22:42:26 +02:00
console . debug ( `completed prefetching hls playlist: ${ hlsPlaylistUrl } ` );
2019-01-10 15:39:37 +03:00
loading . hide ();
streamInfo . url = hlsPlaylistUrl ;
}, function () {
2020-07-22 22:42:26 +02:00
console . error ( `error prefetching hls playlist: ${ hlsPlaylistUrl } ` );
2019-01-10 15:39:37 +03:00
loading . hide ();
});
} else {
return Promise . resolve ();
}
}
2020-04-15 12:27:04 +02:00
play ( options ) {
2020-07-22 22:42:26 +02:00
this . # started = false ;
this . # timeUpdated = false ;
2019-01-10 15:39:37 +03:00
2020-07-22 22:42:26 +02:00
this . # currentTime = null ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
this . resetSubtitleOffset ();
2019-11-30 20:01:32 +01:00
2020-04-15 12:27:04 +02:00
return this . createMediaElement ( options ). then ( elem => {
return this . updateVideoUrl ( options ). then (() => {
return this . setCurrentSrc ( elem , options );
2019-01-10 15:39:37 +03:00
});
});
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
/**
* @private
*/
2020-07-22 22:42:26 +02:00
setSrcWithFlvJs ( elem , options , url ) {
2020-08-16 20:24:45 +02:00
return import ( 'flv.js' ). then (({ default : flvjs }) => {
2020-04-15 12:27:04 +02:00
const flvPlayer = flvjs . createPlayer ({
2019-01-10 15:39:37 +03:00
type : 'flv' ,
2018-10-23 01:05:09 +03:00
url : url
2019-01-10 15:39:37 +03:00
},
2019-11-23 00:29:38 +09:00
{
seekType : 'range' ,
lazyLoad : false
});
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
flvPlayer . attachMediaElement ( elem );
flvPlayer . load ();
2019-01-10 15:39:37 +03:00
2020-07-22 22:42:26 +02:00
this . # flvPlayer = flvPlayer ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
// This is needed in setCurrentTrackElement
2020-07-22 22:42:26 +02:00
this . # currentSrc = url ;
2020-04-15 12:27:04 +02:00
return flvPlayer . play ();
2019-01-10 15:39:37 +03:00
});
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
/**
* @private
*/
2020-07-27 18:43:59 +02:00
setSrcWithHlsJs ( elem , options , url ) {
2020-04-19 23:23:46 +02:00
return new Promise (( resolve , reject ) => {
2020-04-15 12:27:04 +02:00
requireHlsPlayer (() => {
const hls = new Hls ({
2020-08-29 16:44:36 +02:00
manifestLoadingTimeOut : 20000
2018-10-23 01:05:09 +03:00
});
2019-01-10 15:39:37 +03:00
hls . loadSource ( url );
hls . attachMedia ( elem );
2020-07-26 14:18:34 +02:00
bindEventsToHlsPlayer ( this , hls , elem , this . onError , resolve , reject );
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
this . _hlsPlayer = hls ;
2019-01-10 15:39:37 +03:00
// This is needed in setCurrentTrackElement
2020-07-22 22:42:26 +02:00
this . # currentSrc = url ;
2019-01-10 15:39:37 +03:00
});
});
}
2020-04-15 12:27:04 +02:00
/**
* @private
*/
setCurrentSrc ( elem , options ) {
2020-05-27 00:38:01 +02:00
elem . removeEventListener ( 'error' , this . onError );
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
let val = options . url ;
2020-07-22 22:42:26 +02:00
console . debug ( `playing url: ${ val } ` );
2019-01-10 15:39:37 +03:00
// Convert to seconds
2020-04-15 12:27:04 +02:00
const seconds = ( options . playerStartPositionTicks || 0 ) / 10000000 ;
2019-01-10 15:39:37 +03:00
if ( seconds ) {
2020-07-22 22:42:26 +02:00
val += `#t= ${ seconds } ` ;
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
2020-07-26 14:18:34 +02:00
destroyHlsPlayer ( this );
destroyFlvPlayer ( this );
destroyCastPlayer ( this );
2019-01-10 15:39:37 +03:00
2020-07-22 22:42:26 +02:00
this . # subtitleTrackIndexToSetOnPlaying = options . mediaSource . DefaultSubtitleStreamIndex == null ? - 1 : options . mediaSource . DefaultSubtitleStreamIndex ;
if ( this . # subtitleTrackIndexToSetOnPlaying != null && this . # subtitleTrackIndexToSetOnPlaying >= 0 ) {
const initialSubtitleStream = options . mediaSource . MediaStreams [ this . # subtitleTrackIndexToSetOnPlaying ];
2019-01-10 15:39:37 +03:00
if ( ! initialSubtitleStream || initialSubtitleStream . DeliveryMethod === 'Encode' ) {
2020-07-22 22:42:26 +02:00
this . # subtitleTrackIndexToSetOnPlaying = - 1 ;
2019-01-10 15:39:37 +03:00
}
}
2020-07-22 22:42:26 +02:00
this . # audioTrackIndexToSetOnPlaying = options . playMethod === 'Transcode' ? null : options . mediaSource . DefaultAudioStreamIndex ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
this . _currentPlayOptions = options ;
2019-01-10 15:39:37 +03:00
2020-07-26 14:18:34 +02:00
const crossOrigin = getCrossOriginValue ( options . mediaSource );
2019-01-10 15:39:37 +03:00
if ( crossOrigin ) {
elem . crossOrigin = crossOrigin ;
}
2020-07-27 22:07:10 +02:00
if ( enableHlsJsPlayer ( options . mediaSource . RunTimeTicks , 'Video' ) && val . includes ( '.m3u8' )) {
2020-07-27 18:43:59 +02:00
return this . setSrcWithHlsJs ( elem , options , val );
2019-01-10 15:39:37 +03:00
} else if ( options . playMethod !== 'Transcode' && options . mediaSource . Container === 'flv' ) {
2020-07-22 22:42:26 +02:00
return this . setSrcWithFlvJs ( elem , options , val );
2019-01-10 15:39:37 +03:00
} else {
elem . autoplay = true ;
2020-05-07 21:04:26 -07:00
// Safari will not send cookies without this
elem . crossOrigin = 'use-credentials' ;
2020-07-26 14:18:34 +02:00
return applySrc ( elem , val , options ). then (() => {
2020-07-22 22:42:26 +02:00
this . # currentSrc = val ;
2019-01-10 15:39:37 +03:00
2020-07-26 14:18:34 +02:00
return playWithPromise ( elem , this . onError );
2019-01-10 15:39:37 +03:00
});
}
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
setSubtitleStreamIndex ( index ) {
this . setCurrentTrackElement ( index );
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
resetSubtitleOffset () {
2020-07-22 22:42:26 +02:00
this . # currentTrackOffset = 0 ;
this . # showTrackOffset = false ;
2020-04-15 12:27:04 +02:00
}
2019-11-30 20:01:32 +01:00
2020-04-15 12:27:04 +02:00
enableShowingSubtitleOffset () {
2020-07-22 22:42:26 +02:00
this . # showTrackOffset = true ;
2020-04-15 12:27:04 +02:00
}
2019-04-08 20:21:16 +02:00
2020-04-15 12:27:04 +02:00
disableShowingSubtitleOffset () {
2020-07-22 22:42:26 +02:00
this . # showTrackOffset = false ;
2020-04-15 12:27:04 +02:00
}
2019-04-08 20:21:16 +02:00
2020-04-15 12:27:04 +02:00
isShowingSubtitleOffsetEnabled () {
2020-07-22 22:42:26 +02:00
return this . # showTrackOffset ;
2020-04-15 12:27:04 +02:00
}
2019-04-08 20:21:16 +02:00
2020-04-15 12:27:04 +02:00
/**
* @private
*/
getTextTrack () {
2020-07-22 22:42:26 +02:00
const videoElement = this . # mediaElement ;
2019-12-07 20:29:14 +01:00
if ( videoElement ) {
return Array . from ( videoElement . textTracks )
2020-04-15 12:27:04 +02:00
. find ( function ( trackElement ) {
2019-12-09 21:37:05 +01:00
// get showing .vtt textTack
return trackElement . mode === 'showing' ;
});
2019-12-07 20:29:14 +01:00
} else {
2020-01-07 21:12:25 +01:00
return null ;
2019-12-07 20:29:14 +01:00
}
}
2020-04-15 12:27:04 +02:00
/**
* @private
*/
setSubtitleOffset ( offset ) {
const offsetValue = parseFloat ( offset );
2019-03-30 22:11:39 +01:00
2019-10-13 14:10:51 +02:00
// if .ass currently rendering
2020-07-22 22:42:26 +02:00
if ( this . # currentSubtitlesOctopus ) {
2020-04-15 12:27:04 +02:00
this . updateCurrentTrackOffset ( offsetValue );
2020-07-22 22:42:26 +02:00
this . # currentSubtitlesOctopus . timeOffset = ( this . _currentPlayOptions . transcodingOffsetTicks || 0 ) / 10000000 + offsetValue ;
2019-10-13 14:10:51 +02:00
} else {
2020-04-15 12:27:04 +02:00
const trackElement = this . getTextTrack ();
2019-12-07 20:29:14 +01:00
// if .vtt currently rendering
if ( trackElement ) {
2020-04-15 12:27:04 +02:00
this . setTextTrackSubtitleOffset ( trackElement , offsetValue );
2020-07-22 22:42:26 +02:00
} else if ( this . # currentTrackEvents ) {
this . setTrackEventsSubtitleOffset ( this . # currentTrackEvents , offsetValue );
2019-12-07 20:29:14 +01:00
} else {
2020-05-04 12:44:12 +02:00
console . debug ( 'No available track, cannot apply offset: ' , offsetValue );
2019-12-07 20:29:14 +01:00
}
2019-10-13 14:10:51 +02:00
}
2020-04-15 12:27:04 +02:00
}
2019-03-30 22:11:39 +01:00
2020-04-15 12:27:04 +02:00
/**
* @private
*/
updateCurrentTrackOffset ( offsetValue ) {
let relativeOffset = offsetValue ;
const newTrackOffset = offsetValue ;
2020-07-22 22:42:26 +02:00
if ( this . # currentTrackOffset ) {
relativeOffset -= this . # currentTrackOffset ;
2019-03-30 22:11:39 +01:00
}
2020-07-22 22:42:26 +02:00
this . # currentTrackOffset = newTrackOffset ;
2019-03-30 22:11:39 +01:00
// relative to currentTrackOffset
return relativeOffset ;
}
2020-04-15 12:27:04 +02:00
/**
* @private
*/
setTextTrackSubtitleOffset ( currentTrack , offsetValue ) {
2019-11-23 00:29:38 +09:00
if ( currentTrack . cues ) {
2020-04-15 12:27:04 +02:00
offsetValue = this . updateCurrentTrackOffset ( offsetValue );
2019-03-30 22:11:39 +01:00
Array . from ( currentTrack . cues )
2020-04-15 12:27:04 +02:00
. forEach ( function ( cue ) {
2019-11-23 00:29:38 +09:00
cue . startTime -= offsetValue ;
cue . endTime -= offsetValue ;
});
2019-03-30 22:11:39 +01:00
}
2020-01-07 21:12:25 +01:00
}
2019-03-30 22:11:39 +01:00
2020-04-15 12:27:04 +02:00
/**
* @private
*/
setTrackEventsSubtitleOffset ( trackEvents , offsetValue ) {
2020-01-07 21:12:25 +01:00
if ( Array . isArray ( trackEvents )) {
2020-04-15 12:27:04 +02:00
offsetValue = this . updateCurrentTrackOffset ( offsetValue ) * 1e7 ; // ticks
trackEvents . forEach ( function ( trackEvent ) {
2019-12-07 20:29:14 +01:00
trackEvent . StartPositionTicks -= offsetValue ;
trackEvent . EndPositionTicks -= offsetValue ;
});
2019-03-30 22:11:39 +01:00
}
}
2020-04-15 12:27:04 +02:00
getSubtitleOffset () {
2020-07-22 22:42:26 +02:00
return this . # currentTrackOffset ;
2020-04-15 12:27:04 +02:00
}
2019-04-08 20:21:16 +02:00
2020-04-15 12:27:04 +02:00
/**
* @private
*/
isAudioStreamSupported ( stream , deviceProfile ) {
2020-07-21 22:22:16 +02:00
const codec = ( stream . Codec || '' ). toLowerCase ();
2019-01-10 15:39:37 +03:00
if ( ! codec ) {
return true ;
}
if ( ! deviceProfile ) {
// This should never happen
return true ;
}
2020-04-15 12:27:04 +02:00
const profiles = deviceProfile . DirectPlayProfiles || [];
2019-01-10 15:39:37 +03:00
return profiles . filter ( function ( p ) {
if ( p . Type === 'Video' ) {
if ( ! p . AudioCodec ) {
return true ;
}
2020-04-19 23:23:46 +02:00
return p . AudioCodec . toLowerCase (). includes ( codec );
2019-01-10 15:39:37 +03:00
}
return false ;
}). length > 0 ;
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
/**
* @private
*/
getSupportedAudioStreams () {
2020-07-22 22:42:26 +02:00
const profile = this . # lastProfile ;
2019-01-10 15:39:37 +03:00
2020-04-19 17:38:03 +02:00
return getMediaStreamAudioTracks ( this . _currentPlayOptions . mediaSource ). filter (( stream ) => {
2020-04-15 12:27:04 +02:00
return this . isAudioStreamSupported ( stream , profile );
2019-01-10 15:39:37 +03:00
});
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
setAudioStreamIndex ( index ) {
const streams = this . getSupportedAudioStreams ();
2019-01-10 15:39:37 +03:00
if ( streams . length < 2 ) {
// If there's only one supported stream then trust that the player will handle it on it's own
return ;
}
2020-04-15 12:27:04 +02:00
let audioIndex = - 1 ;
2019-01-10 15:39:37 +03:00
2020-04-19 17:38:03 +02:00
for ( const stream of streams ) {
2019-01-10 15:39:37 +03:00
audioIndex ++ ;
if ( stream . Index === index ) {
break ;
}
}
if ( audioIndex === - 1 ) {
return ;
}
2020-07-22 22:42:26 +02:00
const elem = this . # mediaElement ;
2019-01-10 15:39:37 +03:00
if ( ! elem ) {
return ;
}
2020-07-30 00:44:33 +02:00
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/audioTracks
2019-01-10 15:39:37 +03:00
2020-07-29 19:51:33 +02:00
/**
2020-07-30 00:41:11 +02:00
* @type {ArrayLike<any>|any[]}
2020-07-29 19:51:33 +02:00
*/
2020-04-15 12:27:04 +02:00
const elemAudioTracks = elem . audioTracks || [];
2020-07-22 22:42:26 +02:00
console . debug ( `found ${ elemAudioTracks . length } audio tracks` );
2019-01-10 15:39:37 +03:00
2020-07-29 19:51:33 +02:00
for ( const [ i , audioTrack ] of Array . from ( elemAudioTracks ). entries ()) {
2019-01-10 15:39:37 +03:00
if ( audioIndex === i ) {
2020-04-19 17:38:03 +02:00
console . debug ( `setting audio track ${ i } to enabled` );
audioTrack . enabled = true ;
2019-01-10 15:39:37 +03:00
} else {
2020-04-19 17:38:03 +02:00
console . debug ( `setting audio track ${ i } to disabled` );
audioTrack . enabled = false ;
2019-01-10 15:39:37 +03:00
}
}
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
stop ( destroyPlayer ) {
2020-07-22 22:42:26 +02:00
const elem = this . # mediaElement ;
const src = this . # currentSrc ;
2019-01-10 15:39:37 +03:00
if ( elem ) {
if ( src ) {
elem . pause ();
}
2020-07-26 14:18:34 +02:00
onEndedInternal ( this , elem , this . onError );
2019-01-10 15:39:37 +03:00
if ( destroyPlayer ) {
2020-04-15 12:27:04 +02:00
this . destroy ();
2019-01-10 15:39:37 +03:00
}
}
2020-04-15 12:27:04 +02:00
this . destroyCustomTrack ( elem );
2019-01-10 15:39:37 +03:00
return Promise . resolve ();
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
destroy () {
2020-07-26 14:18:34 +02:00
destroyHlsPlayer ( this );
destroyFlvPlayer ( this );
2019-01-10 15:39:37 +03:00
appRouter . setTransparency ( 'none' );
2020-07-22 22:42:26 +02:00
const videoElement = this . # mediaElement ;
2019-01-10 15:39:37 +03:00
if ( videoElement ) {
2020-07-22 22:42:26 +02:00
this . # mediaElement = null ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
this . destroyCustomTrack ( videoElement );
2020-05-27 00:38:01 +02:00
videoElement . removeEventListener ( 'timeupdate' , this . onTimeUpdate );
videoElement . removeEventListener ( 'ended' , this . onEnded );
videoElement . removeEventListener ( 'volumechange' , this . onVolumeChange );
videoElement . removeEventListener ( 'pause' , this . onPause );
videoElement . removeEventListener ( 'playing' , this . onPlaying );
videoElement . removeEventListener ( 'play' , this . onPlay );
videoElement . removeEventListener ( 'click' , this . onClick );
videoElement . removeEventListener ( 'dblclick' , this . onDblClick );
videoElement . removeEventListener ( 'waiting' , this . onWaiting );
2019-01-10 15:39:37 +03:00
videoElement . parentNode . removeChild ( videoElement );
}
2020-07-22 22:42:26 +02:00
const dlg = this . # videoDialog ;
2019-01-10 15:39:37 +03:00
if ( dlg ) {
2020-07-22 22:42:26 +02:00
this . # videoDialog = null ;
2019-01-10 15:39:37 +03:00
dlg . parentNode . removeChild ( dlg );
}
2019-02-17 23:29:31 +01:00
2020-08-16 20:24:45 +02:00
if ( Screenfull . isEnabled ) {
Screenfull . exit ();
2020-07-29 09:28:06 -04:00
} else {
// iOS Safari
if ( document . webkitIsFullScreen && document . webkitCancelFullscreen ) {
document . webkitCancelFullscreen ();
}
2020-04-26 16:16:48 +02:00
}
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
/**
* @private
2020-05-27 00:38:01 +02:00
* @param e {Event} The event received from the `<video>` element
2020-04-15 12:27:04 +02:00
*/
2020-05-27 00:38:01 +02:00
onEnded = ( e ) => {
/**
* @type {HTMLMediaElement}
*/
const elem = e . target ;
this . destroyCustomTrack ( elem );
2020-07-26 14:18:34 +02:00
onEndedInternal ( this , elem , this . onError );
2020-08-01 04:07:00 +02:00
};
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
/**
* @private
2020-05-27 00:38:01 +02:00
* @param e {Event} The event received from the `<video>` element
2020-04-15 12:27:04 +02:00
*/
2020-05-27 00:38:01 +02:00
onTimeUpdate = ( e ) => {
/**
* @type {HTMLMediaElement}
*/
const elem = e . target ;
2020-04-15 12:27:04 +02:00
// get the player position and the transcoding offset
2020-05-27 00:38:01 +02:00
const time = elem . currentTime ;
2020-04-15 12:27:04 +02:00
2020-07-22 22:42:26 +02:00
if ( time && ! this . # timeUpdated ) {
this . # timeUpdated = true ;
2020-05-27 00:38:01 +02:00
this . ensureValidVideo ( elem );
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
2020-07-22 22:42:26 +02:00
this . # currentTime = time ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
const currentPlayOptions = this . _currentPlayOptions ;
2019-01-10 15:39:37 +03:00
// Not sure yet how this is coming up null since we never null it out, but it is causing app crashes
if ( currentPlayOptions ) {
2020-04-15 12:27:04 +02:00
let timeMs = time * 1000 ;
2019-01-10 15:39:37 +03:00
timeMs += (( currentPlayOptions . transcodingOffsetTicks || 0 ) / 10000 );
2020-04-15 12:27:04 +02:00
this . updateSubtitleText ( timeMs );
2019-01-10 15:39:37 +03:00
}
2020-09-08 02:05:02 -04:00
Events . trigger ( this , 'timeupdate' );
2020-08-01 04:07:00 +02:00
};
2018-10-23 01:05:09 +03:00
2020-04-15 12:27:04 +02:00
/**
* @private
2020-05-27 00:38:01 +02:00
* @param e {Event} The event received from the `<video>` element
2020-04-15 12:27:04 +02:00
*/
2020-05-27 00:38:01 +02:00
onVolumeChange = ( e ) => {
/**
* @type {HTMLMediaElement}
*/
const elem = e . target ;
2020-07-26 14:18:34 +02:00
saveVolume ( elem . volume );
2020-09-08 02:05:02 -04:00
Events . trigger ( this , 'volumechange' );
2020-08-01 04:07:00 +02:00
};
2018-10-23 01:05:09 +03:00
2020-04-15 12:27:04 +02:00
/**
* @private
*/
2020-05-27 00:38:01 +02:00
onNavigatedToOsd = () => {
2020-07-22 22:42:26 +02:00
const dlg = this . # videoDialog ;
2019-01-10 15:39:37 +03:00
if ( dlg ) {
dlg . classList . remove ( 'videoPlayerContainer-onTop' );
2020-04-15 12:27:04 +02:00
this . onStartedAndNavigatedToOsd ();
2019-01-10 15:39:37 +03:00
}
2020-08-01 04:07:00 +02:00
};
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
/**
* @private
*/
onStartedAndNavigatedToOsd () {
2019-01-10 15:39:37 +03:00
// If this causes a failure during navigation we end up in an awkward UI state
2020-07-22 22:42:26 +02:00
this . setCurrentTrackElement ( this . # subtitleTrackIndexToSetOnPlaying );
2019-01-10 15:39:37 +03:00
2020-07-22 22:42:26 +02:00
if ( this . # audioTrackIndexToSetOnPlaying != null && this . canSetAudioStreamIndex ()) {
this . setAudioStreamIndex ( this . # audioTrackIndexToSetOnPlaying );
2019-01-10 15:39:37 +03:00
}
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
/**
* @private
2020-05-27 00:38:01 +02:00
* @param e {Event} The event received from the `<video>` element
2020-04-15 12:27:04 +02:00
*/
2020-05-27 00:38:01 +02:00
onPlaying = ( e ) => {
/**
* @type {HTMLMediaElement}
*/
const elem = e . target ;
2020-07-22 22:42:26 +02:00
if ( ! this . # started ) {
this . # started = true ;
2020-05-27 00:38:01 +02:00
elem . removeAttribute ( 'controls' );
2019-01-10 15:39:37 +03:00
loading . hide ();
2020-07-26 14:18:34 +02:00
seekOnPlaybackStart ( this , e . target , this . _currentPlayOptions . playerStartPositionTicks , () => {
2020-07-22 22:42:26 +02:00
if ( this . # currentSubtitlesOctopus ) {
this . # currentSubtitlesOctopus . timeOffset = ( this . _currentPlayOptions . transcodingOffsetTicks || 0 ) / 10000000 + this . # currentTrackOffset ;
this . # currentSubtitlesOctopus . resize ();
this . # currentSubtitlesOctopus . resetRenderAheadCache ( false );
2020-04-27 18:43:39 +03:00
}
2020-04-15 19:29:21 +03:00
});
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
if ( this . _currentPlayOptions . fullscreen ) {
2020-05-27 00:38:01 +02:00
appRouter . showVideoOsd (). then ( this . onNavigatedToOsd );
2019-01-10 15:39:37 +03:00
} else {
appRouter . setTransparency ( 'backdrop' );
2020-07-22 22:42:26 +02:00
this . # videoDialog . classList . remove ( 'videoPlayerContainer-onTop' );
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
this . onStartedAndNavigatedToOsd ();
2019-01-10 15:39:37 +03:00
}
}
2020-09-08 02:05:02 -04:00
Events . trigger ( this , 'playing' );
2020-08-01 04:07:00 +02:00
};
2018-10-23 01:05:09 +03:00
2020-04-15 12:27:04 +02:00
/**
* @private
*/
2020-05-27 00:38:01 +02:00
onPlay = () => {
2020-09-08 02:05:02 -04:00
Events . trigger ( this , 'unpause' );
2020-08-01 04:07:00 +02:00
};
2018-10-23 01:05:09 +03:00
2020-04-15 12:27:04 +02:00
/**
* @private
*/
ensureValidVideo ( elem ) {
2020-07-22 22:42:26 +02:00
if ( elem !== this . # mediaElement ) {
2019-01-10 15:39:37 +03:00
return ;
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
if ( elem . videoWidth === 0 && elem . videoHeight === 0 ) {
2020-04-15 12:27:04 +02:00
const mediaSource = ( this . _currentPlayOptions || {}). mediaSource ;
2019-01-10 15:39:37 +03:00
// Only trigger this if there is media info
// Avoid triggering in situations where it might not actually have a video stream (audio only live tv channel)
if ( ! mediaSource || mediaSource . RunTimeTicks ) {
2020-07-26 14:18:34 +02:00
onErrorInternal ( this , 'mediadecodeerror' );
2019-01-10 15:39:37 +03:00
}
}
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
/**
* @private
*/
2020-05-27 00:38:01 +02:00
onClick = () => {
2020-09-08 02:05:02 -04:00
Events . trigger ( this , 'click' );
2020-08-01 04:07:00 +02:00
};
2018-10-23 01:05:09 +03:00
2020-04-15 12:27:04 +02:00
/**
* @private
*/
2020-05-27 00:38:01 +02:00
onDblClick = () => {
2020-09-08 02:05:02 -04:00
Events . trigger ( this , 'dblclick' );
2020-08-01 04:07:00 +02:00
};
2018-10-23 01:05:09 +03:00
2020-04-15 12:27:04 +02:00
/**
* @private
*/
2020-05-27 00:38:01 +02:00
onPause = () => {
2020-09-08 02:05:02 -04:00
Events . trigger ( this , 'pause' );
2020-08-01 04:07:00 +02:00
};
2018-10-23 01:05:09 +03:00
2020-04-15 12:27:04 +02:00
onWaiting () {
2020-09-08 02:05:02 -04:00
Events . trigger ( this , 'waiting' );
2020-04-01 17:53:14 +02:00
}
2020-04-15 12:27:04 +02:00
/**
* @private
2020-05-27 00:38:01 +02:00
* @param e {Event} The event received from the `<video>` element
2020-04-15 12:27:04 +02:00
*/
2020-05-27 00:38:01 +02:00
onError = ( e ) => {
/**
* @type {HTMLMediaElement}
*/
2020-07-21 22:22:16 +02:00
const elem = e . target ;
2020-05-27 00:38:01 +02:00
const errorCode = elem . error ? ( elem . error . code || 0 ) : 0 ;
2020-07-21 22:22:16 +02:00
const errorMessage = elem . error ? ( elem . error . message || '' ) : '' ;
2020-07-22 22:42:26 +02:00
console . error ( `media element error: ${ errorCode } ${ errorMessage } ` );
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
let type ;
2019-01-10 15:39:37 +03:00
2018-10-23 01:05:09 +03:00
switch ( errorCode ) {
case 1 :
2019-01-10 15:39:37 +03:00
// MEDIA_ERR_ABORTED
// This will trigger when changing media while something is playing
2018-10-23 01:05:09 +03:00
return ;
case 2 :
2019-01-10 15:39:37 +03:00
// MEDIA_ERR_NETWORK
type = 'network' ;
2018-10-23 01:05:09 +03:00
break ;
case 3 :
2019-01-10 15:39:37 +03:00
// MEDIA_ERR_DECODE
2020-04-19 17:38:03 +02:00
if ( this . _hlsPlayer ) {
2020-07-26 14:18:34 +02:00
handleHlsJsMediaError ( this );
2019-01-10 15:39:37 +03:00
return ;
} else {
type = 'mediadecodeerror' ;
}
2018-10-23 01:05:09 +03:00
break ;
case 4 :
2019-01-10 15:39:37 +03:00
// MEDIA_ERR_SRC_NOT_SUPPORTED
type = 'medianotsupported' ;
2018-10-23 01:05:09 +03:00
break ;
default :
2019-01-10 15:39:37 +03:00
// seeing cases where Edge is firing error events with no error code
// example is start playing something, then immediately change src to something else
return ;
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
2020-07-26 14:18:34 +02:00
onErrorInternal ( this , type );
2020-08-01 04:07:00 +02:00
};
2018-10-23 01:05:09 +03:00
2020-04-15 12:27:04 +02:00
/**
* @private
*/
destroyCustomTrack ( videoElement ) {
2020-07-22 22:42:26 +02:00
if ( this . # resizeObserver ) {
this . # resizeObserver . disconnect ();
this . # resizeObserver = null ;
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
2020-07-22 22:42:26 +02:00
if ( this . # videoSubtitlesElem ) {
const subtitlesContainer = this . # videoSubtitlesElem . parentNode ;
2019-01-10 15:39:37 +03:00
if ( subtitlesContainer ) {
tryRemoveElement ( subtitlesContainer );
2018-10-23 01:05:09 +03:00
}
2020-07-22 22:42:26 +02:00
this . # videoSubtitlesElem = null ;
2019-01-10 15:39:37 +03:00
}
2020-07-22 22:42:26 +02:00
this . # currentTrackEvents = null ;
2019-01-10 15:39:37 +03:00
if ( videoElement ) {
2020-04-15 12:27:04 +02:00
const allTracks = videoElement . textTracks || []; // get list of tracks
2020-04-19 17:38:03 +02:00
for ( const track of allTracks ) {
if ( track . label . includes ( 'manualTrack' )) {
track . mode = 'disabled' ;
2019-01-10 15:39:37 +03:00
}
}
}
2020-07-22 22:42:26 +02:00
this . # customTrackIndex = - 1 ;
this . # currentClock = null ;
2020-04-15 12:27:04 +02:00
this . _currentAspectRatio = null ;
2019-01-10 15:39:37 +03:00
2020-07-22 22:42:26 +02:00
const octopus = this . # currentSubtitlesOctopus ;
2019-09-27 10:52:15 -04:00
if ( octopus ) {
octopus . dispose ();
}
2020-07-22 22:42:26 +02:00
this . # currentSubtitlesOctopus = null ;
2019-09-27 10:52:15 -04:00
2020-07-22 22:42:26 +02:00
const renderer = this . # currentAssRenderer ;
2019-01-10 15:39:37 +03:00
if ( renderer ) {
renderer . setEnabled ( false );
}
2020-07-22 22:42:26 +02:00
this . # currentAssRenderer = null ;
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
/**
* @private
*/
2020-07-27 18:54:04 +02:00
fetchSubtitlesUwp ( track ) {
2019-01-10 15:39:37 +03:00
return Windows . Storage . StorageFile . getFileFromPathAsync ( track . Path ). then ( function ( storageFile ) {
2020-04-15 12:27:04 +02:00
return Windows . Storage . FileIO . readTextAsync ( storageFile );
}). then ( function ( text ) {
return JSON . parse ( text );
2019-01-10 15:39:37 +03:00
});
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
/**
* @private
*/
2020-07-22 22:42:26 +02:00
async fetchSubtitles ( track , item ) {
2019-01-10 15:39:37 +03:00
if ( window . Windows && itemHelper . isLocalItem ( item )) {
2020-04-15 12:27:04 +02:00
return this . fetchSubtitlesUwp ( track , item );
2019-01-10 15:39:37 +03:00
}
2020-04-15 12:27:04 +02:00
this . incrementFetchQueue ();
2020-07-22 22:42:26 +02:00
try {
const response = await fetch ( getTextTrackUrl ( track , item , '.js' ));
2019-01-10 15:39:37 +03:00
2020-07-27 22:07:10 +02:00
if ( ! response . ok ) {
throw new Error ( response );
}
2019-01-10 15:39:37 +03:00
2020-07-22 22:42:26 +02:00
return response . json ();
} finally {
this . decrementFetchQueue ();
}
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
/**
* @private
*/
setTrackForDisplay ( videoElement , track ) {
2019-01-10 15:39:37 +03:00
if ( ! track ) {
2020-04-15 12:27:04 +02:00
this . destroyCustomTrack ( videoElement );
2019-01-10 15:39:37 +03:00
return ;
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
2019-05-22 23:06:39 -07:00
// skip if already playing this track
2020-07-22 22:42:26 +02:00
if ( this . # customTrackIndex === track . Index ) {
2019-01-10 15:39:37 +03:00
return ;
}
2020-04-15 12:27:04 +02:00
this . resetSubtitleOffset ();
2020-04-19 17:38:03 +02:00
const item = this . _currentPlayOptions . item ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
this . destroyCustomTrack ( videoElement );
2020-07-22 22:42:26 +02:00
this . # customTrackIndex = track . Index ;
2020-04-15 12:27:04 +02:00
this . renderTracksEvents ( videoElement , track , item );
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
/**
* @private
*/
renderSsaAss ( videoElement , track , item ) {
2020-04-19 17:38:03 +02:00
const attachments = this . _currentPlayOptions . mediaSource . MediaAttachments || [];
2020-08-16 20:24:45 +02:00
const apiClient = ConnectionManager . getApiClient ( item );
2020-04-19 23:23:46 +02:00
const htmlVideoPlayer = this ;
2020-04-15 12:27:04 +02:00
const options = {
2019-09-27 10:52:15 -04:00
video : videoElement ,
subUrl : getTextTrackUrl ( track , item ),
2020-01-25 18:05:54 +03:00
fonts : attachments . map ( function ( i ) {
2020-04-04 14:06:55 +03:00
return apiClient . getUrl ( i . DeliveryUrl );
2020-01-25 18:05:54 +03:00
}),
2020-07-22 22:42:26 +02:00
workerUrl : ` ${ appRouter . baseUrl () } /libraries/subtitles-octopus-worker.js` ,
legacyWorkerUrl : ` ${ appRouter . baseUrl () } /libraries/subtitles-octopus-worker-legacy.js` ,
2020-04-15 12:27:04 +02:00
onError () {
2020-07-26 14:18:34 +02:00
onErrorInternal ( htmlVideoPlayer , 'mediadecodeerror' );
2020-03-29 22:04:36 +03:00
},
2020-04-15 12:27:04 +02:00
timeOffset : ( this . _currentPlayOptions . transcodingOffsetTicks || 0 ) / 10000000 ,
2020-03-29 22:04:36 +03:00
// new octopus options; override all, even defaults
renderMode : 'blend' ,
dropAllAnimations : false ,
libassMemoryLimit : 40 ,
libassGlyphLimit : 40 ,
targetFps : 24 ,
prescaleTradeoff : 0.8 ,
softHeightLimit : 1080 ,
hardHeightLimit : 2160 ,
resizeVariation : 0.2 ,
renderAhead : 90
2019-09-27 10:52:15 -04:00
};
2020-08-16 20:24:45 +02:00
import ( 'libass-wasm' ). then (({ default : SubtitlesOctopus }) => {
2020-07-22 22:42:26 +02:00
this . # currentSubtitlesOctopus = new SubtitlesOctopus ( options );
2019-09-27 10:52:15 -04:00
});
}
2020-04-15 12:27:04 +02:00
/**
* @private
*/
requiresCustomSubtitlesElement () {
2019-01-10 15:39:37 +03:00
// after a system update, ps4 isn't showing anything when creating a track element dynamically
// going to have to do it ourselves
if ( browser . ps4 ) {
return true ;
}
// This is unfortunate, but we're unable to remove the textTrack that gets added via addTextTrack
if ( browser . firefox || browser . web0s ) {
return true ;
}
if ( browser . edge ) {
return true ;
}
2018-10-23 01:05:09 +03:00
if ( browser . iOS ) {
2020-04-15 12:27:04 +02:00
const userAgent = navigator . userAgent . toLowerCase ();
2019-01-10 15:39:37 +03:00
// works in the browser but not the native app
2020-04-19 23:23:46 +02:00
if (( userAgent . includes ( 'os 9' ) || userAgent . includes ( 'os 8' )) && ! userAgent . includes ( 'safari' )) {
2019-01-10 15:39:37 +03:00
return true ;
}
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
return false ;
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
/**
* @private
*/
renderSubtitlesWithCustomElement ( videoElement , track , item ) {
this . fetchSubtitles ( track , item ). then (( data ) => {
2020-07-22 22:42:26 +02:00
if ( ! this . # videoSubtitlesElem ) {
2020-07-21 22:22:16 +02:00
const subtitlesContainer = document . createElement ( 'div' );
2019-01-10 15:39:37 +03:00
subtitlesContainer . classList . add ( 'videoSubtitles' );
subtitlesContainer . innerHTML = '<div class="videoSubtitlesInner"></div>' ;
2020-07-22 22:42:26 +02:00
this . # videoSubtitlesElem = subtitlesContainer . querySelector ( '.videoSubtitlesInner' );
this . setSubtitleAppearance ( subtitlesContainer , this . # videoSubtitlesElem );
2019-01-10 15:39:37 +03:00
videoElement . parentNode . appendChild ( subtitlesContainer );
2020-07-22 22:42:26 +02:00
this . # currentTrackEvents = data . TrackEvents ;
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
});
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
/**
* @private
*/
setSubtitleAppearance ( elem , innerElem ) {
2020-08-16 20:24:45 +02:00
Promise . all ([ import ( '../../scripts/settings/userSettings' ), import ( '../../components/subtitlesettings/subtitleappearancehelper' )]). then (([ userSettings , subtitleAppearanceHelper ]) => {
2018-10-23 01:05:09 +03:00
subtitleAppearanceHelper . applyStyles ({
text : innerElem ,
window : elem
2019-01-10 15:39:37 +03:00
}, userSettings . getSubtitleAppearanceSettings ());
});
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
/**
* @private
*/
getCueCss ( appearance , selector ) {
2020-07-22 22:42:26 +02:00
return ` ${ selector } ::cue {
2020-07-30 17:49:07 +03:00
${ appearance . text . map (( s ) => s . value !== undefined && s . value !== '' ? ` ${ s . name } : ${ s . value } !important;` : '' ). join ( '' ) }
2020-07-22 22:42:26 +02:00
}` ;
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
/**
* @private
*/
setCueAppearance () {
2020-08-16 20:24:45 +02:00
Promise . all ([ import ( '../../scripts/settings/userSettings' ), import ( '../../components/subtitlesettings/subtitleappearancehelper' )]). then (([ userSettings , subtitleAppearanceHelper ]) => {
2020-07-22 22:42:26 +02:00
const elementId = ` ${ this . id } -cuestyle` ;
2019-01-10 15:39:37 +03:00
2020-07-22 22:42:26 +02:00
let styleElem = document . querySelector ( `# ${ elementId } ` );
2019-01-10 15:39:37 +03:00
if ( ! styleElem ) {
styleElem = document . createElement ( 'style' );
styleElem . id = elementId ;
document . getElementsByTagName ( 'head' )[ 0 ]. appendChild ( styleElem );
}
2020-07-30 17:49:07 +03:00
styleElem . innerHTML = this . getCueCss ( subtitleAppearanceHelper . getStyles ( userSettings . getSubtitleAppearanceSettings ()), '.htmlvideoplayer' );
2019-01-10 15:39:37 +03:00
});
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
/**
* @private
*/
renderTracksEvents ( videoElement , track , item ) {
2018-10-23 01:05:09 +03:00
if ( ! itemHelper . isLocalItem ( item ) || track . IsExternal ) {
2020-07-21 22:22:16 +02:00
const format = ( track . Codec || '' ). toLowerCase ();
2019-01-10 15:39:37 +03:00
if ( format === 'ssa' || format === 'ass' ) {
2020-04-15 12:27:04 +02:00
this . renderSsaAss ( videoElement , track , item );
2019-01-10 15:39:37 +03:00
return ;
}
2020-04-15 12:27:04 +02:00
if ( this . requiresCustomSubtitlesElement ()) {
this . renderSubtitlesWithCustomElement ( videoElement , track , item );
2019-01-10 15:39:37 +03:00
return ;
}
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
let trackElement = null ;
2019-10-11 15:48:28 +02:00
if ( videoElement . textTracks && videoElement . textTracks . length > 0 ) {
trackElement = videoElement . textTracks [ 0 ];
2019-01-10 15:39:37 +03:00
2019-10-11 15:48:28 +02:00
// This throws an error in IE, but is fine in chrome
// In IE it's not necessary anyway because changing the src seems to be enough
try {
2019-01-10 15:39:37 +03:00
trackElement . mode = 'showing' ;
2019-10-11 15:48:28 +02:00
while ( trackElement . cues . length ) {
trackElement . removeCue ( trackElement . cues [ 0 ]);
}
} catch ( e ) {
2020-02-16 03:44:43 +01:00
console . error ( 'error removing cue from textTrack' );
2019-10-11 15:48:28 +02:00
}
trackElement . mode = 'disabled' ;
2019-01-10 15:39:37 +03:00
} else {
2019-10-11 15:48:28 +02:00
// There is a function addTextTrack but no function for removeTextTrack
// Therefore we add ONE element and replace its cue data
trackElement = videoElement . addTextTrack ( 'subtitles' , 'manualTrack' , 'und' );
2019-01-10 15:39:37 +03:00
}
2019-10-11 15:48:28 +02:00
// download the track json
2020-04-15 12:27:04 +02:00
this . fetchSubtitles ( track , item ). then ( function ( data ) {
2020-08-16 20:24:45 +02:00
import ( '../../scripts/settings/userSettings' ). then (( userSettings ) => {
2020-07-07 01:06:47 +03:00
// show in ui
2020-07-30 17:49:07 +03:00
console . debug ( `downloaded ${ data . TrackEvents . length } track events` );
2019-10-11 15:48:28 +02:00
2020-07-07 01:06:47 +03:00
const subtitleAppearance = userSettings . getSubtitleAppearanceSettings ();
2020-07-30 23:36:52 +03:00
const cueLine = parseInt ( subtitleAppearance . verticalPosition , 10 );
2019-10-11 15:48:28 +02:00
2020-07-07 01:06:47 +03:00
// add some cues to show the text
// in safari, the cues need to be added before setting the track mode to showing
2020-07-30 17:49:07 +03:00
for ( const trackEvent of data . TrackEvents ) {
const trackCueObject = window . VTTCue || window . TextTrackCue ;
const cue = new trackCueObject ( trackEvent . StartPositionTicks / 10000000 , trackEvent . EndPositionTicks / 10000000 , normalizeTrackEventText ( trackEvent . Text , false ));
2019-10-11 15:48:28 +02:00
2020-07-07 01:06:47 +03:00
if ( cue . line === 'auto' ) {
cue . line = cueLine ;
}
trackElement . addCue ( cue );
2020-07-30 17:49:07 +03:00
}
2020-07-07 01:06:47 +03:00
trackElement . mode = 'showing' ;
2019-10-11 15:48:28 +02:00
});
});
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
/**
* @private
*/
updateSubtitleText ( timeMs ) {
2020-07-22 22:42:26 +02:00
const clock = this . # currentClock ;
2019-01-10 15:39:37 +03:00
if ( clock ) {
try {
clock . seek ( timeMs / 1000 );
} catch ( err ) {
2020-07-22 22:42:26 +02:00
console . error ( `error in libjass: ${ err } ` );
2019-01-10 15:39:37 +03:00
}
return ;
}
2020-07-22 22:42:26 +02:00
const trackEvents = this . # currentTrackEvents ;
const subtitleTextElement = this . # videoSubtitlesElem ;
2019-01-10 15:39:37 +03:00
if ( trackEvents && subtitleTextElement ) {
2020-04-15 12:27:04 +02:00
const ticks = timeMs * 10000 ;
let selectedTrackEvent ;
2020-04-19 17:38:03 +02:00
for ( const trackEvent of trackEvents ) {
if ( trackEvent . StartPositionTicks <= ticks && trackEvent . EndPositionTicks >= ticks ) {
selectedTrackEvent = trackEvent ;
2019-01-10 15:39:37 +03:00
break ;
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
}
if ( selectedTrackEvent && selectedTrackEvent . Text ) {
2020-04-09 02:28:07 +03:00
subtitleTextElement . innerHTML = normalizeTrackEventText ( selectedTrackEvent . Text , true );
2019-01-10 15:39:37 +03:00
subtitleTextElement . classList . remove ( 'hide' );
} else {
subtitleTextElement . classList . add ( 'hide' );
2018-10-23 01:05:09 +03:00
}
}
}
2020-04-15 12:27:04 +02:00
/**
* @private
*/
setCurrentTrackElement ( streamIndex ) {
2020-07-22 22:42:26 +02:00
console . debug ( `setting new text track index to: ${ streamIndex } ` );
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
const mediaStreamTextTracks = getMediaStreamTextTracks ( this . _currentPlayOptions . mediaSource );
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
let track = streamIndex === - 1 ? null : mediaStreamTextTracks . filter ( function ( t ) {
2019-01-10 15:39:37 +03:00
return t . Index === streamIndex ;
})[ 0 ];
2020-07-22 22:42:26 +02:00
this . setTrackForDisplay ( this . # mediaElement , track );
if ( enableNativeTrackSupport ( this . # currentSrc , track )) {
2019-01-10 15:39:37 +03:00
if ( streamIndex !== - 1 ) {
2020-04-15 12:27:04 +02:00
this . setCueAppearance ();
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
} else {
// null these out to disable the player's native display (handled below)
streamIndex = - 1 ;
track = null ;
}
}
2020-04-15 12:27:04 +02:00
/**
* @private
*/
createMediaElement ( options ) {
2020-07-21 22:22:16 +02:00
const dlg = document . querySelector ( '.videoPlayerContainer' );
2019-01-10 15:39:37 +03:00
if ( ! dlg ) {
2020-08-16 20:24:45 +02:00
return import ( './style.css' ). then (() => {
2019-01-10 15:39:37 +03:00
loading . show ();
2020-07-21 22:22:16 +02:00
const dlg = document . createElement ( 'div' );
2019-01-10 15:39:37 +03:00
dlg . classList . add ( 'videoPlayerContainer' );
if ( options . fullscreen ) {
dlg . classList . add ( 'videoPlayerContainer-onTop' );
}
2020-07-21 22:22:16 +02:00
let html = '' ;
2020-07-16 22:19:09 +02:00
const cssClass = 'htmlvideoplayer' ;
2019-01-10 15:39:37 +03:00
// Can't autoplay in these browsers so we need to use the full controls, at least until playback starts
if ( ! appHost . supports ( 'htmlvideoautoplay' )) {
html += '<video class="' + cssClass + '" preload="metadata" autoplay="autoplay" controls="controls" webkit-playsinline playsinline>' ;
} else {
// Chrome 35 won't play with preload none
html += '<video class="' + cssClass + '" preload="metadata" autoplay="autoplay" webkit-playsinline playsinline>' ;
}
html += '</video>' ;
dlg . innerHTML = html ;
2020-07-21 22:22:16 +02:00
const videoElement = dlg . querySelector ( 'video' );
2019-01-10 15:39:37 +03:00
2020-07-26 14:18:34 +02:00
videoElement . volume = getSavedVolume ();
2020-04-15 12:27:04 +02:00
videoElement . addEventListener ( 'timeupdate' , this . onTimeUpdate );
videoElement . addEventListener ( 'ended' , this . onEnded );
videoElement . addEventListener ( 'volumechange' , this . onVolumeChange );
videoElement . addEventListener ( 'pause' , this . onPause );
videoElement . addEventListener ( 'playing' , this . onPlaying );
videoElement . addEventListener ( 'play' , this . onPlay );
videoElement . addEventListener ( 'click' , this . onClick );
videoElement . addEventListener ( 'dblclick' , this . onDblClick );
videoElement . addEventListener ( 'waiting' , this . onWaiting );
2020-05-08 00:07:59 +02:00
if ( options . backdropUrl ) {
videoElement . poster = options . backdropUrl ;
}
2019-01-10 15:39:37 +03:00
document . body . insertBefore ( dlg , document . body . firstChild );
2020-07-22 22:42:26 +02:00
this . # videoDialog = dlg ;
this . # mediaElement = videoElement ;
2019-01-10 15:39:37 +03:00
2020-09-26 22:48:55 +03:00
if ( options . fullscreen ) {
hidePrePlaybackPage ();
}
2019-01-10 15:39:37 +03:00
// don't animate on smart tv's, too slow
if ( options . fullscreen && browser . supportsCssAnimation () && ! browser . slow ) {
2020-07-27 18:43:59 +02:00
return zoomIn ( dlg ). then ( function () {
2020-04-15 12:27:04 +02:00
return videoElement ;
2019-01-10 15:39:37 +03:00
});
} else {
2020-04-15 12:27:04 +02:00
return videoElement ;
2019-01-10 15:39:37 +03:00
}
});
} else {
2020-09-26 22:48:55 +03:00
if ( options . fullscreen ) {
hidePrePlaybackPage ();
}
2020-04-15 12:27:04 +02:00
return Promise . resolve ( dlg . querySelector ( 'video' ));
2019-01-10 15:39:37 +03:00
}
}
2020-04-15 12:27:04 +02:00
/**
* @private
*/
canPlayMediaType ( mediaType ) {
return ( mediaType || '' ). toLowerCase () === 'video' ;
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
/**
* @private
*/
supportsPlayMethod ( playMethod , item ) {
2019-01-10 15:39:37 +03:00
if ( appHost . supportsPlayMethod ) {
return appHost . supportsPlayMethod ( playMethod , item );
}
return true ;
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
/**
* @private
*/
getDeviceProfile ( item , options ) {
return HtmlVideoPlayer . getDeviceProfileInternal ( item , options ). then (( profile ) => {
2020-07-22 22:42:26 +02:00
this . # lastProfile = profile ;
2019-01-10 15:39:37 +03:00
return profile ;
});
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
/**
* @private
*/
static getDeviceProfileInternal ( item , options ) {
2019-01-10 15:39:37 +03:00
if ( appHost . getDeviceProfile ) {
return appHost . getDeviceProfile ( item , options );
}
return getDefaultProfile ();
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
/**
* @private
*/
static getSupportedFeatures () {
const list = [];
2019-05-22 23:06:39 -07:00
2020-07-21 22:22:16 +02:00
const video = document . createElement ( 'video' );
2020-08-31 12:37:01 -04:00
if (
// Check non-standard Safari PiP support
typeof video . webkitSupportsPresentationMode === 'function' && video . webkitSupportsPresentationMode ( 'picture-in-picture' ) && typeof video . webkitSetPresentationMode === 'function'
// Check standard PiP support
|| document . pictureInPictureEnabled
) {
2019-01-10 15:39:37 +03:00
list . push ( 'PictureInPicture' );
} else if ( window . Windows ) {
if ( Windows . UI . ViewManagement . ApplicationView . getForCurrentView (). isViewModeSupported ( Windows . UI . ViewManagement . ApplicationViewMode . compactOverlay )) {
list . push ( 'PictureInPicture' );
}
}
2020-01-10 11:31:03 -05:00
if ( browser . safari || browser . iOS || browser . iPad ) {
2020-04-05 13:48:10 +02:00
list . push ( 'AirPlay' );
2020-01-10 11:31:03 -05:00
}
2020-05-05 12:01:43 +02:00
if ( typeof video . playbackRate === 'number' ) {
list . push ( 'PlaybackRate' );
2020-04-01 17:53:14 +02:00
}
2019-01-10 15:39:37 +03:00
list . push ( 'SetBrightness' );
2020-05-04 12:44:12 +02:00
list . push ( 'SetAspectRatio' );
2019-01-10 15:39:37 +03:00
return list ;
}
2020-04-15 12:27:04 +02:00
supports ( feature ) {
2020-07-22 22:42:26 +02:00
if ( ! this . # supportedFeatures ) {
this . # supportedFeatures = HtmlVideoPlayer . getSupportedFeatures ();
2019-01-10 15:39:37 +03:00
}
2020-07-22 22:42:26 +02:00
return this . # supportedFeatures . includes ( feature );
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
// Save this for when playback stops, because querying the time at that point might return 0
2020-04-15 12:27:04 +02:00
currentTime ( val ) {
2020-07-22 22:42:26 +02:00
const mediaElement = this . # mediaElement ;
2018-10-23 01:05:09 +03:00
if ( mediaElement ) {
2019-01-10 15:39:37 +03:00
if ( val != null ) {
mediaElement . currentTime = val / 1000 ;
return ;
}
2020-07-22 22:42:26 +02:00
const currentTime = this . # currentTime ;
2019-01-10 15:39:37 +03:00
if ( currentTime ) {
return currentTime * 1000 ;
}
return ( mediaElement . currentTime || 0 ) * 1000 ;
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
2020-07-27 18:54:04 +02:00
duration () {
2020-07-22 22:42:26 +02:00
const mediaElement = this . # mediaElement ;
2018-10-23 01:05:09 +03:00
if ( mediaElement ) {
2020-04-15 12:27:04 +02:00
const duration = mediaElement . duration ;
2020-07-26 14:18:34 +02:00
if ( isValidDuration ( duration )) {
2019-01-10 15:39:37 +03:00
return duration * 1000 ;
}
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
return null ;
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
2020-07-27 18:54:04 +02:00
canSetAudioStreamIndex () {
2019-01-10 15:39:37 +03:00
if ( browser . tizen || browser . orsay ) {
return true ;
}
2020-07-22 22:42:26 +02:00
const video = this . # mediaElement ;
2019-01-10 15:39:37 +03:00
if ( video ) {
if ( video . audioTracks ) {
return true ;
}
}
return false ;
}
2020-04-15 12:27:04 +02:00
static onPictureInPictureError ( err ) {
2020-07-22 22:42:26 +02:00
console . error ( `Picture in picture error: ${ err } ` );
2019-01-10 15:39:37 +03:00
}
2020-04-15 12:27:04 +02:00
setPictureInPictureEnabled ( isEnabled ) {
2020-07-22 22:42:26 +02:00
const video = this . # mediaElement ;
2019-01-10 15:39:37 +03:00
if ( document . pictureInPictureEnabled ) {
if ( video ) {
if ( isEnabled ) {
2020-04-15 12:27:04 +02:00
video . requestPictureInPicture (). catch ( HtmlVideoPlayer . onPictureInPictureError );
2019-01-10 15:39:37 +03:00
} else {
2020-04-15 12:27:04 +02:00
document . exitPictureInPicture (). catch ( HtmlVideoPlayer . onPictureInPictureError );
2019-01-10 15:39:37 +03:00
}
}
2019-05-22 23:06:39 -07:00
} else if ( window . Windows ) {
2019-01-10 15:39:37 +03:00
this . isPip = isEnabled ;
if ( isEnabled ) {
Windows . UI . ViewManagement . ApplicationView . getForCurrentView (). tryEnterViewModeAsync ( Windows . UI . ViewManagement . ApplicationViewMode . compactOverlay );
2019-11-23 00:29:38 +09:00
} else {
2019-01-10 15:39:37 +03:00
Windows . UI . ViewManagement . ApplicationView . getForCurrentView (). tryEnterViewModeAsync ( Windows . UI . ViewManagement . ApplicationViewMode . default );
}
2019-05-22 23:06:39 -07:00
} else {
2020-05-04 12:44:12 +02:00
if ( video && video . webkitSupportsPresentationMode && typeof video . webkitSetPresentationMode === 'function' ) {
video . webkitSetPresentationMode ( isEnabled ? 'picture-in-picture' : 'inline' );
2019-01-10 15:39:37 +03:00
}
}
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
isPictureInPictureEnabled () {
2019-01-10 15:39:37 +03:00
if ( document . pictureInPictureEnabled ) {
2020-07-22 22:42:26 +02:00
return !! document . pictureInPictureElement ;
2019-05-22 23:06:39 -07:00
} else if ( window . Windows ) {
2019-01-10 15:39:37 +03:00
return this . isPip || false ;
2019-05-22 23:06:39 -07:00
} else {
2020-07-22 22:42:26 +02:00
const video = this . # mediaElement ;
2019-01-10 15:39:37 +03:00
if ( video ) {
2020-05-04 12:44:12 +02:00
return video . webkitPresentationMode === 'picture-in-picture' ;
2019-01-10 15:39:37 +03:00
}
}
return false ;
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
static isAirPlayEnabled () {
2020-01-10 11:31:03 -05:00
if ( document . AirPlayEnabled ) {
2020-07-22 22:42:26 +02:00
return !! document . AirplayElement ;
2020-01-10 11:31:03 -05:00
}
return false ;
2020-04-15 12:27:04 +02:00
}
2020-01-10 11:31:03 -05:00
2020-04-15 12:27:04 +02:00
setAirPlayEnabled ( isEnabled ) {
2020-07-22 22:42:26 +02:00
const video = this . # mediaElement ;
2020-01-10 11:31:03 -05:00
if ( document . AirPlayEnabled ) {
if ( video ) {
if ( isEnabled ) {
2020-02-26 01:58:15 -05:00
video . requestAirPlay (). catch ( function ( err ) {
2020-05-04 12:44:12 +02:00
console . error ( 'Error requesting AirPlay' , err );
2020-02-26 01:58:15 -05:00
});
2020-01-10 11:31:03 -05:00
} else {
2020-02-26 01:58:15 -05:00
document . exitAirPLay (). catch ( function ( err ) {
2020-05-04 12:44:12 +02:00
console . error ( 'Error exiting AirPlay' , err );
2020-02-26 01:58:15 -05:00
});
2020-01-10 11:31:03 -05:00
}
}
} else {
video . webkitShowPlaybackTargetPicker ();
}
2020-04-15 12:27:04 +02:00
}
2020-01-10 11:31:03 -05:00
2020-04-15 12:27:04 +02:00
setBrightness ( val ) {
2020-07-22 22:42:26 +02:00
const elem = this . # mediaElement ;
2019-01-10 15:39:37 +03:00
2018-10-23 01:05:09 +03:00
if ( elem ) {
2019-01-10 15:39:37 +03:00
val = Math . max ( 0 , val );
val = Math . min ( 100 , val );
2020-04-15 12:27:04 +02:00
let rawValue = val ;
2018-10-23 01:05:09 +03:00
rawValue = Math . max ( 20 , rawValue );
2019-01-10 15:39:37 +03:00
2020-07-21 22:22:16 +02:00
const cssValue = rawValue >= 100 ? 'none' : ( rawValue / 100 );
2020-07-22 22:42:26 +02:00
elem . style [ '-webkit-filter' ] = `brightness( ${ cssValue } )` ;
elem . style . filter = `brightness( ${ cssValue } )` ;
2019-01-10 15:39:37 +03:00
elem . brightnessValue = val ;
2020-09-08 02:05:02 -04:00
Events . trigger ( this , 'brightnesschange' );
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
getBrightness () {
2020-07-22 22:42:26 +02:00
const elem = this . # mediaElement ;
2018-10-23 01:05:09 +03:00
if ( elem ) {
2020-04-15 12:27:04 +02:00
const val = elem . brightnessValue ;
2019-01-10 15:39:37 +03:00
return val == null ? 100 : val ;
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
seekable () {
2020-07-22 22:42:26 +02:00
const mediaElement = this . # mediaElement ;
2018-10-23 01:05:09 +03:00
if ( mediaElement ) {
2020-04-15 12:27:04 +02:00
const seekable = mediaElement . seekable ;
2018-10-23 01:05:09 +03:00
if ( seekable && seekable . length ) {
2020-04-15 12:27:04 +02:00
let start = seekable . start ( 0 );
let end = seekable . end ( 0 );
2019-01-10 15:39:37 +03:00
2020-07-26 14:18:34 +02:00
if ( ! isValidDuration ( start )) {
2019-01-10 15:39:37 +03:00
start = 0 ;
}
2020-07-26 14:18:34 +02:00
if ( ! isValidDuration ( end )) {
2019-01-10 15:39:37 +03:00
end = 0 ;
}
return ( end - start ) > 0 ;
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
return false ;
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
pause () {
2020-07-22 22:42:26 +02:00
const mediaElement = this . # mediaElement ;
2019-01-10 15:39:37 +03:00
if ( mediaElement ) {
mediaElement . pause ();
}
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
// This is a retry after error
2020-04-15 12:27:04 +02:00
resume () {
2020-07-22 22:42:26 +02:00
const mediaElement = this . # mediaElement ;
2019-01-10 15:39:37 +03:00
if ( mediaElement ) {
mediaElement . play ();
}
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
unpause () {
2020-07-22 22:42:26 +02:00
const mediaElement = this . # mediaElement ;
2019-01-10 15:39:37 +03:00
if ( mediaElement ) {
mediaElement . play ();
}
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
paused () {
2020-07-22 22:42:26 +02:00
const mediaElement = this . # mediaElement ;
2019-01-10 15:39:37 +03:00
if ( mediaElement ) {
return mediaElement . paused ;
}
return false ;
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
setPlaybackRate ( value ) {
2020-07-22 22:42:26 +02:00
const mediaElement = this . # mediaElement ;
2020-04-01 17:53:14 +02:00
if ( mediaElement ) {
mediaElement . playbackRate = value ;
}
2020-07-21 22:22:16 +02:00
}
2020-04-01 17:53:14 +02:00
2020-04-15 12:27:04 +02:00
getPlaybackRate () {
2020-07-22 22:42:26 +02:00
const mediaElement = this . # mediaElement ;
2020-04-01 17:53:14 +02:00
if ( mediaElement ) {
return mediaElement . playbackRate ;
}
return null ;
2020-07-21 22:22:16 +02:00
}
2020-04-01 17:53:14 +02:00
2020-05-22 22:23:18 -06:00
getSupportedPlaybackRates () {
return [{
name : '0.5x' ,
id : 0.5
}, {
name : '0.75x' ,
id : 0.75
}, {
name : '1x' ,
id : 1.0
}, {
name : '1.25x' ,
id : 1.25
}, {
name : '1.5x' ,
id : 1.5
}, {
name : '1.75x' ,
id : 1.75
}, {
name : '2x' ,
id : 2.0
}];
}
2020-04-15 12:27:04 +02:00
setVolume ( val ) {
2020-07-22 22:42:26 +02:00
const mediaElement = this . # mediaElement ;
2019-01-10 15:39:37 +03:00
if ( mediaElement ) {
mediaElement . volume = val / 100 ;
}
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
getVolume () {
2020-07-22 22:42:26 +02:00
const mediaElement = this . # mediaElement ;
2019-01-10 15:39:37 +03:00
if ( mediaElement ) {
return Math . min ( Math . round ( mediaElement . volume * 100 ), 100 );
}
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
volumeUp () {
2019-01-10 15:39:37 +03:00
this . setVolume ( Math . min ( this . getVolume () + 2 , 100 ));
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
volumeDown () {
2019-01-10 15:39:37 +03:00
this . setVolume ( Math . max ( this . getVolume () - 2 , 0 ));
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
setMute ( mute ) {
2020-07-22 22:42:26 +02:00
const mediaElement = this . # mediaElement ;
2019-01-10 15:39:37 +03:00
if ( mediaElement ) {
mediaElement . muted = mute ;
}
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
isMuted () {
2020-07-22 22:42:26 +02:00
const mediaElement = this . # mediaElement ;
2019-01-10 15:39:37 +03:00
if ( mediaElement ) {
return mediaElement . muted ;
}
return false ;
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
setAspectRatio ( val ) {
2020-07-22 22:42:26 +02:00
const mediaElement = this . # mediaElement ;
2019-11-20 15:13:15 +01:00
if ( mediaElement ) {
2020-07-30 16:07:13 +02:00
if ( val === 'auto' ) {
2020-05-04 12:44:12 +02:00
mediaElement . style . removeProperty ( 'object-fit' );
2019-11-20 15:13:15 +01:00
} else {
2020-05-04 12:44:12 +02:00
mediaElement . style [ 'object-fit' ] = val ;
2019-11-20 15:13:15 +01:00
}
}
2020-04-05 13:48:10 +02:00
this . _currentAspectRatio = val ;
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
getAspectRatio () {
2020-05-04 12:44:12 +02:00
return this . _currentAspectRatio || 'auto' ;
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
getSupportedAspectRatios () {
2019-11-20 15:13:15 +01:00
return [{
2020-05-04 12:44:12 +02:00
name : 'Auto' ,
id : 'auto'
2019-11-20 15:13:15 +01:00
}, {
2020-05-04 12:44:12 +02:00
name : 'Cover' ,
id : 'cover'
2019-11-20 15:13:15 +01:00
}, {
2020-05-04 12:44:12 +02:00
name : 'Fill' ,
id : 'fill'
2020-04-05 13:48:10 +02:00
}];
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
togglePictureInPicture () {
2019-01-10 15:39:37 +03:00
return this . setPictureInPictureEnabled ( ! this . isPictureInPictureEnabled ());
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
toggleAirPlay () {
2020-01-10 11:31:03 -05:00
return this . setAirPlayEnabled ( ! this . isAirPlayEnabled ());
2020-04-15 12:27:04 +02:00
}
2020-01-10 11:31:03 -05:00
2020-04-15 12:27:04 +02:00
getBufferedRanges () {
2020-07-22 22:42:26 +02:00
const mediaElement = this . # mediaElement ;
2019-01-10 15:39:37 +03:00
if ( mediaElement ) {
2020-07-26 14:18:34 +02:00
return getBufferedRanges ( this , mediaElement );
2019-01-10 15:39:37 +03:00
}
return [];
2020-04-15 12:27:04 +02:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
getStats () {
2020-07-22 22:42:26 +02:00
const mediaElement = this . # mediaElement ;
2020-04-15 12:27:04 +02:00
const playOptions = this . _currentPlayOptions || [];
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
const categories = [];
2019-01-10 15:39:37 +03:00
if ( ! mediaElement ) {
return Promise . resolve ({
categories : categories
});
}
2020-04-15 12:27:04 +02:00
const mediaCategory = {
2018-10-23 01:05:09 +03:00
stats : [],
2019-01-10 15:39:37 +03:00
type : 'media'
2018-10-23 01:05:09 +03:00
};
2019-01-10 15:39:37 +03:00
categories . push ( mediaCategory );
if ( playOptions . url ) {
// create an anchor element (note: no need to append this element to the document)
2020-07-21 22:22:16 +02:00
let link = document . createElement ( 'a' );
2019-01-10 15:39:37 +03:00
// set href to any path
link . setAttribute ( 'href' , playOptions . url );
2020-07-21 22:22:16 +02:00
const protocol = ( link . protocol || '' ). replace ( ':' , '' );
2019-01-10 15:39:37 +03:00
if ( protocol ) {
mediaCategory . stats . push ({
2020-05-04 12:44:12 +02:00
label : globalize . translate ( 'LabelProtocol' ),
2019-01-10 15:39:37 +03:00
value : protocol
});
}
link = null ;
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
2020-07-23 22:54:34 +02:00
if ( this . _hlsPlayer ) {
2019-01-10 15:39:37 +03:00
mediaCategory . stats . push ({
2020-05-04 12:44:12 +02:00
label : globalize . translate ( 'LabelStreamType' ),
2019-01-10 15:39:37 +03:00
value : 'HLS'
});
} else {
mediaCategory . stats . push ({
2020-05-04 12:44:12 +02:00
label : globalize . translate ( 'LabelStreamType' ),
2019-01-10 15:39:37 +03:00
value : 'Video'
});
}
2020-04-15 12:27:04 +02:00
const videoCategory = {
2018-10-23 01:05:09 +03:00
stats : [],
2019-01-10 15:39:37 +03:00
type : 'video'
2018-10-23 01:05:09 +03:00
};
categories . push ( videoCategory );
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
const rect = mediaElement . getBoundingClientRect ? mediaElement . getBoundingClientRect () : {};
let height = parseInt ( rect . height );
let width = parseInt ( rect . width );
2019-01-10 15:39:37 +03:00
// Don't show player dimensions on smart TVs because the app UI could be lower resolution than the video and this causes users to think there is a problem
if ( width && height && ! browser . tv ) {
2018-10-23 01:05:09 +03:00
videoCategory . stats . push ({
2020-05-04 12:44:12 +02:00
label : globalize . translate ( 'LabelPlayerDimensions' ),
2020-07-22 22:42:26 +02:00
value : ` ${ width } x ${ height } `
2019-01-10 15:39:37 +03:00
});
}
height = mediaElement . videoHeight ;
width = mediaElement . videoWidth ;
if ( width && height ) {
videoCategory . stats . push ({
2020-05-04 12:44:12 +02:00
label : globalize . translate ( 'LabelVideoResolution' ),
2020-07-22 22:42:26 +02:00
value : ` ${ width } x ${ height } `
2019-01-10 15:39:37 +03:00
});
}
if ( mediaElement . getVideoPlaybackQuality ) {
2020-04-15 12:27:04 +02:00
const playbackQuality = mediaElement . getVideoPlaybackQuality ();
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
const droppedVideoFrames = playbackQuality . droppedVideoFrames || 0 ;
2019-01-10 15:39:37 +03:00
videoCategory . stats . push ({
2020-05-04 12:44:12 +02:00
label : globalize . translate ( 'LabelDroppedFrames' ),
2018-10-23 01:05:09 +03:00
value : droppedVideoFrames
});
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
const corruptedVideoFrames = playbackQuality . corruptedVideoFrames || 0 ;
2018-10-23 01:05:09 +03:00
videoCategory . stats . push ({
2020-05-04 12:44:12 +02:00
label : globalize . translate ( 'LabelCorruptedFrames' ),
2018-10-23 01:05:09 +03:00
value : corruptedVideoFrames
2019-01-10 15:39:37 +03:00
});
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
const audioCategory = {
2018-10-23 01:05:09 +03:00
stats : [],
2019-01-10 15:39:37 +03:00
type : 'audio'
2018-10-23 01:05:09 +03:00
};
categories . push ( audioCategory );
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
const sinkId = mediaElement . sinkId ;
2019-01-10 15:39:37 +03:00
if ( sinkId ) {
audioCategory . stats . push ({
label : 'Sink Id:' ,
value : sinkId
});
}
return Promise . resolve ({
2018-10-23 01:05:09 +03:00
categories : categories
2019-01-10 15:39:37 +03:00
});
2020-04-15 12:27:04 +02:00
}
}
/* eslint-enable indent */
2019-01-10 15:39:37 +03:00
2020-04-19 23:23:46 +02:00
export default HtmlVideoPlayer ;