From 6292e628584e45898662b76e250ec0f689678b13 Mon Sep 17 00:00:00 2001 From: Ingo Oppermann Date: Wed, 27 Mar 2024 21:01:53 +0100 Subject: [PATCH] Allow to stream HEVC and AV1 to Youtube via RTMP --- src/misc/coders/helper/index.js | 2 ++ src/misc/controls/Limits.js | 2 +- src/utils/restreamer.js | 29 ++++++++++++++- src/views/Edit/Sources/Network.js | 44 ++++++++++------------- src/views/Publication/Services/Youtube.js | 27 +++++++++++--- src/views/Publication/helper.js | 3 ++ 6 files changed, 75 insertions(+), 32 deletions(-) diff --git a/src/misc/coders/helper/index.js b/src/misc/coders/helper/index.js index 47213e9..9bf5c1b 100644 --- a/src/misc/coders/helper/index.js +++ b/src/misc/coders/helper/index.js @@ -23,6 +23,8 @@ function InitSkills(initialSkills) { skills.ffmpeg = { version: '5.0.0', + version_major: 5, + version_minor: 0, ...skills.ffmpeg, }; diff --git a/src/misc/controls/Limits.js b/src/misc/controls/Limits.js index 16a71d5..3e762c7 100644 --- a/src/misc/controls/Limits.js +++ b/src/misc/controls/Limits.js @@ -46,7 +46,7 @@ export default function Control(props) { onChange={handleChange('cpu_usage')} /> - CPU usage limit in percent (0-100%), 0 for unlimited + CPU usage limit in percent (0-100%), 0 for unlimited. diff --git a/src/utils/restreamer.js b/src/utils/restreamer.js index d64d81c..2344b9a 100644 --- a/src/utils/restreamer.js +++ b/src/utils/restreamer.js @@ -6,6 +6,8 @@ import Handlebars from 'handlebars/dist/cjs/handlebars'; import SemverSatisfies from 'semver/functions/satisfies'; import SemverGt from 'semver/functions/gt'; import SemverGte from 'semver/functions/gte'; +import SemverMajor from 'semver/functions/major'; +import SemverMinor from 'semver/functions/minor'; import * as M from './metadata'; import * as Storage from './storage'; @@ -478,6 +480,8 @@ class Restreamer { const skills = { ffmpeg: { version: '', + version_major: 0, + version_minor: 0, }, codecs: { audio: { @@ -536,6 +540,9 @@ class Restreamer { ...val.ffmpeg, }; + skills.ffmpeg.version_major = SemverMajor(skills.ffmpeg.version); + skills.ffmpeg.version_minor = SemverMinor(skills.ffmpeg.version); + val.codecs = { audio: {}, video: {}, @@ -2616,12 +2623,32 @@ class Restreamer { // from the inputs only the first is used and only its options are considered. let address = ''; + let options = []; if (control.source.source === 'hls+memfs') { address = `{memfs}/${channel.channelid}.m3u8`; + options.push('-re'); } else if (control.source.source === 'hls+diskfs') { address = `{diskfs}/${channel.channelid}.m3u8`; + options.push('-re'); } else if (control.source.source === 'rtmp') { address = `{rtmp,name=${channel.channelid}.stream}`; + const skills = this.Skills(); + if (skills.ffmpeg.version_major >= 6) { + const codecs = []; + if (skills.codecs.video.hevc?.length > 0) { + codecs.push('hvc1'); + } + if (skills.codecs.video.av1?.length > 0) { + codecs.push('av01'); + } + if (skills.codecs.video.vp9?.length > 0) { + codecs.push('vp09'); + } + + if (codecs.length !== 0) { + options.push('-rtmp_enhanced_codecs', codecs.join(',')); + } + } } else if (control.source.source === 'srt') { address = `{srt,name=${channel.channelid},mode=request}`; } @@ -2634,7 +2661,7 @@ class Restreamer { { id: 'input_0', address: address, - options: ['-re', ...inputs[0].options], + options: [...options, ...inputs[0].options], }, ], output: [], diff --git a/src/views/Edit/Sources/Network.js b/src/views/Edit/Sources/Network.js index 44bc68f..4fa2167 100644 --- a/src/views/Edit/Sources/Network.js +++ b/src/views/Edit/Sources/Network.js @@ -1,6 +1,5 @@ import React from 'react'; import { useNavigate } from 'react-router-dom'; -import SemverSatisfies from 'semver/functions/satisfies'; import { useLingui } from '@lingui/react'; import { Trans, t } from '@lingui/macro'; @@ -149,6 +148,8 @@ const initSkills = (initialSkills) => { skills.ffmpeg = { version: '0.0.0', + version_major: 0, + version_minor: 0, ...skills.ffmpeg, }; @@ -182,13 +183,6 @@ const createInputs = (settings, config, skills) => { settings = initSettings(settings, config); skills = initSkills(skills); - let ffmpeg_version = 6; - if (SemverSatisfies(skills.ffmpeg.version, '^5.0.0')) { - ffmpeg_version = 5; - } else if (SemverSatisfies(skills.ffmpeg.version, '^4.1.0')) { - ffmpeg_version = 4; - } - const input = { address: '', options: [], @@ -241,7 +235,7 @@ const createInputs = (settings, config, skills) => { if (settings.general.use_wallclock_as_timestamps) { input.options.push('-use_wallclock_as_timestamps', '1'); } - if (ffmpeg_version === 5 && settings.general.avoid_negative_ts !== 'auto') { + if (skills.ffmpeg.version_major >= 5 && settings.general.avoid_negative_ts !== 'auto') { input.options.push('-avoid_negative_ts', settings.general.avoid_negative_ts); } @@ -259,7 +253,7 @@ const createInputs = (settings, config, skills) => { input.options.push('-analyzeduration', settings.general.analyzeduration_rtmp); } - if (ffmpeg_version === 6) { + if (skills.ffmpeg.version_major >= 6) { const codecs = []; if (skills.codecs.video.hevc?.length > 0) { codecs.push('hvc1'); @@ -290,7 +284,7 @@ const createInputs = (settings, config, skills) => { input.options.push('-analyzeduration', settings.general.analyzeduration_rtmp); } - if (ffmpeg_version === 6) { + if (skills.ffmpeg.version_major >= 6) { const codecs = []; if (skills.codecs.video.hevc?.length > 0) { codecs.push('hvc1'); @@ -317,7 +311,7 @@ const createInputs = (settings, config, skills) => { input.address = addUsernamePassword(input.address, settings.username, settings.password); if (protocol === 'rtsp') { - if (ffmpeg_version === 4) { + if (skills.ffmpeg.version_major === 4) { input.options.push('-stimeout', settings.rtsp.stimeout); } else { input.options.push('-timeout', settings.rtsp.stimeout); @@ -461,7 +455,7 @@ const getRTMPAddress = (host, app, name, token, secure) => { let url = 'rtmp' + (secure ? 's' : '') + '://' + host + app + '/' + name + '.stream'; if (token.length !== 0) { - url += '?token=' + encodeURIComponent(token); + url += '/token=' + encodeURIComponent(token); } return url; @@ -689,13 +683,13 @@ function AdvancedSettings(props) { ? settings.push.type === 'hls' ? settings.general.analyzeduration_http : settings.push.type === 'rtmp' - ? settings.general.analyzeduration_rtmp - : settings.general.analyzeduration + ? settings.general.analyzeduration_rtmp + : settings.general.analyzeduration : protocolClass === 'http' - ? settings.general.analyzeduration_http - : protocolClass === 'rtmp' - ? settings.general.analyzeduration_rtmp - : settings.general.analyzeduration + ? settings.general.analyzeduration_http + : protocolClass === 'rtmp' + ? settings.general.analyzeduration_rtmp + : settings.general.analyzeduration } onChange={props.onChange( 'general', @@ -703,13 +697,13 @@ function AdvancedSettings(props) { ? settings.push.type === 'hls' ? 'analyzeduration_http' : settings.push.type === 'rtmp' - ? 'analyzeduration_rtmp' - : 'analyzeduration' + ? 'analyzeduration_rtmp' + : 'analyzeduration' : protocolClass === 'http' - ? 'analyzeduration_http' - : protocolClass === 'rtmp' - ? 'analyzeduration_rtmp' - : 'analyzeduration', + ? 'analyzeduration_http' + : protocolClass === 'rtmp' + ? 'analyzeduration_rtmp' + : 'analyzeduration', )} /> diff --git a/src/views/Publication/Services/Youtube.js b/src/views/Publication/Services/Youtube.js index 0977a7a..478e9f6 100644 --- a/src/views/Publication/Services/Youtube.js +++ b/src/views/Publication/Services/Youtube.js @@ -15,7 +15,7 @@ import Select from '../../../misc/Select'; const id = 'youtube'; const name = 'YouTube Live'; -const version = '1.0'; +const version = '1.1'; const stream_key_link = 'https://www.youtube.com/live_dashboard'; const description = ( @@ -54,8 +54,7 @@ const requires = { formats: ['flv', 'hls'], codecs: { audio: ['aac', 'mp3'], - video: ['h264'], - // video: ['h264', 'h265', 'vp9' , 'av1'], + video: ['h264', 'hevc', 'av1'], }, }; @@ -100,18 +99,36 @@ function Service(props) { } if (settings.mode === 'rtmps') { + let options = ['-f', 'flv']; + + console.log('codecs', props.skills.codecs); + + if (props.skills.ffmpeg.version_major >= 6) { + const codecs = []; + if (props.skills.codecs.video.includes('hevc')) { + codecs.push('hvc1'); + } + if (props.skills.codecs.video.includes('av1')) { + codecs.push('av01'); + } + + if (codecs.length !== 0) { + options.push('-rtmp_enhanced_codecs', codecs.join(',')); + } + } + // https://developers.google.com/youtube/v3/live/guides/rtmps-ingestion if (settings.primary === true) { outputs.push({ address: 'rtmps://a.rtmp.youtube.com/live2/' + settings.stream_key, - options: ['-f', 'flv'], + options: options.slice(), }); } if (settings.backup === true) { outputs.push({ address: 'rtmps://b.rtmp.youtube.com/live2?backup=1/' + settings.stream_key, - options: ['-f', 'flv'], + options: options.slice(), }); } } else if (settings.mode === 'hls') { diff --git a/src/views/Publication/helper.js b/src/views/Publication/helper.js index 88e4e4c..a8c1be8 100644 --- a/src/views/Publication/helper.js +++ b/src/views/Publication/helper.js @@ -193,6 +193,9 @@ export function conflateServiceSkills(requires, skills) { requires = validateRequirements(requires); const serviceSkills = { + ffmpeg: { + ...skills.ffmpeg, + }, protocols: [], formats: [], devices: {},