Allow to stream HEVC and AV1 to Youtube via RTMP

This commit is contained in:
Ingo Oppermann 2024-03-27 21:01:53 +01:00
parent 82bd4f2d76
commit 6292e62858
No known key found for this signature in database
GPG Key ID: 2AB32426E9DD229E
6 changed files with 75 additions and 32 deletions

View File

@ -23,6 +23,8 @@ function InitSkills(initialSkills) {
skills.ffmpeg = {
version: '5.0.0',
version_major: 5,
version_minor: 0,
...skills.ffmpeg,
};

View File

@ -46,7 +46,7 @@ export default function Control(props) {
onChange={handleChange('cpu_usage')}
/>
<Typography variant="caption">
<Trans>CPU usage limit in percent (0-100%), 0 for unlimited</Trans>
<Trans>CPU usage limit in percent (0-100%), 0 for unlimited.</Trans>
</Typography>
</Grid>
<Grid item xs={12} md={4}>

View File

@ -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: [],

View File

@ -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',
)}
/>
<Typography variant="caption">

View File

@ -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 = (
<Trans>
@ -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') {

View File

@ -193,6 +193,9 @@ export function conflateServiceSkills(requires, skills) {
requires = validateRequirements(requires);
const serviceSkills = {
ffmpeg: {
...skills.ffmpeg,
},
protocols: [],
formats: [],
devices: {},