Update website and @ffmpeg/util

This commit is contained in:
Jerome Wu
2022-10-06 20:52:39 +08:00
parent a40571f1b9
commit cf7f537bd8
14 changed files with 549 additions and 52 deletions

View File

@@ -0,0 +1,20 @@
import React from "react";
import { useColorMode } from "@docusaurus/theme-common";
import { ThemeProvider, createTheme } from "@mui/material/styles";
const lightTheme = createTheme({});
const darkTheme = createTheme({
palette: {
mode: "dark",
},
});
export default function MuiThemeProvider(props: any) {
const { colorMode } = useColorMode();
return (
<ThemeProvider
theme={colorMode === "dark" ? darkTheme : lightTheme}
{...props}
/>
);
}

View File

@@ -0,0 +1,16 @@
import * as React from "react";
import Typography from "@mui/material/Typography";
import Container from "@mui/material/Container";
import LinearProgressWithLabel from "./LinearProgressWithLabel";
import { CORE_SIZE } from "./const";
export default function CoreDownloader({ url, received }) {
const total = CORE_SIZE[url];
return (
<Container>
<Typography>{`Downloading ${url}`}</Typography>
<Typography>{`(${received} / ${total} bytes)`}</Typography>
<LinearProgressWithLabel value={(received / total) * 100} />
</Container>
);
}

View File

@@ -0,0 +1,53 @@
import * as React from "react";
import Button from "@mui/material/Button";
import Container from "@mui/material/Container";
import Radio from "@mui/material/Radio";
import RadioGroup from "@mui/material/RadioGroup";
import FormControlLabel from "@mui/material/FormControlLabel";
import FormControl from "@mui/material/FormControl";
import FormLabel from "@mui/material/FormLabel";
import styles from "./styles.module.css";
interface CoreSelectorProps {
option: string;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => any;
onSubmit: () => any;
}
export default function CoreSelector({
option,
onChange,
onSubmit,
}: CoreSelectorProps) {
return (
<Container className={styles.margin}>
<Container className={styles.margin}>
<FormControl>
<FormLabel id="core-selector">Select a Core Option</FormLabel>
<RadioGroup
aria-labelledby="core-selector"
name="core-selector"
value={option}
onChange={onChange}
>
<FormControlLabel
value="core"
control={<Radio />}
label="Core (Slower, but stable)"
/>
<FormControlLabel
value="core-mt"
disabled={typeof SharedArrayBuffer !== "function"}
control={<Radio />}
label="Core Multi-threaded (Faster, but lower compatibility and unstable)"
/>
</RadioGroup>
</FormControl>
</Container>
<Container>
<Button variant="contained" onClick={onSubmit}>
Load
</Button>
</Container>
</Container>
);
}

View File

