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