bokuweb/tsukiakari

View on GitHub
src/renderer/stylesheets/components/video/Video.js

Summary

Maintainability
B
4 hrs
Test Coverage
import React from 'react';
import Overlay from './overlay/overlay';
import Controls from './../controls/controls';
import Seek from './../controls/seek/seek';
import Play from './../controls/play/play';
import Mute from './../controls/mute/mute';
import Fullscreen from './../controls/fullscreen/fullscreen';
import Time from './../controls/time/time';
import throttle from 'lodash.throttle';
import copy from './../../assets/copy';

var EVENTS = [
    'onAbort',
    'onCanPlay',
    'onCanPlayThrough',
    'onDurationChange',
    'onEmptied',
    'onEncrypted',
    'onEnded',
    'onError',
    'onLoadedData',
    'onLoadedMetadata',
    'onLoadStart',
    'onPause',
    'onPlay',
    'onPlaying',
    'onProgress',
    'onRateChange',
    'onSeeked',
    'onSeeking',
    'onStalled',
    'onSuspend',
    'onTimeUpdate',
    'onVolumeChange',
    'onWaiting'
];

var Video = React.createClass({

    propTypes: {
        // Non-standard props
        copyKeys: React.PropTypes.object,
        children: React.PropTypes.node,

        // HTML5 Video standard attributes
        autoPlay: React.PropTypes.bool,
        muted: React.PropTypes.bool,
        controls: React.PropTypes.bool
    },

    getDefaultProps() {
        return {
            copyKeys: copy
        };
    },

    getInitialState() {
        // Set state from props and always use these
        // to check state of video as they will update
        // on the video events. Changing this state however will not
        // change the video. The API methods must be used.
        return {
            networkState: 0,
            paused: !this.props.autoPlay,
            muted: !!this.props.muted,
            volume: 1,
            error: false,
            loading: false
        };
    },

    /**
     * Creates a throttle update method.
     * @return {undefined}
     */
    componentWillMount() {
        // Also bind 'this' as we call _updateStateFromVideo outside
        // of Reacts synthetic events as well.
        this._updateStateFromVideo = throttle(this.updateStateFromVideo, 100).bind(this);
        // Set up all React media events and call method
        // on props if provided.
        this.mediaEventProps = EVENTS.reduce((p, c) => {
            p[c] = () => {
                if (c in this.props && typeof this.props[c] === 'function') {
                    // A prop exists for this mediaEvent, call it.
                    this.props[c]();
                }
                this._updateStateFromVideo();
            };
            return p;
        }, {});
    },

    /**
     * Bind eventlisteners not supported by React's synthetic events
     * https://facebook.github.io/react/docs/events.html
     * @return {undefined}
     */
    componentDidMount() {
        // Listen to error of last source.
        this.videoEl.children[this.videoEl.children.length - 1]
        .addEventListener('error', this._updateStateFromVideo, { passive: true });
    },

    /**
     * Removes event listeners bound outside of React's synthetic events
     * @return {undefined}
     */
    componentWillUnmount() {
        // Remove event listener from video.
        this.videoEl.children[this.videoEl.children.length - 1]
            .removeEventListener('error', this._updateStateFromVideo);
    },

    /**
     * Toggles the video to play and pause.
     * @return {undefined}
     */
    togglePlay() {
        if (this.state.paused) {
            this.play();
        } else {
            this.pause();
        }
    },

    /**
     * Toggles the video to mute and unmute.
     * @return {undefined}
     */
    toggleMute() {
        if (this.state.muted) {
            this.unmute();
        } else {
            this.mute();
        }
    },

    /**
     * Loads video.
     * @return {undefined}
     */
    load() {
        this.videoEl.load();
    },

    /**
     * Sets the video to fullscreen.
     * @return {undefined}
     */
    fullscreen() {
        if (this.videoEl.requestFullscreen) {
            this.videoEl.requestFullscreen();
        } else if (this.videoEl.msRequestFullscreen) {
            this.videoEl.msRequestFullscreen();
        } else if (this.videoEl.mozRequestFullScreen) {
            this.videoEl.mozRequestFullScreen();
        } else if (this.videoEl.webkitRequestFullscreen) {
            this.videoEl.webkitRequestFullscreen();
        }
    },

    /**
     * Plays the video.
     * @return {undefined}
     */
    play() {
        this.videoEl.play();
    },

    /**
     * Pauses the video.
     * @return {undefined}
     */
    pause() {
        this.videoEl.pause();
    },

    /**
     * Unmutes video.
     * @return {undefined}
     */
    unmute() {
        this.videoEl.muted = false;
    },

    /**
     * Mutes the video.
     * @return {undefined}
     */
    mute() {
        this.videoEl.muted = true;
    },

    /**
     * Seeks the video timeline.
     * @param  {number} time The value in seconds to seek to
     * @return {undefined}
     */
    seek(time) {
        this.videoEl.currentTime = time;
    },

    /**
     * Sets the video volume.
     * @param  {number} volume The volume level between 0 and 1.
     * @return {undefined}
     */
    setVolume(volume) {
        this.videoEl.volume = volume;
    },

    /**
     * Updates the React component state from the DOM video properties.
     * This is where the magic happens.
     * @return {undefined}
     */
    updateStateFromVideo() {
        this.setState({
            // Standard video properties
            duration: this.videoEl.duration,
            currentTime: this.videoEl.currentTime,
            buffered: this.videoEl.buffered,
            paused: this.videoEl.paused,
            muted: this.videoEl.muted,
            volume: this.videoEl.volume,
            readyState: this.videoEl.readyState,

            // Non-standard state computed from properties
            percentageBuffered: this.videoEl.buffered.length && this.videoEl.buffered.end(this.videoEl.buffered.length - 1) / this.videoEl.duration * 100,
            percentagePlayed: this.videoEl.currentTime / this.videoEl.duration * 100,
            error: this.videoEl.networkState === this.videoEl.NETWORK_NO_SOURCE,
            loading: this.videoEl.readyState < this.videoEl.HAVE_ENOUGH_DATA
        });
    },

    /**
     * Returns everything but 'source' nodes from children
     * and extends props so all children have access to Video API and state.
     * If there are no controls provided, returns default Controls.
     * @return {Array.<ReactElement>} An array of components.
     */
    renderControls() {
        var extendedProps = Object.assign({
            // The public methods that all controls should be able to
            // use.
            togglePlay: this.togglePlay,
            toggleMute: this.toggleMute,
            play: this.play,
            pause: this.pause,
            mute: this.mute,
            unmute: this.unmute,
            seek: this.seek,
            fullscreen: this.fullscreen,
            setVolume: this.setVolume
        }, this.state, {copyKeys: this.props.copyKeys});

        var controls = React.Children.map(this.props.children, (child) => {
            if (child.type === 'source') {
                return void 0;
            }
            return React.cloneElement(child, extendedProps);
        });

        if (!controls.length) {
            controls = (
                <div>
                    <Overlay {...extendedProps} />
                    <Controls {...extendedProps} />
                </div>
            );
        }
        return controls;
    },

    /**
     * Returns video 'source' nodes from children.
     * @return {Array.<ReactElement>} An array of components.
     */
    renderSources() {
        return React.Children.map(this.props.children, (child) => {
            if (child.type !== 'source') {
                return void 0;
            }
            return child;
        });
    },

    /**
     * Gets the video class name based on its state
     * @return {string} Class string
     */
    getVideoClassName() {
        var classString = 'video';

        if (this.state.error) {
            classString += ' video--error';
        } else if (this.state.loading) {
            classString += ' video--loading';
        } else if (this.state.paused) {
            classString += ' video--paused';
        } else {
            classString += ' video--playing';
        }

        if (this.state.focused) {
            classString += ' video--focused';
        }
        return classString;
    },

    /**
     * Sets state to show focused class on video player.
     * @return {undefined}
     */
    onFocus() {
        this.setState({
            focused: true
        });
    },

    /**
     * Sets state to not be focused to remove class form video
     * player.
     * @return {undefined}
     */
    onBlur() {
        this.setState({
            focused: false
        });
    },

    render() {
        // If controls prop is provided remove it
        // and use our own controls.
        var {controls, ...otherProps} = this.props;
        return (
            <div className={this.getVideoClassName()}
                tabIndex="0"
                onFocus={this.onFocus}
                onBlur={this.onBlur}>
                <video
                    {...otherProps}
                    className="video__el"
                    ref={(el) => {
                        this.videoEl = el;
                    }}
                    //  We have throttled `_updateStateFromVideo` so listen to
                    //  every available Media event that React allows and
                    //  infer the Video state in that method from the Video properties.
                    {...this.mediaEventProps}>
                        {this.renderSources()}
                </video>
                {controls ? this.renderControls() : ''}
            </div>
        );
    }
});

export {Video as default, Controls, Seek, Play, Mute, Fullscreen, Time, Overlay};