MTSファイルをMP4に変換したい

仕事で SD カードに保存した MTS ファイルを扱う機会が増えました。使い終わったら不要なファイルは即消去しますが、後日再び利用するものは、わりと自由に使える NAS に MTS 形式のままコピー(=保存)していた ・・・ のですが、さすがに数が増えてくると( このままでいいのかなー )みたいな気が。

ファイルサイズが 10 GB を超えてくると、SD カードから NAS へコピーするにしても時間がかかるし、再利用する際に使うのはノート PC なので、ファイル容量に見合うほど高画質でなくても構わないはずですし、それより何より、休日、何もすることがなくてヒマ なので、MTS 形式の動画ファイルを より容量の小さい MP4 形式に変換するプログラムを書いてみることにしました。

てか、何よりも、ほんとはずっと、前から、やってみたかった・・・こと。なので・・・ *(^_^)*♪

動作には別途 FFmpeg.exe が必要です。
( FFmpeg.exe は MTStoMP4.zip に同梱しておりません)

【もくじ】

1.MTS って何?
2.MP4 に変換
3.動作確認用のコード
4.プログレスバーも表示
5.文字列の一部を省略(…)して表示
6.プログラムのダウンロード
7.まとめ
8.お願いとお断り

1.MTS って何?

まずは、ここから勉強します。

ソニー・パナソニックが共同開発した高画質動画を効率よく記録するための仕様がAVCHD(Advanced Video Codec High Definition)で、この方式で記録された動画ファイルの実体が MTS ファイルなんだそうです。

MTS は、MPEG Transport Stream の略で、主にビデオカメラで録画した高画質動画を保存するためのファイルとして利用されており、このファイルの映像部分で使用される圧縮方式(コーデック)が高画質かつ高圧縮の H.264 であるとのこと。

一言で言うと、MTS は、H.264で圧縮された動画を保存する「入れ物(ファイル形式)」のひとつで、映像の他に音声や字幕などの情報も一緒に保存されているファイルコンテナ。

ファイルコンテナと言えば思い浮かぶのは、JR の貨物列車です。

MTS や MP4 の詳しい仕組みについては、まったくわかりませんが、貨物列車に様々な色や形のコンテナが積載されているように、映像や音声を各ファイルそれぞれの方法で乗っけていることだけは理解できます。その載せ方の工夫次第で、貨物の重さや列車の長さが変わってくるということなのでしょう。

2.MP4 に変換

もちろん、わざわざ自分でプログラムなんて書かなくても、MTS ファイルを MP4 ファイルに変換する方法はいくらでもあります。有名なところでは、無料で使える「HandBrake」がありますし、さらに身近なところでは、Windows10 / 11のフォトでも変換できるようです。

僕は HandBrake は実際に使ったことがありますが、フォトでの変換は試したことがありません。

今回やってみたかったのは、これまた有名な「 FFmpeg 」(動画処理のツール)を使ったファイルコンテナの変換プログラムの作成です。前にも書きましたが、どうせヒマだし、FFmpeg は以前にもいろんなところで使ったことがあって、「期待通りに動作した記憶しかない」ので、今回もきっとうまく行く♪と思えたことと、それより何より Delphi で「なんかしてないと落ち着かない」のです(=これはきっと、僕の心の病です)。

3.動作確認用のコード

最初の一歩は、FFmpeg のダウンロードと準備。

ダウンロードサイト : https://ffmpeg.org/download.html

上記リンク先の「Get packages & executable files」にある Windows のマーク上をポイント(or クリック)すると表示される「Windows builds from gyan.dev」のリンク先ページからダウンロードすればよいのですが、いろいろな FFmpeg があって迷いました。

まず、「git master builds」と「release builds」いずれを選択すればいいのか?

今回の使用目的は、最新の機能のテストとか、そんなんじゃなくて、とにかく安定して動作するバージョンが欲しいので、「release builds」の方を選択。

で、latest release を見ると、選択肢が4つ。

・ffmpeg-release-essentials.7z
・ffmpeg-release-essentials.zip
・ffmpeg-release-full.7z
・ffmpeg-release-full-shared.7z

Essentials は、Win7 以降の OS に対応した最小限の機能のみを搭載した軽量な FFmpeg で、Full は 全機能搭載のWin10 以降用、Full Shared は、Full の DLL 版とのこと。

ここで重要になってくるのがライセンスです。

FFmpeg は、ビルド種別によりそのライセンスが異なります。最もライセンス的に無難な選択は、LGPL v2.1+ が適用される「Release Essentials Build(LGPLビルド)」だと思います。

LGPL v2.1+は、「 FFmpeg を改変せずにそのまま使い、アプリとは動的リンク( exe を呼び出す方式)で接続( = ユーザーが FFmpeg を差し替えられるように設定)し、FFmpeg のライセンス表記を Readme.txt 等に表示」すれば商用利用も可能で、クローズドソースでも OK というライセンス形態なので、今回作成したいプログラムでは、勉強を兼ね、公開に耐えうる仕様とするため「ffmpeg-release-essentials.zip」をダウンロードして、アプリケーションの exe と同じ場所にffmpeg という名前のフォルダを作成し、zip ファイルを展開した内容を一式コピペして、プログラムから FFmpeg.exe を直接呼び出して利用したいと思います。

