livestream + old APIs
This commit is contained in:
parent
f7bceb3734
commit
6dea79819d
28 changed files with 638 additions and 434 deletions
|
@ -229,7 +229,7 @@
|
||||||
"unist-util-visit": "^2.0.3",
|
"unist-util-visit": "^2.0.3",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"vast-client": "^3.1.1",
|
"vast-client": "^3.1.1",
|
||||||
"video.js": "^7.13.3",
|
"video.js": "^7.18.1",
|
||||||
"videojs-contrib-quality-levels": "^2.0.9",
|
"videojs-contrib-quality-levels": "^2.0.9",
|
||||||
"videojs-event-tracking": "^1.0.1",
|
"videojs-event-tracking": "^1.0.1",
|
||||||
"villain-react": "^1.0.9",
|
"villain-react": "^1.0.9",
|
||||||
|
|
|
@ -1753,7 +1753,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,5 +1,5 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectTitleForUri, makeSelectClaimWasPurchased, selectClaimForUri } from 'redux/selectors/claims';
|
import { selectTitleForUri, makeSelectClaimWasPurchased } from 'redux/selectors/claims';
|
||||||
import { makeSelectStreamingUrlForUri } from 'redux/selectors/file_info';
|
import { makeSelectStreamingUrlForUri } from 'redux/selectors/file_info';
|
||||||
import {
|
import {
|
||||||
makeSelectNextUrlForCollectionAndUrl,
|
makeSelectNextUrlForCollectionAndUrl,
|
||||||
|
@ -17,7 +17,6 @@ import { selectCostInfoForUri } from 'lbryinc';
|
||||||
import { doUriInitiatePlay, doSetPlayingUri } from 'redux/actions/content';
|
import { doUriInitiatePlay, doSetPlayingUri } from 'redux/actions/content';
|
||||||
import { doFetchRecommendedContent } from 'redux/actions/search';
|
import { doFetchRecommendedContent } from 'redux/actions/search';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import { getChannelIdFromClaim } from 'util/claim';
|
|
||||||
import { selectMobilePlayerDimensions } from 'redux/selectors/app';
|
import { selectMobilePlayerDimensions } from 'redux/selectors/app';
|
||||||
import { selectIsActiveLivestreamForUri } from 'redux/selectors/livestream';
|
import { selectIsActiveLivestreamForUri } from 'redux/selectors/livestream';
|
||||||
import { doSetMobilePlayerDimensions } from 'redux/actions/app';
|
import { doSetMobilePlayerDimensions } from 'redux/actions/app';
|
||||||
|
@ -29,8 +28,6 @@ const select = (state, props) => {
|
||||||
const playingUri = selectPlayingUri(state);
|
const playingUri = selectPlayingUri(state);
|
||||||
const { uri, collectionId } = playingUri || {};
|
const { uri, collectionId } = playingUri || {};
|
||||||
|
|
||||||
const claim = selectClaimForUri(state, uri);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uri,
|
uri,
|
||||||
playingUri,
|
playingUri,
|
||||||
|
@ -47,7 +44,6 @@ const select = (state, props) => {
|
||||||
previousListUri: collectionId && makeSelectPreviousUrlForCollectionAndUrl(collectionId, uri)(state),
|
previousListUri: collectionId && makeSelectPreviousUrlForCollectionAndUrl(collectionId, uri)(state),
|
||||||
collectionId,
|
collectionId,
|
||||||
isCurrentClaimLive: selectIsActiveLivestreamForUri(state, uri),
|
isCurrentClaimLive: selectIsActiveLivestreamForUri(state, uri),
|
||||||
channelClaimId: claim && getChannelIdFromClaim(claim),
|
|
||||||
mobilePlayerDimensions: selectMobilePlayerDimensions(state),
|
mobilePlayerDimensions: selectMobilePlayerDimensions(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,7 +17,6 @@ import debounce from 'util/debounce';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { isURIEqual } from 'util/lbryURI';
|
import { isURIEqual } from 'util/lbryURI';
|
||||||
import AutoplayCountdown from 'component/autoplayCountdown';
|
import AutoplayCountdown from 'component/autoplayCountdown';
|
||||||
import LivestreamIframeRender from 'component/livestreamLayout/iframe-render';
|
|
||||||
import usePlayNext from 'effects/use-play-next';
|
import usePlayNext from 'effects/use-play-next';
|
||||||
import { getScreenWidth, getScreenHeight, clampFloatingPlayerToScreen, calculateRelativePos } from './helper-functions';
|
import { getScreenWidth, getScreenHeight, clampFloatingPlayerToScreen, calculateRelativePos } from './helper-functions';
|
||||||
|
|
||||||
|
@ -53,9 +52,7 @@ type Props = {
|
||||||
doFetchRecommendedContent: (uri: string) => void,
|
doFetchRecommendedContent: (uri: string) => void,
|
||||||
doUriInitiatePlay: (uri: string, collectionId: ?string, isPlayable: ?boolean, isFloating: ?boolean) => void,
|
doUriInitiatePlay: (uri: string, collectionId: ?string, isPlayable: ?boolean, isFloating: ?boolean) => void,
|
||||||
doSetPlayingUri: ({ uri?: ?string }) => void,
|
doSetPlayingUri: ({ uri?: ?string }) => void,
|
||||||
// mobile only
|
|
||||||
isCurrentClaimLive?: boolean,
|
isCurrentClaimLive?: boolean,
|
||||||
channelClaimId?: any,
|
|
||||||
mobilePlayerDimensions?: any,
|
mobilePlayerDimensions?: any,
|
||||||
doSetMobilePlayerDimensions: ({ height?: ?number, width?: ?number }) => void,
|
doSetMobilePlayerDimensions: ({ height?: ?number, width?: ?number }) => void,
|
||||||
};
|
};
|
||||||
|
@ -79,9 +76,7 @@ export default function FileRenderFloating(props: Props) {
|
||||||
doFetchRecommendedContent,
|
doFetchRecommendedContent,
|
||||||
doUriInitiatePlay,
|
doUriInitiatePlay,
|
||||||
doSetPlayingUri,
|
doSetPlayingUri,
|
||||||
// mobile only
|
|
||||||
isCurrentClaimLive,
|
isCurrentClaimLive,
|
||||||
channelClaimId,
|
|
||||||
mobilePlayerDimensions,
|
mobilePlayerDimensions,
|
||||||
doSetMobilePlayerDimensions,
|
doSetMobilePlayerDimensions,
|
||||||
} = props;
|
} = props;
|
||||||
|
@ -116,7 +111,7 @@ export default function FileRenderFloating(props: Props) {
|
||||||
const isFree = costInfo && costInfo.cost === 0;
|
const isFree = costInfo && costInfo.cost === 0;
|
||||||
const canViewFile = isFree || claimWasPurchased;
|
const canViewFile = isFree || claimWasPurchased;
|
||||||
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode) || isCurrentClaimLive;
|
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode) || isCurrentClaimLive;
|
||||||
const isReadyToPlay = isPlayable && streamingUrl;
|
const isReadyToPlay = isCurrentClaimLive || (isPlayable && streamingUrl);
|
||||||
|
|
||||||
// ****************************************************************************
|
// ****************************************************************************
|
||||||
// FUNCTIONS
|
// FUNCTIONS
|
||||||
|
@ -306,7 +301,7 @@ export default function FileRenderFloating(props: Props) {
|
||||||
[FLOATING_PLAYER_CLASS]: isFloating,
|
[FLOATING_PLAYER_CLASS]: isFloating,
|
||||||
'content__viewer--inline': !isFloating,
|
'content__viewer--inline': !isFloating,
|
||||||
'content__viewer--secondary': isComment,
|
'content__viewer--secondary': isComment,
|
||||||
'content__viewer--theater-mode': !isFloating && videoTheaterMode && playingUri?.uri === primaryUri,
|
'content__viewer--theater-mode': videoTheaterMode && mainFilePlaying && !isCurrentClaimLive && !isMobile,
|
||||||
'content__viewer--disable-click': wasDragging,
|
'content__viewer--disable-click': wasDragging,
|
||||||
'content__viewer--mobile': isMobile,
|
'content__viewer--mobile': isMobile,
|
||||||
})}
|
})}
|
||||||
|
@ -334,9 +329,7 @@ export default function FileRenderFloating(props: Props) {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isCurrentClaimLive && channelClaimId ? (
|
{isReadyToPlay ? (
|
||||||
<LivestreamIframeRender channelClaimId={channelClaimId} showLivestream mobileVersion />
|
|
||||||
) : isReadyToPlay ? (
|
|
||||||
<FileRender className="draggable" uri={uri} />
|
<FileRender className="draggable" uri={uri} />
|
||||||
) : collectionId && !canViewFile ? (
|
) : collectionId && !canViewFile ? (
|
||||||
<div className="content__loading">
|
<div className="content__loading">
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doUriInitiatePlay, doSetPlayingUri, doSetPrimaryUri } from 'redux/actions/content';
|
import { doUriInitiatePlay } from 'redux/actions/content';
|
||||||
import { selectThumbnailForUri, makeSelectClaimWasPurchased } from 'redux/selectors/claims';
|
import {
|
||||||
|
selectThumbnailForUri,
|
||||||
|
makeSelectClaimWasPurchased,
|
||||||
|
selectIsLivestreamClaimForUri,
|
||||||
|
} from 'redux/selectors/claims';
|
||||||
import { makeSelectFileInfoForUri } from 'redux/selectors/file_info';
|
import { makeSelectFileInfoForUri } from 'redux/selectors/file_info';
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import { selectCostInfoForUri } from 'lbryinc';
|
import { selectCostInfoForUri } from 'lbryinc';
|
||||||
|
@ -31,13 +35,12 @@ const select = (state, props) => {
|
||||||
claimWasPurchased: makeSelectClaimWasPurchased(uri)(state),
|
claimWasPurchased: makeSelectClaimWasPurchased(uri)(state),
|
||||||
authenticated: selectUserVerifiedEmail(state),
|
authenticated: selectUserVerifiedEmail(state),
|
||||||
isCurrentClaimLive: selectIsActiveLivestreamForUri(state, uri),
|
isCurrentClaimLive: selectIsActiveLivestreamForUri(state, uri),
|
||||||
|
isLivestreamClaim: selectIsLivestreamClaimForUri(state, uri),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const perform = {
|
const perform = {
|
||||||
doUriInitiatePlay,
|
doUriInitiatePlay,
|
||||||
doSetPlayingUri,
|
|
||||||
doSetPrimaryUri,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withRouter(connect(select, perform)(FileRenderInitiator));
|
export default withRouter(connect(select, perform)(FileRenderInitiator));
|
||||||
|
|
|
@ -14,6 +14,7 @@ import Nag from 'component/common/nag';
|
||||||
// $FlowFixMe cannot resolve ...
|
// $FlowFixMe cannot resolve ...
|
||||||
import FileRenderPlaceholder from 'static/img/fileRenderPlaceholder.png';
|
import FileRenderPlaceholder from 'static/img/fileRenderPlaceholder.png';
|
||||||
import * as COLLECTIONS_CONSTS from 'constants/collections';
|
import * as COLLECTIONS_CONSTS from 'constants/collections';
|
||||||
|
import { LayoutRenderContext } from 'page/livestream/view';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isPlaying: boolean,
|
isPlaying: boolean,
|
||||||
|
@ -33,8 +34,8 @@ type Props = {
|
||||||
videoTheaterMode: boolean,
|
videoTheaterMode: boolean,
|
||||||
isCurrentClaimLive?: boolean,
|
isCurrentClaimLive?: boolean,
|
||||||
doUriInitiatePlay: (uri: string, collectionId: ?string, isPlayable: boolean) => void,
|
doUriInitiatePlay: (uri: string, collectionId: ?string, isPlayable: boolean) => void,
|
||||||
doSetPlayingUri: ({ uri: ?string }) => void,
|
isLivestreamClaim: boolean,
|
||||||
doSetPrimaryUri: (uri: ?string) => void,
|
customAction?: any,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function FileRenderInitiator(props: Props) {
|
export default function FileRenderInitiator(props: Props) {
|
||||||
|
@ -54,15 +55,16 @@ export default function FileRenderInitiator(props: Props) {
|
||||||
authenticated,
|
authenticated,
|
||||||
videoTheaterMode,
|
videoTheaterMode,
|
||||||
isCurrentClaimLive,
|
isCurrentClaimLive,
|
||||||
|
isLivestreamClaim,
|
||||||
|
customAction,
|
||||||
doUriInitiatePlay,
|
doUriInitiatePlay,
|
||||||
doSetPlayingUri,
|
|
||||||
doSetPrimaryUri,
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const containerRef = React.useRef<any>();
|
const layountRendered = React.useContext(LayoutRenderContext);
|
||||||
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
const containerRef = React.useRef<any>();
|
||||||
const [thumbnail, setThumbnail] = React.useState(FileRenderPlaceholder);
|
const [thumbnail, setThumbnail] = React.useState(FileRenderPlaceholder);
|
||||||
|
|
||||||
const { search, href, state: locationState } = location;
|
const { search, href, state: locationState } = location;
|
||||||
|
@ -73,27 +75,20 @@ export default function FileRenderInitiator(props: Props) {
|
||||||
const urlTimeParam = href && href.indexOf('t=') > -1;
|
const urlTimeParam = href && href.indexOf('t=') > -1;
|
||||||
const forceAutoplayParam = locationState && locationState.forceAutoplay;
|
const forceAutoplayParam = locationState && locationState.forceAutoplay;
|
||||||
const shouldAutoplay = forceAutoplayParam || urlTimeParam || autoplay;
|
const shouldAutoplay = forceAutoplayParam || urlTimeParam || autoplay;
|
||||||
|
|
||||||
const isFree = costInfo && costInfo.cost === 0;
|
const isFree = costInfo && costInfo.cost === 0;
|
||||||
const canViewFile = isFree || claimWasPurchased;
|
const canViewFile = isLivestreamClaim
|
||||||
|
? (layountRendered || isMobile) && isCurrentClaimLive
|
||||||
|
: isFree || claimWasPurchased;
|
||||||
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode) || isCurrentClaimLive;
|
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode) || isCurrentClaimLive;
|
||||||
const isText = RENDER_MODES.TEXT_MODES.includes(renderMode);
|
const isText = RENDER_MODES.TEXT_MODES.includes(renderMode);
|
||||||
const isMobileClaimLive = isMobile && isCurrentClaimLive;
|
|
||||||
const foundCover = thumbnail !== FileRenderPlaceholder;
|
|
||||||
|
|
||||||
const renderUnsupported = RENDER_MODES.UNSUPPORTED_IN_THIS_APP.includes(renderMode);
|
const renderUnsupported = RENDER_MODES.UNSUPPORTED_IN_THIS_APP.includes(renderMode);
|
||||||
const disabled = renderUnsupported || (!fileInfo && insufficientCredits && !claimWasPurchased);
|
const disabled =
|
||||||
|
(isLivestreamClaim && !isCurrentClaimLive) ||
|
||||||
|
renderUnsupported ||
|
||||||
|
(!fileInfo && insufficientCredits && !claimWasPurchased);
|
||||||
const shouldRedirect = !authenticated && !isFree;
|
const shouldRedirect = !authenticated && !isFree;
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
// Set livestream as playing uri so it can be rendered by <FileRenderFloating /> on mobile
|
|
||||||
// instead of showing an empty cover image. Needs cover to fill the space with the player.
|
|
||||||
if (isMobileClaimLive && foundCover) {
|
|
||||||
doSetPlayingUri({ uri });
|
|
||||||
doSetPrimaryUri(uri);
|
|
||||||
}
|
|
||||||
}, [doSetPlayingUri, doSetPrimaryUri, foundCover, isMobileClaimLive, uri]);
|
|
||||||
|
|
||||||
function doAuthRedirect() {
|
function doAuthRedirect() {
|
||||||
history.push(`/$/${PAGES.AUTH}?redirect=${encodeURIComponent(location.pathname)}`);
|
history.push(`/$/${PAGES.AUTH}?redirect=${encodeURIComponent(location.pathname)}`);
|
||||||
}
|
}
|
||||||
|
@ -148,11 +143,11 @@ export default function FileRenderInitiator(props: Props) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
onClick={disabled || isMobileClaimLive ? undefined : shouldRedirect ? doAuthRedirect : viewFile}
|
onClick={disabled ? undefined : shouldRedirect ? doAuthRedirect : viewFile}
|
||||||
style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}}
|
style={thumbnail && !obscurePreview ? { backgroundImage: `url("${thumbnail}")` } : {}}
|
||||||
className={classnames('content__cover', {
|
className={classnames('content__cover', {
|
||||||
'content__cover--disabled': disabled,
|
'content__cover--disabled': disabled,
|
||||||
'content__cover--theater-mode': videoTheaterMode,
|
'content__cover--theater-mode': videoTheaterMode && !isMobile,
|
||||||
'content__cover--text': isText,
|
'content__cover--text': isText,
|
||||||
'card__media--nsfw': obscurePreview,
|
'card__media--nsfw': obscurePreview,
|
||||||
})}
|
})}
|
||||||
|
@ -178,7 +173,7 @@ export default function FileRenderInitiator(props: Props) {
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!disabled && !isMobileClaimLive && (
|
{!disabled && (
|
||||||
<Button
|
<Button
|
||||||
requiresAuth={shouldRedirect}
|
requiresAuth={shouldRedirect}
|
||||||
onClick={viewFile}
|
onClick={viewFile}
|
||||||
|
@ -190,6 +185,8 @@ export default function FileRenderInitiator(props: Props) {
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{customAction}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,7 @@ type Props = {
|
||||||
doSuperChatList: (uri: string) => void,
|
doSuperChatList: (uri: string) => void,
|
||||||
claimsByUri: { [string]: any },
|
claimsByUri: { [string]: any },
|
||||||
doFetchUserMemberships: (claimIdCsv: string) => void,
|
doFetchUserMemberships: (claimIdCsv: string) => void,
|
||||||
|
setLayountRendered: (boolean) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function LivestreamChatLayout(props: Props) {
|
export default function LivestreamChatLayout(props: Props) {
|
||||||
|
@ -73,6 +74,7 @@ export default function LivestreamChatLayout(props: Props) {
|
||||||
doSuperChatList,
|
doSuperChatList,
|
||||||
doFetchUserMemberships,
|
doFetchUserMemberships,
|
||||||
claimsByUri,
|
claimsByUri,
|
||||||
|
setLayountRendered,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const isMobile = useIsMobile() && !isPopoutWindow;
|
const isMobile = useIsMobile() && !isPopoutWindow;
|
||||||
|
@ -102,7 +104,7 @@ export default function LivestreamChatLayout(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// get commenter claim ids for checking premium status
|
// get commenter claim ids for checking premium status
|
||||||
const commenterClaimIds = commentsByChronologicalOrder.map(function(comment) {
|
const commenterClaimIds = commentsByChronologicalOrder.map((comment) => {
|
||||||
return comment.channel_id;
|
return comment.channel_id;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -114,7 +116,7 @@ export default function LivestreamChatLayout(props: Props) {
|
||||||
claimsByUri,
|
claimsByUri,
|
||||||
doFetchUserMemberships,
|
doFetchUserMemberships,
|
||||||
[commentsByChronologicalOrder],
|
[commentsByChronologicalOrder],
|
||||||
true,
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
const commentsToDisplay =
|
const commentsToDisplay =
|
||||||
|
@ -167,6 +169,10 @@ export default function LivestreamChatLayout(props: Props) {
|
||||||
if (setCustomViewMode) setCustomViewMode(VIEW_MODES.SUPERCHAT);
|
if (setCustomViewMode) setCustomViewMode(VIEW_MODES.SUPERCHAT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (setLayountRendered) setLayountRendered(true);
|
||||||
|
}, [setLayountRendered]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (customViewMode && customViewMode !== viewMode) {
|
if (customViewMode && customViewMode !== viewMode) {
|
||||||
setViewMode(customViewMode);
|
setViewMode(customViewMode);
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
// @flow
|
|
||||||
import { LIVESTREAM_EMBED_URL } from 'constants/livestream';
|
|
||||||
import LivestreamScheduledInfo from 'component/livestreamScheduledInfo';
|
|
||||||
import React from 'react';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
channelClaimId: string,
|
|
||||||
release?: any,
|
|
||||||
showLivestream: boolean,
|
|
||||||
showScheduledInfo?: boolean,
|
|
||||||
mobileVersion?: boolean,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function LivestreamIframeRender(props: Props) {
|
|
||||||
const { channelClaimId, release, showLivestream, showScheduledInfo, mobileVersion } = props;
|
|
||||||
|
|
||||||
const className = mobileVersion
|
|
||||||
? 'file-render file-render--video'
|
|
||||||
: classnames('file-render file-render--video livestream', {
|
|
||||||
'file-render--scheduledLivestream': !showLivestream,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
<div className="file-viewer">
|
|
||||||
{showLivestream && (
|
|
||||||
<iframe
|
|
||||||
src={`${LIVESTREAM_EMBED_URL}/${channelClaimId}?skin=odysee&autoplay=1`}
|
|
||||||
scrolling="no"
|
|
||||||
allowFullScreen
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showScheduledInfo && release && <LivestreamScheduledInfo release={release} />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,9 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import 'scss/component/_swipeable-drawer.scss';
|
import 'scss/component/_swipeable-drawer.scss';
|
||||||
|
|
||||||
// $FlowFixMe
|
|
||||||
import { Global } from '@emotion/react';
|
|
||||||
|
|
||||||
import { lazyImport } from 'util/lazyImport';
|
import { lazyImport } from 'util/lazyImport';
|
||||||
import { useIsMobile } from 'effects/use-screensize';
|
import { useIsMobile } from 'effects/use-screensize';
|
||||||
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
|
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
|
||||||
|
@ -12,7 +9,7 @@ import LivestreamLink from 'component/livestreamLink';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { PRIMARY_PLAYER_WRAPPER_CLASS } from 'page/file/view';
|
import { PRIMARY_PLAYER_WRAPPER_CLASS } from 'page/file/view';
|
||||||
import FileRenderInitiator from 'component/fileRenderInitiator';
|
import FileRenderInitiator from 'component/fileRenderInitiator';
|
||||||
import LivestreamIframeRender from './iframe-render';
|
import LivestreamScheduledInfo from 'component/livestreamScheduledInfo';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import SwipeableDrawer from 'component/swipeableDrawer';
|
import SwipeableDrawer from 'component/swipeableDrawer';
|
||||||
import { DrawerExpandButton } from 'component/swipeableDrawer/view';
|
import { DrawerExpandButton } from 'component/swipeableDrawer/view';
|
||||||
|
@ -63,28 +60,21 @@ export default function LivestreamLayout(props: Props) {
|
||||||
|
|
||||||
if (!claim || !claim.signing_channel) return null;
|
if (!claim || !claim.signing_channel) return null;
|
||||||
|
|
||||||
const { name: channelName, claim_id: channelClaimId } = claim.signing_channel;
|
const { name: channelName } = claim.signing_channel;
|
||||||
|
|
||||||
|
// TODO: use this to show the 'user is not live functionality'
|
||||||
|
// console.log('show livestream, currentclaimlive, activestreamurl');
|
||||||
|
// console.log(showLivestream, isCurrentClaimLive, activeStreamUri);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!isMobile && <GlobalStyles />}
|
|
||||||
|
|
||||||
<div className="section card-stack">
|
<div className="section card-stack">
|
||||||
<React.Suspense fallback={null}>
|
|
||||||
{isMobile && isCurrentClaimLive ? (
|
|
||||||
<div className={PRIMARY_PLAYER_WRAPPER_CLASS}>
|
<div className={PRIMARY_PLAYER_WRAPPER_CLASS}>
|
||||||
{/* Mobile needs to handle the livestream player like any video player */}
|
<FileRenderInitiator
|
||||||
<FileRenderInitiator uri={claim.canonical_url} />
|
uri={claim.canonical_url}
|
||||||
</div>
|
customAction={showScheduledInfo && <LivestreamScheduledInfo release={release} />}
|
||||||
) : (
|
|
||||||
<LivestreamIframeRender
|
|
||||||
channelClaimId={channelClaimId}
|
|
||||||
release={release}
|
|
||||||
showLivestream={showLivestream}
|
|
||||||
showScheduledInfo={showScheduledInfo}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</React.Suspense>
|
|
||||||
|
|
||||||
{hideComments && !showScheduledInfo && (
|
{hideComments && !showScheduledInfo && (
|
||||||
<div className="help--notice">
|
<div className="help--notice">
|
||||||
|
@ -104,7 +94,7 @@ export default function LivestreamLayout(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeStreamUri && (
|
{activeStreamUri !== uri && (
|
||||||
<LivestreamLink
|
<LivestreamLink
|
||||||
title={__("Click here to access the stream that's currently active")}
|
title={__("Click here to access the stream that's currently active")}
|
||||||
claimUri={activeStreamUri}
|
claimUri={activeStreamUri}
|
||||||
|
@ -204,18 +194,3 @@ const ChatDrawerTitle = (titleProps: any) => {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const GlobalStyles = () => (
|
|
||||||
<Global
|
|
||||||
styles={{
|
|
||||||
body: {
|
|
||||||
'scrollbar-width': '0px',
|
|
||||||
|
|
||||||
'&::-webkit-scrollbar': {
|
|
||||||
width: '0px',
|
|
||||||
height: '0px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
|
@ -101,7 +101,7 @@ function Page(props: Props) {
|
||||||
<div
|
<div
|
||||||
className={classnames('main-wrapper__inner', {
|
className={classnames('main-wrapper__inner', {
|
||||||
'main-wrapper__inner--filepage': isOnFilePage,
|
'main-wrapper__inner--filepage': isOnFilePage,
|
||||||
'main-wrapper__inner--theater-mode': isOnFilePage && videoTheaterMode,
|
'main-wrapper__inner--theater-mode': isOnFilePage && videoTheaterMode && !isMobile,
|
||||||
'main-wrapper__inner--auth': authPage,
|
'main-wrapper__inner--auth': authPage,
|
||||||
'main--popout-chat': isPopoutWindow,
|
'main--popout-chat': isPopoutWindow,
|
||||||
})}
|
})}
|
||||||
|
@ -135,16 +135,14 @@ function Page(props: Props) {
|
||||||
'main--file-page': filePage,
|
'main--file-page': filePage,
|
||||||
'main--settings-page': settingsPage,
|
'main--settings-page': settingsPage,
|
||||||
'main--markdown': isMarkdown,
|
'main--markdown': isMarkdown,
|
||||||
'main--theater-mode': isOnFilePage && videoTheaterMode && !livestream && !isMarkdown,
|
'main--theater-mode': isOnFilePage && videoTheaterMode && !livestream && !isMarkdown && !isMobile,
|
||||||
'main--livestream': livestream && !chatDisabled,
|
'main--livestream': livestream && !chatDisabled,
|
||||||
'main--popout-chat': isPopoutWindow,
|
'main--popout-chat': isPopoutWindow,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
{!isMobile && rightSide && (!livestream || !chatDisabled) && (
|
{!isMobile && (!livestream || !chatDisabled) && rightSide}
|
||||||
<div className="main__right-side">{rightSide}</div>
|
|
||||||
)}
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{!noFooter && (
|
{!noFooter && (
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeSelectClaimForUri, selectThumbnailForUri } from 'redux/selectors/claims';
|
import { selectClaimForUri, selectThumbnailForUri } from 'redux/selectors/claims';
|
||||||
|
import { isStreamPlaceholderClaim, getChannelIdFromClaim } from 'util/claim';
|
||||||
|
import { selectActiveLivestreamForChannel } from 'redux/selectors/livestream';
|
||||||
import {
|
import {
|
||||||
makeSelectNextUrlForCollectionAndUrl,
|
makeSelectNextUrlForCollectionAndUrl,
|
||||||
makeSelectPreviousUrlForCollectionAndUrl,
|
makeSelectPreviousUrlForCollectionAndUrl,
|
||||||
|
@ -30,6 +32,8 @@ const select = (state, props) => {
|
||||||
const autoplay = urlParams.get('autoplay');
|
const autoplay = urlParams.get('autoplay');
|
||||||
const uri = props.uri;
|
const uri = props.uri;
|
||||||
|
|
||||||
|
const claim = selectClaimForUri(state, uri);
|
||||||
|
|
||||||
// TODO: eventually this should be received from DB and not local state (https://github.com/lbryio/lbry-desktop/issues/6796)
|
// TODO: eventually this should be received from DB and not local state (https://github.com/lbryio/lbry-desktop/issues/6796)
|
||||||
const position = urlParams.get('t') !== null ? urlParams.get('t') : makeSelectContentPositionForUri(uri)(state);
|
const position = urlParams.get('t') !== null ? urlParams.get('t') : makeSelectContentPositionForUri(uri)(state);
|
||||||
const userId = selectUser(state) && selectUser(state).id;
|
const userId = selectUser(state) && selectUser(state).id;
|
||||||
|
@ -62,12 +66,14 @@ const select = (state, props) => {
|
||||||
muted: selectMute(state),
|
muted: selectMute(state),
|
||||||
videoPlaybackRate: selectClientSetting(state, SETTINGS.VIDEO_PLAYBACK_RATE),
|
videoPlaybackRate: selectClientSetting(state, SETTINGS.VIDEO_PLAYBACK_RATE),
|
||||||
thumbnail: selectThumbnailForUri(state, uri),
|
thumbnail: selectThumbnailForUri(state, uri),
|
||||||
claim: makeSelectClaimForUri(uri)(state),
|
claim,
|
||||||
homepageData: selectHomepageData(state),
|
homepageData: selectHomepageData(state),
|
||||||
authenticated: selectUserVerifiedEmail(state),
|
authenticated: selectUserVerifiedEmail(state),
|
||||||
shareTelemetry: IS_WEB || selectDaemonSettings(state).share_usage_data,
|
shareTelemetry: IS_WEB || selectDaemonSettings(state).share_usage_data,
|
||||||
isFloating: makeSelectIsPlayerFloating(props.location)(state),
|
isFloating: makeSelectIsPlayerFloating(props.location)(state),
|
||||||
videoTheaterMode: selectClientSetting(state, SETTINGS.VIDEO_THEATER_MODE),
|
videoTheaterMode: selectClientSetting(state, SETTINGS.VIDEO_THEATER_MODE),
|
||||||
|
activeLivestreamForChannel: selectActiveLivestreamForChannel(state, getChannelIdFromClaim(claim)),
|
||||||
|
isLivestreamClaim: isStreamPlaceholderClaim(claim),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ const VideoJsEvents = ({
|
||||||
doAnalyticsView,
|
doAnalyticsView,
|
||||||
claimRewards,
|
claimRewards,
|
||||||
playerServerRef,
|
playerServerRef,
|
||||||
|
isLivestreamClaim,
|
||||||
}: {
|
}: {
|
||||||
tapToUnmuteRef: any, // DOM element
|
tapToUnmuteRef: any, // DOM element
|
||||||
tapToRetryRef: any, // DOM element
|
tapToRetryRef: any, // DOM element
|
||||||
|
@ -51,6 +52,7 @@ const VideoJsEvents = ({
|
||||||
doAnalyticsView: (string, number) => any,
|
doAnalyticsView: (string, number) => any,
|
||||||
claimRewards: () => void,
|
claimRewards: () => void,
|
||||||
playerServerRef: any,
|
playerServerRef: any,
|
||||||
|
isLivestreamClaim: boolean,
|
||||||
}) => {
|
}) => {
|
||||||
/**
|
/**
|
||||||
* Analytics functionality that is run on first video start
|
* Analytics functionality that is run on first video start
|
||||||
|
@ -63,6 +65,8 @@ const VideoJsEvents = ({
|
||||||
|
|
||||||
analytics.playerVideoStartedEvent(embedded);
|
analytics.playerVideoStartedEvent(embedded);
|
||||||
|
|
||||||
|
// don't send this data on livestream
|
||||||
|
if (!isLivestreamClaim) {
|
||||||
// convert bytes to bits, and then divide by seconds
|
// convert bytes to bits, and then divide by seconds
|
||||||
const contentInBits = Number(claimValues.source.size) * 8;
|
const contentInBits = Number(claimValues.source.size) * 8;
|
||||||
const durationInSeconds = claimValues.video && claimValues.video.duration;
|
const durationInSeconds = claimValues.video && claimValues.video.duration;
|
||||||
|
@ -85,6 +89,7 @@ const VideoJsEvents = ({
|
||||||
this, // pass the player
|
this, // pass the player
|
||||||
bitrateAsBitsPerSecond
|
bitrateAsBitsPerSecond
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// hit backend to mark a view
|
// hit backend to mark a view
|
||||||
doAnalyticsView(uri, timeToStartVideo).then(() => {
|
doAnalyticsView(uri, timeToStartVideo).then(() => {
|
||||||
|
@ -194,9 +199,10 @@ const VideoJsEvents = ({
|
||||||
const player = playerRef.current;
|
const player = playerRef.current;
|
||||||
if (player) {
|
if (player) {
|
||||||
const controlBar = player.getChild('controlBar');
|
const controlBar = player.getChild('controlBar');
|
||||||
controlBar
|
const theaterButton = controlBar.getChild('TheaterModeButton');
|
||||||
.getChild('TheaterModeButton')
|
if (theaterButton) {
|
||||||
.controlText(videoTheaterMode ? __('Default Mode (t)') : __('Theater Mode (t)'));
|
theaterButton.controlText(videoTheaterMode ? __('Default Mode (t)') : __('Theater Mode (t)'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [videoTheaterMode]);
|
}, [videoTheaterMode]);
|
||||||
|
|
||||||
|
@ -300,6 +306,22 @@ const VideoJsEvents = ({
|
||||||
document.querySelector('.vjs-big-play-button').style.setProperty('display', 'none', 'important');
|
document.querySelector('.vjs-big-play-button').style.setProperty('display', 'none', 'important');
|
||||||
});
|
});
|
||||||
// player.on('ended', onEnded);
|
// player.on('ended', onEnded);
|
||||||
|
|
||||||
|
if (isLivestreamClaim) {
|
||||||
|
player.liveTracker.on('liveedgechange', async () => {
|
||||||
|
// Only respond to when we fall behind
|
||||||
|
if (player.liveTracker.atLiveEdge()) return;
|
||||||
|
// Don't respond to when user has paused the player
|
||||||
|
if (player.paused()) return;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
// Do not jump ahead if user has paused the player
|
||||||
|
if (player.paused()) return;
|
||||||
|
|
||||||
|
player.liveTracker.seekToLiveEdge();
|
||||||
|
}, 5 * 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -97,10 +97,14 @@ const VideoJsKeyboardShorcuts = ({
|
||||||
playNext,
|
playNext,
|
||||||
playPrevious,
|
playPrevious,
|
||||||
toggleVideoTheaterMode,
|
toggleVideoTheaterMode,
|
||||||
|
isMobile,
|
||||||
|
isLivestreamClaim,
|
||||||
}: {
|
}: {
|
||||||
playNext: any, // function
|
playNext: any, // function
|
||||||
playPrevious: any, // function
|
playPrevious: any, // function
|
||||||
toggleVideoTheaterMode: any, // function
|
toggleVideoTheaterMode: any, // function
|
||||||
|
isMobile: boolean,
|
||||||
|
isLivestreamClaim: boolean,
|
||||||
}) => {
|
}) => {
|
||||||
function toggleTheaterMode(playerRef) {
|
function toggleTheaterMode(playerRef) {
|
||||||
const player = playerRef.current;
|
const player = playerRef.current;
|
||||||
|
@ -141,7 +145,7 @@ const VideoJsKeyboardShorcuts = ({
|
||||||
if (e.keyCode === KEYCODES.M) toggleMute(containerRef);
|
if (e.keyCode === KEYCODES.M) toggleMute(containerRef);
|
||||||
if (e.keyCode === KEYCODES.UP) volumeUp(e, playerRef);
|
if (e.keyCode === KEYCODES.UP) volumeUp(e, playerRef);
|
||||||
if (e.keyCode === KEYCODES.DOWN) volumeDown(e, playerRef);
|
if (e.keyCode === KEYCODES.DOWN) volumeDown(e, playerRef);
|
||||||
if (e.keyCode === KEYCODES.T) toggleTheaterMode(playerRef);
|
if (e.keyCode === KEYCODES.T && !isMobile && !isLivestreamClaim) toggleTheaterMode(playerRef);
|
||||||
if (e.keyCode === KEYCODES.L) seekVideo(SEEK_STEP, playerRef, containerRef);
|
if (e.keyCode === KEYCODES.L) seekVideo(SEEK_STEP, playerRef, containerRef);
|
||||||
if (e.keyCode === KEYCODES.J) seekVideo(-SEEK_STEP, playerRef, containerRef);
|
if (e.keyCode === KEYCODES.J) seekVideo(-SEEK_STEP, playerRef, containerRef);
|
||||||
if (e.keyCode === KEYCODES.RIGHT) seekVideo(SEEK_STEP_5, playerRef, containerRef);
|
if (e.keyCode === KEYCODES.RIGHT) seekVideo(SEEK_STEP_5, playerRef, containerRef);
|
||||||
|
|
|
@ -21,6 +21,9 @@ import React, { useEffect, useRef, useState } from 'react';
|
||||||
import recsys from './plugins/videojs-recsys/plugin';
|
import recsys from './plugins/videojs-recsys/plugin';
|
||||||
// import runAds from './ads';
|
// import runAds from './ads';
|
||||||
import videojs from 'video.js';
|
import videojs from 'video.js';
|
||||||
|
import { LIVESTREAM_STREAM_X_PULL, LIVESTREAM_CDN_DOMAIN, LIVESTREAM_STREAM_DOMAIN } from 'constants/livestream';
|
||||||
|
import { useIsMobile } from 'effects/use-screensize';
|
||||||
|
|
||||||
const canAutoplay = require('./plugins/canAutoplay');
|
const canAutoplay = require('./plugins/canAutoplay');
|
||||||
|
|
||||||
require('@silvermine/videojs-chromecast')(videojs);
|
require('@silvermine/videojs-chromecast')(videojs);
|
||||||
|
@ -80,6 +83,9 @@ type Props = {
|
||||||
claimValues: any,
|
claimValues: any,
|
||||||
clearPosition: (string) => void,
|
clearPosition: (string) => void,
|
||||||
centerPlayButton: () => void,
|
centerPlayButton: () => void,
|
||||||
|
isLivestreamClaim: boolean,
|
||||||
|
userClaimId: ?string,
|
||||||
|
activeLivestreamForChannel: any,
|
||||||
};
|
};
|
||||||
|
|
||||||
const videoPlaybackRates = [0.25, 0.5, 0.75, 1, 1.1, 1.25, 1.5, 1.75, 2];
|
const videoPlaybackRates = [0.25, 0.5, 0.75, 1, 1.1, 1.25, 1.5, 1.75, 2];
|
||||||
|
@ -143,8 +149,13 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
uri,
|
uri,
|
||||||
clearPosition,
|
clearPosition,
|
||||||
centerPlayButton,
|
centerPlayButton,
|
||||||
|
userClaimId,
|
||||||
|
isLivestreamClaim,
|
||||||
|
activeLivestreamForChannel,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
// will later store the videojs player
|
// will later store the videojs player
|
||||||
const playerRef = useRef();
|
const playerRef = useRef();
|
||||||
const containerRef = useRef();
|
const containerRef = useRef();
|
||||||
|
@ -154,8 +165,17 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
|
|
||||||
const playerServerRef = useRef();
|
const playerServerRef = useRef();
|
||||||
|
|
||||||
|
const { url: livestreamVideoUrl } = activeLivestreamForChannel || {};
|
||||||
|
const showQualitySelector = !isLivestreamClaim || (livestreamVideoUrl && livestreamVideoUrl.includes('/transcode/'));
|
||||||
|
|
||||||
// initiate keyboard shortcuts
|
// initiate keyboard shortcuts
|
||||||
const { curried_function } = keyboardShorcuts({ toggleVideoTheaterMode, playNext, playPrevious });
|
const { curried_function } = keyboardShorcuts({
|
||||||
|
isMobile,
|
||||||
|
isLivestreamClaim,
|
||||||
|
toggleVideoTheaterMode,
|
||||||
|
playNext,
|
||||||
|
playPrevious,
|
||||||
|
});
|
||||||
|
|
||||||
const [reload, setReload] = useState('initial');
|
const [reload, setReload] = useState('initial');
|
||||||
|
|
||||||
|
@ -178,6 +198,7 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
uri,
|
uri,
|
||||||
playerServerRef,
|
playerServerRef,
|
||||||
clearPosition,
|
clearPosition,
|
||||||
|
isLivestreamClaim,
|
||||||
});
|
});
|
||||||
|
|
||||||
const videoJsOptions = {
|
const videoJsOptions = {
|
||||||
|
@ -186,23 +207,40 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
controls: true,
|
controls: true,
|
||||||
html5: {
|
html5: {
|
||||||
vhs: {
|
hls: {
|
||||||
overrideNative: !videojs.browser.IS_ANY_SAFARI,
|
overrideNative: !videojs.browser.IS_ANY_SAFARI,
|
||||||
|
allowSeeksWithinUnsafeLiveWindow: true,
|
||||||
|
enableLowInitialPlaylist: false,
|
||||||
|
handlePartialData: true,
|
||||||
|
smoothQualityChange: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
liveTracker: {
|
||||||
|
trackingThreshold: 0,
|
||||||
|
liveTolerance: 10,
|
||||||
|
},
|
||||||
|
inactivityTimeout: 2000,
|
||||||
autoplay: autoplay,
|
autoplay: autoplay,
|
||||||
muted: startMuted,
|
muted: startMuted,
|
||||||
poster: poster, // thumb looks bad in app, and if autoplay, flashing poster is annoying
|
poster: poster, // thumb looks bad in app, and if autoplay, flashing poster is annoying
|
||||||
plugins: { eventTracking: true, overlay: OVERLAY.OVERLAY_DATA },
|
plugins: { eventTracking: true, overlay: OVERLAY.OVERLAY_DATA },
|
||||||
// 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: { subsCapsButton: false },
|
controlBar: {
|
||||||
|
subsCapsButton: false,
|
||||||
|
currentTimeDisplay: !isLivestreamClaim,
|
||||||
|
timeDivider: !isLivestreamClaim,
|
||||||
|
durationDisplay: !isLivestreamClaim,
|
||||||
|
remainingTimeDisplay: !isLivestreamClaim,
|
||||||
|
},
|
||||||
techOrder: ['chromecast', 'html5'],
|
techOrder: ['chromecast', 'html5'],
|
||||||
chromecast: {
|
chromecast: {
|
||||||
requestTitleFn: (src) => title || '',
|
requestTitleFn: (src) => title || '',
|
||||||
requestSubtitleFn: (src) => channelName || '',
|
requestSubtitleFn: (src) => channelName || '',
|
||||||
},
|
},
|
||||||
bigPlayButton: embedded, // only show big play button if embedded
|
bigPlayButton: embedded, // only show big play button if embedded
|
||||||
|
liveui: true,
|
||||||
|
suppressNotSupportedError: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize video.js
|
// Initialize video.js
|
||||||
|
@ -236,12 +274,8 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
if (bigPlayButton) bigPlayButton.style.setProperty('display', 'block', 'important');
|
if (bigPlayButton) bigPlayButton.style.setProperty('display', 'block', 'important');
|
||||||
}
|
}
|
||||||
|
|
||||||
Chromecast.initialize(player);
|
|
||||||
|
|
||||||
// Add quality selector to player
|
// Add quality selector to player
|
||||||
player.hlsQualitySelector({
|
if (showQualitySelector) player.hlsQualitySelector({ displayCurrentQuality: true });
|
||||||
displayCurrentQuality: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add recsys plugin
|
// Add recsys plugin
|
||||||
if (shareTelemetry) {
|
if (shareTelemetry) {
|
||||||
|
@ -281,6 +315,8 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
}
|
}
|
||||||
window.player.userActive(true);
|
window.player.userActive(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Chromecast.initialize(player);
|
||||||
});
|
});
|
||||||
|
|
||||||
// fixes #3498 (https://github.com/lbryio/lbry-desktop/issues/3498)
|
// fixes #3498 (https://github.com/lbryio/lbry-desktop/issues/3498)
|
||||||
|
@ -290,6 +326,14 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
return vjs;
|
return vjs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showQualitySelector) {
|
||||||
|
// Add quality selector to player
|
||||||
|
const player = playerRef.current;
|
||||||
|
if (player) player.hlsQualitySelector({ displayCurrentQuality: true });
|
||||||
|
}
|
||||||
|
}, [showQualitySelector]);
|
||||||
|
|
||||||
/** instantiate videoJS and dispose of it when done with code **/
|
/** instantiate videoJS and dispose of it when done with code **/
|
||||||
// This lifecycle hook is only called once (on mount), or when `isAudio` or `source` changes.
|
// This lifecycle hook is only called once (on mount), or when `isAudio` or `source` changes.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -315,6 +359,41 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
document.querySelector('.vjs-control-bar').style.setProperty('opacity', '1', 'important');
|
document.querySelector('.vjs-control-bar').style.setProperty('opacity', '1', 'important');
|
||||||
|
|
||||||
|
if (isLivestreamClaim && userClaimId) {
|
||||||
|
// $FlowFixMe
|
||||||
|
vjsPlayer.addClass('livestreamPlayer');
|
||||||
|
|
||||||
|
// @if process.env.NODE_ENV!='production'
|
||||||
|
videojs.Vhs.xhr.beforeRequest = (options) => {
|
||||||
|
if (!options.headers) options.headers = {};
|
||||||
|
options.headers['X-Pull'] = LIVESTREAM_STREAM_X_PULL;
|
||||||
|
options.uri = options.uri.replace(LIVESTREAM_CDN_DOMAIN, LIVESTREAM_STREAM_DOMAIN);
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
// @endif
|
||||||
|
|
||||||
|
// const newPoster = livestreamData.ThumbnailURL;
|
||||||
|
|
||||||
|
// pretty sure it's not working
|
||||||
|
// vjsPlayer.poster(newPoster);
|
||||||
|
|
||||||
|
// here specifically because we don't allow rewinds at the moment
|
||||||
|
// $FlowFixMe
|
||||||
|
// vjsPlayer.on('play', function () {
|
||||||
|
// // $FlowFixMe
|
||||||
|
// vjsPlayer.liveTracker.seekToLiveEdge();
|
||||||
|
// });
|
||||||
|
|
||||||
|
// $FlowFixMe
|
||||||
|
vjsPlayer.src({
|
||||||
|
type: 'application/x-mpegURL',
|
||||||
|
src: livestreamVideoUrl,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// $FlowFixMe
|
||||||
|
vjsPlayer.removeClass('livestreamPlayer');
|
||||||
|
videojs.Vhs.xhr.beforeRequest = (options) => {};
|
||||||
|
|
||||||
// change to m3u8 if applicable
|
// change to m3u8 if applicable
|
||||||
const response = await fetch(source, { method: 'HEAD', cache: 'no-store' });
|
const response = await fetch(source, { method: 'HEAD', cache: 'no-store' });
|
||||||
|
|
||||||
|
@ -335,6 +414,8 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
src: source,
|
src: source,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// load video once source setup
|
// load video once source setup
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
vjsPlayer.load();
|
vjsPlayer.load();
|
||||||
|
@ -381,7 +462,7 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
window.player = undefined;
|
window.player = undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [isAudio, source, reload]);
|
}, [isAudio, source, reload, userClaimId, isLivestreamClaim]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classnames('video-js-parent', { 'video-js-parent--ios': IS_IOS })} ref={containerRef}>
|
<div className={classnames('video-js-parent', { 'video-js-parent--ios': IS_IOS })} ref={containerRef}>
|
||||||
|
|
|
@ -67,6 +67,8 @@ type Props = {
|
||||||
isMarkdownOrComment: boolean,
|
isMarkdownOrComment: boolean,
|
||||||
doAnalyticsView: (string, number) => void,
|
doAnalyticsView: (string, number) => void,
|
||||||
claimRewards: () => void,
|
claimRewards: () => void,
|
||||||
|
isLivestreamClaim: boolean,
|
||||||
|
activeLivestreamForChannel: any,
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -109,7 +111,10 @@ function VideoViewer(props: Props) {
|
||||||
previousListUri,
|
previousListUri,
|
||||||
videoTheaterMode,
|
videoTheaterMode,
|
||||||
isMarkdownOrComment,
|
isMarkdownOrComment,
|
||||||
|
isLivestreamClaim,
|
||||||
|
activeLivestreamForChannel,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const permanentUrl = claim && claim.permanent_url;
|
const permanentUrl = claim && claim.permanent_url;
|
||||||
const adApprovedChannelIds = homepageData ? getAllIds(homepageData) : [];
|
const adApprovedChannelIds = homepageData ? getAllIds(homepageData) : [];
|
||||||
const claimId = claim && claim.claim_id;
|
const claimId = claim && claim.claim_id;
|
||||||
|
@ -149,11 +154,14 @@ function VideoViewer(props: Props) {
|
||||||
toggleAutoplayNext();
|
toggleAutoplayNext();
|
||||||
}, [localAutoplayNext]);
|
}, [localAutoplayNext]);
|
||||||
|
|
||||||
useInterval(() => {
|
useInterval(
|
||||||
if (playerRef.current && isPlaying) {
|
() => {
|
||||||
|
if (playerRef.current && isPlaying && !isLivestreamClaim) {
|
||||||
handlePosition(playerRef.current);
|
handlePosition(playerRef.current);
|
||||||
}
|
}
|
||||||
}, PLAY_POSITION_SAVE_INTERVAL_MS);
|
},
|
||||||
|
!isLivestreamClaim ? PLAY_POSITION_SAVE_INTERVAL_MS : null
|
||||||
|
);
|
||||||
|
|
||||||
const updateVolumeState = React.useCallback(
|
const updateVolumeState = React.useCallback(
|
||||||
debounce((volume, muted) => {
|
debounce((volume, muted) => {
|
||||||
|
@ -181,11 +189,13 @@ function VideoViewer(props: Props) {
|
||||||
|
|
||||||
// TODO: analytics functionality
|
// TODO: analytics functionality
|
||||||
function doTrackingBuffered(e: Event, data: any) {
|
function doTrackingBuffered(e: Event, data: any) {
|
||||||
|
if (!isLivestreamClaim) {
|
||||||
fetch(source, { method: 'HEAD', cache: 'no-store' }).then((response) => {
|
fetch(source, { method: 'HEAD', cache: 'no-store' }).then((response) => {
|
||||||
data.playerPoweredBy = response.headers.get('x-powered-by');
|
data.playerPoweredBy = response.headers.get('x-powered-by');
|
||||||
doAnalyticsBuffer(uri, data);
|
doAnalyticsBuffer(uri, data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const doPlay = useCallback(
|
const doPlay = useCallback(
|
||||||
(playUri) => {
|
(playUri) => {
|
||||||
|
@ -294,7 +304,7 @@ function VideoViewer(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePosition(player) {
|
function handlePosition(player) {
|
||||||
savePosition(uri, player.currentTime());
|
if (!isLivestreamClaim) savePosition(uri, player.currentTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
function restorePlaybackRate(player) {
|
function restorePlaybackRate(player) {
|
||||||
|
@ -426,7 +436,7 @@ function VideoViewer(props: Props) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (position) {
|
if (position && !isLivestreamClaim) {
|
||||||
player.currentTime(position);
|
player.currentTime(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -510,6 +520,9 @@ function VideoViewer(props: Props) {
|
||||||
uri={uri}
|
uri={uri}
|
||||||
clearPosition={clearPosition}
|
clearPosition={clearPosition}
|
||||||
centerPlayButton={centerPlayButton}
|
centerPlayButton={centerPlayButton}
|
||||||
|
userClaimId={claim && claim.signing_channel && claim.signing_channel.claim_id}
|
||||||
|
isLivestreamClaim={isLivestreamClaim}
|
||||||
|
activeLivestreamForChannel={activeLivestreamForChannel}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,9 +1,18 @@
|
||||||
|
export const LIVESTREAM_CDN_DOMAIN = 'cdn.odysee.live';
|
||||||
|
export const LIVESTREAM_STREAM_DOMAIN = 'stream.odysee.com';
|
||||||
|
export const LIVESTREAM_STREAM_X_PULL = 'bitwaveCDN';
|
||||||
|
|
||||||
export const LIVESTREAM_EMBED_URL = 'https://player.odysee.live/odysee';
|
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.stream.odysee.com/stream/kill';
|
||||||
|
|
||||||
|
// 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_REPLAY_API = 'https://api.odysee.live/replays/list';
|
||||||
|
// export const NEW_LIVESTREAM_LIVE_API = 'https://api.odysee.live/livestream/is_live';
|
||||||
|
|
||||||
export const MAX_LIVESTREAM_COMMENTS = 50;
|
export const MAX_LIVESTREAM_COMMENTS = 50;
|
||||||
|
|
||||||
export const LIVESTREAM_STARTS_SOON_BUFFER = 15;
|
export const LIVESTREAM_STARTS_SOON_BUFFER = 15;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeSelectTagInClaimOrChannelForUri, selectClaimForUri } from 'redux/selectors/claims';
|
import { makeSelectTagInClaimOrChannelForUri, selectClaimForUri } from 'redux/selectors/claims';
|
||||||
import { doSetPlayingUri } from 'redux/actions/content';
|
import { doSetPrimaryUri } from 'redux/actions/content';
|
||||||
import { doUserSetReferrer } from 'redux/actions/user';
|
import { doUserSetReferrer } from 'redux/actions/user';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
import { DISABLE_COMMENTS_TAG } from 'constants/tags';
|
import { DISABLE_COMMENTS_TAG } from 'constants/tags';
|
||||||
|
@ -24,7 +24,7 @@ const select = (state, props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const perform = {
|
const perform = {
|
||||||
doSetPlayingUri,
|
doSetPrimaryUri,
|
||||||
doUserSetReferrer,
|
doUserSetReferrer,
|
||||||
doCommentSocketConnect,
|
doCommentSocketConnect,
|
||||||
doCommentSocketDisconnect,
|
doCommentSocketDisconnect,
|
||||||
|
|
|
@ -7,9 +7,9 @@ import LivestreamLayout from 'component/livestreamLayout';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useIsMobile } from 'effects/use-screensize';
|
|
||||||
|
|
||||||
const LivestreamChatLayout = lazyImport(() => import('component/livestreamChatLayout' /* webpackChunkName: "chat" */));
|
const LivestreamChatLayout = lazyImport(() => import('component/livestreamChatLayout' /* webpackChunkName: "chat" */));
|
||||||
|
const LIVESTREAM_STATUS_CHECK_INTERVAL = 30000;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
activeLivestreamForChannel: any,
|
activeLivestreamForChannel: any,
|
||||||
|
@ -19,13 +19,15 @@ type Props = {
|
||||||
claim: StreamClaim,
|
claim: StreamClaim,
|
||||||
isAuthenticated: boolean,
|
isAuthenticated: boolean,
|
||||||
uri: string,
|
uri: string,
|
||||||
doSetPlayingUri: ({ uri: ?string }) => void,
|
doSetPrimaryUri: (uri: ?string) => void,
|
||||||
doCommentSocketConnect: (string, string, string) => void,
|
doCommentSocketConnect: (string, string, string) => void,
|
||||||
doCommentSocketDisconnect: (string, string) => void,
|
doCommentSocketDisconnect: (string, string) => void,
|
||||||
doFetchChannelLiveStatus: (string) => void,
|
doFetchChannelLiveStatus: (string) => void,
|
||||||
doUserSetReferrer: (string) => void,
|
doUserSetReferrer: (string) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const LayoutRenderContext = React.createContext<any>();
|
||||||
|
|
||||||
export default function LivestreamPage(props: Props) {
|
export default function LivestreamPage(props: Props) {
|
||||||
const {
|
const {
|
||||||
activeLivestreamForChannel,
|
activeLivestreamForChannel,
|
||||||
|
@ -35,19 +37,18 @@ export default function LivestreamPage(props: Props) {
|
||||||
claim,
|
claim,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
uri,
|
uri,
|
||||||
doSetPlayingUri,
|
doSetPrimaryUri,
|
||||||
doCommentSocketConnect,
|
doCommentSocketConnect,
|
||||||
doCommentSocketDisconnect,
|
doCommentSocketDisconnect,
|
||||||
doFetchChannelLiveStatus,
|
doFetchChannelLiveStatus,
|
||||||
doUserSetReferrer,
|
doUserSetReferrer,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
const [activeStreamUri, setActiveStreamUri] = React.useState(false);
|
const [activeStreamUri, setActiveStreamUri] = React.useState(false);
|
||||||
const [showLivestream, setShowLivestream] = React.useState(false);
|
const [showLivestream, setShowLivestream] = React.useState(false);
|
||||||
const [showScheduledInfo, setShowScheduledInfo] = React.useState(false);
|
const [showScheduledInfo, setShowScheduledInfo] = React.useState(false);
|
||||||
const [hideComments, setHideComments] = React.useState(false);
|
const [hideComments, setHideComments] = React.useState(false);
|
||||||
|
const [layountRendered, setLayountRendered] = React.useState(chatDisabled);
|
||||||
|
|
||||||
const isInitialized = Boolean(activeLivestreamForChannel) || activeLivestreamInitialized;
|
const isInitialized = Boolean(activeLivestreamForChannel) || activeLivestreamInitialized;
|
||||||
const isChannelBroadcasting = Boolean(activeLivestreamForChannel);
|
const isChannelBroadcasting = Boolean(activeLivestreamForChannel);
|
||||||
|
@ -82,10 +83,14 @@ export default function LivestreamPage(props: Props) {
|
||||||
};
|
};
|
||||||
}, [claim, uri, doCommentSocketConnect, doCommentSocketDisconnect]);
|
}, [claim, uri, doCommentSocketConnect, doCommentSocketDisconnect]);
|
||||||
|
|
||||||
// Find out current channels status + active live claim.
|
// Find out current channels status + active live claim every 30 seconds
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
doFetchChannelLiveStatus(livestreamChannelId);
|
const fetch = () => doFetchChannelLiveStatus(livestreamChannelId);
|
||||||
const intervalId = setInterval(() => doFetchChannelLiveStatus(livestreamChannelId), 30000);
|
|
||||||
|
fetch();
|
||||||
|
|
||||||
|
const intervalId = setInterval(fetch, LIVESTREAM_STATUS_CHECK_INTERVAL);
|
||||||
|
|
||||||
return () => clearInterval(intervalId);
|
return () => clearInterval(intervalId);
|
||||||
}, [livestreamChannelId, doFetchChannelLiveStatus]);
|
}, [livestreamChannelId, doFetchChannelLiveStatus]);
|
||||||
|
|
||||||
|
@ -144,14 +149,10 @@ export default function LivestreamPage(props: Props) {
|
||||||
}, [uri, stringifiedClaim, isAuthenticated, doUserSetReferrer]);
|
}, [uri, stringifiedClaim, isAuthenticated, doUserSetReferrer]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// Set playing uri to null so the popout player doesnt start playing the dummy claim if a user navigates back
|
doSetPrimaryUri(uri);
|
||||||
// This can be removed when we start using the app video player, not a LIVESTREAM iframe
|
|
||||||
doSetPlayingUri({ uri: null });
|
|
||||||
|
|
||||||
return () => {
|
return () => doSetPrimaryUri(null);
|
||||||
if (isMobile) doSetPlayingUri({ uri: null });
|
}, [doSetPrimaryUri, uri]);
|
||||||
};
|
|
||||||
}, [doSetPlayingUri, isMobile]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page
|
||||||
|
@ -163,12 +164,13 @@ export default function LivestreamPage(props: Props) {
|
||||||
!hideComments &&
|
!hideComments &&
|
||||||
isInitialized && (
|
isInitialized && (
|
||||||
<React.Suspense fallback={null}>
|
<React.Suspense fallback={null}>
|
||||||
<LivestreamChatLayout uri={uri} />
|
<LivestreamChatLayout uri={uri} setLayountRendered={setLayountRendered} />
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isInitialized && (
|
{isInitialized && (
|
||||||
|
<LayoutRenderContext.Provider value={layountRendered}>
|
||||||
<LivestreamLayout
|
<LivestreamLayout
|
||||||
uri={uri}
|
uri={uri}
|
||||||
hideComments={hideComments}
|
hideComments={hideComments}
|
||||||
|
@ -178,6 +180,7 @@ export default function LivestreamPage(props: Props) {
|
||||||
showScheduledInfo={showScheduledInfo}
|
showScheduledInfo={showScheduledInfo}
|
||||||
activeStreamUri={activeStreamUri}
|
activeStreamUri={activeStreamUri}
|
||||||
/>
|
/>
|
||||||
|
</LayoutRenderContext.Provider>
|
||||||
)}
|
)}
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|
|
@ -24,7 +24,6 @@ import { normalizeURI } from 'util/lbryURI';
|
||||||
import * as COLLECTIONS_CONSTS from 'constants/collections';
|
import * as COLLECTIONS_CONSTS from 'constants/collections';
|
||||||
import { selectIsSubscribedForUri } from 'redux/selectors/subscriptions';
|
import { selectIsSubscribedForUri } from 'redux/selectors/subscriptions';
|
||||||
import { selectBlacklistedOutpointMap } from 'lbryinc';
|
import { selectBlacklistedOutpointMap } from 'lbryinc';
|
||||||
import { doAnalyticsView } from 'redux/actions/app';
|
|
||||||
import ShowPage from './view';
|
import ShowPage from './view';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
|
@ -95,7 +94,6 @@ const perform = {
|
||||||
doResolveUri,
|
doResolveUri,
|
||||||
doBeginPublish,
|
doBeginPublish,
|
||||||
doFetchItemsInCollection,
|
doFetchItemsInCollection,
|
||||||
doAnalyticsView,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withRouter(connect(select, perform)(ShowPage));
|
export default withRouter(connect(select, perform)(ShowPage));
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { DOMAIN, ENABLE_NO_SOURCE_CLAIMS } from 'config';
|
import { DOMAIN, ENABLE_NO_SOURCE_CLAIMS } from 'config';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { lazyImport } from 'util/lazyImport';
|
import { lazyImport } from 'util/lazyImport';
|
||||||
import { Redirect, useHistory } from 'react-router-dom';
|
import { Redirect, useHistory } from 'react-router-dom';
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
|
@ -40,7 +40,6 @@ type Props = {
|
||||||
doResolveUri: (uri: string, returnCached: boolean, resolveReposts: boolean, options: any) => void,
|
doResolveUri: (uri: string, returnCached: boolean, resolveReposts: boolean, options: any) => void,
|
||||||
doBeginPublish: (name: ?string) => void,
|
doBeginPublish: (name: ?string) => void,
|
||||||
doFetchItemsInCollection: ({ collectionId: string }) => void,
|
doFetchItemsInCollection: ({ collectionId: string }) => void,
|
||||||
doAnalyticsView: (uri: string) => void,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ShowPage(props: Props) {
|
export default function ShowPage(props: Props) {
|
||||||
|
@ -62,7 +61,6 @@ export default function ShowPage(props: Props) {
|
||||||
doResolveUri,
|
doResolveUri,
|
||||||
doBeginPublish,
|
doBeginPublish,
|
||||||
doFetchItemsInCollection,
|
doFetchItemsInCollection,
|
||||||
doAnalyticsView,
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { push } = useHistory();
|
const { push } = useHistory();
|
||||||
|
@ -136,16 +134,6 @@ export default function ShowPage(props: Props) {
|
||||||
}
|
}
|
||||||
}, [doResolveUri, isResolvingUri, canonicalUrl, uri, claimExists, haventFetchedYet, isMine, claimIsPending, search]);
|
}, [doResolveUri, isResolvingUri, canonicalUrl, uri, claimExists, haventFetchedYet, isMine, claimIsPending, search]);
|
||||||
|
|
||||||
// Regular claims will call the file/view event when a user actually watches the claim
|
|
||||||
// This can be removed when we get rid of the livestream iframe
|
|
||||||
const [viewTracked, setViewTracked] = useState(false);
|
|
||||||
useEffect(() => {
|
|
||||||
if (showLiveStream && !viewTracked) {
|
|
||||||
doAnalyticsView(uri);
|
|
||||||
setViewTracked(true);
|
|
||||||
}
|
|
||||||
}, [showLiveStream, viewTracked]);
|
|
||||||
|
|
||||||
// Don't navigate directly to repost urls
|
// Don't navigate directly to repost urls
|
||||||
// Always redirect to the actual content
|
// Always redirect to the actual content
|
||||||
if (claim && claim.repost_url === uri) {
|
if (claim && claim.repost_url === uri) {
|
||||||
|
|
|
@ -1,24 +1,23 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as ACTIONS from 'constants/action_types';
|
import * as ACTIONS from 'constants/action_types';
|
||||||
import { doClaimSearch } from 'redux/actions/claims';
|
import { doClaimSearch } from 'redux/actions/claims';
|
||||||
import { LIVESTREAM_LIVE_API, LIVESTREAM_STARTS_SOON_BUFFER } from 'constants/livestream';
|
import {
|
||||||
|
LiveStatus,
|
||||||
|
fetchLiveChannel,
|
||||||
|
fetchLiveChannels,
|
||||||
|
determineLiveClaim,
|
||||||
|
filterUpcomingLiveStreamClaims,
|
||||||
|
} from 'util/livestream';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
const LiveStatus = Object.freeze({
|
const FETCH_ACTIVE_LIVESTREAMS_MIN_INTERVAL_MS = 5 * 60 * 1000;
|
||||||
LIVE: 'LIVE',
|
|
||||||
NOT_LIVE: 'NOT_LIVE',
|
|
||||||
UNKNOWN: 'UNKNOWN',
|
|
||||||
});
|
|
||||||
|
|
||||||
type LiveStatusType = $Keys<typeof LiveStatus>;
|
|
||||||
|
|
||||||
type LiveChannelStatus = { channelStatus: LiveStatusType, channelData?: LivestreamInfo };
|
|
||||||
|
|
||||||
export const doFetchNoSourceClaims = (channelId: string) => async (dispatch: Dispatch, getState: GetState) => {
|
export const doFetchNoSourceClaims = (channelId: string) => async (dispatch: Dispatch, getState: GetState) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.FETCH_NO_SOURCE_CLAIMS_STARTED,
|
type: ACTIONS.FETCH_NO_SOURCE_CLAIMS_STARTED,
|
||||||
data: channelId,
|
data: channelId,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await dispatch(
|
await dispatch(
|
||||||
doClaimSearch({
|
doClaimSearch({
|
||||||
|
@ -44,51 +43,8 @@ export const doFetchNoSourceClaims = (channelId: string) => async (dispatch: Dis
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const FETCH_ACTIVE_LIVESTREAMS_MIN_INTERVAL_MS = 5 * 60 * 1000;
|
const fetchUpcomingLivestreamClaims = (channelIds: Array<string>, lang: ?Array<string> = null) =>
|
||||||
|
doClaimSearch(
|
||||||
const transformLivestreamData = (data: Array<any>): LivestreamInfo => {
|
|
||||||
return data.reduce((acc, curr) => {
|
|
||||||
acc[curr.claimId] = {
|
|
||||||
live: curr.live,
|
|
||||||
viewCount: curr.viewCount,
|
|
||||||
creatorId: curr.claimId,
|
|
||||||
startedStreaming: moment(curr.timestamp),
|
|
||||||
};
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchLiveChannels = async (): Promise<LivestreamInfo> => {
|
|
||||||
const response = await fetch(LIVESTREAM_LIVE_API);
|
|
||||||
const json = await response.json();
|
|
||||||
if (!json.data) throw new Error();
|
|
||||||
return transformLivestreamData(json.data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchLiveChannel = async (channelId: string): Promise<LiveChannelStatus> => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${LIVESTREAM_LIVE_API}/${channelId}`);
|
|
||||||
const json = await response.json();
|
|
||||||
if (json.data?.live === false) return { channelStatus: LiveStatus.NOT_LIVE };
|
|
||||||
return { channelStatus: LiveStatus.LIVE, channelData: transformLivestreamData([json.data]) };
|
|
||||||
} catch {
|
|
||||||
return { channelStatus: LiveStatus.UNKNOWN };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const filterUpcomingLiveStreamClaims = (upcomingClaims) => {
|
|
||||||
const startsSoonMoment = moment().startOf('minute').add(LIVESTREAM_STARTS_SOON_BUFFER, 'minutes');
|
|
||||||
const startingSoonClaims = {};
|
|
||||||
Object.keys(upcomingClaims).forEach((key) => {
|
|
||||||
if (moment.unix(upcomingClaims[key].stream.value.release_time).isSameOrBefore(startsSoonMoment)) {
|
|
||||||
startingSoonClaims[key] = upcomingClaims[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return startingSoonClaims;
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchUpcomingLivestreamClaims = (channelIds: Array<string>, lang: ?Array<string> = null) => {
|
|
||||||
return doClaimSearch(
|
|
||||||
{
|
{
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: 50,
|
page_size: 50,
|
||||||
|
@ -105,14 +61,13 @@ const fetchUpcomingLivestreamClaims = (channelIds: Array<string>, lang: ?Array<s
|
||||||
useAutoPagination: true,
|
useAutoPagination: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const fetchMostRecentLivestreamClaims = (
|
const fetchMostRecentLivestreamClaims = (
|
||||||
channelIds: Array<string>,
|
channelIds: Array<string>,
|
||||||
orderBy: Array<string> = ['release_time'],
|
orderBy: Array<string> = ['release_time'],
|
||||||
lang: ?Array<string> = null
|
lang: ?Array<string> = null
|
||||||
) => {
|
) =>
|
||||||
return doClaimSearch(
|
doClaimSearch(
|
||||||
{
|
{
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: 50,
|
page_size: 50,
|
||||||
|
@ -129,36 +84,6 @@ const fetchMostRecentLivestreamClaims = (
|
||||||
useAutoPagination: true,
|
useAutoPagination: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const distanceFromStreamStart = (claimA: any, claimB: any, channelStartedStreaming) => {
|
|
||||||
return [
|
|
||||||
Math.abs(moment.unix(claimA.stream.value.release_time).diff(channelStartedStreaming, 'minutes')),
|
|
||||||
Math.abs(moment.unix(claimB.stream.value.release_time).diff(channelStartedStreaming, 'minutes')),
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
const determineLiveClaim = (claims: any, activeLivestreams: any) => {
|
|
||||||
const activeClaims = {};
|
|
||||||
|
|
||||||
Object.values(claims).forEach((claim: any) => {
|
|
||||||
const channelID = claim.stream.signing_channel.claim_id;
|
|
||||||
if (activeClaims[channelID]) {
|
|
||||||
const [distanceA, distanceB] = distanceFromStreamStart(
|
|
||||||
claim,
|
|
||||||
activeClaims[channelID],
|
|
||||||
activeLivestreams[channelID].startedStreaming
|
|
||||||
);
|
|
||||||
|
|
||||||
if (distanceA < distanceB) {
|
|
||||||
activeClaims[channelID] = claim;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
activeClaims[channelID] = claim;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return activeClaims;
|
|
||||||
};
|
|
||||||
|
|
||||||
const findActiveStreams = async (
|
const findActiveStreams = async (
|
||||||
channelIDs: Array<string>,
|
channelIDs: Array<string>,
|
||||||
|
@ -185,8 +110,7 @@ const findActiveStreams = async (
|
||||||
return determineLiveClaim(allClaims, liveChannels);
|
return determineLiveClaim(allClaims, liveChannels);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const doFetchChannelLiveStatus = (channelId: string) => {
|
export const doFetchChannelLiveStatus = (channelId: string) => async (dispatch: Dispatch) => {
|
||||||
return async (dispatch: Dispatch) => {
|
|
||||||
try {
|
try {
|
||||||
const { channelStatus, channelData } = await fetchLiveChannel(channelId);
|
const { channelStatus, channelData } = await fetchLiveChannel(channelId);
|
||||||
|
|
||||||
|
@ -209,11 +133,12 @@ export const doFetchChannelLiveStatus = (channelId: string) => {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
dispatch({ type: ACTIONS.REMOVE_CHANNEL_FROM_ACTIVE_LIVESTREAMS, data: { channelId } });
|
dispatch({ type: ACTIONS.REMOVE_CHANNEL_FROM_ACTIVE_LIVESTREAMS, data: { channelId } });
|
||||||
}
|
}
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const doFetchActiveLivestreams = (orderBy: Array<string> = ['release_time'], lang: ?Array<string> = null) => {
|
export const doFetchActiveLivestreams = (
|
||||||
return async (dispatch: Dispatch, getState: GetState) => {
|
orderBy: Array<string> = ['release_time'],
|
||||||
|
lang: ?Array<string> = null
|
||||||
|
) => async (dispatch: Dispatch, getState: GetState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const timeDelta = now - state.livestream.activeLivestreamsLastFetchedDate;
|
const timeDelta = now - state.livestream.activeLivestreamsLastFetchedDate;
|
||||||
|
@ -222,11 +147,13 @@ export const doFetchActiveLivestreams = (orderBy: Array<string> = ['release_time
|
||||||
const nextOptions = { order_by: orderBy, ...(lang ? { any_languages: lang } : {}) };
|
const nextOptions = { order_by: orderBy, ...(lang ? { any_languages: lang } : {}) };
|
||||||
const sameOptions = JSON.stringify(prevOptions) === JSON.stringify(nextOptions);
|
const sameOptions = JSON.stringify(prevOptions) === JSON.stringify(nextOptions);
|
||||||
|
|
||||||
|
// already fetched livestreams within the interval, skip for now
|
||||||
if (sameOptions && timeDelta < FETCH_ACTIVE_LIVESTREAMS_MIN_INTERVAL_MS) {
|
if (sameOptions && timeDelta < FETCH_ACTIVE_LIVESTREAMS_MIN_INTERVAL_MS) {
|
||||||
dispatch({ type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_SKIPPED });
|
dispatch({ type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_SKIPPED });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// start fetching livestreams
|
||||||
dispatch({ type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_STARTED });
|
dispatch({ type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_STARTED });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -261,5 +188,4 @@ export const doFetchActiveLivestreams = (orderBy: Array<string> = ['release_time
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
dispatch({ type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_FAILED });
|
dispatch({ type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_FAILED });
|
||||||
}
|
}
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -323,6 +323,11 @@ export const makeSelectTotalPagesInChannelSearch = (uri: string) =>
|
||||||
return byChannel['pageCount'];
|
return byChannel['pageCount'];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const selectIsLivestreamClaimForUri = (state: State, uri: string) => {
|
||||||
|
const claim = selectClaimForUri(state, uri);
|
||||||
|
return isStreamPlaceholderClaim(claim);
|
||||||
|
};
|
||||||
|
|
||||||
export const selectMetadataForUri = (state: State, uri: string) => {
|
export const selectMetadataForUri = (state: State, uri: string) => {
|
||||||
const claim = selectClaimForUri(state, uri);
|
const claim = selectClaimForUri(state, uri);
|
||||||
const metadata = claim && claim.value;
|
const metadata = claim && claim.value;
|
||||||
|
@ -381,6 +386,9 @@ export const makeSelectEffectiveAmountForUri = (uri: string) =>
|
||||||
|
|
||||||
export const makeSelectContentTypeForUri = (uri: string) =>
|
export const makeSelectContentTypeForUri = (uri: string) =>
|
||||||
createSelector(makeSelectClaimForUri(uri), (claim) => {
|
createSelector(makeSelectClaimForUri(uri), (claim) => {
|
||||||
|
const isLivestreamClaim = isStreamPlaceholderClaim(claim);
|
||||||
|
if (isLivestreamClaim) return 'livestream';
|
||||||
|
|
||||||
const source = claim && claim.value && claim.value.source;
|
const source = claim && claim.value && claim.value.source;
|
||||||
return source ? source.media_type : undefined;
|
return source ? source.media_type : undefined;
|
||||||
});
|
});
|
||||||
|
|
|
@ -106,7 +106,7 @@ export const makeSelectFileRenderModeForUri = (uri: string) =>
|
||||||
makeSelectMediaTypeForUri(uri),
|
makeSelectMediaTypeForUri(uri),
|
||||||
makeSelectFileExtensionForUri(uri),
|
makeSelectFileExtensionForUri(uri),
|
||||||
(contentType, mediaType, extension) => {
|
(contentType, mediaType, extension) => {
|
||||||
if (mediaType === 'video' || FORCE_CONTENT_TYPE_PLAYER.includes(contentType)) {
|
if (mediaType === 'video' || FORCE_CONTENT_TYPE_PLAYER.includes(contentType) || mediaType === 'livestream') {
|
||||||
return RENDER_MODES.VIDEO;
|
return RENDER_MODES.VIDEO;
|
||||||
}
|
}
|
||||||
if (mediaType === 'audio') {
|
if (mediaType === 'audio') {
|
||||||
|
|
|
@ -8,7 +8,7 @@ $recent-msg-button__height: 2rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: rgba(var(--color-header-background-base), 1);
|
background-color: rgba(var(--color-header-background-base), 1);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
margin-bottom: var(--spacing-m);
|
margin-left: var(--spacing-m);
|
||||||
|
|
||||||
.credit-amount {
|
.credit-amount {
|
||||||
font-weight: unset;
|
font-weight: unset;
|
||||||
|
@ -25,11 +25,19 @@ $recent-msg-button__height: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-large) {
|
||||||
|
margin-left: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
top: calc(var(--header-height) + var(--spacing-m)) !important;
|
||||||
|
position: sticky;
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: $breakpoint-medium) {
|
@media (min-width: $breakpoint-medium) {
|
||||||
margin: 0;
|
min-width: var(--livestream-comments-width);
|
||||||
width: var(--livestream-comments-width);
|
width: var(--livestream-comments-width);
|
||||||
height: calc(100vh - var(--header-height) - var(--spacing-l));
|
height: calc(100vh - var(--header-height) - var(--spacing-l));
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
@ -79,6 +87,12 @@ $recent-msg-button__height: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-medium) {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
margin-top: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
// Mobile
|
// Mobile
|
||||||
@media (max-width: $breakpoint-small) {
|
@media (max-width: $breakpoint-small) {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
|
|
|
@ -577,17 +577,9 @@ body {
|
||||||
|
|
||||||
.main--livestream {
|
.main--livestream {
|
||||||
@extend .main--file-page;
|
@extend .main--file-page;
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
@media (min-width: $breakpoint-small) {
|
|
||||||
> * {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-stack {
|
.card-stack {
|
||||||
margin-bottom: var(--spacing-m);
|
margin-bottom: var(--spacing-m);
|
||||||
|
|
||||||
@media (min-width: ($breakpoint-large + 300px)) {
|
@media (min-width: ($breakpoint-large + 300px)) {
|
||||||
max-width: calc(var(--page-max-width--filepage) / 1.25);
|
max-width: calc(var(--page-max-width--filepage) / 1.25);
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
@ -595,12 +587,6 @@ body {
|
||||||
width: calc(100% - var(--livestream-comments-width));
|
width: calc(100% - var(--livestream-comments-width));
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: $breakpoint-medium) and (max-width: ($breakpoint-large + 300px)) {
|
|
||||||
max-width: calc(100vw - var(--livestream-comments-width) - var(--spacing-m) * 3);
|
|
||||||
margin-left: var(--spacing-m);
|
|
||||||
margin-right: var(--spacing-m);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: $breakpoint-medium) {
|
@media (max-width: $breakpoint-medium) {
|
||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
@ -620,19 +606,6 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main__right-side {
|
|
||||||
position: sticky !important;
|
|
||||||
top: 76px !important;
|
|
||||||
|
|
||||||
width: var(--livestream-comments-width);
|
|
||||||
margin-left: var(--spacing-l);
|
|
||||||
|
|
||||||
@media (max-width: $breakpoint-medium) {
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: var(--spacing-m);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: $breakpoint-medium) {
|
@media (max-width: $breakpoint-medium) {
|
||||||
padding: 0 var(--spacing-s);
|
padding: 0 var(--spacing-s);
|
||||||
.card__main-actions {
|
.card__main-actions {
|
||||||
|
|
|
@ -272,33 +272,59 @@ button.vjs-big-play-button {
|
||||||
.vjs-volume-panel {
|
.vjs-volume-panel {
|
||||||
order: 4 !important;
|
order: 4 !important;
|
||||||
}
|
}
|
||||||
.vjs-current-time {
|
|
||||||
|
.vjs-seek-to-live-control {
|
||||||
order: 5 !important;
|
order: 5 !important;
|
||||||
}
|
}
|
||||||
.vjs-time-divider {
|
|
||||||
|
.vjs-live-control {
|
||||||
|
order: 5 !important;
|
||||||
|
margin-left: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-current-time {
|
||||||
order: 6 !important;
|
order: 6 !important;
|
||||||
}
|
}
|
||||||
.vjs-duration {
|
.vjs-time-divider {
|
||||||
order: 7 !important;
|
order: 7 !important;
|
||||||
}
|
}
|
||||||
.vjs-custom-control-spacer {
|
.vjs-duration {
|
||||||
order: 8 !important;
|
order: 8 !important;
|
||||||
}
|
}
|
||||||
.vjs-button--autoplay-next {
|
.vjs-custom-control-spacer {
|
||||||
order: 9 !important;
|
order: 9 !important;
|
||||||
}
|
}
|
||||||
.vjs-playback-rate {
|
.vjs-button--autoplay-next {
|
||||||
order: 10 !important;
|
order: 10 !important;
|
||||||
}
|
}
|
||||||
.vjs-chromecast-button {
|
.vjs-playback-rate {
|
||||||
order: 11 !important;
|
order: 11 !important;
|
||||||
}
|
}
|
||||||
.vjs-quality-selector {
|
.vjs-chromecast-button {
|
||||||
order: 12 !important;
|
order: 12 !important;
|
||||||
}
|
}
|
||||||
.vjs-button--theater-mode {
|
.vjs-quality-selector {
|
||||||
order: 13 !important;
|
order: 13 !important;
|
||||||
}
|
}
|
||||||
.vjs-fullscreen-control {
|
.vjs-button--theater-mode {
|
||||||
order: 14 !important;
|
order: 14 !important;
|
||||||
}
|
}
|
||||||
|
.vjs-fullscreen-control {
|
||||||
|
order: 15 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// livestream player
|
||||||
|
.livestreamPlayer {
|
||||||
|
.vjs-current-time,
|
||||||
|
.vjs-duration,
|
||||||
|
.vjs-time-divider,
|
||||||
|
.vjs-button--theater-mode,
|
||||||
|
//.vjs-quality-selector,
|
||||||
|
.vjs-chromecast-button, // hopefully we can use chromecast in the future at some point
|
||||||
|
.vjs-button--autoplay-next
|
||||||
|
//,.vjs-progress-control
|
||||||
|
{
|
||||||
|
// hiding progress control for now
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,27 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import { LIVESTREAM_LIVE_API, LIVESTREAM_KILL, LIVESTREAM_STARTS_SOON_BUFFER } from 'constants/livestream';
|
||||||
|
import { toHex } from 'util/hex';
|
||||||
|
import Lbry from 'lbry';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
export const LiveStatus = Object.freeze({
|
||||||
|
LIVE: 'LIVE',
|
||||||
|
NOT_LIVE: 'NOT_LIVE',
|
||||||
|
UNKNOWN: 'UNKNOWN',
|
||||||
|
});
|
||||||
|
|
||||||
|
type LiveStatusType = $Keys<typeof LiveStatus>;
|
||||||
|
|
||||||
|
type LiveChannelStatus = {
|
||||||
|
channelStatus: LiveStatusType,
|
||||||
|
channelData?: LivestreamInfo,
|
||||||
|
};
|
||||||
|
|
||||||
|
type StreamData = {
|
||||||
|
d: string,
|
||||||
|
s: string,
|
||||||
|
t: string,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to extract livestream claim uris from the output of
|
* Helper to extract livestream claim uris from the output of
|
||||||
|
@ -43,3 +66,133 @@ export function getTipValues(superChatsByAmount: Array<Comment>) {
|
||||||
|
|
||||||
return { superChatsChannelUrls, superChatsFiatAmount, superChatsLBCAmount };
|
return { superChatsChannelUrls, superChatsFiatAmount, superChatsLBCAmount };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const transformLivestreamData = (data: Array<any>): LivestreamInfo => {
|
||||||
|
return data.reduce((acc, curr) => {
|
||||||
|
acc[curr.claimId] = {
|
||||||
|
url: curr.url,
|
||||||
|
type: curr.type,
|
||||||
|
live: curr.live,
|
||||||
|
viewCount: curr.viewCount,
|
||||||
|
creatorId: curr.claimId,
|
||||||
|
startedStreaming: moment(curr.timestamp),
|
||||||
|
};
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchLiveChannels = async (): Promise<LivestreamInfo> => {
|
||||||
|
const response = await fetch(LIVESTREAM_LIVE_API);
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
if (!json.data) throw new Error();
|
||||||
|
|
||||||
|
return transformLivestreamData(json.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether or not the channel is used, used for long polling to display live status on claim viewing page
|
||||||
|
* @param channelId
|
||||||
|
* @returns {Promise<{channelStatus: string}|{channelData: LivestreamInfo, channelStatus: string}>}
|
||||||
|
*/
|
||||||
|
export const fetchLiveChannel = async (channelId: string): Promise<LiveChannelStatus> => {
|
||||||
|
const newApiEndpoint = LIVESTREAM_LIVE_API;
|
||||||
|
const newApiResponse = await fetch(`${newApiEndpoint}/${channelId}`);
|
||||||
|
const newApiData = (await newApiResponse.json()).data;
|
||||||
|
const isLive = newApiData.live;
|
||||||
|
|
||||||
|
// transform data to old API standard
|
||||||
|
const translatedData = {
|
||||||
|
url: newApiData.url,
|
||||||
|
type: 'application/x-mpegurl',
|
||||||
|
viewCount: newApiData.viewCount,
|
||||||
|
claimId: newApiData.claimId,
|
||||||
|
timestamp: newApiData.timestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isLive === false) {
|
||||||
|
return { channelStatus: LiveStatus.NOT_LIVE };
|
||||||
|
}
|
||||||
|
return { channelStatus: LiveStatus.LIVE, channelData: transformLivestreamData([translatedData]) };
|
||||||
|
} catch {
|
||||||
|
return { channelStatus: LiveStatus.UNKNOWN };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const distanceFromStreamStart = (claimA: any, claimB: any, channelStartedStreaming) => {
|
||||||
|
return [
|
||||||
|
Math.abs(moment.unix(claimA.stream.value.release_time).diff(channelStartedStreaming, 'minutes')),
|
||||||
|
Math.abs(moment.unix(claimB.stream.value.release_time).diff(channelStartedStreaming, 'minutes')),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const determineLiveClaim = (claims: any, activeLivestreams: any) => {
|
||||||
|
const activeClaims = {};
|
||||||
|
|
||||||
|
Object.values(claims).forEach((claim: any) => {
|
||||||
|
const channelID = claim.stream.signing_channel.claim_id;
|
||||||
|
if (activeClaims[channelID]) {
|
||||||
|
const [distanceA, distanceB] = distanceFromStreamStart(
|
||||||
|
claim,
|
||||||
|
activeClaims[channelID],
|
||||||
|
activeLivestreams[channelID].startedStreaming
|
||||||
|
);
|
||||||
|
|
||||||
|
if (distanceA < distanceB) {
|
||||||
|
activeClaims[channelID] = claim;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
activeClaims[channelID] = claim;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return activeClaims;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filterUpcomingLiveStreamClaims = (upcomingClaims: any) => {
|
||||||
|
const startsSoonMoment = moment().startOf('minute').add(LIVESTREAM_STARTS_SOON_BUFFER, 'minutes');
|
||||||
|
const startingSoonClaims = {};
|
||||||
|
|
||||||
|
Object.keys(upcomingClaims).forEach((key) => {
|
||||||
|
if (moment.unix(upcomingClaims[key].stream.value.release_time).isSameOrBefore(startsSoonMoment)) {
|
||||||
|
startingSoonClaims[key] = upcomingClaims[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return startingSoonClaims;
|
||||||
|
};
|
||||||
|
|
69
yarn.lock
69
yarn.lock
|
@ -2190,6 +2190,20 @@
|
||||||
mux.js "5.13.0"
|
mux.js "5.13.0"
|
||||||
video.js "^6 || ^7"
|
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"
|
||||||
|
video.js "^6 || ^7"
|
||||||
|
|
||||||
"@videojs/vhs-utils@3.0.3":
|
"@videojs/vhs-utils@3.0.3":
|
||||||
version "3.0.3"
|
version "3.0.3"
|
||||||
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.3.tgz#708bc50742e9481712039695299b32da6582ef92"
|
||||||
|
@ -2199,19 +2213,19 @@
|
||||||
global "^4.4.0"
|
global "^4.4.0"
|
||||||
url-toolkit "^2.2.1"
|
url-toolkit "^2.2.1"
|
||||||
|
|
||||||
"@videojs/vhs-utils@^3.0.0", "@videojs/vhs-utils@^3.0.2":
|
"@videojs/vhs-utils@3.0.4", "@videojs/vhs-utils@^3.0.3", "@videojs/vhs-utils@^3.0.4":
|
||||||
version "3.0.2"
|
version "3.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@videojs/vhs-utils/-/vhs-utils-3.0.2.tgz#0203418ecaaff29bc33c69b6ad707787347b7614"
|
resolved "https://registry.yarnpkg.com/@videojs/vhs-utils/-/vhs-utils-3.0.4.tgz#e253eecd8e9318f767e752010d213587f94bb03a"
|
||||||
integrity sha512-r8Yas1/tNGsGRNoIaDJuiWiQgM0P2yaEnobgzw5JcBiEqxnS8EXoUm4QtKH7nJtnppZ1yqBx1agBZCvBMKXA2w==
|
integrity sha512-hui4zOj2I1kLzDgf8QDVxD3IzrwjS/43KiS8IHQO0OeeSsb4pB/lgNt1NG7Dv0wMQfCccUpMVLGcK618s890Yg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.12.5"
|
"@babel/runtime" "^7.12.5"
|
||||||
global "^4.4.0"
|
global "^4.4.0"
|
||||||
url-toolkit "^2.2.1"
|
url-toolkit "^2.2.1"
|
||||||
|
|
||||||
"@videojs/vhs-utils@^3.0.3":
|
"@videojs/vhs-utils@^3.0.0", "@videojs/vhs-utils@^3.0.2":
|
||||||
version "3.0.4"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@videojs/vhs-utils/-/vhs-utils-3.0.4.tgz#e253eecd8e9318f767e752010d213587f94bb03a"
|
resolved "https://registry.yarnpkg.com/@videojs/vhs-utils/-/vhs-utils-3.0.2.tgz#0203418ecaaff29bc33c69b6ad707787347b7614"
|
||||||
integrity sha512-hui4zOj2I1kLzDgf8QDVxD3IzrwjS/43KiS8IHQO0OeeSsb4pB/lgNt1NG7Dv0wMQfCccUpMVLGcK618s890Yg==
|
integrity sha512-r8Yas1/tNGsGRNoIaDJuiWiQgM0P2yaEnobgzw5JcBiEqxnS8EXoUm4QtKH7nJtnppZ1yqBx1agBZCvBMKXA2w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.12.5"
|
"@babel/runtime" "^7.12.5"
|
||||||
global "^4.4.0"
|
global "^4.4.0"
|
||||||
|
@ -11659,6 +11673,16 @@ mpd-parser@0.19.0:
|
||||||
"@xmldom/xmldom" "^0.7.2"
|
"@xmldom/xmldom" "^0.7.2"
|
||||||
global "^4.4.0"
|
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"
|
||||||
|
integrity sha512-NbpMJ57qQzFmfCiP1pbL7cGMbVTD0X1hqNgL0VYP1wLlZXLf/HtmvQpNkOA1AHkPVeGQng+7/jEtSvNUzV7Gdg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.12.5"
|
||||||
|
"@videojs/vhs-utils" "^3.0.2"
|
||||||
|
"@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"
|
||||||
|
@ -11699,6 +11723,14 @@ mux.js@5.13.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.11.2"
|
"@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"
|
||||||
|
integrity sha512-22CHb59rH8pWGcPGW5Og7JngJ9s+z4XuSlYvnxhLuc58cA1WqGDQPzuG8I+sPm1/p0CdgpzVTaKW408k5DNn8w==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.11.2"
|
||||||
|
global "^4.4.0"
|
||||||
|
|
||||||
nan@^2.12.1:
|
nan@^2.12.1:
|
||||||
version "2.14.0"
|
version "2.14.0"
|
||||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
|
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
|
||||||
|
@ -17237,7 +17269,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@^7.13.3:
|
"video.js@^6 || ^7", "video.js@^6.0.1 || ^7", video.js@^7.0.0:
|
||||||
version "7.15.4"
|
version "7.15.4"
|
||||||
resolved "https://registry.yarnpkg.com/video.js/-/video.js-7.15.4.tgz#0f96ef138035138cb30bf00a989b6174f0d16bac"
|
resolved "https://registry.yarnpkg.com/video.js/-/video.js-7.15.4.tgz#0f96ef138035138cb30bf00a989b6174f0d16bac"
|
||||||
integrity sha512-hghxkgptLUvfkpktB4wxcIVF3VpY/hVsMkrjHSv0jpj1bW9Jplzdt8IgpTm9YhlB1KYAp07syVQeZcBFUBwhkw==
|
integrity sha512-hghxkgptLUvfkpktB4wxcIVF3VpY/hVsMkrjHSv0jpj1bW9Jplzdt8IgpTm9YhlB1KYAp07syVQeZcBFUBwhkw==
|
||||||
|
@ -17256,6 +17288,25 @@ vfile@^2.0.0:
|
||||||
videojs-font "3.2.0"
|
videojs-font "3.2.0"
|
||||||
videojs-vtt.js "^0.15.3"
|
videojs-vtt.js "^0.15.3"
|
||||||
|
|
||||||
|
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==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.12.5"
|
||||||
|
"@videojs/http-streaming" "2.13.1"
|
||||||
|
"@videojs/vhs-utils" "^3.0.4"
|
||||||
|
"@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.21.0"
|
||||||
|
mux.js "6.0.1"
|
||||||
|
safe-json-parse "4.0.0"
|
||||||
|
videojs-font "3.2.0"
|
||||||
|
videojs-vtt.js "^0.15.3"
|
||||||
|
|
||||||
videojs-contrib-ads@^6.6.5, videojs-contrib-ads@^6.7.0, videojs-contrib-ads@^6.9.0:
|
videojs-contrib-ads@^6.6.5, videojs-contrib-ads@^6.7.0, videojs-contrib-ads@^6.9.0:
|
||||||
version "6.9.0"
|
version "6.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/videojs-contrib-ads/-/videojs-contrib-ads-6.9.0.tgz#c792d6fda77254b277545cc3222352fc653b5833"
|
resolved "https://registry.yarnpkg.com/videojs-contrib-ads/-/videojs-contrib-ads-6.9.0.tgz#c792d6fda77254b277545cc3222352fc653b5833"
|
||||||
|
|
Loading…
Reference in a new issue