@bbc/react-transcript-editor
Version: 
A React component to make transcribing audio and video easier and faster.
389 lines (337 loc) • 14.1 kB
JavaScript
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
import React from 'react';
import PropTypes from 'prop-types'; // https://www.npmjs.com/package/react-keyboard-shortcuts
import { hotkeys } from 'react-keyboard-shortcuts';
import returnHotKeys from './defaultHotKeys';
import PlaybackRate from './PlaybackRate.js';
import RollBack from './RollBack.js';
import ProgressBar from './ProgressBar.js';
import PlayerControls from './PlayerControls.js';
import VolumeControl from './VolumeControl.js';
import PauseWhileTyping from './PauseWhileTyping.js';
import ScrollIntoView from './ScrollIntoView.js';
import styles from './index.module.css';
import { secondsToTimecode, timecodeToSeconds } from '../../Util/timecode-converter/index';
class MediaPlayer extends React.Component {
  constructor(props) {
    super(props);
    _defineProperty(this, "hot_keys", returnHotKeys(this));
    _defineProperty(this, "setCurrentTime", newCurrentTime => {
      if (newCurrentTime !== '' && newCurrentTime !== null) {
        // hh:mm:ss:ff - mm:ss - m:ss - ss - seconds number or string and hh:mm:ss
        const newCurrentTimeInSeconds = timecodeToSeconds(newCurrentTime);
        if (this.videoRef.current !== null) {
          const videoRef = this.videoRef.current; // videoRef.load();
          if (videoRef.readyState === 4) {
            // it's loaded
            videoRef.currentTime = newCurrentTimeInSeconds;
            videoRef.play();
          }
        }
      }
    });
    _defineProperty(this, "promptSetCurrentTime", () => {
      const newCurrentTime = prompt('Jump to time - hh:mm:ss:ff hh:mm:ss mm:ss m:ss m.ss seconds');
      this.props.handleAnalyticsEvents({
        category: 'MediaPlayer',
        action: 'promptSetCurrentTime',
        name: 'jumpToTime',
        value: newCurrentTime
      });
      if (newCurrentTime !== '' && newCurrentTime !== null) {
        this.setCurrentTime(newCurrentTime);
      }
    });
    _defineProperty(this, "setTimeCodeOffset", newTimeCodeOffSet => {
      this.props.handleAnalyticsEvents({
        category: 'MediaPlayer',
        action: 'setTimeCodeOffset',
        name: 'timecodeOffsetValue',
        value: newTimeCodeOffSet
      });
      if (newTimeCodeOffSet !== '' && newTimeCodeOffSet !== null) {
        // use similar helper function from above to convert
        let newCurrentTimeInSeconds = newTimeCodeOffSet;
        if (newTimeCodeOffSet.includes(':')) {
          newCurrentTimeInSeconds = timecodeToSeconds(newTimeCodeOffSet);
          this.setState({
            timecodeOffset: newCurrentTimeInSeconds
          });
        }
      }
    });
    _defineProperty(this, "rollBack", () => {
      if (this.videoRef.current !== null) {
        this.props.handleAnalyticsEvents({
          category: 'MediaPlayer',
          action: 'rollBack',
          name: 'rollBackValue',
          value: this.state.rollBackValueInSeconds
        }); // get video duration
        const videoElem = this.videoRef.current;
        const tmpDesiredCurrentTime = videoElem.currentTime - this.state.rollBackValueInSeconds; // > 0 < duration of video
        this.setCurrentTime(tmpDesiredCurrentTime);
      }
    });
    _defineProperty(this, "handleTimeUpdate", e => {
      // eslint-disable-next-line react/prop-types
      this.props.hookOnTimeUpdate(e.target.currentTime);
    });
    _defineProperty(this, "handlePlayBackRateChange", e => {
      this.setPlayBackRate(e.target.value);
    });
    _defineProperty(this, "increasePlaybackRate", () => {
      const currentPlaybackRate = this.getCurrentPlaybackRate();
      let newPlaybackRate = currentPlaybackRate + 0.1; // rounding up eg 0.8-0.1 =  0.7000000000000001   => 0.7
      newPlaybackRate = Number(newPlaybackRate.toFixed(1));
      this.setPlayBackRate(newPlaybackRate);
    });
    _defineProperty(this, "decreasePlaybackRate", () => {
      const currentPlaybackRate = this.getCurrentPlaybackRate();
      let newPlaybackRate = currentPlaybackRate - 0.1; // rounding up eg 0.8-0.1 =  0.7000000000000001   => 0.7
      newPlaybackRate = Number(newPlaybackRate.toFixed(1));
      this.setPlayBackRate(newPlaybackRate);
    });
    _defineProperty(this, "getCurrentPlaybackRate", () => {
      if (this.videoRef.current !== null) {
        return this.videoRef.current.playbackRate;
      }
    });
    _defineProperty(this, "setPlayBackRate", speedValue => {
      // value between 0.2 and 3.5
      if (this.videoRef.current !== null) {
        if (speedValue >= 0.2 && speedValue <= 3.5) {
          this.setState({
            playBackRate: speedValue
          }, () => {
            this.props.handleAnalyticsEvents({
              category: 'MediaPlayer',
              action: 'setPlayBackRate',
              name: 'playbackRateNewValue',
              value: speedValue
            });
            this.videoRef.current.playbackRate = speedValue;
          });
        }
      }
    });
    _defineProperty(this, "handleChangeReplayRollbackValue", e => {
      if (this.videoRef.current !== null) {
        this.setState({
          rollBackValueInSeconds: e.target.value
        });
      }
    });
    _defineProperty(this, "handleMuteVolume", e => {
      // https://www.w3schools.com/tags/av_prop_volume.asp
      if (this.videoRef.current !== null) {
        if (this.videoRef.current.volume > 0) {
          this.videoRef.current.volume = 0;
        } else {
          this.videoRef.current.volume = 1;
        }
      }
    });
    _defineProperty(this, "handleTogglePauseWhileTyping", e => {
      console.log('triggered');
      console.log(this.state.isPausedWhileTyping);
      this.setState((prevState, props) => {
        console.log(prevState.isPausedWhileTyping);
        this.props.handleAnalyticsEvents({
          category: 'MediaPlayer',
          action: 'handleTogglePauseWhileTyping',
          name: '',
          value: !prevState.isPausedWhileTyping
        });
        return {
          isPausedWhileTyping: !prevState.isPausedWhileTyping
        };
      });
    });
    _defineProperty(this, "handleToggleScrollIntoView", e => {
      this.props.handleAnalyticsEvents({
        category: 'MediaPlayer',
        action: 'handleToggleScrollIntoView',
        name: '',
        value: e.target.checked
      });
      this.props.handleIsScrollIntoViewChange(e.target.checked);
    });
    _defineProperty(this, "isPlaying", () => {
      if (this.videoRef.current !== null) {
        if (this.videoRef.current.paused) {
          return false;
        }
        return true;
      }
    });
    _defineProperty(this, "playMedia", playPauseBool => {
      // checks that there is a video player element initialized
      if (this.videoRef.current !== null) {
        // if playMedia is being triggered by PlayerControl or Video element
        // then it will have a target attribute
        if (playPauseBool.target !== undefined) {
          // checks on whether to use default fallback if no param is provided
          if (this.videoRef.current.paused) {
            this.videoRef.current.play();
          } else {
            this.videoRef.current.pause();
          }
        } else {
          // if param is provided and if pausedWhileTyping Toggle is on
          if (this.state.isPausedWhileTyping) {
            if (playPauseBool) {
              this.videoRef.current.play();
            } else {
              this.videoRef.current.pause();
            }
          }
        }
      }
    });
    _defineProperty(this, "skipForward", () => {
      if (this.videoRef.current !== null) {
        // TODO track this?
        const currentTime = this.videoRef.current.currentTime;
        const newCurrentTimeIncreased = currentTime + 10;
        const newCurrentTime = Number(newCurrentTimeIncreased.toFixed(1));
        this.setCurrentTime(newCurrentTime);
      }
    });
    _defineProperty(this, "skipBackward", () => {
      // TODO track this?
      if (this.videoRef.current !== null) {
        const currentTime = this.videoRef.current.currentTime;
        const newCurrentTimeIncreased = currentTime - 10;
        const newCurrentTime = Number(newCurrentTimeIncreased.toFixed(1));
        this.setCurrentTime(newCurrentTime);
      }
    });
    _defineProperty(this, "handleProgressBarClick", e => {
      if (this.videoRef.current !== null) {
        // length of the bar
        const lengthOfBar = e.target.offsetWidth; // distance of the position of the lick from the start of the progress bar element
        // location of click - start point of the bar
        const clickLength = e.clientX - e.target.offsetLeft;
        const positionPercentage = clickLength / lengthOfBar;
        const totalTime = e.target.max;
        const resultInSeconds = totalTime * positionPercentage; // rounding up
        const roundNewCurrentTime = parseFloat(resultInSeconds.toFixed(2));
        this.props.handleAnalyticsEvents({
          category: 'MediaPlayer',
          action: 'handleProgressBarClick',
          name: 'roundNewCurrentTime',
          value: roundNewCurrentTime
        });
        this.setCurrentTime(roundNewCurrentTime);
      }
    });
    _defineProperty(this, "getMediaCurrentTime", () => {
      if (this.videoRef.current !== null) {
        return secondsToTimecode(this.videoRef.current.currentTime + this.state.timecodeOffset);
      }
      return '00:00:00:00';
    });
    _defineProperty(this, "getMediaDuration", () => {
      if (this.videoRef.current !== null) {
        return secondsToTimecode(this.videoRef.current.duration + this.state.timecodeOffset);
      }
      return '00:00:00:00';
    });
    this.videoRef = React.createRef();
    this.state = {
      playBackRate: 1,
      rollBackValueInSeconds: 15,
      timecodeOffset: 0,
      hotKeys: returnHotKeys(this),
      isPausedWhileTyping: false
    };
  }
  /*eslint-disable camel case */
  componentDidMount() {
    this.props.hookSeek(this.setCurrentTime);
    this.props.hookPlayMedia(this.playMedia);
    this.props.hookIsPlaying(this.isPlaying);
  }
  render() {
    // conditional, if media player not defined then don't show
    let mediaPlayerEl;
    if (this.props.mediaUrl !== null) {
      mediaPlayerEl = React.createElement("video", {
        id: "video",
        playsInline: true // autoPlay
        // controls
        ,
        src: this.props.mediaUrl,
        onTimeUpdate: this.handleTimeUpdate // TODO: video type - add logic to identify if video is playable and raise error if it's not
        ,
        type: "video/mp4",
        "data-testid": "media-player-id",
        onClick: this.playMedia.bind(this),
        ref: this.videoRef
      });
    }
    let playerControlsSection;
    if (this.props.mediaUrl !== null) {
      playerControlsSection = React.createElement("section", null, React.createElement(ProgressBar, {
        max: this.videoRef.current !== null ? parseInt(this.videoRef.current.duration) : 100,
        value: this.videoRef.current !== null ? parseInt(this.videoRef.current.currentTime) : 0,
        buttonClick: this.handleProgressBarClick.bind(this)
      }), React.createElement("br", null), React.createElement(PlayerControls, {
        playMedia: this.playMedia.bind(this),
        isPlaying: this.isPlaying.bind(this),
        skipBackward: this.skipBackward.bind(this),
        skipForward: this.skipForward.bind(this),
        currentTime: this.getMediaCurrentTime(),
        duration: this.getMediaDuration(),
        onSetCurrentTime: '',
        onSetTimecodeOffset: '',
        promptSetCurrentTime: this.promptSetCurrentTime.bind(this),
        setTimeCodeOffset: this.setTimeCodeOffset.bind(this),
        timecodeOffset: secondsToTimecode(this.state.timecodeOffset)
      }), React.createElement(VolumeControl, {
        handleMuteVolume: this.handleMuteVolume.bind(this)
      }), React.createElement(PauseWhileTyping, {
        isPausedWhileTyping: this.props.isPausedWhileTyping,
        handleToggle: this.handleTogglePauseWhileTyping.bind(this)
      }), React.createElement(ScrollIntoView, {
        isScrollIntoViewOn: this.props.isScrollIntoViewOn,
        handleToggle: this.handleToggleScrollIntoView.bind(this)
      }), React.createElement(PlaybackRate, {
        playBackRate: this.state.playBackRate,
        handlePlayBackRateChange: this.handlePlayBackRateChange.bind(this),
        setPlayBackRate: this.setPlayBackRate.bind(this)
      }), React.createElement(RollBack, {
        rollBackValueInSeconds: this.state.rollBackValueInSeconds,
        handleChangeReplayRollbackValue: this.handleChangeReplayRollbackValue.bind(this),
        rollBack: this.rollBack.bind(this)
      }));
    }
    ; // list of keyboard shortcuts helper text
    const keyboardShortcutsElements = Object.keys(this.state.hotKeys).map((shortcutKey, index) => {
      return React.createElement("p", {
        className: styles.helpText,
        key: shortcutKey
      }, React.createElement("code", null, shortcutKey), React.createElement("small", null, React.createElement("b", null, " ", this.state.hotKeys[shortcutKey].helperText)));
    });
    let keyboardShortcuts;
    if (this.props.mediaUrl !== null) {
      keyboardShortcuts = React.createElement("section", {
        className: styles.hideInMobile
      }, React.createElement("label", null, keyboardShortcutsElements), React.createElement("br", null), React.createElement("small", {
        className: styles.helpText
      }, "Double click on a word to be taken to that time in the media."));
    }
    return React.createElement("section", {
      className: styles.videoSection
    }, mediaPlayerEl, playerControlsSection, keyboardShortcuts);
  }
}
MediaPlayer.propTypes = {
  hookSeek: PropTypes.func,
  hookPlayMedia: PropTypes.func,
  mediaUrl: PropTypes.string,
  hookOnTimeUpdate: PropTypes.func,
  hookIsScrollSyncToggle: PropTypes.func
};
export default hotkeys(MediaPlayer);