Refactor playground page and init docs

This commit is contained in:
Jerome Wu
2022-10-08 16:10:15 +08:00
parent 9cd1f00ec7
commit 3f79cb5e81
49 changed files with 663 additions and 1024 deletions

View File

@@ -1,7 +1,7 @@
import * as React from "react";
import Typography from "@mui/material/Typography";
import Container from "@mui/material/Container";
import LinearProgressWithLabel from "./LinearProgressWithLabel";
import LinearProgressWithLabel from "@site/src/components/common/LinearProgressWithLabel";
import { CORE_SIZE } from "./const";
export default function CoreDownloader({ url, received }) {

View File

@@ -19,12 +19,18 @@ export default function CoreSwitcher({ checked, onChange }: CoreSwitcherProps) {
<FormGroup>
<FormControlLabel
control={<Switch checked={checked} onChange={onChange} />}
label="Use Multi-thread"
label="Use Multithreading"
disabled={typeof SharedArrayBuffer !== "function"}
/>
</FormGroup>
<Tooltip title="Multi-threaded core is faster, but unstable and not supported by all browsers. Click here for more details.">
<IconButton aria-label="help" size="small">
<IconButton
aria-label="help"
size="small"
onClick={() => {
location.href = "/docs/getting-started/multi-thread";
}}
>
<HelpIcon fontSize="small" />
</IconButton>
</Tooltip>

View File

@@ -1,387 +0,0 @@
/// <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 RefreshIcon from "@mui/icons-material/Refresh";
import UploadFileIcon from "@mui/icons-material/UploadFile";
import UploadIcon from "@mui/icons-material/Upload";
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
import CreateNewFolderIcon from "@mui/icons-material/CreateNewFolder";
import Tooltip from "@mui/material/Tooltip";
import Paper from "@mui/material/Paper";
import { useColorMode } from "@docusaurus/theme-common";
import { FFmpeg } from "@ffmpeg/ffmpeg";
import { fetchFile } from "@ffmpeg/util";
import { downloadFile } from "@site/src/util";
import AceEditor from "react-ace";
import { getFFmpeg } from "./ffmpeg";
import { SAMPLE_FILES } from "./const";
import LinearProgressWithLabel from "./LinearProgressWithLabel";
import MoreButton from "./MoreButton";
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";
import ListItemButton from "@mui/material/ListItemButton";
import Modal from "@mui/material/Modal";
import TextField from "@mui/material/TextField";
const defaultArgs = JSON.stringify(["-i", "video.avi", "video.mp4"], null, 2);
const options = [
{ text: "Download", key: "download" },
{ text: "Download as Text File", key: "download-text" },
{ text: "Delete", key: "delete" },
];
const modalStyle = {
position: "absolute" as "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
bgcolor: "background.paper",
p: 4,
};
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([]);
const [progress, setProgress] = useState<number>(0);
const [open, setOpen] = useState(false);
const [time, setTime] = useState(0);
const [folderName, setFolderName] = useState("");
const handleModalOpen = () => setOpen(true);
const handleModalClose = () => setOpen(false);
const { colorMode } = useColorMode();
const theme = colorMode === "dark" ? "github" : "dracula";
const scrollToEnd = () => {
output && output.renderer.scrollToLine(Number.POSITIVE_INFINITY);
};
const refreshDir = async (curPath: string) => {
const ffmpeg = getFFmpeg();
if (ffmpeg.loaded) {
setNodes(
(await ffmpeg.listDir(curPath)).filter(({ name }) => name !== ".")
);
}
};
const loadSamples = async () => {
const ffmpeg = getFFmpeg();
for (const name of Object.keys(SAMPLE_FILES)) {
await ffmpeg.writeFile(name, await fetchFile(SAMPLE_FILES[name]));
}
refreshDir(path);
};
const exec = async () => {
const ffmpeg = getFFmpeg();
setProgress(0);
setTime(0);
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);
const start = performance.now();
await ffmpeg.exec(JSON.parse(args));
setTime(performance.now() - start);
ffmpeg.removeListener(FFmpeg.LOG, logListener);
ffmpeg.removeListener(FFmpeg.PROGRESS, progListener);
refreshDir(path);
};
const cd = (name: string) => async () => {
let nextPath = path;
if (path === "/") {
if (name !== "..") nextPath = `/${name}`;
} else if (name === "..") {
const cols = path.split("/");
cols.pop();
nextPath = cols.length === 1 ? "/" : cols.join("/");
} else {
nextPath = `${path}/${name}`;
}
setPath(nextPath);
refreshDir(nextPath);
};
const handleFileUpload =
(isText: boolean = false) =>
async ({ target: { files } }: React.ChangeEvent<HTMLInputElement>) => {
const ffmpeg = getFFmpeg();
for (let i = 0; i < files.length; i++) {
const file = files[i];
let data: Uint8Array | string = await fetchFile(file);
if (isText) data = new TextDecoder().decode(data);
await ffmpeg.writeFile(`${path}/${file.name}`, data);
}
refreshDir(path);
};
const handleItemClick = (name: string) => async (option: string) => {
const ffmpeg = getFFmpeg();
const fullPath = `${path}/${name}`;
switch (option) {
case "download":
downloadFile(
name,
((await ffmpeg.readFile(fullPath, "binary")) as Uint8Array).buffer
);
break;
case "download-text":
downloadFile(name, await ffmpeg.readFile(fullPath, "utf8"));
break;
case "delete":
await ffmpeg.deleteFile(fullPath);
refreshDir(path);
break;
default:
break;
}
};
const handleFolderCreate = async () => {
if (folderName !== "") {
await getFFmpeg().createDir(`${path}/${folderName}`);
}
refreshDir(path);
handleModalClose();
};
useEffect(() => {
refreshDir(path);
}, []);
return (
<Box sx={{ flexGrow: 1 }}>
<Grid container spacing={{ xs: 1 }} columns={{ xs: 4, md: 12 }}>
<Grid item xs={4}>
<Paper variant="outlined" style={{ padding: 8, height: "100%" }}>
<Stack justifyContent="space-between" style={{ height: "100%" }}>
<>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Typography>File System:</Typography>
<Box>
<Tooltip title="Upload a media file">
<IconButton
aria-label="upload-media-file"
component="label"
size="small"
>
<input
hidden
multiple
type="file"
onChange={handleFileUpload(false)}
/>
<UploadFileIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Upload a text file">
<IconButton
onClick={() => {}}
aria-label="upload-text"
component="label"
size="small"
>
<input
hidden
multiple
type="file"
onChange={handleFileUpload(true)}
/>
<UploadIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Create a new folder">
<IconButton
onClick={() => {
setFolderName("");
handleModalOpen();
}}
aria-label="create-a-new-folder"
size="small"
>
<CreateNewFolderIcon />
</IconButton>
</Tooltip>
<Tooltip title="Refresh directory">
<IconButton
onClick={() => refreshDir(path)}
aria-label="fresh"
size="small"
>
<RefreshIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
</Stack>
<Typography>{`Path: ${path}`}</Typography>
<List style={{ height: 480, overflowX: "auto" }}>
{nodes.map(({ name, isDir }, index) =>
isDir ? (
<ListItemButton key={index} onClick={cd(name)}>
<ListItemIcon>
<FolderIcon />
</ListItemIcon>
<ListItemText primary={name} />
</ListItemButton>
) : (
<ListItem
key={index}
secondaryAction={
<MoreButton
options={options}
onItemClick={handleItemClick(name)}
/>
}
>
<ListItemIcon>
<InsertDriveFileIcon />
</ListItemIcon>
<ListItemText primary={name} />
</ListItem>
)
)}
</List>
</>
<Button onClick={loadSamples}>Load Sample Files</Button>
</Stack>
</Paper>
</Grid>
<Grid item xs={8}>
<Paper variant="outlined" style={{ padding: 8, height: "100%" }}>
<Stack spacing={1}>
<Stack>
<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 }}
/>
</Stack>
<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 }}
/>
<Typography>Console Output:</Typography>
<AceEditor
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)}
/>
<Typography>Transcoding Progress:</Typography>
<LinearProgressWithLabel value={progress} />
<Stack direction="row" spacing={2} justifyContent="space-between">
<Typography>
{time === 0
? ""
: `Time Elapsed: ${(time / 1000).toFixed(2)} s`}
</Typography>
<Button variant="contained" onClick={exec}>
Run
</Button>
</Stack>
</Stack>
</Paper>
</Grid>
</Grid>
<Modal
open={open}
onClose={handleModalClose}
aria-labelledby="new-folder-name"
aria-describedby="new-folder-name-description"
>
<Box sx={modalStyle}>
<Stack spacing={4}>
<Typography id="modal-modal-title" variant="h6" component="h2">
Folder Name:
</Typography>
<TextField
id="outlined-basic"
label="my-folder"
variant="outlined"
value={folderName}
onChange={(event) => setFolderName(event.target.value)}
/>
<Stack direction="row" justifyContent="flex-end">
<Button onClick={handleModalClose}>Cancel</Button>
<Button variant="contained" onClick={handleFolderCreate}>
Create
</Button>
</Stack>
</Stack>
</Box>
</Modal>
</Box>
);
}

