aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2023-11-15 01:41:32 -0800
committerPinapelz <yukais@pinapelz.com>2023-11-15 01:41:32 -0800
commit782159c7a965203f4f134dabe13634e59b579cc7 (patch)
treea5ef7443a0bc81e097ae1eac61ec509a078a081f
parent71b680fb9d29057b97748c54d1ad20229fe3394c (diff)
feat: add support for srv3
-rw-r--r--package.json3
-rw-r--r--pnpm-lock.yaml49
-rw-r--r--src/app/App.tsx288
-rw-r--r--src/app/components/VideoControls.tsx153
4 files changed, 456 insertions, 37 deletions
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<HTMLVideoElement>(null);
- const [offset, setOffset] = useState('0');
+ const [captionsText, setCaptionsText] = useState("");
+ const [offset, setOffset] = useState("0");
const handleLrcFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
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<HTMLInputElement>) => {
+
+ const handleVideoFileChange = (
+ event: React.ChangeEvent<HTMLInputElement>
+ ) => {
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<HTMLInputElement>) => {
+ 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;
-
+ <CaptionsRenderer srv3={captionsText} currentTime={currentMillisecond} />;
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<HTMLInputElement>) => {
+ const time =
+ (parseFloat(event.target.value) / 100) * videoRef.current!.duration;
+ videoRef.current!.currentTime = time;
+ setScrubValue(parseFloat(event.target.value));
+ };
+
+ const handleVolumeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ const video = videoRef.current;
+ if (!video) return;
+
+ video.volume = Number(event.target.value) / 100;
+ };
+
return (
<Root>
<ToastContainer />
- <div style={{ display: 'flex', width: '100%', height: '100vh' }}>
+ <div style={{ display: "flex", width: "100%", height: "100vh" }}>
<KaraokePlayer
lrc={lrcContent}
currentMillisecond={currentMillisecond}
/>
- <div style={{ flex: 1, position: 'relative' }} onMouseEnter={() => setShowFileInputs(true)} onMouseLeave={() => setShowFileInputs(false)}>
- {videoUrl ? <video ref={videoRef} src={videoUrl} controls style={{ width: '100%', height: '100%' }} /> :<div style={{ width: '100%', height: '100%', backgroundColor: '#ddd', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
- <p style={{fontSize: '30px', textAlign: 'center', fontFamily:'Arial', fontWeight:'bold'}}>
- Please select the video and lrc (lyrics) file <br/>
- Hover over me for a menu</p>
- </div>
- }
+ <div
+ style={{ flex: 1, position: "relative" }}
+ onMouseEnter={() => setShowFileInputs(true)}
+ onMouseLeave={() => setShowFileInputs(false)}
+ >
+ {videoUrl ? (
+ <>
+ <video
+ ref={videoRef}
+ src={videoUrl}
+ style={{ position: "absolute", width: "100%", height: "100%" }}
+ />
+ <div style={{ width: '90%', height: '90%', margin: 'auto' }}>
+ <CaptionsRenderer
+ srv3={captionsText}
+ currentTime={currentMillisecond / 1000}
+ />
+ </div>
+ <div
+ style={{
+ position: "absolute",
+ bottom: 0,
+ left: 0,
+ width: "100%",
+ display: "flex",
+ alignItems: "center",
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
+ height: "50px",
+ }}
+ >
+ <button
+ onClick={handlePlayPause}
+ style={{
+ width: "50px",
+ height: "50px",
+ padding: "10px",
+ border: "none",
+ borderRadius: "5px",
+ backgroundColor: "white",
+ color: "black",
+ }}
+ >
+ {isPlaying ? <FaPause /> : <FaPlay />}
+ </button>
+ <input
+ type="range"
+ min="0"
+ max="100"
+ value={scrubValue}
+ style={{ flex: 1, height: "50px" }}
+ onInput={handleScrub}
+ />
+ <div
+ style={{
+ position: "relative",
+ width: "50px",
+ height: "50px",
+ }}
+ >
+ <button
+ onClick={handleVolumeToggle}
+ style={{
+ width: "100%",
+ height: "100%",
+ padding: "10px",
+ border: "none",
+ borderRadius: "5px",
+ backgroundColor: "white",
+ color: "black",
+ }}
+ >
+ {!showVolume ? <FaVolumeMute /> : <FaVolumeUp />}
+ </button>
+ {showVolume && (
+ <div
+ style={{
+ position: "absolute",
+ bottom: "100%",
+ transform:
+ "translateX(-50%) translateY(-100%) rotate(-90deg)",
+ }}
+ >
+ <input
+ type="range"
+ min="0"
+ max="100"
+ defaultValue="100"
+ style={{ height: "40px", width: "100px" }}
+ onChange={handleVolumeChange}
+ />
+ </div>
+ )}
+ </div>
+ </div>
+ </>
+ ) : (
+ <div
+ style={{
+ width: "100%",
+ height: "100%",
+ backgroundColor: "#ddd",
+ display: "flex",
+ justifyContent: "center",
+ alignItems: "center",
+ }}
+ >
+ <p
+ style={{
+ fontSize: "30px",
+ textAlign: "center",
+ fontFamily: "Arial",
+ fontWeight: "bold",
+ }}
+ >
+ Please select the video and lrc (lyrics) file <br />
+ Hover over me for a menu
+ </p>
+ </div>
+ )}
{showFileInputs && (
- <FileInputContainer style={{ position: 'absolute', bottom: '20px', left: 0 }}>
- <FileInputLabel htmlFor="lrcUpload" style={{ cursor: 'pointer' }}>LRC</FileInputLabel>
- <FileInput id="lrcUpload" type="file" accept=".lrc" onChange={handleLrcFileChange} />
- <FileInputLabel htmlFor="videoUpload" style={{ cursor: 'pointer' }}>Video</FileInputLabel>
- <FileInput id="videoUpload" type="file" accept="video/*" onChange={handleVideoFileChange} />
- <FileInputLabel htmlFor="srvUpload" style={{ cursor: 'pointer' }}>SRV</FileInputLabel>
- <FileInput disabled type="file" accept=".srv" />
- <div style={{ display: 'flex', flexDirection: 'column', fontFamily: 'Arial' }}>
+ <FileInputContainer
+ style={{ position: "absolute", bottom: "30px", left: 0 }}
+ >
+ <FileInputLabel htmlFor="lrcUpload" style={{ cursor: "pointer" }}>
+ LRC
+ </FileInputLabel>
+ <FileInput
+ id="lrcUpload"
+ type="file"
+ accept=".lrc"
+ onChange={handleLrcFileChange}
+ />
+ <FileInputLabel
+ htmlFor="videoUpload"
+ style={{ cursor: "pointer" }}
+ >
+ Video
+ </FileInputLabel>
+ <FileInput
+ id="videoUpload"
+ type="file"
+ accept="video/*"
+ onChange={handleVideoFileChange}
+ />
+ <FileInputLabel htmlFor="srvUpload" style={{ cursor: "pointer" }}>
+ SRV
+ </FileInputLabel>
+ <FileInput id="srvUpload" type="file" accept=".srv3" onChange={handleSrvFileChange}/>
+ <div
+ style={{
+ display: "flex",
+ flexDirection: "column",
+ fontFamily: "Arial",
+ }}
+ >
<label>Offset (±ms) </label>
<input
type="number"
- style={{ fontSize: '20px' }}
+ style={{ fontSize: "20px" }}
id="numberInput"
value={offset}
onChange={(e) => setOffset(e.target.value)}
diff --git a/src/app/components/VideoControls.tsx b/src/app/components/VideoControls.tsx
new file mode 100644
index 0000000..bca8e97
--- /dev/null
+++ b/src/app/components/VideoControls.tsx
@@ -0,0 +1,153 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import {
+ faPlay,
+ faPause,
+ faExpand,
+ faCompress,
+ faVolumeUp,
+ faVolumeMute
+} from '@fortawesome/free-solid-svg-icons';
+
+// Define a TypeScript interface for the component's props
+interface VideoControlsProps {
+ videoRef: React.RefObject<HTMLVideoElement | null>;
+ isPlaying: boolean;
+ onPlayStateChange: (isPlaying: boolean) => void;
+ }
+const VideoControls: React.FC<VideoControlsProps> = ({ videoRef, isPlaying, onPlayStateChange }) => {
+ const [isFullscreen, setIsFullscreen] = useState(false);
+ const [progress, setProgress] = useState(0);
+ const [volume, setVolume] = useState(1);
+ const [showVolumeSlider, setShowVolumeSlider] = useState(false);
+ const [currentTime, setCurrentTime] = useState(0);
+
+ const formatTime = (seconds: number) => {
+ const hrs = Math.floor(seconds / 3600);
+ const mins = Math.floor((seconds % 3600) / 60);
+ const secs = Math.floor(seconds % 60);
+ if (hrs === 0) {
+ return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
+ }
+ return `${hrs.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
+ };
+
+ useEffect(() => {
+ const updateProgress = () => {
+ const newProgress = (videoRef.current?.currentTime / videoRef.current?.duration) * 100;
+ setCurrentTime(videoRef.current?.currentTime || 0);
+ setProgress(newProgress || 0);
+ };
+
+ const currentVideoRef = videoRef.current;
+ currentVideoRef?.addEventListener('timeupdate', updateProgress);
+ return () => currentVideoRef?.removeEventListener('timeupdate', updateProgress);
+ }, [videoRef]);
+
+ const handlePlayPause = () => {
+ if (videoRef.current?.paused) {
+ videoRef.current?.play();
+ } else {
+ videoRef.current?.pause();
+ }
+ onPlayStateChange(!videoRef.current?.paused);
+ };
+
+ const handleProgressChange = (e) => {
+ const newTime = (e.target.value / 100) * videoRef.current?.duration;
+ videoRef.current.currentTime = newTime;
+ setProgress(e.target.value);
+ };
+
+ const handleVolumeChange = (e) => {
+ const vol = e.target.value;
+ if (videoRef.current) {
+ videoRef.current.volume = vol;
+ }
+ setVolume(vol);
+ };
+
+ const toggleFullscreen = () => {
+ if (!document.fullscreenElement) {
+ videoRef.current?.requestFullscreen();
+ setIsFullscreen(true);
+ } else {
+ document.exitFullscreen();
+ setIsFullscreen(false);
+ }
+ };
+
+ const handleWheel = (e) => {
+ e.preventDefault();
+ e.target.blur();
+ };
+
+ return (
+ <div className="video-controls bg-gray-700 bg-opacity-75 p-1 rounded space-x-4 flex items-center">
+ <button
+ onClick={handlePlayPause}
+ className="p-2 rounded-full hover:bg-gray-600 transition"
+ >
+ <FontAwesomeIcon
+ icon={isPlaying ? faPause : faPlay}
+ className="text-white"
+ size="lg"
+ />
+ </button>
+ <div className="text-white ml-3">
+ {formatTime(currentTime)} /{" "}
+ {formatTime(videoRef.current?.duration || 0)}
+ </div>
+ <div className="relative flex-grow mx-4 flex items-center">
+ <input
+ type="range"
+ value={progress}
+ onChange={handleProgressChange}
+ onWheel={handleWheel}
+ className="w-full cursor-pointer slider-thumb bg-red-500"
+ title=""
+ />
+ </div>
+
+ <button
+ onClick={() => setShowVolumeSlider(!showVolumeSlider)}
+ className="p-2 rounded-full hover:bg-gray-600 transition relative"
+ >
+ <FontAwesomeIcon
+ icon={volume > 0 ? faVolumeUp : faVolumeMute}
+ className="text-white"
+ size="lg"
+ />
+ {showVolumeSlider && (
+ <div className="absolute mb-7 bottom-8 left-1/2 transform -translate-x-1/2 w-20">
+ <input
+ type="range"
+ value={volume}
+ onChange={handleVolumeChange}
+ min="0"
+ max="1"
+ step="0.01"
+ className="w-full cursor-pointer slider-thumb mb-4"
+ title=""
+ style={{ transform: "rotate(270deg)" }}
+ />
+ </div>
+ )}
+ </button>
+
+ <button
+ onClick={toggleFullscreen}
+ className="p-2 rounded-full hover:bg-gray-600 transition"
+ >
+ <FontAwesomeIcon
+ icon={isFullscreen ? faCompress : faExpand}
+ className="text-white"
+ size="lg"
+ />
+ </button>
+ </div>
+ );
+};
+
+
+export default VideoControls;
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage