@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);