1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Add trickplay functionality

This commit is contained in:
Nick 2024-02-11 20:34:30 -08:00
parent 675a59adc4
commit 8045b95d93
16 changed files with 335 additions and 8 deletions

View file

@ -135,6 +135,12 @@
<span>${AllowAv1Encoding}</span>
</label>
</div>
<div class="checkboxList">
<label>
<input type="checkbox" is="emby-checkbox" id="chkAllowMjpegEncoding" />
<span>${AllowMjpegEncoding}</span>
</label>
</div>
</div>
<div class="vppTonemappingOptions hide">

View file

@ -19,6 +19,7 @@ function loadPage(page, config, systemInfo) {
page.querySelector('#chkHardwareEncoding').checked = config.EnableHardwareEncoding;
page.querySelector('#chkAllowHevcEncoding').checked = config.AllowHevcEncoding;
page.querySelector('#chkAllowAv1Encoding').checked = config.AllowAv1Encoding;
page.querySelector('#chkAllowMjpegEncoding').checked = config.AllowMjpegEncoding;
$('#selectVideoDecoder', page).val(config.HardwareAccelerationType);
$('#selectThreadCount', page).val(config.EncodingThreadCount);
page.querySelector('#chkEnableAudioVbr').checked = config.EnableAudioVbr;
@ -125,6 +126,7 @@ function onSubmit() {
config.EnableHardwareEncoding = form.querySelector('#chkHardwareEncoding').checked;
config.AllowHevcEncoding = form.querySelector('#chkAllowHevcEncoding').checked;
config.AllowAv1Encoding = form.querySelector('#chkAllowAv1Encoding').checked;
config.AllowMjpegEncoding = form.querySelector('#chkAllowMjpegEncoding').checked;
ApiClient.updateNamedConfiguration('encoding', config).then(function () {
updateEncoder(form);
}, function () {
@ -175,6 +177,9 @@ function getTabs() {
}, {
href: '#/dashboard/playback/streaming',
name: globalize.translate('TabStreaming')
}, {
href: '#/dashboard/playback/trickplay',
name: globalize.translate('Trickplay')
}];
}

View file

@ -39,6 +39,9 @@ function getTabs() {
}, {
href: '#/dashboard/playback/streaming',
name: globalize.translate('TabStreaming')
}, {
href: '#/dashboard/playback/trickplay',
name: globalize.translate('Trickplay')
}];
}
@ -52,4 +55,3 @@ $(document).on('pageinit', '#playbackConfigurationPage', function () {
loadPage(page, config);
});
});

View file

@ -30,6 +30,9 @@ function getTabs() {
}, {
href: '#/dashboard/playback/streaming',
name: globalize.translate('TabStreaming')
}, {
href: '#/dashboard/playback/trickplay',
name: globalize.translate('Trickplay')
}];
}

View file

@ -0,0 +1,65 @@
<div id="trickplayConfigurationPage" data-role="page" class="page type-interior playbackConfigurationPage withTabs">
<div>
<div class="content-primary">
<form class="trickplayConfigurationForm">
<div class="sectionTitleContainer flex align-items-center">
<h2 class="sectionTitle">${Trickplay}</h2>
</div>
<div class="checkboxListContainer">
<div class="checkboxList">
<label>
<input type="checkbox" is="emby-checkbox" id="chkEnableHwAcceleration" />
<span>${TrickplayHardwareAccel}</span>
</label>
</div>
</div>
<div class="inputContainer">
<select is="emby-select" id="selectScanBehavior" name="Scan Behavior" label="${LabelScanBehavior}">
<option id="optNonBlocking" value="NonBlocking">${NonBlockingScan}</option>
<option id="optBlocking" value="Blocking">${BlockingScan}</option>
</select>
<div class="fieldDescription">${LabelScanBehaviorHelp}</div>
</div>
<div class="inputContainer">
<select is="emby-select" id="selectProcessPriority" name="Process Priority" label="${LabelProcessPriority}">
<option id="optPriorityHigh" value="High">${PriorityHigh}</option>
<option id="optPriorityAboveNormal" value="AboveNormal">${PriorityAboveNormal}</option>
<option id="optPriorityNormal" value="Normal">${PriorityNormal}</option>
<option id="optPriorityBelowNormal" value="BelowNormal">${PriorityBelowNormal}</option>
<option id="optPriorityIdle" value="Idle">${PriorityIdle}</option>
</select>
<div class="fieldDescription">${LabelProcessPriorityHelp}</div>
</div>
<div class="inputContainer">
<input is="emby-input" type="number" id="txtInterval" pattern="[0-9]*" min="0" required label="${LabelImageInterval}" />
<div class="fieldDescription">${LabelImageIntervalHelp}</div>
</div>
<div class="inputContainer">
<input is="emby-input" id="txtWidthResolutions" pattern="[0-9,]*" required label="${LabelWidthResolutions}">
<div class="fieldDescription">${LabelWidthResolutionsHelp}</div>
</div>
<div class="inputContainer">
<input is="emby-input" type="number" id="txtTileWidth" pattern="[0-9]*" min="1" required label="${LabelTileWidth}">
<div class="fieldDescription">${LabelTileWidthHelp}</div>
</div>
<div class="inputContainer">
<input is="emby-input" type="number" id="txtTileHeight" pattern="[0-9]*" min="1" required label="${LabelTileHeight}">
<div class="fieldDescription">${LabelTileHeightHelp}</div>
</div>
<div class="inputContainer">
<input is="emby-input" type="number" id="txtJpegQuality" pattern="[0-9]*" min="1" max="100" required label="${LabelJpegQuality}">
<div class="fieldDescription">${LabelJpegQualityHelp}</div>
</div>
<div class="inputContainer">
<input is="emby-input" type="number" id="txtQscale" pattern="[0-9]*" min="2" max="31" required label="${LabelQscale}">
<div class="fieldDescription">${LabelQscaleHelp}</div>
</div>
<div class="inputContainer">
<input is="emby-input" type="number" id="txtProcessThreads" pattern="[0-9]*" required="" label="${LabelTrickplayThreads}">
<div class="fieldDescription"></div>
</div>
<div><button is="emby-button" type="submit" class="raised button-submit block"><span>${Save}</span></button></div>
</form>
</div>
</div>
</div>

