From 1ce0a9eb35fd75c3555a499ef3cb94e54c296cae Mon Sep 17 00:00:00 2001 From: Stefan Kraus <stefan.kraus@methodpark.de> Date: Sun, 13 Dec 2020 14:30:52 +0100 Subject: [PATCH] Add subtitles --- frontend/package.json | 5 ++++ frontend/src/App.tsx | 1 + frontend/src/player/Player.tsx | 30 +++++++++++++++---- frontend/src/url/UrlBox.tsx | 54 ++++++++++++++++++++++++++++++++-- frontend/src/url/UrlSlice.ts | 12 ++++++-- frontend/tsconfig.json | 2 +- 6 files changed, 91 insertions(+), 13 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 75757f7..8673b94 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,6 +3,8 @@ "version": "0.1.0", "private": true, "dependencies": { + "@ffmpeg/core": "^0.8.5", + "@ffmpeg/ffmpeg": "^0.9.6", "@giantmachines/redux-websocket": "^1.4.0", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.11.2", @@ -10,7 +12,10 @@ "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", + "@types/ffmpeg.js": "^3.1.1", "@types/react-redux": "^7.1.11", + "ffmpeg.js": "^4.2.9003", + "memfs": "^3.2.0", "react": "^17.0.1", "react-dom": "^17.0.1", "react-player": "^2.7.0", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 78106f5..ec8ab32 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,6 +5,7 @@ import StopButton from "./player/StopButton"; import SyncToMeButton from "./player/SyncToMeButton"; import SyncAllButton from "./player/SyncAllButton"; + import "./App.css"; import UrlBox from "./url/UrlBox"; import { Box, Container, Grid } from "@material-ui/core"; diff --git a/frontend/src/player/Player.tsx b/frontend/src/player/Player.tsx index 63cc3f6..9c32b6b 100644 --- a/frontend/src/player/Player.tsx +++ b/frontend/src/player/Player.tsx @@ -2,6 +2,11 @@ import React, { RefObject } from "react"; import ReactPlayer from "react-player"; import { RootState } from "../store/store"; import { connect } from "react-redux"; +//import { createFFmpeg, FFmpeg } from "@ffmpeg/ffmpeg" +import { readFileSync } from "fs"; +import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg'; +import { fs } from 'memfs'; + import { selectPlaying, @@ -11,12 +16,15 @@ import { PlayerPlayingAction, PlayerSecondsAction, } from "./PlayerSlice"; -import { selectUrl } from "../url/UrlSlice"; +import { selectSubtibleFile, selectUrl } from "../url/UrlSlice"; +import { TableBody } from "@material-ui/core"; +import { findNonSerializableValue } from "@reduxjs/toolkit"; interface StateProps { playing: boolean; seconds: number; url: string; + subtitleFile: string; } interface DispatchProps { @@ -53,7 +61,7 @@ class Player extends React.Component<Props> { this.props.setSeconds(seconds); } - componentDidUpdate() { + async componentDidUpdate() { if (this.playerref.current === null) return; if (!ReactPlayer.canPlay(this.props.url)) return; @@ -62,20 +70,24 @@ class Player extends React.Component<Props> { if ( Math.abs(this.playerref.current.getCurrentTime() - this.props.seconds) <= 0.05 - ) + ) { return; - /* - */ + } // Enough difference now seek to it! this.playerref.current.seekTo(this.props.seconds); } render() { + + let url = this.props.url || "?" + let subs = this.props.subtitleFile || "?" + return ( <div style={{ position: "relative", paddingTop: "56.25%" }}> <ReactPlayer - url={this.props.url || "?"} + key={url + subs} + url={url} style={{ position: "absolute", top: 0, @@ -91,6 +103,11 @@ class Player extends React.Component<Props> { onPause={this.props.pause} onSeek={this.handleSeek} ref={this.playerref} + config={{ file: { + tracks: [ + {kind: 'subtitles', src: subs, srcLang: 'en', default: true, label: 'en'}, + ] + }}} /> </div> ); @@ -101,6 +118,7 @@ const mapStateToProps = (state: RootState): StateProps => ({ playing: selectPlaying(state), seconds: selectSeconds(state), url: selectUrl(state), + subtitleFile: selectSubtibleFile(state), }); type Dispatch = (action: PlayerPlayingAction | PlayerSecondsAction) => {}; diff --git a/frontend/src/url/UrlBox.tsx b/frontend/src/url/UrlBox.tsx index 0a723b8..93a9c97 100644 --- a/frontend/src/url/UrlBox.tsx +++ b/frontend/src/url/UrlBox.tsx @@ -2,8 +2,10 @@ import React from "react"; import { RootState } from "../store/store"; import { connect } from "react-redux"; -import { UrlAction, selectUrl, setUrl } from "./UrlSlice"; +import { UrlAction, selectUrl, setUrl, setSubtitleFile } from "./UrlSlice"; import { Button, Grid, TextField } from "@material-ui/core"; +import { createFFmpeg, fetchFile } from "@ffmpeg/ffmpeg"; +import { fs } from "memfs"; interface StateProps { url: string; @@ -11,6 +13,7 @@ interface StateProps { interface DispatchProps { dispatchUrl: (url: string) => void; + dispatchSubtitleFile: (file: string) => void; } export type Props = StateProps & DispatchProps; @@ -21,6 +24,7 @@ class UrlBox extends React.Component<Props> { this.handleChange = this.handleChange.bind(this); this.handleServerClick = this.handleServerClick.bind(this); this.handleInput = this.handleInput.bind(this); + this.handleSubtitleClick = this.handleSubtitleClick.bind(this); } handleChange(event: React.ChangeEvent<HTMLInputElement>): void | undefined { @@ -31,6 +35,37 @@ class UrlBox extends React.Component<Props> { this.props.dispatchUrl("https://mpvsync.de/video/index.mp4"); } + async handleSubtitleClick(): Promise<void | undefined> { + + try { + + //let buffer = new Uint8Array(readFileSync(this.props.url)) + const ffmpeg = createFFmpeg({ + log: true, + }); + await ffmpeg.load() + + // load file in browser + ffmpeg.FS('writeFile', 'video.avi', await fetchFile(this.props.url)) + // convert to vtt file + await ffmpeg.run('-i', 'video.avi', 'outfile.vtt') + // free memory from video file + ffmpeg.FS('unlink', 'video.avi'); + + // create blob-url to read from + let data: Uint8Array = ffmpeg.FS('readFile', 'outfile.vtt') + let blob = new Blob([data]) + let u = URL.createObjectURL(blob) + + this.props.dispatchSubtitleFile(u); + + } catch (error) { + console.log(error) + } + + + } + handleInput(event: React.ChangeEvent<HTMLInputElement>): void | undefined { if (event.target.files === null || event.target.files.length < 1) { return; @@ -45,7 +80,7 @@ class UrlBox extends React.Component<Props> { render() { return ( <Grid container alignItems="center" spacing={2}> - <Grid item xs={8}> + <Grid item xs={6}> <TextField label="Source URL" variant="outlined" @@ -59,7 +94,7 @@ class UrlBox extends React.Component<Props> { hidden id="contained-button-file" type="file" - accept="video/*" + accept="*" onInput={this.handleInput} /> <label htmlFor="contained-button-file"> @@ -83,6 +118,16 @@ class UrlBox extends React.Component<Props> { Stream from server </Button> </Grid> + <Grid item xs={2}> + <Button + size="large" + variant="outlined" + fullWidth + onClick={this.handleSubtitleClick} + > + Extract subtitles (beta) + </Button> + </Grid> </Grid> ); } @@ -98,6 +143,9 @@ const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ dispatchUrl(url: string): void { dispatch(setUrl(url)); }, + dispatchSubtitleFile(file: string): void { + dispatch(setSubtitleFile(file)); + }, }); export default connect(mapStateToProps, mapDispatchToProps)(UrlBox); diff --git a/frontend/src/url/UrlSlice.ts b/frontend/src/url/UrlSlice.ts index 6c9bf57..58c5a16 100644 --- a/frontend/src/url/UrlSlice.ts +++ b/frontend/src/url/UrlSlice.ts @@ -3,10 +3,12 @@ import { RootState } from "../store/store"; export interface UrlState { url: string; + subtitleFile: string; } const initialState: UrlState = { url: "", + subtitleFile: "", }; export type UrlAction = PayloadAction<string>; @@ -16,15 +18,19 @@ export const UrlSlice = createSlice({ initialState, reducers: { setUrl: (state: UrlState, action: UrlAction): void => { - console.log(action.payload); + console.log("set url: " + action.payload); state.url = action.payload; - console.log(state.url); }, + setSubtitleFile: (state: UrlState, action: UrlAction): void => { + console.log("set subtitles: " + action.payload); + state.subtitleFile = action.payload; + } }, }); -export const { setUrl } = UrlSlice.actions; +export const { setUrl, setSubtitleFile } = UrlSlice.actions; export const selectUrl = (state: RootState) => state.url.url; +export const selectSubtibleFile = (state: RootState) => state.url.subtitleFile; export default UrlSlice.reducer; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index a0553fb..e88c508 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -18,4 +18,4 @@ "jsx": "react" }, "include": ["src"] -} +} \ No newline at end of file -- GitLab