diff --git a/ui/component/viewers/videoViewer/internal/videojs-events.jsx b/ui/component/viewers/videoViewer/internal/videojs-events.jsx index 9be86a73b..efa15cbe0 100644 --- a/ui/component/viewers/videoViewer/internal/videojs-events.jsx +++ b/ui/component/viewers/videoViewer/internal/videojs-events.jsx @@ -307,7 +307,7 @@ const VideoJsEvents = ({ }); // player.on('ended', onEnded); - if (isLivestreamClaim) { + if (isLivestreamClaim && player) { player.liveTracker.on('liveedgechange', async () => { // Only respond to when we fall behind if (player.liveTracker.atLiveEdge()) return; diff --git a/ui/component/viewers/videoViewer/internal/videojs.jsx b/ui/component/viewers/videoViewer/internal/videojs.jsx index d523386df..964169c98 100644 --- a/ui/component/viewers/videoViewer/internal/videojs.jsx +++ b/ui/component/viewers/videoViewer/internal/videojs.jsx @@ -207,12 +207,12 @@ export default React.memo(function VideoJs(props: Props) { responsive: true, controls: true, html5: { - hls: { + vhs: { overrideNative: !videojs.browser.IS_ANY_SAFARI, allowSeeksWithinUnsafeLiveWindow: true, enableLowInitialPlaylist: false, handlePartialData: true, - smoothQualityChange: true, + fastQualityChange: true, }, }, liveTracker: { diff --git a/ui/constants/livestream.js b/ui/constants/livestream.js index e0cf023d2..e16702db5 100644 --- a/ui/constants/livestream.js +++ b/ui/constants/livestream.js @@ -15,7 +15,8 @@ export const LIVESTREAM_KILL = 'https://api.stream.odysee.com/stream/kill'; export const MAX_LIVESTREAM_COMMENTS = 50; -export const LIVESTREAM_STATUS_CHECK_INTERVAL = 30 * 1000; +export const LIVESTREAM_STATUS_CHECK_INTERVAL = 45 * 1000; +export const LIVESTREAM_STATUS_CHECK_INTERVAL_SOON = 15 * 1000; export const LIVESTREAM_STARTS_SOON_BUFFER = 15; export const LIVESTREAM_STARTED_RECENTLY_BUFFER = 15; export const LIVESTREAM_UPCOMING_BUFFER = 35; diff --git a/ui/page/livestream/view.jsx b/ui/page/livestream/view.jsx index 5586276c0..fc719f727 100644 --- a/ui/page/livestream/view.jsx +++ b/ui/page/livestream/view.jsx @@ -3,6 +3,7 @@ import { formatLbryChannelName } from 'util/url'; import { lazyImport } from 'util/lazyImport'; import { LIVESTREAM_STATUS_CHECK_INTERVAL, + LIVESTREAM_STATUS_CHECK_INTERVAL_SOON, LIVESTREAM_STARTS_SOON_BUFFER, LIVESTREAM_STARTED_RECENTLY_BUFFER, } from 'constants/livestream'; @@ -98,16 +99,27 @@ export default function LivestreamPage(props: Props) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [claim, uri, doCommentSocketConnect, doCommentSocketDisconnect]); - // Find out current channels status + active live claim every 30 seconds + const claimReleaseStartingSoonStatic = () => + release.isBetween(moment(), moment().add(LIVESTREAM_STARTS_SOON_BUFFER, 'minutes')); + + const claimReleaseStartedRecentlyStatic = () => + release.isBetween(moment().subtract(LIVESTREAM_STARTED_RECENTLY_BUFFER, 'minutes'), moment()); + + // Find out current channels status + active live claim every 30 seconds (or 15 if not live) + const fasterPoll = !isCurrentClaimLive && (claimReleaseStartingSoonStatic() || claimReleaseStartedRecentlyStatic()); + React.useEffect(() => { const fetch = () => doFetchChannelLiveStatus(livestreamChannelId); fetch(); - const intervalId = setInterval(fetch, LIVESTREAM_STATUS_CHECK_INTERVAL); + const intervalId = setInterval( + fetch, + fasterPoll ? LIVESTREAM_STATUS_CHECK_INTERVAL_SOON : LIVESTREAM_STATUS_CHECK_INTERVAL + ); return () => clearInterval(intervalId); - }, [livestreamChannelId, doFetchChannelLiveStatus]); + }, [livestreamChannelId, doFetchChannelLiveStatus, fasterPoll]); React.useEffect(() => { setActiveStreamUri(!isCurrentClaimLive && isChannelBroadcasting ? activeLivestreamForChannel.claimUri : false); @@ -117,7 +129,6 @@ export default function LivestreamPage(props: Props) { if (!isInitialized) return; const claimReleaseInFuture = () => release.isAfter(); - const claimReleaseInPast = () => release.isBefore(); const claimReleaseStartingSoon = () => @@ -127,7 +138,9 @@ export default function LivestreamPage(props: Props) { release.isBetween(moment().subtract(LIVESTREAM_STARTED_RECENTLY_BUFFER, 'minutes'), moment()); const checkShowLivestream = () => - isChannelBroadcasting && isCurrentClaimLive && (claimReleaseInPast() || claimReleaseStartingSoon()); + isChannelBroadcasting && + isCurrentClaimLive && + (claimReleaseInPast() || claimReleaseStartingSoon() || claimReleaseInFuture()); const checkShowScheduledInfo = () => (!isChannelBroadcasting && (claimReleaseInFuture() || claimReleaseStartedRecently())) || @@ -144,7 +157,7 @@ export default function LivestreamPage(props: Props) { }; calculateStreamReleaseState(); - const intervalId = setInterval(calculateStreamReleaseState, 1000); + const intervalId = setInterval(calculateStreamReleaseState, 5000); if (isCurrentClaimLive && claimReleaseInPast() && isChannelBroadcasting === true) { clearInterval(intervalId); diff --git a/ui/redux/actions/livestream.js b/ui/redux/actions/livestream.js index 2cf9a899f..e8d444e32 100644 --- a/ui/redux/actions/livestream.js +++ b/ui/redux/actions/livestream.js @@ -9,6 +9,10 @@ import { filterUpcomingLiveStreamClaims, } from 'util/livestream'; import moment from 'moment'; +import { isLocalStorageAvailable } from 'util/storage'; +import { isEmpty } from 'util/object'; + +const localStorageAvailable = isLocalStorageAvailable(); const FETCH_ACTIVE_LIVESTREAMS_MIN_INTERVAL_MS = 5 * 60 * 1000; @@ -100,26 +104,35 @@ const findActiveStreams = async ( // Find the first upcoming claim (if one exists) for each channel that's actively broadcasting a stream. const upcomingClaims = await dispatch(fetchUpcomingLivestreamClaims(channelIDs, lang)); - // Filter out any of those claims that aren't scheduled to start within the configured "soon" buffer time (ex. next 5 min). + // Filter out any of those claims that aren't scheduled to start within the configured "soon" buffer time (ex. next 15 min). const startingSoonClaims = filterUpcomingLiveStreamClaims(upcomingClaims); // Reduce the claim list to one "live" claim per channel, based on how close each claim's // release time is to the time the channels stream started. - const allClaims = Object.assign({}, mostRecentClaims, startingSoonClaims); + const allClaims = Object.assign( + {}, + mostRecentClaims, + !isEmpty(startingSoonClaims) ? startingSoonClaims : upcomingClaims + ); return determineLiveClaim(allClaims, liveChannels); }; export const doFetchChannelLiveStatus = (channelId: string) => async (dispatch: Dispatch) => { - const statusForId = `live-status-${channelId}`; - // const localStatus = window.localStorage.getItem(statusForId); + const statusForId = `channel-live-status`; + const localStatus = localStorageAvailable && window.localStorage.getItem(statusForId); try { const { channelStatus, channelData } = await fetchLiveChannel(channelId); + // store live state locally, and force 2 non-live statuses before returninig NOT LIVE. This allows for the stream to finish before disposing player. + if (localStatus === LiveStatus.LIVE && channelStatus === LiveStatus.NOT_LIVE) { + localStorageAvailable && window.localStorage.removeItem(statusForId); + return; + } - if (channelStatus === LiveStatus.NOT_LIVE) { + if (channelStatus === LiveStatus.NOT_LIVE && !localStatus) { dispatch({ type: ACTIONS.REMOVE_CHANNEL_FROM_ACTIVE_LIVESTREAMS, data: { channelId } }); - window.localStorage.removeItem(statusForId); + localStorageAvailable && window.localStorage.removeItem(statusForId); return; } @@ -136,10 +149,10 @@ export const doFetchChannelLiveStatus = (channelId: string) => async (dispatch: dispatch({ type: ACTIONS.ADD_CHANNEL_TO_ACTIVE_LIVESTREAMS, data: { ...channelData } }); } - window.localStorage.setItem(statusForId, channelStatus); + localStorageAvailable && window.localStorage.setItem(statusForId, channelStatus); } catch (err) { dispatch({ type: ACTIONS.REMOVE_CHANNEL_FROM_ACTIVE_LIVESTREAMS, data: { channelId } }); - window.localStorage.removeItem(statusForId); + localStorageAvailable && window.localStorage.removeItem(statusForId); } };