2019-02-17 23:13:42 +01:00
define ( [ 'browser' , 'require' , 'events' , 'apphost' , 'loading' , 'dom' , 'playbackManager' , 'appRouter' , 'appSettings' , 'connectionManager' , 'htmlMediaHelper' , 'itemHelper' , 'fullscreenManager' ] , function ( browser , require , events , appHost , loading , dom , playbackManager , appRouter , appSettings , connectionManager , htmlMediaHelper , itemHelper , fullscreenManager ) {
2018-10-23 01:05:09 +03:00
"use strict" ;
2019-01-10 15:39:37 +03:00
var mediaManager ;
2018-10-23 01:05:09 +03:00
function tryRemoveElement ( elem ) {
var parentNode = elem . parentNode ;
2019-01-10 15:39:37 +03:00
if ( parentNode ) {
// Seeing crashes in edge webview
try {
parentNode . removeChild ( elem ) ;
} catch ( err ) {
console . log ( 'Error removing dialog element: ' + err ) ;
}
}
}
var _supportsTextTracks ;
function supportsTextTracks ( ) {
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
}
function enableNativeTrackSupport ( currentSrc , track ) {
2019-01-10 15:39:37 +03:00
if ( track ) {
if ( track . DeliveryMethod === 'Embed' ) {
return true ;
}
}
if ( browser . firefox ) {
if ( ( currentSrc || '' ) . toLowerCase ( ) . indexOf ( '.m3u8' ) !== - 1 ) {
return false ;
}
}
// subs getting blocked due to CORS
if ( browser . chromecast ) {
if ( ( currentSrc || '' ) . toLowerCase ( ) . indexOf ( '.m3u8' ) !== - 1 ) {
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 ) {
2019-01-10 15:39:37 +03:00
var format = ( track . Codec || '' ) . toLowerCase ( ) ;
if ( format === 'ssa' || format === 'ass' ) {
// libjass is needed here
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
}
function zoomIn ( elem ) {
2019-01-10 15:39:37 +03:00
return new Promise ( function ( resolve , reject ) {
var duration = 240 ;
elem . style . animation = 'htmlvideoplayer-zoomin ' + duration + 'ms ease-in normal' ;
dom . addEventListener ( elem , dom . whichAnimationEvent ( ) , resolve , {
once : true
} ) ;
} ) ;
2018-10-23 01:05:09 +03:00
}
function normalizeTrackEventText ( text ) {
2019-01-10 15:39:37 +03:00
return text . replace ( /\\N/gi , '\n' ) ;
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 ;
}
2018-10-23 01:05:09 +03:00
var 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 '' ;
}
var defaultAttribute = mediaSource . DefaultSubtitleStreamIndex === t . Index ? ' default' : '' ;
var language = t . Language || 'und' ;
var label = t . Language || 'und' ;
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 ( ) {
2019-01-10 15:39:37 +03:00
return new Promise ( function ( resolve , reject ) {
require ( [ 'browserdeviceprofile' ] , function ( profileBuilder ) {
resolve ( profileBuilder ( { } ) ) ;
} ) ;
} ) ;
2018-10-23 01:05:09 +03:00
}
function HtmlVideoPlayer ( ) {
2019-01-10 15:39:37 +03:00
if ( browser . edgeUwp ) {
this . name = 'Windows Video Player' ;
} else {
this . name = 'Html Video Player' ;
}
this . type = 'mediaplayer' ;
this . id = 'htmlvideoplayer' ;
// Let any players created by plugins take priority
this . priority = 1 ;
var videoDialog ;
var winJsPlaybackItem ;
var subtitleTrackIndexToSetOnPlaying ;
var audioTrackIndexToSetOnPlaying ;
var lastCustomTrackMs = 0 ;
var currentClock ;
var currentAssRenderer ;
var customTrackIndex = - 1 ;
2019-12-04 18:13:31 +01:00
var showTrackOffset ;
2019-03-30 22:11:39 +01:00
var currentTrackOffset ;
2019-01-10 15:39:37 +03:00
var videoSubtitlesElem ;
var currentTrackEvents ;
var self = this ;
self . currentSrc = function ( ) {
return self . _currentSrc ;
} ;
2019-09-12 21:24:16 +02:00
self . _fetchQueue = 0 ;
self . isFetching = false ;
function incrementFetchQueue ( ) {
if ( self . _fetchQueue <= 0 ) {
self . isFetching = true ;
events . trigger ( self , "beginFetch" ) ;
}
self . _fetchQueue ++ ;
}
function decrementFetchQueue ( ) {
self . _fetchQueue -- ;
if ( self . _fetchQueue <= 0 ) {
self . isFetching = false ;
events . trigger ( self , "endFetch" ) ;
}
}
2018-10-23 01:05:09 +03:00
function updateVideoUrl ( streamInfo ) {
2019-01-10 15:39:37 +03:00
var isHls = streamInfo . url . toLowerCase ( ) . indexOf ( '.m3u8' ) !== - 1 ;
var mediaSource = streamInfo . mediaSource ;
var item = streamInfo . item ;
// 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 ) ) {
var hlsPlaylistUrl = streamInfo . url . replace ( 'master.m3u8' , 'live.m3u8' ) ;
loading . show ( ) ;
console . log ( 'prefetching hls playlist: ' + hlsPlaylistUrl ) ;
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 ( ) {
console . log ( 'completed prefetching hls playlist: ' + hlsPlaylistUrl ) ;
loading . hide ( ) ;
streamInfo . url = hlsPlaylistUrl ;
return Promise . resolve ( ) ;
} , function ( ) {
console . log ( 'error prefetching hls playlist: ' + hlsPlaylistUrl ) ;
loading . hide ( ) ;
return Promise . resolve ( ) ;
} ) ;
} else {
return Promise . resolve ( ) ;
2018-10-23 01:05:09 +03:00
}
}
2019-01-10 15:39:37 +03:00
self . play = function ( options ) {
if ( browser . msie ) {
if ( options . playMethod === 'Transcode' && ! window . MediaSource ) {
alert ( 'Playback of this content is not supported in Internet Explorer. For a better experience, try a modern browser such as Microsoft Edge, Google Chrome, Firefox or Opera.' ) ;
return Promise . reject ( ) ;
}
}
self . _started = false ;
self . _timeUpdated = false ;
self . _currentTime = null ;
2019-11-30 20:01:32 +01:00
self . resetSubtitleOffset ( ) ;
2019-01-10 15:39:37 +03:00
return createMediaElement ( options ) . then ( function ( elem ) {
return updateVideoUrl ( options , options . mediaSource ) . then ( function ( ) {
return setCurrentSrc ( elem , options ) ;
} ) ;
} ) ;
} ;
2018-10-23 01:05:09 +03:00
function setSrcWithFlvJs ( instance , elem , options , url ) {
2019-01-10 15:39:37 +03:00
return new Promise ( function ( resolve , reject ) {
require ( [ 'flvjs' ] , function ( flvjs ) {
2018-10-23 01:05:09 +03:00
var 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
flvPlayer . attachMediaElement ( elem ) ;
flvPlayer . load ( ) ;
flvPlayer . play ( ) . then ( resolve , reject ) ;
instance . _flvPlayer = flvPlayer ;
// This is needed in setCurrentTrackElement
self . _currentSrc = url ;
} ) ;
} ) ;
2018-10-23 01:05:09 +03:00
}
function setSrcWithHlsJs ( instance , elem , options , url ) {
2019-01-10 15:39:37 +03:00
return new Promise ( function ( resolve , reject ) {
requireHlsPlayer ( function ( ) {
2018-10-23 01:05:09 +03:00
var hls = new Hls ( {
2019-01-10 15:39:37 +03:00
manifestLoadingTimeOut : 20000
//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 ) ;
htmlMediaHelper . bindEventsToHlsPlayer ( self , hls , elem , onError , resolve , reject ) ;
self . _hlsPlayer = hls ;
// This is needed in setCurrentTrackElement
self . _currentSrc = url ;
} ) ;
} ) ;
}
function onShakaError ( event ) {
var error = event . detail ;
console . error ( 'Error code' , error . code , 'object' , error ) ;
}
function setSrcWithShakaPlayer ( instance , elem , options , url ) {
return new Promise ( function ( resolve , reject ) {
require ( [ 'shaka' ] , function ( ) {
var player = new shaka . Player ( elem ) ;
//player.configure({
// abr: {
// enabled: false
// },
// streaming: {
// failureCallback: function () {
// alert(2);
// }
// }
//});
//shaka.log.setLevel(6);
// Listen for error events.
player . addEventListener ( 'error' , onShakaError ) ;
// Try to load a manifest.
// This is an asynchronous process.
player . load ( url ) . then ( function ( ) {
// This runs if the asynchronous load is successful.
resolve ( ) ;
} , reject ) ;
self . _shakaPlayer = player ;
// This is needed in setCurrentTrackElement
self . _currentSrc = url ;
} ) ;
} ) ;
2018-10-23 01:05:09 +03:00
}
function setCurrentSrcChromecast ( instance , elem , options , url ) {
2019-01-10 15:39:37 +03:00
elem . autoplay = true ;
var lrd = new cast . receiver . MediaManager . LoadRequestData ( ) ;
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 ;
console . log ( 'loading media url into mediaManager' ) ;
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
self . _currentSrc = url ;
return Promise . resolve ( ) ;
2018-10-23 01:05:09 +03:00
} catch ( err ) {
2019-01-10 15:39:37 +03:00
console . log ( 'mediaManager error: ' + err ) ;
return Promise . reject ( ) ;
2018-10-23 01:05:09 +03:00
}
}
2019-01-10 15:39:37 +03:00
// Adapted from : https://github.com/googlecast/CastReferencePlayer/blob/master/player.js
2018-10-23 01:05:09 +03:00
function onMediaManagerLoadMedia ( event ) {
2019-01-10 15:39:37 +03:00
if ( self . _castPlayer ) {
2019-11-23 00:29:38 +09:00
self . _castPlayer . unload ( ) ; // Must unload before starting again.
2019-01-10 15:39:37 +03:00
}
self . _castPlayer = null ;
var data = event . data ;
var media = event . data . media || { } ;
var url = media . contentId ;
var contentType = media . contentType . toLowerCase ( ) ;
var options = media . customData ;
var protocol ;
var ext = 'm3u8' ;
var mediaElement = self . _mediaElement ;
var host = new cast . player . api . Host ( {
'url' : url ,
'mediaElement' : mediaElement
} ) ;
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 ) ;
} else if ( url . indexOf ( '.ism' ) > - 1 ||
contentType === 'application/vnd.ms-sstr+xml' ) {
protocol = cast . player . api . CreateSmoothStreamingProtocol ( host ) ;
}
console . log ( 'loading playback url: ' + url ) ;
console . log ( 'contentType: ' + contentType ) ;
host . onError = function ( errorCode ) {
console . log ( "Fatal Error - " + errorCode ) ;
} ;
mediaElement . autoplay = false ;
self . _castPlayer = new cast . player . api . Player ( host ) ;
self . _castPlayer . load ( protocol , data . currentTime || 0 ) ;
self . _castPlayer . playWhenHaveEnoughData ( ) ;
2018-10-23 01:05:09 +03:00
}
function initMediaManager ( ) {
2019-01-10 15:39:37 +03:00
mediaManager . defaultOnLoad = mediaManager . onLoad . bind ( mediaManager ) ;
mediaManager . onLoad = onMediaManagerLoadMedia . bind ( self ) ;
//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
}
function setCurrentSrc ( elem , options ) {
2019-01-10 15:39:37 +03:00
elem . removeEventListener ( 'error' , onError ) ;
2018-10-23 01:05:09 +03:00
var val = options . url ;
2019-01-10 15:39:37 +03:00
console . log ( 'playing url: ' + val ) ;
// Convert to seconds
var seconds = ( options . playerStartPositionTicks || 0 ) / 10000000 ;
if ( seconds ) {
val += '#t=' + seconds ;
}
htmlMediaHelper . destroyHlsPlayer ( self ) ;
htmlMediaHelper . destroyFlvPlayer ( self ) ;
htmlMediaHelper . destroyCastPlayer ( self ) ;
2018-10-23 01:05:09 +03:00
var tracks = getMediaStreamTextTracks ( options . mediaSource ) ;
2019-01-10 15:39:37 +03:00
subtitleTrackIndexToSetOnPlaying = options . mediaSource . DefaultSubtitleStreamIndex == null ? - 1 : options . mediaSource . DefaultSubtitleStreamIndex ;
if ( subtitleTrackIndexToSetOnPlaying != null && subtitleTrackIndexToSetOnPlaying >= 0 ) {
2018-10-23 01:05:09 +03:00
var initialSubtitleStream = options . mediaSource . MediaStreams [ subtitleTrackIndexToSetOnPlaying ] ;
2019-01-10 15:39:37 +03:00
if ( ! initialSubtitleStream || initialSubtitleStream . DeliveryMethod === 'Encode' ) {
subtitleTrackIndexToSetOnPlaying = - 1 ;
}
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
audioTrackIndexToSetOnPlaying = options . playMethod === 'Transcode' ? null : options . mediaSource . DefaultAudioStreamIndex ;
self . _currentPlayOptions = options ;
2018-10-23 01:05:09 +03:00
var 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
2019-01-10 15:39:37 +03: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 d e x O f ( ' . m 3 u 8 ' ) ! = = - 1 ) {
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
2019-01-10 15:39:37 +03:00
return setSrcWithShakaPlayer ( self , elem , options , val ) ;
2018-10-23 01:05:09 +03:00
2019-01-10 15:39:37 +03:00
} else * / i f ( b r o w s e r . c h r o m e c a s t & & v a l . i n d e x O f ( ' . m 3 u 8 ' ) ! = = - 1 & & o p t i o n s . m e d i a S o u r c e . R u n T i m e T i c k s ) {
return setCurrentSrcChromecast ( self , elem , options , val ) ;
2019-11-23 00:29:38 +09:00
} else if ( htmlMediaHelper . enableHlsJsPlayer ( options . mediaSource . RunTimeTicks , 'Video' ) && val . indexOf ( '.m3u8' ) !== - 1 ) {
2019-01-10 15:39:37 +03:00
return setSrcWithHlsJs ( self , elem , options , val ) ;
} else if ( options . playMethod !== 'Transcode' && options . mediaSource . Container === 'flv' ) {
2018-10-23 01:05:09 +03:00
2019-01-10 15:39:37 +03:00
return setSrcWithFlvJs ( self , elem , options , val ) ;
} else {
elem . autoplay = true ;
return htmlMediaHelper . applySrc ( elem , val , options ) . then ( function ( ) {
self . _currentSrc = val ;
return htmlMediaHelper . playWithPromise ( elem , onError ) ;
} ) ;
2018-10-23 01:05:09 +03:00
}
}
2019-01-10 15:39:37 +03:00
self . setSubtitleStreamIndex = function ( index ) {
2018-10-23 01:05:09 +03:00
2019-01-10 15:39:37 +03:00
setCurrentTrackElement ( index ) ;
} ;
2018-10-23 01:05:09 +03:00
2019-11-30 20:01:32 +01:00
self . resetSubtitleOffset = function ( ) {
2019-12-04 18:13:31 +01:00
currentTrackOffset = 0 ;
showTrackOffset = false ;
2019-11-30 20:01:32 +01:00
}
2019-04-08 20:21:16 +02:00
self . enableShowingSubtitleOffset = function ( ) {
showTrackOffset = true ;
}
self . disableShowingSubtitleOffset = function ( ) {
showTrackOffset = false ;
}
self . isShowingSubtitleOffsetEnabled = function ( ) {
return showTrackOffset ;
}
2019-12-07 20:29:14 +01:00
function getTextTrack ( ) {
var videoElement = self . _mediaElement ;
if ( videoElement ) {
return Array . from ( videoElement . textTracks )
. find ( function ( trackElement ) {
// get showing .vtt textTack
return trackElement . mode === 'showing' ;
} ) ;
} else {
// get track events
return currentTrackEvents ;
}
}
2019-03-30 22:11:39 +01:00
self . setSubtitleOffset = function ( offset ) {
var offsetValue = parseFloat ( offset ) ;
2019-10-13 14:10:51 +02:00
// if .ass currently rendering
2019-11-23 00:29:38 +09:00
if ( currentAssRenderer ) {
2019-10-13 14:10:51 +02:00
updateCurrentTrackOffset ( offsetValue ) ;
} else {
2019-12-07 20:29:14 +01:00
var trackElement = getTextTrack ( ) ;
// if .vtt currently rendering
if ( trackElement ) {
offsetValue = updateCurrentTrackOffset ( offsetValue ) ;
setVttSubtitleOffset ( trackElement , offsetValue ) ;
} else {
console . log ( "No available track, cannot apply offset : " + offsetValue ) ;
}
2019-10-13 14:10:51 +02:00
}
2019-03-30 22:11:39 +01:00
} ;
function updateCurrentTrackOffset ( offsetValue ) {
var relativeOffset = offsetValue ;
var newTrackOffset = offsetValue ;
2019-11-23 00:29:38 +09:00
if ( currentTrackOffset ) {
2019-03-30 22:11:39 +01:00
relativeOffset -= currentTrackOffset ;
}
currentTrackOffset = newTrackOffset ;
// relative to currentTrackOffset
return relativeOffset ;
}
function setVttSubtitleOffset ( currentTrack , offsetValue ) {
2019-11-23 00:29:38 +09:00
if ( currentTrack . cues ) {
2019-03-30 22:11:39 +01:00
Array . from ( currentTrack . cues )
2019-11-23 00:29:38 +09:00
. forEach ( function ( cue ) {
cue . startTime -= offsetValue ;
cue . endTime -= offsetValue ;
} ) ;
2019-12-07 20:29:14 +01:00
} else if ( Array . isArray ( currentTrack ) ) {
currentTrack . forEach ( function ( trackEvent ) {
trackEvent . StartPositionTicks -= offsetValue ;
trackEvent . EndPositionTicks -= offsetValue ;
} ) ;
2019-03-30 22:11:39 +01:00
}
}
2019-04-08 20:21:16 +02:00
self . getSubtitleOffset = function ( ) {
return currentTrackOffset ;
}
2019-01-10 15:39:37 +03:00
function isAudioStreamSupported ( stream , deviceProfile ) {
2018-10-23 01:05:09 +03:00
2019-01-10 15:39:37 +03:00
var codec = ( stream . Codec || '' ) . toLowerCase ( ) ;
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
var profiles = deviceProfile . DirectPlayProfiles || [ ] ;
return profiles . filter ( function ( p ) {
if ( p . Type === 'Video' ) {
if ( ! p . AudioCodec ) {
return true ;
}
return p . AudioCodec . toLowerCase ( ) . indexOf ( codec ) !== - 1 ;
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
}
2019-01-10 15:39:37 +03:00
function getSupportedAudioStreams ( ) {
var profile = self . _lastProfile ;
return getMediaStreamAudioTracks ( self . _currentPlayOptions . mediaSource ) . filter ( function ( stream ) {
return isAudioStreamSupported ( stream , profile ) ;
} ) ;
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
self . setAudioStreamIndex = function ( index ) {
var streams = getSupportedAudioStreams ( ) ;
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
var audioIndex = - 1 ;
2019-11-23 00:29:38 +09:00
var i ;
var length ;
var stream ;
2019-01-10 15:39:37 +03:00
for ( i = 0 , length = streams . length ; i < length ; i ++ ) {
stream = streams [ i ] ;
audioIndex ++ ;
if ( stream . Index === index ) {
break ;
}
}
if ( audioIndex === - 1 ) {
return ;
}
var elem = self . _mediaElement ;
if ( ! elem ) {
return ;
}
// https://msdn.microsoft.com/en-us/library/hh772507(v=vs.85).aspx
var elemAudioTracks = elem . audioTracks || [ ] ;
console . log ( 'found ' + elemAudioTracks . length + ' audio tracks' ) ;
for ( i = 0 , length = elemAudioTracks . length ; i < length ; i ++ ) {
if ( audioIndex === i ) {
console . log ( 'setting audio track ' + i + ' to enabled' ) ;
elemAudioTracks [ i ] . enabled = true ;
} else {
console . log ( 'setting audio track ' + i + ' to disabled' ) ;
elemAudioTracks [ i ] . enabled = false ;
}
}
setTimeout ( function ( ) {
elem . currentTime = elem . currentTime ;
} , 100 ) ;
} ;
self . stop = function ( destroyPlayer ) {
var elem = self . _mediaElement ;
var src = self . _currentSrc ;
if ( elem ) {
if ( src ) {
elem . pause ( ) ;
}
htmlMediaHelper . onEndedInternal ( self , elem , onError ) ;
if ( destroyPlayer ) {
self . destroy ( ) ;
}
}
destroyCustomTrack ( elem ) ;
return Promise . resolve ( ) ;
} ;
self . destroy = function ( ) {
htmlMediaHelper . destroyHlsPlayer ( self ) ;
htmlMediaHelper . destroyFlvPlayer ( self ) ;
appRouter . setTransparency ( 'none' ) ;
var videoElement = self . _mediaElement ;
if ( videoElement ) {
self . _mediaElement = null ;
destroyCustomTrack ( videoElement ) ;
videoElement . removeEventListener ( 'timeupdate' , onTimeUpdate ) ;
videoElement . removeEventListener ( 'ended' , onEnded ) ;
videoElement . removeEventListener ( 'volumechange' , onVolumeChange ) ;
videoElement . removeEventListener ( 'pause' , onPause ) ;
videoElement . removeEventListener ( 'playing' , onPlaying ) ;
videoElement . removeEventListener ( 'play' , onPlay ) ;
videoElement . removeEventListener ( 'click' , onClick ) ;
videoElement . removeEventListener ( 'dblclick' , onDblClick ) ;
videoElement . parentNode . removeChild ( videoElement ) ;
}
var dlg = videoDialog ;
if ( dlg ) {
videoDialog = null ;
dlg . parentNode . removeChild ( dlg ) ;
}
2019-02-17 23:29:31 +01:00
2019-02-17 23:13:42 +01:00
fullscreenManager . exitFullscreen ( ) ;
2019-01-10 15:39:37 +03:00
} ;
function onEnded ( ) {
destroyCustomTrack ( this ) ;
htmlMediaHelper . onEndedInternal ( self , this , onError ) ;
}
function onTimeUpdate ( e ) {
2019-05-22 23:06:39 -07:00
// get the player position and the transcoding offset
2019-01-10 15:39:37 +03:00
var time = this . currentTime ;
if ( time && ! self . _timeUpdated ) {
self . _timeUpdated = true ;
ensureValidVideo ( this ) ;
}
self . _currentTime = time ;
var currentPlayOptions = self . _currentPlayOptions ;
// Not sure yet how this is coming up null since we never null it out, but it is causing app crashes
if ( currentPlayOptions ) {
var timeMs = time * 1000 ;
timeMs += ( ( currentPlayOptions . transcodingOffsetTicks || 0 ) / 10000 ) ;
updateSubtitleText ( timeMs ) ;
}
events . trigger ( self , 'timeupdate' ) ;
}
function onVolumeChange ( ) {
htmlMediaHelper . saveVolume ( this . volume ) ;
events . trigger ( self , 'volumechange' ) ;
}
function onNavigatedToOsd ( ) {
var dlg = videoDialog ;
if ( dlg ) {
dlg . classList . remove ( 'videoPlayerContainer-withBackdrop' ) ;
dlg . classList . remove ( 'videoPlayerContainer-onTop' ) ;
onStartedAndNavigatedToOsd ( ) ;
}
}
2019-05-22 23:06:39 -07:00
function onStartedAndNavigatedToOsd ( ) {
2019-01-10 15:39:37 +03:00
// If this causes a failure during navigation we end up in an awkward UI state
setCurrentTrackElement ( subtitleTrackIndexToSetOnPlaying ) ;
if ( audioTrackIndexToSetOnPlaying != null && self . canSetAudioStreamIndex ( ) ) {
self . setAudioStreamIndex ( audioTrackIndexToSetOnPlaying ) ;
}
}
function onPlaying ( e ) {
if ( ! self . _started ) {
self . _started = true ;
this . removeAttribute ( 'controls' ) ;
loading . hide ( ) ;
htmlMediaHelper . seekOnPlaybackStart ( self , e . target , self . _currentPlayOptions . playerStartPositionTicks ) ;
if ( self . _currentPlayOptions . fullscreen ) {
appRouter . showVideoOsd ( ) . then ( onNavigatedToOsd ) ;
} else {
appRouter . setTransparency ( 'backdrop' ) ;
videoDialog . classList . remove ( 'videoPlayerContainer-withBackdrop' ) ;
videoDialog . classList . remove ( 'videoPlayerContainer-onTop' ) ;
onStartedAndNavigatedToOsd ( ) ;
}
}
events . trigger ( self , 'playing' ) ;
}
function onPlay ( e ) {
events . trigger ( self , 'unpause' ) ;
}
function ensureValidVideo ( elem ) {
if ( elem !== self . _mediaElement ) {
return ;
}
if ( elem . videoWidth === 0 && elem . videoHeight === 0 ) {
var mediaSource = ( self . _currentPlayOptions || { } ) . mediaSource ;
// 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 ) {
htmlMediaHelper . onErrorInternal ( self , 'mediadecodeerror' ) ;
return ;
}
}
}
function onClick ( ) {
events . trigger ( self , 'click' ) ;
}
function onDblClick ( ) {
events . trigger ( self , 'dblclick' ) ;
}
function onPause ( ) {
events . trigger ( self , 'pause' ) ;
}
function onError ( ) {
var errorCode = this . error ? ( this . error . code || 0 ) : 0 ;
var errorMessage = this . error ? ( this . error . message || '' ) : '' ;
console . log ( 'Media element error: ' + errorCode . toString ( ) + ' ' + errorMessage ) ;
var type ;
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
if ( self . _hlsPlayer ) {
htmlMediaHelper . handleHlsJsMediaError ( self ) ;
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 ;
}
htmlMediaHelper . onErrorInternal ( self , type ) ;
}
function destroyCustomTrack ( videoElement ) {
if ( self . _resizeObserver ) {
self . _resizeObserver . disconnect ( ) ;
self . _resizeObserver = null ;
}
if ( videoSubtitlesElem ) {
var subtitlesContainer = videoSubtitlesElem . parentNode ;
if ( subtitlesContainer ) {
tryRemoveElement ( subtitlesContainer ) ;
}
videoSubtitlesElem = null ;
}
currentTrackEvents = null ;
if ( videoElement ) {
var allTracks = videoElement . textTracks || [ ] ; // get list of tracks
for ( var i = 0 ; i < allTracks . length ; i ++ ) {
var currentTrack = allTracks [ i ] ;
if ( currentTrack . label . indexOf ( 'manualTrack' ) !== - 1 ) {
currentTrack . mode = 'disabled' ;
}
}
}
customTrackIndex = - 1 ;
currentClock = null ;
self . _currentAspectRatio = null ;
var renderer = currentAssRenderer ;
if ( renderer ) {
renderer . setEnabled ( false ) ;
}
currentAssRenderer = null ;
}
self . destroyCustomTrack = destroyCustomTrack ;
function fetchSubtitlesUwp ( track , item ) {
return Windows . Storage . StorageFile . getFileFromPathAsync ( track . Path ) . then ( function ( storageFile ) {
return Windows . Storage . FileIO . readTextAsync ( storageFile ) . then ( function ( text ) {
return JSON . parse ( text ) ;
} ) ;
} ) ;
}
function fetchSubtitles ( track , item ) {
if ( window . Windows && itemHelper . isLocalItem ( item ) ) {
return fetchSubtitlesUwp ( track , item ) ;
}
2019-09-12 21:24:16 +02:00
incrementFetchQueue ( ) ;
2019-01-10 15:39:37 +03:00
return new Promise ( function ( resolve , reject ) {
var xhr = new XMLHttpRequest ( ) ;
var url = getTextTrackUrl ( track , item , '.js' ) ;
xhr . open ( 'GET' , url , true ) ;
xhr . onload = function ( e ) {
resolve ( JSON . parse ( this . response ) ) ;
2019-09-12 21:24:16 +02:00
decrementFetchQueue ( ) ;
2019-01-10 15:39:37 +03:00
} ;
2019-09-12 21:24:16 +02:00
xhr . onerror = function ( e ) {
reject ( e ) ;
decrementFetchQueue ( ) ;
}
2019-01-10 15:39:37 +03:00
xhr . send ( ) ;
} ) ;
}
2019-09-12 21:24:16 +02:00
function setTrackForDisplay ( videoElement , track ) {
2019-01-10 15:39:37 +03:00
if ( ! track ) {
destroyCustomTrack ( videoElement ) ;
return ;
}
2019-05-22 23:06:39 -07:00
// skip if already playing this track
2019-01-10 15:39:37 +03:00
if ( customTrackIndex === track . Index ) {
return ;
}
var item = self . _currentPlayOptions . item ;
destroyCustomTrack ( videoElement ) ;
customTrackIndex = track . Index ;
renderTracksEvents ( videoElement , track , item ) ;
lastCustomTrackMs = 0 ;
}
2018-10-23 01:05:09 +03:00
function renderWithLibjass ( videoElement , track , item ) {
2019-01-10 15:39:37 +03:00
2018-10-23 01:05:09 +03:00
var rendererSettings = { } ;
2019-01-10 15:39:37 +03:00
if ( browser . ps4 ) {
// Text outlines are not rendering very well
rendererSettings . enableSvg = false ;
2019-11-23 00:29:38 +09:00
} else if ( browser . edge || browser . msie ) {
2019-01-10 15:39:37 +03:00
// svg not rendering at all
rendererSettings . enableSvg = false ;
}
// probably safer to just disable everywhere
rendererSettings . enableSvg = false ;
require ( [ 'libjass' , 'ResizeObserver' ] , function ( libjass , ResizeObserver ) {
libjass . ASS . fromUrl ( getTextTrackUrl ( track , item ) ) . then ( function ( ass ) {
var clock = new libjass . renderers . ManualClock ( ) ;
2018-10-23 01:05:09 +03:00
currentClock = clock ;
2019-01-10 15:39:37 +03:00
// Create a DefaultRenderer using the video element and the ASS object
2018-10-23 01:05:09 +03:00
var renderer = new libjass . renderers . WebRenderer ( ass , clock , videoElement . parentNode , rendererSettings ) ;
2019-01-10 15:39:37 +03:00
currentAssRenderer = renderer ;
renderer . addEventListener ( "ready" , function ( ) {
2018-10-23 01:05:09 +03:00
try {
2019-01-10 15:39:37 +03:00
renderer . resize ( videoElement . offsetWidth , videoElement . offsetHeight , 0 , 0 ) ;
if ( ! self . _resizeObserver ) {
self . _resizeObserver = new ResizeObserver ( onVideoResize , { } ) ;
self . _resizeObserver . observe ( videoElement ) ;
}
//clock.pause();
} catch ( ex ) {
//alert(ex);
}
} ) ;
} , function ( ) {
htmlMediaHelper . onErrorInternal ( self , 'mediadecodeerror' ) ;
} ) ;
} ) ;
}
function onVideoResize ( ) {
if ( browser . iOS ) {
2019-05-22 23:06:39 -07:00
// the new sizes will be delayed for about 500ms with wkwebview
2019-01-10 15:39:37 +03:00
setTimeout ( resetVideoRendererSize , 500 ) ;
} else {
resetVideoRendererSize ( ) ;
}
}
function resetVideoRendererSize ( ) {
var renderer = currentAssRenderer ;
if ( renderer ) {
var videoElement = self . _mediaElement ;
var width = videoElement . offsetWidth ;
var height = videoElement . offsetHeight ;
console . log ( 'videoElement resized: ' + width + 'x' + height ) ;
renderer . resize ( width , height , 0 , 0 ) ;
}
}
function requiresCustomSubtitlesElement ( ) {
// 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 ) {
var userAgent = navigator . userAgent . toLowerCase ( ) ;
// works in the browser but not the native app
if ( ( userAgent . indexOf ( 'os 9' ) !== - 1 || userAgent . indexOf ( 'os 8' ) !== - 1 ) && userAgent . indexOf ( 'safari' ) === - 1 ) {
return true ;
}
}
return false ;
}
function renderSubtitlesWithCustomElement ( videoElement , track , item ) {
fetchSubtitles ( track , item ) . then ( function ( data ) {
if ( ! videoSubtitlesElem ) {
var subtitlesContainer = document . createElement ( 'div' ) ;
subtitlesContainer . classList . add ( 'videoSubtitles' ) ;
subtitlesContainer . innerHTML = '<div class="videoSubtitlesInner"></div>' ;
videoSubtitlesElem = subtitlesContainer . querySelector ( '.videoSubtitlesInner' ) ;
setSubtitleAppearance ( subtitlesContainer , videoSubtitlesElem ) ;
videoElement . parentNode . appendChild ( subtitlesContainer ) ;
currentTrackEvents = data . TrackEvents ;
}
} ) ;
}
function setSubtitleAppearance ( elem , innerElem ) {
require ( [ 'userSettings' , 'subtitleAppearanceHelper' ] , function ( userSettings , subtitleAppearanceHelper ) {
subtitleAppearanceHelper . applyStyles ( {
text : innerElem ,
window : elem
} , userSettings . getSubtitleAppearanceSettings ( ) ) ;
} ) ;
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
function getCueCss ( appearance , selector ) {
var html = selector + '::cue {' ;
html += appearance . text . map ( function ( s ) {
return s . name + ':' + s . value + '!important;' ;
} ) . join ( '' ) ;
html += '}' ;
return html ;
}
function setCueAppearance ( ) {
require ( [ 'userSettings' , 'subtitleAppearanceHelper' ] , function ( userSettings , subtitleAppearanceHelper ) {
var elementId = self . id + '-cuestyle' ;
var styleElem = document . querySelector ( '#' + elementId ) ;
if ( ! styleElem ) {
styleElem = document . createElement ( 'style' ) ;
styleElem . id = elementId ;
styleElem . type = 'text/css' ;
document . getElementsByTagName ( 'head' ) [ 0 ] . appendChild ( styleElem ) ;
}
styleElem . innerHTML = getCueCss ( subtitleAppearanceHelper . getStyles ( userSettings . getSubtitleAppearanceSettings ( ) , true ) , '.htmlvideoplayer' ) ;
} ) ;
}
function renderTracksEvents ( videoElement , track , item ) {
if ( ! itemHelper . isLocalItem ( item ) || track . IsExternal ) {
var format = ( track . Codec || '' ) . toLowerCase ( ) ;
if ( format === 'ssa' || format === 'ass' ) {
// libjass is needed here
renderWithLibjass ( videoElement , track , item ) ;
return ;
}
if ( requiresCustomSubtitlesElement ( ) ) {
renderSubtitlesWithCustomElement ( videoElement , track , item ) ;
return ;
}
}
var 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 ) {
console . log ( '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
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
console . log ( 'downloaded ' + data . TrackEvents . length + ' track events' ) ;
// add some cues to show the text
// in safari, the cues need to be added before setting the track mode to showing
data . TrackEvents . forEach ( function ( trackEvent ) {
2019-01-10 15:39:37 +03:00
2019-10-11 15:48:28 +02:00
var trackCueObject = window . VTTCue || window . TextTrackCue ;
var cue = new trackCueObject ( trackEvent . StartPositionTicks / 10000000 , trackEvent . EndPositionTicks / 10000000 , normalizeTrackEventText ( trackEvent . Text ) ) ;
2019-01-10 15:39:37 +03:00
2019-10-11 15:48:28 +02:00
trackElement . addCue ( cue ) ;
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
}
function updateSubtitleText ( timeMs ) {
2019-03-30 22:11:39 +01:00
// handle offset for ass tracks
2019-11-23 00:29:38 +09:00
if ( currentTrackOffset ) {
2019-03-30 22:11:39 +01:00
timeMs += ( currentTrackOffset * 1000 ) ;
}
2019-01-10 15:39:37 +03:00
var clock = currentClock ;
if ( clock ) {
try {
clock . seek ( timeMs / 1000 ) ;
} catch ( err ) {
console . log ( 'Error in libjass: ' + err ) ;
}
return ;
}
var trackEvents = currentTrackEvents ;
var subtitleTextElement = videoSubtitlesElem ;
if ( trackEvents && subtitleTextElement ) {
var ticks = timeMs * 10000 ;
var selectedTrackEvent ;
for ( var i = 0 ; i < trackEvents . length ; i ++ ) {
var currentTrackEvent = trackEvents [ i ] ;
if ( currentTrackEvent . StartPositionTicks <= ticks && currentTrackEvent . EndPositionTicks >= ticks ) {
selectedTrackEvent = currentTrackEvent ;
break ;
}
}
if ( selectedTrackEvent && selectedTrackEvent . Text ) {
subtitleTextElement . innerHTML = normalizeTrackEventText ( selectedTrackEvent . Text ) ;
subtitleTextElement . classList . remove ( 'hide' ) ;
} else {
subtitleTextElement . classList . add ( 'hide' ) ;
}
}
}
function setCurrentTrackElement ( streamIndex ) {
console . log ( 'Setting new text track index to: ' + streamIndex ) ;
var mediaStreamTextTracks = getMediaStreamTextTracks ( self . _currentPlayOptions . mediaSource ) ;
var track = streamIndex === - 1 ? null : mediaStreamTextTracks . filter ( function ( t ) {
return t . Index === streamIndex ;
} ) [ 0 ] ;
2019-09-12 21:24:16 +02:00
setTrackForDisplay ( self . _mediaElement , track ) ;
2019-01-10 15:39:37 +03:00
if ( enableNativeTrackSupport ( self . _currentSrc , track ) ) {
if ( streamIndex !== - 1 ) {
setCueAppearance ( ) ;
}
} else {
// null these out to disable the player's native display (handled below)
streamIndex = - 1 ;
track = null ;
}
}
function updateTextStreamUrls ( startPositionTicks ) {
if ( ! supportsTextTracks ( ) ) {
return ;
}
var allTracks = self . _mediaElement . textTracks ; // get list of tracks
var i ;
var track ;
for ( i = 0 ; i < allTracks . length ; i ++ ) {
track = allTracks [ i ] ;
// 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 {
while ( track . cues . length ) {
track . removeCue ( track . cues [ 0 ] ) ;
}
} catch ( e ) {
console . log ( 'Error removing cue from textTrack' ) ;
}
}
var tracks = self . _mediaElement . querySelectorAll ( 'track' ) ;
for ( i = 0 ; i < tracks . length ; i ++ ) {
track = tracks [ i ] ;
track . src = replaceQueryString ( track . src , 'startPositionTicks' , startPositionTicks ) ;
}
}
function createMediaElement ( options ) {
if ( browser . tv || browser . iOS || browser . mobile ) {
// too slow
// also on iOS, the backdrop image doesn't look right
// on android mobile, it works, but can be slow to have the video surface fully cover the backdrop
options . backdropUrl = null ;
}
return new Promise ( function ( resolve , reject ) {
var dlg = document . querySelector ( '.videoPlayerContainer' ) ;
if ( ! dlg ) {
require ( [ 'css!./style' ] , function ( ) {
loading . show ( ) ;
var dlg = document . createElement ( 'div' ) ;
dlg . classList . add ( 'videoPlayerContainer' ) ;
if ( options . backdropUrl ) {
dlg . classList . add ( 'videoPlayerContainer-withBackdrop' ) ;
dlg . style . backgroundImage = "url('" + options . backdropUrl + "')" ;
}
if ( options . fullscreen ) {
dlg . classList . add ( 'videoPlayerContainer-onTop' ) ;
}
// playsinline new for iOS 10
// https://developer.apple.com/library/content/releasenotes/General/WhatsNewInSafari/Articles/Safari_10_0.html
var html = '' ;
var cssClass = 'htmlvideoplayer' ;
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 ;
var videoElement = dlg . querySelector ( 'video' ) ;
videoElement . volume = htmlMediaHelper . getSavedVolume ( ) ;
videoElement . addEventListener ( 'timeupdate' , onTimeUpdate ) ;
videoElement . addEventListener ( 'ended' , onEnded ) ;
videoElement . addEventListener ( 'volumechange' , onVolumeChange ) ;
videoElement . addEventListener ( 'pause' , onPause ) ;
videoElement . addEventListener ( 'playing' , onPlaying ) ;
videoElement . addEventListener ( 'play' , onPlay ) ;
videoElement . addEventListener ( 'click' , onClick ) ;
videoElement . addEventListener ( 'dblclick' , onDblClick ) ;
2018-10-23 01:05:09 +03:00
2019-01-10 15:39:37 +03:00
document . body . insertBefore ( dlg , document . body . firstChild ) ;
videoDialog = dlg ;
self . _mediaElement = videoElement ;
2018-10-23 01:05:09 +03:00
2019-01-10 15:39:37 +03:00
if ( mediaManager ) {
2018-10-23 01:05:09 +03:00
2019-01-10 15:39:37 +03:00
if ( ! mediaManager . embyInit ) {
initMediaManager ( ) ;
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 ( ) {
resolve ( videoElement ) ;
} ) ;
} else {
resolve ( videoElement ) ;
}
} ) ;
} else {
if ( options . backdropUrl ) {
dlg . classList . add ( 'videoPlayerContainer-withBackdrop' ) ;
dlg . style . backgroundImage = "url('" + options . backdropUrl + "')" ;
}
resolve ( dlg . querySelector ( 'video' ) ) ;
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
} ) ;
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
}
2018-10-23 01:05:09 +03:00
2019-01-10 15:39:37 +03:00
HtmlVideoPlayer . prototype . canPlayMediaType = function ( mediaType ) {
return ( mediaType || '' ) . toLowerCase ( ) === 'video' ;
} ;
HtmlVideoPlayer . prototype . supportsPlayMethod = function ( playMethod , item ) {
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 ;
} ;
HtmlVideoPlayer . prototype . getDeviceProfile = function ( item , options ) {
var instance = this ;
return getDeviceProfileInternal ( item , options ) . then ( function ( profile ) {
instance . _lastProfile = profile ;
return profile ;
} ) ;
} ;
function getDeviceProfileInternal ( item , options ) {
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 ( ) ;
}
var supportedFeatures ;
2019-05-22 23:06:39 -07:00
2019-01-10 15:39:37 +03:00
function getSupportedFeatures ( ) {
var list = [ ] ;
var video = document . createElement ( 'video' ) ;
2019-02-04 03:56:41 -05:00
if ( video . webkitSupportsPresentationMode && typeof video . webkitSetPresentationMode === "function" || document . pictureInPictureEnabled ) {
2019-01-10 15:39:37 +03:00
list . push ( 'PictureInPicture' ) ;
2019-05-22 23:06:39 -07:00
} else if ( browser . ipad ) {
2019-01-10 15:39:37 +03:00
// Unfortunately this creates a false positive on devices where its' not actually supported
if ( navigator . userAgent . toLowerCase ( ) . indexOf ( 'os 9' ) === - 1 ) {
if ( video . webkitSupportsPresentationMode && video . webkitSupportsPresentationMode && typeof video . webkitSetPresentationMode === "function" ) {
list . push ( 'PictureInPicture' ) ;
2018-10-23 01:05:09 +03:00
}
}
2019-01-10 15:39:37 +03:00
} 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
}
}
2019-01-10 15:39:37 +03:00
list . push ( 'SetBrightness' ) ;
2019-11-20 15:13:15 +01: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
}
2019-01-10 15:39:37 +03:00
HtmlVideoPlayer . prototype . supports = function ( feature ) {
if ( ! supportedFeatures ) {
supportedFeatures = getSupportedFeatures ( ) ;
}
return supportedFeatures . indexOf ( feature ) !== - 1 ;
2018-10-23 01:05:09 +03: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
HtmlVideoPlayer . prototype . currentTime = function ( val ) {
2018-10-23 01:05:09 +03:00
var mediaElement = this . _mediaElement ;
if ( mediaElement ) {
2019-01-10 15:39:37 +03:00
if ( val != null ) {
mediaElement . currentTime = val / 1000 ;
return ;
}
2018-10-23 01:05:09 +03:00
var 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
}
2019-01-10 15:39:37 +03:00
} ;
HtmlVideoPlayer . prototype . duration = function ( val ) {
2018-10-23 01:05:09 +03:00
var mediaElement = this . _mediaElement ;
if ( mediaElement ) {
var 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 ;
} ;
HtmlVideoPlayer . prototype . canSetAudioStreamIndex = function ( index ) {
if ( browser . tizen || browser . orsay ) {
return true ;
}
2018-10-23 01:05:09 +03:00
var video = this . _mediaElement ;
2019-01-10 15:39:37 +03:00
if ( video ) {
if ( video . audioTracks ) {
return true ;
}
}
return false ;
} ;
function onPictureInPictureError ( err ) {
console . log ( 'Picture in picture error: ' + err . toString ( ) ) ;
}
HtmlVideoPlayer . prototype . setPictureInPictureEnabled = function ( isEnabled ) {
2018-10-23 01:05:09 +03:00
var video = this . _mediaElement ;
2019-01-10 15:39:37 +03:00
if ( document . pictureInPictureEnabled ) {
if ( video ) {
if ( isEnabled ) {
video . requestPictureInPicture ( ) . catch ( onPictureInPictureError ) ;
} else {
document . exitPictureInPicture ( ) . catch ( onPictureInPictureError ) ;
}
}
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 {
if ( video && video . webkitSupportsPresentationMode && typeof video . webkitSetPresentationMode === "function" ) {
video . webkitSetPresentationMode ( isEnabled ? "picture-in-picture" : "inline" ) ;
2019-01-10 15:39:37 +03:00
}
}
} ;
HtmlVideoPlayer . prototype . isPictureInPictureEnabled = function ( ) {
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 {
2019-01-10 15:39:37 +03:00
var video = this . _mediaElement ;
if ( video ) {
return video . webkitPresentationMode === "picture-in-picture" ;
}
}
return false ;
} ;
HtmlVideoPlayer . prototype . setBrightness = function ( val ) {
2018-10-23 01:05:09 +03:00
var 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 ) ;
2018-10-23 01:05:09 +03:00
var rawValue = val ;
rawValue = Math . max ( 20 , rawValue ) ;
2019-01-10 15:39:37 +03:00
var cssValue = rawValue >= 100 ? 'none' : ( rawValue / 100 ) ;
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
}
2019-01-10 15:39:37 +03:00
} ;
HtmlVideoPlayer . prototype . getBrightness = function ( ) {
2018-10-23 01:05:09 +03:00
var elem = this . _mediaElement ;
if ( elem ) {
var val = elem . brightnessValue ;
2019-01-10 15:39:37 +03:00
return val == null ? 100 : val ;
2018-10-23 01:05:09 +03:00
}
2019-01-10 15:39:37 +03:00
} ;
HtmlVideoPlayer . prototype . seekable = function ( ) {
2018-10-23 01:05:09 +03:00
var mediaElement = this . _mediaElement ;
if ( mediaElement ) {
2019-01-10 15:39:37 +03:00
2018-10-23 01:05:09 +03:00
var seekable = mediaElement . seekable ;
if ( seekable && seekable . length ) {
2019-01-10 15:39:37 +03:00
var start = seekable . start ( 0 ) ;
var end = seekable . end ( 0 ) ;
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
}
2019-01-10 15:39:37 +03:00
} ;
HtmlVideoPlayer . prototype . pause = function ( ) {
var mediaElement = this . _mediaElement ;
if ( mediaElement ) {
mediaElement . pause ( ) ;
}
} ;
// This is a retry after error
HtmlVideoPlayer . prototype . resume = function ( ) {
2018-10-23 01:05:09 +03:00
var mediaElement = this . _mediaElement ;
2019-01-10 15:39:37 +03:00
if ( mediaElement ) {
mediaElement . play ( ) ;
}
} ;
HtmlVideoPlayer . prototype . unpause = function ( ) {
2018-10-23 01:05:09 +03:00
var mediaElement = this . _mediaElement ;
2019-01-10 15:39:37 +03:00
if ( mediaElement ) {
mediaElement . play ( ) ;
}
} ;
HtmlVideoPlayer . prototype . paused = function ( ) {
2018-10-23 01:05:09 +03:00
var mediaElement = this . _mediaElement ;
2019-01-10 15:39:37 +03:00
if ( mediaElement ) {
return mediaElement . paused ;
}
return false ;
} ;
HtmlVideoPlayer . prototype . setVolume = function ( val ) {
2018-10-23 01:05:09 +03:00
var mediaElement = this . _mediaElement ;
2019-01-10 15:39:37 +03:00
if ( mediaElement ) {
mediaElement . volume = val / 100 ;
}
} ;
HtmlVideoPlayer . prototype . getVolume = function ( ) {
2018-10-23 01:05:09 +03:00
var mediaElement = this . _mediaElement ;
2019-01-10 15:39:37 +03:00
if ( mediaElement ) {
return Math . min ( Math . round ( mediaElement . volume * 100 ) , 100 ) ;
}
} ;
HtmlVideoPlayer . prototype . volumeUp = function ( ) {
this . setVolume ( Math . min ( this . getVolume ( ) + 2 , 100 ) ) ;
} ;
HtmlVideoPlayer . prototype . volumeDown = function ( ) {
this . setVolume ( Math . max ( this . getVolume ( ) - 2 , 0 ) ) ;
} ;
HtmlVideoPlayer . prototype . setMute = function ( mute ) {
2018-10-23 01:05:09 +03:00
var mediaElement = this . _mediaElement ;
2019-01-10 15:39:37 +03:00
if ( mediaElement ) {
mediaElement . muted = mute ;
}
} ;
HtmlVideoPlayer . prototype . isMuted = function ( ) {
2018-10-23 01:05:09 +03:00
var mediaElement = this . _mediaElement ;
2019-01-10 15:39:37 +03:00
if ( mediaElement ) {
return mediaElement . muted ;
}
return false ;
} ;
HtmlVideoPlayer . prototype . setAspectRatio = function ( val ) {
2019-11-20 15:13:15 +01:00
var mediaElement = this . _mediaElement ;
if ( mediaElement ) {
if ( "auto" === val ) {
mediaElement . style . removeProperty ( "object-fit" )
} else {
mediaElement . style [ "object-fit" ] = val
}
}
this . _currentAspectRatio = val
2019-01-10 15:39:37 +03:00
} ;
HtmlVideoPlayer . prototype . getAspectRatio = function ( ) {
return this . _currentAspectRatio ;
} ;
HtmlVideoPlayer . prototype . getSupportedAspectRatios = function ( ) {
2019-11-20 15:13:15 +01:00
return [ {
name : "Auto" ,
id : "auto"
} , {
name : "Cover" ,
id : "cover"
} , {
name : "Fill" ,
id : "fill"
} ]
2019-01-10 15:39:37 +03:00
} ;
HtmlVideoPlayer . prototype . togglePictureInPicture = function ( ) {
return this . setPictureInPictureEnabled ( ! this . isPictureInPictureEnabled ( ) ) ;
} ;
HtmlVideoPlayer . prototype . getBufferedRanges = function ( ) {
2018-10-23 01:05:09 +03:00
var mediaElement = this . _mediaElement ;
2019-01-10 15:39:37 +03:00
if ( mediaElement ) {
return htmlMediaHelper . getBufferedRanges ( this , mediaElement ) ;
}
return [ ] ;
} ;
HtmlVideoPlayer . prototype . getStats = function ( ) {
2018-10-23 01:05:09 +03:00
var mediaElement = this . _mediaElement ;
2019-01-10 15:39:37 +03:00
var playOptions = this . _currentPlayOptions || [ ] ;
var categories = [ ] ;
if ( ! mediaElement ) {
return Promise . resolve ( {
categories : categories
} ) ;
}
2018-10-23 01:05:09 +03:00
var mediaCategory = {
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)
var link = document . createElement ( 'a' ) ;
// set href to any path
link . setAttribute ( 'href' , playOptions . url ) ;
var protocol = ( link . protocol || '' ) . replace ( ':' , '' ) ;
if ( protocol ) {
mediaCategory . stats . push ( {
label : 'Protocol:' ,
value : protocol
} ) ;
}
link = null ;
}
if ( this . _hlsPlayer || this . _shakaPlayer ) {
mediaCategory . stats . push ( {
label : 'Stream type:' ,
value : 'HLS'
} ) ;
} else {
mediaCategory . stats . push ( {
label : 'Stream type:' ,
value : 'Video'
} ) ;
}
2018-10-23 01:05:09 +03:00
var videoCategory = {
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
var rect = mediaElement . getBoundingClientRect ? mediaElement . getBoundingClientRect ( ) : { } ;
var height = rect . height ;
var width = rect . width ;
// 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 ( {
label : 'Player dimensions:' ,
value : width + 'x' + height
} ) ;
}
height = mediaElement . videoHeight ;
width = mediaElement . videoWidth ;
if ( width && height ) {
videoCategory . stats . push ( {
label : 'Video resolution:' ,
value : width + 'x' + height
} ) ;
}
if ( mediaElement . getVideoPlaybackQuality ) {
var playbackQuality = mediaElement . getVideoPlaybackQuality ( ) ;
var droppedVideoFrames = playbackQuality . droppedVideoFrames || 0 ;
2018-10-23 01:05:09 +03:00
videoCategory . stats . push ( {
2019-01-10 15:39:37 +03:00
label : 'Dropped frames:' ,
2018-10-23 01:05:09 +03:00
value : droppedVideoFrames
} ) ;
2019-01-10 15:39:37 +03:00
2018-10-23 01:05:09 +03:00
var corruptedVideoFrames = playbackQuality . corruptedVideoFrames || 0 ;
videoCategory . stats . push ( {
2019-01-10 15:39:37 +03:00
label : 'Corrupted frames:' ,
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
2018-10-23 01:05:09 +03:00
var audioCategory = {
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
2018-10-23 01:05:09 +03:00
var 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
} ) ;
} ;
if ( browser . chromecast ) {
mediaManager = new cast . receiver . MediaManager ( document . createElement ( 'video' ) ) ;
}
return HtmlVideoPlayer ;
2019-02-03 03:34:50 -05:00
} ) ;