Complete Playground v1
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
34
apps/website/src/components/Playground/CoreSwitcher.tsx
Normal file
34
apps/website/src/components/Playground/CoreSwitcher.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -12,29 +12,47 @@ import ListItem from "@mui/material/ListItem";
|
||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||
import UploadFileIcon from "@mui/icons-material/UploadFile";
|
||||
import UploadIcon from "@mui/icons-material/Upload";
|
||||
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
|
||||
import CreateNewFolderIcon from "@mui/icons-material/CreateNewFolder";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
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 styles from "./styles.module.css";
|
||||
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 = ["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) => {
|
||||
let data: any = [];
|
||||
@@ -54,33 +72,42 @@ export default function Editor() {
|
||||
const [logs, setLogs] = useState<string[]>([]);
|
||||
const [output, setOutput] = useState<Ace.Editor>();
|
||||
const [path, setPath] = useState<string>("/");
|
||||
const [nodes, setNodes] = useState<string[]>([]);
|
||||
const [progress, setProgress] = useState<number>(-1);
|
||||
const [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 loadSamples = async () => {
|
||||
const refreshDir = async (curPath: string) => {
|
||||
const ffmpeg = getFFmpeg();
|
||||
Object.keys(SAMPLE_FILES).forEach(async (name) => {
|
||||
await ffmpeg.writeFile(name, await fetchFile(SAMPLE_FILES[name]));
|
||||
});
|
||||
// Somehow we need to wait a little bit before reading the new nodes.
|
||||
setTimeout(async () => {
|
||||
setNodes(await ffmpeg.listDir(path));
|
||||
}, 500);
|
||||
if (ffmpeg.loaded) {
|
||||
setNodes(
|
||||
(await ffmpeg.listDir(curPath)).filter(({ name }) => name !== ".")
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshDir = async () => {
|
||||
setNodes(await getFFmpeg().listDir(path));
|
||||
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(-1);
|
||||
setProgress(0);
|
||||
setTime(0);
|
||||
const logListener = ({ message }) => {
|
||||
setLogs((_logs) => [..._logs, message]);
|
||||
scrollToEnd();
|
||||
@@ -90,157 +117,271 @@ export default function Editor() {
|
||||
};
|
||||
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);
|
||||
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(() => {
|
||||
const ffmpeg = getFFmpeg();
|
||||
ffmpeg.listDir(path).then((nodes) => {
|
||||
setNodes(nodes);
|
||||
});
|
||||
refreshDir(path);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Grid
|
||||
container
|
||||
spacing={{ xs: 2, md: 3 }}
|
||||
columns={{ xs: 4, sm: 8, md: 12 }}
|
||||
>
|
||||
<Grid container spacing={{ xs: 1 }} columns={{ xs: 4, md: 12 }}>
|
||||
<Grid item xs={4}>
|
||||
<Stack direction="row" className={styles.fsTitle}>
|
||||
<Typography>File System:</Typography>
|
||||
<Box>
|
||||
<Tooltip title="Upload a media file">
|
||||
<IconButton onClick={() => {}} aria-label="upload-media-file">
|
||||
<UploadFileIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Upload a text file">
|
||||
<IconButton onClick={() => {}} aria-label="upload-text">
|
||||
<UploadIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Refresh directory">
|
||||
<IconButton onClick={refreshDir} aria-label="fresh">
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Typography>{`${path}`}</Typography>
|
||||
<List dense={true}>
|
||||
{nodes.map((node, index) => (
|
||||
<ListItem
|
||||
key={index}
|
||||
secondaryAction={
|
||||
<>
|
||||
<IconButton
|
||||
aria-label="more"
|
||||
id="long-button"
|
||||
aria-haspopup="true"
|
||||
edge="end"
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="long-menu"
|
||||
MenuListProps={{
|
||||
"aria-labelledby": "long-button",
|
||||
}}
|
||||
PaperProps={{
|
||||
style: {
|
||||
width: "20ch",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<MenuItem key={option} selected={option === "Download"}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<FolderIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={node} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
<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}>
|
||||
<Stack spacing={1}>
|
||||
<Box>
|
||||
<Typography>Edit JSON below to update command:</Typography>
|
||||
<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="json"
|
||||
mode="javascript"
|
||||
theme={theme}
|
||||
name="input-args"
|
||||
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}
|
||||
showPrintMargin={true}
|
||||
showGutter={true}
|
||||
width="100%"
|
||||
minLines={8}
|
||||
maxLines={8}
|
||||
highlightActiveLine={true}
|
||||
value={args}
|
||||
onChange={(value) => setArgs(value)}
|
||||
readOnly
|
||||
showPrintMargin={true}
|
||||
highlightActiveLine={false}
|
||||
value={logs.join("\n")}
|
||||
setOptions={{ tabSize: 2 }}
|
||||
onLoad={(editor) => setOutput(editor)}
|
||||
/>
|
||||
</Box>
|
||||
<AceEditor
|
||||
mode="javascript"
|
||||
theme={theme}
|
||||
name="ffmpeg.wasm"
|
||||
fontSize={16}
|
||||
showGutter={false}
|
||||
width="100%"
|
||||
minLines={6}
|
||||
maxLines={6}
|
||||
readOnly
|
||||
highlightActiveLine={false}
|
||||
value={genFFmpegText(args)}
|
||||
setOptions={{ tabSize: 2 }}
|
||||
/>
|
||||
<Stack direction="row" spacing={2} className={styles.alignRight}>
|
||||
<Button onClick={loadSamples}>Load Sample Files</Button>
|
||||
<Button variant="contained" onClick={exec}>
|
||||
Run
|
||||
</Button>
|
||||
<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>
|
||||
{progress === -1 ? (
|
||||
<></>
|
||||
) : (
|
||||
<>
|
||||
<Typography>Transcoding Progress:</Typography>
|
||||
<LinearProgressWithLabel value={progress} />
|
||||
</>
|
||||
)}
|
||||
<AceEditor
|
||||
placeholder="ffmpeg console output"
|
||||
mode="text"
|
||||
theme={theme}
|
||||
name="console"
|
||||
fontSize={16}
|
||||
width="100%"
|
||||
minLines={8}
|
||||
maxLines={8}
|
||||
readOnly
|
||||
showPrintMargin={true}
|
||||
highlightActiveLine={false}
|
||||
value={logs.join("\n")}
|
||||
setOptions={{ tabSize: 2 }}
|
||||
onLoad={(editor) => setOutput(editor)}
|
||||
/>
|
||||
</Stack>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
69
apps/website/src/components/Playground/MoreButton.tsx
Normal file
69
apps/website/src/components/Playground/MoreButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 = {
|
||||
"https://unpkg.com/@ffmpeg/core@0.12.0-alpha.2/dist/umd/ffmpeg-core.js": 111646,
|
||||
"https://unpkg.com/@ffmpeg/core@0.12.0-alpha.2/dist/umd/ffmpeg-core.wasm": 31967534,
|
||||
"https://unpkg.com/@ffmpeg/core-mt@0.12.0-alpha.2/dist/umd/ffmpeg-core.js": 130002,
|
||||
"https://unpkg.com/@ffmpeg/core-mt@0.12.0-alpha.2/dist/umd/ffmpeg-core.wasm": 32441947,
|
||||
"https://unpkg.com/@ffmpeg/core-mt@0.12.0-alpha.2/dist/umd/ffmpeg-core.worker.js": 2978,
|
||||
[`https://unpkg.com/@ffmpeg/core@${CORE_VERSION}/dist/umd/ffmpeg-core.js`]: 111646,
|
||||
[`https://unpkg.com/@ffmpeg/core@${CORE_VERSION}/dist/umd/ffmpeg-core.wasm`]: 31967534,
|
||||
[`https://unpkg.com/@ffmpeg/core-mt@${CORE_VERSION}/dist/umd/ffmpeg-core.js`]: 130002,
|
||||
[`https://unpkg.com/@ffmpeg/core-mt@${CORE_VERSION}/dist/umd/ffmpeg-core.wasm`]: 32441947,
|
||||
[`https://unpkg.com/@ffmpeg/core-mt@${CORE_VERSION}/dist/umd/ffmpeg-core.worker.js`]: 2978,
|
||||
};
|
||||
|
||||
export const SAMPLE_FILES = {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import * as React from "react";
|
||||
import { FFmpeg } from "@ffmpeg/ffmpeg";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import MuiThemeProvider from "@site/src/components/MuiThemeProvider";
|
||||
import CoreSelector from "./CoreSelector";
|
||||
import CoreDownloader from "./CoreDownloader";
|
||||
import Editor from "./Editor";
|
||||
import { getFFmpeg } from "./ffmpeg";
|
||||
import { CORE_URL, CORE_MT_URL } from "./const";
|
||||
import CoreSwitcher from "./CoreSwitcher";
|
||||
|
||||
enum State {
|
||||
NOT_LOADED,
|
||||
@@ -13,12 +15,12 @@ enum State {
|
||||
}
|
||||
|
||||
export default function Playground() {
|
||||
const { useState } = React;
|
||||
const { useState, useEffect } = React;
|
||||
const [state, setState] = useState(State.LOADED);
|
||||
const [option, setOption] = useState("core");
|
||||
const [isCoreMT, setIsCoreMT] = useState(false);
|
||||
const [url, setURL] = useState("");
|
||||
const [received, setReceived] = useState(0);
|
||||
const load = async () => {
|
||||
const load = async (mt: boolean = false) => {
|
||||
setState(State.LOADING);
|
||||
const ffmpeg = getFFmpeg();
|
||||
ffmpeg.terminate();
|
||||
@@ -26,29 +28,38 @@ export default function Playground() {
|
||||
setURL(_url as string);
|
||||
setReceived(_received);
|
||||
});
|
||||
await ffmpeg.load();
|
||||
await ffmpeg.load({
|
||||
coreURL: mt ? CORE_MT_URL : CORE_URL,
|
||||
thread: mt,
|
||||
});
|
||||
setState(State.LOADED);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
load(isCoreMT);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MuiThemeProvider>
|
||||
{(() => {
|
||||
switch (state) {
|
||||
case State.LOADING:
|
||||
return <CoreDownloader url={url} received={received} />;
|
||||
case State.LOADED:
|
||||
return <Editor />;
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
})()}
|
||||
<CoreSelector
|
||||
option={option}
|
||||
onChange={(event) => {
|
||||
setOption((event.target as HTMLInputElement).value);
|
||||
}}
|
||||
onSubmit={load}
|
||||
/>
|
||||
<Stack spacing={4}>
|
||||
<CoreSwitcher
|
||||
checked={isCoreMT}
|
||||
onChange={(evt) => {
|
||||
setIsCoreMT(evt.target.checked);
|
||||
load(evt.target.checked);
|
||||
}}
|
||||
/>
|
||||
{(() => {
|
||||
switch (state) {
|
||||
case State.LOADING:
|
||||
return <CoreDownloader url={url} received={received} />;
|
||||
case State.LOADED:
|
||||
return <Editor />;
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
})()}
|
||||
</Stack>
|
||||
</MuiThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
9
apps/website/src/util.ts
Normal 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);
|
||||
};
|
||||
Reference in New Issue
Block a user