diff --git a/ui/component/viewers/videoViewer/index.js b/ui/component/viewers/videoViewer/index.js index 4f7d98193..df6e352c7 100644 --- a/ui/component/viewers/videoViewer/index.js +++ b/ui/component/viewers/videoViewer/index.js @@ -1,9 +1,20 @@ import { connect } from 'react-redux'; -import { makeSelectClaimForUri, makeSelectFileInfoForUri, makeSelectThumbnailForUri, SETTINGS } from 'lbry-redux'; +import { + makeSelectClaimForUri, + makeSelectFileInfoForUri, + makeSelectThumbnailForUri, + SETTINGS, + COLLECTIONS_CONSTS, + makeSelectNextUrlForCollectionAndUrl, +} from 'lbry-redux'; import { doChangeVolume, doChangeMute, doAnalyticsView, doAnalyticsBuffer } from 'redux/actions/app'; import { selectVolume, selectMute } from 'redux/selectors/app'; -import { savePosition, clearPosition } from 'redux/actions/content'; -import { makeSelectContentPositionForUri } from 'redux/selectors/content'; +import { savePosition, clearPosition, doSetPlayingUri, doPlayUri } from 'redux/actions/content'; +import { + makeSelectContentPositionForUri, + selectPlayingUri, + makeSelectIsPlayerFloating, +} from 'redux/selectors/content'; import VideoViewer from './view'; import { withRouter } from 'react-router'; import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards'; @@ -18,6 +29,13 @@ const select = (state, props) => { // 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(props.uri)(state); const userId = selectUser(state) && selectUser(state).id; + const playingUri = selectPlayingUri(state); + const collectionId = urlParams.get(COLLECTIONS_CONSTS.COLLECTION_ID) || (playingUri && playingUri.collectionId); + + let playNextUri; + if (collectionId) { + playNextUri = makeSelectNextUrlForCollectionAndUrl(collectionId, props.uri)(state); + } return { autoplayIfEmbedded: Boolean(autoplay), @@ -34,6 +52,9 @@ const select = (state, props) => { userId: userId, shareTelemetry: IS_WEB || selectDaemonSettings(state).share_usage_data, videoTheaterMode: makeSelectClientSetting(SETTINGS.VIDEO_THEATER_MODE)(state), + isFloating: makeSelectIsPlayerFloating(props.location)(state), + collectionId, + playNextUri, }; }; @@ -47,6 +68,8 @@ const perform = (dispatch) => ({ claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()), toggleVideoTheaterMode: () => dispatch(toggleVideoTheaterMode()), setVideoPlaybackRate: (rate) => dispatch(doSetClientSetting(SETTINGS.VIDEO_PLAYBACK_RATE, rate)), + doSetPlayingUri: (uri, collectionId) => dispatch(doSetPlayingUri({ uri, collectionId })), + doPlayUri: (uri) => dispatch(doPlayUri(uri)), }); export default withRouter(connect(select, perform)(VideoViewer)); diff --git a/ui/component/viewers/videoViewer/internal/videojs.jsx b/ui/component/viewers/videoViewer/internal/videojs.jsx index bf9a716aa..8e1c2b942 100644 --- a/ui/component/viewers/videoViewer/internal/videojs.jsx +++ b/ui/component/viewers/videoViewer/internal/videojs.jsx @@ -61,6 +61,7 @@ type Props = { shareTelemetry: boolean, replay: boolean, videoTheaterMode: boolean, + setStartPlayNext: (boolean) => void, }; // type VideoJSOptions = { @@ -105,6 +106,8 @@ const SMALL_J_KEYCODE = 74; const SMALL_K_KEYCODE = 75; const SMALL_L_KEYCODE = 76; +const N_KEYCODE = 78; + const ZERO_KEYCODE = 48; const ONE_KEYCODE = 49; const TWO_KEYCODE = 50; @@ -212,6 +215,7 @@ export default React.memo(function VideoJs(props: Props) { shareTelemetry, replay, videoTheaterMode, + setStartPlayNext, } = props; const [reload, setReload] = useState('initial'); @@ -399,6 +403,7 @@ export default React.memo(function VideoJs(props: Props) { if (e.altKey || e.ctrlKey || e.metaKey || !e.shiftKey) return; if (e.keyCode === PERIOD_KEYCODE) changePlaybackSpeed(true); if (e.keyCode === COMMA_KEYCODE) changePlaybackSpeed(false); + if (e.keyCode === N_KEYCODE) setStartPlayNext(true); } function handleSingleKeyActions(e: KeyboardEvent) { diff --git a/ui/component/viewers/videoViewer/view.jsx b/ui/component/viewers/videoViewer/view.jsx index 601083ca5..711874f04 100644 --- a/ui/component/viewers/videoViewer/view.jsx +++ b/ui/component/viewers/videoViewer/view.jsx @@ -16,12 +16,15 @@ import FileViewerEmbeddedEnded from 'web/component/fileViewerEmbeddedEnded'; import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle'; import LoadingScreen from 'component/common/loading-screen'; import { addTheaterModeButton } from './internal/theater-mode'; +import { addPlayNextButton } from './internal/play-next'; import { useGetAds } from 'effects/use-get-ads'; import Button from 'component/button'; import I18nMessage from 'component/i18nMessage'; import { useHistory } from 'react-router'; import { getAllIds } from 'util/buildHomepage'; import type { HomepageCat } from 'util/buildHomepage'; +import { formatLbryUrlForWeb } from 'util/url'; +import { COLLECTIONS_CONSTS } from 'lbry-redux'; const PLAY_TIMEOUT_ERROR = 'play_timeout_error'; const PLAY_TIMEOUT_LIMIT = 2000; @@ -48,11 +51,16 @@ type Props = { clearPosition: (string) => void, toggleVideoTheaterMode: () => void, setVideoPlaybackRate: (number) => void, + doSetPlayingUri: (string, string) => void, + doPlayUri: (string) => void, + playNextUri: string, authenticated: boolean, userId: number, homepageData?: { [string]: HomepageCat }, shareTelemetry: boolean, videoTheaterMode: boolean, + collectionId: string, + isFloating: boolean, }; /* @@ -83,11 +91,16 @@ function VideoViewer(props: Props) { desktopPlayStartTime, toggleVideoTheaterMode, setVideoPlaybackRate, + doSetPlayingUri, + doPlayUri, + playNextUri, homepageData, authenticated, userId, shareTelemetry, videoTheaterMode, + collectionId, + isFloating, } = props; const adApprovedChannelIds = homepageData ? getAllIds(homepageData) : []; @@ -97,6 +110,7 @@ function VideoViewer(props: Props) { const forcePlayer = FORCE_CONTENT_TYPE_PLAYER.includes(contentType); const { location: { pathname }, + push, } = useHistory(); const [isPlaying, setIsPlaying] = useState(false); const [showAutoplayCountdown, setShowAutoplayCountdown] = useState(false); @@ -111,6 +125,7 @@ function VideoViewer(props: Props) { breaks because some browsers (e.g. Firefox) block autoplay but leave the player.play Promise pending */ const [isLoading, setIsLoading] = useState(false); const [replay, setReplay] = useState(false); + const [startPlayNext, setStartPlayNext] = useState(false); // force everything to recent when URI changes, can cause weird corner cases otherwise (e.g. navigate while autoplay is true) useEffect(() => { @@ -129,6 +144,29 @@ function VideoViewer(props: Props) { }; }, [embedded, videoPlaybackRate]); + let navigateUrl; + if (playNextUri) { + navigateUrl = formatLbryUrlForWeb(playNextUri); + if (collectionId) { + const collectionParams = new URLSearchParams(); + collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId); + navigateUrl = navigateUrl + `?` + collectionParams.toString(); + } + } + + useEffect(() => { + if (startPlayNext) { + if (!isFloating && navigateUrl) { + push(navigateUrl); + } + if (playNextUri) { + doSetPlayingUri(playNextUri, collectionId); + doPlayUri(playNextUri); + } + setStartPlayNext(false); + } + }, [isFloating, navigateUrl, push, doSetPlayingUri, playNextUri, doPlayUri, startPlayNext, collectionId]); + function doTrackingBuffered(e: Event, data: any) { fetch(source, { method: 'HEAD', cache: 'no-store' }).then((response) => { data.playerPoweredBy = response.headers.get('x-powered-by'); @@ -213,6 +251,7 @@ function VideoViewer(props: Props) { player.volume(volume); player.playbackRate(videoPlaybackRate); addTheaterModeButton(player, toggleVideoTheaterMode); + if (collectionId) addPlayNextButton(player, () => setStartPlayNext(true)); } const shouldPlay = !embedded || autoplayIfEmbedded; @@ -342,6 +381,7 @@ function VideoViewer(props: Props) { shareTelemetry={shareTelemetry} replay={replay} videoTheaterMode={videoTheaterMode} + setStartPlayNext={setStartPlayNext} /> )}