*

LinuxでMPEG2 TSから字幕情報を抽出してFFmpegで動画に埋め込む方法

  最終更新日:2016/05/22

MPEG2 TSから字幕抽出 & FFmpegで字幕埋め込み

 LinuxでMPEG2 TSから字幕情報を抽出してFFmpegで動画に埋め込む方法について説明します。

生のMPEG2 TS

 Linux録画サーバのChinachuやepgrec UNAで録画した場合、通常はMPEG2 TSファイルに字幕情報は含まれていません。理由は、recpt1コマンドのオプションで”–sid”を指定しているため、字幕情報などが省略されているためです。

 そのため、字幕情報を抽出したい場合、省略されていない生(なま)のTSを録画する必要があります。ただし問題なのは、生TSはマルチ編成の番組(1チャンネルでニュースと野球を放送等)やEPG、ワンセグを全てまとめた1つのファイルに録画してしまうことです。そのため、ファイルサイズが”–sid”オプション指定時よりも大きくなります。

 また、マルチ編成の番組の場合、PCでは現状はメインの番組の字幕情報しか抽出できなさそうです。私が調べた限りでは方法が見つかりませんでした。これは、マルチ編成で放送されることがあまり無い地デジではほとんど問題になりませんが、BSやCSで生のTSを録画すると他のチャンネルと一緒に録画されてしまうため注意が必要です。

 生のTSを録画する方法は、epgrec UNAでは録画予約時、録画MODEに”Full TS”を選択するだけで良いです。Chinachuでは番組個別に録画方法を指定する方法がありません。そのため、Chinachuの環境ではrecpt1コマンドを直接実行して録画するしか方法はないでしょう(多分)。

字幕の抽出方法

 Linuxで生TSから字幕情報を抽出するにはassdumperコマンドを使用します。調べた限り、Linuxで使い勝手の良いコマンドはこれ以外に見つかりませんでした。このコマンドはASS形式の字幕情報を出力します。

Ubuntu・Debianの場合)  ※ Debianではsudoを抜いてrootユーザーで実行して下さい
 $ sudo apt-get install subversion ruby
 $ svn export https://github.com/eagletmt/eagletmt-recutils/trunk/assdumper
 $ cd assdumper
 $ make
 $ strip assdumper
 $ sudo cp -p assdumper assadjust.rb /usr/local/bin/
 $ sudo chown root:root /usr/local/bin/ass*

CentOS 7の場合)
 # yum install subversion ruby
 # svn export https://github.com/eagletmt/eagletmt-recutils/trunk/assdumper
 # cd assdumper
 # make
 # strip assdumper
 # cp -p assdumper assadjust.rb /usr/local/bin/
 # chown root:root /usr/local/bin/ass*

 サーバー用の最小環境しかインストールしていない場合、日本語フォントをインストールする必要があります。

Ubuntu・Debianの場合)
 $ sudo apt-get install fontconfig fonts-takao

 その他の日本語フォントを探したい場合は以下を実行
  $ apt-cache search fonts | grep -i japan

  あるいは

  $ apt-cache search fonts | grep -i 日本語

CentOS 7の場合)
 # yum install fontconfig ipa-pgothic-fonts

 その他の日本語フォントを探したい場合は以下を実行
  $ yum search fonts | grep -i japan  

 コマンドの使用方法は、生TSファイルを引数に指定し、リダイレクトでファイルに出力します。ただし、このままだと字幕の時間情報が録画時の時刻のままなので、録画開始時の時刻を0秒としてassadjust.rbスクリプトで相対時刻に直します。それらの手順を実行したものが以下となります。

$ assdumper input.ts > output.raw.ass
$ assadjust.rb 19:00:00 output.raw.ass > output.ass

 assdumperコマンドを実行しただけのASSファイルの内容は以下になります。

[Script Info]
ScriptType: v4.00+
Collisions: Normal
ScaledBorderAndShadow: yes
Timer: 100.0000

[Events]
Dialogue: 0,21:27:17.92,21:27:21.85,Default,,,,,, 下落率はともに6年連続で縮小しました。
Dialogue: 0,21:27:21.76,21:27:25.46,Default,,,,,, 中東やアフリカからドイツなどを目指す難民や移民。

 assadjust.rbスクリプトで録画開始時刻を”21:27:17”に指定して実行した結果が以下になります。時刻がずれていることが分かります。

