ffmpeg + Canvas を使用してオンラインのビジュアルオーディオおよびビデオ編集ツールを作成する方法

概要

この記事では、ffmpeg + Canvas を使用してオンラインのビジュアル オーディオおよびビデオ編集ツールを作成する方法を紹介します。

  • ブラウザで ffmpeg を実行してオーディオとビデオを処理する方法。
  • キャンバスを使用してビデオ トラックを実装する方法。
  • トラック、プレーヤー、ffmpeg を組み合わせてビデオ編集の視覚化を実現する方法。

キーワード

ffmpeg、ビデオ、トラック、視覚化、キャンバス

エフェクトのスクリーンショット

体験アドレス

videoCut(vercel で展開、科学的インターネット アクセスが必要)

コードリポジトリ

img-generate (クリックしました。無料の星をクリックしてください)

プロジェクトの背景

プロジェクトを立ち上げた理由

仕事や生活の中で、次のようなオーディオやビデオ関連の問題に遭遇することがよくあります。

  • ビデオから写真をキャプチャしたい。
  • 長いビデオから特定のセグメントをキャプチャしたい。
  • フォーマット変換、ビデオからオーディオを抽出。
  • フレームレートなどのビデオ情報を取得します。

ローカルで分析する場合は、まず環境を構成して ffmpeg などの分析ツールをインストールする必要があり、より多くの準備作業が必要になります。 wasm テクノロジーを使用し、ブラウザ上で ffmpeg を直接実行する場合は、ローカルにインストールしてダウンロードする必要がなく、初心者ユーザーにとってより使いやすくなります。

自分で試してみましょう - ブラウザで ffmpeg を使用してください

@ffmpeg/ffmpeg

npmアドレス

これはコンパイルされたパッケージです。欠点は、サイズが比較的大きく、初めてロードするときに数秒待つ必要があることですが、利点は直接使用できることです。ここではこのパッケージを直接参照し、後で最適化が必要になった場合は、自分でコンパイルするか、より適切なパッケージを探すことを検討します。

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

プロジェクトリファレンス

github.com/cs8425/ffmp…

github.com/xiguaxigua/…

github.com/Shirtiny/sh…

原文 ffmpeg + Canvas を使用してオンラインのビジュアルオーディオおよびビデオ編集ツールを作成する方法

★記事末尾の名刺では、(FFmpeg、webRTC、rtmp、hls、rtsp、ffplay、srs)を含むオーディオおよびビデオ開発学習教材やオーディオおよびビデオ学習ロードマップなどを無料で受け取ることができます。

以下を参照してください! ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

 

おすすめ

転載: blog.csdn.net/yinshipin007/article/details/134619447