Add title and description fields to streaming services and pre-fill from metadata

This commit is contained in:
CesarMendivil 2026-02-28 19:43:17 -07:00
parent 9386273731
commit 2455251423
12 changed files with 474 additions and 87 deletions

View File

@ -500,6 +500,7 @@ export default function Profile(props) {
onChange={handleSourceSettingsChange}
onRefresh={handleRefresh}
onStore={handleStore}
onYoutubeMetadata={props.onYoutubeMetadata}
/>
</Grid>
{$videoProbe.status !== 'none' && (
@ -807,4 +808,5 @@ Profile.defaultProps = {
onStore: function (name, data) {
return '';
},
onYoutubeMetadata: function (title, description) {},
};

View File

@ -102,6 +102,7 @@ export default function SourceSelect(props) {
onProbe={handleProbe}
onRefresh={handleRefresh}
onStore={handleStore}
onYoutubeMetadata={props.onYoutubeMetadata}
/>
);
}
@ -135,6 +136,7 @@ SourceSelect.defaultProps = {
onChange: function (type, device, settings) {},
onRefresh: function () {},
onStore: function (name, data) {},
onYoutubeMetadata: function (title, description) {},
};
function Select(props) {

View File

@ -814,6 +814,14 @@ function Pull(props) {
if (data && data.stream_url) {
props.onChange('', 'address')({ target: { value: data.stream_url } });
setExtractorError('');
// Propagar title y description si el servicio los devuelve
if (typeof props.onYoutubeMetadata === 'function') {
const title = data.title || data.video_title || '';
const description = data.description || data.video_description || '';
if (title || description) {
props.onYoutubeMetadata(title, description);
}
}
} else {
setExtractorError('No stream_url found in service response.');
}
@ -1255,7 +1263,7 @@ function Source(props) {
</Grid>
</Grid>
{settings.mode === 'pull' ? (
<Pull settings={settings} config={config} skills={skills} onChange={handleChange} onProbe={handleProbe} />
<Pull settings={settings} config={config} skills={skills} onChange={handleChange} onProbe={handleProbe} onYoutubeMetadata={props.onYoutubeMetadata} />
) : (
<Push
settings={settings}
@ -1278,6 +1286,7 @@ Source.defaultProps = {
skills: null,
onChange: function (settings) {},
onProbe: function (settings, inputs) {},
onYoutubeMetadata: function (title, description) {},
};
function SourceIcon(props) {

View File

@ -278,6 +278,20 @@ export default function Edit(props) {
});
};
const handleYoutubeMetadata = (title, description) => {
setData((prev) => ({
...prev,
meta: {
...prev.meta,
...(title ? { name: title } : {}),
...(description ? { description: description } : {}),
},
}));
if (title || description) {
notify.Dispatch('success', 'youtube:metadata', i18n._(t`Title and description filled from YouTube`));
}
};
const handleLicenseChange = (license) => {
setData({
...$data,
@ -472,6 +486,7 @@ export default function Edit(props) {
onDone={handleSourceDone}
onAbort={handleSourceAbort}
onStore={handleSourceStore}
onYoutubeMetadata={handleYoutubeMetadata}
/>
)}
</TabPanel>

View File

@ -41,6 +41,8 @@ function ServiceIcon(props) {
function init(settings) {
const initSettings = {
key: '',
title: '',
description: '',
...settings,
};
@ -50,6 +52,14 @@ function init(settings) {
function Service(props) {
const settings = init(props.settings);
// Pre-fill title/description from channel metadata if not already set
if (!settings.title && props.metadata && props.metadata.name) {
settings.title = props.metadata.name;
}
if (!settings.description && props.metadata && props.metadata.description) {
settings.description = props.metadata.description;
}
const handleChange = (what) => (event) => {
const value = event.target.value;
@ -79,6 +89,28 @@ function Service(props) {
<Trans>GET</Trans>
</FormInlineButton>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
fullWidth
label={<Trans>Stream title</Trans>}
placeholder={props.metadata && props.metadata.name ? props.metadata.name : ''}
value={settings.title}
onChange={handleChange('title')}
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
fullWidth
multiline
rows={3}
label={<Trans>Stream description</Trans>}
placeholder={props.metadata && props.metadata.description ? props.metadata.description : ''}
value={settings.description}
onChange={handleChange('description')}
/>
</Grid>
</Grid>
);
}

View File

@ -4,10 +4,14 @@ import { faFacebook } from '@fortawesome/free-brands-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Trans } from '@lingui/macro';
import Grid from '@mui/material/Grid';
import Link from '@mui/material/Link';
import MenuItem from '@mui/material/MenuItem';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import Checkbox from '../../../misc/Checkbox';
import FormInlineButton from '../../../misc/FormInlineButton';
import Select from '../../../misc/Select';
const id = 'facebook';
const name = 'Facebook Live';
@ -16,14 +20,8 @@ const stream_key_link = 'https://www.facebook.com/live/producer?ref=datarhei/res
const description = <Trans>Live-Streaming to Facebook Live RTMP service</Trans>;
const image_copyright = <Trans>More about licenses here</Trans>;
const author = {
creator: {
name: 'datarhei',
link: 'https://github.com/datarhei',
},
maintainer: {
name: 'datarhei',
link: 'https://github.com/datarhei',
},
creator: { name: 'datarhei', link: 'https://github.com/datarhei' },
maintainer: { name: 'datarhei', link: 'https://github.com/datarhei' },
};
const category = 'platform';
const requires = {
@ -40,12 +38,13 @@ function ServiceIcon(props) {
}
function init(settings) {
const initSettings = {
return {
stream_key_primary: '',
stream_key_backup: '',
rtmp_primary: true,
rtmp_backup: false,
// new fields
// API fields
account_type: 'page', // 'page' | 'user'
page_id: '',
page_access_token: '',
title: '',
@ -53,15 +52,26 @@ function init(settings) {
create_live: false,
...settings,
};
return initSettings;
}
function Service(props) {
const settings = init(props.settings);
const [$loading, setLoading] = React.useState(false);
const [$apiError, setApiError] = React.useState('');
const [$apiSuccess, setApiSuccess] = React.useState('');
// Pre-fill title/description from channel metadata if not already set
if (!settings.title && props.metadata && props.metadata.name) {
settings.title = props.metadata.name;
}
if (!settings.description && props.metadata && props.metadata.description) {
settings.description = props.metadata.description;
}
const handleChange = (what) => (event) => {
const value = event && event.target ? event.target.value : event;
setApiError('');
setApiSuccess('');
if (['rtmp_primary', 'rtmp_backup', 'create_live'].includes(what)) {
settings[what] = !settings[what];
@ -69,97 +79,116 @@ function Service(props) {
settings[what] = value;
}
const output = createOutput(settings);
props.onChange(output, settings);
props.onChange(createOutput(settings), settings);
};
const createOutput = (settings) => {
const outputs = [];
const output_primary = {
address: 'rtmps://live-api-s.facebook.com:443/rtmp/' + settings.stream_key_primary,
options: ['-f', 'flv'],
};
const output_backup = {
address: 'rtmps://live-api-s.facebook.com:443/rtmp/' + settings.stream_key_backup,
options: ['-f', 'flv'],
};
if (settings.stream_key_primary.length !== 0) {
if (settings.rtmp_primary) {
outputs.push(output_primary);
}
if (settings.stream_key_primary.length !== 0 && settings.rtmp_primary) {
outputs.push({
address: 'rtmps://live-api-s.facebook.com:443/rtmp/' + settings.stream_key_primary,
options: ['-f', 'flv'],
});
}
if (settings.stream_key_backup.length !== 0) {
if (settings.rtmp_backup) {
outputs.push(output_backup);
}
if (settings.stream_key_backup.length !== 0 && settings.rtmp_backup) {
outputs.push({
address: 'rtmps://live-api-s.facebook.com:443/rtmp/' + settings.stream_key_backup,
options: ['-f', 'flv'],
});
}
return outputs;
};
// Creates a Facebook live video via Graph API and fills the primary stream key
// Extraer stream key desde stream_url de Facebook
const extractKey = (streamUrl) => {
if (!streamUrl) return '';
const parts = streamUrl.split('/');
return parts[parts.length - 1] || '';
};
// Crea un Facebook Live via Graph API
const createFacebookLive = async () => {
if (!settings.page_id || !settings.page_access_token) {
window.alert('Please provide Page ID and Page Access Token to create a Facebook Live.');
setApiError('');
setApiSuccess('');
if (!settings.page_access_token) {
setApiError('You must provide a Page Access Token (or User Access Token for personal profile).');
return;
}
// Para tipo 'page' se requiere page_id
if (settings.account_type === 'page' && !settings.page_id) {
setApiError('You must provide the Page ID when using a Page account. For personal profile, switch to "Personal profile".');
return;
}
setLoading(true);
try {
const url = `https://graph.facebook.com/${encodeURIComponent(settings.page_id)}/live_videos`;
// El subject puede ser el page_id (para páginas) o 'me' (para perfil personal)
const subject = settings.account_type === 'page'
? encodeURIComponent(settings.page_id)
: 'me';
const url = `https://graph.facebook.com/v19.0/${subject}/live_videos`;
const form = new URLSearchParams();
if (settings.title) form.append('title', settings.title);
if (settings.description) form.append('description', settings.description);
form.append('status', 'LIVE_NOW');
form.append('access_token', settings.page_access_token);
const resp = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: form.toString(),
});
if (!resp.ok) {
const text = await resp.text();
throw new Error('Facebook API error: ' + text);
}
const data = await resp.json();
// Facebook returns a stream_url or stream_url/secure_stream_url — extract key
let stream = '';
if (data.stream_url) stream = data.stream_url;
else if (data.secure_stream_url) stream = data.secure_stream_url;
if (!resp.ok || data.error) {
const errMsg = data.error
? `[#${data.error.code}] ${data.error.message}`
: `HTTP ${resp.status}`;
if (stream.length === 0) {
window.alert('Facebook did not return a stream URL.');
// Mensajes de error específicos con solución
if (data.error && data.error.code === 100) {
setApiError(
'Error #100: The subject must be a Page account, not a personal profile. ' +
'Switch to "Personal profile" mode or provide a valid Page ID with a Page Access Token. ' +
'Get your Page Access Token at: https://developers.facebook.com/tools/explorer/'
);
} else if (data.error && data.error.code === 190) {
setApiError('Error #190: Invalid or expired Access Token. Generate a new one at https://developers.facebook.com/tools/explorer/');
} else if (data.error && data.error.code === 200 || data.error && data.error.code === 10) {
setApiError('Permission error: Your token needs the "publish_video" permission. Check your app settings at developers.facebook.com');
} else {
setApiError('Facebook API error: ' + errMsg);
}
return;
}
// stream like rtmps://live-api-s.facebook.com:443/rtmp/STREAM_KEY
const parts = stream.split('/');
const key = parts[parts.length - 1] || '';
// Extraer stream_url
const streamUrl = data.stream_url || data.secure_stream_url || '';
if (!streamUrl) {
setApiError('Facebook did not return a stream URL. Check that your token has the "publish_video" permission.');
return;
}
const key = extractKey(streamUrl);
settings.stream_key_primary = key;
settings.create_live = true;
const outputs = createOutput(settings);
props.onChange(outputs, settings);
window.alert('Facebook Live creado y stream key rellenada.');
props.onChange(createOutput(settings), settings);
setApiSuccess(`✅ Facebook Live created! Stream key filled automatically. Live ID: ${data.id}`);
} catch (err) {
console.error(err);
window.alert('Error creando Facebook Live: ' + err.message);
setApiError('Network error: ' + err.message + '. Check if CORS is enabled or try from a server-side proxy.');
} finally {
setLoading(false);
}
};
return (
<Grid container spacing={2}>
{/* Stream keys */}
{settings.rtmp_primary === true && (
<Grid item xs={12} md={9}>
<TextField
@ -169,11 +198,6 @@ function Service(props) {
value={settings.stream_key_primary}
onChange={handleChange('stream_key_primary')}
/>
{/* Button to create FB Live and populate stream key */}
<FormInlineButton onClick={createFacebookLive} style={{ marginTop: 8 }}>
<Trans>Create</Trans>
</FormInlineButton>
</Grid>
)}
{settings.rtmp_primary === true && (
@ -201,27 +225,138 @@ function Service(props) {
</FormInlineButton>
</Grid>
)}
{/* New fields for page id, token, title and description */}
<Grid item xs={12} md={6}>
<TextField variant="outlined" fullWidth label={<Trans>Facebook Page ID</Trans>} value={settings.page_id} onChange={handleChange('page_id')} />
</Grid>
<Grid item xs={12} md={6}>
<TextField variant="outlined" fullWidth label={<Trans>Page Access Token</Trans>} value={settings.page_access_token} onChange={handleChange('page_access_token')} />
</Grid>
<Grid item xs={12}>
<TextField variant="outlined" fullWidth label={<Trans>Title</Trans>} value={settings.title} onChange={handleChange('title')} />
</Grid>
<Grid item xs={12}>
<TextField variant="outlined" fullWidth label={<Trans>Description</Trans>} value={settings.description} onChange={handleChange('description')} />
</Grid>
<Grid item xs={12}>
<Checkbox label={<Trans>Enable primary stream</Trans>} checked={settings.rtmp_primary} onChange={handleChange('rtmp_primary')} />
<Checkbox label={<Trans>Enable backup stream</Trans>} checked={settings.rtmp_backup} onChange={handleChange('rtmp_backup')} />
{/* Optionally mark that we created the live via API */}
<Checkbox label={<Trans>Create live via API</Trans>} checked={settings.create_live} onChange={handleChange('create_live')} />
</Grid>
{/* Divider visual */}
<Grid item xs={12}>
<Typography variant="h4" style={{ marginTop: 8 }}>
<Trans>Create Live via Facebook API (optional)</Trans>
</Typography>
<Typography variant="caption" style={{ color: '#aaa' }}>
<Trans>Fill in the fields below to automatically create the live broadcast and get the stream key.</Trans>{' '}
<Link color="secondary" target="_blank" href="https://developers.facebook.com/tools/explorer/">
<Trans>Get your token here</Trans>
</Link>
</Typography>
</Grid>
{/* Account type selector */}
<Grid item xs={12}>
<Select
label={<Trans>Account type</Trans>}
value={settings.account_type}
onChange={handleChange('account_type')}
>
<MenuItem value="page">
Facebook Page (requires Page ID + Page Access Token)
</MenuItem>
<MenuItem value="user">
Personal profile (requires User Access Token with publish_video)
</MenuItem>
</Select>
</Grid>
{/* Page ID — solo visible cuando account_type = 'page' */}
{settings.account_type === 'page' && (
<Grid item xs={12} md={6}>
<TextField
variant="outlined"
fullWidth
label={<Trans>Facebook Page ID</Trans>}
placeholder="123456789012345"
helperText={
<Trans>
Find it at: facebook.com/YOUR_PAGE About Page transparency Page ID
</Trans>
}
value={settings.page_id}
onChange={handleChange('page_id')}
/>
</Grid>
)}
{/* Access Token */}
<Grid item xs={12} md={settings.account_type === 'page' ? 6 : 12}>
<TextField
variant="outlined"
fullWidth
label={
settings.account_type === 'page'
? <Trans>Page Access Token</Trans>
: <Trans>User Access Token</Trans>
}
placeholder="EAAxxxxxxx..."
helperText={
settings.account_type === 'page'
? <Trans>Must have publish_video permission. Get it from Graph API Explorer selecting your Page.</Trans>
: <Trans>User token with publish_video permission. Get it from Graph API Explorer.</Trans>
}
value={settings.page_access_token}
onChange={handleChange('page_access_token')}
/>
</Grid>
{/* Title */}
<Grid item xs={12}>
<TextField
variant="outlined"
fullWidth
label={<Trans>Stream title</Trans>}
placeholder={props.metadata && props.metadata.name ? props.metadata.name : ''}
value={settings.title}
onChange={handleChange('title')}
/>
</Grid>
{/* Description */}
<Grid item xs={12}>
<TextField
variant="outlined"
fullWidth
multiline
rows={3}
label={<Trans>Stream description</Trans>}
placeholder={props.metadata && props.metadata.description ? props.metadata.description : ''}
value={settings.description}
onChange={handleChange('description')}
/>
</Grid>
{/* Error message */}
{$apiError && (
<Grid item xs={12}>
<Typography variant="body2" style={{ color: '#f44336', whiteSpace: 'pre-wrap', fontSize: '0.8rem' }}>
{$apiError}
</Typography>
</Grid>
)}
{/* Success message */}
{$apiSuccess && (
<Grid item xs={12}>
<Typography variant="body2" style={{ color: '#4caf50', fontSize: '0.8rem' }}>
{$apiSuccess}
</Typography>
</Grid>
)}
{/* Create button */}
<Grid item xs={12}>
<FormInlineButton
onClick={createFacebookLive}
disabled={$loading || !settings.page_access_token}
>
{$loading ? <Trans>Creating...</Trans> : <Trans>Create Live &amp; get stream key</Trans>}
</FormInlineButton>
<Typography variant="caption" style={{ display: 'block', marginTop: 4, color: '#888' }}>
<Trans>
Required permissions: <strong>publish_video</strong>.
For pages, also: <strong>pages_manage_posts</strong>, <strong>pages_read_engagement</strong>.
</Trans>
</Typography>
</Grid>
</Grid>
);

View File

@ -47,6 +47,8 @@ function init(settings) {
key: '',
service_instafeed: false,
service_yellowduck: false,
title: '',
description: '',
...settings,
};
@ -56,6 +58,14 @@ function init(settings) {
function Service(props) {
const settings = init(props.settings);
// Pre-fill title/description from channel metadata if not already set
if (!settings.title && props.metadata && props.metadata.name) {
settings.title = props.metadata.name;
}
if (!settings.description && props.metadata && props.metadata.description) {
settings.description = props.metadata.description;
}
const handleChange = (what) => (event) => {
const value = event.target.value;
@ -85,6 +95,28 @@ function Service(props) {
<Trans>GET</Trans>
</FormInlineButton>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
fullWidth
label={<Trans>Stream title</Trans>}
placeholder={props.metadata && props.metadata.name ? props.metadata.name : ''}
value={settings.title}
onChange={handleChange('title')}
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
fullWidth
multiline
rows={3}
label={<Trans>Stream description</Trans>}
placeholder={props.metadata && props.metadata.description ? props.metadata.description : ''}
value={settings.description}
onChange={handleChange('description')}
/>
</Grid>
</Grid>
);
}

View File

@ -50,6 +50,8 @@ function init(settings) {
const initSettings = {
protocol: 'rtmp://',
address: '',
title: '',
description: '',
...settings,
};
@ -59,6 +61,14 @@ function init(settings) {
function Service(props) {
const settings = init(props.settings);
// Pre-fill title/description from channel metadata if not already set
if (!settings.title && props.metadata && props.metadata.name) {
settings.title = props.metadata.name;
}
if (!settings.description && props.metadata && props.metadata.description) {
settings.description = props.metadata.description;
}
const handleChange = (what) => (event) => {
const value = event.target.value;
@ -96,6 +106,28 @@ function Service(props) {
placeholder="{custom_id}.channel.media.azure.net:2935/live/{custom_id}"
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
fullWidth
label={<Trans>Stream title</Trans>}
placeholder={props.metadata && props.metadata.name ? props.metadata.name : ''}
value={settings.title}
onChange={handleChange('title')}
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
fullWidth
multiline
rows={3}
label={<Trans>Stream description</Trans>}
placeholder={props.metadata && props.metadata.description ? props.metadata.description : ''}
value={settings.description}
onChange={handleChange('description')}
/>
</Grid>
</Grid>
);
}

View File

@ -50,6 +50,8 @@ function init(settings) {
const initSettings = {
server_url: '',
stream_key: '',
title: '',
description: '',
...settings,
};
@ -59,6 +61,14 @@ function init(settings) {
function Service(props) {
const settings = init(props.settings);
// Pre-fill title/description from channel metadata if not already set
if (!settings.title && props.metadata && props.metadata.name) {
settings.title = props.metadata.name;
}
if (!settings.description && props.metadata && props.metadata.description) {
settings.description = props.metadata.description;
}
const handleChange = (what) => (event) => {
const value = event.target.value;
@ -107,6 +117,28 @@ function Service(props) {
<Trans>GET</Trans>
</FormInlineButton>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
fullWidth
label={<Trans>Stream title</Trans>}
placeholder={props.metadata && props.metadata.name ? props.metadata.name : ''}
value={settings.title}
onChange={handleChange('title')}
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
fullWidth
multiline
rows={3}
label={<Trans>Stream description</Trans>}
placeholder={props.metadata && props.metadata.description ? props.metadata.description : ''}
value={settings.description}
onChange={handleChange('description')}
/>
</Grid>
</Grid>
);
}

View File

@ -44,6 +44,8 @@ function init(settings) {
const initSettings = {
region: 'live',
key: '',
title: '',
description: '',
...settings,
};
@ -53,6 +55,14 @@ function init(settings) {
function Service(props) {
const settings = init(props.settings);
// Pre-fill title/description from channel metadata if not already set
if (!settings.title && props.metadata && props.metadata.name) {
settings.title = props.metadata.name;
}
if (!settings.description && props.metadata && props.metadata.description) {
settings.description = props.metadata.description;
}
const handleChange = (what) => (event) => {
const value = event.target.value;
@ -155,6 +165,28 @@ function Service(props) {
<Trans>GET</Trans>
</FormInlineButton>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
fullWidth
label={<Trans>Stream title</Trans>}
placeholder={props.metadata && props.metadata.name ? props.metadata.name : ''}
value={settings.title}
onChange={handleChange('title')}
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
fullWidth
multiline
rows={3}
label={<Trans>Stream description</Trans>}
placeholder={props.metadata && props.metadata.description ? props.metadata.description : ''}
value={settings.description}
onChange={handleChange('description')}
/>
</Grid>
</Grid>
);
}

View File

@ -66,6 +66,8 @@ function init(settings) {
mode: 'rtmps',
stream_key: '',
region: 'de',
title: '',
description: '',
...settings,
};
@ -75,6 +77,14 @@ function init(settings) {
function Service(props) {
const settings = init(props.settings);
// Pre-fill title/description from channel metadata if not already set
if (!settings.title && props.metadata && props.metadata.name) {
settings.title = props.metadata.name;
}
if (!settings.description && props.metadata && props.metadata.description) {
settings.description = props.metadata.description;
}
const handleChange = (what) => (event) => {
const value = event.target.value;
@ -179,6 +189,28 @@ function Service(props) {
<Trans>GET</Trans>
</FormInlineButton>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
fullWidth
label={<Trans>Stream title</Trans>}
placeholder={props.metadata && props.metadata.name ? props.metadata.name : ''}
value={settings.title}
onChange={handleChange('title')}
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
fullWidth
multiline
rows={3}
label={<Trans>Stream description</Trans>}
placeholder={props.metadata && props.metadata.description ? props.metadata.description : ''}
value={settings.description}
onChange={handleChange('description')}
/>
</Grid>
</Grid>
);
}

View File

@ -68,6 +68,8 @@ function init(settings) {
stream_key: '',
primary: true,
backup: false,
title: '',
description: '',
...settings,
};
@ -77,6 +79,14 @@ function init(settings) {
function Service(props) {
const settings = init(props.settings);
// Pre-fill title/description from channel metadata if not already set
if (!settings.title && props.metadata && props.metadata.name) {
settings.title = props.metadata.name;
}
if (!settings.description && props.metadata && props.metadata.description) {
settings.description = props.metadata.description;
}
const handleChange = (what) => (event) => {
const value = event.target.value;
@ -197,6 +207,28 @@ function Service(props) {
<Checkbox label={<Trans>Primary stream</Trans>} checked={settings.primary} onChange={handleChange('primary')} />
<Checkbox label={<Trans>Backup stream</Trans>} checked={settings.backup} onChange={handleChange('backup')} />
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
fullWidth
label={<Trans>Stream title</Trans>}
placeholder={props.metadata && props.metadata.name ? props.metadata.name : ''}
value={settings.title}
onChange={handleChange('title')}
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
fullWidth
multiline
rows={3}
label={<Trans>Stream description</Trans>}
placeholder={props.metadata && props.metadata.description ? props.metadata.description : ''}
value={settings.description}
onChange={handleChange('description')}
/>
</Grid>
</Grid>
);
}