Refactor playground page and init docs
This commit is contained in:
@@ -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 }) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
121
apps/website/src/components/Playground/Workspace/Editor.tsx
Normal file
121
apps/website/src/components/Playground/Workspace/Editor.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
154
apps/website/src/components/Playground/Workspace/index.tsx
Normal file
154
apps/website/src/components/Playground/Workspace/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface Node {
|
||||
name: string;
|
||||
isDir: boolean;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { FFmpeg } from "@ffmpeg/ffmpeg";
|
||||
|
||||
let ffmpeg = new FFmpeg();
|
||||
|
||||
export const getFFmpeg = () => ffmpeg;
|
||||
@@ -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 <></>;
|
||||
}
|
||||
|
||||
11
apps/website/src/components/common/ThemedButton/index.tsx
Normal file
11
apps/website/src/components/common/ThemedButton/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user