⚛️

Electronを使ってFFmpegのGUIを作った話

2024/12/28に公開

この記事はmast Advent Calender 2024の15日目の記事です。

https://adventar.org/calendars/10425

14日目の記事はUmi Onoderaさんの「???」(記事執筆時点[1]ではおそらく未公開)でした。

はじめまして、mast23のhuntです。
知人[2]からアドカレを勧められたので、FFmpegのGUIを開発したことについて書こうと思います。
普段は映像制作をしている人間なので、自分が使いやすいかどうかにかなり主眼を置いて開発しました。
今回初めて触れた[3]言語などを使用して開発したため、文中のコードが汚いことなどがあるかもしれませんが、温かい目で見守ってください。

開発したソフトウェア

動画のコーデックやそのオプション、拡張子を変更するソフトウェアです。

主な機能

  • 複数ファイルの選択・エンコード
  • コーデックの選択
  • コーデックごとのオプションの指定
  • コンテナフォーマット / 拡張子の指定
  • ファイル名へのサフィックス[4]の追加
  • 出力先フォルダの指定
  • 実行中のログの表示する

便利さへの意識

ただ機能を実装するだけではなく、実際にソフトウェアを使う身として、こうであってほしい!という部分については実装コストと天秤にかけながらもできる限り実装しています。

例えば、コーデックを変換するためだけのソフトウェアなのに全画面を占有されては困るため、ウィンドウが一般的なフルHDディスプレイの画面の半分以下[5]に収まるようにしました。

他にも、コーデックを変更した際に他のオプションの選択をリセットする機能を実装したり、エンコードまでの一連の流れを行う際、マウスのカーソルの動きが自然になる[6]よう、UIの配置に気を配ったりしています。

実装

技術スタックは以下の通りです。

  • 言語:TypeScript
  • フレームワーク:Electron
  • フロントエンドライブラリ:React, Material-UI
  • その他のライブラリ:ffmpeg-static, fluent-ffmpeg

ここから、ElectronやReactに初めて触れた人間の視点で、実装にあたって苦労した点などについて書いていきます。

環境構築

いきなり初心者らしい項目ですが、やっぱり初めて触る人間には大変でした。Electronの環境構築はWebアプリの環境構築とよく似ている(と思っている)のですが、Webアプリを開発した経験もなかったため、package.jsonが何たるかすら知らない状態からいきなり環境構築するのはかなり時間を要しました。

紆余曲折あって[7]最終的にはelectron-viteというボイラープレートをもとに環境構築しました。

https://github.com/alex8088/electron-vite

Reactのカスタムフック

コーデックなどのオプションについては、最終的に丸ごとオブジェクトとして定義し、それらをuseStateで管理し、状態を更新する関数をカスタムフックとして実装しました。

import {useState} from 'react'

interface EncodeOptions {
    videoCodec:string;
    codecOption:string[];
    containerFormat:string;
    suffix: string;
    outputFolder:string;
}

interface UseEncodeOptionsReturn {
    encodeOptions: EncodeOptions;
    setVideoCodec: (videocodec: string) => void;
    setCodecOption: (codecOption: string[]) => void;
    setContainerFormat: (containerFormat: string) => void;
    setSuffix: (suffix: string) => void;
    setOutputFolder: (outputFolder: string) => void;
}

const initialState: EncodeOptions = {
    videoCodec: '',
    codecOption: [],
    containerFormat: '',
    suffix: '',
    outputFolder: ''
};

export const useEncodeOptions = (): UseEncodeOptionsReturn => {
    const [encodeOptions, setEncodeOptions] = useState<EncodeOptions>(initialState);
    const setVideoCodec = (videoCodec: string) => {
        setEncodeOptions(prev => ({ ...prev, videoCodec, codecOption:[], containerFormat: '', pixelFormat:'', suffix:'' }));
    };
    const setCodecOption = (codecOption: string[]) => {
        setEncodeOptions(prev => ({ ...prev, codecOption }));
    };
    const setContainerFormat = (containerFormat: string) => {
        setEncodeOptions(prev => ({ ...prev, containerFormat }));
    };
    const setSuffix = (suffix: string) => {
        setEncodeOptions(prev => ({ ...prev, suffix }));
    };
    const setOutputFolder = (outputFolder: string) => {
        setEncodeOptions(prev => ({ ...prev, outputFolder }));
    };
    return {
        encodeOptions,
        setVideoCodec,
        setCodecOption,
        setContainerFormat,
        setSuffix,
        setOutputFolder
    }
}

カスタムフックについては理解すれば実装自体はそう難しくなかったのですが、そこに行きつくまでが大変でした。

そもそも当初はレンダラープロセスは一つのファイル内にすべてが記述されており、これをコンポーネントに切り分けていく作業が後から発生したのですが、この時にとんでもない量のpropsを指定し、各コンポーネントで毎回状態管理のための関数を定義していたため、機能の拡張のためにはこれをどうにかする必要がありました。

