livestream new api

Re-used lots of Anthony's code + made fixes to a few areas including the publish page.

a

new videojs

video.js@7.18.1 + http-streaming@2.14.2

remove console log
This commit is contained in:
Thomas Zarebczan 2022-04-20 15:48:45 -04:00 committed by Thomas Zarebczan
parent 575b8f5aac
commit 8144a9bb87
14 changed files with 189 additions and 314 deletions

View file

@ -251,6 +251,9 @@
"node": ">=7", "node": ">=7",
"yarn": "^1.3" "yarn": "^1.3"
}, },
"resolutions": {
"@videojs/http-streaming": "2.14.2"
},
"lbrySettings": { "lbrySettings": {
"lbrynetDaemonVersion": "0.99.0", "lbrynetDaemonVersion": "0.99.0",
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip", "lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",

View file

@ -1730,7 +1730,7 @@
"Select advanced mode from the dropdown at the top.": "Select advanced mode from the dropdown at the top.", "Select advanced mode from the dropdown at the top.": "Select advanced mode from the dropdown at the top.",
"Ensure the following settings are selected under the streaming tab:": "Ensure the following settings are selected under the streaming tab:", "Ensure the following settings are selected under the streaming tab:": "Ensure the following settings are selected under the streaming tab:",
"Bitrate: 1000 to 2500 kbps": "Bitrate: 1000 to 2500 kbps", "Bitrate: 1000 to 2500 kbps": "Bitrate: 1000 to 2500 kbps",
"Keyframes: 1": "Keyframes: 1", "Keyframes: 2": "Keyframes: 2",
"Profile: High": "Profile: High", "Profile: High": "Profile: High",
"Tune: Zerolatency": "Tune: Zerolatency", "Tune: Zerolatency": "Tune: Zerolatency",
"If using other streaming software, make sure the bitrate is below 4500 kbps or the stream will not work.": "If using other streaming software, make sure the bitrate is below 4500 kbps or the stream will not work.", "If using other streaming software, make sure the bitrate is below 4500 kbps or the stream will not work.": "If using other streaming software, make sure the bitrate is below 4500 kbps or the stream will not work.",

View file

@ -1,14 +1,19 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectActiveChannelClaim } from 'redux/selectors/app'; import { selectClaimIsMineForUri, selectClaimForUri } from 'redux/selectors/claims';
import { selectClaimIsMineForUri } from 'redux/selectors/claims';
import { doToast } from 'redux/actions/notifications'; import { doToast } from 'redux/actions/notifications';
import ClaimPreviewReset from './view'; import ClaimPreviewReset from './view';
import { selectActiveLivestreamForChannel } from 'redux/selectors/livestream';
import { getChannelIdFromClaim, getChannelNameFromClaim } from 'util/claim';
const select = (state, props) => { const select = (state, props) => {
const { claim_id: channelId, name: channelName } = selectActiveChannelClaim(state) || {}; const { uri } = props;
const claim = selectClaimForUri(state, uri);
const channelId = getChannelIdFromClaim(claim);
const channelName = getChannelNameFromClaim(claim);
return { return {
channelName, activeLivestreamForChannel: selectActiveLivestreamForChannel(state, channelId),
channelId, channelId,
channelName,
claimIsMine: props.uri && selectClaimIsMineForUri(state, props.uri), claimIsMine: props.uri && selectClaimIsMineForUri(state, props.uri),
}; };
}; };

View file

@ -3,8 +3,7 @@
import React from 'react'; import React from 'react';
import { SITE_HELP_EMAIL } from 'config'; import { SITE_HELP_EMAIL } from 'config';
import Button from 'component/button'; import Button from 'component/button';
import { killStream } from '$web/src/livestreaming'; import { killStream } from 'util/livestream';
import watchLivestreamStatus from '$web/src/livestreaming/long-polling';
import 'scss/component/claim-preview-reset.scss'; import 'scss/component/claim-preview-reset.scss';
type Props = { type Props = {
@ -12,19 +11,12 @@ type Props = {
channelName: string, channelName: string,
claimIsMine: boolean, claimIsMine: boolean,
doToast: ({ message: string, isError?: boolean }) => void, doToast: ({ message: string, isError?: boolean }) => void,
activeLivestreamForChannel: any,
}; };
const ClaimPreviewReset = (props: Props) => { const ClaimPreviewReset = (props: Props) => {
const { channelId, channelName, claimIsMine, doToast } = props; const { channelId, channelName, claimIsMine, doToast, activeLivestreamForChannel } = props;
if (!claimIsMine || !activeLivestreamForChannel) return null;
const [isLivestreaming, setIsLivestreaming] = React.useState(false);
React.useEffect(() => {
if (!claimIsMine) return;
return watchLivestreamStatus(channelId, (state) => setIsLivestreaming(state));
}, [channelId, setIsLivestreaming, claimIsMine]);
if (!claimIsMine || !isLivestreaming) return null;
const handleClick = async () => { const handleClick = async () => {
try { try {

View file

@ -85,7 +85,7 @@ export default function LivestreamLayout(props: Props) {
)} )}
{!activeStreamUri && !showScheduledInfo && !isCurrentClaimLive && ( {!activeStreamUri && !showScheduledInfo && !isCurrentClaimLive && (
<div className="help--notice"> <div className="help--notice" style={{ marginTop: '20px' }}>
{channelName {channelName
? __("%channelName% isn't live right now, but the chat is! Check back later to watch the stream.", { ? __("%channelName% isn't live right now, but the chat is! Check back later to watch the stream.", {
channelName, channelName,

View file

@ -47,9 +47,9 @@ type Props = {
header: Node, header: Node,
livestreamData: LivestreamReplayData, livestreamData: LivestreamReplayData,
isLivestreamClaim: boolean, isLivestreamClaim: boolean,
checkLivestreams: (string, ?string, ?string) => void, checkLivestreams: (string, string) => void,
channelName: string,
channelId: string, channelId: string,
channelSignature: { signature?: string, signing_ts?: string },
isCheckingLivestreams: boolean, isCheckingLivestreams: boolean,
setWaitForFile: (boolean) => void, setWaitForFile: (boolean) => void,
setOverMaxBitrate: (boolean) => void, setOverMaxBitrate: (boolean) => void,
@ -86,7 +86,7 @@ function PublishFile(props: Props) {
subtitle, subtitle,
checkLivestreams, checkLivestreams,
channelId, channelId,
channelSignature, channelName,
isCheckingLivestreams, isCheckingLivestreams,
setWaitForFile, setWaitForFile,
setOverMaxBitrate, setOverMaxBitrate,
@ -175,9 +175,9 @@ function PublishFile(props: Props) {
} else { } else {
if (url.startsWith('http://')) { if (url.startsWith('http://')) {
return url; return url;
} else { } else if (url) {
return `https://${url}`; return `https://${url}`;
} } else return __('Click Check for Replays to update...');
} }
}; };
// update remoteUrl when replay selected // update remoteUrl when replay selected
@ -550,9 +550,7 @@ function PublishFile(props: Props) {
label={__('Check for Replays')} label={__('Check for Replays')}
disabled={isCheckingLivestreams} disabled={isCheckingLivestreams}
icon={ICONS.REFRESH} icon={ICONS.REFRESH}
onClick={() => onClick={() => checkLivestreams(channelId, channelName)}
checkLivestreams(channelId, channelSignature.signature, channelSignature.signing_ts)
}
/> />
)} )}
</div> </div>
@ -605,7 +603,9 @@ function PublishFile(props: Props) {
</div> </div>
</td> </td>
<td> <td>
{`${Math.floor(item.data.fileDuration / 60)} ${ {item.data.fileDuration && isNaN(item.data.fileDuration)
? item.data.fileDuration
: `${Math.floor(item.data.fileDuration / 60)} ${
Math.floor(item.data.fileDuration / 60) > 1 ? __('minutes') : __('minute') Math.floor(item.data.fileDuration / 60) > 1 ? __('minutes') : __('minute')
}`} }`}
<div className="table__item-label"> <div className="table__item-label">

View file

@ -30,7 +30,7 @@ import * as PUBLISH_MODES from 'constants/publish_types';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import Spinner from 'component/spinner'; import Spinner from 'component/spinner';
import { toHex } from 'util/hex'; import { toHex } from 'util/hex';
import { LIVESTREAM_REPLAY_API } from 'constants/livestream'; import { LIVESTREAM_REPLAY_API, NEW_LIVESTREAM_REPLAY_API } from 'constants/livestream';
import PublishStreamReleaseDate from 'component/publishStreamReleaseDate'; import PublishStreamReleaseDate from 'component/publishStreamReleaseDate';
import { SOURCE_NONE } from 'constants/publish_sources'; import { SOURCE_NONE } from 'constants/publish_sources';
@ -208,15 +208,15 @@ function PublishForm(props: Props) {
const [waitForFile, setWaitForFile] = useState(false); const [waitForFile, setWaitForFile] = useState(false);
const [overMaxBitrate, setOverMaxBitrate] = useState(false); const [overMaxBitrate, setOverMaxBitrate] = useState(false);
const [livestreamData, setLivestreamData] = React.useState([]); const [livestreamData, setLivestreamData] = React.useState([]);
const [signedMessage, setSignedMessage] = React.useState({ signature: undefined, signing_ts: undefined });
const signedMessageStr = JSON.stringify(signedMessage);
const TAGS_LIMIT = 5; const TAGS_LIMIT = 5;
const fileFormDisabled = mode === PUBLISH_MODES.FILE && !filePath && !remoteUrl; const fileFormDisabled = mode === PUBLISH_MODES.FILE && !filePath && !remoteUrl;
const emptyPostError = mode === PUBLISH_MODES.POST && (!fileText || fileText.trim() === ''); const emptyPostError = mode === PUBLISH_MODES.POST && (!fileText || fileText.trim() === '');
const formDisabled = (fileFormDisabled && !editingURI) || emptyPostError || publishing; const formDisabled = (fileFormDisabled && !editingURI) || emptyPostError || publishing;
const isInProgress = filePath || editingURI || name || title; const isInProgress = filePath || editingURI || name || title;
const activeChannelName = activeChannelClaim && activeChannelClaim.name; const activeChannelName =
const activeChannelClaimStr = activeChannelClaim && JSON.stringify(activeChannelClaim); (myClaimForUri && myClaimForUri.signing_channel && myClaimForUri.signing_channel.name) ||
(activeChannelClaim && activeChannelClaim.name);
// Editing content info // Editing content info
const fileMimeType = const fileMimeType =
myClaimForUri && myClaimForUri.value && myClaimForUri.value.source myClaimForUri && myClaimForUri.value && myClaimForUri.value.source
@ -253,26 +253,11 @@ function PublishForm(props: Props) {
const [previewing, setPreviewing] = React.useState(false); const [previewing, setPreviewing] = React.useState(false);
React.useEffect(() => { useEffect(() => {
if (activeChannelClaimStr) { if (claimChannelId) {
const channelClaim = JSON.parse(activeChannelClaimStr); fetchLivestreams(claimChannelId, activeChannelName);
const message = 'get-claim-id-replays';
setSignedMessage({ signature: null, signing_ts: null });
// ensure we have a channel
if (channelClaim.claim_id) {
Lbry.channel_sign({
channel_id: channelClaim.claim_id,
hexdata: toHex(message),
})
.then((data) => {
setSignedMessage(data);
})
.catch((error) => {
setSignedMessage({ signature: null, signing_ts: null });
});
} }
} }, [claimChannelId]);
}, [activeChannelClaimStr, setSignedMessage]);
useEffect(() => { useEffect(() => {
if (!hasClaimedInitialRewards) { if (!hasClaimedInitialRewards) {
@ -289,29 +274,75 @@ function PublishForm(props: Props) {
}, [modal]); }, [modal]);
// move this to lbryinc OR to a file under ui, and/or provide a standardized livestreaming config. // move this to lbryinc OR to a file under ui, and/or provide a standardized livestreaming config.
function fetchLivestreams(channelId, signature, timestamp) { async function fetchLivestreams(channelId, channelName) {
setCheckingLivestreams(true); setCheckingLivestreams(true);
fetch(`${LIVESTREAM_REPLAY_API}/${channelId}?signature=${signature || ''}&signing_ts=${timestamp || ''}`) // claimChannelId let signedMessage;
.then((res) => res.json()) try {
.then((res) => { await Lbry.channel_sign({
if (!res || !res.data) { channel_id: channelId,
setLivestreamData([]); hexdata: toHex(channelName || ''),
} }).then((data) => {
setLivestreamData(res.data); signedMessage = data;
setCheckingLivestreams(false);
})
.catch((e) => {
setLivestreamData([]);
setCheckingLivestreams(false);
}); });
} catch (e) {
throw e;
}
if (signedMessage) {
const newEndpointUrl =
`${NEW_LIVESTREAM_REPLAY_API}?channel_claim_id=${channelId}` +
`&signature=${signedMessage.signature}&signature_ts=${signedMessage.signing_ts}&channel_name=${
channelName || ''
}`;
const responseFromNewApi = await fetch(newEndpointUrl);
const data = (await responseFromNewApi.json()).data;
let newData = [];
if (data && data.length > 0) {
for (const dataItem of data) {
if (dataItem.Status.toLowerCase() === 'inprogress' || dataItem.Status.toLowerCase() === 'ready') {
const objectToPush = {
data: {
fileLocation: dataItem.URL,
fileDuration:
dataItem.Status.toLowerCase() === 'inprogress'
? __('Processing...(') + dataItem.PercentComplete + '%)'
: (dataItem.Duration / 1000000000).toString(),
thumbnails: dataItem.ThumbnailURLs !== null ? dataItem.ThumbnailURLs : [],
uploadedAt: dataItem.Created,
},
};
newData.push(objectToPush);
}
}
} }
useEffect(() => { const responseFromOldApi = await fetch(
const signedMessage = JSON.parse(signedMessageStr); `${LIVESTREAM_REPLAY_API}/${channelId}?signature=${signedMessage.signature || ''}&signing_ts=${
if (claimChannelId && signedMessage.signature) { signedMessage.signing_ts || ''
fetchLivestreams(claimChannelId, signedMessage.signature, signedMessage.signing_ts); }`
);
const oldData = (await responseFromOldApi.json()).data;
// TODO: this code could still use some attention, it just chops off oldapi replays, and keeps new ones
const amountOfUploadsToRemove = newData.length;
let dataToSend = [];
if (amountOfUploadsToRemove > 0) {
// TODO: use a pure functional method instead
oldData.splice(0, amountOfUploadsToRemove);
dataToSend = newData.concat(oldData);
} else if (oldData) {
dataToSend = oldData;
} else {
dataToSend = newData;
}
setLivestreamData(dataToSend);
setCheckingLivestreams(false);
}
} }
}, [claimChannelId, signedMessageStr]);
const isLivestreamMode = mode === PUBLISH_MODES.LIVESTREAM; const isLivestreamMode = mode === PUBLISH_MODES.LIVESTREAM;
let submitLabel; let submitLabel;
@ -600,7 +631,7 @@ function PublishForm(props: Props) {
isCheckingLivestreams={isCheckingLivestreams} isCheckingLivestreams={isCheckingLivestreams}
checkLivestreams={fetchLivestreams} checkLivestreams={fetchLivestreams}
channelId={claimChannelId} channelId={claimChannelId}
channelSignature={signedMessage} channelName={activeChannelName}
header={ header={
<> <>
{AVAILABLE_MODES.map((modeName) => ( {AVAILABLE_MODES.map((modeName) => (
@ -627,7 +658,7 @@ function PublishForm(props: Props) {
{mode !== PUBLISH_MODES.POST && <PublishDescription disabled={formDisabled} />} {mode !== PUBLISH_MODES.POST && <PublishDescription disabled={formDisabled} />}
<Card actions={<SelectThumbnail livestreamdData={livestreamData} />} /> <Card actions={<SelectThumbnail livestreamData={livestreamData} />} />
<label style={{ marginTop: 'var(--spacing-l)' }}>{__('Tags')}</label> <label style={{ marginTop: 'var(--spacing-l)' }}>{__('Tags')}</label>
<TagsSelect <TagsSelect

View file

@ -215,10 +215,9 @@ export default React.memo<Props>(function VideoJs(props: Props) {
html5: { html5: {
vhs: { vhs: {
overrideNative: !videojs.browser.IS_ANY_SAFARI, overrideNative: !videojs.browser.IS_ANY_SAFARI,
allowSeeksWithinUnsafeLiveWindow: true,
enableLowInitialPlaylist: false, enableLowInitialPlaylist: false,
handlePartialData: true,
fastQualityChange: true, fastQualityChange: true,
useDtsForTimestampOffset: true,
}, },
}, },
liveTracker: { liveTracker: {
@ -233,7 +232,6 @@ export default React.memo<Props>(function VideoJs(props: Props) {
// fixes problem of errant CC button showing up on iOS // fixes problem of errant CC button showing up on iOS
// the true fix here is to fix the m3u8 file, see: https://github.com/lbryio/lbry-desktop/pull/6315 // the true fix here is to fix the m3u8 file, see: https://github.com/lbryio/lbry-desktop/pull/6315
controlBar: { controlBar: {
subsCapsButton: false,
currentTimeDisplay: !isLivestreamClaim, currentTimeDisplay: !isLivestreamClaim,
timeDivider: !isLivestreamClaim, timeDivider: !isLivestreamClaim,
durationDisplay: !isLivestreamClaim, durationDisplay: !isLivestreamClaim,

View file

@ -6,7 +6,7 @@ export const LIVESTREAM_EMBED_URL = 'https://player.odysee.live/odysee';
export const LIVESTREAM_LIVE_API = 'https://api.live.odysee.com/v1/odysee/live'; export const LIVESTREAM_LIVE_API = 'https://api.live.odysee.com/v1/odysee/live';
export const LIVESTREAM_REPLAY_API = 'https://api.live.odysee.com/v1/replays/odysee'; export const LIVESTREAM_REPLAY_API = 'https://api.live.odysee.com/v1/replays/odysee';
export const LIVESTREAM_RTMP_URL = 'rtmp://stream.odysee.com/live'; export const LIVESTREAM_RTMP_URL = 'rtmp://stream.odysee.com/live';
export const LIVESTREAM_KILL = 'https://api.stream.odysee.com/stream/kill'; export const LIVESTREAM_KILL = 'https://api.odysee.live/streams/kill?app=live&';
// new livestream endpoints (old can be removed at some future point) // new livestream endpoints (old can be removed at some future point)
export const NEW_LIVESTREAM_RTMP_URL = 'rtmp://publish.odysee.live/live'; export const NEW_LIVESTREAM_RTMP_URL = 'rtmp://publish.odysee.live/live';

View file

@ -84,7 +84,7 @@ export default function LivestreamSetupPage(props: Props) {
<p>{__(`Ensure the following settings are selected under the streaming tab:`)}</p> <p>{__(`Ensure the following settings are selected under the streaming tab:`)}</p>
<ul> <ul>
<li>{__(`Bitrate: 1000 to 2500 kbps`)}</li> <li>{__(`Bitrate: 1000 to 2500 kbps`)}</li>
<li>{__(`Keyframes: 1`)}</li> <li>{__(`Keyframes: 2`)}</li>
<li>{__(`Profile: High`)}</li> <li>{__(`Profile: High`)}</li>
<li>{__(`Tune: Zerolatency`)}</li> <li>{__(`Tune: Zerolatency`)}</li>
</ul> </ul>

View file

@ -96,13 +96,33 @@ const transformLivestreamData = (data: Array<any>): LivestreamInfo => {
}, {}); }, {});
}; };
const transformNewLivestreamData = (data: Array<any>): LivestreamInfo => {
return data.reduce((acc, curr) => {
acc[curr.ChannelClaimID] = {
url: curr.VideoURL,
type: 'application/x-mpegurl',
live: curr.Live,
viewCount: curr.ViewerCount,
creatorId: curr.ChannelClaimID,
startedStreaming: moment(curr.Start),
};
return acc;
}, {});
};
export const fetchLiveChannels = async (): Promise<LivestreamInfo> => { export const fetchLiveChannels = async (): Promise<LivestreamInfo> => {
const response = await fetch(LIVESTREAM_LIVE_API); const newApiResponse = await fetch(`${NEW_LIVESTREAM_LIVE_API}/all`);
const json = await response.json(); const newApiData = (await newApiResponse.json()).data;
if (!newApiData) throw new Error();
const newTranslatedData = transformNewLivestreamData(newApiData);
if (!json.data) throw new Error(); const oldApiResponse = await fetch(`${LIVESTREAM_LIVE_API}`);
const oldApiData = (await oldApiResponse.json()).data;
if (!oldApiData) throw new Error();
const oldTranslatedData = transformLivestreamData(oldApiData);
const mergedData = { ...oldTranslatedData, ...newTranslatedData };
return transformLivestreamData(json.data); return mergedData;
}; };
/** /**
@ -116,35 +136,26 @@ export const fetchLiveChannel = async (channelId: string): Promise<LiveChannelSt
const newApiResponse = await fetch(`${newApiEndpoint}/is_live?channel_claim_id=${channelId}`); const newApiResponse = await fetch(`${newApiEndpoint}/is_live?channel_claim_id=${channelId}`);
const newApiData = (await newApiResponse.json()).data; const newApiData = (await newApiResponse.json()).data;
let isLive = newApiData.Live; let isLive = newApiData.Live;
let translatedData = []; let translatedData;
// transform data to old API standard // transform data to old API standard
if (isLive) { if (isLive) {
translatedData = { translatedData = transformNewLivestreamData([newApiData]);
url: newApiData.VideoURL,
type: 'application/x-mpegurl',
viewCount: newApiData.ViewerCount,
claimId: newApiData.ChannelClaimID,
timestamp: newApiData.Start,
};
} else { } else {
const oldApiResponse = await fetch(`${oldApiEndpoint}/${channelId}`); const oldApiResponse = await fetch(`${oldApiEndpoint}/${channelId}`);
const oldApiData = (await oldApiResponse.json()).data; const oldApiData = (await oldApiResponse.json()).data;
isLive = oldApiData.live; isLive = oldApiData.live;
translatedData = { translatedData = transformLivestreamData([oldApiData]);
url: oldApiData.url,
type: 'application/x-mpegurl',
viewCount: oldApiData.viewCount,
claimId: oldApiData.claimId,
timestamp: oldApiData.timestamp,
};
} }
try { try {
if (isLive === false) { if (isLive === false) {
return { channelStatus: LiveStatus.NOT_LIVE }; return { channelStatus: LiveStatus.NOT_LIVE };
} }
return { channelStatus: LiveStatus.LIVE, channelData: transformLivestreamData([translatedData]) }; return {
channelStatus: LiveStatus.LIVE,
channelData: translatedData,
};
} catch { } catch {
return { channelStatus: LiveStatus.UNKNOWN }; return { channelStatus: LiveStatus.UNKNOWN };
} }
@ -172,14 +183,13 @@ export const killStream = async (channelId: string, channelName: string) => {
try { try {
const streamData = await getStreamData(channelId, channelName); const streamData = await getStreamData(channelId, channelName);
fetch(`${LIVESTREAM_KILL}/${channelId}`, { const apiData = await fetch(
method: 'POST', `${LIVESTREAM_KILL}channel_claim_id=${channelId}&channel_name=${channelName}&signature_ts=${streamData.t}&signature=${streamData.s}`
mode: 'no-cors', );
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(streamData), const data = (await apiData.json()).data;
}).then((res) => {
if (res.status !== 200) throw new Error('Kill stream API failed.'); if (!data) throw new Error('Kill stream API failed.');
});
} catch (e) { } catch (e) {
throw e; throw e;
} }

View file

@ -1,55 +0,0 @@
// @flow
import Lbry from 'lbry';
import { LIVESTREAM_KILL, LIVESTREAM_LIVE_API } from 'constants/livestream';
import { toHex } from 'util/hex';
type StreamData = {
d: string,
s: string,
t: string,
};
export const getStreamData = async (channelId: string, channelName: string): Promise<StreamData> => {
if (!channelId || !channelName) throw new Error('Invalid channel data provided.');
const channelNameHex = toHex(channelName);
let channelSignature;
try {
channelSignature = await Lbry.channel_sign({ channel_id: channelId, hexdata: channelNameHex });
if (!channelSignature || !channelSignature.signature || !channelSignature.signing_ts) {
throw new Error('Error getting channel signature.');
}
} catch (e) {
throw e;
}
return { d: channelNameHex, s: channelSignature.signature, t: channelSignature.signing_ts };
};
export const killStream = async (channelId: string, channelName: string) => {
try {
const streamData = await getStreamData(channelId, channelName);
fetch(`${LIVESTREAM_KILL}/${channelId}`, {
method: 'POST',
mode: 'no-cors',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(streamData),
}).then((res) => {
if (res.status !== 200) throw new Error('Kill stream API failed.');
});
} catch (e) {
throw e;
}
};
export const isLiveStreaming = async (channelId: string): Promise<boolean> => {
try {
const response = await fetch(`${LIVESTREAM_LIVE_API}/${channelId}?1`);
const stream = await response.json();
return stream.data?.live;
} catch {
return false;
}
};

View file

@ -1,70 +0,0 @@
// @flow
/*
* This module is responsible for long polling the server to determine if a channel is actively streaming.
*
* One or many entities can subscribe to the live status while instantiating just one long poll interval per channel.
* Once all interested parties have disconnected the poll will shut down. For this reason, be sure to always call the
* disconnect function returned upon connecting.
*/
import { isLiveStreaming } from '$web/src/livestreaming';
const POLL_INTERVAL = 30000; // 30 seconds.
const pollers = {};
const pollingMechanism = {
streaming: false,
startPolling() {
if (this.interval !== 0) return;
const poll = async () => {
this.streaming = await isLiveStreaming(this.channelId);
this.subscribers.forEach((cb) => {
if (cb) cb(this.streaming);
});
};
poll();
this.interval = setInterval(poll, POLL_INTERVAL);
},
stopPolling() {
clearInterval(this.interval);
this.interval = 0;
},
connect(cb): number {
cb(this.streaming);
this.startPolling();
return this.subscribers.push(cb) - 1;
},
disconnect(subscriberIndex: number) {
this.subscribers[subscriberIndex] = null;
if (this.subscribers.every((item) => item === null)) {
this.stopPolling();
delete pollers[this.channelId];
}
},
};
const generateLongPoll = (channelId: string) => {
if (pollers[channelId]) return pollers[channelId];
pollers[channelId] = Object.create({
channelId,
interval: 0,
subscribers: [],
...pollingMechanism,
});
return pollers[channelId];
};
const watchLivestreamStatus = (channelId: ?string, cb: (boolean) => void) => {
if (!channelId || typeof cb !== 'function') return undefined;
const poll = generateLongPoll(channelId);
const subscriberIndex = poll.connect(cb);
return () => poll.disconnect(subscriberIndex);
};
export default watchLivestreamStatus;

123
yarn.lock
View file

@ -2181,56 +2181,24 @@
resolved "https://registry.yarnpkg.com/@ungap/from-entries/-/from-entries-0.2.1.tgz#7e86196b8b2e99d73106a8f25c2a068326346354" resolved "https://registry.yarnpkg.com/@ungap/from-entries/-/from-entries-0.2.1.tgz#7e86196b8b2e99d73106a8f25c2a068326346354"
integrity sha512-CAqefTFAfnUPwYqsWHXpOxHaq1Zo5UQ3m9Zm2p09LggGe57rqHoBn3c++xcoomzXKynAUuiBMDUCQvKMnXjUpA== integrity sha512-CAqefTFAfnUPwYqsWHXpOxHaq1Zo5UQ3m9Zm2p09LggGe57rqHoBn3c++xcoomzXKynAUuiBMDUCQvKMnXjUpA==
"@videojs/http-streaming@2.10.2": "@videojs/http-streaming@2.13.1", "@videojs/http-streaming@2.14.2":
version "2.10.2" version "2.14.2"
resolved "https://registry.yarnpkg.com/@videojs/http-streaming/-/http-streaming-2.10.2.tgz#02e6fcfa74f7850b5f9eb40a8e5c85c9d7d33eaf" resolved "https://registry.yarnpkg.com/@videojs/http-streaming/-/http-streaming-2.14.2.tgz#c083c106b13c7cc59ee441fdb46a94169e19a50b"
integrity sha512-JTAlAUHzj0sTsge2WBh4DWKM2I5BDFEZYOvzxmsK/ySILmI0GRyjAHx9uid68ZECQ2qbEAIRmZW5lWp0R5PeNA== integrity sha512-K1raSfO/pq5r8iUas3OSYni0kXOj91n8ealIpV02khghzGv9LQ6O3YUqYd/eAhJ1HIrmZWOnrYpK/P+mhUExXQ==
dependencies: dependencies:
"@babel/runtime" "^7.12.5" "@babel/runtime" "^7.12.5"
"@videojs/vhs-utils" "3.0.3" "@videojs/vhs-utils" "3.0.5"
aes-decrypter "3.1.2" aes-decrypter "3.1.3"
global "^4.4.0" global "^4.4.0"
m3u8-parser "4.7.0" m3u8-parser "4.7.1"
mpd-parser "0.19.0" mpd-parser "0.21.1"
mux.js "5.13.0"
video.js "^6 || ^7"
"@videojs/http-streaming@2.13.1":
version "2.13.1"
resolved "https://registry.yarnpkg.com/@videojs/http-streaming/-/http-streaming-2.13.1.tgz#b7688d91eec969181430e00868b514b16b3b21b7"
integrity sha512-1x3fkGSPyL0+iaS3/lTvfnPTtfqzfgG+ELQtPPtTvDwqGol9Mx3TNyZwtSTdIufBrqYRn7XybB/3QNMsyjq13A==
dependencies:
"@babel/runtime" "^7.12.5"
"@videojs/vhs-utils" "3.0.4"
aes-decrypter "3.1.2"
global "^4.4.0"
m3u8-parser "4.7.0"
mpd-parser "0.21.0"
mux.js "6.0.1" mux.js "6.0.1"
video.js "^6 || ^7" video.js "^6 || ^7"
"@videojs/vhs-utils@3.0.3": "@videojs/vhs-utils@3.0.5", "@videojs/vhs-utils@^3.0.0", "@videojs/vhs-utils@^3.0.2", "@videojs/vhs-utils@^3.0.4", "@videojs/vhs-utils@^3.0.5":
version "3.0.3" version "3.0.5"
resolved "https://registry.yarnpkg.com/@videojs/vhs-utils/-/vhs-utils-3.0.3.tgz#708bc50742e9481712039695299b32da6582ef92" resolved "https://registry.yarnpkg.com/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz#665ba70d78258ba1ab977364e2fe9f4d4799c46c"
integrity sha512-bU7daxDHhzcTDbmty1cXjzsTYvx2cBGbA8hG5H2Gvpuk4sdfuvkZtMCwtCqL59p6dsleMPspyaNS+7tWXx2Y0A== integrity sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==
dependencies:
"@babel/runtime" "^7.12.5"
global "^4.4.0"
url-toolkit "^2.2.1"
"@videojs/vhs-utils@3.0.4", "@videojs/vhs-utils@^3.0.3", "@videojs/vhs-utils@^3.0.4":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@videojs/vhs-utils/-/vhs-utils-3.0.4.tgz#e253eecd8e9318f767e752010d213587f94bb03a"
integrity sha512-hui4zOj2I1kLzDgf8QDVxD3IzrwjS/43KiS8IHQO0OeeSsb4pB/lgNt1NG7Dv0wMQfCccUpMVLGcK618s890Yg==
dependencies:
"@babel/runtime" "^7.12.5"
global "^4.4.0"
url-toolkit "^2.2.1"
"@videojs/vhs-utils@^3.0.0", "@videojs/vhs-utils@^3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@videojs/vhs-utils/-/vhs-utils-3.0.2.tgz#0203418ecaaff29bc33c69b6ad707787347b7614"
integrity sha512-r8Yas1/tNGsGRNoIaDJuiWiQgM0P2yaEnobgzw5JcBiEqxnS8EXoUm4QtKH7nJtnppZ1yqBx1agBZCvBMKXA2w==
dependencies: dependencies:
"@babel/runtime" "^7.12.5" "@babel/runtime" "^7.12.5"
global "^4.4.0" global "^4.4.0"
@ -2487,6 +2455,16 @@ aes-decrypter@3.1.2:
global "^4.4.0" global "^4.4.0"
pkcs7 "^1.0.4" pkcs7 "^1.0.4"
aes-decrypter@3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/aes-decrypter/-/aes-decrypter-3.1.3.tgz#65ff5f2175324d80c41083b0e135d1464b12ac35"
integrity sha512-VkG9g4BbhMBy+N5/XodDeV6F02chEk9IpgRTq/0bS80y4dzy79VH2Gtms02VXomf3HmyRe3yyJYkJ990ns+d6A==
dependencies:
"@babel/runtime" "^7.12.5"
"@videojs/vhs-utils" "^3.0.5"
global "^4.4.0"
pkcs7 "^1.0.4"
agent-base@5: agent-base@5:
version "5.1.1" version "5.1.1"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c"
@ -11124,6 +11102,15 @@ m3u8-parser@4.7.0:
"@videojs/vhs-utils" "^3.0.0" "@videojs/vhs-utils" "^3.0.0"
global "^4.4.0" global "^4.4.0"
m3u8-parser@4.7.1:
version "4.7.1"
resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-4.7.1.tgz#d6df2c940bb19a01112a04ccc4ff44886a945305"
integrity sha512-pbrQwiMiq+MmI9bl7UjtPT3AK603PV9bogNlr83uC+X9IoxqL5E4k7kU7fMQ0dpRgxgeSMygqUa0IMLQNXLBNA==
dependencies:
"@babel/runtime" "^7.12.5"
"@videojs/vhs-utils" "^3.0.5"
global "^4.4.0"
macos-release@^2.2.0: macos-release@^2.2.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.3.0.tgz#eb1930b036c0800adebccd5f17bc4c12de8bb71f" resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.3.0.tgz#eb1930b036c0800adebccd5f17bc4c12de8bb71f"
@ -11668,16 +11655,6 @@ move-concurrently@^1.0.1:
rimraf "^2.5.4" rimraf "^2.5.4"
run-queue "^1.0.3" run-queue "^1.0.3"
mpd-parser@0.19.0:
version "0.19.0"
resolved "https://registry.yarnpkg.com/mpd-parser/-/mpd-parser-0.19.0.tgz#8937044040ca85e20398ecf5d94552655e2c6728"
integrity sha512-FDLIXtZMZs99fv5iXNFg94quNFT26tobo0NUgHu7L3XgZvEq1NBarf5yxDFFJ1zzfbcmtj+NRaml6nYIxoPWvw==
dependencies:
"@babel/runtime" "^7.12.5"
"@videojs/vhs-utils" "^3.0.2"
"@xmldom/xmldom" "^0.7.2"
global "^4.4.0"
mpd-parser@0.21.0: mpd-parser@0.21.0:
version "0.21.0" version "0.21.0"
resolved "https://registry.yarnpkg.com/mpd-parser/-/mpd-parser-0.21.0.tgz#c2036cce19522383b93c973180fdd82cd646168e" resolved "https://registry.yarnpkg.com/mpd-parser/-/mpd-parser-0.21.0.tgz#c2036cce19522383b93c973180fdd82cd646168e"
@ -11688,6 +11665,16 @@ mpd-parser@0.21.0:
"@xmldom/xmldom" "^0.7.2" "@xmldom/xmldom" "^0.7.2"
global "^4.4.0" global "^4.4.0"
mpd-parser@0.21.1:
version "0.21.1"
resolved "https://registry.yarnpkg.com/mpd-parser/-/mpd-parser-0.21.1.tgz#4f4834074ed0a8e265d8b04a5d2d7b5045a4fa55"
integrity sha512-BxlSXWbKE1n7eyEPBnTEkrzhS3PdmkkKdM1pgKbPnPOH0WFZIc0sPOWi7m0Uo3Wd2a4Or8Qf4ZbS7+ASqQ49fw==
dependencies:
"@babel/runtime" "^7.12.5"
"@videojs/vhs-utils" "^3.0.5"
"@xmldom/xmldom" "^0.7.2"
global "^4.4.0"
ms@2.0.0: ms@2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@ -11721,13 +11708,6 @@ mute-stream@0.0.7:
version "0.0.7" version "0.0.7"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
mux.js@5.13.0:
version "5.13.0"
resolved "https://registry.yarnpkg.com/mux.js/-/mux.js-5.13.0.tgz#99c3da21f6c0362a1529729d1c5e5b51f34f606d"
integrity sha512-PkmnzHcTQjUBEHp3KRPQAFoNkJtKlpCEvsYtXDfDrC+/WqbMuxHvoYfmAbHVAH7Sa/KliPVU0dT1ureO8wilog==
dependencies:
"@babel/runtime" "^7.11.2"
mux.js@6.0.1: mux.js@6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/mux.js/-/mux.js-6.0.1.tgz#65ce0f7a961d56c006829d024d772902d28c7755" resolved "https://registry.yarnpkg.com/mux.js/-/mux.js-6.0.1.tgz#65ce0f7a961d56c006829d024d772902d28c7755"
@ -17274,26 +17254,7 @@ vfile@^2.0.0:
unist-util-stringify-position "^1.0.0" unist-util-stringify-position "^1.0.0"
vfile-message "^1.0.0" vfile-message "^1.0.0"
"video.js@^6 || ^7", "video.js@^6.0.1 || ^7", video.js@^7.0.0: "video.js@^6 || ^7", "video.js@^6.0.1 || ^7", video.js@^7.0.0, video.js@^7.18.1:
version "7.15.4"
resolved "https://registry.yarnpkg.com/video.js/-/video.js-7.15.4.tgz#0f96ef138035138cb30bf00a989b6174f0d16bac"
integrity sha512-hghxkgptLUvfkpktB4wxcIVF3VpY/hVsMkrjHSv0jpj1bW9Jplzdt8IgpTm9YhlB1KYAp07syVQeZcBFUBwhkw==
dependencies:
"@babel/runtime" "^7.12.5"
"@videojs/http-streaming" "2.10.2"
"@videojs/vhs-utils" "^3.0.3"
"@videojs/xhr" "2.6.0"
aes-decrypter "3.1.2"
global "^4.4.0"
keycode "^2.2.0"
m3u8-parser "4.7.0"
mpd-parser "0.19.0"
mux.js "5.13.0"
safe-json-parse "4.0.0"
videojs-font "3.2.0"
videojs-vtt.js "^0.15.3"
video.js@^7.18.1:
version "7.18.1" version "7.18.1"
resolved "https://registry.yarnpkg.com/video.js/-/video.js-7.18.1.tgz#d93cd4992710d4d95574a00e7d29a2518f9b30f7" resolved "https://registry.yarnpkg.com/video.js/-/video.js-7.18.1.tgz#d93cd4992710d4d95574a00e7d29a2518f9b30f7"
integrity sha512-mnXdmkVcD5qQdKMZafDjqdhrnKGettZaGSVkExjACiylSB4r2Yt5W1bchsKmjFpfuNfszsMjTUnnoIWSSqoe/Q== integrity sha512-mnXdmkVcD5qQdKMZafDjqdhrnKGettZaGSVkExjACiylSB4r2Yt5W1bchsKmjFpfuNfszsMjTUnnoIWSSqoe/Q==