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) { "use strict"; var mediaManager; function tryRemoveElement(elem) { var parentNode = elem.parentNode; 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; } // For now, until ready return _supportsTextTracks; } function supportsCanvas() { return !!document.createElement('canvas').getContext; } function supportsWebWorkers() { return !!window.Worker; } function enableNativeTrackSupport(currentSrc, track) { 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; } } if (track) { var format = (track.Codec || '').toLowerCase(); if (format === 'ssa' || format === 'ass') { // libjass is needed here return false; } } return true; } function requireHlsPlayer(callback) { require(['hlsjs'], function (hls) { window.Hls = hls; callback(); }); } function getMediaStreamAudioTracks(mediaSource) { return mediaSource.MediaStreams.filter(function (s) { return s.Type === 'Audio'; }); } function getMediaStreamTextTracks(mediaSource) { return mediaSource.MediaStreams.filter(function (s) { return s.Type === 'Subtitle'; }); } function zoomIn(elem) { 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 }); }); } function normalizeTrackEventText(text) { return text.replace(/\\N/gi, '\n'); } function setTracks(elem, tracks, item, mediaSource) { elem.innerHTML = getTracksHtml(tracks, item, mediaSource); } function getTextTrackUrl(track, item, format) { if (itemHelper.isLocalItem(item) && track.Path) { return track.Path; } var url = playbackManager.getSubtitleUrl(track, item.ServerId); if (format) { url = url.replace('.vtt', format); } return url; } function getTracksHtml(tracks, item, mediaSource) { 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 ''; }).join(''); } function getDefaultProfile() { return new Promise(function (resolve, reject) { require(['browserdeviceprofile'], function (profileBuilder) { resolve(profileBuilder({})); }); }); } function HtmlVideoPlayer() { 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 currentSubtitlesOctopus; var currentAssRenderer; var customTrackIndex = -1; var showTrackOffset; var currentTrackOffset; var videoSubtitlesElem; var currentTrackEvents; var self = this; self.currentSrc = function () { return self._currentSrc; }; 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"); } } function updateVideoUrl(streamInfo) { 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', url: hlsPlaylistUrl }).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(); } } 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; self.resetSubtitleOffset(); return createMediaElement(options).then(function (elem) { return updateVideoUrl(options, options.mediaSource).then(function () { return setCurrentSrc(elem, options); }); }); }; function setSrcWithFlvJs(instance, elem, options, url) { return new Promise(function (resolve, reject) { require(['flvjs'], function (flvjs) { var flvPlayer = flvjs.createPlayer({ type: 'flv', url: url }, { seekType: 'range', lazyLoad: false }); flvPlayer.attachMediaElement(elem); flvPlayer.load(); flvPlayer.play().then(resolve, reject); instance._flvPlayer = flvPlayer; // This is needed in setCurrentTrackElement self._currentSrc = url; }); }); } function setSrcWithHlsJs(instance, elem, options, url) { return new Promise(function (resolve, reject) { requireHlsPlayer(function () { var hls = new Hls({ manifestLoadingTimeOut: 20000 //appendErrorMaxRetry: 6, //debug: true }); 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; }); }); } function setCurrentSrcChromecast(instance, elem, options, url) { 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'); try { mediaManager.load(lrd); // This is needed in setCurrentTrackElement self._currentSrc = url; return Promise.resolve(); } catch (err) { console.log('mediaManager error: ' + err); return Promise.reject(); } } // Adapted from : https://github.com/googlecast/CastReferencePlayer/blob/master/player.js function onMediaManagerLoadMedia(event) { if (self._castPlayer) { self._castPlayer.unload(); // Must unload before starting again. } 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(); } function initMediaManager() { 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); }; } function setCurrentSrc(elem, options) { elem.removeEventListener('error', onError); var val = options.url; 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); var tracks = getMediaStreamTextTracks(options.mediaSource); subtitleTrackIndexToSetOnPlaying = options.mediaSource.DefaultSubtitleStreamIndex == null ? -1 : options.mediaSource.DefaultSubtitleStreamIndex; if (subtitleTrackIndexToSetOnPlaying != null && subtitleTrackIndexToSetOnPlaying >= 0) { var initialSubtitleStream = options.mediaSource.MediaStreams[subtitleTrackIndexToSetOnPlaying]; if (!initialSubtitleStream || initialSubtitleStream.DeliveryMethod === 'Encode') { subtitleTrackIndexToSetOnPlaying = -1; } } audioTrackIndexToSetOnPlaying = options.playMethod === 'Transcode' ? null : options.mediaSource.DefaultAudioStreamIndex; self._currentPlayOptions = options; var crossOrigin = htmlMediaHelper.getCrossOriginValue(options.mediaSource); if (crossOrigin) { elem.crossOrigin = crossOrigin; } /*if (htmlMediaHelper.enableHlsShakaPlayer(options.item, options.mediaSource, 'Video') && val.indexOf('.m3u8') !== -1) { setTracks(elem, tracks, options.item, options.mediaSource); return setSrcWithShakaPlayer(self, elem, options, val); } else*/ if (browser.chromecast && val.indexOf('.m3u8') !== -1 && options.mediaSource.RunTimeTicks) { return setCurrentSrcChromecast(self, elem, options, val); } else if (htmlMediaHelper.enableHlsJsPlayer(options.mediaSource.RunTimeTicks, 'Video') && val.indexOf('.m3u8') !== -1) { return setSrcWithHlsJs(self, elem, options, val); } else if (options.playMethod !== 'Transcode' && options.mediaSource.Container === 'flv') { 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); }); } } self.setSubtitleStreamIndex = function (index) { setCurrentTrackElement(index); }; self.resetSubtitleOffset = function() { currentTrackOffset = 0; showTrackOffset = false; } self.enableShowingSubtitleOffset = function() { showTrackOffset = true; } self.disableShowingSubtitleOffset = function() { showTrackOffset = false; } self.isShowingSubtitleOffsetEnabled = function() { return showTrackOffset; } 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 { return null; } } self.setSubtitleOffset = function(offset) { var offsetValue = parseFloat(offset); // if .ass currently rendering if (currentAssRenderer) { updateCurrentTrackOffset(offsetValue); } else { var trackElement = getTextTrack(); // if .vtt currently rendering if (trackElement) { setTextTrackSubtitleOffset(trackElement, offsetValue); } else if (currentTrackEvents) { setTrackEventsSubtitleOffset(currentTrackEvents, offsetValue); } else { console.log("No available track, cannot apply offset: ", offsetValue); } } }; function updateCurrentTrackOffset(offsetValue) { var relativeOffset = offsetValue; var newTrackOffset = offsetValue; if (currentTrackOffset) { relativeOffset -= currentTrackOffset; } currentTrackOffset = newTrackOffset; // relative to currentTrackOffset return relativeOffset; } function setTextTrackSubtitleOffset(currentTrack, offsetValue) { if (currentTrack.cues) { offsetValue = updateCurrentTrackOffset(offsetValue); Array.from(currentTrack.cues) .forEach(function(cue) { cue.startTime -= offsetValue; cue.endTime -= offsetValue; }); } } function setTrackEventsSubtitleOffset(trackEvents, offsetValue) { if (Array.isArray(trackEvents)) { offsetValue = updateCurrentTrackOffset(offsetValue); trackEvents.forEach(function(trackEvent) { trackEvent.StartPositionTicks -= offsetValue; trackEvent.EndPositionTicks -= offsetValue; }); } } self.getSubtitleOffset = function() { return currentTrackOffset; } function isAudioStreamSupported(stream, deviceProfile) { var codec = (stream.Codec || '').toLowerCase(); if (!codec) { return true; } if (!deviceProfile) { // This should never happen return true; } 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; } return false; }).length > 0; } function getSupportedAudioStreams() { var profile = self._lastProfile; return getMediaStreamAudioTracks(self._currentPlayOptions.mediaSource).filter(function (stream) { return isAudioStreamSupported(stream, profile); }); } 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; } var audioIndex = -1; var i; var length; var stream; 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); } fullscreenManager.exitFullscreen(); }; function onEnded() { destroyCustomTrack(this); htmlMediaHelper.onEndedInternal(self, this, onError); } function onTimeUpdate(e) { // get the player position and the transcoding offset 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(); } } function onStartedAndNavigatedToOsd() { // 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 octopus = currentSubtitlesOctopus; if (octopus) { octopus.dispose(); } currentSubtitlesOctopus = 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); } incrementFetchQueue(); 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)); decrementFetchQueue(); }; xhr.onerror = function (e) { reject(e); decrementFetchQueue(); } xhr.send(); }); } function setTrackForDisplay(videoElement, track) { if (!track) { destroyCustomTrack(videoElement); return; } // skip if already playing this track if (customTrackIndex === track.Index) { return; } self.resetSubtitleOffset(); var item = self._currentPlayOptions.item; destroyCustomTrack(videoElement); customTrackIndex = track.Index; renderTracksEvents(videoElement, track, item); lastCustomTrackMs = 0; } function renderWithSubtitlesOctopus(videoElement, track, item) { var attachments = self._currentPlayOptions.mediaSource.MediaAttachments || []; var options = { video: videoElement, subUrl: getTextTrackUrl(track, item), fonts: attachments.map(i => i.DeliveryUrl), workerUrl: appRouter.baseUrl() + "/libraries/subtitles-octopus-worker.js", onError: function() { htmlMediaHelper.onErrorInternal(self, 'mediadecodeerror') } }; require(['JavascriptSubtitlesOctopus'], function(SubtitlesOctopus) { currentSubtitlesOctopus = new SubtitlesOctopus(options); }); } function renderWithLibjass(videoElement, track, item) { var rendererSettings = {}; if (browser.ps4) { // Text outlines are not rendering very well rendererSettings.enableSvg = false; } else if (browser.edge || browser.msie) { // 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(); currentClock = clock; // Create a DefaultRenderer using the video element and the ASS object var renderer = new libjass.renderers.WebRenderer(ass, clock, videoElement.parentNode, rendererSettings); currentAssRenderer = renderer; renderer.addEventListener("ready", function () { try { 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 renderSsaAss(videoElement, track, item) { if (supportsCanvas() && supportsWebWorkers()) { renderWithSubtitlesOctopus(videoElement, track, item); } else { console.log('rendering subtitles with libjass'); renderWithLibjass(videoElement, track, item); } } function onVideoResize() { if (browser.iOS) { // the new sizes will be delayed for about 500ms with wkwebview 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 = '
'; 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()); }); } 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 renderSsaAss(videoElement, track, item); return; } if (requiresCustomSubtitlesElement()) { renderSubtitlesWithCustomElement(videoElement, track, item); return; } } var trackElement = null; if (videoElement.textTracks && videoElement.textTracks.length > 0) { trackElement = videoElement.textTracks[0]; // 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'); } 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'); } // download the track json fetchSubtitles(track, item).then(function (data) { // 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) { var trackCueObject = window.VTTCue || window.TextTrackCue; var cue = new trackCueObject(trackEvent.StartPositionTicks / 10000000, trackEvent.EndPositionTicks / 10000000, normalizeTrackEventText(trackEvent.Text)); trackElement.addCue(cue); }); trackElement.mode = 'showing'; }); } function updateSubtitleText(timeMs) { // handle offset for ass tracks if (currentTrackOffset) { timeMs += (currentTrackOffset * 1000); } 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]; setTrackForDisplay(self._mediaElement, track); 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 += '