1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00
jellyfin-web/src/components/htmlvideoplayer/plugin.js
MrTimscampi b1d1cee634 Use forked version of JavascriptSubtitlesOctopus
The fork contains an asm.js compatibility patch that we need for older client. This commit switched to using our forked version while we wait for an upstream merge.
2020-03-02 19:54:29 +01:00

1918 lines
63 KiB
JavaScript

define(['browser', 'require', 'events', 'apphost', 'loading', 'dom', 'playbackManager', 'appRouter', 'appSettings', 'connectionManager', 'htmlMediaHelper', 'itemHelper', 'fullscreenManager', 'globalize'], function (browser, require, events, appHost, loading, dom, playbackManager, appRouter, appSettings, connectionManager, htmlMediaHelper, itemHelper, fullscreenManager, globalize) {
"use strict";
/* globals cast */
var mediaManager;
function tryRemoveElement(elem) {
var parentNode = elem.parentNode;
if (parentNode) {
// Seeing crashes in edge webview
try {
parentNode.removeChild(elem);
} catch (err) {
console.error('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 '<track id="textTrack' + t.Index + '" label="' + label + '" kind="subtitles" src="' + getTextTrackUrl(t, item) + '" srclang="' + language + '"' + defaultAttribute + '></track>';
}).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.debug('prefetching hls playlist: ' + hlsPlaylistUrl);
return connectionManager.getApiClient(item.ServerId).ajax({
type: 'GET',
url: hlsPlaylistUrl
}).then(function () {
console.debug('completed prefetching hls playlist: ' + hlsPlaylistUrl);
loading.hide();
streamInfo.url = hlsPlaylistUrl;
return Promise.resolve();
}, function () {
console.error('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 () {
/* globals shaka */
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.debug('loading media url into media manager');
try {
mediaManager.load(lrd);
// This is needed in setCurrentTrackElement
self._currentSrc = url;
return Promise.resolve();
} catch (err) {
console.debug('media manager 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.debug('loading playback url: ' + url);
console.debug('content type: ' + contentType);
host.onError = function (errorCode) {
console.error("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.debug('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.debug("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.debug('found ' + elemAudioTracks.length + ' audio tracks');
for (i = 0, length = elemAudioTracks.length; i < length; i++) {
if (audioIndex === i) {
console.debug('setting audio track ' + i + ' to enabled');
elemAudioTracks[i].enabled = true;
} else {
console.debug('setting audio track ' + i + ' to disabled');
elemAudioTracks[i].enabled = false;
}
}
};
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.error('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(function (i) {
return i.DeliveryUrl;
}),
workerUrl: appRouter.baseUrl() + "/libraries/subtitles-octopus-worker.js",
legacyWorkerUrl: appRouter.baseUrl() + "/libraries/subtitles-octopus-worker-legacy.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()) {
console.debug('rendering subtitles with SubtitlesOctopus');
renderWithSubtitlesOctopus(videoElement, track, item);
} else {
console.debug('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.debug('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());
});
}
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.error('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.debug('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.error('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.debug('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 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');
}
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);
document.body.insertBefore(dlg, document.body.firstChild);
videoDialog = dlg;
self._mediaElement = videoElement;
if (mediaManager) {
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'));
}
});
}
}
HtmlVideoPlayer.prototype.canPlayMediaType = function (mediaType) {
return (mediaType || '').toLowerCase() === 'video';
};
HtmlVideoPlayer.prototype.supportsPlayMethod = function (playMethod, item) {
if (appHost.supportsPlayMethod) {
return appHost.supportsPlayMethod(playMethod, item);
}
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);
}
return getDefaultProfile();
}
var supportedFeatures;
function getSupportedFeatures() {
var list = [];
var video = document.createElement('video');
if (video.webkitSupportsPresentationMode && typeof video.webkitSetPresentationMode === "function" || document.pictureInPictureEnabled) {
list.push('PictureInPicture');
} else if (browser.ipad) {
// 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');
}
}
} else if (window.Windows) {
if (Windows.UI.ViewManagement.ApplicationView.getForCurrentView().isViewModeSupported(Windows.UI.ViewManagement.ApplicationViewMode.compactOverlay)) {
list.push('PictureInPicture');
}
}
if (browser.safari || browser.iOS || browser.iPad) {
list.push('AirPlay')
}
list.push('SetBrightness');
list.push("SetAspectRatio")
return list;
}
HtmlVideoPlayer.prototype.supports = function (feature) {
if (!supportedFeatures) {
supportedFeatures = getSupportedFeatures();
}
return supportedFeatures.indexOf(feature) !== -1;
};
// Save this for when playback stops, because querying the time at that point might return 0
HtmlVideoPlayer.prototype.currentTime = function (val) {
var mediaElement = this._mediaElement;
if (mediaElement) {
if (val != null) {
mediaElement.currentTime = val / 1000;
return;
}
var currentTime = this._currentTime;
if (currentTime) {
return currentTime * 1000;
}
return (mediaElement.currentTime || 0) * 1000;
}
};
HtmlVideoPlayer.prototype.duration = function (val) {
var mediaElement = this._mediaElement;
if (mediaElement) {
var duration = mediaElement.duration;
if (htmlMediaHelper.isValidDuration(duration)) {
return duration * 1000;
}
}
return null;
};
HtmlVideoPlayer.prototype.canSetAudioStreamIndex = function (index) {
if (browser.tizen || browser.orsay) {
return true;
}
var video = this._mediaElement;
if (video) {
if (video.audioTracks) {
return true;
}
}
return false;
};
function onPictureInPictureError(err) {
console.error('Picture in picture error: ' + err.toString());
}
HtmlVideoPlayer.prototype.setPictureInPictureEnabled = function (isEnabled) {
var video = this._mediaElement;
if (document.pictureInPictureEnabled) {
if (video) {
if (isEnabled) {
video.requestPictureInPicture().catch(onPictureInPictureError);
} else {
document.exitPictureInPicture().catch(onPictureInPictureError);
}
}
} else if (window.Windows) {
this.isPip = isEnabled;
if (isEnabled) {
Windows.UI.ViewManagement.ApplicationView.getForCurrentView().tryEnterViewModeAsync(Windows.UI.ViewManagement.ApplicationViewMode.compactOverlay);
} else {
Windows.UI.ViewManagement.ApplicationView.getForCurrentView().tryEnterViewModeAsync(Windows.UI.ViewManagement.ApplicationViewMode.default);
}
} else {
if (video && video.webkitSupportsPresentationMode && typeof video.webkitSetPresentationMode === "function") {
video.webkitSetPresentationMode(isEnabled ? "picture-in-picture" : "inline");
}
}
};
HtmlVideoPlayer.prototype.isPictureInPictureEnabled = function () {
if (document.pictureInPictureEnabled) {
return document.pictureInPictureElement ? true : false;
} else if (window.Windows) {
return this.isPip || false;
} else {
var video = this._mediaElement;
if (video) {
return video.webkitPresentationMode === "picture-in-picture";
}
}
return false;
};
HtmlVideoPlayer.prototype.isAirPlayEnabled = function () {
if (document.AirPlayEnabled) {
return document.AirplayElement ? true : false;
}
return false;
};
HtmlVideoPlayer.prototype.setAirPlayEnabled = function (isEnabled) {
var video = this._mediaElement;
if (document.AirPlayEnabled) {
if (video) {
if (isEnabled) {
video.requestAirPlay().catch(function(err) {
console.error("Error requesting AirPlay", err)
});
} else {
document.exitAirPLay().catch(function(err) {
console.error("Error exiting AirPlay", err)
});
}
}
} else {
video.webkitShowPlaybackTargetPicker();
}
};
HtmlVideoPlayer.prototype.setBrightness = function (val) {
var elem = this._mediaElement;
if (elem) {
val = Math.max(0, val);
val = Math.min(100, val);
var rawValue = val;
rawValue = Math.max(20, rawValue);
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');
}
};
HtmlVideoPlayer.prototype.getBrightness = function () {
var elem = this._mediaElement;
if (elem) {
var val = elem.brightnessValue;
return val == null ? 100 : val;
}
};
HtmlVideoPlayer.prototype.seekable = function () {
var mediaElement = this._mediaElement;
if (mediaElement) {
var seekable = mediaElement.seekable;
if (seekable && seekable.length) {
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;
}
return false;
}
};
HtmlVideoPlayer.prototype.pause = function () {
var mediaElement = this._mediaElement;
if (mediaElement) {
mediaElement.pause();
}
};
// This is a retry after error
HtmlVideoPlayer.prototype.resume = function () {
var mediaElement = this._mediaElement;
if (mediaElement) {
mediaElement.play();
}
};
HtmlVideoPlayer.prototype.unpause = function () {
var mediaElement = this._mediaElement;
if (mediaElement) {
mediaElement.play();
}
};
HtmlVideoPlayer.prototype.paused = function () {
var mediaElement = this._mediaElement;
if (mediaElement) {
return mediaElement.paused;
}
return false;
};
HtmlVideoPlayer.prototype.setVolume = function (val) {
var mediaElement = this._mediaElement;
if (mediaElement) {
mediaElement.volume = val / 100;
}
};
HtmlVideoPlayer.prototype.getVolume = function () {
var mediaElement = this._mediaElement;
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) {
var mediaElement = this._mediaElement;
if (mediaElement) {
mediaElement.muted = mute;
}
};
HtmlVideoPlayer.prototype.isMuted = function () {
var mediaElement = this._mediaElement;
if (mediaElement) {
return mediaElement.muted;
}
return false;
};
HtmlVideoPlayer.prototype.setAspectRatio = function (val) {
var mediaElement = this._mediaElement;
if (mediaElement) {
if ("auto" === val) {
mediaElement.style.removeProperty("object-fit")
} else {
mediaElement.style["object-fit"] = val
}
}
this._currentAspectRatio = val
};
HtmlVideoPlayer.prototype.getAspectRatio = function () {
return this._currentAspectRatio || "auto";
};
HtmlVideoPlayer.prototype.getSupportedAspectRatios = function () {
return [{
name: "Auto",
id: "auto"
}, {
name: "Cover",
id: "cover"
}, {
name: "Fill",
id: "fill"
}]
};
HtmlVideoPlayer.prototype.togglePictureInPicture = function () {
return this.setPictureInPictureEnabled(!this.isPictureInPictureEnabled());
};
HtmlVideoPlayer.prototype.toggleAirPlay = function () {
return this.setAirPlayEnabled(!this.isAirPlayEnabled());
};
HtmlVideoPlayer.prototype.getBufferedRanges = function () {
var mediaElement = this._mediaElement;
if (mediaElement) {
return htmlMediaHelper.getBufferedRanges(this, mediaElement);
}
return [];
};
HtmlVideoPlayer.prototype.getStats = function () {
var mediaElement = this._mediaElement;
var playOptions = this._currentPlayOptions || [];
var categories = [];
if (!mediaElement) {
return Promise.resolve({
categories: categories
});
}
var mediaCategory = {
stats: [],
type: 'media'
};
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: globalize.translate("LabelProtocol"),
value: protocol
});
}
link = null;
}
if (this._hlsPlayer || this._shakaPlayer) {
mediaCategory.stats.push({
label: globalize.translate("LabelStreamType"),
value: 'HLS'
});
} else {
mediaCategory.stats.push({
label: globalize.translate("LabelStreamType"),
value: 'Video'
});
}
var videoCategory = {
stats: [],
type: 'video'
};
categories.push(videoCategory);
var rect = mediaElement.getBoundingClientRect ? mediaElement.getBoundingClientRect() : {};
var height = parseInt(rect.height);
var width = parseInt(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: globalize.translate("LabelPlayerDimensions"),
value: width + 'x' + height
});
}
height = mediaElement.videoHeight;
width = mediaElement.videoWidth;
if (width && height) {
videoCategory.stats.push({
label: globalize.translate("LabelVideoResolution"),
value: width + 'x' + height
});
}
if (mediaElement.getVideoPlaybackQuality) {
var playbackQuality = mediaElement.getVideoPlaybackQuality();
var droppedVideoFrames = playbackQuality.droppedVideoFrames || 0;
videoCategory.stats.push({
label: globalize.translate("LabelDroppedFrames"),
value: droppedVideoFrames
});
var corruptedVideoFrames = playbackQuality.corruptedVideoFrames || 0;
videoCategory.stats.push({
label: globalize.translate("LabelCorruptedFrames"),
value: corruptedVideoFrames
});
}
var audioCategory = {
stats: [],
type: 'audio'
};
categories.push(audioCategory);
var sinkId = mediaElement.sinkId;
if (sinkId) {
audioCategory.stats.push({
label: 'Sink Id:',
value: sinkId
});
}
return Promise.resolve({
categories: categories
});
};
if (browser.chromecast) {
mediaManager = new cast.receiver.MediaManager(document.createElement('video'));
}
return HtmlVideoPlayer;
});