diff --git a/viewer/public/data/MIT Regressions intro metadata.vtt b/viewer/public/data/MIT Regressions intro metadata.vtt index ba9e4e6..1b0301f 100644 --- a/viewer/public/data/MIT Regressions intro metadata.vtt +++ b/viewer/public/data/MIT Regressions intro metadata.vtt @@ -4,5 +4,6 @@ Language: en NOTE We could have a separation between MUSIC SOURCE, VIDEO SOURCE, NARRATION SOURCE, COMMENTARY, and TRANSCRIPT. +NOTE ["The Vanishing American Family" by ScubaZ] 00:00:01.550 --> 00:01:15.448 -["The Vanishing American Family" by ScubaZ] \ No newline at end of file +{"keywords_alt": "", "gpspoints": {"gps_zoom": "11", "gps_text_alt": "", "gps_text": "Stockholm, Sweden", "gps": "59.3388502,18.0616981"}, "synopsis": "", "subjects": "", "hyperlinks": {"hyperlink_text_alt": "", "hyperlink_text": "", "hyperlink": ""}, "synopsis_alt": "", "title": "The Vanishing American Family", "keywords": "ScubaZ", "title_alt": "", "subjects_alt": ""} \ No newline at end of file diff --git a/viewer/src/components/Player.tsx b/viewer/src/components/Player.tsx index d156fa9..265dec2 100644 --- a/viewer/src/components/Player.tsx +++ b/viewer/src/components/Player.tsx @@ -2,7 +2,7 @@ import dynamic from "next/dynamic"; const ReactPlayer = dynamic(() => import("react-player/lazy"), { ssr: false }); import { useState, useRef, useEffect } from "react"; import { useRouter } from 'next/router'; -import { Player as WebVttPlayer } from "webvtt-player"; +import WebVttPlayer from "./WebVttPlayer/WebVttPlayer"; // functional component PlayerReact that uses ReactPlayer // TODO: parameterize video source and VTT source with props (general spec for 3rd party use!). must define spec. @@ -39,12 +39,11 @@ export default function Player() { />
- {/* div "control-panel" has multiple buttons for displaying settings on WebVttPlayer */}
diff --git a/viewer/src/components/WebVttPlayer/Metadata.module.css b/viewer/src/components/WebVttPlayer/Metadata.module.css new file mode 100644 index 0000000..e69de29 diff --git a/viewer/src/components/WebVttPlayer/Metadata.tsx b/viewer/src/components/WebVttPlayer/Metadata.tsx new file mode 100644 index 0000000..0c1091e --- /dev/null +++ b/viewer/src/components/WebVttPlayer/Metadata.tsx @@ -0,0 +1,34 @@ +import React, { Component } from 'react' +import MetadataPoint from './MetadataPoint' + +class Metadata extends Component { + + render() { + const lines = [] + if (this.props.track && this.props.track.cues) { + for (let i = 0; i < this.props.track.cues.length; i++) { + lines.push( + + ) + } + } + return ( +
+ {lines} +
+ ) + } + +} + +type MetadataProps = { + url: string, + track: TextTrack, + seek: (time: number) => void +} + +export default Metadata \ No newline at end of file diff --git a/viewer/src/components/WebVttPlayer/MetadataPoint.module.css b/viewer/src/components/WebVttPlayer/MetadataPoint.module.css new file mode 100644 index 0000000..e69de29 diff --git a/viewer/src/components/WebVttPlayer/MetadataPoint.tsx b/viewer/src/components/WebVttPlayer/MetadataPoint.tsx new file mode 100644 index 0000000..4a36ef2 --- /dev/null +++ b/viewer/src/components/WebVttPlayer/MetadataPoint.tsx @@ -0,0 +1,113 @@ +import React, { Component } from 'react' +// import './MetadataPoint.css' + + +type MetadataPointProps = { + cue: VTTCue + active: boolean + seek: (time: number) => void +} + +class MetadataPoint extends Component { + + constructor(props: MetadataPointProps) { + super(props) + this.onClick = this.onClick.bind(this) + } + + render() { + const data = JSON.parse(this.props.cue.text) + const titleAlt = data.title_alt + ?

{data.title_alt}