具体的なフォルダとファイルの構成(位置関係)は、次の通りです。

MTStoMP4\
 ├ Dst
 ├ FFmpeg\bin\ffmpeg.exe
 ├ Src
 ├ Readme.txt
 └ MTStoMP4.exe

早速、次の GUI を Delphi で作成しました。

「テスト」ボタンは動作確認用(動作確認後に削除する予定)。


実際には「変換実行」ボタンをクリックするとプログレスバーを表示して変換作業の進捗状況を可視化する予定なのですが、そこに行きつく前に FFmpeg の動画変換機能を使えるようにならないといけません。なので、取り敢えず、「テスト」ボタンを準備し、そのクリックイベントの中で、コマンドプロンプトを表示して変換の動作確認を行えるようなテスト用のプログラムを書いてみます。

まず、変換元のファイルを選択する部分(ファイル選択ボタンをクリックした場合)の手続きの作成から始めました。

Form 上に TOpenDialog をひとつ準備して、次のコードを書きます。変換元の MTS 形式の動画ファイルは、exe と同じ場所に Src という名前のフォルダを作成して、そこに保存しておく前提です。また、変換先のファイルは、こちらも exe と同じ場所に Dst という名前のフォルダを作成し、そちらへ拡張子に mp4 を指定して書き出すよう、予め、変換先ファイルパスとして準備( Label のキャプションとして表示)しておきます。このように設定したのは、FFmpeg は変換先ファイルの拡張子を見て自動的に出力フォーマットを判別する仕様だからです。

procedure TForm1.Button1Click(Sender: TObject);
begin
  OpenDialog1.Filter := '動画ファイル (*.MTS;*.mp4;*.avi;*.mkv)|*.MTS;*.mp4;*.avi;*.mkv|すべてのファイル (*.*)|*.*';
  OpenDialog1.Title := '動画ファイルを選択してください';
  OpenDialog1.InitialDir:=ExtractFilePath(Application.ExeName)+'Src';
  if OpenDialog1.Execute then
  begin
    Edit1.Text:=OpenDialog1.FileName;
    Label1.Caption:=ExtractFilePath(Application.ExeName)+
      'Dst\'+ChangeFileExt(ExtractFileName(OpenDialog1.FileName), '')+'.mp4';
  end;
end;

上記コードの動作を確認します。実行時の画面は次の通りです。

思った通りに動作しました☆
まぁ、ここはそんなに難しいところではありませんが、「幸先よし」と感じます。


ただ、ちょっと気になったのが変換元ファイルの Path 文字列が長くて TEdit からはみ出している部分です。ここは後からなんとかしたいと思います。

変換に際して指定できるパラメータは3つです。

1つめが CRF 値です。CRF は Constant Rate Factor の略で、これは動画の品質を一定に保ちつつ、ファイルサイズを自動的に調整するために設定するパラメータで、0 ~ 51 までの数値で指定します。数値が小さいほど高画質ですがファイルサイズも大きくなり、数値が大きいほど低画質になりますがファイルサイズは小さくなります。デフォルトで使用する値は 23 のようです。

2つめがプリセット指定で、これは FFmpeg の H.264( libx264 )エンコーダーで使われる「圧縮処理の速度と効率のバランス」を設定するパラメータです。エンコードの速度(=処理時間)と圧縮効率(=ファイルサイズ)のトレードオフを制御します。

ultrafast → superfast → veryfast → faster → fast → medium(デフォルト) → slow → slower → veryslow → placebo の 10 段階の設定が可能で、より右側のパラメータほど処理速度が増加し、ファイルサイズは小さくなります(逆に言えば、左側のパラメータほど処理速度が速く、ファイルサイズは大きくなります)。すべてを試すヒマはないので、取りあえず medium で動作確認することにします。

3つめが AudioBitrate で、これは1秒あたりの音声のデータ量を指定する値です。もちろん、値が大きいほど音質が良くなりますが、ファイルサイズも大きくなります。単位は kbps(キロビット毎秒)です。

で、様々な問題点をクリアしながら最終的に完成したのが次のコードです。動作状況の確認が目的なので、ShellExecute 関数の引数には /K を指定してコマンドプロンプトが自動で閉じないようにしています。また、上記3つのパラメータはわかりやすさを優先し、コード内で直接「値」を指定しています。

procedure TForm1.ButtonXClick(Sender: TObject);
var
  FFmpegPath, Command: string;
  AudioBitrate, VideoCRF: Integer;
  strPreset: string;
  InputFile, OutputFile: string;
