Add title and description fields to streaming services and pre-fill from metadata
This commit is contained in:
parent
9386273731
commit
2455251423
@ -500,6 +500,7 @@ export default function Profile(props) {
|
|||||||
onChange={handleSourceSettingsChange}
|
onChange={handleSourceSettingsChange}
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
onStore={handleStore}
|
onStore={handleStore}
|
||||||
|
onYoutubeMetadata={props.onYoutubeMetadata}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
{$videoProbe.status !== 'none' && (
|
{$videoProbe.status !== 'none' && (
|
||||||
@ -807,4 +808,5 @@ Profile.defaultProps = {
|
|||||||
onStore: function (name, data) {
|
onStore: function (name, data) {
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
|
onYoutubeMetadata: function (title, description) {},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -102,6 +102,7 @@ export default function SourceSelect(props) {
|
|||||||
onProbe={handleProbe}
|
onProbe={handleProbe}
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
onStore={handleStore}
|
onStore={handleStore}
|
||||||
|
onYoutubeMetadata={props.onYoutubeMetadata}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -135,6 +136,7 @@ SourceSelect.defaultProps = {
|
|||||||
onChange: function (type, device, settings) {},
|
onChange: function (type, device, settings) {},
|
||||||
onRefresh: function () {},
|
onRefresh: function () {},
|
||||||
onStore: function (name, data) {},
|
onStore: function (name, data) {},
|
||||||
|
onYoutubeMetadata: function (title, description) {},
|
||||||
};
|
};
|
||||||
|
|
||||||
function Select(props) {
|
function Select(props) {
|
||||||
|
|||||||
@ -814,6 +814,14 @@ function Pull(props) {
|
|||||||
if (data && data.stream_url) {
|
if (data && data.stream_url) {
|
||||||
props.onChange('', 'address')({ target: { value: data.stream_url } });
|
props.onChange('', 'address')({ target: { value: data.stream_url } });
|
||||||
setExtractorError('');
|
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 {
|
} else {
|
||||||
setExtractorError('No stream_url found in service response.');
|
setExtractorError('No stream_url found in service response.');
|
||||||
}
|
}
|
||||||
@ -1255,7 +1263,7 @@ function Source(props) {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
{settings.mode === 'pull' ? (
|
{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
|
<Push
|
||||||
settings={settings}
|
settings={settings}
|
||||||
@ -1278,6 +1286,7 @@ Source.defaultProps = {
|
|||||||
skills: null,
|
skills: null,
|
||||||
onChange: function (settings) {},
|
onChange: function (settings) {},
|
||||||
onProbe: function (settings, inputs) {},
|
onProbe: function (settings, inputs) {},
|
||||||
|
onYoutubeMetadata: function (title, description) {},
|
||||||
};
|
};
|
||||||
|
|
||||||
function SourceIcon(props) {
|
function SourceIcon(props) {
|
||||||
|
|||||||
@ -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) => {
|
const handleLicenseChange = (license) => {
|
||||||
setData({
|
setData({
|
||||||
...$data,
|
...$data,
|
||||||
@ -472,6 +486,7 @@ export default function Edit(props) {
|
|||||||
onDone={handleSourceDone}
|
onDone={handleSourceDone}
|
||||||
onAbort={handleSourceAbort}
|
onAbort={handleSourceAbort}
|
||||||
onStore={handleSourceStore}
|
onStore={handleSourceStore}
|
||||||
|
onYoutubeMetadata={handleYoutubeMetadata}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|||||||
@ -41,6 +41,8 @@ function ServiceIcon(props) {
|
|||||||
function init(settings) {
|
function init(settings) {
|
||||||
const initSettings = {
|
const initSettings = {
|
||||||
key: '',
|
key: '',
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
...settings,
|
...settings,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -50,6 +52,14 @@ function init(settings) {
|
|||||||
function Service(props) {
|
function Service(props) {
|
||||||
const settings = init(props.settings);
|
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 handleChange = (what) => (event) => {
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
|
|
||||||
@ -79,6 +89,28 @@ function Service(props) {
|
|||||||
<Trans>GET</Trans>
|
<Trans>GET</Trans>
|
||||||
</FormInlineButton>
|
</FormInlineButton>
|
||||||
</Grid>
|
</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>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,10 +4,14 @@ import { faFacebook } from '@fortawesome/free-brands-svg-icons';
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/macro';
|
||||||
import Grid from '@mui/material/Grid';
|
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 TextField from '@mui/material/TextField';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
import Checkbox from '../../../misc/Checkbox';
|
import Checkbox from '../../../misc/Checkbox';
|
||||||
import FormInlineButton from '../../../misc/FormInlineButton';
|
import FormInlineButton from '../../../misc/FormInlineButton';
|
||||||
|
import Select from '../../../misc/Select';
|
||||||
|
|
||||||
const id = 'facebook';
|
const id = 'facebook';
|
||||||
const name = 'Facebook Live';
|
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 description = <Trans>Live-Streaming to Facebook Live RTMP service</Trans>;
|
||||||
const image_copyright = <Trans>More about licenses here</Trans>;
|
const image_copyright = <Trans>More about licenses here</Trans>;
|
||||||
const author = {
|
const author = {
|
||||||
creator: {
|
creator: { name: 'datarhei', link: 'https://github.com/datarhei' },
|
||||||
name: 'datarhei',
|
maintainer: { name: 'datarhei', link: 'https://github.com/datarhei' },
|
||||||
link: 'https://github.com/datarhei',
|
|
||||||
},
|
|
||||||
maintainer: {
|
|
||||||
name: 'datarhei',
|
|
||||||
link: 'https://github.com/datarhei',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
const category = 'platform';
|
const category = 'platform';
|
||||||
const requires = {
|
const requires = {
|
||||||
@ -40,12 +38,13 @@ function ServiceIcon(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function init(settings) {
|
function init(settings) {
|
||||||
const initSettings = {
|
return {
|
||||||
stream_key_primary: '',
|
stream_key_primary: '',
|
||||||
stream_key_backup: '',
|
stream_key_backup: '',
|
||||||
rtmp_primary: true,
|
rtmp_primary: true,
|
||||||
rtmp_backup: false,
|
rtmp_backup: false,
|
||||||
// new fields
|
// API fields
|
||||||
|
account_type: 'page', // 'page' | 'user'
|
||||||
page_id: '',
|
page_id: '',
|
||||||
page_access_token: '',
|
page_access_token: '',
|
||||||
title: '',
|
title: '',
|
||||||
@ -53,15 +52,26 @@ function init(settings) {
|
|||||||
create_live: false,
|
create_live: false,
|
||||||
...settings,
|
...settings,
|
||||||
};
|
};
|
||||||
|
|
||||||
return initSettings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Service(props) {
|
function Service(props) {
|
||||||
const settings = init(props.settings);
|
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 handleChange = (what) => (event) => {
|
||||||
const value = event && event.target ? event.target.value : event;
|
const value = event && event.target ? event.target.value : event;
|
||||||
|
setApiError('');
|
||||||
|
setApiSuccess('');
|
||||||
|
|
||||||
if (['rtmp_primary', 'rtmp_backup', 'create_live'].includes(what)) {
|
if (['rtmp_primary', 'rtmp_backup', 'create_live'].includes(what)) {
|
||||||
settings[what] = !settings[what];
|
settings[what] = !settings[what];
|
||||||
@ -69,97 +79,116 @@ function Service(props) {
|
|||||||
settings[what] = value;
|
settings[what] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const output = createOutput(settings);
|
props.onChange(createOutput(settings), settings);
|
||||||
|
|
||||||
props.onChange(output, settings);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const createOutput = (settings) => {
|
const createOutput = (settings) => {
|
||||||
const outputs = [];
|
const outputs = [];
|
||||||
|
if (settings.stream_key_primary.length !== 0 && settings.rtmp_primary) {
|
||||||
const output_primary = {
|
outputs.push({
|
||||||
address: 'rtmps://live-api-s.facebook.com:443/rtmp/' + settings.stream_key_primary,
|
address: 'rtmps://live-api-s.facebook.com:443/rtmp/' + settings.stream_key_primary,
|
||||||
options: ['-f', 'flv'],
|
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_backup.length !== 0 && settings.rtmp_backup) {
|
||||||
if (settings.stream_key_backup.length !== 0) {
|
outputs.push({
|
||||||
if (settings.rtmp_backup) {
|
address: 'rtmps://live-api-s.facebook.com:443/rtmp/' + settings.stream_key_backup,
|
||||||
outputs.push(output_backup);
|
options: ['-f', 'flv'],
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return outputs;
|
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 () => {
|
const createFacebookLive = async () => {
|
||||||
if (!settings.page_id || !settings.page_access_token) {
|
setApiError('');
|
||||||
window.alert('Please provide Page ID and Page Access Token to create a Facebook Live.');
|
setApiSuccess('');
|
||||||
|
|
||||||
|
if (!settings.page_access_token) {
|
||||||
|
setApiError('You must provide a Page Access Token (or User Access Token for personal profile).');
|
||||||
return;
|
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 {
|
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();
|
const form = new URLSearchParams();
|
||||||
if (settings.title) form.append('title', settings.title);
|
if (settings.title) form.append('title', settings.title);
|
||||||
if (settings.description) form.append('description', settings.description);
|
if (settings.description) form.append('description', settings.description);
|
||||||
|
form.append('status', 'LIVE_NOW');
|
||||||
form.append('access_token', settings.page_access_token);
|
form.append('access_token', settings.page_access_token);
|
||||||
|
|
||||||
const resp = await fetch(url, {
|
const resp = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
},
|
|
||||||
body: form.toString(),
|
body: form.toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!resp.ok) {
|
|
||||||
const text = await resp.text();
|
|
||||||
throw new Error('Facebook API error: ' + text);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
|
|
||||||
// Facebook returns a stream_url or stream_url/secure_stream_url — extract key
|
if (!resp.ok || data.error) {
|
||||||
let stream = '';
|
const errMsg = data.error
|
||||||
if (data.stream_url) stream = data.stream_url;
|
? `[#${data.error.code}] ${data.error.message}`
|
||||||
else if (data.secure_stream_url) stream = data.secure_stream_url;
|
: `HTTP ${resp.status}`;
|
||||||
|
|
||||||
if (stream.length === 0) {
|
// Mensajes de error específicos con solución
|
||||||
window.alert('Facebook did not return a stream URL.');
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// stream like rtmps://live-api-s.facebook.com:443/rtmp/STREAM_KEY
|
// Extraer stream_url
|
||||||
const parts = stream.split('/');
|
const streamUrl = data.stream_url || data.secure_stream_url || '';
|
||||||
const key = parts[parts.length - 1] || '';
|
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.stream_key_primary = key;
|
||||||
settings.create_live = true;
|
settings.create_live = true;
|
||||||
|
|
||||||
const outputs = createOutput(settings);
|
props.onChange(createOutput(settings), settings);
|
||||||
props.onChange(outputs, settings);
|
setApiSuccess(`✅ Facebook Live created! Stream key filled automatically. Live ID: ${data.id}`);
|
||||||
|
|
||||||
window.alert('Facebook Live creado y stream key rellenada.');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
setApiError('Network error: ' + err.message + '. Check if CORS is enabled or try from a server-side proxy.');
|
||||||
window.alert('Error creando Facebook Live: ' + err.message);
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
|
{/* Stream keys */}
|
||||||
{settings.rtmp_primary === true && (
|
{settings.rtmp_primary === true && (
|
||||||
<Grid item xs={12} md={9}>
|
<Grid item xs={12} md={9}>
|
||||||
<TextField
|
<TextField
|
||||||
@ -169,11 +198,6 @@ function Service(props) {
|
|||||||
value={settings.stream_key_primary}
|
value={settings.stream_key_primary}
|
||||||
onChange={handleChange('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>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
{settings.rtmp_primary === true && (
|
{settings.rtmp_primary === true && (
|
||||||
@ -201,27 +225,138 @@ function Service(props) {
|
|||||||
</FormInlineButton>
|
</FormInlineButton>
|
||||||
</Grid>
|
</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}>
|
<Grid item xs={12}>
|
||||||
<Checkbox label={<Trans>Enable primary stream</Trans>} checked={settings.rtmp_primary} onChange={handleChange('rtmp_primary')} />
|
<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')} />
|
<Checkbox label={<Trans>Enable backup stream</Trans>} checked={settings.rtmp_backup} onChange={handleChange('rtmp_backup')} />
|
||||||
{/* Optionally mark that we created the live via API */}
|
</Grid>
|
||||||
<Checkbox label={<Trans>Create live via API</Trans>} checked={settings.create_live} onChange={handleChange('create_live')} />
|
|
||||||
|
{/* 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 & 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>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -47,6 +47,8 @@ function init(settings) {
|
|||||||
key: '',
|
key: '',
|
||||||
service_instafeed: false,
|
service_instafeed: false,
|
||||||
service_yellowduck: false,
|
service_yellowduck: false,
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
...settings,
|
...settings,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -56,6 +58,14 @@ function init(settings) {
|
|||||||
function Service(props) {
|
function Service(props) {
|
||||||
const settings = init(props.settings);
|
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 handleChange = (what) => (event) => {
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
|
|
||||||
@ -85,6 +95,28 @@ function Service(props) {
|
|||||||
<Trans>GET</Trans>
|
<Trans>GET</Trans>
|
||||||
</FormInlineButton>
|
</FormInlineButton>
|
||||||
</Grid>
|
</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>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,6 +50,8 @@ function init(settings) {
|
|||||||
const initSettings = {
|
const initSettings = {
|
||||||
protocol: 'rtmp://',
|
protocol: 'rtmp://',
|
||||||
address: '',
|
address: '',
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
...settings,
|
...settings,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -59,6 +61,14 @@ function init(settings) {
|
|||||||
function Service(props) {
|
function Service(props) {
|
||||||
const settings = init(props.settings);
|
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 handleChange = (what) => (event) => {
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
|
|
||||||
@ -96,6 +106,28 @@ function Service(props) {
|
|||||||
placeholder="{custom_id}.channel.media.azure.net:2935/live/{custom_id}"
|
placeholder="{custom_id}.channel.media.azure.net:2935/live/{custom_id}"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</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>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,6 +50,8 @@ function init(settings) {
|
|||||||
const initSettings = {
|
const initSettings = {
|
||||||
server_url: '',
|
server_url: '',
|
||||||
stream_key: '',
|
stream_key: '',
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
...settings,
|
...settings,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -59,6 +61,14 @@ function init(settings) {
|
|||||||
function Service(props) {
|
function Service(props) {
|
||||||
const settings = init(props.settings);
|
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 handleChange = (what) => (event) => {
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
|
|
||||||
@ -107,6 +117,28 @@ function Service(props) {
|
|||||||
<Trans>GET</Trans>
|
<Trans>GET</Trans>
|
||||||
</FormInlineButton>
|
</FormInlineButton>
|
||||||
</Grid>
|
</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>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,6 +44,8 @@ function init(settings) {
|
|||||||
const initSettings = {
|
const initSettings = {
|
||||||
region: 'live',
|
region: 'live',
|
||||||
key: '',
|
key: '',
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
...settings,
|
...settings,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -53,6 +55,14 @@ function init(settings) {
|
|||||||
function Service(props) {
|
function Service(props) {
|
||||||
const settings = init(props.settings);
|
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 handleChange = (what) => (event) => {
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
|
|
||||||
@ -155,6 +165,28 @@ function Service(props) {
|
|||||||
<Trans>GET</Trans>
|
<Trans>GET</Trans>
|
||||||
</FormInlineButton>
|
</FormInlineButton>
|
||||||
</Grid>
|
</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>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,6 +66,8 @@ function init(settings) {
|
|||||||
mode: 'rtmps',
|
mode: 'rtmps',
|
||||||
stream_key: '',
|
stream_key: '',
|
||||||
region: 'de',
|
region: 'de',
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
...settings,
|
...settings,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -75,6 +77,14 @@ function init(settings) {
|
|||||||
function Service(props) {
|
function Service(props) {
|
||||||
const settings = init(props.settings);
|
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 handleChange = (what) => (event) => {
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
|
|
||||||
@ -179,6 +189,28 @@ function Service(props) {
|
|||||||
<Trans>GET</Trans>
|
<Trans>GET</Trans>
|
||||||
</FormInlineButton>
|
</FormInlineButton>
|
||||||
</Grid>
|
</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>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,6 +68,8 @@ function init(settings) {
|
|||||||
stream_key: '',
|
stream_key: '',
|
||||||
primary: true,
|
primary: true,
|
||||||
backup: false,
|
backup: false,
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
...settings,
|
...settings,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -77,6 +79,14 @@ function init(settings) {
|
|||||||
function Service(props) {
|
function Service(props) {
|
||||||
const settings = init(props.settings);
|
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 handleChange = (what) => (event) => {
|
||||||
const value = event.target.value;
|
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>Primary stream</Trans>} checked={settings.primary} onChange={handleChange('primary')} />
|
||||||
<Checkbox label={<Trans>Backup stream</Trans>} checked={settings.backup} onChange={handleChange('backup')} />
|
<Checkbox label={<Trans>Backup stream</Trans>} checked={settings.backup} onChange={handleChange('backup')} />
|
||||||
</Grid>
|
</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>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user