diff options
| author | Pinapelz <yukais@pinapelz.com> | 2024-11-10 20:07:42 -0800 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2024-11-10 20:07:42 -0800 |
| commit | d05ec56a64bba63dbb9bf0e8b9657736ee40adf1 (patch) | |
| tree | 6bbbb6123d2c5cce3feaea1c243afd7986916c51 | |
| parent | debb2f639c1ceea3d79ee82d3716bc4e3e286424 (diff) | |
add line animations in as an option
| -rw-r--r-- | src/app/about/page.tsx | 155 | ||||
| -rw-r--r-- | src/app/components/LRCPlayer.tsx | 99 | ||||
| -rw-r--r-- | src/app/page.tsx | 1088 |
3 files changed, 763 insertions, 579 deletions
diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index dd5f35b..881e5a2 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -1,6 +1,6 @@ "use client"; -import React from 'react'; -import styled, { createGlobalStyle } from 'styled-components'; +import React from "react"; +import styled, { createGlobalStyle } from "styled-components"; const GlobalStyle = createGlobalStyle` body { @@ -81,69 +81,146 @@ const AboutPage: React.FC = () => { <Title>About</Title> <Subtitle>What is this player?</Subtitle> <Paragraph> - This player is capable of simultaneously playing back a lyric file (LRC), a main video/audio file, a SRV3 YouTube Timed Text, and a backing audio file. - <br />The idea is that this helps with not only karaoke but also checking how well a LRC or SRV3 file syncs with the main video/audio. + This player is capable of simultaneously playing back a + lyric file (LRC), a main video/audio file, a SRV3 YouTube + Timed Text, and a backing audio file. + <br /> + The idea is that this helps with not only karaoke but also + checking how well a LRC or SRV3 file syncs with the main + video/audio. </Paragraph> <Subtitle>How to use this player?</Subtitle> <Paragraph> - You'll need to prepare a few files for the media you want to play back first. - <br />Theoretically you can mix/match any of the files below since the main video/audio is all that's mandatory for playback. - <br />In this guide I'll assume that you're after a karaoke experience, and want the works. - <br /><br />To add any files to the player simply drag it onto the right part of the player page. - <br />EVERYTHING IS RAN LOCALLY, NO FILES ARE EVER UPLOADED TO ANY SERVERS. + You'll need to prepare a few files for the media you + want to play back first. + <br /> + Theoretically you can mix/match any of the files below since + the main video/audio is all that's mandatory for + playback. + <br /> + In this guide I'll assume that you're after a + karaoke experience, and want the works. + <br /> + <br /> + To add any files to the player simply drag it onto the right + part of the player page. + <br /> + EVERYTHING IS RAN LOCALLY, NO FILES ARE EVER UPLOADED TO ANY + SERVERS. </Paragraph> <Subtitle>1. Main video/audio file</Subtitle> <Paragraph> - Note: I've renamed the second button seen in the demos from Video to Media to avoid confusion since it can support audio/video files + Note: I've renamed the second button seen in the demos + from Video to Media to avoid confusion since it can support + audio/video files </Paragraph> <Paragraph> - This is the file that you want to play back. It can be a video or an audio file. - <br />Supported formats: mp4, webm, ogg, mp3, wav, flac, and more. - <br />If you choose to use an audio file here, the right part of the player will not show a video preview. - <br /> <br />A good way would be to download some video from YouTube. You may need to make adjustments to the offset later depending on how well the LRC file syncs with the video. - <br /> How you do that will be up to you, but I recommend using <a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a> to download the video. + This is the file that you want to play back. It can be a + video or an audio file. + <br /> + Supported formats: mp4, webm, ogg, mp3, wav, flac, and more. + <br /> + If you choose to use an audio file here, the right part of + the player will not show a video preview. + <br /> <br />A good way would be to download some video from + YouTube. You may need to make adjustments to the offset + later depending on how well the LRC file syncs with the + video. + <br /> How you do that will be up to you, but I recommend + using <a href="https://github.com/yt-dlp/yt-dlp"> + yt-dlp + </a>{" "} + to download the video. </Paragraph> <Subtitle>2. Lyric File (LRC)</Subtitle> <Paragraph> - This is the file that contains the lyrics of the song you want to sing. - <br />An example LRC file is shown below... + This is the file that contains the lyrics of the song you + want to sing. + <br /> + An example LRC file is shown below... </Paragraph> - <Preformatted dangerouslySetInnerHTML={{ __html: lyrics.replace(/\n/g, '<br/>') }} /> + <Preformatted + dangerouslySetInnerHTML={{ + __html: lyrics.replace(/\n/g, "<br/>"), + }} + /> <Paragraph> - The player will highlight the current line of the lyrics as the main media progresses. + The player will highlight the current line of the lyrics as + the main media progresses. <br /> - If you need a LRC file, a good way is to rip it from Spotify using <a href="https://github.com/akashrchandran/syrics">Syrics</a>. + If you need a LRC file, a good way is to rip it from Spotify + using{" "} + <a href="https://github.com/akashrchandran/syrics"> + Syrics + </a> + . </Paragraph> - <br/> - <br/> + <br /> + <br /> <Paragraph> - At this point you should already be able to play back the main media and have the lyrics highlighted as the media progresses. - <br />Depending on how well the LRC file syncs with the main media, you may need to adjust the main offset labelled as "Offset (±ms)" + At this point you should already be able to play back the + main media and have the lyrics highlighted as the media + progresses. + <br /> + Depending on how well the LRC file syncs with the main + media, you may need to adjust the main offset labelled as + "Offset (±ms)" </Paragraph> <Video controls> - <source src="https://files.catbox.moe/mfaei6.mp4" type="video/mp4" /> + <source + src="https://files.catbox.moe/mfaei6.mp4" + type="video/mp4" + /> </Video> <Subtitle>3. Instrumental/Vocals (Audio 2)</Subtitle> <Paragraph> - If you only wanted one or the other, simply add that as the main media then you're done - <br />There are a ton of tools online to remove this but you'll want to make sure you get the instrumental track in an audio format (mp3, wav, etc.) - <br />Then hover over the right side of the player, click the "Audio #2" button, and find your instrumental track. - <br /><br />(TIP!) I suggest going back and setting the main media in step 1 (Media button) in the previous step to a vocal only video/audio. - <br /> This will make it significantly easier to offset the 2 tracks. You can always mux a video file on top of that if you want visuals too! - <br/>Ultimately it doesn't matter which "slot" the instrumental or "vocals" go into, it's just better to have them separated! - <br/><br/>Now adjust the offset using the numerical inputs, the "Sync" button will adjust Audio 2 relative to the main media. - <br/>I suggest positioning the playhead at 00:00 and then adding the secondary audio; this will make adjustments much easier. - <br/><br/> - Now you should be able to control the balance between both of these files (which one is louder) by using the slider! + If you only wanted one or the other, simply add that as the + main media then you're done + <br /> + There are a ton of tools online to remove this but + you'll want to make sure you get the instrumental track + in an audio format (mp3, wav, etc.) + <br /> + Then hover over the right side of the player, click the + "Audio #2" button, and find your instrumental + track. + <br /> + <br /> + (TIP!) I suggest going back and setting the main media in + step 1 (Media button) in the previous step to a vocal only + video/audio. + <br /> This will make it significantly easier to offset the + 2 tracks. You can always mux a video file on top of that if + you want visuals too! + <br /> + Ultimately it doesn't matter which "slot" the + instrumental or "vocals" go into, it's just + better to have them separated! + <br /> + <br /> + Now adjust the offset using the numerical inputs, the + "Sync" button will adjust Audio 2 relative to the + main media. + <br />I suggest positioning the playhead at 00:00 and then + adding the secondary audio; this will make adjustments much + easier. + <br /> + <br /> + Now you should be able to control the balance between both + of these files (which one is louder) by using the slider! </Paragraph> <Subtitle>4. YouTube Timed Text</Subtitle> <Paragraph> - If the YouTube video you downloaded has subtitles (sometimes they look really cool and fancy), you can download that using yt-dlp - for use in the player as well. - <br/><br/>Unfortunately there is no way to adjust the offset for this, it'll play according to the main media. + If the YouTube video you downloaded has subtitles (sometimes + they look really cool and fancy), you can download that + using yt-dlp for use in the player as well. + <br /> + <br /> + Unfortunately there is no way to adjust the offset for this, + it'll play according to the main media. </Paragraph> <Video controls> - <source src="https://files.catbox.moe/ir6bs3.mp4"/> + <source src="https://files.catbox.moe/ir6bs3.mp4" /> </Video> <BackLink href="/">Back to player</BackLink> </Container> diff --git a/src/app/components/LRCPlayer.tsx b/src/app/components/LRCPlayer.tsx index e691957..e6d9551 100644 --- a/src/app/components/LRCPlayer.tsx +++ b/src/app/components/LRCPlayer.tsx @@ -1,8 +1,14 @@ -import React, { CSSProperties, useCallback } from 'react'; -import styled, { css } from 'styled-components'; -import { Lrc, LrcLine } from 'react-lrc'; +import React, { CSSProperties, useCallback } from "react"; +import styled, { css } from "styled-components"; +import { Lrc, LrcLine } from "react-lrc"; -const Line = styled.div<{ $active: boolean; $next: boolean }>` +interface LineProps { + $active: boolean; + $next: boolean; + $animate: boolean; +} + +const Line = styled.div<LineProps>` min-height: 10px; padding: 14px 30px; @@ -10,48 +16,71 @@ const Line = styled.div<{ $active: boolean; $next: boolean }>` font-family: "Roboto", sans-serif; font-weight: 500; text-align: center; - color: rgb(72,72,72); + color: rgb(72, 72, 72); - background: linear-gradient(to right, rgba(0,0,0,0) 50%, rgb(200, 190, 190) 50%); + background: linear-gradient( + to right, + rgba(0, 0, 0, 0) 50%, + rgb(200, 190, 190) 50% + ); background-size: 200% 100%; background-position: right bottom; - ${({ $active }) => $active && css` - color: black; - font-weight: 700; - background-position: left bottom; - color: rgb(50, 50, 50); - `} + ${({ $animate }) => + $animate && + css` + transition: + color 0.3s ease, + background-position 0.5s ease; + `} + + ${({ $active }) => + $active && + css` + color: rgb(50, 50, 50); + font-weight: 700; + background-position: left bottom; + `} `; + const lrcStyle: CSSProperties = { - flex: 1, - minHeight: 0, - overflow: 'hidden !important' + flex: 1, + minHeight: 0, + overflow: "hidden !important", }; interface LrcPlayerProps { - currentMillisecond: number; - lrc: string; + currentMillisecond: number; + lrc: string; + animate: boolean; } -const LrcPlayer: React.FC<LrcPlayerProps> = ({ currentMillisecond, lrc }) => { - const lineRenderer = useCallback( - ({ active, line: { content } }: { active: boolean; line: LrcLine }) => { - const next = active && content === ''; - return <Line $active={active} $next={next}>{content}</Line>; - }, - [] - ); +const LrcPlayer: React.FC<LrcPlayerProps> = ({ + currentMillisecond, + lrc, + animate, +}) => { + const lineRenderer = useCallback( + ({ active, line: { content } }: { active: boolean; line: LrcLine }) => { + const next = active && content === ""; + return ( + <Line $active={active} $next={next} $animate={animate}> + {content} + </Line> + ); + }, + [animate], + ); - return ( - <Lrc - lrc={lrc} - lineRenderer={lineRenderer} - currentMillisecond={currentMillisecond} - style={lrcStyle} - recoverAutoScrollInterval={0} - /> - ); + return ( + <Lrc + lrc={lrc} + lineRenderer={lineRenderer} + currentMillisecond={currentMillisecond} + style={lrcStyle} + recoverAutoScrollInterval={0} + /> + ); }; -export default LrcPlayer;
\ No newline at end of file +export default LrcPlayer; diff --git a/src/app/page.tsx b/src/app/page.tsx index 3b23eca..3e8dfb6 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -10,559 +10,637 @@ import { Button } from "react-bootstrap"; // Srtyled components const Root = styled.div` - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - display: flex; - flex-direction: column; - align-items: center; - background-color: #f5f5f5; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + display: flex; + flex-direction: column; + align-items: center; + background-color: #f5f5f5; `; const FileInputContainer = styled.div` - margin-bottom: 20px; - display: flex; - justify-content: center; - gap: 20px; - padding: 10px; - border-radius: 5px; - background-color: #ffffff; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + margin-bottom: 20px; + display: flex; + justify-content: center; + gap: 20px; + padding: 10px; + border-radius: 5px; + background-color: #ffffff; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); `; const FileInput = styled.input` - padding: 10px 15px; - border-radius: 5px; - border: 1px solid #ddd; - justify-content: center; - cursor: pointer; - display: none; - font-family: Arial; - &:hover, - &:focus { - background-color: #eaeaea; - outline: none; - } + padding: 10px 15px; + border-radius: 5px; + border: 1px solid #ddd; + justify-content: center; + cursor: pointer; + display: none; + font-family: Arial; + &:hover, + &:focus { + background-color: #eaeaea; + outline: none; + } `; const FileInputLabel = styled.label` - padding: 10px 15px; - border-radius: 5px; - border: 1px solid #ddd; - cursor: pointer; - &:hover, - &:focus { - background-color: #eaeaea; - outline: none; - } + padding: 10px 15px; + border-radius: 5px; + border: 1px solid #ddd; + cursor: pointer; + &:hover, + &:focus { + background-color: #eaeaea; + outline: none; + } `; const ControlBarButton = styled.button` - padding: 10px 15px; - border-radius: 5px; - border: 1px solid #ddd; - align-items: center; - cursor: pointer; - &:hover, - &:focus { - background-color: #eaeaea; - outline: none; - } + padding: 10px 15px; + border-radius: 5px; + border: 1px solid #ddd; + align-items: center; + cursor: pointer; + &:hover, + &:focus { + background-color: #eaeaea; + outline: none; + } `; const StyledLink = styled.a` - font-size: 20px; - font-family: Arial; - text-decoration: none; - text-color: black; - &:hover { - text-decoration: underline; - } + font-size: 20px; + font-family: Arial; + text-decoration: none; + text-color: black; + &:hover { + text-decoration: underline; + } `; function KaraokePage() { - const [currentMillisecond, setCurrentMillisecond] = useState(0); - const [lrcContent, setLrcContent] = useState<string>(""); - const [videoUrl, setVideoUrl] = useState<string>(""); - const [supplementAudioUrl, setSupplementAudioUrl] = useState<string>(""); - const [isPlaying, setIsPlaying] = useState<boolean>(false); - const [showVolume, setShowVolume] = useState<boolean>(false); - const [scrubValue, setScrubValue] = useState<number>(0); - const [showFileInputs, setShowFileInputs] = useState<boolean>(true); - const videoRef = useRef<HTMLVideoElement>(null); - const supplementAudioRef = useRef<HTMLAudioElement>(null); - const [captionsText, setCaptionsText] = useState<string>(""); - const [offset, setOffset] = useState<number>(0); - const [dragOver, setDragOver] = useState<boolean>(false); - const [statusText, setStatusText] = useState<string>("No video selected"); - const [balance, setBalance] = useState<number>(0); - const [supplementAudioOffset, setSupplementAudioOffset] = useState<number>(0); + const [currentMillisecond, setCurrentMillisecond] = useState(0); + const [lrcContent, setLrcContent] = useState<string>(""); + const [videoUrl, setVideoUrl] = useState<string>(""); + const [supplementAudioUrl, setSupplementAudioUrl] = useState<string>(""); + const [isPlaying, setIsPlaying] = useState<boolean>(false); + const [showVolume, setShowVolume] = useState<boolean>(false); + const [scrubValue, setScrubValue] = useState<number>(0); + const [showFileInputs, setShowFileInputs] = useState<boolean>(true); + const videoRef = useRef<HTMLVideoElement>(null); + const supplementAudioRef = useRef<HTMLAudioElement>(null); + const [captionsText, setCaptionsText] = useState<string>(""); + const [offset, setOffset] = useState<number>(0); + const [dragOver, setDragOver] = useState<boolean>(false); + const [statusText, setStatusText] = useState<string>("No video selected"); + const [balance, setBalance] = useState<number>(0); + const [animate, setAnimate] = useState<boolean>(true); + const [supplementAudioOffset, setSupplementAudioOffset] = + useState<number>(0); - // Functions for handling file input changes - const handleLrcFileChange = (event: React.ChangeEvent<HTMLInputElement>) => { - const file = event.target.files?.[0]; - if (file) { - const reader = new FileReader(); - reader.onload = (e) => { - setLrcContent(e.target?.result as string); - if (videoUrl) setShowFileInputs(false); - }; - reader.readAsText(file); - toast.success("LRC file loaded successfully", { autoClose: 2000 }); - } - }; - - const handleVideoFileChange = ( - event: React.ChangeEvent<HTMLInputElement> - ) => { - const file = event.target.files?.[0]; - if (file) { - const url = URL.createObjectURL(file); - setVideoUrl(url); - setCurrentMillisecond(0); - setScrubValue(0); - setIsPlaying(false); - 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 }); - } - }; - - const handleSupplementAudioFileChange = ( - event: React.ChangeEvent<HTMLInputElement> - ) => { - const file = event.target.files?.[0]; - const video = videoRef.current; - if (file) { - const url = URL.createObjectURL(file); - setSupplementAudioUrl(url); - setCurrentMillisecond(0); - setScrubValue(0); - setIsPlaying(false); - if (video) - video.pause(); - toast.success("Supplemental Audio file loaded successfully", { - autoClose: 2000, - }); - } - }; + // Functions for handling file input changes + const handleLrcFileChange = ( + event: React.ChangeEvent<HTMLInputElement>, + ) => { + const file = event.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + setLrcContent(e.target?.result as string); + if (videoUrl) setShowFileInputs(false); + }; + reader.readAsText(file); + toast.success("LRC file loaded successfully", { autoClose: 2000 }); + } + }; - const handleOnClickDemoButton = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => { - event.preventDefault(); - setOffset(-1550); - fetch("https://utfs.io/f/e2e18ea7-9841-437b-9ca3-5723355bd41a-rlck46.lrc") - .then(function (response) { - response.text().then(function (responseString) { - setLrcContent(responseString) - }); - }) - setVideoUrl("https://utfs.io/f/84f5dfa6-821d-407f-a16d-a685b09c11d9-7xx2h4.webm") - toast.success("Loading Demo: Mr.Raindrop - Amplified") - toast.success("Applied offset of -1550ms") + const handleVideoFileChange = ( + event: React.ChangeEvent<HTMLInputElement>, + ) => { + const file = event.target.files?.[0]; + if (file) { + const url = URL.createObjectURL(file); + setVideoUrl(url); + setCurrentMillisecond(0); + setScrubValue(0); + setIsPlaying(false); + 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 }); + } + }; - // Side effects for keyboard shortcuts - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.code === "Space") { - handlePlayPause(); - } - if (e.code === "ArrowRight") { - if (document.activeElement?.tagName === "INPUT") return; - const video = videoRef.current; - if (!video) return; - video.currentTime += 5; - } - if (e.code === "ArrowLeft") { - if (document.activeElement?.tagName === "INPUT") return; + const handleSupplementAudioFileChange = ( + event: React.ChangeEvent<HTMLInputElement>, + ) => { + const file = event.target.files?.[0]; const video = videoRef.current; - if (!video) return; - video.currentTime -= 5; - } - }; - document.addEventListener("keydown", handleKeyDown); - return () => { - document.removeEventListener("keydown", handleKeyDown); + if (file) { + const url = URL.createObjectURL(file); + setSupplementAudioUrl(url); + setCurrentMillisecond(0); + setScrubValue(0); + setIsPlaying(false); + if (video) video.pause(); + toast.success("Supplemental Audio file loaded successfully", { + autoClose: 2000, + }); + } }; - }); - // Side effects for the video itself - useEffect(() => { - const video = videoRef.current; - if (!video) return; - const syncLrcWithVideo = () => { - setCurrentMillisecond(video.currentTime * 1000 + offset); // updates lrc position - setScrubValue((video.currentTime / video.duration) * 100); // update playhead position + const handleOnClickDemoButton = ( + event: React.MouseEvent<HTMLAnchorElement, MouseEvent>, + ) => { + event.preventDefault(); + setOffset(-1550); + fetch( + "https://utfs.io/f/e2e18ea7-9841-437b-9ca3-5723355bd41a-rlck46.lrc", + ).then(function (response) { + response.text().then(function (responseString) { + setLrcContent(responseString); + }); + }); + setVideoUrl( + "https://utfs.io/f/84f5dfa6-821d-407f-a16d-a685b09c11d9-7xx2h4.webm", + ); + toast.success("Loading Demo: Mr.Raindrop - Amplified"); + toast.success("Applied offset of -1550ms"); }; - video.addEventListener("timeupdate", syncLrcWithVideo); - return () => { - video.removeEventListener("timeupdate", syncLrcWithVideo); - }; - }); + // Side effects for keyboard shortcuts + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.code === "Space") { + handlePlayPause(); + } + if (e.code === "ArrowRight") { + if (document.activeElement?.tagName === "INPUT") return; + const video = videoRef.current; + if (!video) return; + video.currentTime += 5; + } + if (e.code === "ArrowLeft") { + if (document.activeElement?.tagName === "INPUT") return; + const video = videoRef.current; + if (!video) return; + video.currentTime -= 5; + } + }; + document.addEventListener("keydown", handleKeyDown); + return () => { + document.removeEventListener("keydown", handleKeyDown); + }; + }); - useEffect(() => { - const video = videoRef.current; - const audio = supplementAudioRef.current; - if (!video || !audio) return; + // Side effects for the video itself + useEffect(() => { + const video = videoRef.current; + if (!video) return; + const syncLrcWithVideo = () => { + setCurrentMillisecond(video.currentTime * 1000 + offset); // updates lrc position + setScrubValue((video.currentTime / video.duration) * 100); // update playhead position + }; + video.addEventListener("timeupdate", syncLrcWithVideo); - if (balance < 0) { - video.volume = (1 + balance); - } else { - video.volume = 1; - audio.volume = (1 - balance); - } - }, [balance]); + return () => { + video.removeEventListener("timeupdate", syncLrcWithVideo); + }; + }); - useEffect(() => { - const video = videoRef.current; - const audio = supplementAudioRef.current; - if (!video || !audio) return; - if (supplementAudioOffset === null || supplementAudioOffset == null) return; - audio.currentTime = video.currentTime + supplementAudioOffset / 1000; - }, [supplementAudioOffset]); + useEffect(() => { + const video = videoRef.current; + const audio = supplementAudioRef.current; + if (!video || !audio) return; - // General video control functionality + if (balance < 0) { + video.volume = 1 + balance; + } else { + video.volume = 1; + audio.volume = 1 - balance; + } + }, [balance]); - const handleVolumeToggle = () => { - setShowVolume(!showVolume); - }; + useEffect(() => { + const video = videoRef.current; + const audio = supplementAudioRef.current; + if (!video || !audio) return; + if (supplementAudioOffset === null || supplementAudioOffset == null) + return; + audio.currentTime = video.currentTime + supplementAudioOffset / 1000; + }, [supplementAudioOffset]); - const handlePlayPause = () => { - const video = videoRef.current; - if (!video) return; + // General video control functionality - if (video.paused) { - video.play(); - if (supplementAudioUrl) supplementAudioRef.current?.play(); - setIsPlaying(true); - } else { - video.pause(); - if (supplementAudioUrl) supplementAudioRef.current?.pause(); - setIsPlaying(false); - } - }; + const handleVolumeToggle = () => { + setShowVolume(!showVolume); + }; - // Status text styling depending on whats loaded. Not all visible - useEffect(() => { - if (videoUrl && lrcContent) { - setStatusText("Ready to play!"); - } else if (videoUrl) { - setStatusText("No lyrics file selected"); - } else if (lrcContent) { - setStatusText("No video file selected"); - } else { - setStatusText("No video or lyrics file selected"); - } - }, [videoUrl, lrcContent]); + const handlePlayPause = () => { + const video = videoRef.current; + if (!video) return; - // Video Control Bar functionality - const handleScrub = (event: React.ChangeEvent<HTMLInputElement>) => { - const time = - (parseFloat(event.target.value) / 100) * videoRef.current!.duration; - videoRef.current!.currentTime = time; - if (supplementAudioOffset === null || supplementAudioOffset == null) { - supplementAudioRef.current!.currentTime = time; - } - else { - supplementAudioRef.current!.currentTime = time + supplementAudioOffset / 1000; - } - setScrubValue(parseFloat(event.target.value)); - }; + if (video.paused) { + video.play(); + if (supplementAudioUrl) supplementAudioRef.current?.play(); + setIsPlaying(true); + } else { + video.pause(); + if (supplementAudioUrl) supplementAudioRef.current?.pause(); + setIsPlaying(false); + } + }; + // Status text styling depending on whats loaded. Not all visible + useEffect(() => { + if (videoUrl && lrcContent) { + setStatusText("Ready to play!"); + } else if (videoUrl) { + setStatusText("No lyrics file selected"); + } else if (lrcContent) { + setStatusText("No video file selected"); + } else { + setStatusText("No video or lyrics file selected"); + } + }, [videoUrl, lrcContent]); - const handleVideoEnded = () => { - setIsPlaying(false); - supplementAudioRef.current?.pause(); - }; + // Video Control Bar functionality + const handleScrub = (event: React.ChangeEvent<HTMLInputElement>) => { + const time = + (parseFloat(event.target.value) / 100) * videoRef.current!.duration; + videoRef.current!.currentTime = time; + if (supplementAudioOffset === null || supplementAudioOffset == null) { + supplementAudioRef.current!.currentTime = time; + } else { + supplementAudioRef.current!.currentTime = + time + supplementAudioOffset / 1000; + } + setScrubValue(parseFloat(event.target.value)); + }; - const syncSupplementAudioWithVideo = () => { - const video = videoRef.current; - const audio = supplementAudioRef.current; - if (!video || !audio) return; - if (supplementAudioOffset === null || supplementAudioOffset == null) return; - audio.currentTime = video.currentTime + supplementAudioOffset / 1000; - } + const handleVideoEnded = () => { + setIsPlaying(false); + supplementAudioRef.current?.pause(); + }; - // Handling drag and drop files - const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => { - setDragOver(true); - event.preventDefault(); - }; + const syncSupplementAudioWithVideo = () => { + const video = videoRef.current; + const audio = supplementAudioRef.current; + if (!video || !audio) return; + if (supplementAudioOffset === null || supplementAudioOffset == null) + return; + audio.currentTime = video.currentTime + supplementAudioOffset / 1000; + }; - const handleDragEnter = (event: React.DragEvent<HTMLDivElement>) => { - setDragOver(true); - event.preventDefault(); - }; + // Handling drag and drop files + const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => { + setDragOver(true); + event.preventDefault(); + }; - const handleDragLeave = (event: React.DragEvent<HTMLDivElement>) => { - setDragOver(false); - event.preventDefault(); - }; + const handleDragEnter = (event: React.DragEvent<HTMLDivElement>) => { + setDragOver(true); + event.preventDefault(); + }; - const handleDrop = (event: React.DragEvent<HTMLDivElement>) => { - event.preventDefault(); - setDragOver(false); - const file = event.dataTransfer.files?.[0]; - if (file.name.endsWith(".lrc")) { - const reader = new FileReader(); - reader.onload = (e) => { - setLrcContent(e.target?.result as string); - if (videoUrl) setShowFileInputs(false); - }; - reader.readAsText(file); - toast.success("LRC file loaded successfully", { autoClose: 2000 }); - } else if (file.name.endsWith(".srv3")) { - const reader = new FileReader(); - reader.onload = (e) => { - setCaptionsText(e.target?.result as string); - }; - reader.readAsText(file); - toast.success("SRV file loaded successfully", { autoClose: 2000 }); - } else if (file.type.startsWith("video") || file.type.startsWith("audio")) { - const url = URL.createObjectURL(file); - setVideoUrl(url); - setCurrentMillisecond(0); - setScrubValue(0); - setIsPlaying(false); - toast.success("Video/Audio file loaded successfully", { - autoClose: 2000, - }); - } else { - toast.error("Unsupported file type", { autoClose: 2000 }); - } - }; + const handleDragLeave = (event: React.DragEvent<HTMLDivElement>) => { + setDragOver(false); + event.preventDefault(); + }; - return ( - <Root> - <ToastContainer /> - {/*LRC viewer*/} - <div style={{ display: "flex", width: "100%", height: "100vh" }}> - <LRCPlayer - lrc={lrcContent} - currentMillisecond={currentMillisecond} - /> + const handleDrop = (event: React.DragEvent<HTMLDivElement>) => { + event.preventDefault(); + setDragOver(false); + const file = event.dataTransfer.files?.[0]; + if (file.name.endsWith(".lrc")) { + const reader = new FileReader(); + reader.onload = (e) => { + setLrcContent(e.target?.result as string); + if (videoUrl) setShowFileInputs(false); + }; + reader.readAsText(file); + toast.success("LRC file loaded successfully", { autoClose: 2000 }); + } else if (file.name.endsWith(".srv3")) { + const reader = new FileReader(); + reader.onload = (e) => { + setCaptionsText(e.target?.result as string); + }; + reader.readAsText(file); + toast.success("SRV file loaded successfully", { autoClose: 2000 }); + } else if ( + file.type.startsWith("video") || + file.type.startsWith("audio") + ) { + const url = URL.createObjectURL(file); + setVideoUrl(url); + setCurrentMillisecond(0); + setScrubValue(0); + setIsPlaying(false); + toast.success("Video/Audio file loaded successfully", { + autoClose: 2000, + }); + } else { + toast.error("Unsupported file type", { autoClose: 2000 }); + } + }; - {/* Ternary operation for if videoUrl has been set */} - <div - style={{ - flex: 1, - position: "relative", - backgroundColor: dragOver ? "lightblue" : "white", - }} - onMouseEnter={() => setShowFileInputs(true)} - onMouseLeave={() => setShowFileInputs(false)} - onDragOver={handleDragOver} - onDragEnter={handleDragEnter} - onDragLeave={handleDragLeave} - onDrop={handleDrop} - > - {videoUrl ? ( - <> - <video - ref={videoRef} - src={videoUrl} - style={{ position: "absolute", width: "100%", height: "100%" }} - onEnded={handleVideoEnded} - /> - <audio - ref={supplementAudioRef} - src={supplementAudioUrl} - style={{ display: "none" }} - /> - <div - style={{ width: "90%", height: "90%", margin: "auto" }} - onClick={() => handlePlayPause()} - > - <CaptionsRenderer - srv3={captionsText} - currentTime={currentMillisecond / 1000} + return ( + <Root> + <ToastContainer /> + {/*LRC viewer*/} + <div style={{ display: "flex", width: "100%", height: "100vh" }}> + <LRCPlayer + lrc={lrcContent} + currentMillisecond={currentMillisecond} + animate={animate} /> - </div> - {/*Video control bar*/} - <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", - }} + + {/* Ternary operation for if videoUrl has been set */} + <div + style={{ + flex: 1, + position: "relative", + backgroundColor: dragOver ? "lightblue" : "white", + }} + onMouseEnter={() => setShowFileInputs(true)} + onMouseLeave={() => setShowFileInputs(false)} + onDragOver={handleDragOver} + onDragEnter={handleDragEnter} + onDragLeave={handleDragLeave} + onDrop={handleDrop} > - {isPlaying ? <FaPause /> : <FaPlay />} - </button> - <input - type="range" - min="0" - max="100" - value={scrubValue} - style={{ flex: 1, height: "50px", width: "100%" }} - onInput={handleScrub} - /> - </div> - </> - ) : ( - <div - style={{ - width: "100%", - height: "100%", - display: "flex", - justifyContent: "center", - alignItems: "center", - flexDirection: "column", - }} - > - <h1 - style={{ - fontFamily: "Arial", - fontWeight: "bold", - fontSize: "32px", - }} - > - {statusText} - </h1> - {/* Show a placeholder while no video selected */} - <p - style={{ - fontSize: "30px", - textAlign: "center", - fontFamily: "Arial", - }} - > - Please select the video and lrc (lyrics) file <br /> - (Drag and Drop them here, or use the menus below!) - <br /> - <StyledLink href="/about"> About </StyledLink> - <StyledLink href="" onClick={handleOnClickDemoButton}> Demo </StyledLink> - </p> - </div> - )} + {videoUrl ? ( + <> + <video + ref={videoRef} + src={videoUrl} + style={{ + position: "absolute", + width: "100%", + height: "100%", + }} + onEnded={handleVideoEnded} + /> + <audio + ref={supplementAudioRef} + src={supplementAudioUrl} + style={{ display: "none" }} + /> + <div + style={{ + width: "90%", + height: "90%", + margin: "auto", + }} + onClick={() => handlePlayPause()} + > + <CaptionsRenderer + srv3={captionsText} + currentTime={currentMillisecond / 1000} + /> + </div> + {/*Video control bar*/} + <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", + width: "100%", + }} + onInput={handleScrub} + /> + </div> + </> + ) : ( + <div + style={{ + width: "100%", + height: "100%", + display: "flex", + justifyContent: "center", + alignItems: "center", + flexDirection: "column", + }} + > + <h1 + style={{ + fontFamily: "Arial", + fontWeight: "bold", + fontSize: "32px", + }} + > + {statusText} + </h1> + {/* Show a placeholder while no video selected */} + <p + style={{ + fontSize: "30px", + textAlign: "center", + fontFamily: "Arial", + }} + > + Please select the video and lrc (lyrics) file{" "} + <br /> + (Drag and Drop them here, or use the menus + below!) <br /> + <br /> + Chrome is recommended! + <br /> + <StyledLink href="/about"> About </StyledLink> + <StyledLink + href="" + onClick={handleOnClickDemoButton} + > + {" "} + Demo{" "} + </StyledLink> + </p> + </div> + )} - {/* File inputs, shown on hover over video div region*/} - {showFileInputs && ( - <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" }} - > - Media - </FileInputLabel> - <FileInput - id="videoUpload" - type="file" - accept="video/*,audio/*" - onChange={handleVideoFileChange} - /> - <FileInputLabel htmlFor="srvUpload" style={{ cursor: "pointer" }}> - SRV - </FileInputLabel> - <FileInput - id="srvUpload" - type="file" - accept=".srv3" - onChange={handleSrvFileChange} - /> - <FileInputLabel - htmlFor="supplementAudioUpload" - style={{ cursor: "pointer" }} - > - Audio #2 - </FileInputLabel> - <FileInput - id="supplementAudioUpload" - type="file" - accept="audio/*" - onChange={handleSupplementAudioFileChange} - /> - <ControlBarButton onClick={syncSupplementAudioWithVideo}>Sync Audio</ControlBarButton> - <div - style={{ - display: "flex", - flexDirection: "column", - fontFamily: "Arial", - }} - > - <label>Audio/Video Balance</label> - <input - type="range" - min="-1" - max="1" - step="0.01" - value={balance} - onChange={(e) => setBalance(Number(e.target.value))} - /> - <label>Offset (±ms) </label> - <input - type="number" - style={{ fontSize: "14px" }} - id="numberInput" - value={offset} - onChange={(e) => setOffset(Number(e.target.value))} - step="25" - /> - </div> - <div - style={{ - display: "flex", - flexDirection: "column", - fontFamily: "Arial", - }} - > - <label>Audio 2 Offset (±ms) </label> - <input - type="number" - style={{ fontSize: "14px" }} - id="numberInput" - value={supplementAudioOffset} - onChange={(e) => setSupplementAudioOffset(Number(e.target.value))} - step="25" - /> - </div> - </FileInputContainer> - )} - </div> - </div> - </Root> - ); + {/* File inputs, shown on hover over video div region*/} + {showFileInputs && ( + <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" }} + > + Media + </FileInputLabel> + <FileInput + id="videoUpload" + type="file" + accept="video/*,audio/*" + onChange={handleVideoFileChange} + /> + <FileInputLabel + htmlFor="srvUpload" + style={{ cursor: "pointer" }} + > + SRV + </FileInputLabel> + <FileInput + id="srvUpload" + type="file" + accept=".srv3" + onChange={handleSrvFileChange} + /> + <FileInputLabel + htmlFor="supplementAudioUpload" + style={{ cursor: "pointer" }} + > + Audio #2 + </FileInputLabel> + <FileInput + id="supplementAudioUpload" + type="file" + accept="audio/*" + onChange={handleSupplementAudioFileChange} + /> + <ControlBarButton + onClick={syncSupplementAudioWithVideo} + > + Sync Audio + </ControlBarButton> + <div + style={{ + display: "flex", + flexDirection: "column", + fontFamily: "Arial", + }} + > + <label>Audio/Video Balance</label> + <input + type="range" + min="-1" + max="1" + step="0.01" + value={balance} + onChange={(e) => + setBalance(Number(e.target.value)) + } + /> + <label>Offset (±ms) </label> + <input + type="number" + style={{ fontSize: "14px" }} + id="numberInput" + value={offset} + onChange={(e) => + setOffset(Number(e.target.value)) + } + step="25" + /> + </div> + <div + style={{ + display: "flex", + flexDirection: "column", + fontFamily: "Arial", + }} + > + <label>Audio 2 Offset (±ms) </label> + <input + type="number" + style={{ fontSize: "14px" }} + id="numberInput" + value={supplementAudioOffset} + onChange={(e) => + setSupplementAudioOffset( + Number(e.target.value), + ) + } + step="25" + /> + </div> + <label + style={{ + fontSize: "14px", + fontFamily: "Arial", + userSelect: "none", + }} + > + <input + type="checkbox" + checked={animate} + onChange={(e) => + setAnimate(e.target.checked) + } + onSelect={(e) => e.preventDefault()} + style={{ marginRight: "8px" }} + /> + Line Animation + </label> + </FileInputContainer> + )} + </div> + </div> + </Root> + ); } export default KaraokePage; |
