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",
"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",

View file

@ -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.",

View file

@ -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),
};
};

View file

@ -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 {

View file

@ -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,

View file

@ -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">

View file

@ -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

View file

@ -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,

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_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';

View file

@ -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>

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> => {
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;
}

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"
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==