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:
parent
675a59adc4
commit
8045b95d93
16 changed files with 335 additions and 8 deletions
|
@ -21,7 +21,8 @@ const LIBRARY_PATHS = [
|
|||
const PLAYBACK_PATHS = [
|
||||
'/dashboard/playback/transcoding',
|
||||
'/dashboard/playback/resume',
|
||||
'/dashboard/playback/streaming'
|
||||
'/dashboard/playback/streaming',
|
||||
'/dashboard/playback/trickplay'
|
||||
];
|
||||
|
||||
const ServerDrawerSection = () => {
|
||||
|
@ -108,6 +109,9 @@ const ServerDrawerSection = () => {
|
|||
<ListItemLink to='/dashboard/playback/streaming' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('TabStreaming')} />
|
||||
</ListItemLink>
|
||||
<ListItemLink to='/dashboard/playback/trickplay' sx={{ pl: 4 }}>
|
||||
<ListItemText inset primary={globalize.translate('Trickplay')} />
|
||||
</ListItemLink>
|
||||
</List>
|
||||
</Collapse>
|
||||
</List>
|
||||
|
|
|
@ -163,5 +163,11 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
|||
view: 'dashboard/streaming.html',
|
||||
controller: 'dashboard/streaming'
|
||||
}
|
||||
}, {
|
||||
path: 'playback/trickplay',
|
||||
pageProps: {
|
||||
view: 'dashboard/trickplay.html',
|
||||
controller: 'dashboard/trickplay'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
|
|
@ -36,5 +36,6 @@ export const REDIRECTS: Redirect[] = [
|
|||
{ from: 'usernew.html', to: '/dashboard/users/add' },
|
||||
{ from: 'userparentalcontrol.html', to: '/dashboard/users/parentalcontrol' },
|
||||
{ from: 'userpassword.html', to: '/dashboard/users/password' },
|
||||
{ from: 'userprofiles.html', to: '/dashboard/users' }
|
||||
{ from: 'userprofiles.html', to: '/dashboard/users' },
|
||||
{ from: 'trickplayconfiguration.html', to: '/dashboard/playback/trickplay' }
|
||||
];
|
||||
|
|
|
@ -391,8 +391,10 @@ export function setContentType(parent, contentType) {
|
|||
}
|
||||
|
||||
if (contentType !== 'tvshows' && contentType !== 'movies' && contentType !== 'homevideos' && contentType !== 'musicvideos' && contentType !== 'mixed') {
|
||||
parent.querySelector('.trickplaySettingsSection').classList.add('hide');
|
||||
parent.querySelector('.chapterSettingsSection').classList.add('hide');
|
||||
} else {
|
||||
parent.querySelector('.trickplaySettingsSection').classList.remove('hide');
|
||||
parent.querySelector('.chapterSettingsSection').classList.remove('hide');
|
||||
}
|
||||
|
||||
|
@ -515,6 +517,8 @@ export function getLibraryOptions(parent) {
|
|||
EnablePhotos: parent.querySelector('.chkEnablePhotos').checked,
|
||||
EnableRealtimeMonitor: parent.querySelector('.chkEnableRealtimeMonitor').checked,
|
||||
EnableLUFSScan: parent.querySelector('.chkEnableLUFSScan').checked,
|
||||
ExtractTrickplayImagesDuringLibraryScan: parent.querySelector('.chkExtractTrickplayDuringLibraryScan').checked,
|
||||
EnableTrickplayImageExtraction: parent.querySelector('.chkExtractTrickplayImages').checked,
|
||||
ExtractChapterImagesDuringLibraryScan: parent.querySelector('.chkExtractChaptersDuringLibraryScan').checked,
|
||||
EnableChapterImageExtraction: parent.querySelector('.chkExtractChapterImages').checked,
|
||||
EnableInternetProviders: true,
|
||||
|
@ -577,6 +581,8 @@ export function setLibraryOptions(parent, options) {
|
|||
parent.querySelector('.chkEnablePhotos').checked = options.EnablePhotos;
|
||||
parent.querySelector('.chkEnableRealtimeMonitor').checked = options.EnableRealtimeMonitor;
|
||||
parent.querySelector('.chkEnableLUFSScan').checked = options.EnableLUFSScan;
|
||||
parent.querySelector('.chkExtractTrickplayDuringLibraryScan').checked = options.ExtractTrickplayImagesDuringLibraryScan;
|
||||
parent.querySelector('.chkExtractTrickplayImages').checked = options.EnableTrickplayImageExtraction;
|
||||
parent.querySelector('.chkExtractChaptersDuringLibraryScan').checked = options.ExtractChapterImagesDuringLibraryScan;
|
||||
parent.querySelector('.chkExtractChapterImages').checked = options.EnableChapterImageExtraction;
|
||||
parent.querySelector('#chkSaveLocal').checked = options.SaveLocalMetadata;
|
||||
|
|
|
@ -104,6 +104,25 @@
|
|||
<div class="fieldDescription checkboxFieldDescription">${OptionAutomaticallyGroupSeriesHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="trickplaySettingsSection hide">
|
||||
<h2>${Trickplay}</h2>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription fldExtractTrickplayImages">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkExtractTrickplayImages" />
|
||||
<span>${OptionExtractTrickplayImage}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${ExtractTrickplayImagesHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription fldExtractTrickplayDuringLibraryScan advanced">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkExtractTrickplayDuringLibraryScan" />
|
||||
<span>${LabelExtractTrickplayDuringLibraryScan}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${LabelExtractTrickplayDuringLibraryScanHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chapterSettingsSection hide">
|
||||
<h2>${HeaderChapterImages}</h2>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription fldExtractChapterImages">
|
||||
|
|
|
@ -125,7 +125,7 @@ function getItemsForPlayback(serverId, query) {
|
|||
} else {
|
||||
query.Limit = query.Limit || 300;
|
||||
}
|
||||
query.Fields = 'Chapters';
|
||||
query.Fields = ['Chapters', 'Trickplay'];
|
||||
query.ExcludeLocationTypes = 'Virtual';
|
||||
query.EnableTotalRecordCount = false;
|
||||
query.CollapseBoxSetItems = false;
|
||||
|
@ -1858,7 +1858,7 @@ class PlaybackManager {
|
|||
IsVirtualUnaired: false,
|
||||
IsMissing: false,
|
||||
UserId: apiClient.getCurrentUserId(),
|
||||
Fields: 'Chapters'
|
||||
Fields: ['Chapters', 'Trickplay']
|
||||
}).then(function (episodesResult) {
|
||||
const originalResults = episodesResult.Items;
|
||||
const isSeries = firstItem.Type === 'Series';
|
||||
|
@ -1940,7 +1940,7 @@ class PlaybackManager {
|
|||
IsVirtualUnaired: false,
|
||||
IsMissing: false,
|
||||
UserId: apiClient.getCurrentUserId(),
|
||||
Fields: 'Chapters'
|
||||
Fields: ['Chapters', 'Trickplay']
|
||||
}).then(function (episodesResult) {
|
||||
let foundItem = false;
|
||||
episodesResult.Items = episodesResult.Items.filter(function (e) {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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')
|
||||
}];
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -30,6 +30,9 @@ function getTabs() {
|
|||
}, {
|
||||
href: '#/dashboard/playback/streaming',
|
||||
name: globalize.translate('TabStreaming')
|
||||
}, {
|
||||
href: '#/dashboard/playback/trickplay',
|
||||
name: globalize.translate('Trickplay')
|
||||
}];
|
||||
}
|
||||
|
||||
|
|
65
src/controllers/dashboard/trickplay.html
Normal file
65
src/controllers/dashboard/trickplay.html
Normal 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>
|
71
src/controllers/dashboard/trickplay.js
Normal file
71
src/controllers/dashboard/trickplay.js
Normal 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);
|
||||
});
|
||||
});
|
|
@ -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) {
|
||||
|
|
|
@ -161,6 +161,10 @@ function updateBubble(range, percent, value, bubble) {
|
|||
|
||||
let html;
|
||||
|
||||
if (range.updateBubbleHtml?.(bubble, value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (range.getBubbleHtml) {
|
||||
html = range.getBubbleHtml(percent, value);
|
||||
} else {
|
||||
|
|
|
@ -84,7 +84,7 @@ export function getItemsForPlayback(apiClient, query) {
|
|||
});
|
||||
} else {
|
||||
query.Limit = query.Limit || 300;
|
||||
query.Fields = 'Chapters';
|
||||
query.Fields = ['Chapters', 'Trickplay'];
|
||||
query.ExcludeLocationTypes = 'Virtual';
|
||||
query.EnableTotalRecordCount = false;
|
||||
query.CollapseBoxSetItems = false;
|
||||
|
@ -200,7 +200,7 @@ export function translateItemsForPlayback(apiClient, items, options) {
|
|||
IsVirtualUnaired: false,
|
||||
IsMissing: false,
|
||||
UserId: apiClient.getCurrentUserId(),
|
||||
Fields: 'Chapters'
|
||||
Fields: ['Chapters', 'Trickplay']
|
||||
}).then(function (episodesResult) {
|
||||
let foundItem = false;
|
||||
episodesResult.Items = episodesResult.Items.filter(function (e) {
|
||||
|
|
|
@ -1,4 +1,37 @@
|
|||
{
|
||||
"Trickplay": "Trickplay",
|
||||
"TrickplayHardwareAccel": "Enable hardware acceleration",
|
||||
"NonBlockingScan": "Non Blocking - queues generation, then returns",
|
||||
"BlockingScan": "Blocking - queues generation, blocks scan until complete",
|
||||
"LabelScanBehavior": "Scan Behavior",
|
||||
"LabelScanBehaviorHelp": "The default behavior is non blocking, which will add media to the library before trickplay generation is done. Blocking will ensure trickplay files are generated before media is added to the library, but will make scans significantly longer.",
|
||||
"PriorityHigh": "High",
|
||||
"PriorityAboveNormal": "Above Normal",
|
||||
"PriorityNormal": "Normal",
|
||||
"PriorityBelowNormal": "Below Normal",
|
||||
"PriorityIdle": "Idle",
|
||||
"LabelProcessPriority": "Process Priority",
|
||||
"LabelProcessPriorityHelp": "Setting this lower or higher will determine how the CPU prioritizes the ffmpeg preview images generation process in relation to other processes. If you notice slowdown while generating preview images but don't want to fully stop their generation, try lowering this as well as the thread count.",
|
||||
"LabelImageInterval": "ImageInterval",
|
||||
"LabelImageIntervalHelp": "Interval of time between each new trickplay image. A shorter interval and more images will take up more space.",
|
||||
"LabelWidthResolutions": "Width Resolutions",
|
||||
"LabelWidthResolutionsHelp": "Comma separated list of the width (px) that trickplay images will be generated at. All images should generate proportionally to the source, so a width of 320 on a 16:9 video ends up around 320x180.",
|
||||
"LabelTileWidth": "Tile Width",
|
||||
"LabelTileWidthHelp": "Maximum number of images to tile in X direction.",
|
||||
"LabelTileHeight": "Tile Height",
|
||||
"LabelTileHeightHelp": "Maximum number of images to tile in Y direction.",
|
||||
"LabelJpegQuality": "JPEG Quality",
|
||||
"LabelJpegQualityHelp": "The image quality for preview image tiles.",
|
||||
"LabelQscale": "Qscale",
|
||||
"LabelQscaleHelp": "The quality scale of images output by ffmpeg, with 2 being the highest quality and 31 being the lowest.",
|
||||
"LabelTrickplayThreads": "FFmpeg Threads",
|
||||
"LabelTrickplayThreadsHelp": "The number of threads to pass to the \"-threads\" argument of ffmpeg.",
|
||||
"OptionExtractTrickplayImage": "Enable trickplay image extraction",
|
||||
"ExtractTrickplayImagesHelp": "Trickplay images are similar to chapter images, except they span the entire length of the content and are used to show a preview when scrubbing through videos.",
|
||||
"LabelExtractTrickplayDuringLibraryScan": "Extract trickplay images during the library scan",
|
||||
"LabelExtractTrickplayDuringLibraryScanHelp": "Generate trickplay images when videos are imported during the library scan. Otherwise, they will be extracted during the chapter images scheduled task. If generation is set to non-blocking this will not affect the time a library scan takes to complete.",
|
||||
|
||||
|
||||
"Absolute": "Absolute",
|
||||
"AccessRestrictedTryAgainLater": "Access is currently restricted. Please try again later.",
|
||||
"Actor": "Actor",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue