define(["browser", "require", "events", "apphost", "loading", "dom", "playbackManager", "appRouter", "appSettings", "connectionManager", "htmlMediaHelper", "itemHelper"], function(browser, require, events, appHost, loading, dom, playbackManager, appRouter, appSettings, connectionManager, htmlMediaHelper, itemHelper) { "use strict"; function tryRemoveElement(elem) { var parentNode = elem.parentNode; if (parentNode) try { parentNode.removeChild(elem) } catch (err) { console.log("Error removing dialog element: " + err) } } function enableNativeTrackSupport(currentSrc, track) { if (track && "Embed" === track.DeliveryMethod) return !0; if (browser.firefox && -1 !== (currentSrc || "").toLowerCase().indexOf(".m3u8")) return !1; if (browser.chromecast && -1 !== (currentSrc || "").toLowerCase().indexOf(".m3u8")) return !1; if (browser.ps4) return !1; if (browser.web0s) return !1; if (browser.edge) return !1; if (browser.iOS && (browser.iosVersion || 10) < 10) return !1; if (track) { var format = (track.Codec || "").toLowerCase(); if ("ssa" === format || "ass" === format) return !1 } return !0 } function requireHlsPlayer(callback) { require(["hlsjs"], function(hls) { window.Hls = hls, callback() }) } function getMediaStreamAudioTracks(mediaSource) { return mediaSource.MediaStreams.filter(function(s) { return "Audio" === s.Type }) } function getMediaStreamTextTracks(mediaSource) { return mediaSource.MediaStreams.filter(function(s) { return "Subtitle" === s.Type }) } function zoomIn(elem) { return new Promise(function(resolve, reject) { elem.style.animation = "htmlvideoplayer-zoomin 240ms ease-in normal", dom.addEventListener(elem, dom.whichAnimationEvent(), resolve, { once: !0 }) }) } 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); return format && (url = url.replace(".vtt", format)), url } function getTracksHtml(tracks, item, mediaSource) { return tracks.map(function(t) { if ("External" !== t.DeliveryMethod) return ""; var defaultAttribute = mediaSource.DefaultSubtitleStreamIndex === t.Index ? " default" : "", language = t.Language || "und", label = t.Language || "und"; return '" }).join("") } function getDefaultProfile() { return new Promise(function(resolve, reject) { require(["browserdeviceprofile"], function(profileBuilder) { resolve(profileBuilder({})) }) }) } function HtmlVideoPlayer() { function updateVideoUrl(streamInfo) { var isHls = -1 !== streamInfo.url.toLowerCase().indexOf(".m3u8"), mediaSource = streamInfo.mediaSource, item = streamInfo.item; if (mediaSource && item && !mediaSource.RunTimeTicks && isHls && "Transcode" === streamInfo.playMethod && (browser.iOS || browser.osx)) { var hlsPlaylistUrl = streamInfo.url.replace("master.m3u8", "live.m3u8"); return loading.show(), console.log("prefetching hls playlist: " + hlsPlaylistUrl), connectionManager.getApiClient(item.ServerId).ajax({ type: "GET", url: hlsPlaylistUrl }).then(function() { return console.log("completed prefetching hls playlist: " + hlsPlaylistUrl), loading.hide(), streamInfo.url = hlsPlaylistUrl, Promise.resolve() }, function() { return console.log("error prefetching hls playlist: " + hlsPlaylistUrl), loading.hide(), Promise.resolve() }) } return Promise.resolve() } 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: !1 }); flvPlayer.attachMediaElement(elem), flvPlayer.load(), flvPlayer.play().then(resolve, reject), instance._flvPlayer = flvPlayer, self._currentSrc = url }) }) } function setSrcWithHlsJs(instance, elem, options, url) { return new Promise(function(resolve, reject) { requireHlsPlayer(function() { var hls = new Hls({ manifestLoadingTimeOut: 2e4 }); hls.loadSource(url), hls.attachMedia(elem), htmlMediaHelper.bindEventsToHlsPlayer(self, hls, elem, onError, resolve, reject), self._hlsPlayer = hls, self._currentSrc = url }) }) } function setCurrentSrcChromecast(instance, elem, options, url) { elem.autoplay = !0; var lrd = new cast.receiver.MediaManager.LoadRequestData; lrd.currentTime = (options.playerStartPositionTicks || 0) / 1e7, lrd.autoplay = !0, 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 { return mediaManager.load(lrd), self._currentSrc = url, Promise.resolve() } catch (err) { return console.log("mediaManager error: " + err), Promise.reject() } } function onMediaManagerLoadMedia(event) { self._castPlayer && self._castPlayer.unload(), self._castPlayer = null; var protocol, data = event.data, media = event.data.media || {}, url = media.contentId, contentType = media.contentType.toLowerCase(), mediaElement = (media.customData, self._mediaElement), host = new cast.player.api.Host({ url: url, mediaElement: mediaElement }); protocol = cast.player.api.CreateHlsStreamingProtocol(host), console.log("loading playback url: " + url), console.log("contentType: " + contentType), host.onError = function(errorCode) { console.log("Fatal Error - " + errorCode) }, mediaElement.autoplay = !1, 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.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); var seconds = (options.playerStartPositionTicks || 0) / 1e7; seconds && (val += "#t=" + seconds), htmlMediaHelper.destroyHlsPlayer(self), htmlMediaHelper.destroyFlvPlayer(self), htmlMediaHelper.destroyCastPlayer(self); var tracks = getMediaStreamTextTracks(options.mediaSource); if (null != (subtitleTrackIndexToSetOnPlaying = null == options.mediaSource.DefaultSubtitleStreamIndex ? -1 : options.mediaSource.DefaultSubtitleStreamIndex) && subtitleTrackIndexToSetOnPlaying >= 0) { var initialSubtitleStream = options.mediaSource.MediaStreams[subtitleTrackIndexToSetOnPlaying]; initialSubtitleStream && "Encode" !== initialSubtitleStream.DeliveryMethod || (subtitleTrackIndexToSetOnPlaying = -1) } audioTrackIndexToSetOnPlaying = "Transcode" === options.playMethod ? null : options.mediaSource.DefaultAudioStreamIndex, self._currentPlayOptions = options; var crossOrigin = htmlMediaHelper.getCrossOriginValue(options.mediaSource); return crossOrigin && (elem.crossOrigin = crossOrigin), browser.chromecast && -1 !== val.indexOf(".m3u8") && options.mediaSource.RunTimeTicks ? (setTracks(elem, tracks, options.item, options.mediaSource), setCurrentSrcChromecast(self, elem, options, val)) : htmlMediaHelper.enableHlsJsPlayer(options.mediaSource.RunTimeTicks, "Video") && -1 !== val.indexOf(".m3u8") ? (setTracks(elem, tracks, options.item, options.mediaSource), setSrcWithHlsJs(self, elem, options, val)) : "Transcode" !== options.playMethod && "flv" === options.mediaSource.Container ? (setTracks(elem, tracks, options.item, options.mediaSource), setSrcWithFlvJs(self, elem, options, val)) : (elem.autoplay = !0, htmlMediaHelper.applySrc(elem, val, options).then(function() { return setTracks(elem, tracks, options.item, options.mediaSource), self._currentSrc = val, htmlMediaHelper.playWithPromise(elem, onError) })) } function isAudioStreamSupported(stream, deviceProfile) { var codec = (stream.Codec || "").toLowerCase(); return !codec || (!deviceProfile || (deviceProfile.DirectPlayProfiles || []).filter(function(p) { return "Video" === p.Type && (!p.AudioCodec || -1 !== p.AudioCodec.toLowerCase().indexOf(codec)) }).length > 0) } function getSupportedAudioStreams() { var profile = self._lastProfile; return getMediaStreamAudioTracks(self._currentPlayOptions.mediaSource).filter(function(stream) { return isAudioStreamSupported(stream, profile) }) } function onEnded() { destroyCustomTrack(this), htmlMediaHelper.onEndedInternal(self, this, onError) } function onTimeUpdate(e) { var time = this.currentTime; time && !self._timeUpdated && (self._timeUpdated = !0, ensureValidVideo(this)), self._currentTime = time; var currentPlayOptions = self._currentPlayOptions; if (currentPlayOptions) { var timeMs = 1e3 * time; timeMs += (currentPlayOptions.transcodingOffsetTicks || 0) / 1e4, updateSubtitleText(timeMs) } events.trigger(self, "timeupdate") } function onVolumeChange() { htmlMediaHelper.saveVolume(this.volume), events.trigger(self, "volumechange") } function onNavigatedToOsd() { var dlg = videoDialog; dlg && (dlg.classList.remove("videoPlayerContainer-withBackdrop"), dlg.classList.remove("videoPlayerContainer-onTop"), onStartedAndNavigatedToOsd()) } function onStartedAndNavigatedToOsd() { setCurrentTrackElement(subtitleTrackIndexToSetOnPlaying), null != audioTrackIndexToSetOnPlaying && self.canSetAudioStreamIndex() && self.setAudioStreamIndex(audioTrackIndexToSetOnPlaying) } function onPlaying(e) { self._started || (self._started = !0, this.removeAttribute("controls"), loading.hide(), htmlMediaHelper.seekOnPlaybackStart(self, e.target, self._currentPlayOptions.playerStartPositionTicks), self._currentPlayOptions.fullscreen ? appRouter.showVideoOsd().then(onNavigatedToOsd) : (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 && 0 === elem.videoWidth && 0 === elem.videoHeight) { var mediaSource = (self._currentPlayOptions || {}).mediaSource; if (!mediaSource || mediaSource.RunTimeTicks) return void htmlMediaHelper.onErrorInternal(self, "mediadecodeerror") } } 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, errorMessage = this.error ? this.error.message || "" : ""; console.log("Media element error: " + errorCode.toString() + " " + errorMessage); var type; switch (errorCode) { case 1: return; case 2: type = "network"; break; case 3: if (self._hlsPlayer) return void htmlMediaHelper.handleHlsJsMediaError(self); type = "mediadecodeerror"; break; case 4: type = "medianotsupported"; break; default: return } htmlMediaHelper.onErrorInternal(self, type) } function destroyCustomTrack(videoElement) { if (self._resizeObserver && (self._resizeObserver.disconnect(), self._resizeObserver = null), videoSubtitlesElem) { var subtitlesContainer = videoSubtitlesElem.parentNode; subtitlesContainer && tryRemoveElement(subtitlesContainer), videoSubtitlesElem = null } if (currentTrackEvents = null, videoElement) for (var allTracks = videoElement.textTracks || [], i = 0; i < allTracks.length; i++) { var currentTrack = allTracks[i]; - 1 !== currentTrack.label.indexOf("manualTrack") && (currentTrack.mode = "disabled") } customTrackIndex = -1, currentClock = null, self._currentAspectRatio = null; var renderer = currentAssRenderer; renderer && renderer.setEnabled(!1), currentAssRenderer = null } 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) { return window.Windows && itemHelper.isLocalItem(item) ? fetchSubtitlesUwp(track, item) : new Promise(function(resolve, reject) { var xhr = new XMLHttpRequest, url = getTextTrackUrl(track, item, ".js"); xhr.open("GET", url, !0), xhr.onload = function(e) { resolve(JSON.parse(this.response)) }, xhr.onerror = reject, xhr.send() }) } function setTrackForCustomDisplay(videoElement, track) { if (!track) return void destroyCustomTrack(videoElement); if (customTrackIndex !== track.Index) { var item = self._currentPlayOptions.item; destroyCustomTrack(videoElement), customTrackIndex = track.Index, renderTracksEvents(videoElement, track, item), lastCustomTrackMs = 0 } } function renderWithLibjass(videoElement, track, item) { var rendererSettings = {}; browser.ps4 ? rendererSettings.enableSvg = !1 : (browser.edge || browser.msie) && (rendererSettings.enableSvg = !1), rendererSettings.enableSvg = !1, require(["libjass", "ResizeObserver"], function(libjass, ResizeObserver) { libjass.ASS.fromUrl(getTextTrackUrl(track, item)).then(function(ass) { var clock = new libjass.renderers.ManualClock; currentClock = clock; 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), self._resizeObserver || (self._resizeObserver = new ResizeObserver(onVideoResize, {}), self._resizeObserver.observe(videoElement)) } catch (ex) {} }) }, function() { htmlMediaHelper.onErrorInternal(self, "mediadecodeerror") }) }) } function onVideoResize() { browser.iOS ? setTimeout(resetVideoRendererSize, 500) : resetVideoRendererSize() } function resetVideoRendererSize() { var renderer = currentAssRenderer; if (renderer) { var videoElement = self._mediaElement, width = videoElement.offsetWidth, height = videoElement.offsetHeight; console.log("videoElement resized: " + width + "x" + height), renderer.resize(width, height, 0, 0) } } function requiresCustomSubtitlesElement() { if (browser.ps4) return !0; if (browser.firefox || browser.web0s) return !0; if (browser.edge) return !0; if (browser.iOS) { var userAgent = navigator.userAgent.toLowerCase(); if ((-1 !== userAgent.indexOf("os 9") || -1 !== userAgent.indexOf("os 8")) && -1 === userAgent.indexOf("safari")) return !0 } return !1 } 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 {"; return html += appearance.text.map(function(s) { return s.name + ":" + s.value + "!important;" }).join(""), html += "}" } function setCueAppearance() { require(["userSettings", "subtitleAppearanceHelper"], function(userSettings, subtitleAppearanceHelper) { var elementId = self.id + "-cuestyle", styleElem = document.querySelector("#" + elementId); 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(), !0), ".htmlvideoplayer") }) } function renderTracksEvents(videoElement, track, item) { if (!itemHelper.isLocalItem(item) || track.IsExternal) { var format = (track.Codec || "").toLowerCase(); if ("ssa" === format || "ass" === format) return void renderWithLibjass(videoElement, track, item); if (requiresCustomSubtitlesElement()) return void renderSubtitlesWithCustomElement(videoElement, track, item) } for (var trackElement = null, expectedId = "manualTrack" + track.Index, allTracks = videoElement.textTracks, i = 0; i < allTracks.length; i++) { var currentTrack = allTracks[i]; if (currentTrack.label === expectedId) { trackElement = currentTrack; break } currentTrack.mode = "disabled" } trackElement ? trackElement.mode = "showing" : (trackElement = videoElement.addTextTrack("subtitles", "manualTrack" + track.Index, track.Language || "und"), fetchSubtitles(track, item).then(function(data) { console.log("downloaded " + data.TrackEvents.length + " track events"), data.TrackEvents.forEach(function(trackEvent) { var trackCueObject = window.VTTCue || window.TextTrackCue, cue = new trackCueObject(trackEvent.StartPositionTicks / 1e7, trackEvent.EndPositionTicks / 1e7, normalizeTrackEventText(trackEvent.Text)); trackElement.addCue(cue) }), trackElement.mode = "showing" })) } function updateSubtitleText(timeMs) { var clock = currentClock; if (clock) try { clock.seek(timeMs / 1e3) } catch (err) { console.log("Error in libjass: " + err) } else { var trackEvents = currentTrackEvents, subtitleTextElement = videoSubtitlesElem; if (trackEvents && subtitleTextElement) { for (var selectedTrackEvent, ticks = 1e4 * timeMs, i = 0; i < trackEvents.length; i++) { var currentTrackEvent = trackEvents[i]; if (currentTrackEvent.StartPositionTicks <= ticks && currentTrackEvent.EndPositionTicks >= ticks) { selectedTrackEvent = currentTrackEvent; break } } selectedTrackEvent && selectedTrackEvent.Text ? (subtitleTextElement.innerHTML = normalizeTrackEventText(selectedTrackEvent.Text), subtitleTextElement.classList.remove("hide")) : subtitleTextElement.classList.add("hide") } } } function setCurrentTrackElement(streamIndex) { console.log("Setting new text track index to: " + streamIndex); var mediaStreamTextTracks = getMediaStreamTextTracks(self._currentPlayOptions.mediaSource), track = -1 === streamIndex ? null : mediaStreamTextTracks.filter(function(t) { return t.Index === streamIndex })[0]; enableNativeTrackSupport(self._currentSrc, track) ? (setTrackForCustomDisplay(self._mediaElement, null), -1 !== streamIndex && setCueAppearance()) : (setTrackForCustomDisplay(self._mediaElement, track), streamIndex = -1, track = null); for (var expectedId = "textTrack" + streamIndex, trackIndex = -1 !== streamIndex && track ? mediaStreamTextTracks.indexOf(track) : -1, modes = ["disabled", "showing", "hidden"], allTracks = self._mediaElement.textTracks, i = 0; i < allTracks.length; i++) { var currentTrack = allTracks[i]; console.log("currentTrack id: " + currentTrack.id); var mode; if (console.log("expectedId: " + expectedId + "--currentTrack.Id:" + currentTrack.id), browser.msie || browser.edge) mode = trackIndex === i ? 1 : 0; else { if (-1 !== currentTrack.label.indexOf("manualTrack")) continue; mode = currentTrack.id === expectedId ? 1 : 0 } console.log("Setting track " + i + " mode to: " + mode), currentTrack.mode = modes[mode] } } function createMediaElement(options) { return (browser.tv || browser.iOS || browser.mobile) && (options.backdropUrl = null), new Promise(function(resolve, reject) { var dlg = document.querySelector(".videoPlayerContainer"); dlg ? (options.backdropUrl && (dlg.classList.add("videoPlayerContainer-withBackdrop"), dlg.style.backgroundImage = "url('" + options.backdropUrl + "')"), resolve(dlg.querySelector("video"))) : require(["css!./style"], function() { loading.show(); var dlg = document.createElement("div"); dlg.classList.add("videoPlayerContainer"), options.backdropUrl && (dlg.classList.add("videoPlayerContainer-withBackdrop"), dlg.style.backgroundImage = "url('" + options.backdropUrl + "')"), options.fullscreen && dlg.classList.add("videoPlayerContainer-onTop"); var html = "", cssClass = "htmlvideoplayer"; browser.chromecast || (cssClass += " htmlvideoplayer-moveupsubtitles"), appHost.supports("htmlvideoautoplay") ? html += '