begin
  //明示的にエスケープ('ffmpeg\bin\ffmpeg.exe' の中の \b が「バックスペース」として扱われる危険を排除)
  FFmpegPath :=
    IncludeTrailingPathDelimiter(ExtractFilePath(Application.ExeName)) + 'ffmpeg\\bin\\ffmpeg.exe';

  //もしくは PathDelim を使う
  //FFmpegPath := IncludeTrailingPathDelimiter(ExtractFilePath(Application.ExeName))
  //            + 'ffmpeg' + PathDelim + 'bin' + PathDelim + 'ffmpeg.exe';

  //ファイルパスを安全な形式(8.3形式)で取得
  InputFile := ExtractShortPathName(Edit1.Text);
  //ExtractShortPathName関数は存在しないファイルを指定すると空文字列を返すことに注意する。
  //変換先の mp4形式の動画ファイルはプログラムの実行後に生成され、実行時には存在しない!
  OutputFile := Label1.Caption;

  //CRF(0~51)
  VideoCRF := 23;

  //プリセット(ultrafast, superfast, medium, slow, veryslow など)
  strPreset := 'slow';

  //数値の変数(単位はkbps)
  AudioBitrate := 192;

  //-ac 2 を追加して、5.1ch → 2ch ステレオ に変換して出力
  //5.1ch(サラウンド)をうまく処理できない場合があるようです。
  //この場合、変換された mp4ファイルが無音になってしまいます(ハマりました)。
  Command := Format(
    '"%s" -i "%s" -map 0:v -map 0:a -vcodec libx264 -acodec aac -ac 2 -b:a %dk -crf %d -preset %s -y "%s"',
    [FFmpegPath, InputFile, AudioBitrate, VideoCRF, strPreset, OutputFile]
  );

  //コマンドはダブルクォートで囲む(コマンド全体を1つの文字列として渡す)
  ShellExecute(0, 'open', 'cmd.exe', PChar('/K "' + Command + '"'), nil, SW_SHOWNORMAL);

end;

特に、最後の ShellExecute 関数で、Command 部分をダブルクォートで囲む処理を忘れると・・・

My PC 環境では、上のようなエラーが発生します。


原因がわかってしまえば( なぁーんだ )みたいな問題ですが、(私は)なかなか原因がわからなくて、解決までにちょっと時間を要しました。Command 部分をダブルクォートで囲むのを忘れてもコンパイルは通るので、ここはコーディング上の要注意部分です。

また、実行パスに全角文字が含まれている場合でも動作することを確認しましたが、より安定した動作を実現するためには CreateProcess を使って直接実行した方が良いはずです。なので、本番の処理では CreateProcess を使う方法をとることにします(加えて、FFmpeg の処理の進捗状況をプログレスバーに表示する処理も実装しなければいけません)。

CreateProcess を使った場合の、単なる動作確認用コードは、次の通りです。

procedure TForm1.ButtonXXClick(Sender: TObject);
var
  FFmpegPath, CmdLine, InputFile, OutputFile: string;
  AudioBitrate, VideoCRF: Integer;
  strPreset: string;
  StartInfo: TStartupInfo;
  ProcInfo: TProcessInformation;
begin
  //明示的にエスケープ('ffmpeg\bin\ffmpeg.exe' の中の \b が「バックスペース」として扱われる危険を排除)
  FFmpegPath := IncludeTrailingPathDelimiter(ExtractFilePath(Application.ExeName)) + 'ffmpeg\\bin\\ffmpeg.exe';

  //入力・出力ファイル
  InputFile := ExtractShortPathName(Edit1.Text);
  OutputFile := Label1.Caption;

  if (InputFile = '') or (OutputFile = '') then
  begin
    ShowMessage('入力または出力ファイルのパスが無効です');
    Edit1.SetFocus;
    Exit;
  end;

  //エンコード設定
  VideoCRF := 23;
  strPreset := 'slow';
  AudioBitrate := 192;

  //コマンドライン
  CmdLine := Format(
    '"%s" -i "%s" -map 0:v -map 0:a -vcodec libx264 -acodec aac -ac 2 -b:a %dk -crf %d -preset %s -y "%s"',
    [FFmpegPath, InputFile, AudioBitrate, VideoCRF, strPreset, OutputFile]
  );

  ZeroMemory(@StartInfo, SizeOf(StartInfo));
  StartInfo.cb := SizeOf(StartInfo);
  StartInfo.dwFlags := STARTF_USESHOWWINDOW;
  StartInfo.wShowWindow := SW_SHOW;  // 非表示にするなら SW_HIDE

  ZeroMemory(@ProcInfo, SizeOf(ProcInfo));

  if not CreateProcess(
    nil,               //アプリケーション名(CmdLine 内に含まれるので nil)
    PChar(CmdLine),    //コマンドライン(実行ファイルと引数を含む)
    nil, nil,          //セキュリティ属性
    False,             //ハンドル継承
    CREATE_NEW_CONSOLE,  //新しいコンソールで実行
    nil,               //環境変数
    nil,               //カレントディレクトリ
    StartInfo,         //スタートアップ情報
    ProcInfo           //プロセス情報(プロセスIDなど)
  ) then
  begin
    ShowMessage('CreateProcess に失敗しました: ' + SysErrorMessage(GetLastError));
    Exit;
  end;

  //処理の終了まで待ってから後始末&その他の処理を実行する場合は有効化する
  //ただし、有効化すると、タイトルバーに「応答なし」と表示されるなど動作が重くなる気が。
  //FFmpegに処理を渡すだけなら待機不要とした方が軽快動作?
  //WaitForSingleObject(ProcInfo.hProcess, INFINITE);

  //後始末
  CloseHandle(ProcInfo.hProcess);
  CloseHandle(ProcInfo.hThread);
