Add videos to be played across all pages. #1523

Closed
dan1d wants to merge 13 commits from video-overlay-new into master
23 changed files with 391 additions and 25 deletions

View file

@ -0,0 +1,12 @@
import { connect } from 'react-redux';
import { selectShowOverlay } from 'redux/selectors/media';
import Overlay from './view';
const select = state => ({
showOverlay: selectShowOverlay(state),
});
export default connect(
select,
null
)(Overlay);

View file

@ -0,0 +1,15 @@
// @flow
import React from 'react';
type Props = {
children: ?React.node,
};
class Overlay extends React.PureComponent<Props> {
render() {
const { children } = this.props;
return <div className="overlay">{children}</div>;
}
}
export default Overlay;

View file

@ -2,8 +2,8 @@ import { connect } from 'react-redux';
import * as settings from 'constants/settings';
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 { doPlayUri, doSetPlayingUri, doLoadVideo } from 'redux/actions/content';
import { doPlay, doPause, savePosition, doHideOverlay, doShowOverlay } from 'redux/actions/media';
import {
makeSelectMetadataForUri,
makeSelectContentTypeForUri,
@ -15,7 +15,11 @@ import {
selectSearchBarFocused,
} from 'lbry-redux';
import { makeSelectClientSetting, selectShowNsfw } from 'redux/selectors/settings';
import { selectMediaPaused, makeSelectMediaPositionForUri } from 'redux/selectors/media';
import {
selectMediaPaused,
makeSelectMediaPositionForUri,
selectShowOverlay,
} from 'redux/selectors/media';
import { selectPlayingUri } from 'redux/selectors/content';
import Video from './view';
@ -34,14 +38,20 @@ const select = (state, props) => ({
mediaPosition: makeSelectMediaPositionForUri(props.uri)(state),
autoplay: makeSelectClientSetting(settings.AUTOPLAY)(state),
searchBarFocused: selectSearchBarFocused(state),
showOverlay: selectShowOverlay(state),
hiddenControls: props.hiddenControls,
fromOverlay: props.fromOverlay,
});
const perform = dispatch => ({
play: uri => dispatch(doPlayUri(uri)),
load: uri => dispatch(doLoadVideo(uri)),
cancelPlay: () => dispatch(doSetPlayingUri(null)),
changeVolume: volume => dispatch(doChangeVolume(volume)),
doPlay: () => dispatch(doPlay()),
doPause: () => dispatch(doPause()),
doShowOverlay: () => dispatch(doShowOverlay()),
doHideOverlay: () => dispatch(doHideOverlay()),
savePosition: (claimId, position) => dispatch(savePosition(claimId, position)),
});

View file

@ -22,14 +22,9 @@ class VideoPlayer extends React.PureComponent {
this.toggleFullScreenVideo = this.toggleFullScreen.bind(this);
}
componentWillReceiveProps(nextProps) {
const el = this.refs.media.children[0];
if (!this.props.paused && nextProps.paused && !el.paused) el.pause();
}
componentDidMount() {
const container = this.media;
const { contentType, changeVolume, volume, position, claim } = this.props;
const { contentType, changeVolume, volume, position, claim, hiddenControls } = this.props;
const loadedMetadata = () => {
this.setState({ hasMetadata: true, startedPlaying: true });
@ -48,6 +43,12 @@ class VideoPlayer extends React.PureComponent {
}
};
// Hide overlay video when the video ends only if its overlayed
const ended = () => {
this.props.doPause();
this.props.savePosition(claim.claim_id, 0);
};
// use renderAudio override for mp3
if (VideoPlayer.MP3_CONTENT_TYPES.indexOf(contentType) > -1) {
this.renderAudio(container, null, false);
@ -55,7 +56,7 @@ class VideoPlayer extends React.PureComponent {
player.append(
this.file(),
container,
{ autoplay: true, controls: true },
{ autoplay: true, controls: !hiddenControls },
renderMediaCallback.bind(this)
);
}
@ -79,16 +80,21 @@ class VideoPlayer extends React.PureComponent {
});
mediaElement.volume = volume;
mediaElement.addEventListener('dblclick', this.toggleFullScreenVideo);
mediaElement.addEventListener('ended', ended);
}
}
componentWillReceiveProps(next) {
const el = this.media.children[0];
if (!this.props.paused && next.paused && !el.paused) el.pause();
if (!this.props.paused && next.paused && !el.paused) {
el.pause();
} else if (this.props.paused && !next.paused && el.paused) {
el.play();
}
}
componentDidUpdate() {
const { contentType, downloadCompleted } = this.props;
const { contentType, downloadCompleted, hiddenControls } = this.props;
const { startedPlaying } = this.state;
if (this.playableType() && !startedPlaying && downloadCompleted) {
@ -99,7 +105,7 @@ class VideoPlayer extends React.PureComponent {
} else {
player.render(this.file(), container, {
autoplay: true,
controls: true,
controls: !hiddenControls,
});
}
}
@ -111,7 +117,7 @@ class VideoPlayer extends React.PureComponent {
if (mediaElement) {
mediaElement.removeEventListener('click', this.togglePlayListener);
}
this.props.doPause();
// this.props.doPause();
}
toggleFullScreen(event) {

View file

@ -6,11 +6,11 @@ import type { Claim } from 'types/claim';
import VideoPlayer from './internal/player';
import VideoPlayButton from './internal/play-button';
import LoadingScreen from './internal/loading-screen';
import ReactDOM from 'react-dom';
const SPACE_BAR_KEYCODE = 32;
type Props = {
cancelPlay: () => void,
fileInfo: {
outpoint: string,
file_name: string,
@ -34,12 +34,19 @@ type Props = {
doPlay: () => void,
doPause: () => void,
savePosition: (string, number) => void,
doShowOverlay: () => void,
doHideOverlay: () => void,
mediaPaused: boolean,
mediaPosition: ?number,
className: ?string,
obscureNsfw: boolean,
play: string => void,
searchBarFocused: boolean,
showOverlay: boolean,
hiddenControls: boolean,
fromOverlay: boolean,
overlayed: boolean,
fromOverlay: boolean,
};
class Video extends React.PureComponent<Props> {
@ -53,6 +60,11 @@ class Video extends React.PureComponent<Props> {
componentDidMount() {
this.handleAutoplay(this.props);
window.addEventListener('keydown', this.handleKeyDown);
const { showOverlay, doHideOverlay, uri, playingUri } = this.props;
if (showOverlay && uri === playingUri) {
doHideOverlay();
}
}
componentWillReceiveProps(nextProps: Props) {
@ -64,13 +76,53 @@ class Video extends React.PureComponent<Props> {
) {
this.handleAutoplay(nextProps);
}
if (nextProps.fromOverlay) {
this.moveVideoFromOverlayToNormal();
this.destroyVideoOnOverlay();
this.props.doHideOverlay();
}
}
componentWillUnmount() {
this.props.cancelPlay();
const { overlayed, doShowOverlay, mediaPaused } = this.props;
if (!overlayed && !mediaPaused) {
doShowOverlay();
this.moveVideoToOverlay();
}
window.removeEventListener('keydown', this.handleKeyDown);
}
moveVideoToOverlay() {
const topContainer = document.getElementById('video__overlay_id_top_container');
const container = document.getElementById('video__overlay_id');
const videoContainer = this.mediaContainer.media ? this.mediaContainer.media : document.getElementById('insert_video');
const video = videoContainer.getElementsByTagName('video')[0];
if (video) {
topContainer.classList.remove('hiddenContainer');
container.appendChild(video);
video.controls = false;
video.play();
}
}
moveVideoFromOverlayToNormal() {
const videoContainer = document.getElementById('video__overlay_id');
if (!videoContainer) return;
const video = videoContainer.getElementsByTagName('video')[0];
if (!video) return;
const filePageVideoContainer = document.getElementById('insert_video');
filePageVideoContainer.appendChild(video);
video.controls = true;
video.play();
}
destroyVideoOnOverlay() {
const topContainer = document.getElementById('video__overlay_id_top_container');
const videoContainer = document.getElementById('video__overlay_id');
topContainer.classList.add('hiddenContainer');
videoContainer.innerHTML = '';
}
handleKeyDown(event: SyntheticKeyboardEvent<*>) {
const { searchBarFocused } = this.props;
if (!searchBarFocused && event.keyCode === SPACE_BAR_KEYCODE) {
@ -100,9 +152,17 @@ class Video extends React.PureComponent<Props> {
}
playContent() {
const { play, uri } = this.props;
const { play, uri, playingUri, doHideOverlay } = this.props;
if (playingUri) {
if (playingUri === uri) {
this.moveVideoFromOverlayToNormal();
}
this.destroyVideoOnOverlay();
doHideOverlay();
} else {
play(uri);
}
}
render() {
const {
@ -123,6 +183,10 @@ class Video extends React.PureComponent<Props> {
mediaPosition,
className,
obscureNsfw,
hiddenControls,
doHideOverlay,
showOverlay,
fromOverlay,
} = this.props;
const isPlaying = playingUri === uri;
@ -147,6 +211,7 @@ class Video extends React.PureComponent<Props> {
const layoverStyle =
!shouldObscureNsfw && poster ? { backgroundImage: `url("${poster}")` } : {};
const commingFromOverlay = playingUri === uri;
return (
<div className={classnames('video', {}, className)}>
{isPlaying && (
@ -155,7 +220,7 @@ class Video extends React.PureComponent<Props> {
<div className={layoverClass} style={layoverStyle}>
<LoadingScreen status={loadStatusMessage} />
</div>
) : (
) : (commingFromOverlay && fromOverlay ? <div id="insert_video" ref={mediaContainer => this.mediaContainer = mediaContainer} /> :
<VideoPlayer
filename={fileInfo.file_name}
poster={poster}
@ -172,6 +237,9 @@ class Video extends React.PureComponent<Props> {
uri={uri}
paused={mediaPaused}
position={mediaPosition}
hiddenControls={hiddenControls}
doHideOverlay={doHideOverlay}
ref={mediaContainer => this.mediaContainer = mediaContainer }
/>
)}
</div>

View file

@ -0,0 +1,26 @@
import { connect } from 'react-redux';
import { selectPlayingUri } from 'redux/selectors/content';
import { doSetPlayingUri } from 'redux/actions/content';
import { doNavigate } from 'redux/actions/navigation';
import { doPlay, doPause, doHideOverlay } from 'redux/actions/media';
import { selectMediaPaused, selectShowOverlay } from 'redux/selectors/media';
import VideoOverlay from './view';
const select = state => ({
playingUri: selectPlayingUri(state),
mediaPaused: selectMediaPaused(state),
showOverlay: selectShowOverlay(state),
});
const perform = dispatch => ({
navigate: (path, params) => dispatch(doNavigate(path, params)),
doCancelPlay: () => dispatch(doSetPlayingUri(null)),
doHideOverlay: () => dispatch(doHideOverlay()),
doPlay: () => dispatch(doPlay()),
doPause: () => dispatch(doPause()),
});
export default connect(
select,
perform
)(VideoOverlay);

View file

@ -0,0 +1,92 @@
// @flow
import React from 'react';
import Video from 'component/video';
import Overlay from 'component/overlay';
import VideoOverlayHeader from 'component/videoOverlayHeader';
import Button from 'component/button';
import * as icons from 'constants/icons';
type Props = {
doCancelPlay: () => void,
doHideOverlay: () => void,
navigate: (string, ?{}) => void,
doPlay: () => void,
doPause: () => void,
playingUri: ?string,
mediaPaused: boolean,
showOverlay: boolean,
};
class VideoOverlay extends React.Component<Props> {
constructor() {
super();
(this: any).closeVideo = this.closeVideo.bind(this);
(this: any).returnToMedia = this.returnToMedia.bind(this);
}
closeVideo() {
const { doCancelPlay, doHideOverlay } = this.props;
doCancelPlay();
doHideOverlay();
this.destroyMediaPlayer();
}
returnToMedia() {
const { navigate, playingUri, doHideOverlay } = this.props;
doHideOverlay();
this.destroyMediaPlayer(false);
navigate('/show', { uri: playingUri, fromOverlay: true });
}
renderPlayOrPauseButton() {
const { mediaPaused, doPause, doPlay } = this.props;
if (mediaPaused) {
return <Button noPadding button="secondary" icon={icons.PLAY} onClick={() => this.getPlayer().play()} />;
}
return <Button noPadding button="secondary" icon={icons.PAUSE} onClick={() => this.getPlayer().pause()} />;
}
getPlayer() {
return document.getElementById('video__overlay_id').getElementsByTagName("video")[0];
}
destroyMediaPlayer(clearVideo = true){
const topContainer = document.getElementById('video__overlay_id_top_container')
const videoContainer = document.getElementById('video__overlay_id');
topContainer.classList.add('hiddenContainer');
if (clearVideo) videoContainer.innerHTML = '';
}
render() {
const { playingUri, showOverlay } = this.props;
return (
<Overlay>
{(showOverlay && <VideoOverlayHeader uri={playingUri} onClose={this.closeVideo} />)}
<div className="video__overlay">
{/* <Video className="content__embedded" uri={playingUri} overlayed hiddenControls /> */}
{/* <div id="asdf"></div> */}
<div className="video content__embedded hiddenContainer" id="video__overlay_id_top_container">
<div className="content__view">
<div className="content__view--container" id="video__overlay_id">
</div>
</div>
</div>
{(showOverlay && <div className="video__mask" id="video_mask">
{this.renderPlayOrPauseButton()}
<Button
noPadding
button="secondary"
icon={icons.MAXIMIZE}
onClick={() => this.returnToMedia()}
/>
</div>)}
</div>
</Overlay>
);
}
}
export default VideoOverlay;

View file

@ -0,0 +1,10 @@
import { connect } from 'react-redux';
import { makeSelectTitleForUri } from 'lbry-redux';
import VideoOverlayHeader from './view';
const select = (state, props) => ({
title: makeSelectTitleForUri(props.uri)(state),
onClose: props.onClose,
});
export default connect(select, null)(VideoOverlayHeader);

View file

@ -0,0 +1,31 @@
// @flow
import React from 'react';
import Button from 'component/button';
import * as icons from 'constants/icons';
import TruncatedText from 'component/common/truncated-text';
type Props = {
onClose: () => void,
title: string,
};
class VideoOverlayHeader extends React.Component<Props> {
render() {
const { onClose, title } = this.props;
return (
<header className="video_overlay__header">
<h4 className="overlay__title--small">
<TruncatedText lines={2}>{title}</TruncatedText>
</h4>
<Button
icon={icons.CLOSE}
onClick={() => {
onClose();
}}
/>
</header>
);
}
}
export default VideoOverlayHeader;

View file

@ -188,6 +188,9 @@ export const SET_VIDEO_PAUSE = 'SET_VIDEO_PAUSE';
export const MEDIA_PLAY = 'MEDIA_PLAY';
export const MEDIA_PAUSE = 'MEDIA_PAUSE';
export const MEDIA_POSITION = 'MEDIA_POSITION';
// Overlay Media
export const SHOW_OVERLAY_MEDIA = 'SHOW_OVERLAY_MEDIA';
export const HIDE_OVERLAY_MEDIA = 'HIDE_OVERLAY_MEDIA';
// Publishing
export const CLEAR_PUBLISH = 'CLEAR_PUBLISH';

View file

@ -25,4 +25,7 @@ export const CHECK = 'CheckCircle';
export const HEART = 'Heart';
export const UNLOCK = 'Unlock';
export const CHECK_SIMPLE = 'Check';
export const PLAY = 'Play';
export const MAXIMIZE = 'Maximize2';
export const PAUSE = 'Pause';
export const GLOBE = 'Globe';

View file

@ -16,6 +16,7 @@ import 'scss/all.scss';
import store from 'store';
import app from './app';
import analytics from './analytics';
import VideoOverlay from './component/videoOverlay/';
import doLogWarningConsoleMessage from './logWarningConsoleMessage';
const { autoUpdater } = remote.require('electron-updater');
@ -148,6 +149,7 @@ const init = () => {
<div>
<App />
<SnackBar />
<VideoOverlay />
</div>
</Provider>,
document.getElementById('app')

View file

@ -34,6 +34,7 @@ const select = (state, props) => ({
isPaused: selectMediaPaused(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
autoplay: makeSelectClientSetting(settings.AUTOPLAY)(state),
fromOverlay: Boolean(props.fromOverlay),
});
const perform = dispatch => ({

View file

@ -45,6 +45,7 @@ type Props = {
prepareEdit: ({}, string) => void,
setClientSetting: (string, boolean | string) => void,
checkSubscription: ({ channelName: string, uri: string }) => void,
fromOverlay: boolean,
subscriptions: Array<Subscription>,
};
@ -107,6 +108,7 @@ class FilePage extends React.Component<Props> {
prepareEdit,
navigate,
autoplay,
fromOverlay,
costInfo,
} = this.props;
@ -150,7 +152,7 @@ class FilePage extends React.Component<Props> {
</section>
) : (
<section className="card">
{isPlayable && <Video className="content__embedded" uri={uri} />}
{isPlayable && <Video className="content__embedded" uri={uri} fromOverlay={fromOverlay} />}
{!isPlayable &&
(thumbnail ? (
<Thumbnail shouldObscure={shouldObscureThumbnail} src={thumbnail} />

View file

@ -11,6 +11,7 @@ const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
blackListedOutpoints: selectBlackListedOutpoints(state),
fromOverlay: props.fromOverlay,
});
const perform = dispatch => ({

View file

@ -12,6 +12,7 @@ type Props = {
resolveUri: string => void,
uri: string,
claim: Claim,
fromOverlay: boolean,
blackListedOutpoints: Array<{
txid: string,
nout: number,
@ -34,7 +35,7 @@ class ShowPage extends React.PureComponent<Props> {
}
render() {
const { claim, isResolvingUri, uri, blackListedOutpoints } = this.props;
const { claim, isResolvingUri, uri, blackListedOutpoints, fromOverlay } = this.props;
let innerContent = '';
@ -85,7 +86,7 @@ class ShowPage extends React.PureComponent<Props> {
</Page>
);
} else {
innerContent = <FilePage uri={uri} />;
innerContent = <FilePage uri={uri} fromOverlay={fromOverlay} />;
}
}

View file

@ -26,3 +26,13 @@ export function savePosition(claimId: String, position: Number) {
});
};
}
export const doShowOverlay = () => (dispatch: Dispatch) =>
dispatch({
type: actions.SHOW_OVERLAY_MEDIA,
});
export const doHideOverlay = () => (dispatch: Dispatch) =>
dispatch({
type: actions.HIDE_OVERLAY_MEDIA,
});

View file

@ -2,6 +2,7 @@ import * as ACTIONS from 'constants/action_types';
const reducers = {};
const defaultState = {
showOverlay: false,
playingUri: null,
currentlyIsPlaying: false,
rewardedContentClaimIds: [],

View file

@ -3,6 +3,7 @@ import * as actions from 'constants/action_types';
import { handleActions } from 'util/redux-utils';
export type MediaState = {
showOverlay: Boolean,
paused: Boolean,
positions: {
[string]: number,
@ -12,16 +13,17 @@ export type MediaState = {
export type Action = any;
export type Dispatch = (action: Action) => any;
const defaultState = { paused: true, positions: {} };
const defaultState = { paused: true, positions: {}, showOverlay: false };
export default handleActions(
{
[actions.MEDIA_PLAY]: (state: MediaState, action: Action) => ({
// if parameters: state: MediaState, action: Action
[actions.MEDIA_PLAY]: (state: MediaState) => ({
...state,
paused: false,
}),
[actions.MEDIA_PAUSE]: (state: MediaState, action: Action) => ({
[actions.MEDIA_PAUSE]: (state: MediaState) => ({
...state,
paused: true,
}),
@ -36,6 +38,16 @@ export default handleActions(
},
};
},
[actions.SHOW_OVERLAY_MEDIA]: (state: MediaState) => ({
...state,
showOverlay: true,
}),
[actions.HIDE_OVERLAY_MEDIA]: (state: MediaState) => ({
...state,
showOverlay: false,
}),
},
defaultState
);

View file

@ -10,3 +10,5 @@ export const makeSelectMediaPositionForUri = uri =>
const outpoint = `${claim.txid}:${claim.nout}`;
return state.positions[outpoint] || null;
});
export const selectShowOverlay = createSelector(selectState, state => state.showOverlay);

View file

@ -24,4 +24,5 @@
@import 'component/_nav.scss';
@import 'component/_file-list.scss';
@import 'component/_search.scss';
@import 'component/_overlay.scss';
@import 'component/_toggle.scss';

View file

@ -0,0 +1,57 @@
.hiddenContainer {
display: none;
}
.overlay {
position: fixed;
max-height: 50%;
max-width: 50%;
width: 20%;
height: inherit;
bottom: 1%;
right: 1%;
z-index: 3;
box-shadow: var(--box-shadow-layer);
.video__overlay {
position: relative;
.video__mask {
opacity: 0;
background-color: rgba(0, 0, 8, 0.7);
transition: all 0.4s ease-in-out;
width: 100%;
height: 100%;
overflow: hidden;
top: 0;
left: 0;
position: absolute;
display: flex;
flex-direction: row;
.btn {
margin: auto;
}
}
&:hover .video__mask {
opacity: 1;
}
}
}
.video_overlay__header {
flex-direction: row;
justify-content: space-between;
align-items: center;
flex-wrap: nowrap;
display: flex;
width: 100%;
height: 2rem;
background-color: var(--header-primary-color);
padding: 20px 10px;
.overlay__title--small {
font-size: 15px;
line-height: 15px;
}
}