restreamer-ui-v2/src/views/Main/Publication.js

280 lines
7.2 KiB
JavaScript

import React from 'react';
import { useNavigate } from 'react-router-dom';
import { Trans } from '@lingui/macro';
import makeStyles from '@mui/styles/makeStyles';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import Divider from '@mui/material/Divider';
import Grid from '@mui/material/Grid';
import PersonIcon from '@mui/icons-material/Person';
import Typography from '@mui/material/Typography';
import useInterval from '../../hooks/useInterval';
import Egress from './Egress';
import H from '../../utils/help';
import Number from '../../misc/Number';
import Paper from '../../misc/Paper';
import PaperHeader from '../../misc/PaperHeader';
import Services from '../Publication/Services';
import { refreshEgressStreamKey } from '../../utils/autoStreamKey';
import ytOAuth from '../../utils/ytOAuth';
// ─── helpers para extraer info de cuenta desde metadata ───────────────────────
function extractAccountInfo(service, meta) {
if (!meta) return null;
const s = (meta.settings) || {};
if (service === 'facebook') {
const hasToken = !!(s.page_access_token);
if (!hasToken) return null;
const isPage = s.account_type !== 'user';
// Nombre resuelto: page_name siempre tiene prioridad (viene de Graph API /me o /{page_id})
// Si no hay nombre aún, mostramos fallback descriptivo mientras carga
let label;
if (s.page_name) {
label = s.page_name;
} else if (isPage && s.page_id) {
label = `Page ${s.page_id}`;
} else {
label = 'Facebook account';
}
// Sublabel: tipo de cuenta + id si aplica
let sublabel = null;
if (isPage && s.page_id) {
sublabel = `Page · ID: ${s.page_id}`;
} else if (!isPage) {
sublabel = s.page_id ? `Personal · UID: ${s.page_id}` : 'Personal profile';
}
return {
label,
sublabel,
connected: true,
platform: 'facebook',
};
}
if (service === 'youtube') {
const accountKey = s.account_key;
if (!accountKey) return null;
const account = ytOAuth.getAccount(accountKey);
if (!account) return null;
const expired = account.token_expiry && Date.now() > account.token_expiry - 60000;
return {
label: account.channel_title || account.label || accountKey,
sublabel: account.channel_id || null,
connected: !!(account.access_token),
expired,
platform: 'youtube',
};
}
return null;
}
const useStyles = makeStyles((theme) => ({
viewerCount: {
fontSize: '3.5rem',
fontWeight: 600,
},
vierwerDescription: {
marginTop: '-1em',
},
vierwerTypo: {
fontSize: '1.1rem',
},
bandwidth: {
marginBottom: '.3em',
},
bandwidthCount: {
fontSize: '2.5rem',
fontWeight: 600,
},
bandwidthDescription: {
marginTop: '-.5em',
},
bandwidthIcon: {
fontSize: '1.7rem',
paddingRight: 7,
},
}));
export default function Publication(props) {
const classes = useStyles();
const navigate = useNavigate();
const services = Services.IDs();
const [$egresses, setEgresses] = React.useState([]);
const [$session, setSession] = React.useState({
viewer: 0,
bandwidth: 0,
});
useInterval(async () => {
await update();
}, 1000);
useInterval(async () => {
await sessions();
}, 1000);
React.useEffect(() => {
(async () => {
await update();
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const update = async () => {
const egresses = [];
const processes = await props.restreamer.ListIngestEgresses(props.channelid, services);
for (let p of processes) {
// Cargar metadata del egress para extraer info de cuenta conectada
let accountInfo = null;
if (p.service === 'facebook' || p.service === 'youtube') {
try {
const meta = await props.restreamer.GetEgressMetadata(props.channelid, p.id);
accountInfo = extractAccountInfo(p.service, meta);
} catch (e) {
// silencioso
}
}
egresses.push({
id: p.id,
name: p.name,
service: p.service,
index: p.index,
progress: p.progress,
accountInfo,
});
}
setEgresses(egresses);
};
const sessions = async () => {
const current = await props.restreamer.CurrentSessions(['ffmpeg', 'hls', 'rtmp', 'srt']);
setSession({
viewer: current.sessions,
bandwidth: current.bitrate_kbit,
});
};
const handleServiceAdd = (event) => {
event.preventDefault();
navigate(`/${props.channelid}/publication/`);
};
const handleServiceEdit = (service, index) => () => {
let target = `/${props.channelid}/publication/${service}`;
if (service !== 'player') {
target = target + '/' + index;
}
navigate(target);
};
const handleOrderChange = (id, service) => async (order) => {
let res = false;
if (order === 'start' || order === 'restart') {
// Auto-refresh stream key for YouTube (OAuth2) and Facebook (Graph API)
// before starting the process. Errors are non-fatal — we start anyway.
try {
await refreshEgressStreamKey(props.restreamer, props.channelid, id, service);
} catch (e) {
console.warn('[Publication] autoStreamKey error (non-fatal):', e.message);
}
}
if (order === 'start') {
res = await props.restreamer.StartEgress(props.channelid, id);
} else if (order === 'restart') {
res = await props.restreamer.StopEgress(props.channelid, id);
if (res === true) {
res = await props.restreamer.StartEgress(props.channelid, id);
}
} else if (order === 'stop') {
res = await props.restreamer.StopEgress(props.channelid, id);
}
return res;
};
const handleHelp = () => {
H('publication');
};
let egresses = [];
for (let e of $egresses.values()) {
egresses.push(
<React.Fragment key={e.id}>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Egress
service={e.service}
name={e.name}
state={e.progress.state}
order={e.progress.order}
reconnect={e.progress.reconnect !== -1}
accountInfo={e.accountInfo}
onEdit={handleServiceEdit(e.service, e.index)}
onOrder={handleOrderChange(e.id, e.service)}
/>
</Grid>
</React.Fragment>
);
}
return (
<React.Fragment>
<Paper marginBottom="0">
<PaperHeader title={<Trans>Publications</Trans>} onAdd={handleServiceAdd} onHelp={handleHelp} />
<Grid container spacing={1}>
<Grid item xs={12} align="center">
<Divider />
<Typography component="div" className={classes.viewerCount}>
<Number value={$session.viewer} />
</Typography>
<Grid container direction="row" justifyContent="center" alignItems="center" className={classes.vierwerDescription}>
<PersonIcon fontSize="small" />
<Typography className={classes.vierwerTypo}>
<Trans>Viewer</Trans>
</Typography>
</Grid>
</Grid>
<Grid item xs={12} align="center" className={classes.bandwidth}>
<Typography component="div" className={classes.bandwidthCount}>
<Number value={$session.bandwidth} />
</Typography>
<Grid container direction="row" justifyContent="center" alignItems="center" className={classes.bandwidthDescription}>
<CloudUploadIcon className={classes.bandwidthIcon} />
<Typography>
<Trans>kbit/s</Trans>
</Typography>
</Grid>
</Grid>
{egresses}
</Grid>
</Paper>
</React.Fragment>
);
}
Publication.defaultProps = {
channelid: '',
restreamer: null,
};