Refactor playground page and init docs

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

View File

@ -1,12 +0,0 @@
---
slug: first-blog-post
title: First Blog Post
authors:
name: Gao Wei
title: Docusaurus Core Team
url: https://github.com/wgao19
image_url: https://github.com/wgao19.png
tags: [hola, docusaurus]
---
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

View File

@ -1,44 +0,0 @@
---
slug: long-blog-post
title: Long Blog Post
authors: endi
tags: [hello, docusaurus]
---
This is the summary of a very long blog post,
Use a `<!--` `truncate` `-->` comment to limit blog post size in the list view.
<!--truncate-->
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

View File

@ -1,20 +0,0 @@
---
slug: mdx-blog-post
title: MDX Blog Post
authors: [slorber]
tags: [docusaurus]
---
Blog posts support [Docusaurus Markdown features](https://docusaurus.io/docs/markdown-features), such as [MDX](https://mdxjs.com/).
:::tip
Use the power of React to create interactive blog posts.
```js
<button onClick={() => alert('button clicked!')}>Click me!</button>
```
<button onClick={() => alert('button clicked!')}>Click me!</button>
:::

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

View File

@ -1,25 +0,0 @@
---
slug: welcome
title: Welcome
authors: [slorber, yangshun]
tags: [facebook, hello, docusaurus]
---
[Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog).
Simply add Markdown files (or folders) to the `blog` directory.
Regular blog authors can be added to `authors.yml`.
The blog post date can be extracted from filenames, such as:
- `2019-05-30-welcome.md`
- `2019-05-30-welcome/index.md`
A blog post folder can be convenient to co-locate blog post images:
![Docusaurus Plushie](./docusaurus-plushie-banner.jpeg)
The blog supports tags as well!
**And if you don't want a blog**: just delete this directory, and use `blog: false` in your Docusaurus config.

View File

@ -0,0 +1,10 @@
---
slug: release-ffmpeg.wasm-0.12.0
title: Release ffmpeg.wasm 0.12.0
authors: [jeromewu]
tags: [ffmpeg.wasm]
---
See what are the differences.
<!--truncate-->

View File

@ -1,17 +1,5 @@
endi:
name: Endilie Yacop Sucipto
title: Maintainer of Docusaurus
url: https://github.com/endiliey
image_url: https://github.com/endiliey.png
yangshun:
name: Yangshun Tay
title: Front End Engineer @ Facebook
url: https://github.com/yangshun
image_url: https://github.com/yangshun.png
slorber:
name: Sébastien Lorber
title: Docusaurus maintainer
url: https://sebastienlorber.com
image_url: https://github.com/slorber.png
jeromewu:
name: Jerome Wu
title: Maintainer of ffmpeg.wasm
url: https://github.com/jeromewu
image_url: https://github.com/jeromewu.png

View File

@ -0,0 +1 @@
# @ffmpeg/core

View File

@ -0,0 +1 @@
# @ffmpeg/ffmpeg

View File

@ -0,0 +1 @@
# @ffmpeg/util

1
apps/website/docs/faq.md Normal file
View File

@ -0,0 +1 @@
# FAQ

View File

@ -0,0 +1 @@
# Configuration

View File

@ -0,0 +1 @@
# Installation

View File

@ -0,0 +1 @@
# Library Versions

View File

@ -0,0 +1 @@
# Use Mutlithreading

View File

@ -1,8 +1,4 @@
---
sidebar_position: 1
---
# Tutorial Intro
# Introduction
Let's discover **Docusaurus in less than 5 minutes**.

View File

@ -0,0 +1 @@
# Migrating from 0.11.x to 0.12+

View File

@ -1,8 +0,0 @@
{
"label": "Tutorial - Basics",
"position": 2,
"link": {
"type": "generated-index",
"description": "5 minutes to learn the most important Docusaurus concepts."
}
}

View File

@ -1,23 +0,0 @@
---
sidebar_position: 6
---
# Congratulations!
You have just learned the **basics of Docusaurus** and made some changes to the **initial template**.
Docusaurus has **much more to offer**!
Have **5 more minutes**? Take a look at **[versioning](../tutorial-extras/manage-docs-versions.md)** and **[i18n](../tutorial-extras/translate-your-site.md)**.
Anything **unclear** or **buggy** in this tutorial? [Please report it!](https://github.com/facebook/docusaurus/discussions/4610)
## What's next?
- Read the [official documentation](https://docusaurus.io/)
- Modify your site configuration with [`docusaurus.config.js`](https://docusaurus.io/docs/api/docusaurus-config)
- Add navbar and footer items with [`themeConfig`](https://docusaurus.io/docs/api/themes/configuration)
- Add a custom [Design and Layout](https://docusaurus.io/docs/styling-layout)
- Add a [search bar](https://docusaurus.io/docs/search)
- Find inspirations in the [Docusaurus showcase](https://docusaurus.io/showcase)
- Get involved in the [Docusaurus Community](https://docusaurus.io/community/support)

View File

@ -1,34 +0,0 @@
---
sidebar_position: 3
---
# Create a Blog Post
Docusaurus creates a **page for each blog post**, but also a **blog index page**, a **tag system**, an **RSS** feed...
## Create your first Post
Create a file at `blog/2021-02-28-greetings.md`:
```md title="blog/2021-02-28-greetings.md"
---
slug: greetings
title: Greetings!
authors:
- name: Joel Marcey
title: Co-creator of Docusaurus 1
url: https://github.com/JoelMarcey
image_url: https://github.com/JoelMarcey.png
- name: Sébastien Lorber
title: Docusaurus maintainer
url: https://sebastienlorber.com
image_url: https://github.com/slorber.png
tags: [greetings]
---
Congratulations, you have made your first post!
Feel free to play around and edit this post as much you like.
```
A new blog post is now available at [http://localhost:3000/blog/greetings](http://localhost:3000/blog/greetings).

View File

@ -1,57 +0,0 @@
---
sidebar_position: 2
---
# Create a Document
Documents are **groups of pages** connected through:
- a **sidebar**
- **previous/next navigation**
- **versioning**
## Create your first Doc
Create a Markdown file at `docs/hello.md`:
```md title="docs/hello.md"
# Hello
This is my **first Docusaurus document**!
```
A new document is now available at [http://localhost:3000/docs/hello](http://localhost:3000/docs/hello).
## Configure the Sidebar
Docusaurus automatically **creates a sidebar** from the `docs` folder.
Add metadata to customize the sidebar label and position:
```md title="docs/hello.md" {1-4}
---
sidebar_label: 'Hi!'
sidebar_position: 3
---
# Hello
This is my **first Docusaurus document**!
```
It is also possible to create your sidebar explicitly in `sidebars.js`:
```js title="sidebars.js"
module.exports = {
tutorialSidebar: [
'intro',
// highlight-next-line
'hello',
{
type: 'category',
label: 'Tutorial',
items: ['tutorial-basics/create-a-document'],
},
],
};
```

View File

@ -1,43 +0,0 @@
---
sidebar_position: 1
---
# Create a Page
Add **Markdown or React** files to `src/pages` to create a **standalone page**:
- `src/pages/index.js``localhost:3000/`
- `src/pages/foo.md``localhost:3000/foo`
- `src/pages/foo/bar.js``localhost:3000/foo/bar`
## Create your first React Page
Create a file at `src/pages/my-react-page.js`:
```jsx title="src/pages/my-react-page.js"
import React from 'react';
import Layout from '@theme/Layout';
export default function MyReactPage() {
return (
<Layout>
<h1>My React page</h1>
<p>This is a React page</p>
</Layout>
);
}
```
A new page is now available at [http://localhost:3000/my-react-page](http://localhost:3000/my-react-page).
## Create your first Markdown Page
Create a file at `src/pages/my-markdown-page.md`:
```mdx title="src/pages/my-markdown-page.md"
# My Markdown page
This is a Markdown page
```
A new page is now available at [http://localhost:3000/my-markdown-page](http://localhost:3000/my-markdown-page).

View File

@ -1,31 +0,0 @@
---
sidebar_position: 5
---
# Deploy your site
Docusaurus is a **static-site-generator** (also called **[Jamstack](https://jamstack.org/)**).
It builds your site as simple **static HTML, JavaScript and CSS files**.
## Build your site
Build your site **for production**:
```bash
npm run build
```
The static files are generated in the `build` folder.
## Deploy your site
Test your production build locally:
```bash
npm run serve
```
The `build` folder is now served at [http://localhost:3000/](http://localhost:3000/).
You can now deploy the `build` folder **almost anywhere** easily, **for free** or very small cost (read the **[Deployment Guide](https://docusaurus.io/docs/deployment)**).

View File

@ -1,146 +0,0 @@
---
sidebar_position: 4
---
# Markdown Features
Docusaurus supports **[Markdown](https://daringfireball.net/projects/markdown/syntax)** and a few **additional features**.
## Front Matter
Markdown documents have metadata at the top called [Front Matter](https://jekyllrb.com/docs/front-matter/):
```text title="my-doc.md"
// highlight-start
---
id: my-doc-id
title: My document title
description: My document description
slug: /my-custom-url
---
// highlight-end
## Markdown heading
Markdown text with [links](./hello.md)
```
## Links
Regular Markdown links are supported, using url paths or relative file paths.
```md
Let's see how to [Create a page](/create-a-page).
```
```md
Let's see how to [Create a page](./create-a-page.md).
```
**Result:** Let's see how to [Create a page](./create-a-page.md).
## Images
Regular Markdown images are supported.
<!-- You can use absolute paths to reference images in the static directory (`static/img/docusaurus.png`): -->
<!-- ```md -->
<!-- ![Docusaurus logo](/img/docusaurus.png) -->
<!-- ``` -->
<!-- ![Docusaurus logo](/img/docusaurus.png) -->
You can reference images relative to the current file as well, as shown in [the extra guides](../tutorial-extras/manage-docs-versions.md).
## Code Blocks
Markdown code blocks are supported with Syntax highlighting.
```jsx title="src/components/HelloDocusaurus.js"
function HelloDocusaurus() {
return (
<h1>Hello, Docusaurus!</h1>
)
}
```
```jsx title="src/components/HelloDocusaurus.js"
function HelloDocusaurus() {
return <h1>Hello, Docusaurus!</h1>;
}
```
## Admonitions
Docusaurus has a special syntax to create admonitions and callouts:
:::tip My tip
Use this awesome feature option
:::
:::danger Take care
This action is dangerous
:::
:::tip My tip
Use this awesome feature option
:::
:::danger Take care
This action is dangerous
:::
## MDX and React Components
[MDX](https://mdxjs.com/) can make your documentation more **interactive** and allows using any **React components inside Markdown**:
```jsx
export const Highlight = ({children, color}) => (
<span
style={{
backgroundColor: color,
borderRadius: '20px',
color: '#fff',
padding: '10px',
cursor: 'pointer',
}}
onClick={() => {
alert(`You clicked the color ${color} with label ${children}`)
}}>
{children}
</span>
);
This is <Highlight color="#25c2a0">Docusaurus green</Highlight> !
This is <Highlight color="#1877F2">Facebook blue</Highlight> !
```
export const Highlight = ({children, color}) => (
<span
style={{
backgroundColor: color,
borderRadius: '20px',
color: '#fff',
padding: '10px',
cursor: 'pointer',
}}
onClick={() => {
alert(`You clicked the color ${color} with label ${children}`);
}}>
{children}
</span>
);
This is <Highlight color="#25c2a0">Docusaurus green</Highlight> !
This is <Highlight color="#1877F2">Facebook blue</Highlight> !

View File

@ -1,7 +0,0 @@
{
"label": "Tutorial - Extras",
"position": 3,
"link": {
"type": "generated-index"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

View File

@ -1,55 +0,0 @@
---
sidebar_position: 1
---
# Manage Docs Versions
Docusaurus can manage multiple versions of your docs.
## Create a docs version
Release a version 1.0 of your project:
```bash
npm run docusaurus docs:version 1.0
```
The `docs` folder is copied into `versioned_docs/version-1.0` and `versions.json` is created.
Your docs now have 2 versions:
- `1.0` at `http://localhost:3000/docs/` for the version 1.0 docs
- `current` at `http://localhost:3000/docs/next/` for the **upcoming, unreleased docs**
## Add a Version Dropdown
To navigate seamlessly across versions, add a version dropdown.
Modify the `docusaurus.config.js` file:
```js title="docusaurus.config.js"
module.exports = {
themeConfig: {
navbar: {
items: [
// highlight-start
{
type: 'docsVersionDropdown',
},
// highlight-end
],
},
},
};
```
The docs version dropdown appears in your navbar:
![Docs Version Dropdown](./img/docsVersionDropdown.png)
## Update an existing version
It is possible to edit versioned docs in their respective folder:
- `versioned_docs/version-1.0/hello.md` updates `http://localhost:3000/docs/hello`
- `docs/hello.md` updates `http://localhost:3000/docs/next/hello`

View File

@ -1,88 +0,0 @@
---
sidebar_position: 2
---
# Translate your site
Let's translate `docs/intro.md` to French.
## Configure i18n
Modify `docusaurus.config.js` to add support for the `fr` locale:
```js title="docusaurus.config.js"
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr'],
},
};
```
## Translate a doc
Copy the `docs/intro.md` file to the `i18n/fr` folder:
```bash
mkdir -p i18n/fr/docusaurus-plugin-content-docs/current/
cp docs/intro.md i18n/fr/docusaurus-plugin-content-docs/current/intro.md
```
Translate `i18n/fr/docusaurus-plugin-content-docs/current/intro.md` in French.
## Start your localized site
Start your site on the French locale:
```bash
npm run start -- --locale fr
```
Your localized site is accessible at [http://localhost:3000/fr/](http://localhost:3000/fr/) and the `Getting Started` page is translated.
:::caution
In development, you can only use one locale at a same time.
:::
## Add a Locale Dropdown
To navigate seamlessly across languages, add a locale dropdown.
Modify the `docusaurus.config.js` file:
```js title="docusaurus.config.js"
module.exports = {
themeConfig: {
navbar: {
items: [
// highlight-start
{
type: 'localeDropdown',
},
// highlight-end
],
},
},
};
```
The locale dropdown now appears in your navbar:
![Locale Dropdown](./img/localeDropdown.png)
## Build your localized site
Build your site for a specific locale:
```bash
npm run build -- --locale fr
```
Or build your site to include all the locales at once:
```bash
npm run build
```

View File

@ -76,8 +76,8 @@ const config = {
position: "left",
label: "Docs",
},
{ to: "/blog", label: "Blog", position: "left" },
{ to: "/playground", label: "Playground", position: "left" },
{ to: "/blog", label: "Blog", position: "left" },
{
href: "https://github.com/ffmpegwasm/ffmpeg.wasm",
label: "GitHub",

View File

@ -19,11 +19,28 @@ const sidebars = {
// But you can create a sidebar manually
tutorialSidebar: [
"intro",
{
type: "category",
label: "Getting Started",
items: [
"getting-started/installation",
"getting-started/configuration",
"getting-started/multi-thread",
"getting-started/lib-versions",
],
},
"migration",
"faq",
{
type: "category",
label: "API",
items: ["api/classes/FFmpeg"],
},
{
type: "category",
label: "Contribution",
items: ["contribution/core", "contribution/ffmpeg", "contribution/util"],
},
],
};

View File

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

View File

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

View File

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

View File

@ -0,0 +1,121 @@
/// <reference types="ace" />
import React, { useEffect, useState } from "react";
import AceEditor from "react-ace";
import Paper from "@mui/material/Paper";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import LinearProgressWithLabel from "@site/src/components/common/LinearProgressWithLabel";
import { useColorMode } from "@docusaurus/theme-common";
import "ace-builds/src-noconflict/mode-json";
import "ace-builds/src-noconflict/mode-javascript";
import "ace-builds/src-noconflict/mode-text";
import "ace-builds/src-noconflict/theme-dracula";
import "ace-builds/src-noconflict/theme-github";
const genFFmpegText = (args: string) => {
let data: any = [];
try {
data = JSON.parse(args);
} catch (e) {}
return `// equivalent ffmpeg.wasm API call
ffmpeg.exec(${JSON.stringify(data)});
// equivalent ffmpeg command line
ffmpeg ${data.join(" ")}`;
};
interface EditorProps {
args: string;
logs: string[];
progress: number;
time: number;
onArgsUpdate: (args: string) => void;
onExec: () => Promise<void>;
}
export default function Editor({
args = "",
logs = [],
progress = 0,
time = 0,
onArgsUpdate,
onExec,
}: EditorProps) {
const { colorMode } = useColorMode();
const [output, setOutput] = useState<Ace.Editor>();
useEffect(() => {
// scroll logs to the end.
output && output.renderer.scrollToLine(Number.POSITIVE_INFINITY);
}, [logs]);
const theme = colorMode === "dark" ? "github" : "dracula";
return (
<Paper variant="outlined" style={{ padding: 8, height: "100%" }}>
<Stack spacing={1}>
<Stack>
<Typography>Editor:</Typography>
<Typography>Edit arguments below to update command:</Typography>
<AceEditor
mode="json"
theme={theme}
name="input-args"
fontSize={16}
showPrintMargin={true}
showGutter={true}
width="100%"
minLines={8}
maxLines={8}
highlightActiveLine={true}
value={args}
onChange={onArgsUpdate}
setOptions={{ tabSize: 2 }}
/>
</Stack>
<AceEditor
mode="javascript"
theme={theme}
name="ffmpeg.wasm"
fontSize={16}
showGutter={false}
width="100%"
minLines={6}
maxLines={6}
readOnly
highlightActiveLine={false}
value={genFFmpegText(args)}
setOptions={{ tabSize: 2 }}
/>
<Typography>Console Output:</Typography>
<AceEditor
mode="text"
theme={theme}
name="console"
fontSize={16}
width="100%"
minLines={8}
maxLines={8}
readOnly
showPrintMargin={true}
highlightActiveLine={false}
value={logs.join("\n")}
setOptions={{ tabSize: 2 }}
onLoad={(editor) => setOutput(editor)}
/>
<Typography>Transcoding Progress:</Typography>
<LinearProgressWithLabel value={progress} />
<Stack direction="row" spacing={2} justifyContent="space-between">
<Typography>
{time === 0 ? "" : `Time Elapsed: ${(time / 1000).toFixed(2)} s`}
</Typography>
<Button variant="contained" onClick={onExec}>
Run
</Button>
</Stack>
</Stack>
</Paper>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.