[Script Info]
ScriptType: v4.00+
Collisions: Normal
ScaledBorderAndShadow: yes
Timer: 100.0000

[Events]
Dialogue: 0,00:00:00.92,00:00:04.85,Default,,,,,, 下落率はともに6年連続で縮小しました。
Dialogue: 0,00:00:04.76,00:00:08.46,Default,,,,,, 中東やアフリカからドイツなどを目指す難民や移民。

 assadjust.rbスクリプトを使うのでも良いのですが、フォント情報が入っていないため、このままではFFmpeg用には使用できません。また、epgrec UNAでは録画ファイル名に録画開始時刻が含まれているため、ファイル名を元に時刻をずらすことが出来れば使い勝手が良さそうです。

 と言う訳で、スクリプトを自作してみました。

#!/usr/bin/perl

use strict;
use warnings;
use utf8;
use open ":utf8";
use Getopt::Long;
use File::Basename qw/basename dirname/;
use Time::Local;

# ************************
# ***** ユーザー設定 *****
# ************************
# assdumperの実行ファイル名
my $assdumper_cmd="/usr/local/bin/assdumper";
# 先頭のマージン秒 (59秒以内)
my $margin_sec = 0;
# ************************
# ***** 設定ここまで *****
# ************************

#binmode(STDOUT, ":utf8");

# 関数: ヘルプ
sub usage {
    no utf8;

    print <<END;
Usage:
  assdumperでMPEG2-TSファイルから字幕情報を抽出し、録画開始時刻を基準にして出力する

書式
  assconv.pl -i|-input <file> -o|-output <file> [-h|-help] [-m|-margin <int>] [-s|-start <hh:mm:ss>]

  -h, -help                本内容の表示
  -i, -input <file>        入力 MPEG2-TS ファイル名(必須)
  -o, -output <file>       出力 ASS ファイル名(必須)
  -m, -margin <int>        入力ファイルの先頭のマージン秒(59秒以内)
  -s, -start <hh:mm:ss>    入力 MPEG2-TS ファイルの録画開始時刻

注意事項
  - 録画開始時刻の形式は"01:02:34"の様な8文字厳守. "1:2:34"の省略形式は不可.
END

    exit(0);
}

my %opts = ();

GetOptions(\%opts, qw(help|h input|i=s output|o=s start|s=s margin|m=s))
    or die "Error(GetOptions)";

unless (exists $opts{'input'} && exists $opts{'output'}) {
    &usage();
} elsif (exists $opts{'help'}) {
    &usage();
}

my $ts_file = $opts{'input'};
my $ass_file = $opts{'output'};
my $start_time = "";

# マージン秒指定
if (exists $opts{'margin'} && $opts{'margin'} < 60) {
    $margin_sec = $opts{'margin'};
}

sub time2sec {
    my($time1) = @_;
    my($hour, $min, $sec, $msec) = split(/[:\.]/, $time1);

    return timegm($sec, $min, $hour, 1, 0, 0);
}

sub sec2time {
    my($sec_time) = @_;
    my($sec, $min, $hour, @tmp) = gmtime($sec_time);

    return sprintf("%02d:%02d:%02d", $hour, $min, $sec);
}

sub time_subtract {
    my($std_time, $rec_time) = @_;

    return &sec2time(&time2sec($rec_time) - &time2sec($std_time) + $margin_sec);
}


my $base_name = basename $ts_file;

if (exists $opts{'start'}) {
    if ($opts{'start'} =~ /^(\d{2}:\d{2}:\d{2})/) {
	$start_time = $1;
    } else {
	&usage();
    }
} elsif ($base_name =~ /^20\d{6}_(\d{2})(\d{2})(\d{2})/) {
    $start_time = sprintf("%02d:%02d:%02d", $1, $2, $3)
} elsif ($base_name =~ /^20\d{6}_(\d{2})(\d{2})/) {
    $start_time = sprintf("%02d:%02d:00", $1, $2)
} else {
    die "not specific the start time";
}

my @result = `$assdumper_cmd $ts_file 2>/dev/null`;

open(my $ass_handle, ">", $ass_file)
    or die "can't open $ass_file: $!";

