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:
parent
575b8f5aac
commit
8144a9bb87
14 changed files with 189 additions and 314 deletions
|
@ -251,6 +251,9 @@
|
|||
"node": ">=7",
|
||||
"yarn": "^1.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"@videojs/http-streaming": "2.14.2"
|
||||
},
|
||||
"lbrySettings": {
|
||||
"lbrynetDaemonVersion": "0.99.0",
|
||||
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
|
||||
|
|
|
@ -1730,7 +1730,7 @@
|
|||
"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:",
|
||||
"Bitrate: 1000 to 2500 kbps": "Bitrate: 1000 to 2500 kbps",
|
||||
"Keyframes: 1": "Keyframes: 1",
|
||||
"Keyframes: 2": "Keyframes: 2",
|
||||
"Profile: High": "Profile: High",
|
||||
"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.",
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||
import { selectClaimIsMineForUri } from 'redux/selectors/claims';
|
||||
import { selectClaimIsMineForUri, selectClaimForUri } from 'redux/selectors/claims';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
import ClaimPreviewReset from './view';
|
||||
import { selectActiveLivestreamForChannel } from 'redux/selectors/livestream';
|
||||
import { getChannelIdFromClaim, getChannelNameFromClaim } from 'util/claim';
|
||||
|
||||
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 {
|
||||
channelName,
|
||||
activeLivestreamForChannel: selectActiveLivestreamForChannel(state, channelId),
|
||||
channelId,
|
||||
channelName,
|
||||
claimIsMine: props.uri && selectClaimIsMineForUri(state, props.uri),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -3,8 +3,7 @@
|
|||
import React from 'react';
|
||||
import { SITE_HELP_EMAIL } from 'config';
|
||||
import Button from 'component/button';
|
||||
import { killStream } from '$web/src/livestreaming';
|
||||
import watchLivestreamStatus from '$web/src/livestreaming/long-polling';
|
||||
import { killStream } from 'util/livestream';
|
||||
import 'scss/component/claim-preview-reset.scss';
|
||||
|
||||
type Props = {
|
||||
|
@ -12,19 +11,12 @@ type Props = {
|
|||
channelName: string,
|
||||
claimIsMine: boolean,
|
||||
doToast: ({ message: string, isError?: boolean }) => void,
|
||||
activeLivestreamForChannel: any,
|
||||
};
|
||||
|
||||
const ClaimPreviewReset = (props: Props) => {
|
||||
const { channelId, channelName, claimIsMine, doToast } = props;
|
||||
|
||||
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 { channelId, channelName, claimIsMine, doToast, activeLivestreamForChannel } = props;
|
||||
if (!claimIsMine || !activeLivestreamForChannel) return null;
|
||||
|
||||
const handleClick = async () => {
|
||||
try {
|
||||
|
|
|
@ -85,7 +85,7 @@ export default function LivestreamLayout(props: Props) {
|
|||
)}
|
||||
|
||||
{!activeStreamUri && !showScheduledInfo && !isCurrentClaimLive && (
|
||||
<div className="help--notice">
|
||||
<div className="help--notice" style={{ marginTop: '20px' }}>
|
||||
{channelName
|
||||
? __("%channelName% isn't live right now, but the chat is! Check back later to watch the stream.", {
|
||||
channelName,
|
||||
|
|
|
@ -47,9 +47,9 @@ type Props = {
|
|||
header: Node,
|
||||
livestreamData: LivestreamReplayData,
|
||||
isLivestreamClaim: boolean,
|
||||
checkLivestreams: (string, ?string, ?string) => void,
|
||||
checkLivestreams: (string, string) => void,
|
||||
channelName: string,
|
||||
channelId: string,
|
||||
channelSignature: { signature?: string, signing_ts?: string },
|
||||
isCheckingLivestreams: boolean,
|
||||
setWaitForFile: (boolean) => void,
|
||||
setOverMaxBitrate: (boolean) => void,
|
||||
|
@ -86,7 +86,7 @@ function PublishFile(props: Props) {
|
|||
subtitle,
|
||||
checkLivestreams,
|
||||
channelId,
|
||||
channelSignature,
|
||||
channelName,
|
||||
isCheckingLivestreams,
|
||||
setWaitForFile,
|
||||
setOverMaxBitrate,
|
||||
|
@ -175,9 +175,9 @@ function PublishFile(props: Props) {
|
|||
} else {
|
||||
if (url.startsWith('http://')) {
|
||||
return url;
|
||||
} else {
|
||||
} else if (url) {
|
||||
return `https://${url}`;
|
||||
}
|
||||
} else return __('Click Check for Replays to update...');
|
||||
}
|
||||
};
|
||||
// update remoteUrl when replay selected
|
||||
|
@ -550,9 +550,7 @@ function PublishFile(props: Props) {
|
|||
label={__('Check for Replays')}
|
||||
disabled={isCheckingLivestreams}
|
||||
icon={ICONS.REFRESH}
|
||||
onClick={() =>
|
||||
checkLivestreams(channelId, channelSignature.signature, channelSignature.signing_ts)
|
||||
}
|
||||
onClick={() => checkLivestreams(channelId, channelName)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -605,7 +603,9 @@ function PublishFile(props: Props) {
|
|||
</div>
|
||||
</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')
|
||||
}`}
|
||||
<div className="table__item-label">
|
||||
|
|
|
@ -30,7 +30,7 @@ import * as PUBLISH_MODES from 'constants/publish_types';
|
|||
import { useHistory } from 'react-router';
|
||||
import Spinner from 'component/spinner';
|
||||
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 { SOURCE_NONE } from 'constants/publish_sources';
|
||||
|
||||
|
@ -208,15 +208,15 @@ function PublishForm(props: Props) {
|
|||
const [waitForFile, setWaitForFile] = useState(false);
|
||||
const [overMaxBitrate, setOverMaxBitrate] = useState(false);
|
||||
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 fileFormDisabled = mode === PUBLISH_MODES.FILE && !filePath && !remoteUrl;
|
||||
const emptyPostError = mode === PUBLISH_MODES.POST && (!fileText || fileText.trim() === '');
|
||||
const formDisabled = (fileFormDisabled && !editingURI) || emptyPostError || publishing;
|
||||
const isInProgress = filePath || editingURI || name || title;
|
||||
const activeChannelName = activeChannelClaim && activeChannelClaim.name;
|
||||
const activeChannelClaimStr = activeChannelClaim && JSON.stringify(activeChannelClaim);
|
||||
const activeChannelName =
|
||||
(myClaimForUri && myClaimForUri.signing_channel && myClaimForUri.signing_channel.name) ||
|
||||
(activeChannelClaim && activeChannelClaim.name);
|
||||
// Editing content info
|
||||
const fileMimeType =
|
||||
myClaimForUri && myClaimForUri.value && myClaimForUri.value.source
|
||||
|
@ -253,26 +253,11 @@ function PublishForm(props: Props) {
|
|||
|
||||
const [previewing, setPreviewing] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (activeChannelClaimStr) {
|
||||
const channelClaim = JSON.parse(activeChannelClaimStr);
|
||||
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 });
|
||||
});
|
||||
useEffect(() => {
|
||||
if (claimChannelId) {
|
||||
fetchLivestreams(claimChannelId, activeChannelName);
|
||||
}
|
||||
}
|
||||
}, [activeChannelClaimStr, setSignedMessage]);
|
||||
}, [claimChannelId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasClaimedInitialRewards) {
|
||||
|
@ -289,29 +274,75 @@ function PublishForm(props: Props) {
|
|||
}, [modal]);
|
||||
|
||||
// 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);
|
||||
fetch(`${LIVESTREAM_REPLAY_API}/${channelId}?signature=${signature || ''}&signing_ts=${timestamp || ''}`) // claimChannelId
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
if (!res || !res.data) {
|
||||
setLivestreamData([]);
|
||||
}
|
||||
setLivestreamData(res.data);
|
||||
setCheckingLivestreams(false);
|
||||
})
|
||||
.catch((e) => {
|
||||
setLivestreamData([]);
|
||||
setCheckingLivestreams(false);
|
||||
let signedMessage;
|
||||
try {
|
||||
await Lbry.channel_sign({
|
||||
channel_id: channelId,
|
||||
hexdata: toHex(channelName || ''),
|
||||
}).then((data) => {
|
||||
signedMessage = data;
|
||||
});
|
||||
} 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 signedMessage = JSON.parse(signedMessageStr);
|
||||
if (claimChannelId && signedMessage.signature) {
|
||||
fetchLivestreams(claimChannelId, signedMessage.signature, signedMessage.signing_ts);
|
||||
const responseFromOldApi = await fetch(
|
||||
`${LIVESTREAM_REPLAY_API}/${channelId}?signature=${signedMessage.signature || ''}&signing_ts=${
|
||||
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;
|
||||
let submitLabel;
|
||||
|
@ -600,7 +631,7 @@ function PublishForm(props: Props) {
|
|||
isCheckingLivestreams={isCheckingLivestreams}
|
||||
checkLivestreams={fetchLivestreams}
|
||||
channelId={claimChannelId}
|
||||
channelSignature={signedMessage}
|
||||
channelName={activeChannelName}
|
||||
header={
|
||||
<>
|
||||
{AVAILABLE_MODES.map((modeName) => (
|
||||
|
@ -627,7 +658,7 @@ function PublishForm(props: Props) {
|
|||
|
||||
{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>
|
||||
<TagsSelect
|
||||
|
|
|
@ -215,10 +215,9 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
html5: {
|
||||
vhs: {
|
||||
overrideNative: !videojs.browser.IS_ANY_SAFARI,
|
||||
allowSeeksWithinUnsafeLiveWindow: true,
|
||||
enableLowInitialPlaylist: false,
|
||||
handlePartialData: true,
|
||||
fastQualityChange: true,
|
||||
useDtsForTimestampOffset: true,
|
||||
},
|
||||
},
|
||||
liveTracker: {
|
||||
|
@ -233,7 +232,6 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
// 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
|
||||
controlBar: {
|
||||
subsCapsButton: false,
|
||||
currentTimeDisplay: !isLivestreamClaim,
|
||||
timeDivider: !isLivestreamClaim,
|
||||
durationDisplay: !isLivestreamClaim,
|
||||
|
|
|
@ -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_REPLAY_API = 'https://api.live.odysee.com/v1/replays/odysee';
|
||||
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)
|
||||
export const NEW_LIVESTREAM_RTMP_URL = 'rtmp://publish.odysee.live/live';
|
||||
|
|
|
@ -84,7 +84,7 @@ export default function LivestreamSetupPage(props: Props) {
|
|||
<p>{__(`Ensure the following settings are selected under the streaming tab:`)}</p>
|
||||
<ul>
|
||||
<li>{__(`Bitrate: 1000 to 2500 kbps`)}</li>
|
||||
<li>{__(`Keyframes: 1`)}</li>
|
||||
<li>{__(`Keyframes: 2`)}</li>
|
||||
<li>{__(`Profile: High`)}</li>
|
||||
<li>{__(`Tune: Zerolatency`)}</li>
|
||||
</ul>
|
||||
|
|
|
@ -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> => {
|
||||
const response = await fetch(LIVESTREAM_LIVE_API);
|
||||
const json = await response.json();
|
||||
const newApiResponse = await fetch(`${NEW_LIVESTREAM_LIVE_API}/all`);
|
||||
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 newApiData = (await newApiResponse.json()).data;
|
||||
let isLive = newApiData.Live;
|
||||
let translatedData = [];
|
||||
let translatedData;
|
||||
// transform data to old API standard
|
||||
if (isLive) {
|
||||
translatedData = {
|
||||
url: newApiData.VideoURL,
|
||||
type: 'application/x-mpegurl',
|
||||
viewCount: newApiData.ViewerCount,
|
||||
claimId: newApiData.ChannelClaimID,
|
||||
timestamp: newApiData.Start,
|
||||
};
|
||||
translatedData = transformNewLivestreamData([newApiData]);
|
||||
} else {
|
||||
const oldApiResponse = await fetch(`${oldApiEndpoint}/${channelId}`);
|
||||
const oldApiData = (await oldApiResponse.json()).data;
|
||||
|
||||
isLive = oldApiData.live;
|
||||
translatedData = {
|
||||
url: oldApiData.url,
|
||||
type: 'application/x-mpegurl',
|
||||
viewCount: oldApiData.viewCount,
|
||||
claimId: oldApiData.claimId,
|
||||
timestamp: oldApiData.timestamp,
|
||||
};
|
||||
translatedData = transformLivestreamData([oldApiData]);
|
||||
}
|
||||
|
||||
try {
|
||||
if (isLive === false) {
|
||||
return { channelStatus: LiveStatus.NOT_LIVE };
|
||||
}
|
||||
return { channelStatus: LiveStatus.LIVE, channelData: transformLivestreamData([translatedData]) };
|
||||
return {
|
||||
channelStatus: LiveStatus.LIVE,
|
||||
channelData: translatedData,
|
||||
};
|
||||
} catch {
|
||||
return { channelStatus: LiveStatus.UNKNOWN };
|
||||
}
|
||||
|
@ -172,14 +183,13 @@ 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.');
|
||||
});
|
||||
const apiData = await fetch(
|
||||
`${LIVESTREAM_KILL}channel_claim_id=${channelId}&channel_name=${channelName}&signature_ts=${streamData.t}&signature=${streamData.s}`
|
||||
);
|
||||
|
||||
const data = (await apiData.json()).data;
|
||||
|
||||
if (!data) throw new Error('Kill stream API failed.');
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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
123
yarn.lock
|
@ -2181,56 +2181,24 @@
|
|||
resolved "https://registry.yarnpkg.com/@ungap/from-entries/-/from-entries-0.2.1.tgz#7e86196b8b2e99d73106a8f25c2a068326346354"
|
||||
integrity sha512-CAqefTFAfnUPwYqsWHXpOxHaq1Zo5UQ3m9Zm2p09LggGe57rqHoBn3c++xcoomzXKynAUuiBMDUCQvKMnXjUpA==
|
||||
|
||||
"@videojs/http-streaming@2.10.2":
|
||||
version "2.10.2"
|
||||
resolved "https://registry.yarnpkg.com/@videojs/http-streaming/-/http-streaming-2.10.2.tgz#02e6fcfa74f7850b5f9eb40a8e5c85c9d7d33eaf"
|
||||
integrity sha512-JTAlAUHzj0sTsge2WBh4DWKM2I5BDFEZYOvzxmsK/ySILmI0GRyjAHx9uid68ZECQ2qbEAIRmZW5lWp0R5PeNA==
|
||||
"@videojs/http-streaming@2.13.1", "@videojs/http-streaming@2.14.2":
|
||||
version "2.14.2"
|
||||
resolved "https://registry.yarnpkg.com/@videojs/http-streaming/-/http-streaming-2.14.2.tgz#c083c106b13c7cc59ee441fdb46a94169e19a50b"
|
||||
integrity sha512-K1raSfO/pq5r8iUas3OSYni0kXOj91n8ealIpV02khghzGv9LQ6O3YUqYd/eAhJ1HIrmZWOnrYpK/P+mhUExXQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@videojs/vhs-utils" "3.0.3"
|
||||
aes-decrypter "3.1.2"
|
||||
"@videojs/vhs-utils" "3.0.5"
|
||||
aes-decrypter "3.1.3"
|
||||
global "^4.4.0"
|
||||
m3u8-parser "4.7.0"
|
||||
mpd-parser "0.19.0"
|
||||
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"
|
||||
m3u8-parser "4.7.1"
|
||||
mpd-parser "0.21.1"
|
||||
mux.js "6.0.1"
|
||||
video.js "^6 || ^7"
|
||||
|
||||
"@videojs/vhs-utils@3.0.3":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@videojs/vhs-utils/-/vhs-utils-3.0.3.tgz#708bc50742e9481712039695299b32da6582ef92"
|
||||
integrity sha512-bU7daxDHhzcTDbmty1cXjzsTYvx2cBGbA8hG5H2Gvpuk4sdfuvkZtMCwtCqL59p6dsleMPspyaNS+7tWXx2Y0A==
|
||||
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==
|
||||
"@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.5"
|
||||
resolved "https://registry.yarnpkg.com/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz#665ba70d78258ba1ab977364e2fe9f4d4799c46c"
|
||||
integrity sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
global "^4.4.0"
|
||||
|
@ -2487,6 +2455,16 @@ aes-decrypter@3.1.2:
|
|||
global "^4.4.0"
|
||||
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:
|
||||
version "5.1.1"
|
||||
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"
|
||||
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:
|
||||
version "2.3.0"
|
||||
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"
|
||||
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:
|
||||
version "0.21.0"
|
||||
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"
|
||||
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:
|
||||
version "2.0.0"
|
||||
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"
|
||||
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:
|
||||
version "6.0.1"
|
||||
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"
|
||||
vfile-message "^1.0.0"
|
||||
|
||||
"video.js@^6 || ^7", "video.js@^6.0.1 || ^7", video.js@^7.0.0:
|
||||
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:
|
||||
"video.js@^6 || ^7", "video.js@^6.0.1 || ^7", video.js@^7.0.0, video.js@^7.18.1:
|
||||
version "7.18.1"
|
||||
resolved "https://registry.yarnpkg.com/video.js/-/video.js-7.18.1.tgz#d93cd4992710d4d95574a00e7d29a2518f9b30f7"
|
||||
integrity sha512-mnXdmkVcD5qQdKMZafDjqdhrnKGettZaGSVkExjACiylSB4r2Yt5W1bchsKmjFpfuNfszsMjTUnnoIWSSqoe/Q==
|
||||
|
|
Loading…
Reference in a new issue