mirror of
https://github.com/mit-regressions/viewer.git
synced 2025-04-19 02:20:17 -04:00
make functioning video seeker via vanilla Video elem
This commit is contained in:
parent
e04dd91fd6
commit
44724a7a6a
|
@ -1,53 +1,29 @@
|
||||||
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 { useRouter } from 'next/router';
|
||||||
import WebVttPlayer from "./WebVttPlayer/WebVttPlayer";
|
import WebVttPlayerFunctional from "./WebVttPlayer/WebVttPlayerFunctional";
|
||||||
|
|
||||||
// functional component PlayerReact that uses ReactPlayer
|
// functional component PlayerReact that uses ReactPlayer
|
||||||
// TODO: parameterize video source and VTT source with props (general spec for 3rd party use!). must define spec.
|
// TODO: better parameterize video source and VTT source with props (general spec for 3rd party use!). must define spec.
|
||||||
export default function Player() {
|
export default function Player() {
|
||||||
|
|
||||||
// get files in directory "../../data" without using fs
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const videoUrl = '/data/MIT Regressions intro video.mp4' // BIZARRE BUG: react-player component only works when I live-reload URL to valid path. if Chrome loads directly, then returns "failed to load media".
|
||||||
const audioUrl = router.asPath + 'data/MIT Regressions intro audio.mp3'
|
const audioUrl = router.asPath + 'data/MIT Regressions intro audio.mp3'
|
||||||
const transcriptUrl = router.asPath + "data/MIT Regressions intro captions.vtt"
|
const transcriptUrl = router.asPath + "data/MIT Regressions intro captions.vtt"
|
||||||
const metadataUrl = router.asPath + "data/MIT Regressions intro metadata.vtt"
|
const metadataUrl = router.asPath + "data/MIT Regressions intro metadata.vtt"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="player-wrapper">
|
<div id="control-panel" className="flex flex-row">
|
||||||
<ReactPlayer
|
|
||||||
className="react-player"
|
|
||||||
url="https://youtu.be/TGKk3iwoI9I"
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
config={{
|
|
||||||
file: {
|
|
||||||
tracks: [
|
|
||||||
{
|
|
||||||
kind: 'subtitles',
|
|
||||||
label: 'test',
|
|
||||||
src: 'subs/mit regressions subtitles unfinished.vtt',
|
|
||||||
srcLang: 'en',
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<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>
|
<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
|
|
||||||
preload={false}
|
|
||||||
audio={audioUrl}
|
|
||||||
transcript={transcriptUrl}
|
|
||||||
metadata={metadataUrl} />
|
|
||||||
</div>
|
</div>
|
||||||
|
<WebVttPlayerFunctional
|
||||||
|
preload={false}
|
||||||
|
audio={audioUrl}
|
||||||
|
videoUrl={videoUrl}
|
||||||
|
transcript={transcriptUrl}
|
||||||
|
metadataUrl={metadataUrl}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
38
viewer/src/components/VideoPlayer.tsx
Normal file
38
viewer/src/components/VideoPlayer.tsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import ReactPlayer from "react-player/lazy";
|
||||||
|
// import CustomReactPlayer from "./CustomReactPlayer";
|
||||||
|
|
||||||
|
interface PlayerRef {
|
||||||
|
seeking: boolean;
|
||||||
|
played: number;
|
||||||
|
duration: number;
|
||||||
|
seekTo: (time: number) => void;
|
||||||
|
}
|
||||||
|
// inherit prop playing from parent component
|
||||||
|
export default function VideoPlayer({ playerRef, playing, videoUrl, transcriptUrl }: { playerRef: React.RefObject<PlayerRef>, playing: boolean, videoUrl: string, transcriptUrl: string }) {
|
||||||
|
// ReactPlayer.addCustomPlayer(CustomReactPlayer);
|
||||||
|
return (
|
||||||
|
<ReactPlayer
|
||||||
|
ref={playerRef}
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
playing={playing}
|
||||||
|
controls={true}
|
||||||
|
// url="https://youtu.be/TGKk3iwoI9I"
|
||||||
|
url={videoUrl}
|
||||||
|
onSeek={e => console.log('onSeek', e)}
|
||||||
|
config={{
|
||||||
|
file: {
|
||||||
|
forceVideo: true,
|
||||||
|
tracks: [
|
||||||
|
{
|
||||||
|
label: 'English',
|
||||||
|
kind: 'captions',
|
||||||
|
src: transcriptUrl,
|
||||||
|
srcLang: 'en',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
import TranscriptLine from './TranscriptLine'
|
import TranscriptLine from './TranscriptLine'
|
||||||
// import './Track.css' // currently exists in global instead. gotta consolidate this
|
// import './Track.css' // currently exists in global instead. TODO: consolidate this styling
|
||||||
|
|
||||||
class Transcript extends Component<TranscriptProps> {
|
class Transcript extends Component<TranscriptProps> {
|
||||||
|
|
||||||
|
|
|
@ -4,32 +4,47 @@ import Metadata from './Metadata'
|
||||||
import Search from './Search'
|
import Search from './Search'
|
||||||
import './WebVttPlayer.module.css'
|
import './WebVttPlayer.module.css'
|
||||||
|
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
const ReactPlayer = dynamic(() => import("react-player/lazy"), { ssr: false });
|
||||||
|
|
||||||
// type for props
|
// type for props
|
||||||
type WebVttProps = {
|
type WebVttProps = {
|
||||||
audio: string,
|
video: string,
|
||||||
transcript: string,
|
transcript: string,
|
||||||
metadata: string,
|
metadata: string,
|
||||||
preload: boolean
|
preload: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class WebVttPlayer extends Component<WebVttProps, { loaded: boolean, currentTime: number, query: string }> {
|
interface VideoRef {
|
||||||
|
seeking: boolean;
|
||||||
|
played: number;
|
||||||
|
duration: number;
|
||||||
|
seekTo: (time: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class WebVttPlayer extends Component<WebVttProps, { loaded: boolean, currentTime: number, query: string, seeking: boolean, played: Float64Array, playing: boolean }> {
|
||||||
metatrack: React.RefObject<unknown>
|
metatrack: React.RefObject<unknown>
|
||||||
|
video: React.RefObject<unknown>
|
||||||
audio: React.RefObject<unknown>
|
audio: React.RefObject<unknown>
|
||||||
track: React.RefObject<unknown>
|
track: React.RefObject<unknown>
|
||||||
|
|
||||||
|
|
||||||
constructor(props: WebVttProps) {
|
constructor(props: WebVttProps) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = {
|
this.state = {
|
||||||
loaded: false,
|
loaded: false,
|
||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
query: ''
|
query: '',
|
||||||
|
seeking: false,
|
||||||
|
played: new Float64Array(0),
|
||||||
|
playing: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.track = React.createRef()
|
this.track = React.createRef()
|
||||||
this.metatrack = React.createRef()
|
this.metatrack = React.createRef()
|
||||||
this.audio = React.createRef()
|
this.audio = React.createRef()
|
||||||
|
this.video = React.createRef();
|
||||||
|
// const playerRef=useRef();
|
||||||
|
|
||||||
this.onLoaded = this.onLoaded.bind(this)
|
this.onLoaded = this.onLoaded.bind(this)
|
||||||
this.seek = this.seek.bind(this)
|
this.seek = this.seek.bind(this)
|
||||||
|
@ -41,12 +56,19 @@ class WebVttPlayer extends Component<WebVttProps, { loaded: boolean, currentTime
|
||||||
this.checkIfLoaded()
|
this.checkIfLoaded()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handlePause = () => {
|
||||||
|
console.log('onPause')
|
||||||
|
this.setState({ playing: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let track = null
|
let track = null
|
||||||
let metatrack = null
|
let metatrack = null
|
||||||
if (this.state.loaded) {
|
if (this.state.loaded) {
|
||||||
track = this.track.current.track
|
track = this.track.current.track
|
||||||
metatrack = this.metatrack.current.track
|
metatrack = this.metatrack.current.track
|
||||||
|
console.log("loaded video.current : ", this.video.current);
|
||||||
}
|
}
|
||||||
const preload = this.props.preload ? "true" : "false"
|
const preload = this.props.preload ? "true" : "false"
|
||||||
const metadata = this.props.metadata
|
const metadata = this.props.metadata
|
||||||
|
@ -60,6 +82,18 @@ class WebVttPlayer extends Component<WebVttProps, { loaded: boolean, currentTime
|
||||||
<div className="webvtt-player">
|
<div className="webvtt-player">
|
||||||
<div className="media">
|
<div className="media">
|
||||||
<div className="player">
|
<div className="player">
|
||||||
|
|
||||||
|
<ReactPlayer
|
||||||
|
// a ref that works in our class component
|
||||||
|
ref={this.video}
|
||||||
|
controls={true}
|
||||||
|
onPause={this.handlePause}
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
className="react-player"
|
||||||
|
url="https://youtu.be/TGKk3iwoI9I"
|
||||||
|
onSeek={e => console.log('onSeek', e)}
|
||||||
|
onProgress={this.handleProgress}
|
||||||
|
/>
|
||||||
<audio
|
<audio
|
||||||
controls
|
controls
|
||||||
crossOrigin="anonymous"
|
crossOrigin="anonymous"
|
||||||
|
@ -106,9 +140,38 @@ class WebVttPlayer extends Component<WebVttProps, { loaded: boolean, currentTime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleSeekMouseDown = e => {
|
||||||
|
// this.setState({ seeking: true })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// handleSeekChange = e => {
|
||||||
|
// this.setState({ played: parseFloat(e.target.value) })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// handleSeekMouseUp = e => {
|
||||||
|
// this.setState({ seeking: false })
|
||||||
|
// this.player.seekTo(parseFloat(e.target.value))
|
||||||
|
// }
|
||||||
|
|
||||||
|
handleProgress = state => {
|
||||||
|
console.log('onProgress', state)
|
||||||
|
// We only want to update time slider if we are not currently seeking
|
||||||
|
if (!this.state.seeking) {
|
||||||
|
this.setState(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ORIGINAL
|
||||||
seek(secs: number) {
|
seek(secs: number) {
|
||||||
|
// scrub audio
|
||||||
this.audio.current.currentTime = secs
|
this.audio.current.currentTime = secs
|
||||||
this.audio.current.play()
|
this.audio.current.play()
|
||||||
|
|
||||||
|
// scrub video
|
||||||
|
this.setState({ seeking: true })
|
||||||
|
console.log("this.video", this.video);
|
||||||
|
this.video.current?.seekTo(parseFloat(secs)) // TODO: should probs refactor ref to this.player
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateQuery(query: string) {
|
updateQuery(query: string) {
|
||||||
|
|
144
viewer/src/components/WebVttPlayer/WebVttPlayerFunctional.tsx
Normal file
144
viewer/src/components/WebVttPlayer/WebVttPlayerFunctional.tsx
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
import { useLayoutEffect, useState, useEffect, useRef } from 'react';
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
// const ReactPlayer = dynamic(() => import("react-player/lazy"), { ssr: false });
|
||||||
|
import Transcript from './Transcript'
|
||||||
|
import Metadata from './Metadata'
|
||||||
|
import Search from './Search'
|
||||||
|
const VideoPlayer = dynamic(() => import("../VideoPlayer"), { ssr: false });
|
||||||
|
|
||||||
|
type WebVttPlayerFunctionalProps = {
|
||||||
|
audio: string,
|
||||||
|
videoUrl: string, // TODO: make naming scheme consistent lol
|
||||||
|
transcript: string,
|
||||||
|
metadataUrl: string,
|
||||||
|
preload: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ReactPlayerRef {
|
||||||
|
seeking: boolean;
|
||||||
|
played: number;
|
||||||
|
duration: number;
|
||||||
|
seekTo: (time: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NativePlayerRef {
|
||||||
|
currentTime: number;
|
||||||
|
ended: boolean;
|
||||||
|
loop: boolean;
|
||||||
|
muted: boolean;
|
||||||
|
play: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WebVttPlayerFunctional(props: WebVttPlayerFunctionalProps) {
|
||||||
|
const [trackLoaded, setTrackLoaded] = useState(false);
|
||||||
|
const [metatrackLoaded, setMetatrackLoaded] = useState(false);
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
|
||||||
|
// TODO: determine if these should be set
|
||||||
|
const [currentTime, setCurrentTime] = useState(0);
|
||||||
|
const [seeking, setSeeking] = useState(false);
|
||||||
|
const [played, setPlayed] = useState(new Float64Array(0));
|
||||||
|
const [playing, setPlaying] = useState(false);
|
||||||
|
|
||||||
|
const trackRef = useRef(null);
|
||||||
|
const metatrackRef = useRef(null);
|
||||||
|
|
||||||
|
const reactPlayerRef = useRef<ReactPlayerRef>(null);
|
||||||
|
const nativePlayerRef = useRef<NativePlayerRef>(null);
|
||||||
|
|
||||||
|
const preload = props.preload ? "true" : "false"
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
|
||||||
|
// Get a reference to the track and metatrack elements
|
||||||
|
const track = trackRef.current;
|
||||||
|
const metatrack = metatrackRef.current;
|
||||||
|
|
||||||
|
// Check if the tracks are fully loaded
|
||||||
|
if (track && track.track && track.track.cues && track.track.cues.length > 0) {
|
||||||
|
setTrackLoaded(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metatrack && metatrack.track && metatrack.track.cues && metatrack.track.cues.length > 0) {
|
||||||
|
setMetatrackLoaded(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [trackLoaded, metatrackLoaded]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// if we figure out how to get access to Track refs with react-player (even in YouTube videos! That would be awesome), then we can start using this
|
||||||
|
function reactPlayerSeek(secs: string) {
|
||||||
|
|
||||||
|
if (reactPlayerRef.current) {
|
||||||
|
reactPlayerRef.current.seekTo(parseFloat(secs))
|
||||||
|
}
|
||||||
|
setPlaying(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function seek(secs: number) {
|
||||||
|
if (nativePlayerRef.current) {
|
||||||
|
|
||||||
|
nativePlayerRef.current.currentTime = secs;
|
||||||
|
nativePlayerRef.current.play();
|
||||||
|
}
|
||||||
|
setPlaying(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="webvtt-player">
|
||||||
|
<div className="media">
|
||||||
|
<div className="player">
|
||||||
|
{/* <VideoPlayer playerRef={video} playing={playing} videoUrl={props.videoUrl} transcriptUrl={props.transcript} /> */}
|
||||||
|
{/* a vanilla video element with source and tracks. so much easier oh my god */}
|
||||||
|
<video
|
||||||
|
preload={preload}
|
||||||
|
ref={nativePlayerRef}
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
controls={true}
|
||||||
|
>
|
||||||
|
<source src={props.videoUrl} type="video/mp4" />
|
||||||
|
<track
|
||||||
|
ref={trackRef}
|
||||||
|
kind="subtitles"
|
||||||
|
src={props.transcript}
|
||||||
|
srcLang="en"
|
||||||
|
default={true}
|
||||||
|
/>
|
||||||
|
<track default
|
||||||
|
kind="metadata"
|
||||||
|
src={props.metadataUrl}
|
||||||
|
ref={metatrackRef} />
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
<div className="tracks">
|
||||||
|
{trackLoaded ? (
|
||||||
|
<>
|
||||||
|
<Transcript
|
||||||
|
url={props.transcript}
|
||||||
|
seek={seek}
|
||||||
|
track={trackRef.current.track}
|
||||||
|
query={query}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Loading transcript..."
|
||||||
|
)}
|
||||||
|
{metatrackLoaded && props.metadataUrl ? (
|
||||||
|
<>
|
||||||
|
<Metadata
|
||||||
|
url={props.metadataUrl}
|
||||||
|
seek={seek}
|
||||||
|
track={metatrackRef.current.track}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"\nLoading metadata..." // TODO: make better logic for showing if we wanna serve metadata
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,17 +1,18 @@
|
||||||
import React from "react";
|
import React, { useRef } from "react";
|
||||||
import { type NextPage } from "next";
|
import { type NextPage } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
|
||||||
import { signIn, signOut, useSession } from "next-auth/react";
|
import { signIn, signOut, useSession } from "next-auth/react";
|
||||||
|
|
||||||
import { trpc } from "../utils/trpc";
|
import { trpc } from "../utils/trpc";
|
||||||
import {useTheme} from 'next-themes'
|
import { useTheme } from 'next-themes'
|
||||||
import Player from '../components/Player'
|
import Player from '../components/Player'
|
||||||
|
|
||||||
|
|
||||||
const Home: NextPage = () => {
|
const Home: NextPage = () => {
|
||||||
|
|
||||||
const hello = trpc.example.hello.useQuery({ text: "from tRPC" });
|
const hello = trpc.example.hello.useQuery({ text: "from tRPC" });
|
||||||
const {theme, setTheme} = useTheme()
|
const { theme, setTheme } = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -21,9 +22,9 @@ const Home: NextPage = () => {
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<main className="flex min-h-screen flex-col items-center justify-center bg-white dark:bg-black">
|
<main className="flex min-h-screen flex-col items-center justify-center bg-white dark:bg-black">
|
||||||
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} className="bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-200 py-2 px-4 absolute top-4 right-4">
|
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} className="bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-200 py-2 px-4 absolute top-4 right-4">
|
||||||
toggle theme
|
toggle theme
|
||||||
</button>
|
</button>
|
||||||
<div className="container flex flex-col items-center justify-center gap-12 px-4 py-16 ">
|
<div className="container flex flex-col items-center justify-center gap-12 px-4 py-16 ">
|
||||||
<h1 className="text-1xl font-extrabold tracking-tight text-black dark:text-white sm:text-[3rem]">
|
<h1 className="text-1xl font-extrabold tracking-tight text-black dark:text-white sm:text-[3rem]">
|
||||||
viewer
|
viewer
|
||||||
|
@ -32,9 +33,6 @@ const Home: NextPage = () => {
|
||||||
<Player />
|
<Player />
|
||||||
|
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<p className="text-2xl text-black dark:text-white">
|
|
||||||
{hello.data ? hello.data.greeting : "Loading tRPC query..."}
|
|
||||||
</p>
|
|
||||||
<AuthShowcase />
|
<AuthShowcase />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user