end;

【ご注意願います】

もくじの「5.文字列の一部を省略(…)して表示」の処理を実行(設定)した場合は、Edit1.Text や Label1.Caption の値を参照せず、グローバル変数に保存した省略のない Path 文字列を参照するようにコードを修正する必要があります(参考コードは後述)。どうか、ご注意ください。

4.プログレスバーも表示

FFmpegは実行中に、標準出力(stdout)や標準エラー(stderr)にログを出力するので、このログを利用して処理の進捗状況(フレーム数、時間、速度など)等を取得することが可能です。

なので、Delphi で CreateProcess を利用して FFmpeg を起動する際に、標準出力・標準エラーをパイプで受け取るように設定すれば、ログをリアルタイムで取得でき、これに基づいてプログレスバーで処理の進捗状況を表示することができます。

PC に詳しい方なら次のような画面が表示され、より詳細な変換処理の進捗状況が見えた方が安心かもしれませんが、この背景が真っ黒な画面にあまり馴染みのない方にとっては、この画面よりもプログレスバーに進捗状況が表示されるという、より単純な GUI による表示の方が安心できるのではないでしょうか?(私は、本質的に難しいことが苦手なので、そのように感じてしまいます)

CreateProcess でファイルコンテナの変換を実行中
(StartInfo.wShowWindow := SW_SHOW;)


なので、動作確認後は StartupInfo.wShowWindow := SW_HIDE を指定し、コマンドプロンプト画面は非表示に設定、その代わりにプログレスバーを表示して、変換処理の進捗状況を表示します。

(変換処理の進捗状況を表示する方法は後述)

StatusBar に ProgressBar を埋め込む方法もありますが・・・
それはスペース的に余裕のない場合のお話。


今回の場合、「終了」ボタンと「変換実行」ボタンの間が空いていますので、ProgressBar はここに設置することにします。

さて、問題は進捗状況を表示する機能の実装です。

調べて見ると、FFmpeg は進行状況(Duration: …, time=…など)を 標準エラー(stderr)に出力する仕様のようでした。この進行状況の出力先が標準エラー(stderr)となっている理由は、 FFmpeg は「標準出力(stdout)」を、エンコード結果(映像などのバイナリ)をパイプ出力する用途にも使うため、ここにログを混ぜると混乱が生じる恐れがあり、ログ類は意図的にすべて stderr に分離して出力する仕様となっているとのことでした。

また、デフォルト設定のままログを出力すると多くの情報が入り混じって流れてくるので、経過時間等の取得したい情報が探しにくくなってしまいます。

そこで、出力されるログを行単位で処理し、進捗状況を表示するためのキーワードを正確に検出できるようにしました。

具体的には、FFmpeg に渡すコマンドラインの中で -progress pipe:1 を指定して意図的にログ出力が標準出力( stdout )へ為されるようにして、ここに key=value 形式で送られてくるログ出力中の「out_time=」という文字列を探して経過時間の情報を得ています。

上記内容を実装する具体的手順です。

まず、パラメータ設定を含めて FFmpeg に渡すコマンドラインを作成する部分です。

  FFmpegPath:=IncludeTrailingPathDelimiter(ExtractFilePath(Application.ExeName)) + 'ffmpeg\\bin\\ffmpeg.exe';

  //CRF(0~51)
  //VideoCRF:= 23;
  VideoCRF:=StrToInt(ComboBox1.Text);

  //プリセット
  //strPreset:= 'slow';
  strPreset:= ComboBox2.Text;

  //音声の処理
  //AudioBitrate:= 192;
  AudioBitrate:= StrToInt(ComboBox3.Text);

  //-ac 2 を追加して、5.1ch → 2ch ステレオ に変換して標準出力(stdout)に出力
  //InputFile, OutputFile はこの手続きを呼び出す際に指定
  CmdLine:= Format(
    '"%s" -i "%s" -map 0:v -map 0:a -vcodec libx264 -acodec aac -ac 2 -b:a %dk -crf %d -preset %s -y -progress pipe:1 "%s"',
    [FFmpegPath, InputFile, AudioBitrate, VideoCRF, strPreset, OutputFile]
  );


動作状況を確実に確認するため、Form に TMemo を1つ追加して、この TMemo にログ出力内容を表示してみます。次は、そのテストを行った際の画像です。

実際のプログラムでは TMemo への出力は行いませんが・・・。


ここで記録されたログの最初の方に MTS ファイルの再生(録画)時間が出力されています。実際に取得したログを下に示します。Duration 部分が再生(録画)時間です。

Input #0, mpegts, from 'C:\Users\XXX\Win32\Release\Src\SampleDoga.mts':  Duration: 00:18:58.21, start: 2165.015522, bitrate: 15843 kb/s

