diff --git a/src/misc/Player/videojs.js b/src/misc/Player/videojs.js
index 52b3d48..0bbb08d 100644
--- a/src/misc/Player/videojs.js
+++ b/src/misc/Player/videojs.js
@@ -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;
diff --git a/src/misc/controls/Preview.js b/src/misc/controls/Preview.js
new file mode 100644
index 0000000..1078b66
--- /dev/null
+++ b/src/misc/controls/Preview.js
@@ -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 (
+
+
+ Enable browser-compatible H.264 stream} checked={settings.enable} onChange={handleChange('enable')} />
+
+
+
+
+ The H.264 encoder used.
+
+
+
+ );
+}
+
+Control.defaulProps = {
+ settings: {},
+ onChange: function (settings, automatic) {},
+};
diff --git a/src/utils/metadata.js b/src/utils/metadata.js
index fc0eea0..a2b34a3 100644
--- a/src/utils/metadata.js
+++ b/src/utils/metadata.js
@@ -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 {
diff --git a/src/utils/restreamer.js b/src/utils/restreamer.js
index 5fcb4ef..c4cbf88 100644
--- a/src/utils/restreamer.js
+++ b/src/utils/restreamer.js
@@ -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;
}
diff --git a/src/views/Edit/index.js b/src/views/Edit/index.js
index a5fa17c..cbea87c 100644
--- a/src/views/Edit/index.js
+++ b/src/views/Edit/index.js
@@ -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) {
+
+
+ Player Playback
+
+
+
+
+
+
+
+
Process
diff --git a/src/views/Main/index.js b/src/views/Main/index.js
index d242617..35a3956 100644
--- a/src/views/Main/index.js
+++ b/src/views/Main/index.js
@@ -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 = Main channel;
+ let title = {$state.progress.video_codec} - Main channel;
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) {
)}
)}
- {$state.state === 'connected' && (
+ {$state.state === 'connected' && $state.progress.video_codec === 'h264' && (
)}
+ {$state.state === 'connected' && $state.progress.video_codec !== 'h264' && $metadata.control.preview.enable && (
+
+ )}
+ {$state.state === 'connected' && $state.progress.video_codec !== 'h264' && !$metadata.control.preview.enable && (
+
+
+
+
+
+
+ No H.264 Stream availabe.
+
+
+
+
+
+ Please{' '}
+ navigate(`/${_channelid}/edit`)}>
+ edit
+ {' '}
+ this channel and enable the browser-compatible H.264 stream in the "Processing & Control" area:
+
+
+
+
+ )}
@@ -433,6 +481,16 @@ export default function Main(props) {
>
Snapshot
+ {$metadata.control.preview.enable && (
+
+ HLS @ H.264
+
+ )}