single.php

Chrome 拡張機能のNative MessageでFFmpegを使ってみる

Google Chromeの拡張機能の Native Message で、FFmpeg を使って.tsファイルから.mp4 に変換する方法を備忘録的に投稿しておきます。

Native Messageで外部プログラムを実行

Chrome 拡張機能では外部のプログラムを実行できる Native Message 機能を利用が利用できます。

先回、C#のコンソールアプリを自作して Chrome拡張機能のアイコンをクリックしたイベントで実行させるための手順を紹介していきます。

詳しい内容は別記事をご覧ください。

C# コンソールアプリでFFmpeg変換

FFmpegがインストールされている前提ですが、C#のコンソールアプリを作成して.ts 形式から .mp4 に変換してみます。

Chrome 拡張機能からNative Messageで動作させるので、”start”、”segment”、”end” に分けて動作するようにしています。

using System.Diagnostics;
using System.Text;
using System.Text.Json;

class Program
{
    static async Task Main()
    {
        string? _outputpath = "";
        Console.Error.WriteLine("Native host started");

        while (true)
        {
            var msg = await ReadMessageAsync();
            if (msg == null) break;

            var doc = JsonDocument.Parse(msg);
            var root = doc.RootElement;

            var action = root.GetProperty("action").GetString();

            if (action == "start")
            {
                _outputpath = root.GetProperty("data").GetString();
                if(_outputpath == null)
                {
                    Console.Error.WriteLine("Output path is null");
                    continue;
                }
                _ = StartFfmpegAsync(_outputpath);
            }
            else if (action == "segment")
            {
                string? base64 = root.GetProperty("data").GetString();
                if(base64 == null)
                {
                    Console.Error.WriteLine("Data is null");
                    continue;
                }
                var bytes = Convert.FromBase64String(base64);
    
                if(_ffmpegStdin == null)
                {
                    Console.Error.WriteLine("FFmpeg is not started");
                    continue;
                }
                await _ffmpegStdin.WriteAsync(bytes, 0, bytes.Length);
            }
            else if (action == "end")
            {
                if(_ffmpegStdin != null)
                {
                    await _ffmpegStdin.FlushAsync();
                    _ffmpegStdin.Close();
                }

                if(_ffmpegProcess != null)
                {
                    _ffmpegProcess.WaitForExit();
                }

                Console.Error.WriteLine("FFmpeg finished");

                if (_outputpath != null)
                {
                    Process.Start(new ProcessStartInfo
                    {
                        FileName = "chrome.exe",
                        Arguments = _outputpath,
                        UseShellExecute = true
                    });
                }
            }
        }
    }

    static Process? _ffmpegProcess;
    static Stream? _ffmpegStdin;

    static async Task StartFfmpegAsync(string outputPath)
    {
        Console.Error.WriteLine("Starting FFmpeg...");

        _ffmpegProcess = new Process();
        _ffmpegProcess.StartInfo.FileName = "ffmpeg";
        _ffmpegProcess.StartInfo.Arguments =
            $"-y -f mpegts -i pipe:0 -c copy \"{outputPath}\"";
        _ffmpegProcess.StartInfo.UseShellExecute = false;
        _ffmpegProcess.StartInfo.RedirectStandardInput = true;
        _ffmpegProcess.StartInfo.RedirectStandardError = true;
        _ffmpegProcess.StartInfo.CreateNoWindow = true;

        _ffmpegProcess.Start();

        _ffmpegStdin = _ffmpegProcess.StandardInput.BaseStream;

        _ = Task.Run(async () =>
        {
            while (!_ffmpegProcess.StandardError.EndOfStream)
            {
                var line = await _ffmpegProcess.StandardError.ReadLineAsync();
                Console.Error.WriteLine(line);
            }
        });
    }

    static async Task<string?> ReadMessageAsync()
    {
        var stdin = Console.OpenStandardInput();

        byte[] lengthBytes = new byte[4];
        int read = await stdin.ReadAsync(lengthBytes, 0, 4);
        if (read == 0) return null;

        int length = BitConverter.ToInt32(lengthBytes, 0);

        byte[] buffer = new byte[length];
        int offset = 0;

        while (offset < length)
        {
            int n = await stdin.ReadAsync(buffer, offset, length - offset);
            offset += n;
        }

        return Encoding.UTF8.GetString(buffer);
    }

}

Chrome拡張機能側では、こんな感じで呼び出します。

let port = null;

chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {

  function ensurePort() {
    if (!port) {
      port = chrome.runtime.connectNative("native.message.convffmpeg");

      port.onDisconnect.addListener(() => {
        console.log("Native disconnected");
        port = null;
      });
    }
  }

  function sendToNative(action, data) {
    ensurePort();

    port.postMessage({
      action,
      data
    });
  }  

  console.log("action:" + msg.action + ", data:" + msg.data);

  if (msg.action === "NM_OPEN") {
    sendToNative("start", msg.data);
    return true;
  }

  if (msg.action === "NM_CLOSE") {
    sendToNative("end", "");
    return true;
  }

  if (msg.action === "NM_CHUNK") {
    sendToNative("segment", msg.data);
    return true;
  }
});

chrome.scripting.executeScript({

  chrome.runtime.sendMessage({
    action: "NM_OPEN",
    data: "<保存する場所>(例:C:\\Data\\sample.mp4)"
  });

  // .tsファイルの取得処理
  const res = await fetch(url, { signal: controller.signal });
  let buffer = await res.arrayBuffer();

  const uint8 = new Uint8Array(buffer);

  chrome.runtime.sendMessage({
    action: "NM_CHUNK",
    data: uint8.toBase64()
  });

  chrome.runtime.sendMessage({
    action: "NM_CLOSE",
    data: ""
  });


});

実行すると、URLから取得した .ts 形式のデータを、C#のコンソールアプリに受け渡して.mp4 形式に変換されます。

Google Chromeから実行する際に[Invalid native messaging host name specified]エラーになる場合には、別記事をご覧ください。

まとめ

今回は、Google Chromeの拡張機能の Native Message で、FFmpeg を使って.tsファイルから.mp4 に変換する方法について紹介しました。

NativeMessageを利用するとC#などで作成した別アプリケーションをChrome拡張機能からデータの受け渡しが可能です。

また、Chrome拡張機能のJavsScriptでは実行できない機能でも、C#などで作成したアプリで代用が可能になります。

Google Chrome 拡張機能で、FFmpegなどを実行したい人の参考になれば幸いです。

スポンサーリンク

最後までご覧いただきありがとうございます。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です