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",
|
"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",
|
||||||
|
|
|
@ -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.",
|
||||||
|
|
|
@ -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),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
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==
|
||||||
|
|
Loading…
Reference in a new issue