Complete Playground v1

This commit is contained in:
Jerome Wu 2022-10-07 17:54:57 +08:00
parent e02437421b
commit f4a27e3491
12 changed files with 500 additions and 270 deletions

View File

@ -28,7 +28,7 @@ const ffmpeg = new FFmpeg();
#### Defined in #### Defined in
[packages/ffmpeg/src/classes.ts:97](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/1582ee2/packages/ffmpeg/src/classes.ts#L97) [packages/ffmpeg/src/classes.ts:99](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/4a950d7/packages/ffmpeg/src/classes.ts#L99)
## Properties ## Properties
@ -38,7 +38,7 @@ const ffmpeg = new FFmpeg();
#### Defined in #### Defined in
[packages/ffmpeg/src/classes.ts:95](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/1582ee2/packages/ffmpeg/src/classes.ts#L95) [packages/ffmpeg/src/classes.ts:95](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/4a950d7/packages/ffmpeg/src/classes.ts#L95)
___ ___
@ -51,7 +51,7 @@ be called when we receive message from web worker.
#### Defined in #### Defined in
[packages/ffmpeg/src/classes.ts:94](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/1582ee2/packages/ffmpeg/src/classes.ts#L94) [packages/ffmpeg/src/classes.ts:94](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/4a950d7/packages/ffmpeg/src/classes.ts#L94)
___ ___
@ -61,7 +61,17 @@ ___
#### Defined in #### Defined in
[packages/ffmpeg/src/classes.ts:88](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/1582ee2/packages/ffmpeg/src/classes.ts#L88) [packages/ffmpeg/src/classes.ts:88](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/4a950d7/packages/ffmpeg/src/classes.ts#L88)
___
### loaded
**loaded**: `boolean` = `false`
#### Defined in
[packages/ffmpeg/src/classes.ts:97](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/4a950d7/packages/ffmpeg/src/classes.ts#L97)
___ ___
@ -121,7 +131,7 @@ node_modules/@types/node/events.d.ts:290
#### Defined in #### Defined in
[packages/ffmpeg/src/classes.ts:84](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/1582ee2/packages/ffmpeg/src/classes.ts#L84) [packages/ffmpeg/src/classes.ts:84](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/4a950d7/packages/ffmpeg/src/classes.ts#L84)
___ ___
@ -131,7 +141,7 @@ ___
#### Defined in #### Defined in
[packages/ffmpeg/src/classes.ts:85](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/1582ee2/packages/ffmpeg/src/classes.ts#L85) [packages/ffmpeg/src/classes.ts:85](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/4a950d7/packages/ffmpeg/src/classes.ts#L85)
___ ___
@ -141,7 +151,7 @@ ___
#### Defined in #### Defined in
[packages/ffmpeg/src/classes.ts:86](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/1582ee2/packages/ffmpeg/src/classes.ts#L86) [packages/ffmpeg/src/classes.ts:86](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/4a950d7/packages/ffmpeg/src/classes.ts#L86)
## Event Methods ## Event Methods
@ -172,7 +182,7 @@ ffmpeg.on(FFmpeg.DOWNLOAD, ({ url, total, received, delta, done }) => {
#### Defined in #### Defined in
[packages/ffmpeg/src/classes.ts:33](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/1582ee2/packages/ffmpeg/src/classes.ts#L33) [packages/ffmpeg/src/classes.ts:33](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/4a950d7/packages/ffmpeg/src/classes.ts#L33)
**on**(`event`, `listener`): [`FFmpeg`](FFmpeg.md) **on**(`event`, `listener`): [`FFmpeg`](FFmpeg.md)
@ -203,7 +213,7 @@ log includes output to stdout and stderr.
#### Defined in #### Defined in
[packages/ffmpeg/src/classes.ts:52](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/1582ee2/packages/ffmpeg/src/classes.ts#L52) [packages/ffmpeg/src/classes.ts:52](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/4a950d7/packages/ffmpeg/src/classes.ts#L52)
**on**(`event`, `listener`): [`FFmpeg`](FFmpeg.md) **on**(`event`, `listener`): [`FFmpeg`](FFmpeg.md)
@ -235,7 +245,7 @@ input and output video/audio file are the same.
#### Defined in #### Defined in
[packages/ffmpeg/src/classes.ts:69](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/1582ee2/packages/ffmpeg/src/classes.ts#L69) [packages/ffmpeg/src/classes.ts:69](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/4a950d7/packages/ffmpeg/src/classes.ts#L69)
___ ___
@ -278,7 +288,7 @@ const data = ffmpeg.readFile("video.mp4");
#### Defined in #### Defined in
[packages/ffmpeg/src/classes.ts:197](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/1582ee2/packages/ffmpeg/src/classes.ts#L197) [packages/ffmpeg/src/classes.ts:202](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/4a950d7/packages/ffmpeg/src/classes.ts#L202)
___ ___
@ -303,7 +313,7 @@ as it initializes WebAssembly and other essential variables.
#### Defined in #### Defined in
[packages/ffmpeg/src/classes.ts:166](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/1582ee2/packages/ffmpeg/src/classes.ts#L166) [packages/ffmpeg/src/classes.ts:171](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/4a950d7/packages/ffmpeg/src/classes.ts#L171)
___ ___
@ -320,7 +330,7 @@ Terminate all ongoing API calls and terminate web worker.
#### Defined in #### Defined in
[packages/ffmpeg/src/classes.ts:218](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/1582ee2/packages/ffmpeg/src/classes.ts#L218) [packages/ffmpeg/src/classes.ts:223](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/4a950d7/packages/ffmpeg/src/classes.ts#L223)
___ ___
@ -344,7 +354,7 @@ Create a directory.
#### Defined in #### Defined in
[packages/ffmpeg/src/classes.ts:315](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/1582ee2/packages/ffmpeg/src/classes.ts#L315) [packages/ffmpeg/src/classes.ts:321](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/4a950d7/packages/ffmpeg/src/classes.ts#L321)
___ ___
@ -366,7 +376,7 @@ Delete an empty directory.
#### Defined in #### Defined in
[packages/ffmpeg/src/classes.ts:337](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/1582ee2/packages/ffmpeg/src/classes.ts#L337) [packages/ffmpeg/src/classes.ts:343](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/4a950d7/packages/ffmpeg/src/classes.ts#L343)
___ ___
@ -388,13 +398,13 @@ Delete a file.
#### Defined in #### Defined in
[packages/ffmpeg/src/classes.ts:293](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/1582ee2/packages/ffmpeg/src/classes.ts#L293) [packages/ffmpeg/src/classes.ts:299](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/4a950d7/packages/ffmpeg/src/classes.ts#L299)
___ ___
### listDir ### listDir
**listDir**(`path`): `Promise`<`FFFSPaths`\> **listDir**(`path`): `Promise`<`FSNode`[]\>
List directory contents. List directory contents.
@ -406,11 +416,11 @@ List directory contents.
#### Returns #### Returns
`Promise`<`FFFSPaths`\> `Promise`<`FSNode`[]\>
#### Defined in #### Defined in
[packages/ffmpeg/src/classes.ts:326](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/1582ee2/packages/ffmpeg/src/classes.ts#L326) [packages/ffmpeg/src/classes.ts:332](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/4a950d7/packages/ffmpeg/src/classes.ts#L332)
___ ___
@ -441,7 +451,7 @@ const data = await ffmpeg.readFile("video.mp4");
#### Defined in #### Defined in
[packages/ffmpeg/src/classes.ts:272](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/1582ee2/packages/ffmpeg/src/classes.ts#L272) [packages/ffmpeg/src/classes.ts:278](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/4a950d7/packages/ffmpeg/src/classes.ts#L278)
___ ___
@ -464,7 +474,7 @@ Rename a file or directory.
#### Defined in #### Defined in
[packages/ffmpeg/src/classes.ts:304](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/1582ee2/packages/ffmpeg/src/classes.ts#L304) [packages/ffmpeg/src/classes.ts:310](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/4a950d7/packages/ffmpeg/src/classes.ts#L310)
___ ___
@ -496,7 +506,7 @@ await ffmpeg.writeFile("text.txt", "hello world");
#### Defined in #### Defined in
[packages/ffmpeg/src/classes.ts:246](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/1582ee2/packages/ffmpeg/src/classes.ts#L246) [packages/ffmpeg/src/classes.ts:252](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/4a950d7/packages/ffmpeg/src/classes.ts#L252)
___ ___
@ -514,7 +524,7 @@ register worker message event handlers.
#### Defined in #### Defined in
[packages/ffmpeg/src/classes.ts:104](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/1582ee2/packages/ffmpeg/src/classes.ts#L104) [packages/ffmpeg/src/classes.ts:106](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/4a950d7/packages/ffmpeg/src/classes.ts#L106)
___ ___
@ -537,7 +547,7 @@ Generic function to send messages to web worker.
#### Defined in #### Defined in
[packages/ffmpeg/src/classes.ts:143](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/1582ee2/packages/ffmpeg/src/classes.ts#L143) [packages/ffmpeg/src/classes.ts:148](https://github.com/ffmpegwasm/ffmpeg.wasm/blob/4a950d7/packages/ffmpeg/src/classes.ts#L148)
___ ___

View File

@ -1,53 +0,0 @@
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,34 @@
import React from "react";
import Stack from "@mui/material/Stack";
import FormGroup from "@mui/material/FormGroup";
import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import IconButton from "@mui/material/IconButton";
import HelpIcon from "@mui/icons-material/HelpOutline";
import Tooltip from "@mui/material/Tooltip";
interface CoreSwitcherProps {
checked: boolean;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
}
export default function CoreSwitcher({ checked, onChange }: CoreSwitcherProps) {
return (
<>
<Stack direction="row" justifyContent="flex-end">
<FormGroup>
<FormControlLabel
control={<Switch checked={checked} onChange={onChange} />}
label="Use Multi-thread"
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">
<HelpIcon fontSize="small" />
</IconButton>
</Tooltip>
</Stack>
</>
);
}

View File

@ -12,29 +12,47 @@ import ListItem from "@mui/material/ListItem";
import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText"; import ListItemText from "@mui/material/ListItemText";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import RefreshIcon from "@mui/icons-material/Refresh"; import RefreshIcon from "@mui/icons-material/Refresh";
import UploadFileIcon from "@mui/icons-material/UploadFile"; import UploadFileIcon from "@mui/icons-material/UploadFile";
import UploadIcon from "@mui/icons-material/Upload"; 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 Tooltip from "@mui/material/Tooltip";
import Menu from "@mui/material/Menu"; import Paper from "@mui/material/Paper";
import MenuItem from "@mui/material/MenuItem";
import { useColorMode } from "@docusaurus/theme-common"; import { useColorMode } from "@docusaurus/theme-common";
import { FFmpeg } from "@ffmpeg/ffmpeg"; import { FFmpeg } from "@ffmpeg/ffmpeg";
import { fetchFile } from "@ffmpeg/util"; import { fetchFile } from "@ffmpeg/util";
import { downloadFile } from "@site/src/util";
import AceEditor from "react-ace"; import AceEditor from "react-ace";
import styles from "./styles.module.css";
import { getFFmpeg } from "./ffmpeg"; import { getFFmpeg } from "./ffmpeg";
import { SAMPLE_FILES } from "./const"; import { SAMPLE_FILES } from "./const";
import LinearProgressWithLabel from "./LinearProgressWithLabel"; import LinearProgressWithLabel from "./LinearProgressWithLabel";
import MoreButton from "./MoreButton";
import "ace-builds/src-noconflict/mode-json"; import "ace-builds/src-noconflict/mode-json";
import "ace-builds/src-noconflict/mode-javascript"; import "ace-builds/src-noconflict/mode-javascript";
import "ace-builds/src-noconflict/mode-text"; import "ace-builds/src-noconflict/mode-text";
import "ace-builds/src-noconflict/theme-dracula"; import "ace-builds/src-noconflict/theme-dracula";
import "ace-builds/src-noconflict/theme-github"; 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 defaultArgs = JSON.stringify(["-i", "video.avi", "video.mp4"], null, 2);
const options = ["Download", "Download as Text File", "Delete"]; 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) => { const genFFmpegText = (args: string) => {
let data: any = []; let data: any = [];
@ -54,33 +72,42 @@ export default function Editor() {
const [logs, setLogs] = useState<string[]>([]); const [logs, setLogs] = useState<string[]>([]);
const [output, setOutput] = useState<Ace.Editor>(); const [output, setOutput] = useState<Ace.Editor>();
const [path, setPath] = useState<string>("/"); const [path, setPath] = useState<string>("/");
const [nodes, setNodes] = useState<string[]>([]); const [nodes, setNodes] = useState([]);
const [progress, setProgress] = useState<number>(-1); 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 { colorMode } = useColorMode();
const theme = colorMode === "dark" ? "github" : "dracula"; const theme = colorMode === "dark" ? "github" : "dracula";
const scrollToEnd = () => { const scrollToEnd = () => {
output && output.renderer.scrollToLine(Number.POSITIVE_INFINITY); output && output.renderer.scrollToLine(Number.POSITIVE_INFINITY);
}; };
const loadSamples = async () => { const refreshDir = async (curPath: string) => {
const ffmpeg = getFFmpeg(); const ffmpeg = getFFmpeg();
Object.keys(SAMPLE_FILES).forEach(async (name) => { if (ffmpeg.loaded) {
await ffmpeg.writeFile(name, await fetchFile(SAMPLE_FILES[name])); setNodes(
}); (await ffmpeg.listDir(curPath)).filter(({ name }) => 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 () => { const loadSamples = async () => {
setNodes(await getFFmpeg().listDir(path)); 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 exec = async () => {
const ffmpeg = getFFmpeg(); const ffmpeg = getFFmpeg();
setProgress(-1); setProgress(0);
setTime(0);
const logListener = ({ message }) => { const logListener = ({ message }) => {
setLogs((_logs) => [..._logs, message]); setLogs((_logs) => [..._logs, message]);
scrollToEnd(); scrollToEnd();
@ -90,93 +117,181 @@ export default function Editor() {
}; };
ffmpeg.on(FFmpeg.LOG, logListener); ffmpeg.on(FFmpeg.LOG, logListener);
ffmpeg.on(FFmpeg.PROGRESS, progListener); ffmpeg.on(FFmpeg.PROGRESS, progListener);
const start = performance.now();
await ffmpeg.exec(JSON.parse(args)); await ffmpeg.exec(JSON.parse(args));
setTime(performance.now() - start);
ffmpeg.removeListener(FFmpeg.LOG, logListener); ffmpeg.removeListener(FFmpeg.LOG, logListener);
ffmpeg.removeListener(FFmpeg.PROGRESS, progListener); ffmpeg.removeListener(FFmpeg.PROGRESS, progListener);
setNodes(await ffmpeg.listDir(path)); 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(() => { useEffect(() => {
const ffmpeg = getFFmpeg(); refreshDir(path);
ffmpeg.listDir(path).then((nodes) => {
setNodes(nodes);
});
}, []); }, []);
return ( return (
<Box sx={{ flexGrow: 1 }}> <Box sx={{ flexGrow: 1 }}>
<Grid <Grid container spacing={{ xs: 1 }} columns={{ xs: 4, md: 12 }}>
container
spacing={{ xs: 2, md: 3 }}
columns={{ xs: 4, sm: 8, md: 12 }}
>
<Grid item xs={4}> <Grid item xs={4}>
<Stack direction="row" className={styles.fsTitle}> <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> <Typography>File System:</Typography>
<Box> <Box>
<Tooltip title="Upload a media file"> <Tooltip title="Upload a media file">
<IconButton onClick={() => {}} aria-label="upload-media-file"> <IconButton
<UploadFileIcon /> aria-label="upload-media-file"
component="label"
size="small"
>
<input
hidden
multiple
type="file"
onChange={handleFileUpload(false)}
/>
<UploadFileIcon fontSize="small" />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Tooltip title="Upload a text file"> <Tooltip title="Upload a text file">
<IconButton onClick={() => {}} aria-label="upload-text"> <IconButton
<UploadIcon /> 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> </IconButton>
</Tooltip> </Tooltip>
<Tooltip title="Refresh directory"> <Tooltip title="Refresh directory">
<IconButton onClick={refreshDir} aria-label="fresh"> <IconButton
<RefreshIcon /> onClick={() => refreshDir(path)}
aria-label="fresh"
size="small"
>
<RefreshIcon fontSize="small" />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</Box> </Box>
</Stack> </Stack>
<Typography>{`${path}`}</Typography> <Typography>{`Path: ${path}`}</Typography>
<List dense={true}> <List style={{ height: 480, overflowX: "auto" }}>
{nodes.map((node, index) => ( {nodes.map(({ name, isDir }, index) =>
<ListItem isDir ? (
key={index} <ListItemButton key={index} onClick={cd(name)}>
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> <ListItemIcon>
<FolderIcon /> <FolderIcon />
</ListItemIcon> </ListItemIcon>
<ListItemText primary={node} /> <ListItemText primary={name} />
</ListItemButton>
) : (
<ListItem
key={index}
secondaryAction={
<MoreButton
options={options}
onItemClick={handleItemClick(name)}
/>
}
>
<ListItemIcon>
<InsertDriveFileIcon />
</ListItemIcon>
<ListItemText primary={name} />
</ListItem> </ListItem>
))} )
)}
</List> </List>
</>
<Button onClick={loadSamples}>Load Sample Files</Button>
</Stack>
</Paper>
</Grid> </Grid>
<Grid item xs={8}> <Grid item xs={8}>
<Paper variant="outlined" style={{ padding: 8, height: "100%" }}>
<Stack spacing={1}> <Stack spacing={1}>
<Box> <Stack>
<Typography>Edit JSON below to update command:</Typography> <Typography>Edit JSON below to update command:</Typography>
<AceEditor <AceEditor
mode="json" mode="json"
@ -193,7 +308,7 @@ export default function Editor() {
onChange={(value) => setArgs(value)} onChange={(value) => setArgs(value)}
setOptions={{ tabSize: 2 }} setOptions={{ tabSize: 2 }}
/> />
</Box> </Stack>
<AceEditor <AceEditor
mode="javascript" mode="javascript"
theme={theme} theme={theme}
@ -208,22 +323,8 @@ export default function Editor() {
value={genFFmpegText(args)} value={genFFmpegText(args)}
setOptions={{ tabSize: 2 }} setOptions={{ tabSize: 2 }}
/> />
<Stack direction="row" spacing={2} className={styles.alignRight}> <Typography>Console Output:</Typography>
<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 <AceEditor
placeholder="ffmpeg console output"
mode="text" mode="text"
theme={theme} theme={theme}
name="console" name="console"
@ -238,9 +339,49 @@ export default function Editor() {
setOptions={{ tabSize: 2 }} setOptions={{ tabSize: 2 }}
onLoad={(editor) => setOutput(editor)} 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>
</Stack>
</Paper>
</Grid> </Grid>
</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> </Box>
); );
} }

View File

@ -0,0 +1,69 @@
import * as React from "react";
import IconButton from "@mui/material/IconButton";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import MoreVertIcon from "@mui/icons-material/MoreVert";
const ITEM_HEIGHT = 48;
interface Option {
key: string;
text: string;
}
interface MoreButtonProps {
options: Option[];
onItemClick: (option: string) => any;
}
export default function MoreButton({ options, onItemClick }: MoreButtonProps) {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const handleItemClick = (option: string) => () => {
onItemClick(option);
setAnchorEl(null);
};
return (
<div>
<IconButton
aria-label="more"
id="more-button"
aria-controls={open ? "menu" : undefined}
aria-expanded={open ? "true" : undefined}
aria-haspopup="true"
onClick={handleClick}
>
<MoreVertIcon />
</IconButton>
<Menu
id="menu"
MenuListProps={{
"aria-labelledby": "more-button",
}}
anchorEl={anchorEl}
open={open}
onClose={handleClose}
PaperProps={{
style: {
maxHeight: ITEM_HEIGHT * 4.5,
width: "20ch",
},
}}
>
{options.map(({ text, key }) => (
<MenuItem key={key} onClick={handleItemClick(key)}>
{text}
</MenuItem>
))}
</Menu>
</div>
);
}

View File

@ -1,9 +1,14 @@
export const CORE_VERSION = "0.12.0-alpha.2";
export const CORE_URL = `https://unpkg.com/@ffmpeg/core@${CORE_VERSION}/dist/umd/ffmpeg-core.js`;
export const CORE_MT_URL = `https://unpkg.com/@ffmpeg/core-mt@${CORE_VERSION}/dist/umd/ffmpeg-core.js`;
export const CORE_SIZE = { 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@${CORE_VERSION}/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@${CORE_VERSION}/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@${CORE_VERSION}/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@${CORE_VERSION}/dist/umd/ffmpeg-core.wasm`]: 32441947,
"https://unpkg.com/@ffmpeg/core-mt@0.12.0-alpha.2/dist/umd/ffmpeg-core.worker.js": 2978, [`https://unpkg.com/@ffmpeg/core-mt@${CORE_VERSION}/dist/umd/ffmpeg-core.worker.js`]: 2978,
}; };
export const SAMPLE_FILES = { export const SAMPLE_FILES = {

View File

@ -1,10 +1,12 @@
import * as React from "react"; import * as React from "react";
import { FFmpeg } from "@ffmpeg/ffmpeg"; import { FFmpeg } from "@ffmpeg/ffmpeg";
import Stack from "@mui/material/Stack";
import MuiThemeProvider from "@site/src/components/MuiThemeProvider"; import MuiThemeProvider from "@site/src/components/MuiThemeProvider";
import CoreSelector from "./CoreSelector";
import CoreDownloader from "./CoreDownloader"; import CoreDownloader from "./CoreDownloader";
import Editor from "./Editor"; import Editor from "./Editor";
import { getFFmpeg } from "./ffmpeg"; import { getFFmpeg } from "./ffmpeg";
import { CORE_URL, CORE_MT_URL } from "./const";
import CoreSwitcher from "./CoreSwitcher";
enum State { enum State {
NOT_LOADED, NOT_LOADED,
@ -13,12 +15,12 @@ enum State {
} }
export default function Playground() { export default function Playground() {
const { useState } = React; const { useState, useEffect } = React;
const [state, setState] = useState(State.LOADED); const [state, setState] = useState(State.LOADED);
const [option, setOption] = useState("core"); const [isCoreMT, setIsCoreMT] = useState(false);
const [url, setURL] = useState(""); const [url, setURL] = useState("");
const [received, setReceived] = useState(0); const [received, setReceived] = useState(0);
const load = async () => { const load = async (mt: boolean = false) => {
setState(State.LOADING); setState(State.LOADING);
const ffmpeg = getFFmpeg(); const ffmpeg = getFFmpeg();
ffmpeg.terminate(); ffmpeg.terminate();
@ -26,12 +28,27 @@ export default function Playground() {
setURL(_url as string); setURL(_url as string);
setReceived(_received); setReceived(_received);
}); });
await ffmpeg.load(); await ffmpeg.load({
coreURL: mt ? CORE_MT_URL : CORE_URL,
thread: mt,
});
setState(State.LOADED); setState(State.LOADED);
}; };
useEffect(() => {
load(isCoreMT);
}, []);
return ( return (
<MuiThemeProvider> <MuiThemeProvider>
<Stack spacing={4}>
<CoreSwitcher
checked={isCoreMT}
onChange={(evt) => {
setIsCoreMT(evt.target.checked);
load(evt.target.checked);
}}
/>
{(() => { {(() => {
switch (state) { switch (state) {
case State.LOADING: case State.LOADING:
@ -42,13 +59,7 @@ export default function Playground() {
return <></>; return <></>;
} }
})()} })()}
<CoreSelector </Stack>
option={option}
onChange={(event) => {
setOption((event.target as HTMLInputElement).value);
}}
onSubmit={load}
/>
</MuiThemeProvider> </MuiThemeProvider>
); );
} }

View File

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

9
apps/website/src/util.ts Normal file
View File

@ -0,0 +1,9 @@
export const downloadFile = (name: string, data: ArrayBuffer | string) => {
const a = document.createElement("a");
const blob = new Blob([data]);
const url = window.URL.createObjectURL(blob);
a.href = url;
a.download = name;
a.click();
window.URL.revokeObjectURL(url);
};

View File

@ -4,7 +4,7 @@ import {
CallbackData, CallbackData,
Callbacks, Callbacks,
DownloadProgressEvent, DownloadProgressEvent,
FFFSPaths, FSNode,
FFMessageEventCallback, FFMessageEventCallback,
FFMessageLoadConfig, FFMessageLoadConfig,
OK, OK,
@ -94,6 +94,8 @@ export class FFmpeg extends EventEmitter {
#resolves: Callbacks = {}; #resolves: Callbacks = {};
#rejects: Callbacks = {}; #rejects: Callbacks = {};
public loaded = false;
constructor() { constructor() {
super(); super();
} }
@ -108,6 +110,9 @@ export class FFmpeg extends EventEmitter {
}: FFMessageEventCallback) => { }: FFMessageEventCallback) => {
switch (type) { switch (type) {
case FFMessageType.LOAD: case FFMessageType.LOAD:
this.loaded = true;
this.#resolves[id](data);
break;
case FFMessageType.EXEC: case FFMessageType.EXEC:
case FFMessageType.WRITE_FILE: case FFMessageType.WRITE_FILE:
case FFMessageType.READ_FILE: case FFMessageType.READ_FILE:
@ -227,6 +232,7 @@ export class FFmpeg extends EventEmitter {
if (this.#worker) { if (this.#worker) {
this.#worker.terminate(); this.#worker.terminate();
this.#worker = null; this.#worker = null;
this.loaded = false;
} }
}; };
@ -323,11 +329,11 @@ export class FFmpeg extends EventEmitter {
* *
* @category File System * @category File System
*/ */
public listDir = (path: string): Promise<FFFSPaths> => public listDir = (path: string): Promise<FSNode[]> =>
this.#send({ this.#send({
type: FFMessageType.LIST_DIR, type: FFMessageType.LIST_DIR,
data: { path }, data: { path },
}) as Promise<FFFSPaths>; }) as Promise<FSNode[]>;
/** /**
* Delete an empty directory. * Delete an empty directory.

View File

@ -1,5 +1,4 @@
export type FFFSPath = string; export type FFFSPath = string;
export type FFFSPaths = FFFSPath[];
/** /**
* ffmpeg-core loading configuration. * ffmpeg-core loading configuration.
@ -136,6 +135,11 @@ export type FileData = Uint8Array | string;
export type IsFirst = boolean; export type IsFirst = boolean;
export type OK = boolean; export type OK = boolean;
export interface FSNode {
name: string;
isDir: boolean;
}
export type CallbackData = export type CallbackData =
| FileData | FileData
| ExitCode | ExitCode
@ -146,7 +150,7 @@ export type CallbackData =
| IsFirst | IsFirst
| OK | OK
| Error | Error
| FFFSPaths | FSNode[]
| undefined; | undefined;
export interface Callbacks { export interface Callbacks {

View File

@ -18,7 +18,7 @@ import type {
IsFirst, IsFirst,
OK, OK,
ExitCode, ExitCode,
FFFSPaths, FSNode,
FileData, FileData,
} from "./types"; } from "./types";
import { toBlobURL } from "./utils"; import { toBlobURL } from "./utils";
@ -118,8 +118,15 @@ const createDir = ({ path }: FFMessageCreateDirData): OK => {
return true; return true;
}; };
const listDir = ({ path }: FFMessageListDirData): FFFSPaths => { const listDir = ({ path }: FFMessageListDirData): FSNode[] => {
return ffmpeg.FS.readdir(path); const names = ffmpeg.FS.readdir(path);
const nodes: FSNode[] = [];
for (const name of names) {
const stat = ffmpeg.FS.stat(`${path}/${name}`);
const isDir = ffmpeg.FS.isDir(stat.mode);
nodes.push({ name, isDir });
}
return nodes;
}; };
// TODO: check if deletion works. // TODO: check if deletion works.