@@ -0,0 +1,246 @@
/// <reference types="ace" />
import * as React from "react";
import Box from "@mui/material/Box";
import Grid from "@mui/material/Grid";
import Stack from "@mui/material/Stack";
import Button from "@mui/material/Button";
import Typography from "@mui/material/Typography";
import FolderIcon from "@mui/icons-material/Folder";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import IconButton from "@mui/material/IconButton";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import RefreshIcon from "@mui/icons-material/Refresh";
import UploadFileIcon from "@mui/icons-material/UploadFile";
import UploadIcon from "@mui/icons-material/Upload";
import Tooltip from "@mui/material/Tooltip";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import { useColorMode } from "@docusaurus/theme-common";
import { FFmpeg } from "@ffmpeg/ffmpeg";
import { fetchFile } from "@ffmpeg/util";
import AceEditor from "react-ace";
import styles from "./styles.module.css";
import { getFFmpeg } from "./ffmpeg";
import { SAMPLE_FILES } from "./const";
import LinearProgressWithLabel from "./LinearProgressWithLabel";
import "ace-builds/src-noconflict/mode-json";
import "ace-builds/src-noconflict/mode-javascript";
import "ace-builds/src-noconflict/mode-text";
import "ace-builds/src-noconflict/theme-dracula";
import "ace-builds/src-noconflict/theme-github";
const defaultArgs = JSON.stringify(["-i", "video.avi", "video.mp4"], null, 2);
const options = ["Download", "Download as Text File", "Delete"];
const genFFmpegText = (args: string) => {
let data: any = [];
try {
data = JSON.parse(args);
} catch (e) {}
return `// equivalent ffmpeg.wasm API call
ffmpeg.exec(${JSON.stringify(data)});
// equivalent ffmpeg command line
ffmpeg ${data.join(" ")}`;
};
export default function Editor() {
const { useState, useEffect } = React;
const [args, setArgs] = useState<string>(defaultArgs);
const [logs, setLogs] = useState<string[]>([]);
const [output, setOutput] = useState<Ace.Editor>();
const [path, setPath] = useState<string>("/");
const [nodes, setNodes] = useState<string[]>([]);
const [progress, setProgress] = useState<number>(-1);
const { colorMode } = useColorMode();
const theme = colorMode === "dark" ? "github" : "dracula";
const scrollToEnd = () => {
output && output.renderer.scrollToLine(Number.POSITIVE_INFINITY);
};
const loadSamples = async () => {
const ffmpeg = getFFmpeg();
Object.keys(SAMPLE_FILES).forEach(async (name) => {
await ffmpeg.writeFile(name, await fetchFile(SAMPLE_FILES[name]));
});
// Somehow we need to wait a little bit before reading the new nodes.
setTimeout(async () => {
setNodes(await ffmpeg.listDir(path));
}, 500);
};
const refreshDir = async () => {
setNodes(await getFFmpeg().listDir(path));
};
const exec = async () => {
const ffmpeg = getFFmpeg();
setProgress(-1);
const logListener = ({ message }) => {
setLogs((_logs) => [..._logs, message]);
scrollToEnd();
};
const progListener = ({ progress: prog }) => {
setProgress(prog * 100);
};
ffmpeg.on(FFmpeg.LOG, logListener);
ffmpeg.on(FFmpeg.PROGRESS, progListener);
await ffmpeg.exec(JSON.parse(args));
ffmpeg.removeListener(FFmpeg.LOG, logListener);
ffmpeg.removeListener(FFmpeg.PROGRESS, progListener);
setNodes(await ffmpeg.listDir(path));
};
useEffect(() => {
const ffmpeg = getFFmpeg();
ffmpeg.listDir(path).then((nodes) => {
setNodes(nodes);
});
}, []);
return (
<Box sx={{ flexGrow: 1 }}>
<Grid
container
spacing={{ xs: 2, md: 3 }}
columns={{ xs: 4, sm: 8, md: 12 }}
>
<Grid item xs={4}>
<Stack direction="row" className={styles.fsTitle}>
<Typography>File System:</Typography>
<Box>
<Tooltip title="Upload a media file">
<IconButton onClick={() => {}} aria-label="upload-media-file">
<UploadFileIcon />
</IconButton>
</Tooltip>
<Tooltip title="Upload a text file">
<IconButton onClick={() => {}} aria-label="upload-text">
<UploadIcon />
</IconButton>
</Tooltip>
<Tooltip title="Refresh directory">
<IconButton onClick={refreshDir} aria-label="fresh">
<RefreshIcon />
</IconButton>
</Tooltip>
</Box>
</Stack>
<Typography>{`${path}`}</Typography>
<List dense={true}>
{nodes.map((node, index) => (
<ListItem
key={index}
secondaryAction={
<>
<IconButton
aria-label="more"
id="long-button"
aria-haspopup="true"
edge="end"
>
<MoreVertIcon />
</IconButton>
<Menu
id="long-menu"
MenuListProps={{
"aria-labelledby": "long-button",
}}
PaperProps={{
style: {
width: "20ch",
},
}}
>
{options.map((option) => (
<MenuItem key={option} selected={option === "Download"}>
{option}
</MenuItem>
))}
</Menu>
</>
}
>
<ListItemIcon>
<FolderIcon />
</ListItemIcon>
<ListItemText primary={node} />
</ListItem>
))}
</List>
</Grid>
<Grid item xs={8}>
<Stack spacing={1}>
<Box>
<Typography>Edit JSON below to update command:</Typography>
<AceEditor
mode="json"
theme={theme}
name="input-args"
fontSize={16}
showPrintMargin={true}
showGutter={true}
width="100%"
minLines={8}
maxLines={8}
highlightActiveLine={true}
value={args}
onChange={(value) => setArgs(value)}
setOptions={{ tabSize: 2 }}
/>
</Box>
<AceEditor
mode="javascript"
theme={theme}
name="ffmpeg.wasm"
fontSize={16}
showGutter={false}
width="100%"
minLines={6}
maxLines={6}
readOnly
highlightActiveLine={false}
value={genFFmpegText(args)}
setOptions={{ tabSize: 2 }}
/>
<Stack direction="row" spacing={2} className={styles.alignRight}>
<Button onClick={loadSamples}>Load Sample Files</Button>
<Button variant="contained" onClick={exec}>
Run
</Button>
</Stack>
{progress === -1 ? (
<></>
) : (
<>
<Typography>Transcoding Progress:</Typography>
<LinearProgressWithLabel value={progress} />
</>
)}
<AceEditor
placeholder="ffmpeg console output"
mode="text"
theme={theme}
name="console"
fontSize={16}
width="100%"
minLines={8}
maxLines={8}
readOnly
showPrintMargin={true}
highlightActiveLine={false}
value={logs.join("\n")}
setOptions={{ tabSize: 2 }}
onLoad={(editor) => setOutput(editor)}
/>
</Stack>
</Grid>
</Grid>
</Box>
);
}

