Video.js と HTTP Live Streaming を使って、自前の動画配信サイトを構築してみようというメモ書きです。
前からやってみたかった動画配信サービスを現在作っていて、その検証内容を残しておこうと思います。
このブログに埋め込んだデモのような動画配信ができるまでを通してやってみます。
スポンサーリンク
Video.jsとHLSについてざっくりと解説
まずは重要な部分を簡単に説明。HLS (HTTP Live Streaming) って最初何それ状態でしたが、昨日一昨日で調べて理解した事を噛み砕いて書きます。
HLS (HTTP Live Streaming)
アップルが iOS 向けに開発した HTTP ベースのストリーミングプロトコル。
iPhone や iPad は勿論、今時のスマートフォンだったら対応している。
ただし、PC の Windows だと Microsoft Edge 以外未対応。
Video.js
HTML5 で追加された <video> タグを拡張する Javascript ライブラリ。
これを使えば HLS 形式の動画も PC で見られるようになる。
動画配信のデモ
そしてこの二つを組み合わせたのが下記のデモ動画です。
使っているもの
この動画は下記を作って作成&配信されています。
- 動画配信サーバ (Nginx)
- FFmpeg 3.1.3
- Video.js 5.17.0
- videojs-contrib-hls 5.3.3
- videojs-contrib-media-sources 4.4.2
HLS は Web サーバがあれば配信できるので Nginx とか Apache を普通に構築するだけで配信可能です。
動作確認ブラウザ
対応ブラウザについてですが、下記の通り主要なブラウザには一通り対応しています。
古いブラウザは確認していませんが、モダンブラウザなら問題ないでしょう。
| Windows10 | Firefox 52, Chrome 57, IE11, Microsoft Edge 38, Opera 43 |
| iOS | Safari, Chrome 57 |
| Android (5.0.2) | Chrome 39, SOL26 内蔵ブラウザ |
mp4を直接配信じゃダメなの?
実のところこんな真似しなくても mp4 形式の動画ファイルを <video> タグにセットすれば簡単に動画配信はできます。
しかし、これには結構デメリットがあってユーザーの事を考えるなら HLS 形式での配信をしたい所です。
- mp4は再生もシークも遅い
- mp4だと無駄なネットワーク帯域を消費する
- 簡単にダウンロード保存されてしまう
良くも悪くも mp4 は一本のファイルを配信するためか、疑似ストリーミングでも再生が始まるまで遅くシークで固まりやすいのが難点です。
トラフィックデータを計測して HLS と mp4 を比較してみると、同じ位のセッション数でも mp4 の方が無駄にトラフィクが流れていたようでした。
mp4 -> HLS に切り替えた後は体感的にも早くなり、トラフィク量も削減できたので本格的にやるなら HLS を採用したいなと個人的には思います。
HLSデータの作り方
次は HLS データの作り方についてです。
HLS はセグメント化された動画データ (.ts)、セグメントリスト (.m3u8) が必要なので、FFmpeg を使ってこれを作成します。
これにはいくつか方法があるので書いてみます。
ちなみに、今回利用した FFmpeg は下記のようにビルドしています。
# tar xfz ffmpeg-3.1.3.tar.gz ; cd ffmpeg-3.1.3 # ./configure --enable-gpl --enable-nonfree --enable-openssl --enable-libfdk_aac --enable-libx264 # make && make install
エンコード
動画データからから .ts ファイルエンコードしながら分割する場合はこんな感じに。一緒に .m3u8 ファイルも FFmpeg が作成してくれます。
ffmpeg -i input.mp4 -vcodec libx264 -acodec aac -flags +loop-global_header -f segment -segment_format mpegts -hls_time 10 -segment_list video.m3u8 segment_%04d.ts
動画データは約10秒毎に .ts ファイルに分割していて、対応しているプレイヤーなら単体でも再生可能です。(短すぎるとできない場合もありますが…)
暗号化しながらエンコード
次は .ts ファイルを Openssl で暗号化しながらエンコードする例です。暗号化すると正しい鍵で復号化しながらでないと再生されないようになり、コンテンツの保護に役立ちます。
まずは暗号化用の鍵を作成するスクリプト (key.sh) を書いてこれを実行します。
#!/bin/sh
BASE_URL=${1:-'.'}
openssl rand 16 > video.key
echo $BASE_URL/video.key > file.keyinfo
echo video.key >> file.keyinfo
echo $(openssl rand -hex 16) >> file.keyinfo
実行すると下記のファイルができます。
$ sh key.sh $ ls file.keyinfo key.sh video.key
つぎにこの鍵を使って暗号化しながら .ts ファイルを作成していきます。
ffmpeg -i input.mp4 -vcodec libx264 -acodec aac -hls_key_info_file file.keyinfo -hls_list_size 0 -hls_time 10 -hls_segment_filename segment_%04d.ts video.m3u8
これで鍵がないと復号化できない形式になりました。
トランスコード
最後は mp4 ファイルを形式変換して .ts に分割する方法です。
こちらはエンコードではなくトランスコードと呼ぶらしいです。
ffmpeg -i input.mp4 -bsf:v h264_mp4toannexb -c copy -f segment -segment_format mpegts -hls_time 10 -segment_list video.m3u8 segment_%04d.ts
エンコードの時と違ってあっという間に処理が終了すると思います。
負荷も殆どかからないので、既に mp4 化している動画ならこっちがオススメかもです。
トランスコードした.tsファイルを暗号化
トランスコードした .ts ファイルは自前で暗号化する必要があります。
手順的には、
- mp4から.tsにトランスコード
- .tsファイルをOpenSSLコマンドで暗号化
- セグメントリスト.m3u8を書き換え
となり、手作業は面倒なのでこれをスクリプトで一括処理します。色々な例を参考に Perl で作ってみました。
#!/usr/bin/perl
## name => mp4tohls.pl
use strict;
use warnings;
use File::Find;
local $ENV{PATH} = "/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin";
my $input = $ARGV[0] || exit;
## スクリプトの設定
my $conf = {
infile => $input,
encode_dir => 'encode',
segment_dir => 'segment',
segment_list => 'video.m3u8',
segment_prefix => 'segment_',
key_file => 'video.key',
};
## セグメントファイル置き場と、エンコード用ディレクトリの作成
if (-d $conf->{encode_dir}) { `rm -r $conf->{encode_dir}`; }
if (-d $conf->{segment_dir}) { `rm -r $conf->{segment_dir}`; }
mkdir($conf->{encode_dir});
mkdir($conf->{segment_dir});
## トランスコード
`ffmpeg -i $conf->{infile} -bsf:v h264_mp4toannexb -c copy -f segment -segment_format mpegts -hls_time 10 -segment_list $conf->{segment_list} $conf->{encode_dir}/$conf->{segment_prefix}%04d.ts > /dev/null 2> encode.log`;
## 鍵の作成
`openssl rand 16 > $conf->{key_file}`;
my $hexkey = `cat $conf->{key_file} | hexdump -e '16/1 "%02x"'`;
chomp($hexkey);
## .ts ファイルを全て暗号化する処理
my @segment_file = ();
find(\&find_segment_file, $conf->{encode_dir});
@segment_file = sort {$a cmp $b} @segment_file;
foreach my $ts (@segment_file) {
$ts =~ /(\d+)/;
my $hexiv = sprintf("%032x", );
my @result = `openssl aes-128-cbc -e -in $conf->{encode_dir}/$ts -out $conf->{segment_dir}/$ts -p -nosalt -iv $hexiv -K $hexkey`;
}
## セグメントリストの編集
&write_m3u8();
## 作業ディレクトリを削除
`rm -r $conf->{encode_dir}`;
sub write_m3u8 {
open(IN, "< $conf->{segment_list}");
my @m3u8 = <IN>;
close(IN);
chomp(@m3u8);
open(OUT, "> $conf->{segment_list}");
foreach (@m3u8) {
next if ($_ =~ /^#EXT-X-ALLOW-CACHE:YES$/);
if ($_ =~ /^$conf->{segment_prefix}/) {
$_ = $conf->{segment_dir} . '/' . $_;
}
print OUT "$_\n";
if ($_ =~ /^#EXT-X-TARGETDURATION/) {
print OUT "#EXT-X-KEY:METHOD=AES-128,URI=\"$conf->{key_file}\"\n";
}
}
close(OUT);
return;
}
sub find_segment_file {
if ($_ =~ /\.ts$/) {
push(@segment_file, $_);
}
return;
}
exit;
これをこんな風に実行します。
$ ls mp4tohls.pl sample.mp4 $ perl mp4tohls.pl sample.mp4 $ ls encode.log mp4tohls.pl sample.mp4 segment video.key video.m3u8
.ts ファイルは segment ディレクトリに配置され、セグメントリスト .m3u8 のは FFmpeg が生成したものを編集しこのような形にしています。
#EXTM3U #EXT-X-VERSION:3 #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-TARGETDURATION:9 #EXT-X-KEY:METHOD=AES-128,URI="video.key" #EXTINF:8.408400, segment/segment_0000.ts #EXTINF:8.341667, segment/segment_0001.ts #EXTINF:3.403400, segment/segment_0002.ts . . ~ 省略 ~ . #EXT-X-ENDLIST
Video.jsの使い方
次は Video.js の使い方についてです。
これは Javascript なので html で読み込ませて使うだけなのですが、使うためには各種ファイルをインストールする必要があります。
インストール方法は後述しますが、最終的に各種ファイルはこのようなディレクトリ構成となるように配置していきます。
htdocs/ │ ├── index.html │ ├── css/ │ │ │ └── style.css │ ├── js/ │ │ │ ├── video-js/ │ │ │ │ │ ├── video-js.min.css │ │ ├── video-js.swf │ │ └── video.min.js │ │ │ ├── videojs-contrib-hls.min.js │ └── videojs-contrib-media-sources.min.js │ ├── segment/ │ │ │ ├── segment_0000.ts │ ├── segment_0001.ts │ └── segment_0002.ts │ ├── thumbnail.jpg ├── video.key └── video.m3u8
- mp4tohls.plは実行後、公開ディレクトリから出しておきます
- mp4tohls.plを実行した際に出来たencode.logは不要ですので削除して大丈夫です
- thumbnail.jpgは動画のイメージ画像なので適当にキャプチャしておきます
Video.jsのインストール
公式を見ると nmp を使ってインストールしろとあって、普段このツール使っていないんだけどと思いつつ nmp のインストールから行いました。便利なんだろうけど良く知らない物をサーバにインストールするのは気が引ける…
# yum install -y epel-release # yum install --enablerepo=epel -y npm # exit $ npm install --save-dev video.js $ npm i videojs-contrib-hls $ npm i videojs-contrib-media-sources
npm コマンドでインストールするとカレントディレクトリに node_modules というディレクトリが出来るので、その中から下記ファイルを適当な js ディレクトリにコピーしておきます。(npm の使い方が良くわかっていないのでアレかもですが)
- video.min.js
- video-js.css
- video-js.swf
- videojs-contrib-media-sources.min.js
- videojs-contrib-hls.min.js
videojs-contrib-media-sources.min.js 以外は CDN もあるのでそれを使うのがお手軽ですが、これだけは上記のようにインストールしないといけない模様。
まぁ、前に CDN から CSS を読み込んでいたらネットワークエラーで酷い目にあったので、それ以来自分はサーバに置く派ですが。
Video.jsをHTMLに組み込む
インストールができたら Video.js を設置して、動画配信用の HTML を作成します。
index.html としてこのように記述します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex,nofollow">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>HTTP Live Streaming Demo</title>
<link href="js/video-js/video-js.min.css" rel="stylesheet">
<link href="css/style.css" rel="stylesheet">
<script src='js/video-js/video.min.js'></script>
<script src='js/videojs-contrib-media-sources.min.js'></script>
<script src='js/videojs-contrib-hls.min.js'></script>
<script>
videojs.options.flash.swf = "js/video-js/video-js.swf";
var ua = navigator.userAgent.toLowerCase();
var isIE = (ua.indexOf('msie') > -1) && (ua.indexOf('opera') == -1) || (ua.indexOf('trident/7') > -1);
if (isIE) {
videojs.options.hls.mode = 'flash';
}
</script>
</head>
<body>
<video id="video" class="video-js vjs-default-skin vjs-big-play-centered" poster='thumbnail.jpg' controls preload="auto" data-setup="{}">
<source src="video.m3u8" type="application/x-mpegURL">
</video>
</body>
</html>
ポイントは15~19行目のIE判定部分。
IEだけは flush を強制で使うようにしないと HLS 形式の動画が再生できない。そのため、ユーザーエージェントによる判定を行っています。
最後に style.css に必要な設定を書き入れます。今回はこのブログに埋め込みしやすいようにこんな感じで書いてみました。
@charset "UTF-8";
html {
height: 100%;
}
body {
width: 100%;
height: 100%;
margin: 0;
background-color: #000;
}
#video {
width: 100%;
height: 100%;
}
あとは使いたいブログでこんな感じに埋め込めば良い感じの動画配信ができると思います。
<iframe width="640" height="480" src="http://example.com/hlspath/" frameborder="0" allowfullscreen></iframe>
※ スマートフォン用の最適化はこの iframe に対して CSS を調整すればOK
コンテンツ保護について
Videos.js と HLS で動画配信をする方法は以上ですが、おまけで配信コンテンツの保護について一言。
HLS 形式でのストリーミング配信は mp4 を直接配信する疑似ストリーミング方式よりは幾分安全だとは思いますが、ファイルの分割や暗号化したとしてもそこまで効果は高くありません。
例えば FFmpeg でこんな風に実行するといとも簡単に復号化して一本の動画ファイルにできてしまいます。
ffmpeg -i http://example.com/input.m3u8 -movflags faststart -c copy -bsf:a aac_adtstoasc download.mp4
他にも、そのサイトに合った違法なダウンロードツール、違法なダウンロード方法の解説サイトなどが目に付きます。結局、コンテンツの保護は各種 DRM ソリューションを使うしかないのかというのが現状のようです。
一応、FFmpeg やこのようなツールからのアクセスを拒否する事も出来なくないですが、これはイタチごっこになりそうです。
ただ、この手のツールは素人レベルだと扱えないのでそれがせめてもの救いかもですが…
動画配信って面白い分野ですしやっていて楽しいですが、コンテンツ保護の問題は正直頭が痛いです。
WEB 版の DRM は何だか揉めているようですが、何かカチッとした物が出来てくれたら嬉しいですね。
WEB側で頑張るコンテンツ保護ロジックの実装案(追記)
追記: 2017-04-20
完璧でないものの、実際にやってみて手軽で有効な方法を見つけたのでそのロジックだけ書き残しておこうと思います。
- 正規ユーザーに Cookie を javascipt を使って埋め込み
- Cookieを持つユーザーにのみプレイリスト (.m3u8) をプログラム経由で渡す
- 復号化用の鍵も同様の処理
- Cookieは有効期限を短めにしておく
こんな感じです。
ざっくりと言うと、動画掲載ページで Cookie を使った簡易のセッション認証を行い、そのページ経由のユーザーにのみプレイリストを渡すという方法ですが、早速効果があったみたいなので素人相手ならこれは結構いけるなと…
ポイントは Cookie を javascipt を使って渡すという点で、javascipt を実行できないダウンロードプログラム何かはこれだけで撃退できるはず。
あとはプレイリストのダウンロードにも Cookie が必要なので、上記で書いた ffmpeg にプレイリストの URL を渡す方法もこれで一応は防げます。(Cookie を持っていないユーザーには 404 をプログラムで返す)
ただ、ffmepg は Cookie を扱えるので、ダウンロード用の Cookie を特定して突っ込まれたらそれでやられてしまいますし、プログラム言語の中には javascipt を実行できるのもあるので正規ユーザーと同じ挙動をされたらアウトです。
なので、一般的?なダウンロードツールやその方法への対処のみですがやらないよりはマシかな程度で…
参考
HLS全般
HLSの暗号化とコンテンツ保護
Protected HLS using ffmpeg and openssl
HTTP Live Streaming(Encrypt)
Encrypted Http Live Streaming
どうやったらDLを阻止できるのか。