Add h264 player stream for unsupported codecs (experimental)
This commit is contained in:
parent
8db285fe89
commit
c3632068a6
@ -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;
|
||||
|
||||
81
src/misc/controls/Preview.js
Normal file
81
src/misc/controls/Preview.js
Normal 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) {},
|
||||
};
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user