View file

@ -0,0 +1,71 @@
import 'jquery';
import loading from '../../components/loading/loading';
import libraryMenu from '../../scripts/libraryMenu';
import globalize from '../../scripts/globalize';
import Dashboard from '../../utils/dashboard';
function loadPage(page, config) {
const trickplayOptions = config.TrickplayOptions;
page.querySelector('#chkEnableHwAcceleration').checked = trickplayOptions.EnableHwAcceleration;
$('#selectScanBehavior', page).val(trickplayOptions.ScanBehavior);
$('#selectProcessPriority', page).val(trickplayOptions.ProcessPriority);
$('#txtInterval', page).val(trickplayOptions.Interval);
$('#txtWidthResolutions', page).val(trickplayOptions.WidthResolutions.join(','));
$('#txtTileWidth', page).val(trickplayOptions.TileWidth);
$('#txtTileHeight', page).val(trickplayOptions.TileHeight);
$('#txtQscale', page).val(trickplayOptions.Qscale);
$('#txtJpegQuality', page).val(trickplayOptions.JpegQuality);
$('#txtProcessThreads', page).val(trickplayOptions.ProcessThreads);
loading.hide();
}
function onSubmit() {
loading.show();
const form = this;
ApiClient.getServerConfiguration().then(function (config) {
const trickplayOptions = config.TrickplayOptions;
trickplayOptions.EnableHwAcceleration = form.querySelector('#chkEnableHwAcceleration').checked;
trickplayOptions.ScanBehavior = $('#selectScanBehavior', form).val();
trickplayOptions.ProcessPriority = $('#selectProcessPriority', form).val();
trickplayOptions.Interval = Math.max(0, parseInt($('#txtInterval', form).val() || '10000', 10));
trickplayOptions.WidthResolutions = $('#txtWidthResolutions', form).val().replace(' ', '').split(',').map(Number);
trickplayOptions.TileWidth = Math.max(1, parseInt($('#txtTileWidth', form).val() || '10', 10));
trickplayOptions.TileHeight = Math.max(1, parseInt($('#txtTileHeight', form).val() || '10', 10));
trickplayOptions.Qscale = Math.min(31, parseInt($('#txtQscale', form).val() || '10', 10));
trickplayOptions.JpegQuality = Math.min(100, parseInt($('#txtJpegQuality', form).val() || '80', 10));
trickplayOptions.ProcessThreads = parseInt($('#txtProcessThreads', form).val() || '0', 10);
ApiClient.updateServerConfiguration(config).then(Dashboard.processServerConfigurationUpdateResult);
});
return false;
}
function getTabs() {
return [{
href: '#/dashboard/playback/transcoding',
name: globalize.translate('Transcoding')
}, {
href: '#/dashboard/playback/resume',
name: globalize.translate('ButtonResume')
}, {
href: '#/dashboard/playback/streaming',
name: globalize.translate('TabStreaming')
}, {
href: '#/dashboard/playback/trickplay',
name: globalize.translate('Trickplay')
}];
}
$(document).on('pageinit', '#trickplayConfigurationPage', function () {
$('.trickplayConfigurationForm').off('submit', onSubmit).on('submit', onSubmit);
}).on('pageshow', '#trickplayConfigurationPage', function () {
loading.show();
libraryMenu.setTabs('playback', 3, getTabs);
const page = this;
ApiClient.getServerConfiguration().then(function (config) {
loadPage(page, config);
});
});