この再生(録画)時間の出力と out_time の値を利用して、プログレスバーに進捗状況を表示します。以下、プログレスバーに進捗状況を表示する部分のコードです。

  LogBuffer := '';
  DurationInSec := 0;

  repeat
    FillChar(Buffer, SizeOf(Buffer), 0);
    if ReadFile(StdOutRead, Buffer, SizeOf(Buffer) - 1, BytesRead, nil) and (BytesRead > 0) then
    begin
      LogBuffer := LogBuffer + string(Copy(Buffer, 0, BytesRead));

      //改行で分割して処理
      Lines := LogBuffer.Split([#10, #13], TStringSplitOptions.ExcludeEmpty);
      if Length(Lines) > 0 then
      begin
        for i := 0 to High(Lines) - 1 do
        begin
          Line := Trim(Lines[i]);

          //ログ出力内容を確認
          Memo1.Lines.Add(Line);

          if (DurationInSec = 0) and (Pos('Duration:', Line) > 0) then
          begin
            TotalDurationStr := Copy(Line, Pos('Duration:', Line) + 9, 12);
            DurationInSec := TimeStringToSeconds(Trim(TotalDurationStr));
          end;

          if Pos('out_time=', Line) > 0 then
          begin
            TimeStr := Copy(Line, Pos('out_time=', Line) + 9, 11);
            CurrentTimeInSec := TimeStringToSeconds(Trim(TimeStr));

            if DurationInSec > 0 then
            begin
              ProgressBar.Position := Min(100, Round((CurrentTimeInSec / DurationInSec) * 100));
              Application.ProcessMessages;
            end;
          end;
        end;
        LogBuffer := Lines[High(Lines)];
      end;
    end;
  until WaitForSingleObject(ProcessInfo.hProcess, 10) = WAIT_OBJECT_0;


上記コード内で、「時刻文字列を秒数 に変換」する TimeStringToSeconds 関数を呼び出していますが、この関数は以下のように、別に準備しておきます。

  function TimeStringToSeconds(const TimeStr: string): Double;
  var
    h, m, s: Integer;
    secFrac: Double;
    Parts: TArray<string>;
  begin
    Result := 0;
    Parts := TimeStr.Split([':']);
    if Length(Parts) < 3 then Exit;

    h := StrToIntDef(Parts[0], 0);
    m := StrToIntDef(Parts[1], 0);
    s := Trunc(StrToFloatDef(Parts[2], 0));
    secFrac := Frac(StrToFloatDef(Parts[2], 0));

    Result := h * 3600 + m * 60 + s + secFrac;
  end;


「変換実行」ボタンをクリックした際の手続き全体のコードです。

procedure TForm1.ButtonXClick(Sender: TObject);

  function TimeStringToSeconds(const TimeStr: string): Double;
  var
    h, m, s: Integer;
    secFrac: Double;
    Parts: TArray<string>;
  begin
    Result := 0;
    Parts := TimeStr.Split([':']);
    if Length(Parts) < 3 then Exit;

    h := StrToIntDef(Parts[0], 0);
    m := StrToIntDef(Parts[1], 0);
    s := Trunc(StrToFloatDef(Parts[2], 0));
    secFrac := Frac(StrToFloatDef(Parts[2], 0));

    Result := h * 3600 + m * 60 + s + secFrac;
  end;

  procedure RunFFmpegWithProgressBar(const InputFile, OutputFile: string; ProgressBar: TProgressBar);
  var
    SecurityAttr: TSecurityAttributes;
    StdOutRead, StdOutWrite: THandle;
    StartupInfo: TStartupInfo;
    ProcessInfo: TProcessInformation;
    Buffer: array[0..1023] of AnsiChar;
    LogBuffer: string;
    Lines: TArray<string>;
    Line: string;
    BytesRead: DWORD;
    DurationInSec, CurrentTimeInSec: Double;
    CmdLine: string;
    TotalDurationStr, TimeStr: string;
    FFmpegPath: string;
    AudioBitrate, VideoCRF: Integer;
    strPreset: string;
    i: Integer;
  begin

    //初期化
    ProgressBar.Min := 0;
    ProgressBar.Max := 100;
    ProgressBar.Position := 0;

    //パイプの準備
    SecurityAttr.nLength := SizeOf(SecurityAttr);
    SecurityAttr.bInheritHandle := True;
    SecurityAttr.lpSecurityDescriptor := nil;

    if not CreatePipe(StdOutRead, StdOutWrite, @SecurityAttr, 0) then
      RaiseLastOSError;

    ZeroMemory(@StartupInfo, SizeOf(StartupInfo));
    StartupInfo.cb := SizeOf(StartupInfo);
    StartupInfo.hStdError := StdOutWrite;
    StartupInfo.hStdOutput := StdOutWrite;
    StartupInfo.dwFlags := STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW;
    StartupInfo.wShowWindow := SW_HIDE;

    //Pathを設定
    FFmpegPath:=IncludeTrailingPathDelimiter(ExtractFilePath(Application.ExeName)) + 'ffmpeg\\bin\\ffmpeg.exe';

    //各パラメータの設定(値は参考)

    //VideoCRF:= 23;
    VideoCRF:=StrToInt(ComboBox1.Text);

    //プリセット(例: ultrafast, superfast, medium, slow, veryslow など)
    //strPreset:= 'slow';
    strPreset:= ComboBox2.Text;

    //AudioBitrate:= 192;
    AudioBitrate:= StrToInt(ComboBox3.Text);

    //-ac 2 を追加して、5.1ch → 2ch ステレオ に変換して出力(My環境ではこうしないと無音になる!)
    CmdLine:= Format(
      '"%s" -i "%s" -map 0:v -map 0:a -vcodec libx264 -acodec aac -ac 2 -b:a %dk -crf %d -preset %s -y -progress pipe:1 "%s"',
      [FFmpegPath, InputFile, AudioBitrate, VideoCRF, strPreset, OutputFile]
    );

    if not CreateProcess(nil, PChar(CmdLine), nil, nil, True,
      CREATE_NO_WINDOW, nil, nil, StartupInfo, ProcessInfo) then
    begin
      CloseHandle(StdOutRead);
      CloseHandle(StdOutWrite);
      RaiseLastOSError;
    end;

    CloseHandle(StdOutWrite);

    LogBuffer := '';
    DurationInSec := 0;

    repeat
      FillChar(Buffer, SizeOf(Buffer), 0);
      if ReadFile(StdOutRead, Buffer, SizeOf(Buffer) - 1, BytesRead, nil) and (BytesRead > 0) then
      begin
        LogBuffer := LogBuffer + string(Copy(Buffer, 0, BytesRead));

        //改行で分割して処理
        Lines := LogBuffer.Split([#10, #13], TStringSplitOptions.ExcludeEmpty);
        if Length(Lines) > 0 then
        begin
          for i := 0 to High(Lines) - 1 do
          begin
            Line := Trim(Lines[i]);

            //ログ出力内容を確認
            //Memo1.Lines.Add(Line);

            if (DurationInSec = 0) and (Pos('Duration:', Line) > 0) then
            begin
              TotalDurationStr := Copy(Line, Pos('Duration:', Line) + 9, 12);
              DurationInSec := TimeStringToSeconds(Trim(TotalDurationStr));
            end;

            if Pos('out_time=', Line) > 0 then
            begin
              TimeStr := Copy(Line, Pos('out_time=', Line) + 9, 11);
              CurrentTimeInSec := TimeStringToSeconds(Trim(TimeStr));

              if DurationInSec > 0 then
              begin
                ProgressBar.Position := Min(100, Round((CurrentTimeInSec / DurationInSec) * 100));
                Application.ProcessMessages;
              end;
            end;
          end;
          LogBuffer := Lines[High(Lines)];
        end;
      end;
    until WaitForSingleObject(ProcessInfo.hProcess, 10) = WAIT_OBJECT_0;

    CloseHandle(StdOutRead);
    CloseHandle(ProcessInfo.hProcess);
    CloseHandle(ProcessInfo.hThread);

  end;

begin

  //変換元ファイルの指定がない場合は、処理しない
  if Edit1.Text='' then
  begin
    Edit1.SetFocus;
    Exit;
  end;

  //プログレスバーを表示
  ProgressBar1.Visible:=True;
  try
    //MTS -> MP4変換
    RunFFmpegWithProgressBar(Edit1.Text, Label1.Caption, ProgressBar1);
  finally
    //非表示にする
    ProgressBar1.Visible:=False;
  end;

end;

私の手持ち機材で録画した MTS ファイルを MP4 ファイルへ変換する作業は、・・・ 安定動作するまでに様々な紆余曲折はありましたが、最終的に上のコードで問題なく動作するようになりました。・・・が、(日々進化する)使用機材とPC環境により、録画&録音の環境は、その利用者により当然異なると思います。

例えば、私の環境では、音声は 5.1ch → 2ch として「品質を低下」させないと生成された MP4 には音声が入らないというトラブル(?)がありました。

//-ac 2 を追加して、5.1ch → 2ch ステレオ に変換して出力
CmdLine:= Format(
      '"%s" -i "%s" -map 0:v -map 0:a -vcodec libx264 -acodec aac -ac 2 ・・・

ですので、上記コードがあらゆる録画&録音設定に対応できるものでは『ない』ことに十分ご留意いただけますよう、心からお願い申し上げます。万一、上記コードを流用される場合、環境によっては、様々な不具合が生じることが予想されます。その場合は、利用者各自の責任でコードに適切な修正または改良を加えていただけますよう、お願い申し上げます。

5.文字列の一部を省略(…)して表示

Delphiでは(他の言語についてはさらに知りませんが)、TEditやTLabelに長い文字列を表示した時、コントロールの幅より文字列の長さが長いと文字列の後半が切れて表示されてしまいます。そうならないように自動的に長い文字列の中央よりの一部を … のように省略して表示する機能はデフォルトの状態では準備されていないようです(間違っていたらすみません)。

この機能を実装してみました。設定可能なコントロールは TEdit と TLabel です。コンポーネント化する方法もあるかと思いますが、より簡単に、関数として実装しました。

最初に、非 Path 文字列用の場合です。

文字列の中央部分を省略して表示します。


次に、フォルダ名部分はなるべく残す Path 文字列専用バージョンです。

Path 文字列の先頭の方を省略して表示します。


TEdit のText や TLabel の Caption を参照したい場合に備えて、省略していない Path 文字列をグローバル変数に保存しておきます。必要な場合は Edit1.Text や Label1.Caption ではなく、グローバル変数から Path 文字列を取得して利用します。コードは次の通りです。

  private
    { Private 宣言 }
    //省略していない Path 文字列をグローバル変数に保存
    SrcFileName, DstFileName:string;

implementation

uses
  Winapi.ShellAPI,
  System.Math;

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
var
  SelectedFile: string;
  strMsg: string;

  //表示する文字列の長さの自動調整
  //非Path用
  function FitTextWithMiddleEllipsis(TargetControl: TControl; const Text: string): string;
  var
    Bitmap: TBitmap;
    Canvas: TCanvas;
    MaxWidth: Integer;
    LeftPart, RightPart: string;
    Ellipsis: string;
    i, j: Integer;
    CharWidth: Double;
    InitKeep: Integer;
  begin
    Bitmap := TBitmap.Create;
    try
      Canvas := Bitmap.Canvas;

      if TargetControl is TLabel then
        Canvas.Font := TLabel(TargetControl).Font
      else if TargetControl is TEdit then
        Canvas.Font := TEdit(TargetControl).Font
      else
        raise Exception.Create('Font にアクセスできないコントロールです。');

      MaxWidth := TargetControl.Width;
      Ellipsis := '...';

      //全部入るならそのまま返す
      if Canvas.TextWidth(Text) <= MaxWidth then
        Exit(Text);

      //1文字あたりの平均幅を計算
      if Length(Text) > 0 then
        CharWidth := Canvas.TextWidth(Text) / Length(Text)
      else
        CharWidth := Canvas.TextWidth('W');

      // 残せる文字数を幅から概算(両端合計)
      InitKeep := Trunc((MaxWidth - Canvas.TextWidth(Ellipsis)) / CharWidth);

      // 左右で半分ずつ残す
      if InitKeep < 2 then InitKeep := 2; //最低1文字ずつ残すため
      i := InitKeep div 2;
      j := Length(Text) - (InitKeep - i) + 1;

      //徐々に調整して収まる長さを探す
      while (i >= 1) and (j <= Length(Text)) do
      begin
        LeftPart := Copy(Text, 1, i);
        RightPart := Copy(Text, j, Length(Text) - j + 1);
        Result := LeftPart + Ellipsis + RightPart;

        if Canvas.TextWidth(Result) <= MaxWidth then
          Exit(Result);

        Dec(i);
        Inc(j);
      end;

      //最後の手段:1文字ずつ残す
      if Length(Text) >= 2 then
        Result := Copy(Text, 1, 1) + Ellipsis + Copy(Text, Length(Text), 1)
      else
        Result := Ellipsis;

    finally
      Bitmap.Free;
    end;
  end;

  //Path用
  function FitPathWithMiddleEllipsis(TargetControl: TControl; const FilePath: string): string;
  var
    Bitmap: TBitmap;
    Canvas: TCanvas;
    MaxWidth: Integer;
    Ellipsis: string;
    DirPart, FilePart, DrivePart: string;
    Parts: TArray<string>;
    i, LeftCount, RightCount: Integer;
    TestPath: string;

    function MeasureTextWidth(const S: string): Integer;
    begin
      Result := Canvas.TextWidth(S);
    end;

  begin
    Bitmap := TBitmap.Create;
    try
      Canvas := Bitmap.Canvas;

      if TargetControl is TLabel then
        Canvas.Font := TLabel(TargetControl).Font
      else if TargetControl is TEdit then
        Canvas.Font := TEdit(TargetControl).Font
      else
        raise Exception.Create('Font にアクセスできないコントロールです。');

      MaxWidth := TargetControl.Width;
      Ellipsis := '...\';

      //全部入る場合
      if MeasureTextWidth(FilePath) <= MaxWidth then
        Exit(FilePath);

      //ファイル部分とディレクトリ部分を分離
      FilePart := ExtractFileName(FilePath);
      DirPart  := ExtractFilePath(FilePath);
      DrivePart := ExtractFileDrive(FilePath);

      //パスのディレクトリ部分を分解(ドライブ部分は除外)
      Parts := DirPart.Substring(Length(DrivePart) + 1).Split([PathDelim], TStringSplitOptions.ExcludeEmpty);

      //初期は全部表示してみる
      TestPath := IncludeTrailingPathDelimiter(DirPart) + FilePart;

      //左右を削っていくアプローチ
      LeftCount := 0; // 先頭から残すディレクトリ数
      RightCount := Length(Parts); // 後ろから残すディレクトリ数

      while (LeftCount < Length(Parts)) and (MeasureTextWidth(TestPath) > MaxWidth) do
      begin
        //最初の方のディレクトリを省略(中央に Ellipsis)
        Inc(LeftCount);
        TestPath := DrivePart + PathDelim;

        if LeftCount > 0 then
          TestPath := TestPath + Parts[0] + PathDelim;

        if LeftCount < Length(Parts) then
          TestPath := TestPath + Ellipsis;

        if RightCount > 0 then
        begin
          for i := Length(Parts) - RightCount to High(Parts) do
            if i >= 0 then
              TestPath := TestPath + Parts[i] + PathDelim;
        end;

        TestPath := TestPath + FilePart;
        Dec(RightCount);
        if RightCount < 0 then RightCount := 0;
      end;

      //収まる長さで返す
      Result := TestPath;

      //それでも収まらなければ中央省略だけで返す
      if MeasureTextWidth(Result) > MaxWidth then
      begin
        Result := Copy(FilePath, 1, 1) + '...' + Copy(FilePath, Length(FilePath), 1);
      end;

    finally
      Bitmap.Free;
    end;
  end;

begin
  OpenDialog1.Filter := 'MTS ファイル (*.MTS)|*.MTS|すべてのファイル (*.*)|*.*';
  OpenDialog1.Title := 'MTS 形式の動画ファイルを選択してください';
  //ofFileMustExist:ファイルが存在していなければ選択できない
  //ofHideReadOnly:読み取り専用チェックボックスを非表示にする
  OpenDialog1.Options := [ofFileMustExist, ofHideReadOnly];
  OpenDialog1.InitialDir:=ExtractFilePath(Application.ExeName)+'Src';

  if OpenDialog1.Execute then
  begin
    //ShowMessage('選ばれたファイルは: ' + OpenDialog1.FileName);
    //拡張子をチェック
    SelectedFile := OpenDialog1.FileName;
    //拡張子を小文字で取得して比較
    if not SameText(ExtractFileExt(SelectedFile), '.mts') then
    begin
      strMsg:='選択されたファイルは .MTS ファイルではありません。処理を中止します。';
      Application.MessageBox(PChar(strMsg), PChar('エラー'), MB_ICONERROR);
      Edit1.Text:='';
      Button1.SetFocus;
      Exit; // 以降の処理をキャンセル
    end;

    //変数内には正しい文字列が保存されている
    SrcFileName:=OpenDialog1.FileName;
    DstFileName:=ExtractFilePath(Application.ExeName)+'Dst\'
      +ChangeFileExt(ExtractFileName(OpenDialog1.FileName), '')+'.mp4';

    //短く表示_非Path用
    //Edit1.Text:= FitTextWithMiddleEllipsis(Edit1, SrcFileName);
    //Label1.Caption:= FitTextWithMiddleEllipsis(Label1, DstFileName);

    //短く表示_Path表示用に特化
    Edit1.Text:= FitPathWithMiddleEllipsis(Edit1, SrcFileName);
    Label1.Caption:= FitPathWithMiddleEllipsis(Label1, DstFileName);

  end;
end;

6.プログラムのダウンロード

FFmpeg 関連のファイルの除いたプログラム一式を以下からダウンロードできます。なお、ダウンロードとご使用にあたっては、免責事項及び使用条件への同意が必要です。免責事項及び使用条件の詳細は付属の Readme.txt をご覧ください。

また、動作には FFmpeg が必要です。

ダウンロードサイト : https://ffmpeg.org/download.html

ダウンロードするファイルは、次のいずれかを推奨します。ご自身の環境で展開しやすい方を選択してください。

・ffmpeg-release-essentials.7z
・ffmpeg-release-essentials.zip

MTStoMP4.zip を展開(解凍)した後、以下のようなフォルダ・ファイル構成となるようにダウンロードした FFmpeg.exe を配置してください。

MTStoMP4\
 ├ Dst
 ├ FFmpeg\bin\ffmpeg.exe
 ├ Src
 ├ License.txt
 └ MTStoMP4.exe

MP4 形式に変換する MTS 形式の動画ファイルは必ず Src フォルダ内に準備してください。

なお、プログラムの初回起動時には、Windows Defender SmartScreen による警告画面が表示されます。この警告画面に関する詳細は、当 Blog の過去記事をご参照ください。

7.まとめ

このプログラムは変換元の MTS ファイルを選択後、オプションを指定して「変換実行」ボタンをクリックすることで動作します。複数の MTS ファイルを同時に指定して、MP4 変換することはできません。

このプログラムを用いて大きさ 2.09 GB(=2135.36 MB)の MTS 形式の動画ファイルを MP4 形式に変換してみました。なお、各パラメータですが、CRF 値は「23」、Preset は「Medium」、AudioBitRate は「128」を指定しました。生成された MP4 形式の動画の大きさは 287 MB でしたので、削減量は 1848.36MB 、削減率は 約86.6% になります。My NotePC ( Panasonic CF-QV )で生成された MP4 ファイルを視聴しましたが、自分個人の感想として、気になるレベルでの画質や音質の劣化はないように思えました(私の矯正視力は両眼とも 1.5、人間ドックでの聴力検査結果は正常範囲です)。

8.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。記載した内容(プログラムを含む)を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

また、FFmpeg.exe 他、FFmpeg 関連のファイルを同梱した状態での MTStoMP4.exe の再配布を禁じます。MTStoMP4.exe を再配布される場合は、FFmpeg関連のファイルはすべて削除し、MTStoMP4.zip に添付した License.txt を必ず添付してください。