diff --git a/src/views/Edit/Profile.js b/src/views/Edit/Profile.js
index c5ee500..e614416 100644
--- a/src/views/Edit/Profile.js
+++ b/src/views/Edit/Profile.js
@@ -500,6 +500,7 @@ export default function Profile(props) {
onChange={handleSourceSettingsChange}
onRefresh={handleRefresh}
onStore={handleStore}
+ onYoutubeMetadata={props.onYoutubeMetadata}
/>
{$videoProbe.status !== 'none' && (
@@ -807,4 +808,5 @@ Profile.defaultProps = {
onStore: function (name, data) {
return '';
},
+ onYoutubeMetadata: function (title, description) {},
};
diff --git a/src/views/Edit/SourceSelect.js b/src/views/Edit/SourceSelect.js
index e9991ee..922bdfb 100644
--- a/src/views/Edit/SourceSelect.js
+++ b/src/views/Edit/SourceSelect.js
@@ -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) {
diff --git a/src/views/Edit/Sources/Network.js b/src/views/Edit/Sources/Network.js
index 283a3c7..ef66265 100644
--- a/src/views/Edit/Sources/Network.js
+++ b/src/views/Edit/Sources/Network.js
@@ -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) {
{settings.mode === 'pull' ? (
-
+
) : (
{
+ 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}
/>
)}
diff --git a/src/views/Publication/Services/DLive.js b/src/views/Publication/Services/DLive.js
index 4ae0af0..b8afbfb 100644
--- a/src/views/Publication/Services/DLive.js
+++ b/src/views/Publication/Services/DLive.js
@@ -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) {
GET
+
+ Stream title}
+ placeholder={props.metadata && props.metadata.name ? props.metadata.name : ''}
+ value={settings.title}
+ onChange={handleChange('title')}
+ />
+
+
+ Stream description}
+ placeholder={props.metadata && props.metadata.description ? props.metadata.description : ''}
+ value={settings.description}
+ onChange={handleChange('description')}
+ />
+
);
}
diff --git a/src/views/Publication/Services/Facebook.js b/src/views/Publication/Services/Facebook.js
index a6fb4b0..02109a2 100644
--- a/src/views/Publication/Services/Facebook.js
+++ b/src/views/Publication/Services/Facebook.js
@@ -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 = Live-Streaming to Facebook Live RTMP service;
const image_copyright = More about licenses here;
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 (
+ {/* Stream keys */}
{settings.rtmp_primary === true && (
-
- {/* Button to create FB Live and populate stream key */}
-
- Create
-
)}
{settings.rtmp_primary === true && (
@@ -201,27 +225,138 @@ function Service(props) {
)}
-
- {/* New fields for page id, token, title and description */}
-
- Facebook Page ID} value={settings.page_id} onChange={handleChange('page_id')} />
-
-
- Page Access Token} value={settings.page_access_token} onChange={handleChange('page_access_token')} />
-
-
-
- Title} value={settings.title} onChange={handleChange('title')} />
-
-
- Description} value={settings.description} onChange={handleChange('description')} />
-
-
Enable primary stream} checked={settings.rtmp_primary} onChange={handleChange('rtmp_primary')} />
Enable backup stream} checked={settings.rtmp_backup} onChange={handleChange('rtmp_backup')} />
- {/* Optionally mark that we created the live via API */}
- Create live via API} checked={settings.create_live} onChange={handleChange('create_live')} />
+
+
+ {/* Divider visual */}
+
+
+ Create Live via Facebook API (optional)
+
+
+ Fill in the fields below to automatically create the live broadcast and get the stream key.{' '}
+
+ Get your token here
+
+
+
+
+ {/* Account type selector */}
+
+
+
+
+ {/* Page ID — solo visible cuando account_type = 'page' */}
+ {settings.account_type === 'page' && (
+
+ Facebook Page ID}
+ placeholder="123456789012345"
+ helperText={
+
+ Find it at: facebook.com/YOUR_PAGE → About → Page transparency → Page ID
+
+ }
+ value={settings.page_id}
+ onChange={handleChange('page_id')}
+ />
+
+ )}
+
+ {/* Access Token */}
+
+ Page Access Token
+ : User Access Token
+ }
+ placeholder="EAAxxxxxxx..."
+ helperText={
+ settings.account_type === 'page'
+ ? Must have publish_video permission. Get it from Graph API Explorer selecting your Page.
+ : User token with publish_video permission. Get it from Graph API Explorer.
+ }
+ value={settings.page_access_token}
+ onChange={handleChange('page_access_token')}
+ />
+
+
+ {/* Title */}
+
+ Stream title}
+ placeholder={props.metadata && props.metadata.name ? props.metadata.name : ''}
+ value={settings.title}
+ onChange={handleChange('title')}
+ />
+
+
+ {/* Description */}
+
+ Stream description}
+ placeholder={props.metadata && props.metadata.description ? props.metadata.description : ''}
+ value={settings.description}
+ onChange={handleChange('description')}
+ />
+
+
+ {/* Error message */}
+ {$apiError && (
+
+
+ ⚠️ {$apiError}
+
+
+ )}
+
+ {/* Success message */}
+ {$apiSuccess && (
+
+
+ {$apiSuccess}
+
+
+ )}
+
+ {/* Create button */}
+
+
+ {$loading ? Creating... : Create Live & get stream key}
+
+
+
+ Required permissions: publish_video.
+ For pages, also: pages_manage_posts, pages_read_engagement.
+
+
);
diff --git a/src/views/Publication/Services/Instagram.js b/src/views/Publication/Services/Instagram.js
index 2eaee4a..4973c77 100644
--- a/src/views/Publication/Services/Instagram.js
+++ b/src/views/Publication/Services/Instagram.js
@@ -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) {
GET
+
+ Stream title}
+ placeholder={props.metadata && props.metadata.name ? props.metadata.name : ''}
+ value={settings.title}
+ onChange={handleChange('title')}
+ />
+
+
+ Stream description}
+ placeholder={props.metadata && props.metadata.description ? props.metadata.description : ''}
+ value={settings.description}
+ onChange={handleChange('description')}
+ />
+
);
}
diff --git a/src/views/Publication/Services/Linkedin.js b/src/views/Publication/Services/Linkedin.js
index ef51577..182b00b 100644
--- a/src/views/Publication/Services/Linkedin.js
+++ b/src/views/Publication/Services/Linkedin.js
@@ -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}"
/>
+
+ Stream title}
+ placeholder={props.metadata && props.metadata.name ? props.metadata.name : ''}
+ value={settings.title}
+ onChange={handleChange('title')}
+ />
+
+
+ Stream description}
+ placeholder={props.metadata && props.metadata.description ? props.metadata.description : ''}
+ value={settings.description}
+ onChange={handleChange('description')}
+ />
+
);
}
diff --git a/src/views/Publication/Services/Rumble.js b/src/views/Publication/Services/Rumble.js
index 7a1677b..1503ade 100644
--- a/src/views/Publication/Services/Rumble.js
+++ b/src/views/Publication/Services/Rumble.js
@@ -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) {
GET
+
+ Stream title}
+ placeholder={props.metadata && props.metadata.name ? props.metadata.name : ''}
+ value={settings.title}
+ onChange={handleChange('title')}
+ />
+
+
+ Stream description}
+ placeholder={props.metadata && props.metadata.description ? props.metadata.description : ''}
+ value={settings.description}
+ onChange={handleChange('description')}
+ />
+
);
}
diff --git a/src/views/Publication/Services/Twitch.js b/src/views/Publication/Services/Twitch.js
index fc97923..6d2dfd2 100644
--- a/src/views/Publication/Services/Twitch.js
+++ b/src/views/Publication/Services/Twitch.js
@@ -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) {
GET
+
+ Stream title}
+ placeholder={props.metadata && props.metadata.name ? props.metadata.name : ''}
+ value={settings.title}
+ onChange={handleChange('title')}
+ />
+
+
+ Stream description}
+ placeholder={props.metadata && props.metadata.description ? props.metadata.description : ''}
+ value={settings.description}
+ onChange={handleChange('description')}
+ />
+
);
}
diff --git a/src/views/Publication/Services/Twitter.js b/src/views/Publication/Services/Twitter.js
index 2798c4a..1b79fac 100644
--- a/src/views/Publication/Services/Twitter.js
+++ b/src/views/Publication/Services/Twitter.js
@@ -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) {
GET
+
+ Stream title}
+ placeholder={props.metadata && props.metadata.name ? props.metadata.name : ''}
+ value={settings.title}
+ onChange={handleChange('title')}
+ />
+
+
+ Stream description}
+ placeholder={props.metadata && props.metadata.description ? props.metadata.description : ''}
+ value={settings.description}
+ onChange={handleChange('description')}
+ />
+
);
}
diff --git a/src/views/Publication/Services/Youtube.js b/src/views/Publication/Services/Youtube.js
index 747f12c..49ad272 100644
--- a/src/views/Publication/Services/Youtube.js
+++ b/src/views/Publication/Services/Youtube.js
@@ -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) {
Primary stream} checked={settings.primary} onChange={handleChange('primary')} />
Backup stream} checked={settings.backup} onChange={handleChange('backup')} />
+
+ Stream title}
+ placeholder={props.metadata && props.metadata.name ? props.metadata.name : ''}
+ value={settings.title}
+ onChange={handleChange('title')}
+ />
+
+
+ Stream description}
+ placeholder={props.metadata && props.metadata.description ? props.metadata.description : ''}
+ value={settings.description}
+ onChange={handleChange('description')}
+ />
+
);
}