Update website and @ffmpeg/util
This commit is contained in:
20
apps/website/src/components/MuiThemeProvider/index.tsx
Normal file
20
apps/website/src/components/MuiThemeProvider/index.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
16
apps/website/src/components/Playground/CoreDownloader.tsx
Normal file
16
apps/website/src/components/Playground/CoreDownloader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
53
apps/website/src/components/Playground/CoreSelector.tsx
Normal file
53
apps/website/src/components/Playground/CoreSelector.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
246
apps/website/src/components/Playground/Editor.tsx
Normal file
246
apps/website/src/components/Playground/Editor.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
12
apps/website/src/components/Playground/const.ts
Normal file
12
apps/website/src/components/Playground/const.ts
Normal 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",
|
||||
};
|
||||
5
apps/website/src/components/Playground/ffmpeg.ts
Normal file
5
apps/website/src/components/Playground/ffmpeg.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { FFmpeg } from "@ffmpeg/ffmpeg";
|
||||
|
||||
let ffmpeg = new FFmpeg();
|
||||
|
||||
export const getFFmpeg = () => ffmpeg;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
13
apps/website/src/components/Playground/styles.module.css
Normal file
13
apps/website/src/components/Playground/styles.module.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.margin {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.alignRight {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.fsTitle {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
Reference in New Issue
Block a user