From 782159c7a965203f4f134dabe13634e59b579cc7 Mon Sep 17 00:00:00 2001 From: Pinapelz Date: Wed, 15 Nov 2023 01:41:32 -0800 Subject: feat: add support for srv3 --- package.json | 3 + pnpm-lock.yaml | 49 ++++++ src/app/App.tsx | 288 ++++++++++++++++++++++++++++++----- src/app/components/VideoControls.tsx | 153 +++++++++++++++++++ 4 files changed, 456 insertions(+), 37 deletions(-) create mode 100644 src/app/components/VideoControls.tsx diff --git a/package.json b/package.json index d6858a3..971e5bf 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,14 @@ "lint": "next lint" }, "dependencies": { + "font-awesome": "^4.7.0", "next": "14.0.2", "react": "^18", "react-bootstrap": "^2.9.1", "react-dom": "^18", + "react-icons": "^4.12.0", "react-lrc": "^3.0.2", + "react-srv3": "^1.0.4", "react-toastify": "^9.1.3", "styled-components": "^6.1.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 90dcfe9..ae7b0c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + font-awesome: + specifier: ^4.7.0 + version: 4.7.0 next: specifier: 14.0.2 version: 14.0.2(react-dom@18.2.0)(react@18.2.0) @@ -17,9 +20,15 @@ dependencies: react-dom: specifier: ^18 version: 18.2.0(react@18.2.0) + react-icons: + specifier: ^4.12.0 + version: 4.12.0(react@18.2.0) react-lrc: specifier: ^3.0.2 version: 3.0.2(react-dom@18.2.0)(react@18.2.0) + react-srv3: + specifier: ^1.0.4 + version: 1.0.4(react@18.2.0) react-toastify: specifier: ^9.1.3 version: 9.1.3(react-dom@18.2.0)(react@18.2.0) @@ -1287,6 +1296,13 @@ packages: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} dev: true + /fast-xml-parser@3.21.1: + resolution: {integrity: sha512-FTFVjYoBOZTJekiUsawGsSYV9QL0A+zDYCRj7y34IO6Jg+2IMYEtQa+bbictpdpV8dHxXywqU7C0gRDEOFtBFg==} + hasBin: true + dependencies: + strnum: 1.0.5 + dev: false + /fastq@1.15.0: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} dependencies: @@ -1328,6 +1344,11 @@ packages: resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} dev: true + /font-awesome@4.7.0: + resolution: {integrity: sha512-U6kGnykA/6bFmg1M/oT9EkFeIYv7JlX3bozwQJWiiLz6L0w3F5vBVPxHlwyX/vtNq1ckcpRKOB9f2Qal/VtFpg==} + engines: {node: '>=0.10.3'} + dev: false + /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: @@ -1520,6 +1541,11 @@ packages: function-bind: 1.1.2 dev: true + /he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + dev: false + /ignore@5.2.4: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} @@ -2234,6 +2260,14 @@ packages: scheduler: 0.23.0 dev: false + /react-icons@4.12.0(react@18.2.0): + resolution: {integrity: sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==} + peerDependencies: + react: '*' + dependencies: + react: 18.2.0 + dev: false + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -2253,6 +2287,17 @@ packages: resize-observer-polyfill: 1.5.1 dev: false + /react-srv3@1.0.4(react@18.2.0): + resolution: {integrity: sha512-fKRX+F4d3gkzQ+VZxopovBO/aUV32s6Z+Rwt1SqmhFo7RSdFeXmhHuHx0jL/E99H7Y4mIMw4isplajGFfR2TnA==} + engines: {node: '>=10'} + peerDependencies: + react: ^16.0.0 + dependencies: + fast-xml-parser: 3.21.1 + he: 1.2.0 + react: 18.2.0 + dev: false + /react-toastify@9.1.3(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==} peerDependencies: @@ -2521,6 +2566,10 @@ packages: engines: {node: '>=8'} dev: true + /strnum@1.0.5: + resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} + dev: false + /styled-components@6.1.1(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-cpZZP5RrKRIClBW5Eby4JM1wElLVP4NQrJbJ0h10TidTyJf4SIIwa3zLXOoPb4gJi8MsJ8mjq5mu2IrEhZIAcQ==} engines: {node: '>= 16'} diff --git a/src/app/App.tsx b/src/app/App.tsx index 871e387..e813cae 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,8 +1,10 @@ -import React, { useEffect, useRef, useState } from 'react'; -import styled from 'styled-components'; -import KaraokePlayer from './components/KaraokePlayer'; -import { toast, ToastContainer } from 'react-toastify'; -import 'react-toastify/dist/ReactToastify.css'; +import React, { useEffect, useRef, useState } from "react"; +import styled from "styled-components"; +import KaraokePlayer from "./components/KaraokePlayer"; +import { toast, ToastContainer } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; +import { FaPlay, FaPause, FaVolumeUp, FaVolumeMute } from "react-icons/fa"; +import { CaptionsRenderer } from "react-srv3"; const Root = styled.div` position: absolute; @@ -24,7 +26,7 @@ const FileInputContainer = styled.div` padding: 10px; border-radius: 5px; background-color: #ffffff; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); `; const FileInput = styled.input` @@ -33,7 +35,8 @@ const FileInput = styled.input` border: 1px solid #ddd; cursor: pointer; display: none; - &:hover, &:focus { + &:hover, + &:focus { background-color: #eaeaea; outline: none; } @@ -44,21 +47,25 @@ const FileInputLabel = styled.label` border-radius: 5px; border: 1px solid #ddd; cursor: pointer; - &:hover, &:focus { + &:hover, + &:focus { background-color: #eaeaea; outline: none; } `; - function App() { - const [currentMillisecond, setCurrentMillisecond] = useState(0); - const [lrcContent, setLrcContent] = useState(''); - const [videoUrl, setVideoUrl] = useState(''); + const [lrcContent, setLrcContent] = useState(""); + const [videoUrl, setVideoUrl] = useState(""); + const [srv3Url, setSrv3Url] = useState(""); + const [isPlaying, setIsPlaying] = useState(false); + const [showVolume, setShowVolume] = useState(false); + const [scrubValue, setScrubValue] = useState(0); const [showFileInputs, setShowFileInputs] = useState(true); const videoRef = useRef(null); - const [offset, setOffset] = useState('0'); + const [captionsText, setCaptionsText] = useState(""); + const [offset, setOffset] = useState("0"); const handleLrcFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file) { @@ -71,60 +78,267 @@ function App() { toast.success("LRC file loaded successfully", { autoClose: 2000 }); } }; - - const handleVideoFileChange = (event: React.ChangeEvent) => { + + const handleVideoFileChange = ( + event: React.ChangeEvent + ) => { const file = event.target.files?.[0]; if (file) { const url = URL.createObjectURL(file); setVideoUrl(url); - setShowFileInputs(true); toast.success("Video file loaded successfully", { autoClose: 2000 }); } }; + const handleSrvFileChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + setCaptionsText(e.target?.result as string); + }; + reader.readAsText(file); + toast.success("SRV file loaded successfully", { autoClose: 2000 }); + } + }; + + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.code === "Space") { + handlePlayPause(); + } + if (e.code === "ArrowRight") { + const video = videoRef.current; + if (!video) return; + video.currentTime += 5; + } + if (e.code === "ArrowLeft") { + const video = videoRef.current; + if (!video) return; + video.currentTime -= 5; + } + }; + document.addEventListener("keydown", handleKeyDown); + return () => { + document.removeEventListener("keydown", handleKeyDown); + }; + }); + useEffect(() => { const video = videoRef.current; if (!video) return; - + ; const syncLrcWithVideo = () => { console.log(offset); - setCurrentMillisecond((video.currentTime * 1000) + parseInt(offset)); + setCurrentMillisecond(video.currentTime * 1000 + parseInt(offset)); + setScrubValue((video.currentTime / video.duration) * 100); }; - video.addEventListener('timeupdate', syncLrcWithVideo); + video.addEventListener("timeupdate", syncLrcWithVideo); return () => { - video.removeEventListener('timeupdate', syncLrcWithVideo); + video.removeEventListener("timeupdate", syncLrcWithVideo); }; }); + const handleVolumeToggle = () => { + setShowVolume(!showVolume); + }; + + const handlePlayPause = () => { + const video = videoRef.current; + if (!video) return; + + if (video.paused) { + video.play(); + setIsPlaying(true); + } else { + video.pause(); + setIsPlaying(false); + } + }; + + const handleScrub = (event: React.ChangeEvent) => { + const time = + (parseFloat(event.target.value) / 100) * videoRef.current!.duration; + videoRef.current!.currentTime = time; + setScrubValue(parseFloat(event.target.value)); + }; + + const handleVolumeChange = (event: React.ChangeEvent) => { + const video = videoRef.current; + if (!video) return; + + video.volume = Number(event.target.value) / 100; + }; + return ( -
+
-
setShowFileInputs(true)} onMouseLeave={() => setShowFileInputs(false)}> - {videoUrl ?