add initial support for fMP4-HLS

This commit is contained in:
nyanmisaka 2020-11-12 20:03:38 +08:00
parent 0985909943
commit 54db12359c
9 changed files with 255 additions and 45 deletions

View file

@ -33,7 +33,6 @@ function getDeviceProfile(item, options = {}) {
profile = window.NativeShell.AppHost.getDeviceProfile(profileBuilder);
} else {
const builderOpts = getBaseProfileOptions(item);
builderOpts.enableSsaRender = (item && !options.isRetry && appSettings.get('subtitleburnin') !== 'allcomplexformats');
profile = profileBuilder(builderOpts);
}

View file

@ -145,6 +145,8 @@ import 'emby-checkbox';
showHideQualityFields(context, user, apiClient);
context.querySelector('#selectAllowedAudioChannels').value = userSettings.allowedAudioChannels();
apiClient.getCultures().then(allCultures => {
populateLanguages(context.querySelector('#selectAudioLanguage'), allCultures);
@ -187,6 +189,7 @@ import 'emby-checkbox';
}
context.querySelector('.chkPlayDefaultAudioTrack').checked = user.Configuration.PlayDefaultAudioTrack || false;
context.querySelector('.chkPreferFmp4HlsContainer').checked = userSettings.preferFmp4HlsContainer();
context.querySelector('.chkEnableCinemaMode').checked = userSettings.enableCinemaMode();
context.querySelector('.chkEnableNextVideoOverlay').checked = userSettings.enableNextVideoInfoOverlay();
context.querySelector('.chkExternalVideoPlayer').checked = appSettings.enableSystemExternalPlayers();
@ -222,10 +225,11 @@ import 'emby-checkbox';
setMaxBitrateFromField(context.querySelector('.selectVideoInternetQuality'), false, 'Video');
setMaxBitrateFromField(context.querySelector('.selectMusicInternetQuality'), false, 'Audio');
userSettingsInstance.allowedAudioChannels(context.querySelector('#selectAllowedAudioChannels').value);
user.Configuration.AudioLanguagePreference = context.querySelector('#selectAudioLanguage').value;
user.Configuration.PlayDefaultAudioTrack = context.querySelector('.chkPlayDefaultAudioTrack').checked;
user.Configuration.EnableNextEpisodeAutoPlay = context.querySelector('.chkEpisodeAutoPlay').checked;
userSettingsInstance.preferFmp4HlsContainer(context.querySelector('.chkPreferFmp4HlsContainer').checked);
userSettingsInstance.enableCinemaMode(context.querySelector('.chkEnableCinemaMode').checked);
userSettingsInstance.enableNextVideoInfoOverlay(context.querySelector('.chkEnableNextVideoOverlay').checked);

View file

@ -4,6 +4,16 @@
${HeaderAudioSettings}
</h2>
<div class="selectContainer">
<select is="emby-select" id="selectAllowedAudioChannels" label="${LabelAllowedAudioChannels}">
<option value="-1">${Auto}</option>
<option value="1">${LabelSelectMono}</option>
<option value="2">${LabelSelectStereo}</option>
<option value="6">5.1 ${LabelSelectAudioChannels}</option>
<option value="8">7.1 ${LabelSelectAudioChannels}</option>
</select>
</div>
<div class="selectContainer">
<select is="emby-select" id="selectAudioLanguage" label="${LabelAudioLanguagePreference}"></select>
</div>
@ -49,6 +59,14 @@
${TabAdvanced}
</h2>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input type="checkbox" is="emby-checkbox" class="chkPreferFmp4HlsContainer" />
<span>${PreferFmp4HlsContainer}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${PreferFmp4HlsContainerHelp}</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription cinemaModeOptions">
<label>
<input type="checkbox" is="emby-checkbox" class="chkEnableCinemaMode" />

View file

@ -92,6 +92,15 @@
</label>
</div>
</div>
<div class="checkboxListContainer">
<div class="checkboxList">
<label>
<input type="checkbox" is="emby-checkbox" id="chkAllowHevcEncoding" />
<span>${AllowHevcEncoding}</span>
</label>
</div>
</div>
</div>
<div class="tonemappingOptions hide">

View file

@ -13,6 +13,7 @@ import libraryMenu from 'libraryMenu';
page.querySelector('#chkDecodingColorDepth10Hevc').checked = config.EnableDecodingColorDepth10Hevc;
page.querySelector('#chkDecodingColorDepth10Vp9').checked = config.EnableDecodingColorDepth10Vp9;
page.querySelector('#chkHardwareEncoding').checked = config.EnableHardwareEncoding;
page.querySelector('#chkAllowHevcEncoding').checked = config.AllowHevcEncoding;
$('#selectVideoDecoder', page).val(config.HardwareAccelerationType);
$('#selectThreadCount', page).val(config.EncodingThreadCount);
$('#txtDownMixAudioBoost', page).val(config.DownMixAudioBoost);
@ -98,6 +99,7 @@ import libraryMenu from 'libraryMenu';
config.EnableDecodingColorDepth10Hevc = form.querySelector('#chkDecodingColorDepth10Hevc').checked;
config.EnableDecodingColorDepth10Vp9 = form.querySelector('#chkDecodingColorDepth10Vp9').checked;
config.EnableHardwareEncoding = form.querySelector('#chkHardwareEncoding').checked;
config.AllowHevcEncoding = form.querySelector('#chkAllowHevcEncoding').checked;
ApiClient.updateNamedConfiguration('encoding', config).then(function () {
updateEncoder(form);
}, function () {

View file

@ -1,4 +1,4 @@
define(['browser'], function (browser) {
define(['browser', 'userSettings', 'appSettings'], function (browser, userSettings, appSettings) {
'use strict';
browser = browser.default || browser;
@ -7,7 +7,7 @@ define(['browser'], function (browser) {
return !!(videoTestElement.canPlayType && videoTestElement.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, ''));
}
function canPlayH265(videoTestElement, options) {
function canPlayHevc(videoTestElement, options) {
if (browser.tizen || browser.xboxOne || browser.web0s || options.supportsHevc) {
return true;
}
@ -16,6 +16,7 @@ define(['browser'], function (browser) {
return false;
}
// hevc main level 4.0
return !!videoTestElement.canPlayType &&
(videoTestElement.canPlayType('video/mp4; codecs="hvc1.1.L120"').replace(/no/, '') ||
videoTestElement.canPlayType('video/mp4; codecs="hev1.1.L120"').replace(/no/, '') ||
@ -207,8 +208,7 @@ define(['browser'], function (browser) {
// Explicitly add supported codecs to make other codecs be transcoded
if (browser.tizenVersion >= 4) {
videoCodecs.push('h264');
if (canPlayH265(videoTestElement, options)) {
videoCodecs.push('h265');
if (canPlayHevc(videoTestElement, options)) {
videoCodecs.push('hevc');
}
}
@ -248,8 +248,8 @@ define(['browser'], function (browser) {
case 'ts':
supported = testCanPlayTs();
videoCodecs.push('h264');
if (canPlayH265(videoTestElement, options)) {
videoCodecs.push('h265');
// safari doesn't support hevc in TS-HLS
if ((browser.tizen || browser.web0s) && canPlayHevc(videoTestElement, options)) {
videoCodecs.push('hevc');
}
if (supportsVc1(videoTestElement)) {
@ -297,7 +297,9 @@ define(['browser'], function (browser) {
return function (options) {
options = options || {};
const physicalAudioChannels = options.audioChannels || (browser.tv || browser.ps4 || browser.xboxOne ? 6 : 2);
const isSurroundSoundSupportedBrowser = browser.safari || browser.chrome || browser.edgeChromium || browser.firefox;
const allowedAudioChannels = parseInt(userSettings.allowedAudioChannels() || '-1');
const physicalAudioChannels = (allowedAudioChannels > 0 ? allowedAudioChannels : null) || options.audioChannels || (isSurroundSoundSupportedBrowser || browser.tv || browser.ps4 || browser.xboxOne ? 6 : 2);
const bitrateSetting = getMaxBitrate();
@ -313,12 +315,13 @@ define(['browser'], function (browser) {
profile.MaxStreamingBitrate = bitrateSetting;
profile.MaxStaticBitrate = 100000000;
profile.MusicStreamingTranscodingBitrate = Math.min(bitrateSetting, 192000);
profile.MusicStreamingTranscodingBitrate = Math.min(bitrateSetting, 384000);
profile.DirectPlayProfiles = [];
let videoAudioCodecs = [];
let hlsVideoAudioCodecs = [];
let hlsInTsVideoAudioCodecs = [];
let hlsInFmp4VideoAudioCodecs = [];
const supportsMp3VideoAudio = videoTestElement.canPlayType('video/mp4; codecs="avc1.640029, mp4a.69"').replace(/no/, '')
|| videoTestElement.canPlayType('video/mp4; codecs="avc1.640029, mp4a.6B"').replace(/no/, '')
@ -353,16 +356,19 @@ define(['browser'], function (browser) {
// Transcoding codec is the first in hlsVideoAudioCodecs
// Put ac3/eac3 first only when the audio channels > 2 and need transcoding
if (canPlayAc3VideoAudioInHls && physicalAudioChannels > 2) {
hlsVideoAudioCodecs.push('ac3');
hlsInTsVideoAudioCodecs.push('ac3');
hlsInFmp4VideoAudioCodecs.push('ac3');
if (canPlayEac3VideoAudio) {
hlsVideoAudioCodecs.push('eac3');
hlsInTsVideoAudioCodecs.push('eac3');
hlsInFmp4VideoAudioCodecs.push('eac3');
}
}
}
if (canPlayAacVideoAudio) {
videoAudioCodecs.push('aac');
hlsVideoAudioCodecs.push('aac');
hlsInTsVideoAudioCodecs.push('aac');
hlsInFmp4VideoAudioCodecs.push('aac');
}
if (supportsMp3VideoAudio) {
@ -370,16 +376,31 @@ define(['browser'], function (browser) {
// PS4 fails to load HLS with mp3 audio
if (!browser.ps4) {
hlsVideoAudioCodecs.push('mp3');
hlsInTsVideoAudioCodecs.push('mp3');
}
hlsInFmp4VideoAudioCodecs.push('mp3');
}
// For ac3/eac3 directstream
if (canPlayAc3VideoAudio) {
if (canPlayAc3VideoAudioInHls && hlsVideoAudioCodecs.indexOf('ac3') === -1) {
hlsVideoAudioCodecs.push('ac3');
if (canPlayEac3VideoAudio && hlsVideoAudioCodecs.indexOf('eac3') === -1) {
hlsVideoAudioCodecs.push('eac3');
if (canPlayAc3VideoAudioInHls) {
if (hlsInTsVideoAudioCodecs.indexOf('ac3') === -1) {
hlsInTsVideoAudioCodecs.push('ac3');
}
if (hlsInFmp4VideoAudioCodecs.indexOf('ac3') === -1) {
hlsInFmp4VideoAudioCodecs.push('ac3');
}
if (canPlayEac3VideoAudio) {
if (hlsInTsVideoAudioCodecs.indexOf('eac3') === -1) {
hlsInTsVideoAudioCodecs.push('eac3');
}
if (hlsInFmp4VideoAudioCodecs.indexOf('eac3') === -1) {
hlsInFmp4VideoAudioCodecs.push('eac3');
}
}
}
}
@ -415,38 +436,54 @@ define(['browser'], function (browser) {
if (canPlayAudioFormat('opus')) {
videoAudioCodecs.push('opus');
hlsVideoAudioCodecs.push('opus');
hlsInTsVideoAudioCodecs.push('opus');
webmAudioCodecs.push('opus');
}
if (canPlayAudioFormat('flac')) {
videoAudioCodecs.push('flac');
hlsInFmp4VideoAudioCodecs.push('flac');
}
if (canPlayAudioFormat('alac')) {
videoAudioCodecs.push('alac');
hlsInFmp4VideoAudioCodecs.push('alac');
}
videoAudioCodecs = videoAudioCodecs.filter(function (c) {
return (options.disableVideoAudioCodecs || []).indexOf(c) === -1;
});
hlsVideoAudioCodecs = hlsVideoAudioCodecs.filter(function (c) {
hlsInTsVideoAudioCodecs = hlsInTsVideoAudioCodecs.filter(function (c) {
return (options.disableHlsVideoAudioCodecs || []).indexOf(c) === -1;
});
hlsInFmp4VideoAudioCodecs = hlsInFmp4VideoAudioCodecs.filter(function (c) {
return (options.disableHlsVideoAudioCodecs || []).indexOf(c) === -1;
});
const mp4VideoCodecs = [];
const webmVideoCodecs = [];
const hlsVideoCodecs = [];
const hlsInTsVideoCodecs = [];
const hlsInFmp4VideoCodecs = [];
if (browser.safari || browser.tizen || browser.web0s && canPlayHevc(videoTestElement, options)) {
hlsInFmp4VideoCodecs.push('hevc');
}
if (canPlayH264(videoTestElement)) {
mp4VideoCodecs.push('h264');
hlsVideoCodecs.push('h264');
hlsInTsVideoCodecs.push('h264');
if (browser.safari || browser.tizen || browser.web0s) {
hlsInFmp4VideoCodecs.push('h264');
}
}
if (canPlayH265(videoTestElement, options)) {
mp4VideoCodecs.push('h265');
mp4VideoCodecs.push('hevc');
if (browser.tizen || browser.web0s) {
hlsVideoCodecs.push('h265');
hlsVideoCodecs.push('hevc');
if (canPlayHevc(videoTestElement, options)) {
// safari is lying on HDR and 60fps videos, use fMP4 instead
if (!browser.safari) {
mp4VideoCodecs.push('hevc');
}
}
@ -606,18 +643,34 @@ define(['browser'], function (browser) {
});
}
if (canPlayHls() && hlsVideoAudioCodecs.length && options.enableHls !== false) {
profile.TranscodingProfiles.push({
Container: 'ts',
Type: 'Video',
AudioCodec: hlsVideoAudioCodecs.join(','),
VideoCodec: hlsVideoCodecs.join(','),
Context: 'Streaming',
Protocol: 'hls',
MaxAudioChannels: physicalAudioChannels.toString(),
MinSegments: browser.iOS || browser.osx ? '2' : '1',
BreakOnNonKeyFrames: hlsBreakOnNonKeyFrames
});
if (canPlayHls() && options.enableHls !== false) {
if (hlsInFmp4VideoCodecs.length && hlsInFmp4VideoAudioCodecs.length && userSettings.preferFmp4HlsContainer() && (browser.safari || browser.tizen || browser.web0s)) {
profile.TranscodingProfiles.push({
Container: 'mp4',
Type: 'Video',
AudioCodec: hlsInFmp4VideoAudioCodecs.join(','),
VideoCodec: hlsInFmp4VideoCodecs.join(','),
Context: 'Streaming',
Protocol: 'hls',
MaxAudioChannels: physicalAudioChannels.toString(),
MinSegments: browser.iOS || browser.osx ? '2' : '1',
BreakOnNonKeyFrames: hlsBreakOnNonKeyFrames
});
}
if (hlsInTsVideoCodecs.length && hlsInTsVideoAudioCodecs.length) {
profile.TranscodingProfiles.push({
Container: 'ts',
Type: 'Video',
AudioCodec: hlsInTsVideoAudioCodecs.join(','),
VideoCodec: hlsInTsVideoCodecs.join(','),
Context: 'Streaming',
Protocol: 'hls',
MaxAudioChannels: physicalAudioChannels.toString(),
MinSegments: browser.iOS || browser.osx ? '2' : '1',
BreakOnNonKeyFrames: hlsBreakOnNonKeyFrames
});
}
}
if (canPlayVp8) {
@ -713,6 +766,36 @@ define(['browser'], function (browser) {
}
}
let maxHevcLevel = 120;
let hevcProfiles = 'main';
// hevc main level 4.1
if (videoTestElement.canPlayType('video/mp4; codecs="hvc1.1.4.L123"').replace(/no/, '') ||
videoTestElement.canPlayType('video/mp4; codecs="hev1.1.4.L123"').replace(/no/, '')) {
maxHevcLevel = 123;
}
// hevc main10 level 4.1
if (videoTestElement.canPlayType('video/mp4; codecs="hvc1.2.4.L123"').replace(/no/, '') ||
videoTestElement.canPlayType('video/mp4; codecs="hev1.2.4.L123"').replace(/no/, '')) {
maxHevcLevel = 123;
hevcProfiles = 'main|main 10';
}
// hevc main10 level 5.1
if (videoTestElement.canPlayType('video/mp4; codecs="hvc1.2.4.L153"').replace(/no/, '') ||
videoTestElement.canPlayType('video/mp4; codecs="hev1.2.4.L153"').replace(/no/, '')) {
maxHevcLevel = 153;
hevcProfiles = 'main|main 10';
}
// hevc main10 level 6.1
if (videoTestElement.canPlayType('video/mp4; codecs="hvc1.2.4.L183"').replace(/no/, '') ||
videoTestElement.canPlayType('video/mp4; codecs="hvc1.2.4.L183"').replace(/no/, '')) {
maxHevcLevel = 183;
hevcProfiles = 'main|main 10';
}
const h264CodecProfileConditions = [
{
Condition: 'NotEquals',
@ -734,6 +817,27 @@ define(['browser'], function (browser) {
}
];
const hevcCodecProfileConditions = [
{
Condition: 'NotEquals',
Property: 'IsAnamorphic',
Value: 'true',
IsRequired: false
},
{
Condition: 'EqualsAny',
Property: 'VideoProfile',
Value: hevcProfiles,
IsRequired: false
},
{
Condition: 'LessThanEqual',
Property: 'VideoLevel',
Value: maxHevcLevel.toString(),
IsRequired: false
}
];
if (!browser.edgeUwp && !browser.tizen && !browser.web0s) {
h264CodecProfileConditions.push({
Condition: 'NotEquals',
@ -741,6 +845,13 @@ define(['browser'], function (browser) {
Value: 'true',
IsRequired: false
});
hevcCodecProfileConditions.push({
Condition: 'NotEquals',
Property: 'IsInterlaced',
Value: 'true',
IsRequired: false
});
}
if (maxVideoWidth) {
@ -750,12 +861,21 @@ define(['browser'], function (browser) {
Value: maxVideoWidth.toString(),
IsRequired: false
});
hevcCodecProfileConditions.push({
Condition: 'LessThanEqual',
Property: 'Width',
Value: maxVideoWidth.toString(),
IsRequired: false
});
}
const globalMaxVideoBitrate = (getGlobalMaxVideoBitrate() || '').toString();
const h264MaxVideoBitrate = globalMaxVideoBitrate;
const hevcMaxVideoBitrate = globalMaxVideoBitrate;
if (h264MaxVideoBitrate) {
h264CodecProfileConditions.push({
Condition: 'LessThanEqual',
@ -765,6 +885,15 @@ define(['browser'], function (browser) {
});
}
if (hevcMaxVideoBitrate) {
hevcCodecProfileConditions.push({
Condition: 'LessThanEqual',
Property: 'VideoBitrate',
Value: hevcMaxVideoBitrate,
IsRequired: true
});
}
// On iOS 12.x, for TS container max h264 level is 4.2
if (browser.iOS && browser.iOSVersion < 13) {
const codecProfile = {
@ -792,6 +921,12 @@ define(['browser'], function (browser) {
Conditions: h264CodecProfileConditions
});
profile.CodecProfiles.push({
Type: 'Video',
Codec: 'hevc',
Conditions: hevcCodecProfileConditions
});
const globalVideoConditions = [];
if (globalMaxVideoBitrate) {
@ -827,7 +962,7 @@ define(['browser'], function (browser) {
Method: 'External'
});
}
if (options.enableSsaRender) {
if (options.enableSsaRender !== false && (!options.isRetry && appSettings.get('subtitleburnin') !== 'allcomplexformats')) {
profile.SubtitleProfiles.push({
Format: 'ass',
Method: 'External'

View file

@ -114,6 +114,33 @@ export class UserSettings {
});
}
/**
* Get or set 'Allowed Audio Channels'.
* @param {string|undefined} val - 'Allowed Audio Channels'.
* @return {string} 'Allowed Audio Channels'.
*/
allowedAudioChannels(val) {
if (val !== undefined) {
return this.set('allowedAudioChannels', val, false);
}
return this.get('allowedAudioChannels', false) || '-1';
}
/**
* Get or set 'Perfer fMP4-HLS Container' state.
* @param {boolean|undefined} val - Flag to enable 'Perfer fMP4-HLS Container' or undefined.
* @return {boolean} 'Prefer fMP4-HLS Container' state.
*/
preferFmp4HlsContainer(val) {
if (val !== undefined) {
return this.set('preferFmp4HlsContainer', val.toString(), false);
}
val = this.get('preferFmp4HlsContainer', false);
return val === 'true';
}
/**
* Get or set 'Cinema Mode' state.
* @param {boolean|undefined} val - Flag to enable 'Cinema Mode' or undefined.
@ -457,6 +484,8 @@ export const importFrom = currentSettings.importFrom.bind(currentSettings);
export const set = currentSettings.set.bind(currentSettings);
export const get = currentSettings.get.bind(currentSettings);
export const serverConfig = currentSettings.serverConfig.bind(currentSettings);
export const allowedAudioChannels = currentSettings.allowedAudioChannels.bind(currentSettings);
export const preferFmp4HlsContainer = currentSettings.preferFmp4HlsContainer.bind(currentSettings);
export const enableCinemaMode = currentSettings.enableCinemaMode.bind(currentSettings);
export const enableNextVideoInfoOverlay = currentSettings.enableNextVideoInfoOverlay.bind(currentSettings);
export const enableThemeSongs = currentSettings.enableThemeSongs.bind(currentSettings);

View file

@ -1424,5 +1424,12 @@
"SubtitleVerticalPositionHelp": "Line number where text appears. Positive numbers indicate top down. Negative numbers indicate bottom up.",
"Preview": "Preview",
"LabelMaxMuxingQueueSize": "Max muxing queue size:",
"LabelMaxMuxingQueueSizeHelp": "Maximum number of packets that can be buffered while waiting for all streams to initialize. Try to increase it if you still encounter \"Too many packets buffered for output stream\" error in ffmpeg logs. The recommended value is 2048."
"LabelMaxMuxingQueueSizeHelp": "Maximum number of packets that can be buffered while waiting for all streams to initialize. Try to increase it if you still encounter \"Too many packets buffered for output stream\" error in ffmpeg logs. The recommended value is 2048.",
"PreferFmp4HlsContainer": "Prefer fMP4-HLS Media Container",
"PreferFmp4HlsContainerHelp": "Prefer to use fMP4 as the default container for HLS, making it possible to direct streaming HEVC content on supported devices.",
"AllowHevcEncoding": "Allow encoding in HEVC format",
"LabelAllowedAudioChannels": "Maximum Allowed Audio Channels",
"LabelSelectAudioChannels": "Channels",
"LabelSelectMono": "Mono",
"LabelSelectStereo": "Stereo"
}

View file

@ -1425,5 +1425,12 @@
"OptionAllowContentDownload": "允许媒体下载",
"HeaderDeleteDevices": "删除所有设备",
"DeleteDevicesConfirmation": "您确定要删除所有设备吗?所有其他会话将被注销。用户下次登录时,设备会重新出现。",
"DeleteAll": "删除全部"
"DeleteAll": "删除全部",
"PreferFmp4HlsContainer": "优先使用 fMP4-HLS 媒体容器",
"PreferFmp4HlsContainerHelp": "优先使用 fMP4 作为 HLS 播放的默认容器,从而可以在支持的设备上直接串流 HEVC 格式的内容。",
"AllowHevcEncoding": "允许以 HEVC 格式编码",
"LabelAllowedAudioChannels": "允许的最大声道数量",
"LabelSelectAudioChannels": "声道",
"LabelSelectMono": "单声道",
"LabelSelectStereo": "立体声"
}