そこで、プログラミングにおけるデザインパターンというものを知り、そこでReactにおけるカスタムフックの使用というものを知ったことで、やっと汚く整備しづらいコードを整えることができました。[8]

コーデックごとのオプション指定

オプションの選択肢はどのコーデックを選択するかによって異なるため、コーデックとオプションの対応をとるためのオブジェクトを別のファイルで定義し[9]、それを参照して適切なオプションを表示する、という形で実装しました。

コーデックごとに指定できるオプションは全く異なるため、実装する際にどんなオプションがあるか調べたのですが、これが本当に大変でした。

例えば、最もよく使われているであろうH.264のエンコードを行うlibx264というエンコーダは公式のドキュメントが存在しません。すべてのオプションとその値を知る手段はx264をインストールし、x264 -fullhelpすることなのですが、ことごとく情報が古く、結局有志がブログに上げている-fullhelpの実行結果を見て何とかしました。

また、DNxHRという映像制作で使われるコーデックがあるのですが、このコーデックを指定するにはFFmpegではDNxHDというコーデックを指定したうえでコーデックごとのオプションで-profileの値を2~5に設定する必要があります。
あまりにもユーザーに優しくない。

他にも、すべてのコーデックで使用できる-pix_fmtというピクセルフォーマット[10]を指定するオプションがあるのですが、これは一部のコーデックでは-profileの値によって指定できるピクセルフォーマットが変わるため、これを個別のSelectとして実装しようとするとコーデックとオプションを両方参照する必要があり非常に複雑になります。
最終的に-pix_fmtはコーデックごとのオプションを指定するときにしれっと追加するよう実装しました。

interface codecOptionList {
    codec: codecOption[];
}
interface codecOption {
    label: string;
    option: string[];
}

const codecOptionList = {
    //他のコーデックのオプションは省略
    dnxhd: [
        {label: "DNxHR 444 with alpha", option: ["-profile 5", "-pix_fmt yuva444p10le"] }, 
        {label: "DNxHR HQX 10bit", option: ["-profile 4", "-pix_fmt yuv422p10le"]}, 
        {label:"DNxHR HQ 8bit", option: ["-profile 3", "-pix_fmt yuv422p"]}, 
        {label: "DNxHR SQ", option: ["-profile 2"]}
    ],
}

export default codecOptionList

おわりに

今回初めてソフトウェアを開発したのですが、初めての割には使いやすいものができて、かなり満足しています。現状Windows向けにしかビルドできていませんが、気が向いたら公開する予定です。動画をよく扱う方はぜひ使ってみてください。

また、このソフトウェアはエンコードを停止する機能がなかったり、各種オプションをテンプレートとして保存する機能がなかったりするため、今後そういった機能を追加するほか、今回諦めたコーデックごとの詳細なオプションを別ウィンドウで指定するといった機能も気が向いたら実装してみようと思っています。
もし実装したらまた記事でも書こうと思いますが、期待はしないでください。

こういったテック系の記事を初めて書いたため読みづらい部分もあったかもしれませんが、最後までお読みいただきありがとうございました。

脚注
  1. かく言う本記事は2024/12/28に書かれています。人のふり見てなんとやら。 ↩︎

  2. 奇しくも(?)Ryoga.exeさんに記事を書くことを勧めた人と同じです。 ↩︎

  3. そもそもプログラミング自体大学から始めたため、プログラミングそのものに対する経験も浅いです。 ↩︎

  4. ファイル名の後ろに追加する文字列のことです。ファイル名の重複を避けるために使います。 ↩︎

  5. 正確にはデフォルトのウィンドウサイズが900px*700px、最小サイズが700px*700pxに設定されています。 ↩︎

  6. ファイル選択からエンコードまでを行う際、マウスの動きは左上から右下への移動となり、またウィンドウサイズが小さいこともあって、無駄な動きがほとんどありません。 ↩︎

  7. 何を思ったかDockerで環境構築しようとしたり、WSL上でやろうとしたりしましたが、ポートフォワーディングに敗北したり、アプリをWindows向けにビルドできなかったりとそれはもう大変だったため、結局Windows環境に落ち着きました。 ↩︎

  8. 自分が書いためちゃくちゃなコードを書き直すのにはそれなりの時間を要したうえかなり虚無な時間でしたが、やってよかったです。 ↩︎

  9. コーデックとオプションの対応などは別ファイルでオブジェクトとして定義したのですが、jsonとかを使った方がよかったのかもしれません。今回は学習するものを増やしたくなかったので敬遠しました。 ↩︎

  10. 適切なものを選択することで、アルファチャンネルに対応するか、また8bitにするか10bitにするかなどが指定できます。 ↩︎

Discussion