概要
この記事では、ffmpeg + Canvas を使用してオンラインのビジュアル オーディオおよびビデオ編集ツールを作成する方法を紹介します。
- ブラウザで ffmpeg を実行してオーディオとビデオを処理する方法。
- キャンバスを使用してビデオ トラックを実装する方法。
- トラック、プレーヤー、ffmpeg を組み合わせてビデオ編集の視覚化を実現する方法。
キーワード
ffmpeg、ビデオ、トラック、視覚化、キャンバス
エフェクトのスクリーンショット
体験アドレス
videoCut(vercel で展開、科学的インターネット アクセスが必要)
コードリポジトリ
img-generate (クリックしました。無料の星をクリックしてください)
プロジェクトの背景
プロジェクトを立ち上げた理由
仕事や生活の中で、次のようなオーディオやビデオ関連の問題に遭遇することがよくあります。
- ビデオから写真をキャプチャしたい。
- 長いビデオから特定のセグメントをキャプチャしたい。
- フォーマット変換、ビデオからオーディオを抽出。
- フレームレートなどのビデオ情報を取得します。
ローカルで分析する場合は、まず環境を構成して ffmpeg などの分析ツールをインストールする必要があり、より多くの準備作業が必要になります。 wasm テクノロジーを使用し、ブラウザ上で ffmpeg を直接実行する場合は、ローカルにインストールしてダウンロードする必要がなく、初心者ユーザーにとってより使いやすくなります。
自分で試してみましょう - ブラウザで ffmpeg を使用してください
@ffmpeg/ffmpeg
これはコンパイルされたパッケージです。欠点は、サイズが比較的大きく、初めてロードするときに数秒待つ必要があることですが、利点は直接使用できることです。ここではこのパッケージを直接参照し、後で最適化が必要になった場合は、自分でコンパイルするか、より適切なパッケージを探すことを検討します。
APIの紹介
公式デモを見るだけで、数行のスクリプトを書くだけで、フォーマット変換タスクを完了できます。
import { writeFile } from 'fs/promises';
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg';
// 设置参数
const ffmpeg = createFFmpeg({ log: true });
(async () => {
// 加载 ffmpeg.wasm-core 脚本
await ffmpeg.load();
// 输入文件需要调用 FS 方法,这样 ffmpeg 才能够进行”消费“
ffmpeg.FS('writeFile', 'test.avi', await fetchFile('./test.avi'));
// 执行 ffmpeg 命令
await ffmpeg.run('-i', 'test.avi', 'test.mp4');
// node 端将生成的文件直接写到文件中
await fs.promises.writeFile('./test.mp4', ffmpeg.FS('readFile', 'test.mp4'));
process.exit(0);
})();
大まかなプロセスは次のとおりです
プロジェクトの初期化
ここでは umi を使用しています。これは次のコマンドで初期化できます。
# 新建文件夹
mkdir myapp && cd myapp
# 初始化项目
yarn create @umijs/umi-app
# 安装依赖
yarn start
上記の手順を完了すると、ブラウザ上でページの効果を確認できます。
@ffmpeg/ffmpeg を紹介します
インストール パッケージは非常に簡単で、 npm i @ffmpeg/ffmpeg
を渡すだけですが、導入プロセス中にいくつかの問題が発生しました。
質問 1: モジュールの解析に失敗しました: 予期しないトークンです
ウェブパック4
ERROR in ./node_modules/@ffmpeg/ffmpeg/src/browser/defaultOptions.js 7:68
Module parse failed: Unexpected token (7:68)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| */
| const corePath = typeof process !== 'undefined' && process.env.NODE_ENV === 'development'
> ? new URL('/node_modules/@ffmpeg/core/dist/ffmpeg-core.js', import.meta.url).href
| : `https://unpkg.com/@ffmpeg/core@${devDependencies['@ffmpeg/core'].substring(1)}/dist/ffmpeg-core.js`;
|
@ ./node_modules/@ffmpeg/ffmpeg/src/browser/index.js 1:23-50
@ ./node_modules/@ffmpeg/ffmpeg/src/index.js
@ ./src/pages/index.tsx
@ ./src/.umi/core/routes.ts
@ ./src/.umi/umi.ts
@ multi ./node_modules/umi/node_modules/@umijs/preset-built-in/bundled/@pmmmwh/react-refresh-webpack-plugin/client/ReactRefreshEntry.js ./src/.umi/umi.ts
解決策 1: インポート方法を変更する
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg';
への変更
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg/dist/ffmpeg.min.js';
このとき、型は失われますので、強制的に設定してください。
import { FFmpeg } from '@ffmpeg/ffmpeg';
const ffmpeg = createFFmpeg({
...
}) as FFmpeg;
解決策 2: Webpack 5 に切り替える
umi
切り替え webpack 5
は比較的簡単です。 .umirc
で直接設定するだけです。
webpack5: {}
次に、コア ファイルを public に配置し、FFmpeg を作成するときにパラメータを設定します。
const ffmpeg = createFFmpeg({
...
corePath: `${location.origin}/static/v0.11.0/ffmpeg-core.js`,
});
質問 2: SharedArrayBuffer が定義されていません
問題の原因は、ffmpeg が SharedArrayBuffer を使用していることですが、SharedArrayBuffer にはセキュリティの問題によりいくつかの制限があります。ページで SharedArrayBuffer を使用する場合は、ページをクロスドメイン分離用に設定するか、一時的な解決策としてトークン トークンを設定する必要があります。 (具体的な理由はフロントエンドの FFmpeg? がまだ準備ができていない可能性があります。紹介文がさらに詳しく説明されています。参照してください)< a i=5> 解決策: クロスドメイン分離を設定する
CORP は、ページ内に 2 つのリクエスト ヘッダーを設定することで設定できます。
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
開発時にはdevServerを設定することで問題を解決できます。
devServer: {
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
},
手順を整理し、基本的なプロセスを実行する
@ffmpeg/ffmpeg
パッケージの導入が完了したら、関数の開発を開始できます。機能の優先順位の原則に従って、機能を最初に実行する必要があり、プロセスと UI は後で最適化できます。
いくつかのデモを参照浏览器 ffmpeg
し、より快適なプロセスをコンパイルしました。
処理するファイルをアップロードする
アップロードするファイルを取得し、FS メソッドを呼び出します。
const props = {
...,
beforeUpload: async (file) => {
const { name, size, type } = file;
ffmpeg.FS('writeFile', name, await fetchFile(file));
...
setOpInput(name);
return false;
},
}
...
<Upload {...props}>
<Button icon={<UploadOutlined />}>选择文件</Button>
</Upload>
注文の実行
输入
、输出文件
、执行脚本
およびその他のパラメータを入力した後、パラメータを結合して < を実行できます。 / a> ffmpeg 命令
で出力結果が得られます。
await ffmpeg.run(...allArgs.split(' '));
効果を見てください、問題ありません
少し最適化を追加
ffmpeg コマンドは実際には覚えるのが比較的難しく、ユーザーが自分で見つけるのは不親切です。したがって、より一般的に使用されるコマンドをすべて記録して、ユーザーが直接選択できるようにすることが期待されています。
export const getOp = (op: string, args?: IGetOp) => {
// const { out = OUT_DEFAULT, input = IN_DEFAULT, timer } = args || {}
let output = '';
let resultOp = '';
const { rangeLeft, rangeRight, input, out, timer } = args || {};
switch (op) {
case OP_NAME.screenshot:
resultOp = `-i ${input} -ss ${timer} -vframes 1 ${out}`;
output = 'out.png';
break;
case OP_NAME.getMp3FromVideo:
resultOp = ` -f mp3 -vn`;
output = 'out.mp3';
break;
case OP_NAME.getInfo:
resultOp = '';
output = OUT_DEFAULT;
break;
case OP_NAME.custom:
resultOp = '';
output = OUT_DEFAULT;
break;
case OP_NAME.cutVideo:
resultOp = `-ss ${rangeLeft} -to ${rangeRight} -i ${input} -c:v copy -c:a copy ${out}`;
output = OUT_DEFAULT;
break;
default:
resultOp = DEFAULT_ARGS;
output = OUT_DEFAULT;
}
return [resultOp, output];
};
効果は以下の通りです
体験アドレス
ffmpeg(vercel にデプロイされ、科学的なインターネット アクセスが必要)
ビデオトラック (ビジュアルオーディオおよびビデオセグメンテーション)
実装するには可视化的音视频分割
、トラックとプレーヤーを実装する必要があります。トラックを通じてセグメンテーション ポイントを正確に設定し、プレーヤーを通じてポイントのビデオ効果をリアルタイムで確認します。ここでは、オープン ソース プロジェクト shWave を参照して、単純なトラックを実装します。
全体的な分析
トラックが分割されている場合は、次のように分割されます。
- 背景
- 規模
- タイムポインター (現在のビデオが再生されている瞬間を指します)
次に、静态部分 -> 动态部分(参数传递) -> 动态部分(事件响应、缩放条)
の順に実装します。
静的な部分実装
ページにキャンバスを追加する
トラックはキャンバスを使用して実装されるため、最初にキャンバスをページに追加する必要があります
<canvas
ref={$canvas}
id="shcanvas"
style={
{
height: '100%',
width: '100%',
zIndex: 0,
pointerEvents: 'auto',
}}
></canvas>
キャンバスがロードされると、トラックの描画が開始されます。
useEffect(() => {
if (!waveCanvas) { return }
draw?.()
}, [waveCanvas, draw, currentTime, duration]);
// 各种场景都有可能触发重新绘制
draw
この関数では、前述の 3 つの要素 (背景、スケール、時間ポインター) がそれぞれ描画されます。
const draw = () => {
const ctx = waveCanvas && waveCanvas?.getContext("2d");
if (!waveCanvas || !ctx) return;
//绘制背景
drawBackground(waveCanvas, ctx, backgroundColor);
// 刻度尺
drawRuler(waveCanvas, ctx, 1, duration)
// 时间指针
drawPointer({
canvas: waveCanvas,
ctx,
pixelRatio: 1,
duration,
currentTime,
color: pointerColor,
pointerWidth,
})
}
背景を描画する(drawBackground)
export const drawBackground = (canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, backgroundColor = "#529393") => {
if (!canvas || !ctx) return;
const { width, height } = canvas;
//清空上一次渲染
ctx.clearRect(0, 0, width, height);
//设置填充绘画的颜色
ctx.fillStyle = backgroundColor;
//填充出一个矩形 (绘制背景)
ctx.fillRect(0, 0, width, height);
};
結果を示す
スケールを描く
スケールをループしてから、さまざまな長さの小さな長方形を描画します。その後、2 番目のスケール全体の下にテキストが表示されます。
- 全 2 番目、最長。
- 0.5秒、秒。
- 最短0.1秒。
まるまる1秒
長さが必要です秒长度 * 10
。その後、反復して 10 * 0.1 ごとに最長の長方形を描画します。このうち、走査するたびに変化するのは x の位置であり、その他のパラメータは変化しません。
for (let index = 0; index < length; index += 1) {
//十格间距
if (index % 10 === 0) {
// x , y , w, h
ctx.fillRect(
index * gap,
0,
pixelRatio,
fontHeight * pixelRatio);
}
}
0.5秒
他はすべて同じままで、長さだけが変わります。
else if (index % 5 === 0) {
ctx.fillRect(index * gap, 0, pixelRatio, (fontHeight * pixelRatio) / 1.5);
}
0.1秒
繰り返しますが、長さが短いだけです。
else {
ctx.fillRect(index * gap, 0, pixelRatio, (fontHeight * pixelRatio) / 3);
}
継続時間テキスト表示テキストは現実に対応する瞬間であり、时刻 = begin + second
if (index % 10 === 0) {
second += 1;
ctx.fillText(
// text x y maxWidth
// time = 开始时间 + 遍历到的秒数
durationToTime(begin + second).split(".")[0], // s
gap * index - fontSize * pixelRatio * 2 + pixelRatio, // x
fontTop * pixelRatio // y
);
}
タイムライン(タイマー)
タイム スケールは実際には長方形で、ビデオ再生の現在の瞬間を示します。これは、currentTime に対応するタイム スケールです。
const { width, height } = canvas;
const length = getLength(duration);
// 每 0.1 s 所对应的像素宽度。
const gap = getGap(width, length)
// 开始点
const begin = getBegin(currentTime, duration);
ctx.fillRect(
Number(((currentTime - begin) * 10 * gap).toFixed(3)), // x
0, // y
pointerWidth * pixelRatio, // width
height, // height
)
こうすることで、静的なタイムラインの効果がすでに利用可能になります。
パラメータの受け渡し
次に、タイムラインをアニメーション化できるように、すべての変数をパラメータとして渡します。ここでは、 storybook
を使用してリアルタイムの効果を表示します。
argTypes: {
currentTime: {
control: {
type: 'number',
},
},
duration: {
control: {
type: 'number',
},
},
backgroundColor: {
control: {
type: 'color',
},
},
pointerWidth: {
control: {
type: 'number',
},
},
pointerColor: {
control: {
type: 'color',
},
},
},
効果は図のとおりで、以下の入力パラメータを変更すると、上のリアルタイム効果が表示されます。
インシデント対応
ズームバー
shwave は、持続時間を調整することでスケーリングされます。この解決策の 1 つの問題は、ズームインすると、後ろの継続時間は表示されず、前方の継続時間のみがズームインできることです。より適切な方法は、トラックの長さを制御するためにスケーリング比率フィールドを追加することだと思います。下にスクロール バーを追加します。トラック上に配置した後、スクロールして次の時間を表示できます。
Slider + InputNumber を使用して、ドラッグして入力できるズーム バーを実装します。
<Col span={5}>
<Slider
min={1}
max={20}
onChange={(value: number) => setRatio(value)}
value={ratio}
step={0.1} />
</Col>
<Col span={8}>
<InputNumber
min={1}
max={20}
style={
{ margin: '0 16px' }}
step={0.1}
value={ratio}
onChange={(value: number | null) => setRatio(value || 1)}
/>
</Col>
クリックを追跡する
トラックをクリックした後、対応するクリック時間を計算し、currentTime をリセットする必要があります。
まず、キャンバスを監視します
useEffect(() => {
if (waveCanvas === null) {
return
}
//设置canvas点击监听
waveCanvas.addEventListener("click", onCanavsClick);
return () => {
waveCanvas.removeEventListener("click", onCanavsClick);
}
}, [waveCanvas])
イベントクリックを聞いた後、対応する時間を直接計算して設定します。
const onCanavsClick = (event: MouseEvent) => {
const time = computeTimeFromEvent(event);
if (currentTime !== time) {
click?.(time);
}
};
オフセット モーメントは次の計算ステップです。
- それぞれが占めるピクセル サイズを計算します。
- 左側からクリック位置のオフセットサイズを計算します。
- オフセットに基づいて、タイムライン内の対応する瞬間を計算します。
const computeTimeFromEvent = (event: MouseEvent) => {
if (!waveCanvas || !$shwave.current) {
return 0
}
const { clientWidth: width } = waveCanvas;// canvas 实际宽度
const pixelRatio = window.devicePixelRatio; // 1
const length = getLength(duration); // 100
const gap = getGap(width, length); // 0.1 s 所占用的像素 宽度
// 偏移的宽度
const left = event.pageX - $shwave.current.offsetLeft / pixelRatio;
const begin = getBegin(currentTime, duration);
// left 在 时间中的位置
const time = clamp(
((left / gap) * pixelRatio) / 10 + begin,
begin,
begin + duration
);
return time;
}
プレーヤー
トラックが完了したら、ポイントのビデオ効果をリアルタイムで確認するためにビデオ プレーヤーを実装する必要があります。
ここでのプレーヤーはビデオ要素であり、トラックとプレーヤー間の連携を実現するには、いくつかのビデオ イベントを監視し、再生中に currentTime を設定する必要があります。
ビデオを設定する
まずインターフェイスにビデオ要素を配置します
const videoRef = useRef<HTMLVideoElement | null>(null)
<video controls width={400} height={300} src={url} ref={videoRef}></video>
次に、監視し、canplay
イベントに期間を設定し、ontimeupdate
に currentTime を設定します。
useEffect(() => {
if (!videoRef.current) {
return
}
videoRef.current.ontimeupdate = () => {
setCurrentTime?.(videoRef.current?.currentTime)
}
videoRef.current.addEventListener('canplay', () => {
setDuration?.(videoRef.current?.duration)
})
}, [url])
基本的なエフェクト表示
ビジュアルビデオ編集
ブラウザ上でffmpegが動作し、トラックとプレーヤーの連携が完了すると、ビジュアルビデオ編集機能を実現できます。
ビデオセグメンテーション
ffmpeg コマンドで編集機能を実装するには、開始点の時刻と終了点の時刻を指定する必要があります。
ffmpeg -ss 00:17:24 -to 02:19:31 -i inputVideo.mp4 -c:v copy -c:a copy outputVideo.mp4
したがって、完全なプロセスは次のようになります。
エフェクトのスクリーンショット
ビデオのスクリーンショット
指定した場所でビデオを再生した後、ffmpeg
コマンドを実行してビデオのスクリーンショットを撮ることができます。
ffmpeg -i ${input} -ss ${timer} -vframes 1 ${out}
エフェクトのスクリーンショット
その他の情報
アップロードしたファイルをローカルに保存する
ファイルのアップロード後にページを更新すると、アップロードしたファイルが失われるため、ローカル キャッシュを追加する必要があります。こうすることで、誤ってページを更新した後でも、以前にアップロードしたファイルを取得できます。記憶域スペースが十分に大きいため、ファイルの保存には IndexDb が選択されます。より一般的に使用される localStorage ストレージ スペースは比較的小さく、最大ストレージ スペースはわずか 5M です。
初期化
react プロジェクトには、直接使用できる既製のライブラリがいくつかあります。最も一般的に使用されるライブラリは dexie
です。まず、データベーステーブルを定義するために dexie を導入します。
// db.ts
import Dexie, { Table } from 'dexie';
export interface FileData {
id?: number;
name: string;
type: string;
data: File;
}
export class FileDexie extends Dexie {
files!: Table<FileData>;
constructor() {
super('myDatabase');
this.version(1).stores({
files: '++id, name, type, data' // Primary key and indexed props
});
}
}
export const dbFileDexie = new FileDexie();
アップロードしたファイルを保存する
indexDb の初期化が完了したら、ファイルのアップロードを開始し、アップロード コンポーネントの onChange
関数の db.files.put
関数を呼び出してファイルを保存します。インデックスデータベース。
const handleMediaChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
try {
const { name } = file;
db.files.put({ name, type: file.type, data: file })
} catch (error) {
console.error('handleMediaChange error', error)
}
};
}
<input type="file" onChange={handleMediaChange} />
使用する場合は、useLiveQuery を呼び出してクエリします。
import { useLiveQuery } from 'dexie-react-hooks';
import { dbFileDexie as db } from '@/db'
const mediaList = useLiveQuery(
() => db.files?.toArray?.()
);
本番環境の展開
ffmpeg はクロスソース分離に依存しているため、応答ヘッダーを構成する必要があり、github にデプロイすることはできません。そこで、別の無料展開プラットフォーム vercel を見つけました。欠点は、国内のインターネット アクセスが科学的である必要があることです。 vercel のデプロイメントに関するチュートリアルは数多くあるため、詳細については説明しません。 vercel.json
を共有するだけで、応答ヘッダーの設定と履歴ルーティングのサポートが実装されます。
{
"rewrites": [
{ "source": "/(.*)", "destination": "/index.html" }
],
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
{ "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" }
]
}
]
}
全て
これを実現するために、実際にデモを完成させたところです。でも、まだまだできることはたくさんあると感じています。このプロジェクトをさらに改善すれば、将来の仕事や生活に利便性をもたらし、役立つツールになる可能性があります。
- スタイルの最適化
- オーディオの視覚化、オーディオクリップのインターセプト
- 試験材料の生成
- 字幕機能対応(分離、追加)
- ffmpeg を自分でコンパイルし、パッケージ サイズを削減し、SharedArrayBuffer への依存関係を削除します。
参考
フロントエンドに FFmpeg? おそらくまだ準備ができていません
ffmpeg.wasm 純粋なフロントエンドを使用してマルチオーディオとビデオの合成を実現する
フロントエンドビデオフレーム抽出 ffmpeg + Webassembly
プロジェクトリファレンス
原文 ffmpeg + Canvas を使用してオンラインのビジュアルオーディオおよびビデオ編集ツールを作成する方法
★記事末尾の名刺では、(FFmpeg、webRTC、rtmp、hls、rtsp、ffplay、srs)を含むオーディオおよびビデオ開発学習教材やオーディオおよびビデオ学習ロードマップなどを無料で受け取ることができます。
以下を参照してください! ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