diff --git a/src/renderer/component/fileCard/view.jsx b/src/renderer/component/fileCard/view.jsx index fdc070cf9..e38bf06c2 100644 --- a/src/renderer/component/fileCard/view.jsx +++ b/src/renderer/component/fileCard/view.jsx @@ -87,7 +87,8 @@ class FileCard extends React.PureComponent {
- {isRewardContent && }{' '} + {' '} + {isRewardContent && }{' '} {fileInfo && } diff --git a/src/renderer/component/fileDownloadLink/index.js b/src/renderer/component/fileDownloadLink/index.js index 4b5ad978c..e13b4b3d4 100644 --- a/src/renderer/component/fileDownloadLink/index.js +++ b/src/renderer/component/fileDownloadLink/index.js @@ -9,7 +9,7 @@ import { makeSelectCostInfoForUri } from 'redux/selectors/cost_info'; import { doFetchAvailability } from 'redux/actions/availability'; import { doOpenFileInShell } from 'redux/actions/file_info'; import { doPurchaseUri, doStartDownload } from 'redux/actions/content'; -import { setVideoPause } from 'redux/actions/video'; +import { doPause } from 'redux/actions/media'; import FileDownloadLink from './view'; const select = (state, props) => ({ @@ -25,7 +25,7 @@ const perform = dispatch => ({ openInShell: path => dispatch(doOpenFileInShell(path)), purchaseUri: uri => dispatch(doPurchaseUri(uri)), restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)), - setVideoPause: val => dispatch(setVideoPause(val)), + doPause: () => dispatch(doPause()), }); export default connect(select, perform)(FileDownloadLink); diff --git a/src/renderer/component/fileDownloadLink/view.jsx b/src/renderer/component/fileDownloadLink/view.jsx index 4595b36d9..57818da51 100644 --- a/src/renderer/component/fileDownloadLink/view.jsx +++ b/src/renderer/component/fileDownloadLink/view.jsx @@ -43,12 +43,12 @@ class FileDownloadLink extends React.PureComponent { purchaseUri, costInfo, loading, - setVideoPause, + doPause, } = this.props; const openFile = () => { openInShell(fileInfo.download_path); - setVideoPause(true); + doPause(); }; if (loading || downloading) { diff --git a/src/renderer/component/video/index.js b/src/renderer/component/video/index.js index 38046f2f8..8c0a91289 100644 --- a/src/renderer/component/video/index.js +++ b/src/renderer/component/video/index.js @@ -1,22 +1,30 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { doChangeVolume } from 'redux/actions/app'; -import { selectVolume } from 'redux/selectors/app'; -import { doPlayUri, doSetPlayingUri } from 'redux/actions/content'; -import { makeSelectMetadataForUri, makeSelectContentTypeForUri } from 'redux/selectors/claims'; -import { setVideoPause } from 'redux/actions/video'; +import React from "react"; +import { connect } from "react-redux"; +import { doChangeVolume } from "redux/actions/app"; +import { selectVolume } from "redux/selectors/app"; +import { doPlayUri, doSetPlayingUri } from "redux/actions/content"; +import { doPlay, doPause, savePosition } from "redux/actions/media"; +import { + makeSelectMetadataForUri, + makeSelectContentTypeForUri, +} from "redux/selectors/claims"; import { makeSelectFileInfoForUri, makeSelectLoadingForUri, makeSelectDownloadingForUri, -} from 'redux/selectors/file_info'; -import { makeSelectCostInfoForUri } from 'redux/selectors/cost_info'; -import { selectShowNsfw } from 'redux/selectors/settings'; -import { selectVideoPause } from 'redux/selectors/video'; -import Video from './view'; -import { selectPlayingUri } from 'redux/selectors/content'; +} from "redux/selectors/file_info"; +import { makeSelectCostInfoForUri } from "redux/selectors/cost_info"; +import { selectShowNsfw } from "redux/selectors/settings"; +import { + selectMediaPaused, + makeSelectMediaPositionForUri, +} from "redux/selectors/media"; +import Video from "./view"; +import { selectPlayingUri } from "redux/selectors/content"; +import { makeSelectClaimForUri } from "redux/selectors/claims"; const select = (state, props) => ({ + claim: makeSelectClaimForUri(props.uri)(state), costInfo: makeSelectCostInfoForUri(props.uri)(state), fileInfo: makeSelectFileInfoForUri(props.uri)(state), metadata: makeSelectMetadataForUri(props.uri)(state), @@ -26,14 +34,18 @@ const select = (state, props) => ({ playingUri: selectPlayingUri(state), contentType: makeSelectContentTypeForUri(props.uri)(state), volume: selectVolume(state), - videoPause: selectVideoPause(state), + mediaPaused: selectMediaPaused(state), + mediaPosition: makeSelectMediaPositionForUri(props.uri)(state), }); const perform = dispatch => ({ play: uri => dispatch(doPlayUri(uri)), cancelPlay: () => dispatch(doSetPlayingUri(null)), changeVolume: volume => dispatch(doChangeVolume(volume)), - setVideoPause: val => dispatch(setVideoPause(val)), + doPlay: () => dispatch(doPlay()), + doPause: () => dispatch(doPause()), + savePosition: (claimId, position) => + dispatch(savePosition(claimId, position)), }); export default connect(select, perform)(Video); diff --git a/src/renderer/component/video/internal/player.jsx b/src/renderer/component/video/internal/player.jsx index 7572243c8..e938a856f 100644 --- a/src/renderer/component/video/internal/player.jsx +++ b/src/renderer/component/video/internal/player.jsx @@ -21,16 +21,24 @@ class VideoPlayer extends React.PureComponent { this.togglePlayListener = this.togglePlay.bind(this); } - componentWillReceiveProps(nextProps) { - if (nextProps.videoPause) { - this.refs.media.children[0].pause(); - this.props.setVideoPause(false); - } + componentWillReceiveProps(next) { + const el = this.refs.media.children[0]; + if (!this.props.paused && next.paused && !el.paused) el.pause(); } componentDidMount() { const container = this.refs.media; - const { contentType, downloadPath, mediaType, changeVolume, volume } = this.props; + const { + contentType, + downloadPath, + mediaType, + changeVolume, + volume, + position, + claim, + uri, + } = this.props; + const loadedMetadata = e => { this.setState({ hasMetadata: true, startedPlaying: true }); this.refs.media.children[0].play(); @@ -61,6 +69,12 @@ class VideoPlayer extends React.PureComponent { document.addEventListener('keydown', this.togglePlayListener); const mediaElement = this.refs.media.children[0]; if (mediaElement) { + mediaElement.currentTime = position || 0; + mediaElement.addEventListener('play', () => this.props.doPlay()); + mediaElement.addEventListener('pause', () => this.props.doPause()); + mediaElement.addEventListener('timeupdate', () => + this.props.savePosition(claim.claim_id, mediaElement.currentTime) + ); mediaElement.addEventListener('click', this.togglePlayListener); mediaElement.addEventListener('loadedmetadata', loadedMetadata.bind(this), { once: true, @@ -79,6 +93,7 @@ class VideoPlayer extends React.PureComponent { if (mediaElement) { mediaElement.removeEventListener('click', this.togglePlayListener); } + this.props.doPause(); } renderAudio(container, autoplay) { diff --git a/src/renderer/component/video/view.jsx b/src/renderer/component/video/view.jsx index 1edd40c7d..9a1afe7e4 100644 --- a/src/renderer/component/video/view.jsx +++ b/src/renderer/component/video/view.jsx @@ -51,9 +51,13 @@ class Video extends React.PureComponent { contentType, changeVolume, volume, + claim, uri, - videoPause, - setVideoPause, + doPlay, + doPause, + savePosition, + mediaPaused, + mediaPosition, } = this.props; const isPlaying = playingUri === uri; @@ -103,8 +107,13 @@ class Video extends React.PureComponent { downloadCompleted={fileInfo.completed} changeVolume={changeVolume} volume={volume} - videoPause={videoPause} - setVideoPause={setVideoPause} + doPlay={doPlay} + doPause={doPause} + savePosition={savePosition} + claim={claim} + uri={uri} + paused={mediaPaused} + position={mediaPosition} /> ))} {!isPlaying && ( diff --git a/src/renderer/constants/action_types.js b/src/renderer/constants/action_types.js index 01c5a4c5d..3c828f5cb 100644 --- a/src/renderer/constants/action_types.js +++ b/src/renderer/constants/action_types.js @@ -157,3 +157,8 @@ export const HAS_FETCHED_SUBSCRIPTIONS = 'HAS_FETCHED_SUBSCRIPTIONS'; // Video controls export const SET_VIDEO_PAUSE = 'SET_VIDEO_PAUSE'; + +// Media controls +export const MEDIA_PLAY = 'MEDIA_PLAY'; +export const MEDIA_PAUSE = 'MEDIA_PAUSE'; +export const MEDIA_POSITION = 'MEDIA_POSITION'; diff --git a/src/renderer/redux/actions/content.js b/src/renderer/redux/actions/content.js index a5df999ed..7309f1c13 100644 --- a/src/renderer/redux/actions/content.js +++ b/src/renderer/redux/actions/content.js @@ -279,7 +279,9 @@ export function doLoadVideo(uri) { }); dispatch( doAlertError( - `Failed to download ${uri}, please try again. If this problem persists, visit https://lbry.io/faq/support for support.` + `Failed to download ${ + uri + }, please try again. If this problem persists, visit https://lbry.io/faq/support for support.` ) ); }); diff --git a/src/renderer/redux/actions/media.js b/src/renderer/redux/actions/media.js new file mode 100644 index 000000000..d3076ed75 --- /dev/null +++ b/src/renderer/redux/actions/media.js @@ -0,0 +1,30 @@ +// @flow +import * as actions from "constants/action_types"; +import type { Action, Dispatch } from "redux/reducers/media"; +import lbry from "lbry"; +import { makeSelectClaimForUri } from "redux/selectors/claims"; + +export const doPlay = () => (dispatch: Dispatch) => + dispatch({ + type: actions.MEDIA_PLAY, + }); + +export const doPause = () => (dispatch: Dispatch) => + dispatch({ + type: actions.MEDIA_PAUSE, + }); + +export function savePosition(claimId: String, position: Number) { + return function(dispatch: Dispatch, getState: Function) { + const state = getState(); + const claim = state.claims.byId[claimId]; + const outpoint = `${claim.txid}:${claim.nout}`; + dispatch({ + type: actions.MEDIA_POSITION, + data: { + outpoint, + position, + }, + }); + }; +} diff --git a/src/renderer/redux/reducers/media.js b/src/renderer/redux/reducers/media.js new file mode 100644 index 000000000..7e3bb9591 --- /dev/null +++ b/src/renderer/redux/reducers/media.js @@ -0,0 +1,41 @@ +// @flow +import * as actions from "constants/action_types"; +import { handleActions } from "util/redux-utils"; + +export type MediaState = { + paused: Boolean, + positions: { + [string]: number, + }, +}; + +export type Action = any; +export type Dispatch = (action: Action) => any; + +const defaultState = { paused: true, positions: {} }; + +export default handleActions( + { + [actions.MEDIA_PLAY]: (state: MediaState, action: Action) => ({ + ...state, + paused: false, + }), + + [actions.MEDIA_PAUSE]: (state: MediaState, action: Action) => ({ + ...state, + paused: true, + }), + + [actions.MEDIA_POSITION]: (state: MediaState, action: Action) => { + const { outpoint, position } = action.data; + return { + ...state, + positions: { + ...state.positions, + [outpoint]: position, + }, + }; + }, + }, + defaultState +); diff --git a/src/renderer/redux/selectors/media.js b/src/renderer/redux/selectors/media.js new file mode 100644 index 000000000..9ef16abaf --- /dev/null +++ b/src/renderer/redux/selectors/media.js @@ -0,0 +1,17 @@ +import * as settings from "constants/settings"; +import { createSelector } from "reselect"; +import lbryuri from "lbryuri"; +import { makeSelectClaimForUri } from "redux/selectors/claims"; + +const _selectState = state => state.media || {}; + +export const selectMediaPaused = createSelector( + _selectState, + state => state.paused +); + +export const makeSelectMediaPositionForUri = uri => + createSelector(_selectState, makeSelectClaimForUri(uri), (state, claim) => { + const outpoint = `${claim.txid}:${claim.nout}`; + return state.positions[outpoint] || null; + }); diff --git a/src/renderer/store.js b/src/renderer/store.js index 74cbadd82..ca6db3afd 100644 --- a/src/renderer/store.js +++ b/src/renderer/store.js @@ -13,7 +13,7 @@ import userReducer from 'redux/reducers/user'; import walletReducer from 'redux/reducers/wallet'; import shapeShiftReducer from 'redux/reducers/shape_shift'; import subscriptionsReducer from 'redux/reducers/subscriptions'; -import videoReducer from 'redux/reducers/video'; +import mediaReducer from 'redux/reducers/media'; import { persistStore, autoRehydrate } from 'redux-persist'; import createCompressor from 'redux-persist-transform-compress'; import createFilter from 'redux-persist-transform-filter'; @@ -64,7 +64,7 @@ const reducers = combineReducers({ user: userReducer, shapeShift: shapeShiftReducer, subscriptions: subscriptionsReducer, - video: videoReducer, + media: mediaReducer, }); const bulkThunk = createBulkThunkMiddleware();