View File

@@ -0,0 +1,23 @@
import * as React from "react";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import LinearProgress, {
LinearProgressProps,
} from "@mui/material/LinearProgress";
export default function LinearProgressWithLabel(
props: LinearProgressProps & { value: number }
) {
return (
<Box sx={{ display: "flex", alignItems: "center" }}>
<Box sx={{ width: "100%", mr: 1 }}>
<LinearProgress variant="determinate" {...props} />
</Box>
<Box sx={{ minWidth: 35 }}>
<Typography variant="body2" color="text.secondary">{`${Math.round(
props.value
)}%`}</Typography>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,12 @@
export const CORE_SIZE = {
"https://unpkg.com/@ffmpeg/core@0.12.0-alpha.2/dist/umd/ffmpeg-core.js": 111646,
"https://unpkg.com/@ffmpeg/core@0.12.0-alpha.2/dist/umd/ffmpeg-core.wasm": 31967534,
"https://unpkg.com/@ffmpeg/core-mt@0.12.0-alpha.2/dist/umd/ffmpeg-core.js": 130002,
"https://unpkg.com/@ffmpeg/core-mt@0.12.0-alpha.2/dist/umd/ffmpeg-core.wasm": 32441947,
"https://unpkg.com/@ffmpeg/core-mt@0.12.0-alpha.2/dist/umd/ffmpeg-core.worker.js": 2978,
};
export const SAMPLE_FILES = {
"video.avi":
"https://raw.githubusercontent.com/ffmpegwasm/testdata/master/video-3s.avi",
};

View File

@@ -0,0 +1,5 @@
import { FFmpeg } from "@ffmpeg/ffmpeg";
let ffmpeg = new FFmpeg();
export const getFFmpeg = () => ffmpeg;

View File

@@ -1,38 +1,54 @@
import React, { useState } from "react";
import * as React from "react";
import { FFmpeg } from "@ffmpeg/ffmpeg";
import Button from "@mui/material/Button";
import { useColorMode } from "@docusaurus/theme-common";
import { ThemeProvider, createTheme } from "@mui/material/styles";
import MuiThemeProvider from "@site/src/components/MuiThemeProvider";
import CoreSelector from "./CoreSelector";
import CoreDownloader from "./CoreDownloader";
import Editor from "./Editor";
import { getFFmpeg } from "./ffmpeg";
const lightTheme = createTheme({});
const darkTheme = createTheme({
palette: {
mode: "dark",
},
});
enum State {
NOT_LOADED,
LOADING,
LOADED,
}
export default function Playground() {
const [loaded, setLoaded] = useState(false);
const { colorMode } = useColorMode();
const ffmpeg = new FFmpeg();
const { useState } = React;
const [state, setState] = useState(State.LOADED);
const [option, setOption] = useState("core");
const [url, setURL] = useState("");
const [received, setReceived] = useState(0);
const load = async () => {
ffmpeg.on(FFmpeg.DOWNLOAD, ({ url, total, received, done }) => {
console.log(url, total, received, done);
setState(State.LOADING);
const ffmpeg = getFFmpeg();
ffmpeg.terminate();
ffmpeg.on(FFmpeg.DOWNLOAD, ({ url: _url, received: _received }) => {
setURL(_url as string);
setReceived(_received);
});
await ffmpeg.load({
coreURL: "http://localhost:8080/packages/core/dist/umd/ffmpeg-core.js",
});
setLoaded(true);
await ffmpeg.load();
setState(State.LOADED);
};
return (
<ThemeProvider theme={colorMode === "dark" ? darkTheme : lightTheme}>
{loaded ? (
<></>
) : (
<Button variant="contained" onClick={load}>
Load
</Button>
)}
</ThemeProvider>
<MuiThemeProvider>
{(() => {
switch (state) {
case State.LOADING:
return <CoreDownloader url={url} received={received} />;
case State.LOADED:
return <Editor />;
default:
return <></>;
}
})()}
<CoreSelector
option={option}
onChange={(event) => {
setOption((event.target as HTMLInputElement).value);
}}
onSubmit={load}
/>
</MuiThemeProvider>
);
}

View File

@@ -0,0 +1,13 @@
.margin {
margin-bottom: 32px;
}
.alignRight {
display: flex;
justify-content: flex-end;
}
.fsTitle {
align-items: center;
justify-content: space-between;
}