View file

@ -1356,6 +1356,81 @@ export default function (view) {
resetIdle();
}
function updateTrickplayBubbleHtml(apiClient, trickplayInfo, item, mediaSourceId, bubble, positionTicks) {
let doFullUpdate = false;
let chapterThumbContainer = bubble.querySelector('.chapterThumbContainer');
let chapterThumb;
let chapterThumbText;
// Create bubble elements if they don't already exist
if (chapterThumbContainer) {
chapterThumb = chapterThumbContainer.querySelector('.chapterThumb');
chapterThumbText = chapterThumbContainer.querySelector('.chapterThumbText');
} else {
doFullUpdate = true;
chapterThumbContainer = document.createElement('div');
chapterThumbContainer.classList.add('chapterThumbContainer');
chapterThumbContainer.style.overflow = 'hidden';
const chapterThumbWrapper = document.createElement('div');
chapterThumbWrapper.classList.add('chapterThumbWrapper');
chapterThumbWrapper.style.overflow = 'hidden';
chapterThumbWrapper.style.position = 'relative';
chapterThumbWrapper.style.width = trickplayInfo.Width + 'px';
chapterThumbWrapper.style.height = trickplayInfo.Height + 'px';
chapterThumbContainer.appendChild(chapterThumbWrapper);
chapterThumb = document.createElement('img');
chapterThumb.classList.add('chapterThumb');
chapterThumb.style.position = 'absolute';
chapterThumb.style.width = 'unset';
chapterThumb.style.minWidth = 'unset';
chapterThumb.style.height = 'unset';
chapterThumb.style.minHeight = 'unset';
chapterThumbWrapper.appendChild(chapterThumb);
const chapterThumbTextContainer = document.createElement('div');
chapterThumbTextContainer.classList.add('chapterThumbTextContainer');
chapterThumbContainer.appendChild(chapterThumbTextContainer);
chapterThumbText = document.createElement('h2');
chapterThumbText.classList.add('chapterThumbText');
chapterThumbTextContainer.appendChild(chapterThumbText);
}
// Update trickplay values
const currentTimeMs = positionTicks / 10_000;
const currentTile = Math.floor(currentTimeMs / trickplayInfo.Interval);
const tileSize = trickplayInfo.TileWidth * trickplayInfo.TileHeight;
const tileOffset = currentTile % tileSize;
const index = Math.floor(currentTile / tileSize);
const tileOffsetX = tileOffset % trickplayInfo.TileWidth;
const tileOffsetY = Math.floor(tileOffset / trickplayInfo.TileWidth);
const offsetX = -(tileOffsetX * trickplayInfo.Width);
const offsetY = -(tileOffsetY * trickplayInfo.Height);
const imgSrc = apiClient.getUrl('Videos/' + item.Id + '/Trickplay/' + trickplayInfo.Width + '/' + index + '.jpg', {
api_key: apiClient.accessToken(),
MediaSourceId: mediaSourceId
});
if (chapterThumb.src != imgSrc) chapterThumb.src = imgSrc;
chapterThumb.style.left = offsetX + 'px';
chapterThumb.style.top = offsetY + 'px';
chapterThumbText.textContent = datetime.getDisplayRunningTime(positionTicks);
// Set bubble innerHTML if container isn't part of DOM
if (doFullUpdate) {
bubble.innerHTML = chapterThumbContainer.outerHTML;
}
return true;
}
function getImgUrl(item, chapter, index, maxWidth, apiClient) {
if (chapter.ImageTag) {
return apiClient.getScaledImageUrl(item.Id, {
@ -1681,6 +1756,33 @@ export default function (view) {
}
});
nowPlayingPositionSlider.updateBubbleHtml = function(bubble, value) {
showOsd();
const item = currentItem;
let ticks = currentRuntimeTicks;
ticks /= 100;
ticks *= value;
if (item?.Trickplay) {
const mediaSourceId = currentPlayer?.streamInfo?.mediaSource?.Id;
const trickplayResolutions = item.Trickplay[mediaSourceId];
if (!trickplayResolutions) return false;
// TODO: just to test. must pick proper resolution and/or check above that trickplay resolutions has at least one key.
return updateTrickplayBubbleHtml(
ServerConnections.getApiClient(item.ServerId),
trickplayResolutions[Object.keys(trickplayResolutions)[0]],
item,
mediaSourceId,
bubble,
ticks);
}
return false;
};
nowPlayingPositionSlider.getBubbleHtml = function (value) {
showOsd();
if (enableProgressByTimeOfDay) {