Refactor playground page and init docs
This commit is contained in:
parent
9cd1f00ec7
commit
3f79cb5e81
@ -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
|
@ -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
|
@ -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 |
@ -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:
|
||||
|
||||

|
||||
|
||||
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.
|
10
apps/website/blog/2022-10-8-release-ffmpeg.wasm-0.12.0.mdx
Normal file
10
apps/website/blog/2022-10-8-release-ffmpeg.wasm-0.12.0.mdx
Normal 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-->
|
@ -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
|
||||
|
1
apps/website/docs/contribution/core.md
Normal file
1
apps/website/docs/contribution/core.md
Normal file
@ -0,0 +1 @@
|
||||
# @ffmpeg/core
|
1
apps/website/docs/contribution/ffmpeg.md
Normal file
1
apps/website/docs/contribution/ffmpeg.md
Normal file
@ -0,0 +1 @@
|
||||
# @ffmpeg/ffmpeg
|
1
apps/website/docs/contribution/util.md
Normal file
1
apps/website/docs/contribution/util.md
Normal file
@ -0,0 +1 @@
|
||||
# @ffmpeg/util
|
1
apps/website/docs/faq.md
Normal file
1
apps/website/docs/faq.md
Normal file
@ -0,0 +1 @@
|
||||
# FAQ
|
1
apps/website/docs/getting-started/configuration.md
Normal file
1
apps/website/docs/getting-started/configuration.md
Normal file
@ -0,0 +1 @@
|
||||
# Configuration
|
1
apps/website/docs/getting-started/installation.md
Normal file
1
apps/website/docs/getting-started/installation.md
Normal file
@ -0,0 +1 @@
|
||||
# Installation
|
1
apps/website/docs/getting-started/lib-versions.md
Normal file
1
apps/website/docs/getting-started/lib-versions.md
Normal file
@ -0,0 +1 @@
|
||||
# Library Versions
|
1
apps/website/docs/getting-started/multi-thread.md
Normal file
1
apps/website/docs/getting-started/multi-thread.md
Normal file
@ -0,0 +1 @@
|
||||
# Use Mutlithreading
|
@ -1,8 +1,4 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Tutorial Intro
|
||||
# Introduction
|
||||
|
||||
Let's discover **Docusaurus in less than 5 minutes**.
|
||||
|
||||
|
1
apps/website/docs/migration.md
Normal file
1
apps/website/docs/migration.md
Normal file
@ -0,0 +1 @@
|
||||
# Migrating from 0.11.x to 0.12+
|
@ -1,8 +0,0 @@
|
||||
{
|
||||
"label": "Tutorial - Basics",
|
||||
"position": 2,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "5 minutes to learn the most important Docusaurus concepts."
|
||||
}
|
||||
}
|
@ -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)
|
@ -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).
|
@ -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'],
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
@ -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).
|
@ -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)**).
|
@ -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 -->
|
||||
<!--  -->
|
||||
<!-- ``` -->
|
||||
|
||||
<!--  -->
|
||||
|
||||
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> !
|
@ -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 |
@ -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:
|
||||
|
||||

|
||||
|
||||
## 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`
|
@ -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:
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
```
|
@ -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",
|
||||
|
@ -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"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as React from "react";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Container from "@mui/material/Container";
|
||||
import LinearProgressWithLabel from "./LinearProgressWithLabel";
|
||||
import LinearProgressWithLabel from "@site/src/components/common/LinearProgressWithLabel";
|
||||
import { CORE_SIZE } from "./const";
|
||||
|
||||
export default function CoreDownloader({ url, received }) {
|
||||
|
@ -19,12 +19,18 @@ export default function CoreSwitcher({ checked, onChange }: CoreSwitcherProps) {
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
control={<Switch checked={checked} onChange={onChange} />}
|
||||
label="Use Multi-thread"
|
||||
label="Use Multithreading"
|
||||
disabled={typeof SharedArrayBuffer !== "function"}
|
||||
/>
|
||||
</FormGroup>
|
||||
<Tooltip title="Multi-threaded core is faster, but unstable and not supported by all browsers. Click here for more details.">
|
||||
<IconButton aria-label="help" size="small">
|
||||
<IconButton
|
||||
aria-label="help"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
location.href = "/docs/getting-started/multi-thread";
|
||||
}}
|
||||
>
|
||||
<HelpIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
@ -1,387 +0,0 @@
|
||||
/// <reference types="ace" />
|
||||
|
||||
import * as React from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Button from "@mui/material/Button";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import FolderIcon from "@mui/icons-material/Folder";
|
||||
import List from "@mui/material/List";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||
import UploadFileIcon from "@mui/icons-material/UploadFile";
|
||||
import UploadIcon from "@mui/icons-material/Upload";
|
||||
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
|
||||
import CreateNewFolderIcon from "@mui/icons-material/CreateNewFolder";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import { useColorMode } from "@docusaurus/theme-common";
|
||||
import { FFmpeg } from "@ffmpeg/ffmpeg";
|
||||
import { fetchFile } from "@ffmpeg/util";
|
||||
import { downloadFile } from "@site/src/util";
|
||||
import AceEditor from "react-ace";
|
||||
import { getFFmpeg } from "./ffmpeg";
|
||||
import { SAMPLE_FILES } from "./const";
|
||||
import LinearProgressWithLabel from "./LinearProgressWithLabel";
|
||||
import MoreButton from "./MoreButton";
|
||||
import "ace-builds/src-noconflict/mode-json";
|
||||
import "ace-builds/src-noconflict/mode-javascript";
|
||||
import "ace-builds/src-noconflict/mode-text";
|
||||
import "ace-builds/src-noconflict/theme-dracula";
|
||||
import "ace-builds/src-noconflict/theme-github";
|
||||
import ListItemButton from "@mui/material/ListItemButton";
|
||||
import Modal from "@mui/material/Modal";
|
||||
import TextField from "@mui/material/TextField";
|
||||
|
||||
const defaultArgs = JSON.stringify(["-i", "video.avi", "video.mp4"], null, 2);
|
||||
const options = [
|
||||
{ text: "Download", key: "download" },
|
||||
{ text: "Download as Text File", key: "download-text" },
|
||||
{ text: "Delete", key: "delete" },
|
||||
];
|
||||
|
||||
const modalStyle = {
|
||||
position: "absolute" as "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 400,
|
||||
bgcolor: "background.paper",
|
||||
p: 4,
|
||||
};
|
||||
|
||||
const genFFmpegText = (args: string) => {
|
||||
let data: any = [];
|
||||
try {
|
||||
data = JSON.parse(args);
|
||||
} catch (e) {}
|
||||
return `// equivalent ffmpeg.wasm API call
|
||||
ffmpeg.exec(${JSON.stringify(data)});
|
||||
|
||||
// equivalent ffmpeg command line
|
||||
ffmpeg ${data.join(" ")}`;
|
||||
};
|
||||
|
||||
export default function Editor() {
|
||||
const { useState, useEffect } = React;
|
||||
const [args, setArgs] = useState<string>(defaultArgs);
|
||||
const [logs, setLogs] = useState<string[]>([]);
|
||||
const [output, setOutput] = useState<Ace.Editor>();
|
||||
const [path, setPath] = useState<string>("/");
|
||||
const [nodes, setNodes] = useState([]);
|
||||
const [progress, setProgress] = useState<number>(0);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [time, setTime] = useState(0);
|
||||
const [folderName, setFolderName] = useState("");
|
||||
const handleModalOpen = () => setOpen(true);
|
||||
const handleModalClose = () => setOpen(false);
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
const theme = colorMode === "dark" ? "github" : "dracula";
|
||||
|
||||
const scrollToEnd = () => {
|
||||
output && output.renderer.scrollToLine(Number.POSITIVE_INFINITY);
|
||||
};
|
||||
|
||||
const refreshDir = async (curPath: string) => {
|
||||
const ffmpeg = getFFmpeg();
|
||||
if (ffmpeg.loaded) {
|
||||
setNodes(
|
||||
(await ffmpeg.listDir(curPath)).filter(({ name }) => name !== ".")
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const loadSamples = async () => {
|
||||
const ffmpeg = getFFmpeg();
|
||||
for (const name of Object.keys(SAMPLE_FILES)) {
|
||||
await ffmpeg.writeFile(name, await fetchFile(SAMPLE_FILES[name]));
|
||||
}
|
||||
refreshDir(path);
|
||||
};
|
||||
|
||||
const exec = async () => {
|
||||
const ffmpeg = getFFmpeg();
|
||||
setProgress(0);
|
||||
setTime(0);
|
||||
const logListener = ({ message }) => {
|
||||
setLogs((_logs) => [..._logs, message]);
|
||||
scrollToEnd();
|
||||
};
|
||||
const progListener = ({ progress: prog }) => {
|
||||
setProgress(prog * 100);
|
||||
};
|
||||
ffmpeg.on(FFmpeg.LOG, logListener);
|
||||
ffmpeg.on(FFmpeg.PROGRESS, progListener);
|
||||
const start = performance.now();
|
||||
await ffmpeg.exec(JSON.parse(args));
|
||||
setTime(performance.now() - start);
|
||||
ffmpeg.removeListener(FFmpeg.LOG, logListener);
|
||||
ffmpeg.removeListener(FFmpeg.PROGRESS, progListener);
|
||||
refreshDir(path);
|
||||
};
|
||||
|
||||
const cd = (name: string) => async () => {
|
||||
let nextPath = path;
|
||||
if (path === "/") {
|
||||
if (name !== "..") nextPath = `/${name}`;
|
||||
} else if (name === "..") {
|
||||
const cols = path.split("/");
|
||||
cols.pop();
|
||||
nextPath = cols.length === 1 ? "/" : cols.join("/");
|
||||
} else {
|
||||
nextPath = `${path}/${name}`;
|
||||
}
|
||||
setPath(nextPath);
|
||||
refreshDir(nextPath);
|
||||
};
|
||||
|
||||
const handleFileUpload =
|
||||
(isText: boolean = false) =>
|
||||
async ({ target: { files } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const ffmpeg = getFFmpeg();
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
let data: Uint8Array | string = await fetchFile(file);
|
||||
if (isText) data = new TextDecoder().decode(data);
|
||||
await ffmpeg.writeFile(`${path}/${file.name}`, data);
|
||||
}
|
||||
refreshDir(path);
|
||||
};
|
||||
|
||||
const handleItemClick = (name: string) => async (option: string) => {
|
||||
const ffmpeg = getFFmpeg();
|
||||
const fullPath = `${path}/${name}`;
|
||||
switch (option) {
|
||||
case "download":
|
||||
downloadFile(
|
||||
name,
|
||||
((await ffmpeg.readFile(fullPath, "binary")) as Uint8Array).buffer
|
||||
);
|
||||
break;
|
||||
case "download-text":
|
||||
downloadFile(name, await ffmpeg.readFile(fullPath, "utf8"));
|
||||
break;
|
||||
case "delete":
|
||||
await ffmpeg.deleteFile(fullPath);
|
||||
refreshDir(path);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleFolderCreate = async () => {
|
||||
if (folderName !== "") {
|
||||
await getFFmpeg().createDir(`${path}/${folderName}`);
|
||||
}
|
||||
refreshDir(path);
|
||||
handleModalClose();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refreshDir(path);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Grid container spacing={{ xs: 1 }} columns={{ xs: 4, md: 12 }}>
|
||||
<Grid item xs={4}>
|
||||
<Paper variant="outlined" style={{ padding: 8, height: "100%" }}>
|
||||
<Stack justifyContent="space-between" style={{ height: "100%" }}>
|
||||
<>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography>File System:</Typography>
|
||||
<Box>
|
||||
<Tooltip title="Upload a media file">
|
||||
<IconButton
|
||||
aria-label="upload-media-file"
|
||||
component="label"
|
||||
size="small"
|
||||
>
|
||||
<input
|
||||
hidden
|
||||
multiple
|
||||
type="file"
|
||||
onChange={handleFileUpload(false)}
|
||||
/>
|
||||
<UploadFileIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Upload a text file">
|
||||
<IconButton
|
||||
onClick={() => {}}
|
||||
aria-label="upload-text"
|
||||
component="label"
|
||||
size="small"
|
||||
>
|
||||
<input
|
||||
hidden
|
||||
multiple
|
||||
type="file"
|
||||
onChange={handleFileUpload(true)}
|
||||
/>
|
||||
<UploadIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Create a new folder">
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setFolderName("");
|
||||
handleModalOpen();
|
||||
}}
|
||||
aria-label="create-a-new-folder"
|
||||
size="small"
|
||||
>
|
||||
<CreateNewFolderIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Refresh directory">
|
||||
<IconButton
|
||||
onClick={() => refreshDir(path)}
|
||||
aria-label="fresh"
|
||||
size="small"
|
||||
>
|
||||
<RefreshIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Typography>{`Path: ${path}`}</Typography>
|
||||
<List style={{ height: 480, overflowX: "auto" }}>
|
||||
{nodes.map(({ name, isDir }, index) =>
|
||||
isDir ? (
|
||||
<ListItemButton key={index} onClick={cd(name)}>
|
||||
<ListItemIcon>
|
||||
<FolderIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={name} />
|
||||
</ListItemButton>
|
||||
) : (
|
||||
<ListItem
|
||||
key={index}
|
||||
secondaryAction={
|
||||
<MoreButton
|
||||
options={options}
|
||||
onItemClick={handleItemClick(name)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<InsertDriveFileIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={name} />
|
||||
</ListItem>
|
||||
)
|
||||
)}
|
||||
</List>
|
||||
</>
|
||||
<Button onClick={loadSamples}>Load Sample Files</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={8}>
|
||||
<Paper variant="outlined" style={{ padding: 8, height: "100%" }}>
|
||||
<Stack spacing={1}>
|
||||
<Stack>
|
||||
<Typography>Edit JSON below to update command:</Typography>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme={theme}
|
||||
name="input-args"
|
||||
fontSize={16}
|
||||
showPrintMargin={true}
|
||||
showGutter={true}
|
||||
width="100%"
|
||||
minLines={8}
|
||||
maxLines={8}
|
||||
highlightActiveLine={true}
|
||||
value={args}
|
||||
onChange={(value) => setArgs(value)}
|
||||
setOptions={{ tabSize: 2 }}
|
||||
/>
|
||||
</Stack>
|
||||
<AceEditor
|
||||
mode="javascript"
|
||||
theme={theme}
|
||||
name="ffmpeg.wasm"
|
||||
fontSize={16}
|
||||
showGutter={false}
|
||||
width="100%"
|
||||
minLines={6}
|
||||
maxLines={6}
|
||||
readOnly
|
||||
highlightActiveLine={false}
|
||||
value={genFFmpegText(args)}
|
||||
setOptions={{ tabSize: 2 }}
|
||||
/>
|
||||
<Typography>Console Output:</Typography>
|
||||
<AceEditor
|
||||
mode="text"
|
||||
theme={theme}
|
||||
name="console"
|
||||
fontSize={16}
|
||||
width="100%"
|
||||
minLines={8}
|
||||
maxLines={8}
|
||||
readOnly
|
||||
showPrintMargin={true}
|
||||
highlightActiveLine={false}
|
||||
value={logs.join("\n")}
|
||||
setOptions={{ tabSize: 2 }}
|
||||
onLoad={(editor) => setOutput(editor)}
|
||||
/>
|
||||
<Typography>Transcoding Progress:</Typography>
|
||||
<LinearProgressWithLabel value={progress} />
|
||||
<Stack direction="row" spacing={2} justifyContent="space-between">
|
||||
<Typography>
|
||||
{time === 0
|
||||
? ""
|
||||
: `Time Elapsed: ${(time / 1000).toFixed(2)} s`}
|
||||
</Typography>
|
||||
<Button variant="contained" onClick={exec}>
|
||||
Run
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={handleModalClose}
|
||||
aria-labelledby="new-folder-name"
|
||||
aria-describedby="new-folder-name-description"
|
||||
>
|
||||
<Box sx={modalStyle}>
|
||||
<Stack spacing={4}>
|
||||
<Typography id="modal-modal-title" variant="h6" component="h2">
|
||||
Folder Name:
|
||||
</Typography>
|
||||
<TextField
|
||||
id="outlined-basic"
|
||||
label="my-folder"
|
||||
variant="outlined"
|
||||
value={folderName}
|
||||
onChange={(event) => setFolderName(event.target.value)}
|
||||
/>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Button onClick={handleModalClose}>Cancel</Button>
|
||||
<Button variant="contained" onClick={handleFolderCreate}>
|
||||
Create
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Modal>
|
||||
</Box>
|
||||
);
|
||||
}
|
121
apps/website/src/components/Playground/Workspace/Editor.tsx
Normal file
121
apps/website/src/components/Playground/Workspace/Editor.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
/// <reference types="ace" />
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import AceEditor from "react-ace";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Button from "@mui/material/Button";
|
||||
import LinearProgressWithLabel from "@site/src/components/common/LinearProgressWithLabel";
|
||||
import { useColorMode } from "@docusaurus/theme-common";
|
||||
import "ace-builds/src-noconflict/mode-json";
|
||||
import "ace-builds/src-noconflict/mode-javascript";
|
||||
import "ace-builds/src-noconflict/mode-text";
|
||||
import "ace-builds/src-noconflict/theme-dracula";
|
||||
import "ace-builds/src-noconflict/theme-github";
|
||||
|
||||
const genFFmpegText = (args: string) => {
|
||||
let data: any = [];
|
||||
try {
|
||||
data = JSON.parse(args);
|
||||
} catch (e) {}
|
||||
return `// equivalent ffmpeg.wasm API call
|
||||
ffmpeg.exec(${JSON.stringify(data)});
|
||||
|
||||
// equivalent ffmpeg command line
|
||||
ffmpeg ${data.join(" ")}`;
|
||||
};
|
||||
|
||||
interface EditorProps {
|
||||
args: string;
|
||||
logs: string[];
|
||||
progress: number;
|
||||
time: number;
|
||||
onArgsUpdate: (args: string) => void;
|
||||
onExec: () => Promise<void>;
|
||||
}
|
||||
|
||||
export default function Editor({
|
||||
args = "",
|
||||
logs = [],
|
||||
progress = 0,
|
||||
time = 0,
|
||||
onArgsUpdate,
|
||||
onExec,
|
||||
}: EditorProps) {
|
||||
const { colorMode } = useColorMode();
|
||||
const [output, setOutput] = useState<Ace.Editor>();
|
||||
|
||||
useEffect(() => {
|
||||
// scroll logs to the end.
|
||||
output && output.renderer.scrollToLine(Number.POSITIVE_INFINITY);
|
||||
}, [logs]);
|
||||
|
||||
const theme = colorMode === "dark" ? "github" : "dracula";
|
||||
|
||||
return (
|
||||
<Paper variant="outlined" style={{ padding: 8, height: "100%" }}>
|
||||
<Stack spacing={1}>
|
||||
<Stack>
|
||||
<Typography>Editor:</Typography>
|
||||
<Typography>Edit arguments below to update command:</Typography>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme={theme}
|
||||
name="input-args"
|
||||
fontSize={16}
|
||||
showPrintMargin={true}
|
||||
showGutter={true}
|
||||
width="100%"
|
||||
minLines={8}
|
||||
maxLines={8}
|
||||
highlightActiveLine={true}
|
||||
value={args}
|
||||
onChange={onArgsUpdate}
|
||||
setOptions={{ tabSize: 2 }}
|
||||
/>
|
||||
</Stack>
|
||||
<AceEditor
|
||||
mode="javascript"
|
||||
theme={theme}
|
||||
name="ffmpeg.wasm"
|
||||
fontSize={16}
|
||||
showGutter={false}
|
||||
width="100%"
|
||||
minLines={6}
|
||||
maxLines={6}
|
||||
readOnly
|
||||
highlightActiveLine={false}
|
||||
value={genFFmpegText(args)}
|
||||
setOptions={{ tabSize: 2 }}
|
||||
/>
|
||||
<Typography>Console Output:</Typography>
|
||||
<AceEditor
|
||||
mode="text"
|
||||
theme={theme}
|
||||
name="console"
|
||||
fontSize={16}
|
||||
width="100%"
|
||||
minLines={8}
|
||||
maxLines={8}
|
||||
readOnly
|
||||
showPrintMargin={true}
|
||||
highlightActiveLine={false}
|
||||
value={logs.join("\n")}
|
||||
setOptions={{ tabSize: 2 }}
|
||||
onLoad={(editor) => setOutput(editor)}
|
||||
/>
|
||||
<Typography>Transcoding Progress:</Typography>
|
||||
<LinearProgressWithLabel value={progress} />
|
||||
<Stack direction="row" spacing={2} justifyContent="space-between">
|
||||
<Typography>
|
||||
{time === 0 ? "" : `Time Elapsed: ${(time / 1000).toFixed(2)} s`}
|
||||
</Typography>
|
||||
<Button variant="contained" onClick={onExec}>
|
||||
Run
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
}
|
@ -0,0 +1,202 @@
|
||||
import React, { useState, ChangeEvent } from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import List from "@mui/material/List";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import ListItemButton from "@mui/material/ListItemButton";
|
||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import Modal from "@mui/material/Modal";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import FolderIcon from "@mui/icons-material/Folder";
|
||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||
import UploadFileIcon from "@mui/icons-material/UploadFile";
|
||||
import UploadIcon from "@mui/icons-material/Upload";
|
||||
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
|
||||
import CreateNewFolderIcon from "@mui/icons-material/CreateNewFolder";
|
||||
import MoreButton from "./MoreButton";
|
||||
import { Node } from "./types";
|
||||
|
||||
interface FileSystemManagerProps {
|
||||
path: string;
|
||||
nodes: Node[];
|
||||
onFileUpload: (
|
||||
isText: boolean
|
||||
) => (event: ChangeEvent<HTMLInputElement>) => Promise<void>;
|
||||
onDirClick: (name: string) => () => Promise<void>;
|
||||
onFileClick: (name: string) => (option: string) => Promise<void>;
|
||||
onDirCreate: (name: string) => () => Promise<void>;
|
||||
onRefresh: () => Promise<void>;
|
||||
onLoadSamples: () => Promise<void>;
|
||||
}
|
||||
|
||||
const modalStyle = {
|
||||
position: "absolute" as "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 400,
|
||||
bgcolor: "background.paper",
|
||||
p: 4,
|
||||
};
|
||||
|
||||
export const options = [
|
||||
{ text: "Download", key: "download" },
|
||||
{ text: "Download as Text File", key: "download-text" },
|
||||
{ text: "Delete", key: "delete" },
|
||||
];
|
||||
|
||||
export default function FileSystemManager({
|
||||
path = "/",
|
||||
nodes = [],
|
||||
onFileUpload = () => () => Promise.resolve(),
|
||||
onFileClick = () => () => Promise.resolve(),
|
||||
onDirClick = () => () => Promise.resolve(),
|
||||
onDirCreate = () => () => Promise.resolve(),
|
||||
onRefresh = () => Promise.resolve(),
|
||||
onLoadSamples = () => Promise.resolve(),
|
||||
}: FileSystemManagerProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [dirName, setDirName] = useState("");
|
||||
const handleModalOpen = () => setOpen(true);
|
||||
const handleModalClose = () => setOpen(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paper variant="outlined" style={{ padding: 8, height: "100%" }}>
|
||||
<Stack justifyContent="space-between" style={{ height: "100%" }}>
|
||||
<>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography>File System:</Typography>
|
||||
<Box>
|
||||
<Tooltip title="Upload a media file">
|
||||
<IconButton
|
||||
aria-label="upload-media-file"
|
||||
component="label"
|
||||
size="small"
|
||||
>
|
||||
<input
|
||||
hidden
|
||||
multiple
|
||||
type="file"
|
||||
onChange={onFileUpload(false)}
|
||||
/>
|
||||
<UploadFileIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Upload a text file">
|
||||
<IconButton
|
||||
onClick={() => {}}
|
||||
aria-label="upload-text"
|
||||
component="label"
|
||||
size="small"
|
||||
>
|
||||
<input
|
||||
hidden
|
||||
multiple
|
||||
type="file"
|
||||
onChange={onFileUpload(true)}
|
||||
/>
|
||||
<UploadIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Create a new folder">
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setDirName("");
|
||||
handleModalOpen();
|
||||
}}
|
||||
aria-label="create-a-new-folder"
|
||||
size="small"
|
||||
>
|
||||
<CreateNewFolderIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Refresh directory">
|
||||
<IconButton
|
||||
onClick={onRefresh}
|
||||
aria-label="fresh"
|
||||
size="small"
|
||||
>
|
||||
<RefreshIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Typography>{`Path: ${path}`}</Typography>
|
||||
<List style={{ height: 480, overflowX: "auto" }}>
|
||||
{nodes.map(({ name, isDir }, index) =>
|
||||
isDir ? (
|
||||
<ListItemButton key={index} onClick={onDirClick(name)}>
|
||||
<ListItemIcon>
|
||||
<FolderIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={name} />
|
||||
</ListItemButton>
|
||||
) : (
|
||||
<ListItem
|
||||
key={index}
|
||||
secondaryAction={
|
||||
<MoreButton
|
||||
options={options}
|
||||
onItemClick={onFileClick(name)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<InsertDriveFileIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={name} />
|
||||
</ListItem>
|
||||
)
|
||||
)}
|
||||
</List>
|
||||
</>
|
||||
<Button onClick={onLoadSamples}>Load Sample Files</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={handleModalClose}
|
||||
aria-labelledby="new-folder-name"
|
||||
aria-describedby="new-folder-name-description"
|
||||
>
|
||||
<Box sx={modalStyle}>
|
||||
<Stack spacing={4}>
|
||||
<Typography id="modal-modal-title" variant="h6" component="h2">
|
||||
Folder Name:
|
||||
</Typography>
|
||||
<TextField
|
||||
id="outlined-basic"
|
||||
label="my-folder"
|
||||
variant="outlined"
|
||||
value={dirName}
|
||||
onChange={(event) => setDirName(event.target.value)}
|
||||
/>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Button onClick={handleModalClose}>Cancel</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
onDirCreate(dirName);
|
||||
handleModalClose();
|
||||
}}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
154
apps/website/src/components/Playground/Workspace/index.tsx
Normal file
154
apps/website/src/components/Playground/Workspace/index.tsx
Normal file
@ -0,0 +1,154 @@
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
useState,
|
||||
useEffect,
|
||||
MutableRefObject,
|
||||
} from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import { FFmpeg } from "@ffmpeg/ffmpeg";
|
||||
import { fetchFile } from "@ffmpeg/util";
|
||||
import { downloadFile } from "@site/src/util";
|
||||
import { Node } from "./types";
|
||||
import FileSystemManager from "./FileSystemManager";
|
||||
import { SAMPLE_FILES } from "../const";
|
||||
import Editor from "./Editor";
|
||||
|
||||
const defaultArgs = JSON.stringify(["-i", "video.avi", "video.mp4"], null, 2);
|
||||
|
||||
interface WorkspaceProps {
|
||||
ffmpeg: MutableRefObject<FFmpeg>;
|
||||
}
|
||||
|
||||
export default function Workspace({ ffmpeg: _ffmpeg }: WorkspaceProps) {
|
||||
const [path, setPath] = useState("/");
|
||||
const [nodes, setNodes] = useState<Node[]>([]);
|
||||
const [args, setArgs] = useState(defaultArgs);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [time, setTime] = useState(0);
|
||||
const [logs, setLogs] = useState<string[]>([]);
|
||||
|
||||
const ffmpeg = _ffmpeg.current;
|
||||
|
||||
const refreshDir = async (curPath: string) => {
|
||||
if (ffmpeg.loaded) {
|
||||
setNodes(
|
||||
(await ffmpeg.listDir(curPath)).filter(({ name }) => name !== ".")
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const onFileUpload =
|
||||
(isText: boolean) =>
|
||||
async ({ target: { files } }: ChangeEvent<HTMLInputElement>) => {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
let data: Uint8Array | string = await fetchFile(file);
|
||||
if (isText) data = new TextDecoder().decode(data);
|
||||
await ffmpeg.writeFile(`${path}/${file.name}`, data);
|
||||
}
|
||||
refreshDir(path);
|
||||
};
|
||||
|
||||
const onFileClick = (name: string) => async (option: string) => {
|
||||
const fullPath = `${path}/${name}`;
|
||||
switch (option) {
|
||||
case "download":
|
||||
downloadFile(
|
||||
name,
|
||||
((await ffmpeg.readFile(fullPath, "binary")) as Uint8Array).buffer
|
||||
);
|
||||
break;
|
||||
case "download-text":
|
||||
downloadFile(name, await ffmpeg.readFile(fullPath, "utf8"));
|
||||
break;
|
||||
case "delete":
|
||||
await ffmpeg.deleteFile(fullPath);
|
||||
refreshDir(path);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const onDirClick = (name: string) => async () => {
|
||||
let nextPath = path;
|
||||
if (path === "/") {
|
||||
if (name !== "..") nextPath = `/${name}`;
|
||||
} else if (name === "..") {
|
||||
const cols = path.split("/");
|
||||
cols.pop();
|
||||
nextPath = cols.length === 1 ? "/" : cols.join("/");
|
||||
} else {
|
||||
nextPath = `${path}/${name}`;
|
||||
}
|
||||
setPath(nextPath);
|
||||
refreshDir(nextPath);
|
||||
};
|
||||
|
||||
const onDirCreate = (name: string) => async () => {
|
||||
if (name !== "") {
|
||||
await ffmpeg.createDir(`${path}/${name}`);
|
||||
}
|
||||
refreshDir(path);
|
||||
};
|
||||
|
||||
const onLoadSamples = async () => {
|
||||
for (const name of Object.keys(SAMPLE_FILES)) {
|
||||
await ffmpeg.writeFile(name, await fetchFile(SAMPLE_FILES[name]));
|
||||
}
|
||||
refreshDir(path);
|
||||
};
|
||||
|
||||
const onExec = async () => {
|
||||
setProgress(0);
|
||||
setTime(0);
|
||||
const logListener = ({ message }) => {
|
||||
setLogs((_logs) => [..._logs, message]);
|
||||
};
|
||||
const progListener = ({ progress: prog }) => {
|
||||
setProgress(prog * 100);
|
||||
};
|
||||
ffmpeg.on(FFmpeg.LOG, logListener);
|
||||
ffmpeg.on(FFmpeg.PROGRESS, progListener);
|
||||
const start = performance.now();
|
||||
await ffmpeg.exec(JSON.parse(args));
|
||||
setTime(performance.now() - start);
|
||||
ffmpeg.removeListener(FFmpeg.LOG, logListener);
|
||||
ffmpeg.removeListener(FFmpeg.PROGRESS, progListener);
|
||||
refreshDir(path);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refreshDir(path);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Grid container spacing={{ xs: 1 }} columns={{ xs: 4, md: 12 }}>
|
||||
<Grid item xs={4}>
|
||||
<FileSystemManager
|
||||
path={path}
|
||||
nodes={nodes}
|
||||
onFileUpload={onFileUpload}
|
||||
onFileClick={onFileClick}
|
||||
onDirClick={onDirClick}
|
||||
onDirCreate={onDirCreate}
|
||||
onLoadSamples={onLoadSamples}
|
||||
onRefresh={() => refreshDir(path)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={8}>
|
||||
<Editor
|
||||
args={args}
|
||||
logs={logs}
|
||||
progress={progress}
|
||||
time={time}
|
||||
onArgsUpdate={(_args) => setArgs(_args)}
|
||||
onExec={onExec}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
export interface Node {
|
||||
name: string;
|
||||
isDir: boolean;
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import { FFmpeg } from "@ffmpeg/ffmpeg";
|
||||
|
||||
let ffmpeg = new FFmpeg();
|
||||
|
||||
export const getFFmpeg = () => ffmpeg;
|
@ -1,10 +1,9 @@
|
||||
import * as React from "react";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { FFmpeg } from "@ffmpeg/ffmpeg";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import MuiThemeProvider from "@site/src/components/MuiThemeProvider";
|
||||
import MuiThemeProvider from "@site/src/components/common/MuiThemeProvider";
|
||||
import CoreDownloader from "./CoreDownloader";
|
||||
import Editor from "./Editor";
|
||||
import { getFFmpeg } from "./ffmpeg";
|
||||
import Workspace from "./Workspace";
|
||||
import { CORE_URL, CORE_MT_URL } from "./const";
|
||||
import CoreSwitcher from "./CoreSwitcher";
|
||||
|
||||
@ -15,20 +14,20 @@ enum State {
|
||||
}
|
||||
|
||||
export default function Playground() {
|
||||
const { useState, useEffect } = React;
|
||||
const [state, setState] = useState(State.LOADED);
|
||||
const [isCoreMT, setIsCoreMT] = useState(false);
|
||||
const [url, setURL] = useState("");
|
||||
const [received, setReceived] = useState(0);
|
||||
const ffmpeg = useRef(new FFmpeg());
|
||||
|
||||
const load = async (mt: boolean = false) => {
|
||||
setState(State.LOADING);
|
||||
const ffmpeg = getFFmpeg();
|
||||
ffmpeg.terminate();
|
||||
ffmpeg.on(FFmpeg.DOWNLOAD, ({ url: _url, received: _received }) => {
|
||||
ffmpeg.current.terminate();
|
||||
ffmpeg.current.on(FFmpeg.DOWNLOAD, ({ url: _url, received: _received }) => {
|
||||
setURL(_url as string);
|
||||
setReceived(_received);
|
||||
});
|
||||
await ffmpeg.load({
|
||||
await ffmpeg.current.load({
|
||||
coreURL: mt ? CORE_MT_URL : CORE_URL,
|
||||
thread: mt,
|
||||
});
|
||||
@ -54,7 +53,7 @@ export default function Playground() {
|
||||
case State.LOADING:
|
||||
return <CoreDownloader url={url} received={received} />;
|
||||
case State.LOADED:
|
||||
return <Editor />;
|
||||
return <Workspace ffmpeg={ffmpeg} />;
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
|
11
apps/website/src/components/common/ThemedButton/index.tsx
Normal file
11
apps/website/src/components/common/ThemedButton/index.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
import MuiThemeProvider from "../MuiThemeProvider";
|
||||
import Button, { ButtonProps } from "@mui/material/Button";
|
||||
|
||||
export default function ThemedButton(props: ButtonProps) {
|
||||
return (
|
||||
<MuiThemeProvider>
|
||||
<Button {...props} />
|
||||
</MuiThemeProvider>
|
||||
);
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
import MuiThemeProvider from "../MuiThemeProvider";
|
||||
import IconButton, { IconButtonProps } from "@mui/material/IconButton";
|
||||
|
||||
export default function ThemedIconButton(props: IconButtonProps) {
|
||||
return (
|
||||
<MuiThemeProvider>
|
||||
<IconButton {...props} />
|
||||
</MuiThemeProvider>
|
||||
);
|
||||
}
|
@ -18,7 +18,7 @@ function HomepageHeader() {
|
||||
<div className={styles.buttons}>
|
||||
<Link
|
||||
className="button button--secondary button--lg"
|
||||
to="/docs/intro"
|
||||
to="/playground"
|
||||
>
|
||||
Try it Now!
|
||||
</Link>
|
||||
|
@ -1,8 +1,104 @@
|
||||
import Playground from "@site/src/components/Playground";
|
||||
import CoreSwitcher from "@site/src/components/Playground/CoreSwitcher";
|
||||
import FileSystemManager from
|
||||
"@site/src/components/Playground/Workspace/FileSystemManager";
|
||||
import Editor from
|
||||
"@site/src/components/Playground/Workspace/Editor";
|
||||
import MuiThemeProvider from "@site/src/components/common/MuiThemeProvider";
|
||||
import ThemedButton from "@site/src/components/common/ThemedButton";
|
||||
import ThemedIconButton from "@site/src/components/common/ThemedIconButton";
|
||||
import CreateNewFolderIcon from "@mui/icons-material/CreateNewFolder";
|
||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||
import UploadFileIcon from "@mui/icons-material/UploadFile";
|
||||
import UploadIcon from "@mui/icons-material/Upload";
|
||||
|
||||
# Playground
|
||||
|
||||
Hi! Welcome to ffmpeg.wasm playground! Here you can try and test ffmpeg.wasm
|
||||
with ease. :smile:
|
||||
Playground allows you to try ffmpeg.wasm without any installation and
|
||||
development!
|
||||
|
||||
:::tip Quick Start
|
||||
|
||||
1. Wait for assets (~32 MB) downloading.
|
||||
2. Press <ThemedButton>Load Sample Files</ThemedButton> to download & add sample files.
|
||||
3. Press <ThemedButton variant="contained">Run</ThemedButton> to convert an AVI file to MP4 file.
|
||||
4. Download output files.
|
||||
|
||||
:::
|
||||
|
||||
<Playground />
|
||||
|
||||
<div style={{ height: 32 }} />
|
||||
|
||||
## How to Use :rocket:
|
||||
|
||||
> It is recommended to read [Introduction](/docs/intro) first to learn
|
||||
ffmpeg.wasm fundamentals.
|
||||
|
||||
Demo Video:
|
||||
<video width="100%" controls>
|
||||
<source src="/video/playground-how-to.webm" type="video/webm" />
|
||||
</video>
|
||||
|
||||
A typical flow to use ffmpeg.wasm is:
|
||||
|
||||
#### Download and load JavaScript & WebAssembly assets
|
||||
|
||||
The assets are downloaded automatically when you enter the Playground. You can
|
||||
choose to use multithreading version instead by click on the switch:
|
||||
|
||||
<MuiThemeProvider>
|
||||
<CoreSwitcher />
|
||||
</MuiThemeProvider>
|
||||
|
||||
#### Load files to in-memory File System
|
||||
|
||||
When ffmpeg.wasm is loaded and ready, you can upload files to its in-memory File
|
||||
System to make sure these files can be consumed by the ffmpeg.wasm APIs:
|
||||
|
||||
<div style={{ maxWidth: 260 }}>
|
||||
<MuiThemeProvider>
|
||||
<FileSystemManager
|
||||
nodes={[
|
||||
{name: "..", isDir: true},
|
||||
{name: "tmp", isDir: true},
|
||||
{name: "home", isDir: true},
|
||||
{name: "dev", isDir: true},
|
||||
{name: "proc", isDir: true},
|
||||
{name: "video.avi", isDir: false},
|
||||
]}
|
||||
/>
|
||||
</MuiThemeProvider>
|
||||
</div>
|
||||
|
||||
<div style={{ height: 32 }} />
|
||||
|
||||
- <ThemedIconButton size="small"><UploadFileIcon fontSize="small"
|
||||
/></ThemedIconButton>: Upload a media file.
|
||||
- <ThemedIconButton size="small"><UploadIcon fontSize="small"
|
||||
/></ThemedIconButton>: Upload a text file.
|
||||
- <ThemedIconButton size="small"><CreateNewFolderIcon fontSize="small"
|
||||
/></ThemedIconButton>: Create a new folder.
|
||||
- <ThemedIconButton size="small"><RefreshIcon fontSize="small"
|
||||
/></ThemedIconButton>: Refresh File System.
|
||||
|
||||
> Press <ThemedButton>Load Sample Files</ThemedButton> to load a set of samples
|
||||
files.
|
||||
|
||||
#### Run a command
|
||||
|
||||
With files are ready in the File System, you can update arguments in the Editor
|
||||
and hit <ThemedButton variant="contained">Run</ThemedButton> afterward:
|
||||
|
||||
<div style={{ maxWidth: 480 }}>
|
||||
<MuiThemeProvider>
|
||||
<Editor args={JSON.stringify(["-i", "video.avi", "video.mp4"], null, 2)} />
|
||||
</MuiThemeProvider>
|
||||
</div>
|
||||
|
||||
<div style={{ height: 32 }} />
|
||||
|
||||
#### Download output files
|
||||
|
||||
Lastly you can download your files using File System panel and check the result.
|
||||
:tada:
|
||||
|
BIN
apps/website/static/video/playground-how-to.webm
Executable file
BIN
apps/website/static/video/playground-how-to.webm
Executable file
Binary file not shown.
Loading…
Reference in New Issue
Block a user