port (working!) webvtt-player over entirely

This commit is contained in:
dukeeagle 2022-12-30 23:08:06 -06:00
parent dfe3eeb7a5
commit e04dd91fd6
15 changed files with 437 additions and 5 deletions

View File

@ -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]
{"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": ""}

View File

@ -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>
<div>
{/* div "control-panel" has multiple buttons for displaying settings on WebVttPlayer */}
<div id="control-panel" className="flex flex-row">
<button id="show-metadata" className="bg-gray-200 hover:bg-gray-400 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-800 dark:text-gray-200 py-2 px-4">Show Metadata</button>
</div>
<WebVttPlayer
// className="custom-webvtt-player"
preload={false}
audio={audioUrl}
transcript={transcriptUrl}
metadata={metadataUrl} />

View File

@ -0,0 +1,34 @@
import React, { Component } from 'react'
import MetadataPoint from './MetadataPoint'
class Metadata extends Component<MetadataProps> {
render() {
const lines = []
if (this.props.track && this.props.track.cues) {
for (let i = 0; i < this.props.track.cues.length; i++) {
lines.push(
<MetadataPoint
key={`point-${i}`}
cue={this.props.track.cues[i]}
active={false}
seek={this.props.seek} />
)
}
}
return (
<div className="track">
{lines}
</div>
)
}
}
type MetadataProps = {
url: string,
track: TextTrack,
seek: (time: number) => void
}
export default Metadata

View File

@ -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<MetadataPointProps> {
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
? <h3 className="titleAlt">{data.title_alt}</h3>
: ""
const synopsis = data.synopsis
? <div className="field">
<span>Synopsis</span>
{data.synopsis}
</div>
: ""
const synopsisAlt = data.synopsis_alt
? <div>{data.synopsis_alt}</div>
: ""
const keywords = data.keywords
? <div className="field">
<span>Keywords: </span>
{data.keywords}
</div>
: ""
const keywordsAlt = data.keywords_alt
? <div className="field">
<span>Alternative Keywords: </span>
{data.keywords_alt}
</div>
: ""
const subjects = data.subjects
? <div className="field">
<span>Subjects: </span>
{data.subjects}
</div>
: ""
const subjectsAlt = data.subjects_alt
? <div className="field">
<span>Alternative Subjects: </span>
{data.subjects_alt}
</div>
: ""
const gpsLink = data.gpspoints.gps
? <div className="field">
<span>Geo: </span>
<a href={`https://www.google.com/maps/@?api=1&map_action=map&center=${data.gpspoints.gps}&zoom=${data.gpspoints.gps_zoom}`}>{data.gpspoints.gps_text}</a>
</div>
: ""
const hyperlinks = data.hyperlinks.hyperlink_text
? <div className="field">
<span>Links: </span>
<a href={data.hyperlinks.hyperlink}>{data.hyperlinks.hyperlink_text}</a>
</div>
: ""
return (
<div className="point">
<div className="time" onClick={this.onClick}>
[{this.startTime()}]
</div>
<div className="text">
<h2 className="title" onClick={this.onClick}>{data.title}</h2>
{titleAlt}
{synopsis}
{synopsisAlt}
{keywords}
{keywordsAlt}
{subjects}
{subjectsAlt}
{gpsLink}
{hyperlinks}
</div>
</div>
)
}
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

View File

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

View File

@ -0,0 +1,24 @@
import React, { FunctionComponent } from "react" // TODO: fix warning
import styles from "./Search.module.css"
const Search: FunctionComponent<SearchProps> = ({ query, updateQuery }) => {
return (
<div className={styles.search}>
<div className={styles.container}>
<span className={styles.icon}>🔍</span>
<input
value={query}
onChange={e => updateQuery(e.target.value)} />
</div>
</div>
)
}
// set prop types using TypeScript
type SearchProps = {
query: string,
updateQuery: (query: string) => void
}
export default Search

View File

@ -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<TranscriptProps> {
render() {
const lines = []
if (this.props.track && this.props.track.cues) {
for (let i = 0; i < this.props.track.cues.length; i++) {
lines.push(
<TranscriptLine
key={`line-${i}`}
cue={this.props.track.cues[i]}
active={false}
seek={this.props.seek}
query={this.props.query} />
)
}
}
return (
<div className="track">
{lines}
</div>
)
}
}
type TranscriptProps = {
track: TextTrack,
url: string,
seek: (time: number) => void,
query: string
}
export default Transcript

View File

@ -0,0 +1,79 @@
import React, { Component } from 'react'
import './TranscriptLine.module.css'
class TranscriptLine extends Component<TranscriptLineProps, { isActive: boolean }> {
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 (
<div className={`${style} line`} onClick={this.onClick}>
<div className="time">
[{this.startTime()} - {this.endTime()}]
</div>
<div
className={`${style} text`}
dangerouslySetInnerHTML={{__html: this.props.cue.text}} />
</div>
)
}
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

View File

@ -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<WebVttProps, { loaded: boolean, currentTime: number, query: string }> {
metatrack: React.RefObject<unknown>
audio: React.RefObject<unknown>
track: React.RefObject<unknown>
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
? <Metadata
url={this.props.metadata}
seek={this.seek}
track={metatrack} />
: ""
return (
<div className="webvtt-player">
<div className="media">
<div className="player">
<audio
controls
crossOrigin="anonymous"
onLoad={this.onLoaded}
preload={preload}
ref={this.audio}>
<source src={this.props.audio} />
<track default
kind="subtitles"
src={this.props.transcript}
ref={this.track} />
<track default
kind="metadata"
src={this.props.metadata}
ref={this.metatrack} />
</audio>
</div>
<div className="tracks">
<Transcript
url={this.props.transcript}
seek={this.seek}
track={track}
query={this.state.query} />
{metadata}
</div>
<Search query={this.state.query} updateQuery={this.updateQuery} />
</div>
</div>
)
}
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

View File

@ -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 */
}
}