+ : "" + const synopsis = data.synopsis + ?
+ Synopsis + {data.synopsis} +
+ : "" + const synopsisAlt = data.synopsis_alt + ?
{data.synopsis_alt}
+ : "" + const keywords = data.keywords + ?
+ Keywords: + {data.keywords} +
+ : "" + const keywordsAlt = data.keywords_alt + ?
+ Alternative Keywords: + {data.keywords_alt} +
+ : "" + const subjects = data.subjects + ?
+ Subjects: + {data.subjects} +
+ : "" + const subjectsAlt = data.subjects_alt + ?
+ Alternative Subjects: + {data.subjects_alt} +
+ : "" + const gpsLink = data.gpspoints.gps + ? + : "" + const hyperlinks = data.hyperlinks.hyperlink_text + ? + : "" + return ( +
+
+ [{this.startTime()}] +
+
+

{data.title}

+ {titleAlt} + {synopsis} + {synopsisAlt} + {keywords} + {keywordsAlt} + {subjects} + {subjectsAlt} + {gpsLink} + {hyperlinks} +
+
+ ) + } + + onClick() { + this.props.seek(this.props.cue.startTime) + } + + startTime() { + return this.formatSeconds(this.props.cue.startTime) + } + + formatSeconds(t) { + let mins = Math.floor(t / 60) + if (mins < 10) { + mins = `0${mins}` + } + + let secs = Math.floor(t % 60) + if (secs < 10) { + secs = `0${secs}` + } + + return `${mins}:${secs}` + } + +} + +export default MetadataPoint \ No newline at end of file diff --git a/viewer/src/components/WebVttPlayer/Search.module.css b/viewer/src/components/WebVttPlayer/Search.module.css new file mode 100644 index 0000000..8c57aab --- /dev/null +++ b/viewer/src/components/WebVttPlayer/Search.module.css @@ -0,0 +1,25 @@ +.search { + border-top: thin solid #ccc; + padding: 5px 5px 5px 5px; + font-size: 14pt; + margin-top: 5px; + } + + .search .container { + border: thin solid #ccc; + } + + .search input { + font-size: 12pt; + border: none; + width: 90%; + } + + .search input:focus { + outline: none; + } + + .search .icon { + padding-left: 5px; + padding-right: 5px; + } \ No newline at end of file diff --git a/viewer/src/components/WebVttPlayer/Search.tsx b/viewer/src/components/WebVttPlayer/Search.tsx new file mode 100644 index 0000000..599ba8f --- /dev/null +++ b/viewer/src/components/WebVttPlayer/Search.tsx @@ -0,0 +1,24 @@ +import React, { FunctionComponent } from "react" // TODO: fix warning +import styles from "./Search.module.css" + +const Search: FunctionComponent = ({ query, updateQuery }) => { + return ( +
+
+ 🔍 + updateQuery(e.target.value)} /> +
+
+ ) +} + +// set prop types using TypeScript +type SearchProps = { + query: string, + updateQuery: (query: string) => void +} + + +export default Search \ No newline at end of file diff --git a/viewer/src/components/WebVttPlayer/Transcript.module.css b/viewer/src/components/WebVttPlayer/Transcript.module.css new file mode 100644 index 0000000..e69de29 diff --git a/viewer/src/components/WebVttPlayer/Transcript.tsx b/viewer/src/components/WebVttPlayer/Transcript.tsx new file mode 100644 index 0000000..70e82ce --- /dev/null +++ b/viewer/src/components/WebVttPlayer/Transcript.tsx @@ -0,0 +1,37 @@ +import React, { Component } from 'react' +import TranscriptLine from './TranscriptLine' +// import './Track.css' // currently exists in global instead. gotta consolidate this + +class Transcript extends Component { + + render() { + const lines = [] + if (this.props.track && this.props.track.cues) { + for (let i = 0; i < this.props.track.cues.length; i++) { + lines.push( + + ) + } + } + return ( +
+ {lines} +
+ ) + } + +} + +type TranscriptProps = { + track: TextTrack, + url: string, + seek: (time: number) => void, + query: string +} + +export default Transcript \ No newline at end of file diff --git a/viewer/src/components/WebVttPlayer/TranscriptLine.module.css b/viewer/src/components/WebVttPlayer/TranscriptLine.module.css new file mode 100644 index 0000000..e69de29 diff --git a/viewer/src/components/WebVttPlayer/TranscriptLine.tsx b/viewer/src/components/WebVttPlayer/TranscriptLine.tsx new file mode 100644 index 0000000..2e190e9 --- /dev/null +++ b/viewer/src/components/WebVttPlayer/TranscriptLine.tsx @@ -0,0 +1,79 @@ +import React, { Component } from 'react' +import './TranscriptLine.module.css' + +class TranscriptLine extends Component { + + constructor(props: TranscriptLineProps) { + super(props) + this.state = { + isActive: false + } + this.props.cue.onenter = this.onEnter.bind(this) + this.props.cue.onexit = this.onExit.bind(this) + this.onClick = this.onClick.bind(this) + } + + render() { + let style = '' + if (this.props.query && this.props.cue.text.match(new RegExp(this.props.query, 'i'))) { + style = 'match' + } else if (this.state.isActive) { + style = 'active' + } + + // note: dangerouslySetInnerHTML is used because the text may contain HTML + return ( +
+
+ [{this.startTime()} - {this.endTime()}] +
+
+
+ ) + } + + onEnter() { + this.setState({isActive: true}) + } + + onExit() { + this.setState({isActive: false}) + } + + onClick() { + this.props.seek(this.props.cue.startTime) + } + + startTime() { + return this.formatSeconds(this.props.cue.startTime) + } + + endTime() { + return this.formatSeconds(this.props.cue.endTime) + } + + formatSeconds(t) { + let mins = Math.floor(t / 60) + if (mins < 10) { + mins = `0${mins}` + } + + let secs = Math.floor(t % 60) + if (secs < 10) { + secs = `0${secs}` + } + + return `${mins}:${secs}` + } + +} + +type TranscriptLineProps = { + cue: TextTrackCue, + seek: (time: number) => void, + query: string +} + +export default TranscriptLine \ No newline at end of file diff --git a/viewer/src/components/WebVttPlayer/WebVttPlayer.module.css b/viewer/src/components/WebVttPlayer/WebVttPlayer.module.css new file mode 100644 index 0000000..e69de29 diff --git a/viewer/src/components/WebVttPlayer/WebVttPlayer.tsx b/viewer/src/components/WebVttPlayer/WebVttPlayer.tsx new file mode 100644 index 0000000..a70f740 --- /dev/null +++ b/viewer/src/components/WebVttPlayer/WebVttPlayer.tsx @@ -0,0 +1,120 @@ +import React, { Component } from 'react' +import Transcript from './Transcript' +import Metadata from './Metadata' +import Search from './Search' +import './WebVttPlayer.module.css' + +// type for props +type WebVttProps = { + audio: string, + transcript: string, + metadata: string, + preload: boolean +} + + +class WebVttPlayer extends Component { + metatrack: React.RefObject + audio: React.RefObject + track: React.RefObject + + + constructor(props: WebVttProps) { + super(props) + this.state = { + loaded: false, + currentTime: 0, + query: '' + } + + this.track = React.createRef() + this.metatrack = React.createRef() + this.audio = React.createRef() + + this.onLoaded = this.onLoaded.bind(this) + this.seek = this.seek.bind(this) + this.checkIfLoaded = this.checkIfLoaded.bind(this) + this.updateQuery = this.updateQuery.bind(this) + } + + componentDidMount() { + this.checkIfLoaded() + } + + render() { + let track = null + let metatrack = null + if (this.state.loaded) { + track = this.track.current.track + metatrack = this.metatrack.current.track + } + const preload = this.props.preload ? "true" : "false" + const metadata = this.props.metadata + ? + : "" + + return ( +
+
+
+ +
+
+ + {metadata} +
+ +
+
+ ) + } + + onLoaded() { + this.setState({ loaded: true }) + } + + checkIfLoaded(tries = 0) { + tries += 1 + const e = this.track.current + if (e && e.track && e.track.cues && e.track.cues.length > 0) { + this.onLoaded() + } else if (!this.state.loaded) { + const wait = 25 * Math.pow(tries, 2) + setTimeout(this.checkIfLoaded, wait, tries) + } + } + + seek(secs: number) { + this.audio.current.currentTime = secs + this.audio.current.play() + } + + updateQuery(query: string) { + this.setState({ query: query }) + } + +} + +export default WebVttPlayer \ No newline at end of file diff --git a/viewer/src/styles/globals.css b/viewer/src/styles/globals.css index 4c71417..1a82fef 100644 --- a/viewer/src/styles/globals.css +++ b/viewer/src/styles/globals.css @@ -20,7 +20,7 @@ /* tailwind flex-CUSTOMNUMBER */ /* @apply h-96 p-[15px] overflow-y-scroll flex-[6]; */ - @apply text-orange-200; + /* @apply text-orange-200; /* this works lol */ } }