2020-04-15 12:27:04 +02:00
import browser from "browser" ;
import events from "events" ;
import appHost from "apphost" ;
import loading from "loading" ;
import dom from "dom" ;
import playbackManager from "playbackManager" ;
import appRouter from "appRouter" ;
import connectionManager from "connectionManager" ;
import htmlMediaHelper from "htmlMediaHelper" ;
import itemHelper from "itemHelper" ;
import screenfull from "screenfull" ;
import globalize from "globalize" ;
/* eslint-disable indent */
2020-02-26 01:58:15 -05:00
/* globals cast */
2018-10-23 01:05:09 +03:00
2020-04-15 12:27:04 +02:00
let mediaManager ;
2019-01-10 15:39:37 +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-02-16 03:44:43 +01:00
console . error ( 'error removing dialog element: ' + err ) ;
2019-01-10 15:39:37 +03:00
}
}
}
2020-04-15 12:27:04 +02:00
let _supportsTextTracks ;
function supportsTextTracks ( ) {
2019-01-10 15:39:37 +03:00
if ( _supportsTextTracks == null ) {
_supportsTextTracks = document . createElement ( 'video' ) . textTracks != null ;
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
// For now, until ready
return _supportsTextTracks ;
2018-10-23 01:05:09 +03:00
}
2019-09-27 10:52:15 -04:00
function supportsCanvas ( ) {
return ! ! document . createElement ( 'canvas' ) . getContext ;
}
function supportsWebWorkers ( ) {
return ! ! window . Worker ;
}
2018-10-23 01:05:09 +03:00
function enableNativeTrackSupport ( currentSrc , track ) {
2019-01-10 15:39:37 +03:00
if ( track ) {
if ( track . DeliveryMethod === 'Embed' ) {
return true ;
}
}
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 ;
}
}
// subs getting blocked due to CORS
if ( browser . chromecast ) {
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 ;
}
}
2018-10-23 01:05:09 +03:00
if ( track ) {
2020-04-15 12:27:04 +02:00
const format = ( track . Codec || "" ) . toLowerCase ( ) ;
2019-01-10 15:39:37 +03:00
if ( format === 'ssa' || format === 'ass' ) {
return false ;
}
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
return true ;
2018-10-23 01:05:09 +03:00
}
function requireHlsPlayer ( callback ) {
2019-01-10 15:39:37 +03:00
require ( [ 'hlsjs' ] , function ( hls ) {
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 ( ) {
let animatedPage = document . querySelector ( '.page:not(.hide)' ) ;
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 ) {
2019-01-10 15:39:37 +03:00
return new Promise ( function ( resolve , reject ) {
2020-04-15 12:27:04 +02:00
const duration = 240 ;
2019-01-10 15:39:37 +03:00
elem . style . animation = 'htmlvideoplayer-zoomin ' + duration + 'ms ease-in normal' ;
2020-05-10 01:08:08 +02:00
hidePrePlaybackPage ( ) ;
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-04-15 12:27:04 +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 setTracks ( elem , tracks , item , mediaSource ) {
2019-01-10 15:39:37 +03:00
elem . innerHTML = getTracksHtml ( tracks , item , mediaSource ) ;
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 getTracksHtml ( tracks , item , mediaSource ) {
2019-01-10 15:39:37 +03:00
return tracks . map ( function ( t ) {
if ( t . DeliveryMethod !== 'External' ) {
return '' ;
}
2020-04-15 12:27:04 +02:00
const defaultAttribute = mediaSource . DefaultSubtitleStreamIndex === t . Index ? " default" : "" ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
const language = t . Language || "und" ;
const label = t . Language || "und" ;
2019-01-10 15:39:37 +03:00
return '<track id="textTrack' + t . Index + '" label="' + label + '" kind="subtitles" src="' + getTextTrackUrl ( t , item ) + '" srclang="' + language + '"' + defaultAttribute + '></track>' ;
} ) . join ( '' ) ;
2018-10-23 01:05:09 +03:00
}
function getDefaultProfile ( ) {
2020-04-15 12:27:04 +02:00
return import ( "browserdeviceprofile" ) . then ( profileBuilder => {
2019-01-10 15:39:37 +03:00
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
/ * *
* Private :
* - videoDialog
* - winJsPlaybackItem
* - subtitleTrackIndexToSetOnPlaying
* - audioTrackIndexToSetOnPlaying
* - lastCustomTrackMs
* - currentClock
* - currentSubtitlesOctopus
* - currentAssRenderer
* - customTrackIndex
* - showTrackOffset
* - currentTrackOffset
* - videoSubtitlesElem
* - currentTrackEvents
* - supportedFeatures
* /
export class HtmlVideoPlayer {
constructor ( ) {
if ( browser . edgeUwp ) {
this . name = 'Windows Video Player' ;
} else {
this . name = 'Html Video Player' ;
}
2019-03-30 22:11:39 +01:00
2020-04-15 12:27:04 +02:00
this . type = 'mediaplayer' ;
this . id = 'htmlvideoplayer' ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
// Let any players created by plugins take priority
this . priority = 1 ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
this . _fetchQueue = 0 ;
this . isFetching = false ;
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
currentSrc ( ) {
return this . _currentSrc ;
}
2019-09-12 21:24:16 +02:00
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
incrementFetchQueue ( ) {
if ( this . _fetchQueue <= 0 ) {
this . isFetching = true ;
events . trigger ( this , "beginFetch" ) ;
2019-09-12 21:24:16 +02:00
}
2020-04-15 12:27:04 +02:00
this . _fetchQueue ++ ;
2019-09-12 21:24:16 +02:00
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
decrementFetchQueue ( ) {
this . _fetchQueue -- ;
2019-09-12 21:24:16 +02:00
2020-04-15 12:27:04 +02:00
if ( this . _fetchQueue <= 0 ) {
this . isFetching = false ;
events . trigger ( this , "endFetch" ) ;
2019-09-12 21:24:16 +02:00
}
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
updateVideoUrl ( streamInfo ) {
2020-04-19 23:23:46 +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-04-15 12:27:04 +02:00
const hlsPlaylistUrl = streamInfo . url . replace ( "master.m3u8" , "live.m3u8" ) ;
2019-01-10 15:39:37 +03:00
loading . show ( ) ;
2020-02-16 03:44:43 +01:00
console . debug ( 'prefetching hls playlist: ' + hlsPlaylistUrl ) ;
2019-01-10 15:39:37 +03:00
return connectionManager . getApiClient ( item . ServerId ) . ajax ( {
type : 'GET' ,
2018-10-23 01:05:09 +03:00
url : hlsPlaylistUrl
2019-01-10 15:39:37 +03:00
} ) . then ( function ( ) {
2020-02-16 03:44:43 +01:00
console . debug ( 'completed prefetching hls playlist: ' + hlsPlaylistUrl ) ;
2019-01-10 15:39:37 +03:00
loading . hide ( ) ;
streamInfo . url = hlsPlaylistUrl ;
} , function ( ) {
2020-02-16 03:44:43 +01:00
console . error ( 'error prefetching hls playlist: ' + hlsPlaylistUrl ) ;
2019-01-10 15:39:37 +03:00
loading . hide ( ) ;
} ) ;
} else {
return Promise . resolve ( ) ;
2018-10-23 01:05:09 +03:00
}
}
2020-04-15 12:27:04 +02:00
play ( options ) {
this . _started = false ;
this . _timeUpdated = false ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
this . _currentTime = null ;
2019-11-30 20:01:32 +01:00
2020-04-15 12:27:04 +02:00
this . resetSubtitleOffset ( ) ;
2019-01-10 15:39:37 +03: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
} ) ;
} ) ;
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
* /
setSrcWithFlvJs ( instance , elem , options , url ) {
return import ( 'flvjs' ) . then ( flvjs => {
const flvPlayer = flvjs . createPlayer ( {
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
{
2020-04-15 12:27:04 +02:00
seekType : "range" ,
2019-11-23 00:29:38 +09:00
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-04-15 12:27:04 +02:00
instance . _flvPlayer = flvPlayer ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
// This is needed in setCurrentTrackElement
this . _currentSrc = url ;
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
* /
setSrcWithHlsJs ( instance , elem , options , url ) {
2019-01-10 15:39:37 +03:00
2020-04-19 23:23:46 +02:00
return new Promise ( ( resolve , reject ) => {
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
requireHlsPlayer ( ( ) => {
const hls = new Hls ( {
2020-05-07 21:04:26 -07:00
manifestLoadingTimeOut : 20000 ,
2020-04-15 12:27:04 +02:00
xhrSetup ( xhr , xhr _url ) {
2020-05-07 21:04:26 -07:00
xhr . withCredentials = true ;
}
2019-01-10 15:39:37 +03:00
//appendErrorMaxRetry: 6,
//debug: true
2018-10-23 01:05:09 +03:00
} ) ;
2019-01-10 15:39:37 +03:00
hls . loadSource ( url ) ;
hls . attachMedia ( elem ) ;
2020-04-15 12:27:04 +02:00
htmlMediaHelper . bindEventsToHlsPlayer ( this , hls , elem , this . onError . bind ( this ) , 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-04-15 12:27:04 +02:00
this . _currentSrc = url ;
2019-01-10 15:39:37 +03:00
} ) ;
} ) ;
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
onShakaError ( event ) {
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
const error = event . detail ;
2019-01-10 15:39:37 +03:00
console . error ( 'Error code' , error . code , 'object' , error ) ;
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
setSrcWithShakaPlayer ( instance , elem , options , url ) {
return import ( 'shaka' ) . then ( ( ) => {
/* globals shaka */
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
const player = new shaka . Player ( elem ) ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
//player.configure({
// abr: {
// enabled: false
// },
// streaming: {
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
// failureCallback: function () {
// alert(2);
// }
// }
//});
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
//shaka.log.setLevel(6);
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
// Listen for error events.
player . addEventListener ( 'error' , this . onShakaError . bind ( this ) ) ;
2019-01-10 15:39:37 +03:00
2020-04-19 17:38:03 +02:00
this . _shakaPlayer = player ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
// This is needed in setCurrentTrackElement
2020-04-19 17:38:03 +02:00
this . _currentSrc = url ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
// Try to load a manifest.
// This is an asynchronous process.
return player . load ( url ) ;
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
* /
setCurrentSrcChromecast ( instance , elem , options , url ) {
2019-01-10 15:39:37 +03:00
elem . autoplay = true ;
2020-04-15 12:27:04 +02:00
const lrd = new cast . receiver . MediaManager . LoadRequestData ( ) ;
2019-01-10 15:39:37 +03:00
lrd . currentTime = ( options . playerStartPositionTicks || 0 ) / 10000000 ;
lrd . autoplay = true ;
lrd . media = new cast . receiver . media . MediaInformation ( ) ;
lrd . media . contentId = url ;
lrd . media . contentType = options . mimeType ;
lrd . media . streamType = cast . receiver . media . StreamType . OTHER ;
lrd . media . customData = options ;
2020-02-17 20:41:04 +01:00
console . debug ( 'loading media url into media manager' ) ;
2019-01-10 15:39:37 +03:00
2018-10-23 01:05:09 +03:00
try {
2019-01-10 15:39:37 +03:00
mediaManager . load ( lrd ) ;
// This is needed in setCurrentTrackElement
2020-04-15 12:27:04 +02:00
this . _currentSrc = url ;
2019-01-10 15:39:37 +03:00
return Promise . resolve ( ) ;
2018-10-23 01:05:09 +03:00
} catch ( err ) {
2019-01-10 15:39:37 +03:00
2020-02-17 20:41:04 +01:00
console . debug ( 'media manager error: ' + err ) ;
2019-01-10 15:39:37 +03:00
return Promise . reject ( ) ;
2018-10-23 01:05:09 +03:00
}
}
2020-04-15 12:27:04 +02:00
/ * *
* Adapted from : https : //github.com/googlecast/CastReferencePlayer/blob/master/player.js
* @ private
* /
onMediaManagerLoadMedia ( event ) {
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
if ( this . _castPlayer ) {
this . _castPlayer . unload ( ) ; // Must unload before starting again.
2019-01-10 15:39:37 +03:00
}
2020-04-15 12:27:04 +02:00
this . _castPlayer = null ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
const data = event . data ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
const media = event . data . media || { } ;
const url = media . contentId ;
const contentType = media . contentType . toLowerCase ( ) ;
const options = media . customData ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
let protocol ;
const ext = "m3u8" ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
const mediaElement = this . _mediaElement ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
const host = new cast . player . api . Host ( {
"url" : url ,
"mediaElement" : mediaElement
2019-01-10 15:39:37 +03:00
} ) ;
if ( ext === 'm3u8' ||
contentType === 'application/x-mpegurl' ||
contentType === 'application/vnd.apple.mpegurl' ) {
protocol = cast . player . api . CreateHlsStreamingProtocol ( host ) ;
} else if ( ext === 'mpd' ||
contentType === 'application/dash+xml' ) {
protocol = cast . player . api . CreateDashStreamingProtocol ( host ) ;
2020-04-19 23:23:46 +02:00
} else if ( url . includes ( '.ism' ) ||
2019-01-10 15:39:37 +03:00
contentType === 'application/vnd.ms-sstr+xml' ) {
protocol = cast . player . api . CreateSmoothStreamingProtocol ( host ) ;
}
2020-02-16 03:44:43 +01:00
console . debug ( 'loading playback url: ' + url ) ;
2020-02-17 20:41:04 +01:00
console . debug ( 'content type: ' + contentType ) ;
2019-01-10 15:39:37 +03:00
host . onError = function ( errorCode ) {
2020-05-04 12:44:12 +02:00
console . error ( 'fatal Error - ' + errorCode ) ;
2019-01-10 15:39:37 +03:00
} ;
mediaElement . autoplay = false ;
2020-04-15 12:27:04 +02:00
this . _castPlayer = new cast . player . api . Player ( host ) ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
this . _castPlayer . load ( protocol , data . currentTime || 0 ) ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
this . _castPlayer . playWhenHaveEnoughData ( ) ;
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
initMediaManager ( ) {
2019-01-10 15:39:37 +03:00
mediaManager . defaultOnLoad = mediaManager . onLoad . bind ( mediaManager ) ;
2020-04-15 12:27:04 +02:00
mediaManager . onLoad = this . onMediaManagerLoadMedia . bind ( this ) ;
2019-01-10 15:39:37 +03:00
//mediaManager.defaultOnPlay = mediaManager.onPlay.bind(mediaManager);
//mediaManager.onPlay = function (event) {
// // TODO ???
// mediaManager.defaultOnPlay(event);
//};
mediaManager . defaultOnStop = mediaManager . onStop . bind ( mediaManager ) ;
mediaManager . onStop = function ( event ) {
playbackManager . stop ( ) ;
mediaManager . defaultOnStop ( event ) ;
} ;
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
setCurrentSrc ( elem , options ) {
elem . removeEventListener ( 'error' , this . onError . bind ( this ) ) ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
let val = options . url ;
2020-02-16 03:44:43 +01: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 ) {
val += '#t=' + seconds ;
}
2020-04-15 12:27:04 +02:00
htmlMediaHelper . destroyHlsPlayer ( this ) ;
htmlMediaHelper . destroyFlvPlayer ( this ) ;
htmlMediaHelper . destroyCastPlayer ( this ) ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
const tracks = getMediaStreamTextTracks ( options . mediaSource ) ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +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-04-15 12:27:04 +02:00
this . subtitleTrackIndexToSetOnPlaying = - 1 ;
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
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-04-15 12:27:04 +02:00
const crossOrigin = htmlMediaHelper . getCrossOriginValue ( options . mediaSource ) ;
2019-01-10 15:39:37 +03:00
if ( crossOrigin ) {
elem . crossOrigin = crossOrigin ;
}
2018-10-23 01:05:09 +03:00
2020-04-19 23:23:46 +02:00
/ * i f ( h t m l M e d i a H e l p e r . e n a b l e H l s S h a k a P l a y e r ( o p t i o n s . i t e m , o p t i o n s . m e d i a S o u r c e , ' V i d e o ' ) & & v a l . i n c l u d e s ( ' . m 3 u 8 ' ) ) {
2018-10-23 01:05:09 +03:00
2019-01-10 15:39:37 +03:00
setTracks ( elem , tracks , options . item , options . mediaSource ) ;
2018-10-23 01:05:09 +03:00
2020-04-19 17:38:03 +02:00
return setSrcWithShakaPlayer ( this , elem , options , val ) ;
2018-10-23 01:05:09 +03:00
2020-04-15 12:27:04 +02:00
} else * /
2020-04-19 23:23:46 +02:00
if ( browser . chromecast && val . includes ( '.m3u8' ) && options . mediaSource . RunTimeTicks ) {
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
return this . setCurrentSrcChromecast ( this , elem , options , val ) ;
2020-04-19 23:23:46 +02:00
} else if ( htmlMediaHelper . enableHlsJsPlayer ( options . mediaSource . RunTimeTicks , 'Video' ) && val . includes ( '.m3u8' ) ) {
2020-04-15 12:27:04 +02:00
return this . setSrcWithHlsJs ( this , elem , options , val ) ;
2019-01-10 15:39:37 +03:00
} else if ( options . playMethod !== 'Transcode' && options . mediaSource . Container === 'flv' ) {
2020-04-15 12:27:04 +02:00
return this . setSrcWithFlvJs ( this , 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-04-15 12:27:04 +02:00
return htmlMediaHelper . applySrc ( elem , val , options ) . then ( ( ) => {
this . _currentSrc = val ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
return htmlMediaHelper . playWithPromise ( elem , this . onError . bind ( this ) ) ;
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 ) ;
}
2018-10-23 01:05:09 +03:00
2020-04-15 12:27:04 +02:00
resetSubtitleOffset ( ) {
this . currentTrackOffset = 0 ;
this . showTrackOffset = false ;
}
2019-11-30 20:01:32 +01:00
2020-04-15 12:27:04 +02:00
enableShowingSubtitleOffset ( ) {
this . showTrackOffset = true ;
}
2019-04-08 20:21:16 +02:00
2020-04-15 12:27:04 +02:00
disableShowingSubtitleOffset ( ) {
this . showTrackOffset = false ;
}
2019-04-08 20:21:16 +02:00
2020-04-15 12:27:04 +02:00
isShowingSubtitleOffsetEnabled ( ) {
return this . showTrackOffset ;
}
2019-04-08 20:21:16 +02:00
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
getTextTrack ( ) {
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-04-15 12:27:04 +02:00
if ( this . currentSubtitlesOctopus ) {
this . updateCurrentTrackOffset ( offsetValue ) ;
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 ) ;
} 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 ;
if ( this . currentTrackOffset ) {
relativeOffset -= this . currentTrackOffset ;
2019-03-30 22:11:39 +01:00
}
2020-04-15 12:27:04 +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
}
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 ( ) {
return this . currentTrackOffset ;
}
2019-04-08 20:21:16 +02:00
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
isAudioStreamSupported ( stream , deviceProfile ) {
2018-10-23 01:05:09 +03:00
2020-04-15 12:27:04 +02:00
const codec = ( stream . Codec || "" ) . toLowerCase ( ) ;
2019-01-10 15:39:37 +03:00
if ( ! codec ) {
return true ;
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
if ( ! deviceProfile ) {
// This should never happen
return true ;
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 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 ) ;
2018-10-23 01:05:09 +03:00
}
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-04-19 17:38:03 +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 ) {
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
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 ;
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 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-04-19 17:38:03 +02:00
const elem = this . _mediaElement ;
2019-01-10 15:39:37 +03:00
if ( ! elem ) {
return ;
}
// https://msdn.microsoft.com/en-us/library/hh772507(v=vs.85).aspx
2020-04-15 12:27:04 +02:00
const elemAudioTracks = elem . audioTracks || [ ] ;
2020-02-16 03:44:43 +01:00
console . debug ( 'found ' + elemAudioTracks . length + ' audio tracks' ) ;
2019-01-10 15:39:37 +03:00
2020-04-19 17:38:03 +02:00
elemAudioTracks . forEach ( ( audioTrack , i ) => {
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-19 17:38:03 +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
stop ( destroyPlayer ) {
const elem = this . _mediaElement ;
const src = this . _currentSrc ;
2019-01-10 15:39:37 +03:00
if ( elem ) {
if ( src ) {
elem . pause ( ) ;
}
2020-04-15 12:27:04 +02:00
htmlMediaHelper . onEndedInternal ( this , elem , this . onError . bind ( this ) ) ;
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 ( ) {
htmlMediaHelper . destroyHlsPlayer ( this ) ;
htmlMediaHelper . destroyFlvPlayer ( this ) ;
2019-01-10 15:39:37 +03:00
appRouter . setTransparency ( 'none' ) ;
2020-04-15 12:27:04 +02:00
const videoElement = this . _mediaElement ;
2019-01-10 15:39:37 +03:00
if ( videoElement ) {
2020-04-15 12:27:04 +02:00
this . _mediaElement = null ;
this . destroyCustomTrack ( videoElement ) ;
videoElement . removeEventListener ( 'timeupdate' , this . onTimeUpdate . bind ( this ) ) ;
videoElement . removeEventListener ( 'ended' , this . onEnded . bind ( this ) ) ;
videoElement . removeEventListener ( 'volumechange' , this . onVolumeChange . bind ( this ) ) ;
videoElement . removeEventListener ( 'pause' , this . onPause . bind ( this ) ) ;
videoElement . removeEventListener ( 'playing' , this . onPlaying . bind ( this ) ) ;
videoElement . removeEventListener ( 'play' , this . onPlay . bind ( this ) ) ;
videoElement . removeEventListener ( 'click' , this . onClick . bind ( this ) ) ;
videoElement . removeEventListener ( 'dblclick' , this . onDblClick . bind ( this ) ) ;
videoElement . removeEventListener ( 'waiting' , this . onWaiting . bind ( this ) ) ;
2019-01-10 15:39:37 +03:00
videoElement . parentNode . removeChild ( videoElement ) ;
}
2020-04-15 12:27:04 +02:00
const dlg = this . videoDialog ;
2019-01-10 15:39:37 +03:00
if ( dlg ) {
2020-04-15 12:27:04 +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-04-26 16:16:48 +02:00
if ( screenfull . isEnabled ) {
screenfull . exit ( ) ;
}
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
* /
onEnded ( ) {
this . destroyCustomTrack ( this ) ;
htmlMediaHelper . onEndedInternal ( this , this . onEnded . bind ( this ) , this . onError . bind ( this ) ) ;
2019-01-10 15:39:37 +03:00
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
onTimeUpdate ( e ) {
2019-05-22 23:06:39 -07:00
// get the player position and the transcoding offset
2020-04-15 12:27:04 +02:00
const time = this . currentTime ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
if ( time && ! this . _timeUpdated ) {
this . _timeUpdated = true ;
this . ensureValidVideo ( this ) ;
2019-01-10 15:39:37 +03:00
}
2020-04-15 12:27:04 +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-04-15 12:27:04 +02:00
events . trigger ( this , 'timeupdate' ) ;
2019-01-10 15:39:37 +03:00
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
onVolumeChange ( ) {
2019-01-10 15:39:37 +03:00
htmlMediaHelper . saveVolume ( this . volume ) ;
2020-04-15 12:27:04 +02:00
events . trigger ( this , 'volumechange' ) ;
2019-01-10 15:39:37 +03:00
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
onNavigatedToOsd ( ) {
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-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-04-15 12:27:04 +02:00
this . setCurrentTrackElement ( this . subtitleTrackIndexToSetOnPlaying ) ;
2019-01-10 15:39:37 +03:00
2020-04-19 17:38:03 +02:00
if ( this . audioTrackIndexToSetOnPlaying != null && this . canSetAudioStreamIndex ( ) ) {
this . setAudioStreamIndex ( this . audioTrackIndexToSetOnPlaying ) ;
2019-01-10 15:39:37 +03:00
}
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
onPlaying ( e ) {
if ( ! this . _started ) {
this . _started = true ;
2020-04-19 23:23:46 +02:00
this . _mediaElement . removeAttribute ( 'controls' ) ;
2019-01-10 15:39:37 +03:00
loading . hide ( ) ;
2020-04-15 12:27:04 +02:00
htmlMediaHelper . seekOnPlaybackStart ( this , e . target , this . _currentPlayOptions . playerStartPositionTicks , function ( ) {
if ( this . currentSubtitlesOctopus ) {
this . currentSubtitlesOctopus . timeOffset = ( this . _currentPlayOptions . transcodingOffsetTicks || 0 ) / 10000000 + 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 ) {
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
appRouter . showVideoOsd ( ) . then ( this . onNavigatedToOsd . bind ( this ) ) ;
2019-01-10 15:39:37 +03:00
} else {
appRouter . setTransparency ( 'backdrop' ) ;
2020-04-15 12:27:04 +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-04-15 12:27:04 +02:00
events . trigger ( this , 'playing' ) ;
2019-01-10 15:39:37 +03:00
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
onPlay ( e ) {
events . trigger ( this , 'unpause' ) ;
2019-01-10 15:39:37 +03:00
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
ensureValidVideo ( elem ) {
if ( elem !== this . _mediaElement ) {
2019-01-10 15:39:37 +03:00
return ;
}
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-04-15 12:27:04 +02:00
htmlMediaHelper . onErrorInternal ( this , 'mediadecodeerror' ) ;
2019-01-10 15:39:37 +03:00
}
}
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
onClick ( ) {
events . trigger ( this , 'click' ) ;
2019-01-10 15:39:37 +03:00
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
onDblClick ( ) {
events . trigger ( this , 'dblclick' ) ;
2019-01-10 15:39:37 +03:00
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
onPause ( ) {
events . trigger ( this , 'pause' ) ;
2019-01-10 15:39:37 +03:00
}
2020-04-15 12:27:04 +02:00
onWaiting ( ) {
events . trigger ( this , 'waiting' ) ;
2020-04-01 17:53:14 +02:00
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
onError ( ) {
const errorCode = this . onError . error ? ( this . onError . error . code || 0 ) : 0 ;
const errorMessage = this . onError . error ? ( this . onError . error . message || "" ) : "" ;
2020-02-17 20:41:04 +01:00
console . error ( 'media element error: ' + errorCode . toString ( ) + ' ' + 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
switch ( errorCode ) {
case 1 :
// MEDIA_ERR_ABORTED
// This will trigger when changing media while something is playing
return ;
case 2 :
// MEDIA_ERR_NETWORK
type = 'network' ;
break ;
case 3 :
// MEDIA_ERR_DECODE
2020-04-19 17:38:03 +02:00
if ( this . _hlsPlayer ) {
htmlMediaHelper . handleHlsJsMediaError ( this ) ;
2019-01-10 15:39:37 +03:00
return ;
} else {
type = 'mediadecodeerror' ;
}
break ;
case 4 :
// MEDIA_ERR_SRC_NOT_SUPPORTED
type = 'medianotsupported' ;
break ;
default :
// 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 ;
}
2020-04-15 12:27:04 +02:00
htmlMediaHelper . onErrorInternal ( this , type ) ;
2019-01-10 15:39:37 +03:00
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
destroyCustomTrack ( videoElement ) {
if ( this . _resizeObserver ) {
this . _resizeObserver . disconnect ( ) ;
this . _resizeObserver = null ;
2019-01-10 15:39:37 +03:00
}
2020-04-15 12:27:04 +02:00
if ( this . videoSubtitlesElem ) {
const subtitlesContainer = this . videoSubtitlesElem . parentNode ;
2019-01-10 15:39:37 +03:00
if ( subtitlesContainer ) {
tryRemoveElement ( subtitlesContainer ) ;
}
2020-04-15 12:27:04 +02:00
this . videoSubtitlesElem = null ;
2019-01-10 15:39:37 +03:00
}
2020-04-15 12:27:04 +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-04-15 12:27:04 +02:00
this . customTrackIndex = - 1 ;
this . currentClock = null ;
this . _currentAspectRatio = null ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
const octopus = this . currentSubtitlesOctopus ;
2019-09-27 10:52:15 -04:00
if ( octopus ) {
octopus . dispose ( ) ;
}
2020-04-15 12:27:04 +02:00
this . currentSubtitlesOctopus = null ;
2019-09-27 10:52:15 -04:00
2020-04-15 12:27:04 +02:00
const renderer = this . currentAssRenderer ;
2019-01-10 15:39:37 +03:00
if ( renderer ) {
renderer . setEnabled ( false ) ;
}
2020-04-15 12:27:04 +02:00
this . currentAssRenderer = null ;
2019-01-10 15:39:37 +03:00
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
fetchSubtitlesUwp ( track , item ) {
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
} ) ;
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
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 ( ) ;
return new Promise ( ( resolve , reject ) => {
const xhr = new XMLHttpRequest ( ) ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
const url = getTextTrackUrl ( track , item , ".js" ) ;
2019-01-10 15:39:37 +03:00
xhr . open ( 'GET' , url , true ) ;
2020-04-15 12:27:04 +02:00
xhr . onload = ( e ) => {
2019-01-10 15:39:37 +03:00
resolve ( JSON . parse ( this . response ) ) ;
2020-04-15 12:27:04 +02:00
this . decrementFetchQueue ( ) ;
2019-01-10 15:39:37 +03:00
} ;
2020-04-15 12:27:04 +02:00
xhr . onerror = ( e ) => {
2019-09-12 21:24:16 +02:00
reject ( e ) ;
2020-04-15 12:27:04 +02:00
this . decrementFetchQueue ( ) ;
2020-04-05 13:48:10 +02:00
} ;
2019-01-10 15:39:37 +03:00
xhr . send ( ) ;
} ) ;
}
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 ;
}
2019-05-22 23:06:39 -07:00
// skip if already playing this track
2020-04-15 12:27:04 +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 ) ;
this . customTrackIndex = track . Index ;
this . renderTracksEvents ( videoElement , track , item ) ;
this . lastCustomTrackMs = 0 ;
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
* /
renderSsaAss ( videoElement , track , item ) {
2020-04-19 17:38:03 +02:00
const attachments = this . _currentPlayOptions . mediaSource . MediaAttachments || [ ] ;
2020-04-15 12:27:04 +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-05-04 12:44:12 +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-04-19 23:23:46 +02:00
htmlMediaHelper . 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
2020-04-15 12:27:04 +02:00
renderMode : "blend" ,
2020-03-29 22:04:36 +03:00
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-04-15 12:27:04 +02:00
import ( 'JavascriptSubtitlesOctopus' ) . then ( SubtitlesOctopus => {
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 ;
}
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 ;
}
}
return false ;
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
renderSubtitlesWithCustomElement ( videoElement , track , item ) {
this . fetchSubtitles ( track , item ) . then ( ( data ) => {
if ( ! this . videoSubtitlesElem ) {
const subtitlesContainer = document . createElement ( "div" ) ;
2019-01-10 15:39:37 +03:00
subtitlesContainer . classList . add ( 'videoSubtitles' ) ;
subtitlesContainer . innerHTML = '<div class="videoSubtitlesInner"></div>' ;
2020-04-15 12:27:04 +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-04-15 12:27:04 +02:00
this . currentTrackEvents = data . TrackEvents ;
2019-01-10 15:39:37 +03:00
}
} ) ;
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
setSubtitleAppearance ( elem , innerElem ) {
Promise . all ( [ import ( 'userSettings' ) , import ( 'subtitleAppearanceHelper' ) ] ) . then ( ( [ userSettings , subtitleAppearanceHelper ] ) => {
2019-01-10 15:39:37 +03:00
subtitleAppearanceHelper . applyStyles ( {
text : innerElem ,
window : elem
} , userSettings . getSubtitleAppearanceSettings ( ) ) ;
} ) ;
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
getCueCss ( appearance , selector ) {
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
let html = selector + "::cue {" ;
2019-01-10 15:39:37 +03:00
html += appearance . text . map ( function ( s ) {
return s . name + ':' + s . value + '!important;' ;
} ) . join ( '' ) ;
html += '}' ;
return html ;
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
setCueAppearance ( ) {
Promise . all ( [ import ( 'userSettings' ) , import ( 'subtitleAppearanceHelper' ) ] ) . then ( ( [ userSettings , subtitleAppearanceHelper ] ) => {
2020-04-19 17:38:03 +02:00
const elementId = this . id + "-cuestyle" ;
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
let styleElem = document . querySelector ( "#" + elementId ) ;
2019-01-10 15:39:37 +03:00
if ( ! styleElem ) {
styleElem = document . createElement ( 'style' ) ;
styleElem . id = elementId ;
styleElem . type = 'text/css' ;
document . getElementsByTagName ( 'head' ) [ 0 ] . appendChild ( styleElem ) ;
}
2020-04-15 12:27:04 +02:00
styleElem . innerHTML = this . getCueCss ( subtitleAppearanceHelper . getStyles ( userSettings . getSubtitleAppearanceSettings ( ) , true ) , '.htmlvideoplayer' ) ;
2019-01-10 15:39:37 +03:00
} ) ;
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
renderTracksEvents ( videoElement , track , item ) {
2019-01-10 15:39:37 +03:00
if ( ! itemHelper . isLocalItem ( item ) || track . IsExternal ) {
2020-04-15 12:27:04 +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 ;
}
}
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 {
trackElement . mode = 'showing' ;
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-01-10 15:39:37 +03:00
}
2019-10-11 15:48:28 +02:00
trackElement . mode = 'disabled' ;
} else {
// 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 ) {
2019-01-10 15:39:37 +03:00
2019-10-11 15:48:28 +02:00
// show in ui
2020-02-16 03:44:43 +01:00
console . debug ( 'downloaded ' + data . TrackEvents . length + ' track events' ) ;
2019-10-11 15:48:28 +02:00
// add some cues to show the text
// in safari, the cues need to be added before setting the track mode to showing
2020-04-19 23:23:46 +02:00
for ( const trackEvent of data . TrackEvents ) {
2020-04-15 12:27:04 +02:00
const trackCueObject = window . VTTCue || window . TextTrackCue ;
const cue = new trackCueObject ( trackEvent . StartPositionTicks / 10000000 , trackEvent . EndPositionTicks / 10000000 , normalizeTrackEventText ( trackEvent . Text , false ) ) ;
2019-01-10 15:39:37 +03:00
2019-10-11 15:48:28 +02:00
trackElement . addCue ( cue ) ;
2020-04-19 23:23:46 +02:00
}
2019-01-10 15:39:37 +03:00
trackElement . mode = 'showing' ;
2019-10-11 15:48:28 +02:00
} ) ;
2019-01-10 15:39:37 +03:00
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
updateSubtitleText ( timeMs ) {
const clock = this . currentClock ;
2019-01-10 15:39:37 +03:00
if ( clock ) {
try {
clock . seek ( timeMs / 1000 ) ;
} catch ( err ) {
2020-02-16 03:44:43 +01:00
console . error ( 'error in libjass: ' + err ) ;
2019-01-10 15:39:37 +03:00
}
return ;
}
2020-04-15 12:27:04 +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 ;
}
}
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' ) ;
}
}
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
setCurrentTrackElement ( streamIndex ) {
2019-01-10 15:39:37 +03:00
2020-02-17 20:41:04 +01: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-04-15 12:27:04 +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 ( ) ;
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 ) {
const dlg = document . querySelector ( ".videoPlayerContainer" ) ;
2019-01-10 15:39:37 +03:00
if ( ! dlg ) {
2020-04-15 12:27:04 +02:00
return import ( 'css!./style' ) . then ( ( ) => {
2019-01-10 15:39:37 +03:00
loading . show ( ) ;
2020-04-15 12:27:04 +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-04-15 12:27:04 +02:00
let html = "" ;
let cssClass = "htmlvideoplayer" ;
2019-01-10 15:39:37 +03:00
if ( ! browser . chromecast ) {
cssClass += ' htmlvideoplayer-moveupsubtitles' ;
}
// 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-04-15 12:27:04 +02:00
const videoElement = dlg . querySelector ( "video" ) ;
2019-01-10 15:39:37 +03:00
videoElement . volume = htmlMediaHelper . 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 ;
}
2018-10-23 01:05:09 +03:00
2019-01-10 15:39:37 +03:00
document . body . insertBefore ( dlg , document . body . firstChild ) ;
2020-04-15 12:27:04 +02:00
this . videoDialog = dlg ;
this . _mediaElement = videoElement ;
2018-10-23 01:05:09 +03:00
2019-01-10 15:39:37 +03:00
if ( mediaManager ) {
if ( ! mediaManager . embyInit ) {
2020-04-15 12:27:04 +02:00
this . initMediaManager ( ) ;
2019-01-10 15:39:37 +03:00
mediaManager . embyInit = true ;
}
mediaManager . setMediaElement ( videoElement ) ;
}
// don't animate on smart tv's, too slow
if ( options . fullscreen && browser . supportsCssAnimation ( ) && ! browser . slow ) {
zoomIn ( dlg ) . then ( function ( ) {
2020-04-15 12:27:04 +02:00
return videoElement ;
2019-01-10 15:39:37 +03:00
} ) ;
} else {
2020-05-10 01:08:08 +02:00
hidePrePlaybackPage ( ) ;
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 Promise . resolve ( dlg . querySelector ( 'video' ) ) ;
2018-10-23 01:05:09 +03:00
}
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
canPlayMediaType ( mediaType ) {
2019-01-10 15:39:37 +03:00
return ( mediaType || '' ) . toLowerCase ( ) === 'video' ;
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
* /
supportsPlayMethod ( playMethod , item ) {
2019-01-10 15:39:37 +03:00
if ( appHost . supportsPlayMethod ) {
return appHost . supportsPlayMethod ( playMethod , item ) ;
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
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 ) => {
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 ) ;
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
return getDefaultProfile ( ) ;
}
2020-04-15 12:27:04 +02:00
/ * *
* @ private
* /
static getSupportedFeatures ( ) {
const list = [ ] ;
2019-05-22 23:06:39 -07:00
2020-04-15 12:27:04 +02:00
const video = document . createElement ( "video" ) ;
if ( video . webkitSupportsPresentationMode && typeof video . webkitSetPresentationMode === "function" || 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' ) ;
2018-10-23 01:05:09 +03:00
}
}
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-04-15 12:27:04 +02:00
list . push ( "SetAspectRatio" ) ;
2018-10-23 01:05:09 +03:00
2019-01-10 15:39:37 +03:00
return list ;
2018-10-23 01:05:09 +03:00
}
2020-04-15 12:27:04 +02:00
supports ( feature ) {
if ( ! this . supportedFeatures ) {
this . supportedFeatures = HtmlVideoPlayer . getSupportedFeatures ( ) ;
2019-01-10 15:39:37 +03:00
}
2020-04-19 23:23:46 +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 ) {
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-04-15 12:27:04 +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-04-15 12:27:04 +02:00
duration ( val ) {
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 ;
2019-01-10 15:39:37 +03:00
if ( htmlMediaHelper . isValidDuration ( duration ) ) {
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-04-15 12:27:04 +02:00
canSetAudioStreamIndex ( index ) {
2019-01-10 15:39:37 +03:00
if ( browser . tizen || browser . orsay ) {
return true ;
}
2020-04-15 12:27:04 +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
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
static onPictureInPictureError ( err ) {
2020-02-16 03:44:43 +01:00
console . error ( 'Picture in picture error: ' + err . toString ( ) ) ;
2019-01-10 15:39:37 +03:00
}
2020-04-15 12:27:04 +02:00
setPictureInPictureEnabled ( isEnabled ) {
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-04-15 12:27:04 +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 ) {
return document . pictureInPictureElement ? true : false ;
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-04-15 12:27:04 +02:00
const video = this . _mediaElement ;
2019-01-10 15:39:37 +03:00
if ( video ) {
2020-04-15 12:27:04 +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
}
2020-01-10 11:31:03 -05:00
2020-04-15 12:27:04 +02:00
static isAirPlayEnabled ( ) {
2020-01-10 11:31:03 -05:00
if ( document . AirPlayEnabled ) {
return document . AirplayElement ? true : false ;
}
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 ) {
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-04-15 12:27:04 +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-04-15 12:27:04 +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
}
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
setBrightness ( val ) {
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-04-15 12:27:04 +02:00
const cssValue = rawValue >= 100 ? "none" : ( rawValue / 100 ) ;
2019-01-10 15:39:37 +03:00
elem . style [ '-webkit-filter' ] = 'brightness(' + cssValue + ');' ;
elem . style . filter = 'brightness(' + cssValue + ')' ;
elem . brightnessValue = val ;
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 ( ) {
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 ( ) {
const mediaElement = this . _mediaElement ;
2018-10-23 01:05:09 +03:00
if ( mediaElement ) {
2019-01-10 15:39:37 +03:00
2020-04-15 12:27:04 +02:00
const seekable = mediaElement . seekable ;
2018-10-23 01:05:09 +03:00
if ( seekable && seekable . length ) {
2019-01-10 15:39:37 +03:00
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
if ( ! htmlMediaHelper . isValidDuration ( start ) ) {
start = 0 ;
}
if ( ! htmlMediaHelper . isValidDuration ( end ) ) {
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 ( ) {
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 ( ) {
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 ( ) {
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 ( ) {
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 ) {
const mediaElement = this . _mediaElement ;
2020-04-01 17:53:14 +02:00
if ( mediaElement ) {
mediaElement . playbackRate = value ;
}
} ;
2020-04-15 12:27:04 +02:00
getPlaybackRate ( ) {
const mediaElement = this . _mediaElement ;
2020-04-01 17:53:14 +02:00
if ( mediaElement ) {
return mediaElement . playbackRate ;
}
return null ;
} ;
2020-04-15 12:27:04 +02:00
setVolume ( val ) {
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 ( ) {
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 ) {
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 ( ) {
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 ) {
const mediaElement = this . _mediaElement ;
2019-11-20 15:13:15 +01:00
if ( mediaElement ) {
2020-04-15 12:27:04 +02:00
if ( "auto" === val ) {
mediaElement . style . removeProperty ( "object-fit" ) ;
2019-11-20 15:13:15 +01:00
} else {
2020-04-15 12:27:04 +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 ( ) {
return this . _currentAspectRatio || "auto" ;
}
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-04-15 12:27:04 +02:00
name : "Auto" ,
id : "auto"
2019-11-20 15:13:15 +01:00
} , {
2020-04-15 12:27:04 +02:00
name : "Cover" ,
id : "cover"
2019-11-20 15:13:15 +01:00
} , {
2020-04-15 12:27:04 +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 ( ) {
const mediaElement = this . _mediaElement ;
2019-01-10 15:39:37 +03:00
if ( mediaElement ) {
return htmlMediaHelper . getBufferedRanges ( this , mediaElement ) ;
}
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 ( ) {
const mediaElement = this . _mediaElement ;
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 : [ ] ,
2020-04-15 12:27:04 +02: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-04-15 12:27:04 +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-04-15 12:27:04 +02:00
const protocol = ( link . protocol || "" ) . replace ( ":" , "" ) ;
2019-01-10 15:39:37 +03:00
if ( protocol ) {
mediaCategory . stats . push ( {
2020-04-15 12:27:04 +02:00
label : globalize . translate ( "LabelProtocol" ) ,
2019-01-10 15:39:37 +03:00
value : protocol
} ) ;
}
link = null ;
}
if ( this . _hlsPlayer || this . _shakaPlayer ) {
mediaCategory . stats . push ( {
2020-04-15 12:27:04 +02:00
label : globalize . translate ( "LabelStreamType" ) ,
2019-01-10 15:39:37 +03:00
value : 'HLS'
} ) ;
} else {
mediaCategory . stats . push ( {
2020-04-15 12:27:04 +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 : [ ] ,
2020-04-15 12:27:04 +02: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 ) {
videoCategory . stats . push ( {
2020-04-15 12:27:04 +02:00
label : globalize . translate ( "LabelPlayerDimensions" ) ,
2019-01-10 15:39:37 +03:00
value : width + 'x' + height
} ) ;
}
height = mediaElement . videoHeight ;
width = mediaElement . videoWidth ;
if ( width && height ) {
videoCategory . stats . push ( {
2020-04-15 12:27:04 +02:00
label : globalize . translate ( "LabelVideoResolution" ) ,
2019-01-10 15:39:37 +03:00
value : width + 'x' + height
} ) ;
}
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 ;
2018-10-23 01:05:09 +03:00
videoCategory . stats . push ( {
2020-04-15 12:27:04 +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-04-15 12:27:04 +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 : [ ] ,
2020-04-15 12:27:04 +02: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
}
}
2019-01-10 15:39:37 +03:00
if ( browser . chromecast ) {
mediaManager = new cast . receiver . MediaManager ( document . createElement ( 'video' ) ) ;
}
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 ;