add previewer

This commit is contained in:
Kyle Fang
2020-06-11 09:31:15 +08:00
commit e559c2c91a
7 changed files with 282 additions and 0 deletions

6
index.html Normal file
View File

@@ -0,0 +1,6 @@
<html>
<body>
<div id="root"></div>
<script src="./index.tsx"></script>
</body>
</html>

5
index.tsx Normal file
View 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
View 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
View 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
View 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
View 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
View 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
}
}