Add h264 player stream for unsupported codecs (experimental)

This commit is contained in:
Jan Stabenow 2024-09-24 17:34:47 +02:00
parent 8db285fe89
commit c3632068a6
6 changed files with 353 additions and 75 deletions

View File

@ -14,6 +14,15 @@ export default function VideoJS(props) {
const playerRef = React.useRef(null);
const { options, onReady } = props;
const retryVideo = () => {
const player = playerRef.current;
if (player) {
player.error(null); // Clear the error
player.src(options.sources); // Reload the source
player.play(); // Attempt to play again
}
};
React.useEffect(() => {
// make sure Video.js player is only initialized once
if (!playerRef.current) {
@ -32,6 +41,17 @@ export default function VideoJS(props) {
}
player.addClass('video-js');
player.addClass('vjs-16-9');
// retry on MEDIA_ERR_NETWORK = 2
let retry_count = 0;
player.on('error', () => {
const error = player.error();
console.log(error);
if (error && (error.code === 2 || error.code === 4) && retry_count < 10) {
retry_count += 1;
setTimeout(retryVideo, 2000);
}
});
} else {
// you can update player here [update player through props]
// const player = playerRef.current;

View File

@ -0,0 +1,81 @@
import React from 'react';
import { Trans } from '@lingui/macro';
import Grid from '@mui/material/Grid';
import MenuItem from '@mui/material/MenuItem';
import Typography from '@mui/material/Typography';
import Checkbox from '../Checkbox';
import Select from '../Select';
function init(settings) {
const initSettings = {
enable: true,
video_encoder: 'libx264',
audio_encoder: 'aac',
...settings,
};
return initSettings;
}
export default function Control(props) {
const settings = init(props.settings);
const encoders = props.encoders;
// Set the defaults
React.useEffect(() => {
props.onChange(settings, true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleChange = (what) => (event) => {
const value = event.target.value;
if (['enable'].includes(what)) {
settings[what] = !settings[what];
} else {
settings[what] = value;
}
props.onChange(settings, false);
};
return (
<Grid container spacing={2}>
<Grid item xs={12}>
<Checkbox label={<Trans>Enable browser-compatible H.264 stream</Trans>} checked={settings.enable} onChange={handleChange('enable')} />
</Grid>
<Grid item xs={12} md={6}>
<Select label={<Trans>Video Codec</Trans>} value={settings.video_codec} onChange={handleChange('video_encoder')}>
<MenuItem value="libx264" disabled={!encoders.includes('libx264')}>
H.264 (libx264)
</MenuItem>
<MenuItem value="h264_nvenc" disabled={!encoders.includes('h264_nvenc')}>
<Trans>H.264 (NVENC)</Trans>
</MenuItem>
<MenuItem value="h264_omx" disabled={!encoders.includes('h264_omx')}>
<Trans>H.264 (OpenMAX IL)</Trans>
</MenuItem>
<MenuItem value="h264_v4l2m2m" disabled={!encoders.includes('h264_v4l2m2m')}>
<Trans>H.264 (V4L2 Memory to Memory)</Trans>
</MenuItem>
<MenuItem value="h264_vaapi" disabled={!encoders.includes('h264_vaapi')}>
<Trans>H.264 (Intel VAAPI)</Trans>
</MenuItem>
<MenuItem value="h264_videotoolbox" disabled={!encoders.includes('h264_videotoolbox')}>
<Trans>H.264 (VideoToolbox)</Trans>
</MenuItem>
</Select>
<Typography variant="caption">
<Trans>The H.264 encoder used.</Trans>
</Typography>
</Grid>
</Grid>
);
}
Control.defaulProps = {
settings: {},
onChange: function (settings, automatic) {},
};

View File

@ -285,6 +285,11 @@ const defaultIngestMetadata = {
enable: true,
interval: 60,
},
preview: {
enable: false,
video_encoder: 'libx264',
audio_encoder: 'aac',
},
limits: {
cpu_usage: 0,
memory_mbytes: 0,
@ -472,6 +477,11 @@ const mergeIngestMetadata = (metadata, base) => {
...metadata.control.snapshot,
};
metadata.control.preview = {
...base.control.preview,
...metadata.control.preview,
};
if (!Array.isArray(metadata.sources)) {
metadata.sources = [];
} else {

View File

@ -1507,15 +1507,21 @@ class Restreamer {
}
// Get the ingest progress
async GetIngestProgress(channelid) {
async GetIngestProgress(channelid, what) {
// [what] for custom id extentions
const channel = this.GetChannel(channelid);
if (channel === null) {
return this._getProgressFromState(null);
}
const state = await this._getProcessState(channel.id);
return this._getProgressFromState(state);
if (!what) {
const state = await this._getProcessState(`${channel.id}`);
return this._getProgressFromState(state);
} else {
// id=abc, what=_preview = abc_preview
const state = await this._getProcessState(`${channel.id}${what}`);
return this._getProgressFromState(state);
}
}
// Get the ingest log
@ -1716,71 +1722,49 @@ class Restreamer {
// fix Malformed AAC bitstream detected for hls version 7
let hls_aac_adtstoasc = false;
const getHLSParams = (lhls, version) => {
if (lhls) {
// lhls
return [
['f', 'dash'],
['strict', 'experimental'],
['hls_playlist', '1'],
['init_seg_name', `init-${channel.channelid}.$ext$`],
['media_seg_name', `chunk-${channel.channelid}-$Number%05d$.$ext$`],
['master_m3u8_publish_rate', '1'],
['adaptation_sets', 'id=0,streams=v id=1,streams=a'],
['lhls', '1'],
['streaming', '1'],
['seg_duration', '' + parseInt(control.hls.segmentDuration)],
['frag_duration', '0.5'],
['use_template', '1'],
['remove_at_exit', '0'],
['window_size', '' + parseInt(control.hls.listSize)],
['http_persistent', '0'],
];
} else {
// hls
switch (version) {
case 6:
return [
['f', 'hls'],
['start_number', '0'],
['hls_time', '' + parseInt(control.hls.segmentDuration)],
['hls_list_size', '' + parseInt(control.hls.listSize)],
['hls_flags', 'append_list+delete_segments+program_date_time+independent_segments+temp_file'],
['hls_delete_threshold', '4'],
['hls_segment_filename', hls_segment_filename],
];
case 7:
// fix Malformed AAC bitstream detected for hls version 7
if (output.options.includes('-codec:a') && output.options.includes('copy')) {
if (!tee_muxer) {
output.options.push('-bsf:a', 'aac_adtstoasc');
}
hls_aac_adtstoasc = true;
const getHLSParams = (version) => {
switch (version) {
case 6:
return [
['f', 'hls'],
['start_number', '0'],
['hls_time', '' + parseInt(control.hls.segmentDuration)],
['hls_list_size', '' + parseInt(control.hls.listSize)],
['hls_flags', 'append_list+delete_segments+program_date_time+independent_segments+temp_file'],
['hls_delete_threshold', '4'],
['hls_segment_filename', hls_segment_filename],
];
case 7:
// fix Malformed AAC bitstream detected for hls version 7
if (output.options.includes('-codec:a') && output.options.includes('copy')) {
if (!tee_muxer) {
output.options.push('-bsf:a', 'aac_adtstoasc');
}
return [
['f', 'hls'],
['start_number', '0'],
['hls_time', '' + parseInt(control.hls.segmentDuration)],
['hls_list_size', '' + parseInt(control.hls.listSize)],
['hls_flags', 'append_list+delete_segments+program_date_time+independent_segments+temp_file'],
['hls_delete_threshold', '4'],
['hls_segment_type', 'fmp4'],
['hls_fmp4_init_filename', hls_fmp4_init_filename],
['hls_fmp4_init_resend', '1'],
['hls_segment_filename', hls_segment_filename],
];
// case 3
default:
return [
['f', 'hls'],
['start_number', '0'],
['hls_time', '' + parseInt(control.hls.segmentDuration)],
['hls_list_size', '' + parseInt(control.hls.listSize)],
['hls_flags', 'append_list+delete_segments+program_date_time+temp_file'],
['hls_delete_threshold', '4'],
['hls_segment_filename', hls_segment_filename],
];
}
hls_aac_adtstoasc = true;
}
return [
['f', 'hls'],
['start_number', '0'],
['hls_time', '' + parseInt(control.hls.segmentDuration)],
['hls_list_size', '' + parseInt(control.hls.listSize)],
['hls_flags', 'append_list+delete_segments+program_date_time+independent_segments+temp_file'],
['hls_delete_threshold', '4'],
['hls_segment_type', 'fmp4'],
['hls_fmp4_init_filename', hls_fmp4_init_filename],
['hls_fmp4_init_resend', '1'],
['hls_segment_filename', hls_segment_filename],
];
// case 3
default:
return [
['f', 'hls'],
['start_number', '0'],
['hls_time', '' + parseInt(control.hls.segmentDuration)],
['hls_list_size', '' + parseInt(control.hls.listSize)],
['hls_flags', 'append_list+delete_segments+program_date_time+temp_file'],
['hls_delete_threshold', '4'],
['hls_segment_filename', hls_segment_filename],
];
}
};
const hls_params_raw = getHLSParams(control.hls.lhls, control.hls.version);
@ -1945,8 +1929,8 @@ class Restreamer {
},
],
options: ['-err_detect', 'ignore_err'],
autostart: control.process.autostart,
reconnect: true,
autostart: control.snapshot.enable ? control.process.autostart : false,
reconnect: control.snapshot.enable ? true : false,
reconnect_delay_seconds: parseInt(control.snapshot.interval),
stale_timeout_seconds: 30,
};
@ -1959,6 +1943,99 @@ class Restreamer {
return [val, null];
}
// Upsert the ingest browser playback process (preview)
async UpsertIngestPreview(channelid, control) {
const channel = this.GetChannel(channelid);
if (channel === null) {
return [null, { message: 'Unknown channel ID' }];
}
// Set hls storage endpoint
const hlsStorage = control.hls.storage;
// Set encoder settings
const video_encoder = control.preview.video_encoder;
const audio_encoder = control.preview.audio_encoder;
const preview = {
type: 'ffmpeg',
id: channel.id + '_h264',
reference: channel.channelid,
input: [
{
id: 'input_0',
address: `{${hlsStorage}}/${channel.channelid}.m3u8`,
options: [],
},
],
output: [
{
id: 'output_0',
address: `{memfs}/${channel.channelid}_output_0_h264.m3u8`,
options: [
'-c:v',
`${video_encoder}`,
'-r',
'25',
'-g',
'50',
'-sc_threshold',
'0',
'-pix_fmt',
'yuv420p',
'-c:a',
`${audio_encoder}`,
'-f',
'hls',
'-start_number',
'0',
'-hls_time',
'2',
'-hls_list_size',
'6',
'-hls_flags',
'append_list+delete_segments+program_date_time+temp_file',
'-hls_delete_threshold',
'4',
'-hls_segment_filename',
`{memfs}/${channel.channelid}_output_0_h264_%04d.ts`,
'-master_pl_name',
`${channel.channelid}_h264.m3u8`,
'-master_pl_publish_rate',
'2',
],
cleanup: [
{
pattern: `memfs:/${channel.channelid}_h264.m3u8`,
purge_on_delete: true,
},
{
pattern: `memfs:/${channel.channelid}_output_0_h264.m3u8`,
purge_on_delete: true,
},
{
pattern: `memfs:/${channel.channelid}_output_0_h264_*.ts`,
max_files: 12,
purge_on_delete: true,
},
],
},
],
options: ['-err_detect', 'ignore_err'],
autostart: control.preview.enable ? control.process.autostart : false,
reconnect: control.preview.enable ? control.process.reconnect : false,
reconnect_delay_seconds: 2,
stale_timeout_seconds: 5,
};
const [val, err] = await this._upsertProcess(channel.id + '_h264', preview);
if (err !== null) {
return [val, err];
}
return [val, null];
}
// Check whether the manifest of the ingest process is available
async HasIngestFiles(channelid) {
const channel = this.GetChannel(channelid);
@ -3410,6 +3487,8 @@ class Restreamer {
command: [],
cpu: 0,
memory: 0,
video_codec: '',
audio_codec: '',
};
if (state === null) {
@ -3452,8 +3531,16 @@ class Restreamer {
progress.dup = state.progress.dup || 0;
progress.cpu = state.cpu_usage || 0;
progress.memory = state.memory_bytes || 0;
}
// check av codec @ preview
for (const o in state.progress.outputs) {
if (state.progress.outputs[o].type === 'video') {
progress.video_codec = state.progress.outputs[o].codec;
} else if (state.progress.outputs[o].type === 'audio') {
progress.audio_codec = state.progress.outputs[o].codec;
}
}
}
return progress;
}

View File

@ -29,6 +29,7 @@ import Paper from '../../misc/Paper';
import PaperHeader from '../../misc/PaperHeader';
import PaperFooter from '../../misc/PaperFooter';
import PaperThumb from '../../misc/PaperThumb';
import PreviewControl from '../../misc/controls/Preview';
import ProcessControl from '../../misc/controls/Process';
import Profile from './Profile';
import ProfileSummary from './ProfileSummary';
@ -322,6 +323,12 @@ export default function Edit(props) {
notify.Dispatch('error', 'save:ingest', i18n._(t`Failed to update ingest snapshot process (${err.message})`));
}
// Create/update the ingest preview process
[, err] = await props.restreamer.UpsertIngestPreview(_channelid, control);
if (err !== null) {
notify.Dispatch('error', 'save:ingest', i18n._(t`Failed to update ingest preview process (${err.message})`));
}
// Create/update the player
res = await props.restreamer.UpdatePlayer(_channelid);
if (res === false) {
@ -549,6 +556,21 @@ export default function Edit(props) {
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>Player Playback</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<PreviewControl
encoders={$skills.encoders.video}
settings={$data.control.preview}
onChange={handleControlChange('preview')}
/>
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>Process</Trans>

View File

@ -38,7 +38,7 @@ const useStyles = makeStyles((theme) => ({
playerL1: {
//padding: '4px 1px 4px 8px',
paddingTop: 10,
paddingLeft: 18
paddingLeft: 18,
},
playerL2: {
position: 'relative',
@ -69,6 +69,7 @@ export default function Main(props) {
progress: {},
state: 'disconnected',
onConnect: null,
preview: null,
});
const [$metadata, setMetadata] = React.useState(M.getDefaultIngestMetadata());
const [$processDetails, setProcessDetails] = React.useState({
@ -149,6 +150,15 @@ export default function Main(props) {
}, 100);
state.onConnect = null;
}
// check av codec @ preview
if (state.progress.video_codec !== 'h264' && $state.preview === null) {
const preview_progress = await props.restreamer.GetIngestProgress(`${_channelid}`, '_preview');
if (preview_progress) {
state.preview = false;
} else {
state.preview = true;
}
}
}
if ($metadata.control.rtmp.enable) {
@ -287,11 +297,16 @@ export default function Main(props) {
const storage = $metadata.control.hls.storage;
const channel = props.restreamer.GetChannel(_channelid);
const manifest = props.restreamer.GetChannelAddress('hls+' + storage, _channelid);
const manifest_preview = props.restreamer.GetChannelAddress('hls+' + storage, `${_channelid}_h264`);
const poster = props.restreamer.GetChannelAddress('snapshot+' + storage, _channelid);
let title = <Trans>Main channel</Trans>;
let title = <Trans>{$state.progress.video_codec} - Main channel</Trans>;
if (channel && channel.name && channel.name.length !== 0) {
title = channel.name;
if ($state.progress.video_codec) {
title = `${$state.progress.video_codec} - ${channel.name}`;
} else {
title = `${channel.name}`;
}
}
return (
@ -382,9 +397,42 @@ export default function Main(props) {
)}
</Grid>
)}
{$state.state === 'connected' && (
{$state.state === 'connected' && $state.progress.video_codec === 'h264' && (
<Player type="videojs-internal" source={manifest} poster={poster} autoplay mute controls />
)}
{$state.state === 'connected' && $state.progress.video_codec !== 'h264' && $metadata.control.preview.enable && (
<Player type="videojs-internal" source={manifest_preview} poster={poster} autoplay mute controls />
)}
{$state.state === 'connected' && $state.progress.video_codec !== 'h264' && !$metadata.control.preview.enable && (
<Grid
container
direction="column"
className={classes.playerL3}
justifyContent="center"
alignItems="center"
spacing={1}
>
<Grid item>
<WarningIcon className={classes.playerWarningIcon} />
</Grid>
<Grid item>
<Typography>
<Trans>No H.264 Stream availabe.</Trans>
</Typography>
</Grid>
<Grid item textAlign={'center'}>
<Typography>
<Trans>
Please{' '}
<Link style={{ textDecoration: 'underline' }} onClick={() => navigate(`/${_channelid}/edit`)}>
edit
</Link>{' '}
this channel and enable the browser-compatible H.264 stream in the "Processing & Control" area:
</Trans>
</Typography>
</Grid>
</Grid>
)}
</Grid>
</Grid>
</Grid>
@ -433,6 +481,16 @@ export default function Main(props) {
>
<Trans>Snapshot</Trans>
</CopyButton>
{$metadata.control.preview.enable && (
<CopyButton
variant="outlined"
color="default"
size="small"
value={props.restreamer.GetPublicAddress('hls+' + storage, `${_channelid}_h264`)}
>
<Trans>HLS @ H.264</Trans>
</CopyButton>
)}
</Stack>
</Stack>
</Grid>