foreach my $line (@result) {
    if ($line =~ /^Dialogue: 0,([0-9:]+)\.\d+,([0-9:]+)/) {
	my ($time1, $time2) = ($1, $2);
	my ($mod_time1, $mod_time2);

	$mod_time1 = &time_subtract($start_time, $time1);
	$mod_time2 = &time_subtract($start_time, $time2);
	if (&time2sec($time2) - &time2sec($time1) > 5) {
	    $mod_time2 = &sec2time(&time2sec($mod_time1) + 5)
	}

	$line =~ s/${time1}/${mod_time1}/;
	$line =~ s/${time2}/${mod_time2}/;
    }

    print $ass_handle $line;
}

print $ass_handle <<END;

[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,TakaoPGothic,20,&H00FFFFFF,&H0000FFFF,&H00000000,&H80000000,0,0,0,0,90,90,0,0,1,1,1,2,20,20,20,128
END

close($ass_handle);

 ダブルクリックするとスクリプト全体を選択した状態になるのでCtrl+Cでコピーして下さい。/usr/local/bin/ディレクトリなどに”assconv.pl”として保存して下さい(UTF8の改行コードはLF)。

# vi /usr/local/bin/assconv.pl
# chmod +x /usr/local/bin/assconv.pl

 デフォルトのフォント指定は”TakaoPGothic”としています(スクリプトの132行目)。CentOS 7の場合、”IPAPGothic”として下さい。他のフォントに変える場合、fc-listコマンドでフォント名を確認して変更します。下記はコマンドの実行例です。日本語名の後ろにASSで指定可能な英語フォント名があります。

$ fc-list | grep -i takao

/usr/share/fonts/truetype/fonts-japanese-gothic.ttf: Takao Pゴシック,TakaoPGothic:style=Regular
/usr/share/fonts/truetype/takao-gothic/TakaoExGothic.ttf: TakaoExゴシック,TakaoExGothic:style=Regular
/usr/share/fonts/truetype/takao-gothic/TakaoPGothic.ttf: Takao Pゴシック,TakaoPGothic:style=Regular/usr/share/fonts/truetype/takao-gothic/TakaoGothic.ttf: Takaoゴシック,TakaoGothic:style=Regular

 assconv.plスクリプトはファイル名の先頭が録画開始時刻(YYYYMMDD_hhmm[ss])の形式に対応しています。この形式のファイル名ならば、ファイル名から取得した録画開始時刻を元にASSファイルの時刻をずらして出力します。

 epgrec UNAの環境で使用する場合、ファイル名の形式を以下とすると利用できます。

  • %YEAR%%MONTH%%DAY%_%HOUR%%MIN%%SEC%_%TYPE%%CH%_%TITLE%

 録画開始時刻の引数指定や録画開始前のマージン秒を指定することも可能です。下記は実行例です。詳細は”-h”オプションで表示される内容を確認して下さい。

$ assconv.pl -i 20151125_1900_GR27_NHKニュース.ts -o output.ass

$ assconv.pl -i 20151125_190003_GR27_NHKニュース.ts -o output.ass -m 2

 ASSファイルはVLC media playerでも使用可能なはずなのですが、ASSファイルを読み込むと再生できませんでした(字幕が表示できないのではなく、再生が始まりません)。Windowsで試したので、フォント名をWindowsのものに変更したり、色々と試したのですが駄目でした。少なくともFFmpegでは読み込むことができるので、今のところ良しとしています。

FFmpegで字幕を動画に埋め込む方法

 FFmpegで字幕を動画に埋め込むには、libassに対応したFFmpegが必要です。本ブログではFFmpegのビルド方法を紹介しています。そちらの記事でlibassに対応したFFmpegをビルドする手順を説明しています。詳細は「LinuxでFFmpegをほぼ全自動でビルドする(CentOS、Ubuntu等に対応)」を参照して下さい。

 下記はFFmpegのASSファイルの指定例です。

$ ffmpeg -i input.ts -vcodec libx264 -vf "subtitles=output.ass" output.mp4

  下記のオプションでASSファイルを指定する。

   -vf "subtitles=ASSファイル名"
FFmpegによる字幕埋め込み例

FFmpegによる字幕埋め込み例

 上の画像は実際にFFmpegで作成した動画のスクリーンショットです。赤で囲った箇所がTakao Pゴシックで埋め込んだ字幕です。ASSではフォントサイズや位置など細かい指定ができます。詳細は参考ページを確認して下さい。

参考ページ