Skip to content
This repository was archived by the owner on Sep 2, 2021. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions src/EditorComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import { Node } from 'prosemirror-model';
import { EditorView } from 'prosemirror-view';
import { exampleSetup } from 'prosemirror-example-setup';
import { keymap } from 'prosemirror-keymap';
import { InputRule, inputRules } from 'prosemirror-inputrules';
import { inputRules } from 'prosemirror-inputrules';
import { DOMParser } from 'prosemirror-model';
import Paper from '@material-ui/core/Paper';
import { makeStyles, createStyles } from '@material-ui/core/styles';

import AppConfig from './AppConfig';
import EditorSchema from './prosemirror-plugins/Schema';
import ImageNodeView from './prosemirror-plugins/ImageNodeView';
import TimeBlockNodeView from './prosemirror-plugins/TimeBlockNodeView';
import AutosavePlugin from './prosemirror-plugins/AutosavePlugin';
import { loadNote } from './LocalStorageHelper';
import keycodes from './keycodeMap';
Expand All @@ -21,7 +22,15 @@ import {
mkSeekToTimestamp,
mkToggleTimestampMark,
textToTimestamp,
mkWrapInTimeBlock,
unwrapAllTimeBlocks,
} from './prosemirror-plugins/commands';
import {
mkToggleVideoPauseInputRule,
ItalicStartTimeInputRule,
ItalicEndTimeInputRule,
} from './prosemirror-plugins/inputrules';
import { InputRule } from 'prosemirror-inputrules';

import 'prosemirror-view/style/prosemirror.css';
import 'prosemirror-menu/style/menu.css';
Expand All @@ -44,7 +53,7 @@ const EditorComponent = (props: any) => {
const editorView = useRef<EditorView>();
const classes = useStyles();

const { player: playerRef, videoId, videoTitle } = props;
const { player: playerRef, videoId, videoTitle, videoDuration } = props;

const player = playerRef.current;

Expand Down Expand Up @@ -92,8 +101,9 @@ const EditorComponent = (props: any) => {
put_timestamp: mkInsertTimestampStr(currentTimestamp),
seek_to_timestamp: mkSeekToTimestamp(videoTime => player.seek(videoTime)),
capture_frame: onCaptureFrame,
debug_print: () => true,
turn_text_to_timestamp: textToTimestamp,
wrap_in_time_block: mkWrapInTimeBlock(videoDuration),
unwrap_all_time_blocks: unwrapAllTimeBlocks,
};

const keymapObject = Object.keys(keycodes)
Expand Down Expand Up @@ -124,7 +134,11 @@ const EditorComponent = (props: any) => {
autosavePlugin,

inputRules({
rules: [ToggleVideoPauseInputRule],
rules: [
// mkToggleVideoPauseInputRule(doCommand),
ItalicStartTimeInputRule,
ItalicEndTimeInputRule,
],
}),
...exampleSetup({ schema: EditorSchema }),
],
Expand All @@ -134,6 +148,10 @@ const EditorComponent = (props: any) => {
inlineImage: (node, view, getPos) => {
return new ImageNodeView(node, view, getPos);
},

time_block: (node, view, getPos) => {
return new TimeBlockNodeView(node, view, getPos);
},
},
});

Expand Down
294 changes: 294 additions & 0 deletions src/EditorPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';

import EditorComponent from './EditorComponent';
import VideoPathInput from './VideoPathInput';
import getYoutubeTitle from 'get-youtube-title';
import PropTypes from 'prop-types';
import IFrameStyleWrapper from './IFrameStyleWrapper';
import { SnackbarContext } from './context/SnackbarContext';
import AppConfig from './AppConfig';

const YOUTUBE_API_KEY = process.env.REACT_APP_YOUTUBE_API_KEY;
if (!YOUTUBE_API_KEY) {
alert('REACT_APP_YOUTUBE_API_KEY required in .env file');
}

const ytNameOfPlayerState = {
'-1': 'unstarted',
'0': 'ended',
'1': 'playing',
'2': 'paused',
'3': 'buffering',
'5': 'video_cued',
};

class YoutubePlayerController {
constructor(YT, playerApi) {
if (!playerApi) {
throw new Error('playerApi is null');
}
this.YT = YT;
this.playerApi = playerApi;
this.currentVideoId = null;
this.currentVideoTitle = null;
this.currentPlayerState = 'unstarted';
}

setVideoTitle() {
if (this.currentVideoId === null) {
return;
}

this.currentVideoTitle = null;

getYoutubeTitle(this.currentVideoId, YOUTUBE_API_KEY, (err, title) => {
if (!err) {
this.currentVideoTitle = title;
} else {
return new Error(`Failed to retrive title of video - ${this.currentVideoId}`);
}
});
}

getPlayerState() {
return this.playerApi.getPlayerState();
}

playVideo(videoId = null) {
if (!videoId && !this.currentVideoId) {
return;
}

this.currentVideoId = videoId ? videoId : this.currentVideoId;

this.playerApi.playVideo(this.currentVideoId);
}

loadAndPlayVideo(videoId) {
this.currentVideoId = videoId;
this.currentVideoTitle = null;
this.playerApi.cueVideoById(this.currentVideoId, 0);
this.playerApi.playVideo(this.currentVideoId);
this.setVideoTitle();
}

pauseVideo() {
this.playerApi.pauseVideo(this.currentVideoId);
}

// Returns state after toggle
togglePause() {
if (this.currentPlayerState === 'paused') {
this.playVideo();
return 'playing';
} else if (this.currentPlayerState === 'playing') {
this.pauseVideo();
return 'paused';
}
return null;
}

addToCurrentTime(seconds) {
const currentTime = this.playerApi.getCurrentTime();
this.playerApi.seekTo(Math.max(currentTime + seconds, 0));
}

getCurrentTime() {
return this.playerApi && this.playerApi.getCurrentTime ? this.playerApi.getCurrentTime() : null;
}

getVideoTitle() {
return this.currentVideoTitle;
}

getDuration() {
return this.playerApi && this.playerApi.getDuration ? this.playerApi.getDuration() : null;
}

seekTo(timeInSeconds) {
this.playerApi.seekTo(timeInSeconds);
}
}