View File

@@ -0,0 +1,121 @@
/// <reference types="ace" />
import React, { useEffect, useState } from "react";
import AceEditor from "react-ace";
import Paper from "@mui/material/Paper";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import LinearProgressWithLabel from "@site/src/components/common/LinearProgressWithLabel";
import { useColorMode } from "@docusaurus/theme-common";
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 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(" ")}`;
};
interface EditorProps {
args: string;
logs: string[];
progress: number;
time: number;
onArgsUpdate: (args: string) => void;
onExec: () => Promise<void>;
}
export default function Editor({
args = "",
logs = [],
progress = 0,
time = 0,
onArgsUpdate,
onExec,
}: EditorProps) {
const { colorMode } = useColorMode();
const [output, setOutput] = useState<Ace.Editor>();
useEffect(() => {
// scroll logs to the end.
output && output.renderer.scrollToLine(Number.POSITIVE_INFINITY);
}, [logs]);
const theme = colorMode === "dark" ? "github" : "dracula";
return (
<Paper variant="outlined" style={{ padding: 8, height: "100%" }}>
<Stack spacing={1}>
<Stack>
<Typography>Editor:</Typography>
<Typography>Edit arguments 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={onArgsUpdate}
setOptions={{ tabSize: 2 }}
/>
</Stack>
<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 }}
/>
<Typography>Console Output:</Typography>
<AceEditor
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)}
/>
<Typography>Transcoding Progress:</Typography>
<LinearProgressWithLabel value={progress} />
<Stack direction="row" spacing={2} justifyContent="space-between">
<Typography>
{time === 0 ? "" : `Time Elapsed: ${(time / 1000).toFixed(2)} s`}
</Typography>
<Button variant="contained" onClick={onExec}>
Run
</Button>
</Stack>
</Stack>
</Paper>
);
}

View File

@@ -0,0 +1,202 @@
import React, { useState, ChangeEvent } from "react";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import Modal from "@mui/material/Modal";
import Paper from "@mui/material/Paper";
import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";
import FolderIcon from "@mui/icons-material/Folder";
import RefreshIcon from "@mui/icons-material/Refresh";
import UploadFileIcon from "@mui/icons-material/UploadFile";
import UploadIcon from "@mui/icons-material/Upload";
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
import CreateNewFolderIcon from "@mui/icons-material/CreateNewFolder";
import MoreButton from "./MoreButton";
import { Node } from "./types";
interface FileSystemManagerProps {
path: string;
nodes: Node[];
onFileUpload: (
isText: boolean
) => (event: ChangeEvent<HTMLInputElement>) => Promise<void>;
onDirClick: (name: string) => () => Promise<void>;
onFileClick: (name: string) => (option: string) => Promise<void>;
onDirCreate: (name: string) => () => Promise<void>;
onRefresh: () => Promise<void>;
onLoadSamples: () => Promise<void>;
}
const modalStyle = {
position: "absolute" as "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
bgcolor: "background.paper",
p: 4,
};
export const options = [
{ text: "Download", key: "download" },
{ text: "Download as Text File", key: "download-text" },
{ text: "Delete", key: "delete" },
];
export default function FileSystemManager({
path = "/",
nodes = [],
onFileUpload = () => () => Promise.resolve(),
onFileClick = () => () => Promise.resolve(),
onDirClick = () => () => Promise.resolve(),
onDirCreate = () => () => Promise.resolve(),
onRefresh = () => Promise.resolve(),
onLoadSamples = () => Promise.resolve(),
}: FileSystemManagerProps) {
const [open, setOpen] = useState(false);
const [dirName, setDirName] = useState("");
const handleModalOpen = () => setOpen(true);
const handleModalClose = () => setOpen(false);
return (
<>
<Paper variant="outlined" style={{ padding: 8, height: "100%" }}>
<Stack justifyContent="space-between" style={{ height: "100%" }}>
<>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Typography>File System:</Typography>
<Box>
<Tooltip title="Upload a media file">
<IconButton
aria-label="upload-media-file"
component="label"
size="small"
>
<input
hidden
multiple
type="file"
onChange={onFileUpload(false)}
/>
<UploadFileIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Upload a text file">
<IconButton
onClick={() => {}}
aria-label="upload-text"
component="label"
size="small"
>
<input
hidden
multiple
type="file"
onChange={onFileUpload(true)}
/>
<UploadIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Create a new folder">
<IconButton
onClick={() => {
setDirName("");
handleModalOpen();
}}
aria-label="create-a-new-folder"
size="small"
>
<CreateNewFolderIcon />
</IconButton>
</Tooltip>
<Tooltip title="Refresh directory">
<IconButton
onClick={onRefresh}
aria-label="fresh"
size="small"
>
<RefreshIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
</Stack>
<Typography>{`Path: ${path}`}</Typography>
<List style={{ height: 480, overflowX: "auto" }}>
{nodes.map(({ name, isDir }, index) =>
isDir ? (
<ListItemButton key={index} onClick={onDirClick(name)}>
<ListItemIcon>
<FolderIcon />
</ListItemIcon>
<ListItemText primary={name} />
</ListItemButton>
) : (
<ListItem
key={index}
secondaryAction={
<MoreButton
options={options}
onItemClick={onFileClick(name)}
/>
}
>
<ListItemIcon>
<InsertDriveFileIcon />
</ListItemIcon>
<ListItemText primary={name} />
</ListItem>
)
)}
</List>
</>
<Button onClick={onLoadSamples}>Load Sample Files</Button>
</Stack>
</Paper>
<Modal
open={open}
onClose={handleModalClose}
aria-labelledby="new-folder-name"
aria-describedby="new-folder-name-description"
>
<Box sx={modalStyle}>
<Stack spacing={4}>
<Typography id="modal-modal-title" variant="h6" component="h2">
Folder Name:
</Typography>
<TextField
id="outlined-basic"
label="my-folder"
variant="outlined"
value={dirName}
onChange={(event) => setDirName(event.target.value)}
/>
<Stack direction="row" justifyContent="flex-end">
<Button onClick={handleModalClose}>Cancel</Button>
<Button
variant="contained"
onClick={() => {
onDirCreate(dirName);
handleModalClose();
}}
>
Create
</Button>
</Stack>
</Stack>
</Box>
</Modal>
</>
);
}

View File

@@ -0,0 +1,154 @@
import React, {
ChangeEvent,
useState,
useEffect,
MutableRefObject,
} from "react";
import Box from "@mui/material/Box";
import Grid from "@mui/material/Grid";
import { FFmpeg } from "@ffmpeg/ffmpeg";
import { fetchFile } from "@ffmpeg/util";
import { downloadFile } from "@site/src/util";
import { Node } from "./types";
import FileSystemManager from "./FileSystemManager";
import { SAMPLE_FILES } from "../const";
import Editor from "./Editor";
const defaultArgs = JSON.stringify(["-i", "video.avi", "video.mp4"], null, 2);
interface WorkspaceProps {
ffmpeg: MutableRefObject<FFmpeg>;
}
export default function Workspace({ ffmpeg: _ffmpeg }: WorkspaceProps) {
const [path, setPath] = useState("/");
const [nodes, setNodes] = useState<Node[]>([]);
const [args, setArgs] = useState(defaultArgs);
const [progress, setProgress] = useState(0);
const [time, setTime] = useState(0);
const [logs, setLogs] = useState<string[]>([]);
const ffmpeg = _ffmpeg.current;
const refreshDir = async (curPath: string) => {
if (ffmpeg.loaded) {
setNodes(
(await ffmpeg.listDir(curPath)).filter(({ name }) => name !== ".")
);
}
};
const onFileUpload =
(isText: boolean) =>
async ({ target: { files } }: ChangeEvent<HTMLInputElement>) => {
for (let i = 0; i < files.length; i++) {
const file = files[i];
let data: Uint8Array | string = await fetchFile(file);
if (isText) data = new TextDecoder().decode(data);
await ffmpeg.writeFile(`${path}/${file.name}`, data);
}
refreshDir(path);
};
const onFileClick = (name: string) => async (option: string) => {
const fullPath = `${path}/${name}`;
switch (option) {
case "download":
downloadFile(
name,
((await ffmpeg.readFile(fullPath, "binary")) as Uint8Array).buffer
);
break;
case "download-text":
downloadFile(name, await ffmpeg.readFile(fullPath, "utf8"));
break;
case "delete":
await ffmpeg.deleteFile(fullPath);
refreshDir(path);
break;
default:
break;
}
};
const onDirClick = (name: string) => async () => {
let nextPath = path;
if (path === "/") {
if (name !== "..") nextPath = `/${name}`;
} else if (name === "..") {
const cols = path.split("/");
cols.pop();
nextPath = cols.length === 1 ? "/" : cols.join("/");
} else {
nextPath = `${path}/${name}`;
}
setPath(nextPath);
refreshDir(nextPath);
};
const onDirCreate = (name: string) => async () => {
if (name !== "") {
await ffmpeg.createDir(`${path}/${name}`);
}
refreshDir(path);
};
const onLoadSamples = async () => {
for (const name of Object.keys(SAMPLE_FILES)) {
await ffmpeg.writeFile(name, await fetchFile(SAMPLE_FILES[name]));
}
refreshDir(path);
};
const onExec = async () => {
setProgress(0);
setTime(0);
const logListener = ({ message }) => {
setLogs((_logs) => [..._logs, message]);
};
const progListener = ({ progress: prog }) => {
setProgress(prog * 100);
};
ffmpeg.on(FFmpeg.LOG, logListener);
ffmpeg.on(FFmpeg.PROGRESS, progListener);
const start = performance.now();
await ffmpeg.exec(JSON.parse(args));
setTime(performance.now() - start);
ffmpeg.removeListener(FFmpeg.LOG, logListener);
ffmpeg.removeListener(FFmpeg.PROGRESS, progListener);
refreshDir(path);
};
useEffect(() => {
refreshDir(path);
}, []);
return (
<Box sx={{ flexGrow: 1 }}>
<Grid container spacing={{ xs: 1 }} columns={{ xs: 4, md: 12 }}>
<Grid item xs={4}>
<FileSystemManager
path={path}
nodes={nodes}
onFileUpload={onFileUpload}
onFileClick={onFileClick}
onDirClick={onDirClick}
onDirCreate={onDirCreate}
onLoadSamples={onLoadSamples}
onRefresh={() => refreshDir(path)}
/>
</Grid>
<Grid item xs={8}>
<Editor
args={args}
logs={logs}
progress={progress}
time={time}
onArgsUpdate={(_args) => setArgs(_args)}
onExec={onExec}
/>
</Grid>
</Grid>
</Box>
);
}

View File

@@ -0,0 +1,4 @@
export interface Node {
name: string;
isDir: boolean;
}

View File

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

View File

@@ -1,10 +1,9 @@
import * as React from "react";
import React, { useState, useEffect, useRef } from "react";
import { FFmpeg } from "@ffmpeg/ffmpeg";
import Stack from "@mui/material/Stack";
import MuiThemeProvider from "@site/src/components/MuiThemeProvider";
import MuiThemeProvider from "@site/src/components/common/MuiThemeProvider";
import CoreDownloader from "./CoreDownloader";
import Editor from "./Editor";
import { getFFmpeg } from "./ffmpeg";
import Workspace from "./Workspace";
import { CORE_URL, CORE_MT_URL } from "./const";
import CoreSwitcher from "./CoreSwitcher";
@@ -15,20 +14,20 @@ enum State {
}
export default function Playground() {
const { useState, useEffect } = React;
const [state, setState] = useState(State.LOADED);
const [isCoreMT, setIsCoreMT] = useState(false);
const [url, setURL] = useState("");
const [received, setReceived] = useState(0);
const ffmpeg = useRef(new FFmpeg());
const load = async (mt: boolean = false) => {
setState(State.LOADING);
const ffmpeg = getFFmpeg();
ffmpeg.terminate();
ffmpeg.on(FFmpeg.DOWNLOAD, ({ url: _url, received: _received }) => {
ffmpeg.current.terminate();
ffmpeg.current.on(FFmpeg.DOWNLOAD, ({ url: _url, received: _received }) => {
setURL(_url as string);
setReceived(_received);
});
await ffmpeg.load({
await ffmpeg.current.load({
coreURL: mt ? CORE_MT_URL : CORE_URL,
thread: mt,
});
@@ -54,7 +53,7 @@ export default function Playground() {
case State.LOADING:
return <CoreDownloader url={url} received={received} />;
case State.LOADED:
return <Editor />;
return <Workspace ffmpeg={ffmpeg} />;
default:
return <></>;
}

View File

@@ -0,0 +1,11 @@
import React from "react";
import MuiThemeProvider from "../MuiThemeProvider";
import Button, { ButtonProps } from "@mui/material/Button";
export default function ThemedButton(props: ButtonProps) {
return (
<MuiThemeProvider>
<Button {...props} />
</MuiThemeProvider>
);
}

View File

@@ -0,0 +1,11 @@
import React from "react";
import MuiThemeProvider from "../MuiThemeProvider";
import IconButton, { IconButtonProps } from "@mui/material/IconButton";
export default function ThemedIconButton(props: IconButtonProps) {
return (
<MuiThemeProvider>
<IconButton {...props} />
</MuiThemeProvider>
);
}

View File

@@ -18,7 +18,7 @@ function HomepageHeader() {
<div className={styles.buttons}>
<Link
className="button button--secondary button--lg"
to="/docs/intro"
to="/playground"
>
Try it Now!
</Link>

View File

@@ -1,8 +1,104 @@
import Playground from "@site/src/components/Playground";
import CoreSwitcher from "@site/src/components/Playground/CoreSwitcher";
import FileSystemManager from
"@site/src/components/Playground/Workspace/FileSystemManager";
import Editor from
"@site/src/components/Playground/Workspace/Editor";
import MuiThemeProvider from "@site/src/components/common/MuiThemeProvider";
import ThemedButton from "@site/src/components/common/ThemedButton";
import ThemedIconButton from "@site/src/components/common/ThemedIconButton";
import CreateNewFolderIcon from "@mui/icons-material/CreateNewFolder";
import RefreshIcon from "@mui/icons-material/Refresh";
import UploadFileIcon from "@mui/icons-material/UploadFile";
import UploadIcon from "@mui/icons-material/Upload";
# Playground
Hi! Welcome to ffmpeg.wasm playground! Here you can try and test ffmpeg.wasm
with ease. :smile:
Playground allows you to try ffmpeg.wasm without any installation and
development!
:::tip Quick Start
1. Wait for assets (~32 MB) downloading.
2. Press <ThemedButton>Load Sample Files</ThemedButton> to download & add sample files.
3. Press <ThemedButton variant="contained">Run</ThemedButton> to convert an AVI file to MP4 file.
4. Download output files.
:::
<Playground />
<div style={{ height: 32 }} />
## How to Use :rocket:
> It is recommended to read [Introduction](/docs/intro) first to learn
ffmpeg.wasm fundamentals.
Demo Video:
<video width="100%" controls>
<source src="/video/playground-how-to.webm" type="video/webm" />
</video>
A typical flow to use ffmpeg.wasm is:
#### Download and load JavaScript & WebAssembly assets
The assets are downloaded automatically when you enter the Playground. You can
choose to use multithreading version instead by click on the switch:
<MuiThemeProvider>
<CoreSwitcher />
</MuiThemeProvider>
#### Load files to in-memory File System
When ffmpeg.wasm is loaded and ready, you can upload files to its in-memory File
System to make sure these files can be consumed by the ffmpeg.wasm APIs:
<div style={{ maxWidth: 260 }}>
<MuiThemeProvider>
<FileSystemManager
nodes={[
{name: "..", isDir: true},
{name: "tmp", isDir: true},
{name: "home", isDir: true},
{name: "dev", isDir: true},
{name: "proc", isDir: true},
{name: "video.avi", isDir: false},
]}
/>
</MuiThemeProvider>
</div>
<div style={{ height: 32 }} />
- <ThemedIconButton size="small"><UploadFileIcon fontSize="small"
/></ThemedIconButton>: Upload a media file.
- <ThemedIconButton size="small"><UploadIcon fontSize="small"
/></ThemedIconButton>: Upload a text file.
- <ThemedIconButton size="small"><CreateNewFolderIcon fontSize="small"
/></ThemedIconButton>: Create a new folder.
- <ThemedIconButton size="small"><RefreshIcon fontSize="small"
/></ThemedIconButton>: Refresh File System.
> Press <ThemedButton>Load Sample Files</ThemedButton> to load a set of samples
files.
#### Run a command
With files are ready in the File System, you can update arguments in the Editor
and hit <ThemedButton variant="contained">Run</ThemedButton> afterward:
<div style={{ maxWidth: 480 }}>
<MuiThemeProvider>
<Editor args={JSON.stringify(["-i", "video.avi", "video.mp4"], null, 2)} />
</MuiThemeProvider>
</div>
<div style={{ height: 32 }} />
#### Download output files
Lastly you can download your files using File System panel and check the result.
:tada: