mirror of
https://github.com/jellyfin/jellyfin-web.git
synced 2024-11-18 03:18:19 -07:00
491 lines
15 KiB
JavaScript
491 lines
15 KiB
JavaScript
(function (window) {
|
|
|
|
function getDeviceProfile(serverAddress, deviceId, item, startPositionTicks, maxBitrate, mediaSourceId, audioStreamIndex, subtitleStreamIndex) {
|
|
|
|
var bitrateSetting = AppSettings.maxStreamingBitrate();
|
|
|
|
var profile = {};
|
|
|
|
profile.MaxStreamingBitrate = bitrateSetting;
|
|
profile.MaxStaticBitrate = 40000000;
|
|
profile.MusicStreamingTranscodingBitrate = Math.min(bitrateSetting, 192000);
|
|
|
|
profile.DirectPlayProfiles = [];
|
|
|
|
profile.DirectPlayProfiles.push({
|
|
Container: 'mkv,mov,mp4,m4v,wmv',
|
|
Type: 'Video'
|
|
});
|
|
|
|
profile.DirectPlayProfiles.push({
|
|
Container: 'aac,mp3,flac,wma',
|
|
Type: 'Audio'
|
|
});
|
|
|
|
profile.TranscodingProfiles = [];
|
|
|
|
profile.TranscodingProfiles.push({
|
|
Container: 'ts',
|
|
Type: 'Video',
|
|
AudioCodec: 'aac',
|
|
VideoCodec: 'h264',
|
|
Context: 'Streaming',
|
|
Protocol: 'hls'
|
|
});
|
|
|
|
profile.TranscodingProfiles.push({
|
|
Container: 'aac',
|
|
Type: 'Audio',
|
|
AudioCodec: 'aac',
|
|
Context: 'Streaming',
|
|
Protocol: 'hls'
|
|
});
|
|
|
|
profile.ContainerProfiles = [];
|
|
|
|
var audioConditions = [];
|
|
|
|
var maxAudioChannels = '6';
|
|
|
|
audioConditions.push({
|
|
Condition: 'LessThanEqual',
|
|
Property: 'AudioChannels',
|
|
Value: maxAudioChannels
|
|
});
|
|
|
|
profile.CodecProfiles = [];
|
|
profile.CodecProfiles.push({
|
|
Type: 'Audio',
|
|
Conditions: audioConditions
|
|
});
|
|
|
|
profile.CodecProfiles.push({
|
|
Type: 'VideoAudio',
|
|
Codec: 'mp3',
|
|
Conditions: [{
|
|
Condition: 'LessThanEqual',
|
|
Property: 'AudioChannels',
|
|
Value: maxAudioChannels
|
|
}]
|
|
});
|
|
|
|
profile.CodecProfiles.push({
|
|
Type: 'VideoAudio',
|
|
Codec: 'aac',
|
|
Conditions: [
|
|
{
|
|
Condition: 'LessThanEqual',
|
|
Property: 'AudioChannels',
|
|
Value: maxAudioChannels
|
|
}
|
|
]
|
|
});
|
|
|
|
profile.CodecProfiles.push({
|
|
Type: 'Video',
|
|
Codec: 'h264',
|
|
Conditions: [
|
|
{
|
|
Condition: 'EqualsAny',
|
|
Property: 'VideoProfile',
|
|
Value: 'high|main|baseline|constrained baseline'
|
|
},
|
|
{
|
|
Condition: 'LessThanEqual',
|
|
Property: 'VideoLevel',
|
|
Value: '41'
|
|
}]
|
|
});
|
|
|
|
// Subtitle profiles
|
|
profile.SubtitleProfiles = [];
|
|
profile.ResponseProfiles = [];
|
|
|
|
return profile;
|
|
}
|
|
|
|
function validatePlaybackInfoResult(result) {
|
|
|
|
if (result.ErrorCode) {
|
|
|
|
MediaController.showPlaybackInfoErrorMessage(result.ErrorCode);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function getOptimalMediaSource(mediaType, versions) {
|
|
|
|
var optimalVersion = versions.filter(function (v) {
|
|
|
|
v.enableDirectPlay = MediaController.supportsDirectPlay(v);
|
|
|
|
return v.enableDirectPlay;
|
|
|
|
})[0];
|
|
|
|
if (!optimalVersion) {
|
|
optimalVersion = versions.filter(function (v) {
|
|
|
|
return v.SupportsDirectStream;
|
|
|
|
})[0];
|
|
}
|
|
|
|
return optimalVersion || versions.filter(function (s) {
|
|
return s.SupportsTranscoding;
|
|
})[0];
|
|
}
|
|
|
|
var currentMediaSource;
|
|
var currentItem;
|
|
var basePlayerState;
|
|
var progressInterval;
|
|
|
|
function getVideoStreamInfo(item) {
|
|
|
|
var deferred = $.Deferred();
|
|
Dashboard.showModalLoadingMsg();
|
|
|
|
var deviceProfile = getDeviceProfile();
|
|
var startPosition = 0;
|
|
|
|
MediaController.getPlaybackInfo(item.Id, deviceProfile, startPosition).done(function (playbackInfoResult) {
|
|
|
|
if (validatePlaybackInfoResult(playbackInfoResult)) {
|
|
|
|
var mediaSource = getOptimalMediaSource(item.MediaType, playbackInfoResult.MediaSources);
|
|
|
|
if (mediaSource) {
|
|
|
|
if (mediaSource.RequiresOpening) {
|
|
|
|
MediaController.getLiveStream(item.Id, playbackInfoResult.PlaySessionId, deviceProfile, startPosition, mediaSource, null, null).done(function (openLiveStreamResult) {
|
|
|
|
openLiveStreamResult.MediaSource.enableDirectPlay = MediaController.supportsDirectPlay(openLiveStreamResult.MediaSource);
|
|
|
|
playInternalPostMediaSourceSelection(item, openLiveStreamResult.MediaSource, startPosition, deferred);
|
|
});
|
|
|
|
} else {
|
|
playInternalPostMediaSourceSelection(item, mediaSource, startPosition, deferred);
|
|
}
|
|
} else {
|
|
Dashboard.hideModalLoadingMsg();
|
|
MediaController.showPlaybackInfoErrorMessage('NoCompatibleStream');
|
|
}
|
|
}
|
|
|
|
});
|
|
|
|
return deferred.promise();
|
|
}
|
|
|
|
function playInternalPostMediaSourceSelection(item, mediaSource, startPosition, deferred) {
|
|
|
|
Dashboard.hideModalLoadingMsg();
|
|
|
|
currentItem = item;
|
|
currentMediaSource = mediaSource;
|
|
|
|
basePlayerState = {
|
|
PlayState: {
|
|
|
|
}
|
|
};
|
|
|
|
var streamInfo = MediaPlayer.createStreamInfo('Video', item, mediaSource, startPosition);
|
|
var currentSrc = streamInfo.url;
|
|
|
|
var audioStreamIndex = getParameterByName('AudioStreamIndex', currentSrc);
|
|
|
|
if (audioStreamIndex) {
|
|
basePlayerState.PlayState.AudioStreamIndex = parseInt(audioStreamIndex);
|
|
}
|
|
basePlayerState.PlayState.SubtitleStreamIndex = self.currentSubtitleStreamIndex;
|
|
|
|
basePlayerState.PlayState.PlayMethod = getParameterByName('static', currentSrc) == 'true' ?
|
|
'DirectStream' :
|
|
'Transcode';
|
|
|
|
basePlayerState.PlayState.LiveStreamId = getParameterByName('LiveStreamId', currentSrc);
|
|
basePlayerState.PlayState.PlaySessionId = getParameterByName('PlaySessionId', currentSrc);
|
|
|
|
basePlayerState.PlayState.MediaSourceId = mediaSource.Id;
|
|
basePlayerState.PlayState.CanSeek = false;
|
|
basePlayerState.NowPlayingItem = MediaPlayer.getNowPlayingItemForReporting(item, mediaSource);
|
|
|
|
deferred.resolveWith(null, [streamInfo]);
|
|
}
|
|
|
|
function getPlayerState(positionTicks) {
|
|
|
|
var state = basePlayerState;
|
|
|
|
state.PlayState.PositionTicks = positionTicks;
|
|
|
|
return state;
|
|
}
|
|
|
|
function onPlaybackStart() {
|
|
|
|
closePlayMenu();
|
|
|
|
var state = getPlayerState();
|
|
|
|
var info = {
|
|
ItemId: state.NowPlayingItem.Id,
|
|
NowPlayingItem: state.NowPlayingItem
|
|
};
|
|
|
|
info = $.extend(info, state.PlayState);
|
|
|
|
ApiClient.reportPlaybackStart(info);
|
|
|
|
// This is really just a ping to let the server know we're still playing
|
|
progressInterval = setInterval(function () {
|
|
onPlaybackProgress(null);
|
|
|
|
}, 10000);
|
|
|
|
// Need a timeout because we can't show a popup at the same time as the previous one is closing
|
|
// Bumping it up to 1000 because the post play menu is hiding for some reason on android
|
|
setTimeout(function () {
|
|
|
|
showPostPlayMenu(currentItem);
|
|
}, 1000);
|
|
}
|
|
|
|
function onPlaybackProgress(positionTicks) {
|
|
|
|
var state = getPlayerState(positionTicks);
|
|
|
|
var info = {
|
|
ItemId: state.NowPlayingItem.Id,
|
|
NowPlayingItem: state.NowPlayingItem
|
|
};
|
|
|
|
info = $.extend(info, state.PlayState);
|
|
|
|
ApiClient.reportPlaybackProgress(info);
|
|
}
|
|
|
|
function onPlaybackStopped(positionTicks) {
|
|
|
|
var state = getPlayerState(positionTicks);
|
|
|
|
var stopInfo = {
|
|
itemId: state.NowPlayingItem.Id,
|
|
mediaSourceId: state.PlayState.MediaSourceId,
|
|
positionTicks: state.PlayState.PositionTicks
|
|
};
|
|
|
|
if (state.PlayState.LiveStreamId) {
|
|
stopInfo.LiveStreamId = state.PlayState.LiveStreamId;
|
|
}
|
|
|
|
if (state.PlayState.PlaySessionId) {
|
|
stopInfo.PlaySessionId = state.PlayState.PlaySessionId;
|
|
}
|
|
|
|
ApiClient.reportPlaybackStopped(stopInfo);
|
|
|
|
if (progressInterval) {
|
|
clearInterval(progressInterval);
|
|
progressInterval = null;
|
|
}
|
|
}
|
|
|
|
function showPostPlayMenu(item) {
|
|
|
|
require(['jqmpopup', 'jqmlistview'], function () {
|
|
$('.externalPlayerPostPlayFlyout').popup("close").remove();
|
|
|
|
var html = '<div data-role="popup" class="externalPlayerPostPlayFlyout" data-history="false" data-theme="a" data-dismissible="false">';
|
|
|
|
html += '<ul data-role="listview" style="min-width: 220px;">';
|
|
html += '<li data-role="list-divider" style="padding: 1em;text-align:center;">' + Globalize.translate('HeaderExternalPlayerPlayback') + '</li>';
|
|
html += '</ul>';
|
|
|
|
html += '<div style="padding:1em;">';
|
|
|
|
var autoMarkWatched = item.RunTimeTicks;
|
|
|
|
if (item.RunTimeTicks && item.RunTimeTicks >= 3000000000) {
|
|
|
|
autoMarkWatched = false;
|
|
|
|
html += '<fieldset data-role="controlgroup">';
|
|
html += '<legend>' + Globalize.translate('LabelMarkAs') + '</legend>';
|
|
html += '<label for="radioMarkUnwatched">' + Globalize.translate('OptionUnwatched') + '</label>';
|
|
html += '<input type="radio" id="radioMarkUnwatched" name="radioGroupMarkPlaystate" class="radioPlaystate" />';
|
|
html += '<label for="radioMarkWatched">' + Globalize.translate('OptionWatched') + '</label>';
|
|
html += '<input type="radio" id="radioMarkWatched" checked="checked" name="radioGroupMarkPlaystate" class="radioPlaystate" />';
|
|
html += '<label for="radioMarkInProgress">' + Globalize.translate('OptionInProgress') + '</label>';
|
|
html += '<input type="radio" id="radioMarkInProgress" name="radioGroupMarkPlaystate" class="radioPlaystate" />';
|
|
html += '</fieldset>';
|
|
|
|
html += '<br/>';
|
|
|
|
html += '<p style="margin-top: 0;">' + Globalize.translate('LabelResumePoint') + '</p>';
|
|
|
|
html += '<div class="sliderContainer" style="display:block;margin-top:4px;">';
|
|
html += '<input class="playstateSlider" type="range" step=".001" min="0" max="100" value="0" style="display:none;" data-theme="a" data-highlight="true" />';
|
|
html += '</div>';
|
|
html += '<div class="sliderValue" style="text-align:center;margin:2px 0 4px;">0:00:00</div>';
|
|
|
|
html += '<br/>';
|
|
}
|
|
|
|
html += '<button type="button" class="btnDone" data-theme="b" data-icon="check">' + Globalize.translate('ButtonImDone') + '</button>';
|
|
|
|
html += '</div>';
|
|
|
|
html += '</div>';
|
|
|
|
$(document.body).append(html);
|
|
|
|
var elem = $('.externalPlayerPostPlayFlyout').popup({}).trigger('create').popup("open").on("popupafterclose", function () {
|
|
|
|
$(this).off("popupafterclose").remove();
|
|
|
|
});
|
|
|
|
$('.radioPlaystate', elem).on('change', function () {
|
|
|
|
if ($('#radioMarkInProgress', elem).checked()) {
|
|
|
|
$('.playstateSlider', elem).slider('enable');
|
|
|
|
} else {
|
|
$('.playstateSlider', elem).slider('disable');
|
|
}
|
|
|
|
}).trigger('change');
|
|
|
|
$('.btnDone', elem).on('click', function () {
|
|
|
|
$('.externalPlayerPostPlayFlyout').popup("close").remove();
|
|
|
|
var position = 0;
|
|
|
|
if ($('#radioMarkInProgress', elem).checked()) {
|
|
|
|
var pct = $(".playstateSlider", elem).val();
|
|
var ticks = item.RunTimeTicks * (Number(pct) * .01);
|
|
|
|
position = ticks;
|
|
}
|
|
else if (autoMarkWatched || $('#radioMarkWatched', elem).checked()) {
|
|
|
|
position = currentMediaSource.RunTimeTicks;
|
|
}
|
|
else if ($('#radioMarkUnwatched', elem).checked()) {
|
|
|
|
position = 0;
|
|
}
|
|
onPlaybackStopped(position);
|
|
});
|
|
|
|
$(".playstateSlider", elem).on("change", function (e) {
|
|
|
|
var pct = $(this).val();
|
|
|
|
var time = item.RunTimeTicks * (Number(pct) * .01);
|
|
|
|
var tooltext = Dashboard.getDisplayTime(time);
|
|
|
|
$('.sliderValue', elem).html(tooltext);
|
|
|
|
Logger.log("slidin", pct, self.currentDurationTicks, time);
|
|
|
|
});
|
|
});
|
|
}
|
|
|
|
function closePlayMenu() {
|
|
$('.externalPlayerFlyout').popup("close").remove();
|
|
}
|
|
|
|
function showMenuForItem(item, players) {
|
|
|
|
require(['jqmpopup', 'jqmlistview'], function () {
|
|
closePlayMenu();
|
|
|
|
var html = '<div data-role="popup" class="externalPlayerFlyout" data-theme="a" data-dismissible="false">';
|
|
|
|
html += '<ul data-role="listview" style="min-width: 200px;">';
|
|
html += '<li data-role="list-divider" style="padding: 1em;text-align:center;">' + Globalize.translate('HeaderSelectExternalPlayer') + '</li>';
|
|
html += '</ul>';
|
|
|
|
html += '<div style="padding:1em;">';
|
|
|
|
html += players.map(function (p) {
|
|
|
|
return '<a href="' + p.url + '" data-role="button" data-icon="play" class="btnExternalPlayer" data-theme="b" data-mini="true">' + p.name + '</a>';
|
|
|
|
}).join('');
|
|
|
|
html += '</div>';
|
|
|
|
html += '</div>';
|
|
|
|
$(document.body).append(html);
|
|
|
|
var elem = $('.externalPlayerFlyout').popup({}).trigger('create').popup("open").on("popupafterclose", function () {
|
|
|
|
$(this).off("popupafterclose").remove();
|
|
|
|
});
|
|
|
|
$('.btnExternalPlayer', elem).on('click', function () {
|
|
|
|
ExternalPlayer.onPlaybackStart();
|
|
});
|
|
});
|
|
}
|
|
|
|
function showPlayMenu(itemId) {
|
|
|
|
var userId = Dashboard.getCurrentUserId();
|
|
|
|
ApiClient.getItem(userId, itemId).done(function (item) {
|
|
|
|
getVideoStreamInfo(item).done(function (streamInfo) {
|
|
|
|
setTimeout(function () {
|
|
ExternalPlayer.showPlayerSelectionMenu(item, streamInfo.url, streamInfo.mimeType);
|
|
}, 500);
|
|
});
|
|
});
|
|
}
|
|
|
|
function getExternalPlayers(url, mimeType) {
|
|
|
|
var deferred = $.Deferred();
|
|
|
|
var players = [
|
|
{ name: 'Vlc', url: 'vlc://' + url, id: 'vlc' }
|
|
];
|
|
deferred.resolveWith(null, [players]);
|
|
|
|
return deferred.promise();
|
|
}
|
|
|
|
function showPlayerSelectionMenu(item, url, mimeType) {
|
|
|
|
ExternalPlayer.getExternalPlayers(url, mimeType).done(function (players) {
|
|
showMenuForItem(item, players);
|
|
});
|
|
}
|
|
|
|
window.ExternalPlayer = {
|
|
|
|
showMenu: showPlayMenu,
|
|
onPlaybackStart: onPlaybackStart,
|
|
getExternalPlayers: getExternalPlayers,
|
|
showPlayerSelectionMenu: showPlayerSelectionMenu
|
|
};
|
|
|
|
})(window); |