// The commands from console are send via the App component
class EditorPage extends Component {
static propTypes = {
startingVideoId: PropTypes.string,
startingPopperMessage: PropTypes.string,
};

static defaultProps = {
startingVideoId: null,
startingPopperMessage: null,
};

static contextType = SnackbarContext;

constructor(props) {
super(props);

this.state = {
infoText: null,
infoLastTime: null,
selectedOption: null,
startingPopperMessage: this.props.startingPopperMessage,
videoId: null,
};

// We keep a handle to the youtube player. This is the player API object, not the dom
// element itself.
this.ytPlayerController = null;
this.doVideoCommand = this.doVideoCommand.bind(this);

this.iframeRef = React.createRef();
}

tellPluginToRemovePauseOverlay = () => {
window.frames[0].postMessage({ type: AppConfig.RemovePauseOverlayMessage }, '*');
};

doVideoCommand = (command, params) => {
const currentTime = this.ytPlayerController.getCurrentTime();

switch (command) {
case 'playVideo':
this.ytPlayerController.playVideo();
break;

case 'pauseVideo':
this.ytPlayerController.pauseVideo();
this.tellPluginToRemovePauseOverlay();
break;

case 'restartVideo':
this.ytPlayerController.seekTo(0);
break;

case 'togglePause':
const playState = this.ytPlayerController.togglePause();
if (playState === 'paused') {
this.tellPluginToRemovePauseOverlay();
}
break;

case 'addToCurrentTime':
this.ytPlayerController.addToCurrentTime(params.secondsToAdd);
break;

case 'seekToTime':
if (params.videoTime) {
this.ytPlayerController.seekTo(params.videoTime);
}
break;

case 'currentTime':
break;

default:
break;
}

return currentTime;
};

currentVideoInfo = () => {
const info = { videoId: null, videoTime: null, videoTitle: null };
if (this.ytPlayerController) {
info.videoId = this.ytPlayerController.currentVideoId;
info.videoTime = this.ytPlayerController.getCurrentTime();
info.videoTitle = this.ytPlayerController.getVideoTitle();
info.videoDuration = this.ytPlayerController.getDuration();
}
return info;
};

componentDidMount() {
const { ytAPI } = this.props;
if (this.iframeRef.current) {
let ytPlayerApi = null;

const { startingVideoId } = this.props;

ytPlayerApi = new ytAPI.Player(this.iframeRef.current, {
height: '100%',
width: '100%',
events: {
onStateChange: newState => {
this.ytPlayerController.currentPlayerState = ytNameOfPlayerState[newState.data];
},

onReady: () => {
this.ytPlayerController = new YoutubePlayerController(ytAPI, ytPlayerApi);
if (startingVideoId) {
this.ytPlayerController.loadAndPlayVideo(startingVideoId);
// console.log('Loading video and note for ', this.props.startingVideoId);
const videoId = this.props.startingVideoId;

this.context.openSnackbar({
message: `Loading video: ${videoId}`,
autoHideDuration: 1000,
});

this.setState({ videoId });
}
},
},
});
}

if (this.state.startingPopperMessage) {
this.context.openSnackbar({ message: this.state.startingPopperMessage });
setTimeout(() => {
this.setState({ startingPopperMessage: null });
});
}
}

render() {
const { match } = this.props;
const videoId = match.params.videoId;

if (!videoId) {
return <VideoPathInput currentVideoId={videoId} />;
}

const info = this.currentVideoInfo();

console.log('videoDuration =', videoDuration);

return (
<div
style={{
display: 'flex',
direction: 'row',
justifyContent: 'flex-start',
}}
>
<IFrameStyleWrapper>
<div
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
}}
ref={this.iframeRef}
id={AppConfig.YoutubeIframeId}
/>
</IFrameStyleWrapper>
<EditorComponent
parentApp={this}
doCommand={this.doVideoCommand}
showInfo={this.props.showInfo}
videoId={videoId}
videoTitle={info.videoTitle}
videoDuration={info.videoDuration}
/>
</div>
);
}
}

export default withRouter(EditorPage);
2 changes: 2 additions & 0 deletions src/keycodeMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const keycodes: { [s: string]: string } = {
seek_to_timestamp: 'Ctrl-g',
capture_frame: 'Ctrl-i',
debug_print: 'Ctrl-d',
wrap_in_time_block: 'Ctrl-o',
unwrap_time_blocks: 'Ctrl-p',
};

export default keycodes;
Loading