mirror of
https://github.com/zhigang1992/rebirthProd.git
synced 2026-01-12 22:41:05 +08:00
add previewer
This commit is contained in:
6
index.html
Normal file
6
index.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<html>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="./index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
5
index.tsx
Normal file
5
index.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import Play from "./src/Previewer";
|
||||
|
||||
ReactDOM.render(<Play />, document.getElementById("root"));
|
||||
26
package.json
Normal file
26
package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@doodlit/previewer",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"start": "parcel index.html",
|
||||
"build": "parcel build index.html",
|
||||
"deploy": "firebase deploy --only hosting:previewer"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cssnano": "^4.1.10",
|
||||
"parcel": "^1.12.4",
|
||||
"typescript": "^3.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^14.0.4",
|
||||
"@types/qs": "^6.9.3",
|
||||
"@types/react": "^16.9.35",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"qs": "^6.9.4",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1"
|
||||
}
|
||||
}
|
||||
171
src/Previewer.tsx
Normal file
171
src/Previewer.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import React, {
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import "./previewer.css";
|
||||
|
||||
type ProcessGenerateFinalVideoParams = {
|
||||
lines: {
|
||||
videos: {
|
||||
video: string;
|
||||
videoDuration: number;
|
||||
}[];
|
||||
audio: string;
|
||||
audioDuration: number;
|
||||
}[];
|
||||
globalVoiceOver?: string;
|
||||
};
|
||||
|
||||
type LineProps = ProcessGenerateFinalVideoParams["lines"][number];
|
||||
|
||||
const Video = (props: {
|
||||
src: string;
|
||||
speed: number;
|
||||
duration: number;
|
||||
play: boolean;
|
||||
onFinish: () => void;
|
||||
}) => {
|
||||
const [played, setPlayed] = useState(false);
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
useLayoutEffect(() => {
|
||||
if (videoRef.current) {
|
||||
videoRef.current.playbackRate = props.speed;
|
||||
}
|
||||
}, [videoRef, props.speed]);
|
||||
useEffect(() => {
|
||||
if (!props.play) {
|
||||
return;
|
||||
}
|
||||
if (!props.src) {
|
||||
const timer = setTimeout(() => {
|
||||
props.onFinish();
|
||||
}, (props.duration / props.speed) * 1000);
|
||||
return () => clearTimeout(timer);
|
||||
} else {
|
||||
videoRef.current?.play();
|
||||
}
|
||||
}, [props.play]);
|
||||
if (played || !props.src) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<video
|
||||
ref={videoRef}
|
||||
muted={true}
|
||||
src={props.src}
|
||||
preload="auto"
|
||||
style={{ display: props.play ? "block" : "none" }}
|
||||
onEnded={() => {
|
||||
setPlayed(true);
|
||||
props.onFinish();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Line = ({
|
||||
line,
|
||||
onFinish,
|
||||
active,
|
||||
hasGlobalVoiceOver,
|
||||
}: {
|
||||
line: LineProps;
|
||||
active: boolean;
|
||||
onFinish: () => void;
|
||||
hasGlobalVoiceOver?: boolean;
|
||||
}) => {
|
||||
const speed = useMemo(() => {
|
||||
const totalVideoDuration = line.videos
|
||||
.map((v) => v.videoDuration)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
return totalVideoDuration / line.audioDuration;
|
||||
}, [line]);
|
||||
const [videoIndex, setVideoIndex] = useState(0);
|
||||
const audioRef = useRef<HTMLAudioElement>(null);
|
||||
useEffect(() => {
|
||||
if (active) {
|
||||
audioRef.current?.play();
|
||||
}
|
||||
}, [active]);
|
||||
return (
|
||||
<>
|
||||
{line.videos.map((video, index) => (
|
||||
<Video
|
||||
key={index}
|
||||
src={video.video}
|
||||
duration={video.videoDuration}
|
||||
speed={speed}
|
||||
play={active && videoIndex === index}
|
||||
onFinish={() => {
|
||||
if (videoIndex < line.videos.length - 1) {
|
||||
setVideoIndex((p) => p + 1);
|
||||
} else {
|
||||
onFinish();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{!hasGlobalVoiceOver && (
|
||||
<audio src={line.audio} preload="auto" ref={audioRef} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Previewer = () => {
|
||||
const [currentLine, setCurrentLine] = useState(0);
|
||||
const {
|
||||
lines,
|
||||
globalVoiceOver,
|
||||
}: ProcessGenerateFinalVideoParams = JSON.parse(
|
||||
decodeURIComponent(window.location.hash.slice(1))
|
||||
);
|
||||
const [play, setPlay] = useState(false);
|
||||
const [showPlayButton, setShowPlayButton] = useState(true);
|
||||
const [finished, setFinished] = useState(false);
|
||||
const audioRef = useRef<HTMLAudioElement>(null);
|
||||
useEffect(() => {
|
||||
if (play) {
|
||||
audioRef.current?.play();
|
||||
}
|
||||
}, [play]);
|
||||
return (
|
||||
<div className={`canvas ${finished ? "finished" : "playing"}`}>
|
||||
{lines.map((line, index) => (
|
||||
<Line
|
||||
line={line}
|
||||
key={index}
|
||||
active={play && currentLine === index}
|
||||
onFinish={() => {
|
||||
if (currentLine < lines.length - 1) {
|
||||
setCurrentLine((p) => p + 1);
|
||||
} else {
|
||||
setFinished(true);
|
||||
}
|
||||
}}
|
||||
hasGlobalVoiceOver={globalVoiceOver != null}
|
||||
/>
|
||||
))}
|
||||
{globalVoiceOver != null && (
|
||||
<audio src={globalVoiceOver} preload="auto" ref={audioRef} />
|
||||
)}
|
||||
{showPlayButton && (
|
||||
<img
|
||||
id="play"
|
||||
className="play-button"
|
||||
src={require("./play.svg")}
|
||||
alt="Play"
|
||||
onClick={() => {
|
||||
setShowPlayButton(false);
|
||||
setTimeout(() => setPlay(true), 1000);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Previewer;
|
||||
3
src/play.svg
Normal file
3
src/play.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path d="M256 0C114.833 0 0 114.844 0 256s114.833 256 256 256 256-114.844 256-256S397.167 0 256 0zm101.771 264.969l-149.333 96a10.62 10.62 0 01-5.771 1.698c-1.75 0-3.521-.438-5.104-1.302A10.653 10.653 0 01192 352V160c0-3.906 2.125-7.49 5.563-9.365 3.375-1.854 7.604-1.74 10.875.396l149.333 96c3.042 1.958 4.896 5.344 4.896 8.969s-1.854 7.01-4.896 8.969z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 427 B |
48
src/previewer.css
Normal file
48
src/previewer.css
Normal file
@@ -0,0 +1,48 @@
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.canvas {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
video {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
audio {
|
||||
position: absolute;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.play-button {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 50%;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-left: -50px;
|
||||
}
|
||||
|
||||
.record-button {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 120px;
|
||||
height: 50px;
|
||||
margin-left: -60px;
|
||||
margin-top: 20px;
|
||||
font-size: 17px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: #ebecf0;
|
||||
}
|
||||
23
tsconfig.json
Normal file
23
tsconfig.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react",
|
||||
"downlevelIteration": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user