Rest Term

Blog

memcachedが10歳の誕生日を迎えました

memcached_banner

そりゃ僕もおっさんになるわけだ。
Gimme the cache! memcached turns 10 years old

Twitterではお祝いのメッセージで溢れています。

memcached作者のBrad Fitzpatrickさんがお誕生日パーティーやろうぜって言ってます。
set party 0 0 33 memcached 10-year birthday party!
(タイトルがmemcachedプロトコルになってる)

Redis作者のSalvatoreさん(@antirez)もお祝いしてますね。
https://twitter.com/antirez/status/336894345309470720
antirez_message

memcached はオンメモリで動作する分散型のKey-Value Store(KVS)。その使い勝手の良さから世界中に普及しました。プロトコルもシンプルであるため、memcachedプロトコル互換のプロダクトも数多く開発されました。

2009年。僕が今の会社に入社した頃には、もう既に多くのサービスでmemcachedを活用していたように記憶しています。MySQLのフロントにキャッシュサーバとしてmemcachedを置くというよく知られている構成でした。僕の担当していた動画配信のバックエンドも当時は MySQL + memcached の構成で作られていて、僕はアプリケーションから叩くWeb APIをApacheモジュール(mod_dbd + libmemcached)で作ったりしていました。懐かしい。

これからもいろいろなサービスを支えるプロダクトであり続けて欲しいです。

memcached initial commit: changelog: livejournal

 

Tags:

RedisのLuaスクリプティング機能について

the Lua interpreter built into Redis

the Lua interpreter built into Redis

僕のRedisについての知識はv2.2くらいで止まっていたので、それ以降のRedisに備わった機能を調べているんですけど、その中でもv2.6からサポートされたLuaスクリプト実行環境について今回は整理します。

技術Wikiの方にもRedisについてのメモを残しています。
* Redis - Tech Note

環境

CentOS 5.8 (x86_64)
Redis 2.6.10 (malloc=jemalloc-3.2.0 bits=64)
Pythonクライアント (redis-py 2.7.2)

※ Redis 2.6で利用できるLuaのバージョンは5.1です。

Lua言語について

Lua - Wikipediaによると、

Lua は、C言語のホストプログラムに組み込まれることを目的に設計されており、高速な動作と、高い移植性、組み込みの容易さが特徴である。いったんバイトコードにコンパイルされ、Lua VM で実行される。LuaJIT は The Computer Language Benchmarks Game によると、変数に型のないスクリプト言語では最速の言語・処理系である。

LuaJITの速さは有名ですね。ただ、Web開発の現場だとLuaを使う人は少ないかもしれません。ちなみにApache2.4からは mod_lua が使えますけど、これも遊びでならともかく実務で活用している人はそんなに多くはないと思います。僕の周りでは独自プロトコルを使っている社内システム用にWireshark(パケットキャプチャツール)のプラグインを書くときなどに使っている人はたまにいますが、Luaの主戦場はやはりゲーム業界のようです。あと、TAS(Tool Assisted Speedrun)制作をする人はLuaスクリプトが書けるといろいろ捗るらしいですね、ゲームは好きなのでちょっと興味あります。

Luaパッケージリポジトリ LuaRocks

LuaRocksはLuaモジュールパッケージのアーカイブ、CPANやRubyGemsみたいなものです。今回はRedisサーバ上でLuaスクリプトを実行するのですが、パッケージ管理ツールを含めたLua開発環境をローカルに整えておくと自作スクリプトの動作確認をする時などに便利なのでぜひ入れておきましょう。

## モジュールのインストール
## --local オプションを付けるとユーザーディレクトリ($HOME/.luarocks 以下)にインストールされる
$ luarocks install --local redis-lua

## インストール済みモジュールの表示
$ luarocks list
Installed rocks:
----------------

lua-cjson
   2.1.0-1 (installed) - /home/ryo/.luarocks/lib/luarocks/rocks

lua-cmsgpack
   0.3-2 (installed) - /home/ryo/.luarocks/lib/luarocks/rocks

luasocket
   2.0.2-5 (installed) - /home/ryo/.luarocks/lib/luarocks/rocks

redis-lua
   2.0.4-1 (installed) - /home/ryo/.luarocks/lib/luarocks/rocks

CPANやRubyGemsほどではないですが、データベースドライバやHTTPクライアントなどいろんなモジュールが揃っているようなので、組み込み用途だけではなく簡単なバッチ処理などでもLuaは使えるのではないでしょうか。

では、Redis公式サイトのEVAL - Redisを読みながら整理を進めていきます。

Hello, World

EVAL script numkeys key [key ...] arg [arg ...]
summary: Execute a Lua script server side

EVALコマンドは引数に渡した文字列ををLuaスクリプトとして実行(評価)します。numkeys には操作するkeyの数を指定します。keyの操作が必要ないスクリプトの場合は0を指定すればOKです。

> eval "return 'hello, world'" 0
"hello, world"

スクリプトファイルを読み込んで実行

redis-cli コマンドの --eval オプションを使えば外部スクリプトファイルを読み込んで実行できます。

-- hello.lua
local message = 'hello, world'
return message
## keyの数は指定しなくてよい
$ redis-cli --eval hello.lua
"hello, world"

または、以下のようにシェルでファイルの中身を展開して渡す方法もあります。

$ redis-cli eval "$(cat hello.lua)" 0
"hello, world"

LuaからRedisのコマンドを呼ぶ

Luaスクリプト内でRedisの各コマンドを呼ぶことができます。Luaから使えるAPIは以下の2つ。

 * redis.call() (エラー発生時は処理を終了)
 * redis.pcall() (エラー発生時はエラーを捕捉して処理を継続)

この2つのAPIはエラー時の挙動が異なるという違いがあり、実務で使うには違いを正しく知っておかなければならないと思いますが、ここでは2つのAPIを使い分けることはせず redis.call() の方を使うことにします。
参考: Lua 5.1 リファレンスマニュアル 2.7 - エラー処理

まずは簡単な例から。

> eval "return redis.call('INCR', KEYS[1])" 1 foo
(integer) 1

ここで新しくKEYS配列(Luaでは配列もテーブルで表現されますが、ここでは便宜上"配列"と表記)が登場しました。これは名前の通り操作対象となるkeyが格納されている配列になります(Luaでは配列の添字は1から始まるので注意)。上記例では'foo'というkeyに対してINCRコマンドを呼んで値をインクリメントしています(存在しないkeyに対してINCRコマンドを実行するとRedisはintegerの1を返却する)。

また、更新する値を指定する場合はKEYSと併せてARGV配列も使います。このARGV配列には操作するkeyに対する値(value)が格納されます。

> eval "return redis.call('SET', KEYS[1], ARGV[1])" 1 foo 100
OK
> get foo
"100"

> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"

なお、redis-cli コマンドの --eval オプションは操作するkeyの数を指定できないためARGV配列は利用できません。

エラー処理

エラー処理用のヘルパー関数が提供されているので、これをLuaスクリプト内で利用することができます。

 * redis.error_reply(message)
 * {err=message} (errフィールドを持ったテーブルオブジェクト)

上のAPIは内部的にerrフィールドを持ったテーブルオブジェクトを生成して返すラッパー関数になっていますので、どちらを使っても挙動に違いはありません。

> eval "return redis.error_reply('Error occurred')" 0
(error) Error occurred
> eval "return {err='Error occurred'}" 0
(error) Error occurred

ログ処理

ロギング用の関数も提供されているので、こちらもLuaスクリプト内で利用できます。

 * redis.log(loglevel, message)
  loglevel: LOG_DEBUG, LOG_VERBOSE, LOG_NOTICE, LOG_WARNING

> eval "redis.log(redis.LOG_NOTICE, 'log message here.')" 0
(nil)
> eval "redis.log(redis.LOG_WARNING, 'something is wrong with this script.')" 0
(nil)

Redisのログファイルに出力されているか確認。

[30518] 11 Apr 16:36:31.597 * log message here.
[30518] 11 Apr 16:36:34.347 # something is wrong with this script.

Luaモジュールの利用

Redis v2.6.10に組み込まれているLua処理系では以下のモジュールが利用できます。

 * base
 * table
 * string
 * math
 * debug
 * cjson (3rd party)
 * cmsgpack (3rd party)

ここではいくつかのモジュールを紹介します。

table (テーブルモジュール)

tableモジュールはLuaの標準モジュールに含まれており、テーブル操作の為のユーティリティを提供しています。

local t = {3,5,1,4,2}
-- テーブル内要素を降順ソート(in-place)
table.sort(t, function(a, b) return a > b end)
-- テーブル内要素を ' > ' で繋げた文字列を返す
return table.concat(t, ' > ')

返される文字列は "5 > 4 > 3 > 2 > 1" となります。

cjson (JSONモジュール)

cjsonモジュールを使えばJSONデータのエンコード/デコードが可能です。せっかくRedisを使っているのだからJSONのような構造化データは使わず、素直にRedisのListやHash型を活用した設計を心掛けた方が良いと思いますが、JSONデータをやりとりするWeb APIとの連携が必要な際などに有用なのではないでしょうか。

-- cjsontest.lua
local key = 'cjsontest'
if redis.call('EXISTS', key) == 1 then
   local encoded = redis.call('GET', key)
   local decoded = cjson.decode(encoded)
   local str = 'table: '
   for k,v in pairs(decoded) do
      str = str..k..'='..v..' '
   end
   return str
else
   local data = {foo=1, bar=2, baz=3}
   local encoded = cjson.encode(data)
   redis.call('SET', key, encoded)
   return 'set ok'
end
$ redis-cli --eval cjsontest.lua
"set ok"
$ redis-cli --eval cjsontest.lua
"table: foo=1 bar=2 baz=3 "

cmsgpack (MessagePackモジュール)

cmsgpackモジュールを使えばMessagePack形式のデータを扱うことができます。

-- cmsgpacktest.lua
local key = 'cmsgpacktest'
if redis.call('EXISTS', key) == 1 then
   local packed = redis.call('GET', key)
   local unpacked = cmsgpack.unpack(packed)
   local str = 'table: '
   for k,v in pairs(unpacked) do
      str = str..k..'='..v..' '
   end
   return str
else
   local data = {foo=1, bar=2, baz=3}
   local packed = cmsgpack.pack(data)
   redis.call('SET', key, packed)
   return 'set ok'
end
$ redis-cli --eval cmsgpacktest.lua
"set ok"
$ redis-cli --eval cmsgpacktest.lua
"table: foo=1 bar=2 baz=3 "

それはそうとMessagePackはIETF絡みに巻き込まれていろいろざわついているようですね。これからどうなるのか気になります。

簡単にですがいくつかのモジュールを紹介しました。今後はcjsonやcmsgpack以外のサードパーティーモジュールも組み込まれるのでしょうか。

RedisへのLuaスクリプトの登録

作成したLuaスクリプトをRedisサーバに登録する機能が提供されています。あらかじめサーバ側に登録しておけばスクリプトの転送コストを削減することができます。MySQLなどのRDBMSでいうところのStored Procedure/Functionのような機能ですね。

## Luaスクリプトのサーバへの登録、スクリプトのSHA1ハッシュダイジェストが返される
SCRIPT LOAD script
summary: Load the specified Lua script into the script cache.

## 登録されているLuaスクリプトを実行、第一引数にはスクリプト本体ではなくSHA1ハッシュ文字列を指定
EVALSHA sha1 numkeys key [key ...] arg [arg ...]
summary: Execute a Lua script server side
## SCRIPT LOADコマンドでLuaスクリプトの登録
> script load "return ARGV[1] + ARGV[2]"
"53c370cb7894e73f9aaf94627c0506c428b5cc97"
## EVALSHAコマンドで登録されているスクリプトを実行
> evalsha 53c370cb7894e73f9aaf94627c0506c428b5cc97 0 100 200
(integer) 300
## 未登録のSHA1ダイジェストを指定するとエラーを返す
> evalsha foobarbaz 0 100 200
(error) NOSCRIPT No matching script. Please use EVAL.

登録したLuaスクリプトの管理はどうするんだろうと悩んでしまいますが、MySQLでいうところの mysql.proc みたいなメタテーブルを別途用意して運用するんでしょうか。それはなんか事故りそう。。

SHA1ダイジェストを計算するLua API

公式ドキュメントには書かれていませんが、SHA1ダイジェストを計算するLua APIも公開されているので(実装はscripting.cの650行目あたり、fritzyさんのパッチ)、SHA1の計算をサーバ側に任せることができるので便利に利用できそうです。

* redis.sha1hex()

> eval "return redis.sha1hex('calculate SHA1 hash')" 0
"24291b3d56a34e934879e1864cf921665de7522b"

アプリケーションからの利用

実際にはコマンドラインからではなくプログラムから使うことが多いと思うので、ここではPythonクライアント(redis-py)からLuaスクリプトの実行を試してみます。

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import redis

try:
    r = redis.Redis(host='localhost', port=6379)
    ## Luaスクリプト
    script = 'return redis.call("INCR", KEYS[1])'
    ## EVAL
    ret = r.eval(script, 1, 'foo')
    print 'INCR',ret
    ## SCRIPT LOAD
    sha1 = r.script_load(script)
    print 'SHA1',sha1
    ## EVALSHA
    ret = r.evalsha(sha1, 1, 'foo')
    print 'INCR',ret
except redis.RedisError as e:
    print e

* 実行結果

INCR 1
SHA1 f793247de6e1e3c553cd42d39c812df499e679e4
INCR 2

特に問題はなさそうです。他言語のクライアントライブラリもEVALコマンド対応済みのものが多いので確認してみてください。

注意点

RedisのLuaスクリプト実行環境を利用する上での注意点をいくつか挙げておきます。

グローバル変数は作れない

RedisのLuaインタプリタではグローバル変数の作成は許可されていません。

> eval 'g=1' 0
(error) ERR Error running script (call to f_97a87ce4bb06826f606c5ff88d7d93417412e4d1): user_script:1: Script attempted to create global variable 'g'

Redis-Lua間での型変換

RedisとLuaの間でデータをやりとりする際の型変換には注意する必要があります。特に数値の扱いは正しく理解しておかないと罠にハマってしまいます。

## Luaの浮動小数点値はRedisでは整数として扱われる
> eval 'return 3.14' 0
(integer) 3
-- numeric.lua
-- Luaの数値(整数/浮動小数点数共に)をRedisに格納する際は文字列に変換される
local pi = 3.14
redis.call('SET', 'pi', pi)
return redis.call('GET', 'pi')
$ redis-cli --eval numeric.lua
"3.14"

ということで、Redis-Lua間で浮動小数点値をやりとりしたい時は文字列データとして運用しましょう。

テーブル内のnil

Luaのテーブルに nil が入ってしまわないように注意してください。テーブル {1, 2, nil, 4} はRedisでは {1, 2} と解釈されてしまいます。忘れがちなので注意。

## 
> eval "return {1,2,nil,4}" 0
1) (integer) 1
2) (integer) 2

スクリプト実行のアトミック性

Redis上で動作させるLuaスクリプトはアトミックに実行されるため、keyに対する操作を安全に行うことができます。ただし、アトミックということは1つのスクリプトの実行時間が長いと他のクライアントのスクリプト実行がブロックされてしまうので注意しておく必要があります。

Redis Luaスクリプトアーカイブ - EVALSHA

EVALSHA

In Redis, each Lua script is identified by taking the SHA1 of its body. This is a public place to discover, share and discuss scripts. Think of it like luarocks, cpan, rubygems, or npm but for Redis scripts.

Redis用Luaスクリプトのアーカイブサイトです。非公式で登録されているスクリプトの数もまだ少ないですが、誰でも簡単に自作Luaスクリプトを登録できます。僕もcjsonモジュールを使った簡単なスクリプトを登録してみました。
* JSON Decode - EVALSHA

感想とか

以上、RedisのLuaスクリプティング機能について簡単に整理しました。Lua言語自体の学習コストは低いですし、Redis-Lua間での型変換についてもルールを正しく知っていれば問題にはならないと思います。頭を悩ませそうなのは、やはりサーバ側に登録したLuaスクリプトの管理でしょうか。また、実務でRedisを活用しているところだと僕の周りではv2.2系のシェアが一番多いのですが、大規模運用している現場だとv2.6系への移行コストがどのくらいなのか気になります。

今回の検証でLua言語自体に強い興味を持ちましたので、C言語との連携等も含めいろいろ試していきたいと思います。

* 参考
EVAL - Redis
Lua: A Guide for Redis Users
Redis reliable queues with Lua scripting

 

Tags: , ,

ScalaでOpenCVを使って画像処理

OpenCV meets Scala

OpenCV meets Scala


OpenCV 2.4.4から正式にJava APIをAndroidプラットフォーム以外でも使えるようになりました。つまりJVM(Java Virtual Machine)上で動く言語ならどんな言語からでもOpenCVが使えるということですね。
* OpenCV now supports desktop Java

、ということで今回はJVM上で動作するScalaからOpenCVを使ってみます。

環境

* CentOS 5.8 (x86_64, 仮想2コア, 1GB RAM)
* Scala 2.10.0 (sbt 0.12)
* Java 1.6.0 (java-1.6.0-openjdk.x86_64)
* OpenCV 2.4.4

Java APIを使う際にAndroidプラットフォームを意識する必要はもうなくなりましたので、ここではアプリケーションサーバから使えるようにLinux機に環境構築して試しました。OpenCVのLinuxへのインストール手順については技術Wikiにメモを残していますのでそちらを参照してください。
* OpenCV - 2.x Tech Note
公式サイトではeclipseやsbtなどでのプロジェクト構築手順もスクリーンショット付きで載っているのでこちらも参考に。
* Introduction to Java Development

ちなみにMac OSXではHomebrewのOpenCVが2.4.4にアップデートされていたので、

$ brew install opencv

でインストールできます。簡単ですけどcmakeのオプションを自由にいじれないのでオススメできないかも。

sbt(Simple Build Tool)

sbt(Simple Build Tool)はScala/Java用のビルドツール。ライブラリの依存関係を自動で解決してくれるし、Scalaで書かれたDSLを使ってビルド設定を簡潔に記述できます。今回はEclipseのようなIDEは使わずにemacs + sbtでストレスなく開発ができました。
参考: 始める sbt

OpenCVのインストールに成功したら、ライブラリファイル(opencv-244.jar, libopencv_java244.so)をsbtプロジェクトのlibディレクトリにコピーしておきます。sbtが自動でクラスパスを追加してくれます。

Hello OpenCV in Scala

Javadocを参考にしながら進めます。
* OpenCV Javadoc

core

まずは基本から。JNI(Java Native Interface)経由でMatの機能を使えるか試します。

import org.opencv.core.Core
import org.opencv.core.Mat
import org.opencv.core.CvType
import org.opencv.core.Scalar

object HelloOpenCV {
  def main(args:Array[String]) {
    println("Welcome to OpenCV " + Core.VERSION)
    // 共有ライブラリ(libopencv_java244.so)をロード
    System.loadLibrary(Core.NATIVE_LIBRARY_NAME)

    println("Welcome to OpenCV " + Core.VERSION)
    // 5行10列1チャンネルの行列(要素の型は符号無し8ビット整数)を生成
    val m1 = new Mat(5, 10, CvType.CV_8UC1, new Scalar(0))
    println("OpenCV Mat: " + m1)
    // 2行目(インデックスは0から)を取得
    val m1r1 = m1.row(1)
    // 2行目の要素の値を全て1にセット
    m1r1.setTo(new Scalar(1))
    // 6列目(インデックスは0から)を取得
    val m1c5 = m1.col(5)
    // 6列目の要素の値を全て5にセット
    m1c5.setTo(new Scalar(5))
    println("OpenCV Mat data:\n" + m1.dump())

    // 3行3列1チャンネルの行列(要素の型は32ビット浮動小数点数)を生成
    val m2 = new Mat(3, 3, CvType.CV_32FC1)
    // 一様分布乱数(0~25)を使って行列の要素に値をセット
    Core.randu(m2, 0, 25)
    println(m2.dump())
    val (v1, v2, v3, v4) = (new Mat, new Mat, new Mat, new Mat)
    // 行列を1行に縮小 (要素の値は合計/平均/最小/最大)
    Core.reduce(m2, v1, 0, Core.REDUCE_SUM)
    Core.reduce(m2, v2, 0, Core.REDUCE_AVG)
    Core.reduce(m2, v3, 0, Core.REDUCE_MIN)
    Core.reduce(m2, v4, 0, Core.REDUCE_MAX)
    println("reduce sum: " + v1.dump())
    println("reduce avg: " + v2.dump())
    println("reduce min: " + v3.dump())
    println("reduce max: " + v4.dump())
  }
}

* 実行結果

$ sbt run
[info] Loading project definition from /home/ryo/workspace/src/opencv/samples/project
[info] Set current project to HelloOpenCVScala (in build file:/home/ryo/workspace/src/opencv/samples/)
[info] Compiling 1 Scala source to /home/ryo/workspace/src/opencv/samples/target/scala-2.10/classes...
[info] Running Main
Welcome to OpenCV 2.4.4.0
Mat: Mat [ 5*10*CV_8UC1, isCont=true, isSubmat=false, nativeObj=0xfe61740, dataAddr=0xfe61800 ]
Mat data:
[0, 0, 0, 0, 0, 5, 0, 0, 0, 0;
  1, 1, 1, 1, 1, 5, 1, 1, 1, 1;
  0, 0, 0, 0, 0, 5, 0, 0, 0, 0;
  0, 0, 0, 0, 0, 5, 0, 0, 0, 0;
  0, 0, 0, 0, 0, 5, 0, 0, 0, 0]
[13.25707, 4.9814796, 10.026485;
  20.359627, 10.928325, 6.2197423;
  19.327625, 19.052343, 7.6948619]
reduce sum: [52.944321, 34.962147, 23.94109]
reduce avg: [17.648108, 11.654049, 7.9803634]
reduce min: [13.25707, 4.9814796, 6.2197423]
reduce max: [20.359627, 19.052343, 10.026485]
[success] Total time: 11 s, completed 2013/03/17 17:43:34

行列演算はネイティブ側で行われるので高速に処理できます。

highgui, imgproc

ファイルI/Oや画像処理も試してみます。sbtプロジェクトの src/main/resources ディレクトリに画像ファイルを置いておきます。

// 画像ファイルへのパスを取得
val filePath = getClass.getResource("/src.png").getPath
// 画像ファイルの読み込み
val src = Highgui.imread(filePath)
// Cannyフィルタでエッジ検出
val edge = new Mat
Imgproc.Canny(src, edge, 80, 100)
// 半分の大きさにリサイズ
Imgproc.resize(edge, edge, new Size(), 0.5, 0.5, Imgproc.INTER_AREA)
// 画像ファイルの書き込み
Highgui.imwrite("edge.png", edge)

Highgui周りはちゃんと動くか心配してたのですが、I/O処理は特に問題ないようでした。

features2d

特徴点検出/特徴量記述、マッチング処理も試してみました。特徴量はパテントフリーで使いやすいORB特徴を使っています。

// ORB特徴点検出
val detect = (mat:Mat) => {
  val keypoints = new MatOfKeyPoint
  FeatureDetector.create(FeatureDetector.ORB).detect(mat, keypoints)
  (mat, keypoints)
}
// ORB特徴量記述
val extract = (t:Tuple2[Mat, MatOfKeyPoint]) => {
  val descriptors = new Mat
  DescriptorExtractor.create(DescriptorExtractor.ORB).compute(t._1, t._2, descriptors)
  (t._2, descriptors)
}
// 関数合成
val detectAndExtract = extract compose detect

// 画像ファイル読み込み
val img1 = Highgui.imread(getClass.getResource("/img1.png").getPath)
val img2 = Highgui.imread(getClass.getResource("/img2.png").getPath)

// 合成関数を適用して特徴点(キーポイント)検出 & 特徴量記述
val (keyPoints1, descriptors1) = detectAndExtract(img1)
val (keyPoints2, descriptors2) = detectAndExtract(img2)

// マッチング (ハミング距離)
val matcher = DescriptorMatcher.create(DescriptorMatcher.BRUTEFORCE_HAMMINGLUT)
val matches = new MatOfDMatch
// Scalaでは match は予約語のためバッククォートで括る
matcher.`match`(descriptors1, descriptors2, matches)
// 距離が近い上位100点を選択
val filtered = matches.toArray.sortBy(_.distance).reverse.take(100)
val filteredMatches = new MatOfDMatch(filtered:_*)

// マッチング結果を画像ファイルに描画
val resultImg = new Mat
Features2d.drawMatches(img1, keyPoints1, img2, keyPoints2, filteredMatches, resultImg)
Highgui.imwrite("result.png", resultImg)

matching_result
多少は関数型っぽくしてみましたけど、Javaの道具を使ってるので普通のJavaプログラムと変わらないように見えてしまうのは仕方ないですかね。

C++ APIで提供されている FeatureDetector/DescriptorExtractor クラスの機能はJava APIでもほぼそのまま利用できるようです。また、上記のサンプルコード内で MatOfKeyPoint や MatOfDMatch というクラスが登場していますが、どうやらJava側では std::vector を Mat のサブクラスとして扱い、JNIで渡すときに内部でコンテナ変換しているようです。ちなみにその変換部分の実装はOpenCVソースツリーの modules/java/generator/src/cpp/generators.cpp にあります。

C++コンテナJavaクラス
std::vector<{型名}>org.opencv.core.MatOf{型名}

それぞれのクラスには toArray や toList メソッドが用意されていて、配列やリスト(java.util.List)のインスタンスに変換できるので運用はしやすいと思います。

以上、簡単にですがOpenCVのJava APIをScalaから使ってみました。他のJVM言語からでも簡単に使えそうなので興味ある方は是非試してみてください。

Scalaを使ってみた感想

ScalaにはREPL(Read-Eval-Print-Loop: 対話型評価環境)があるので思いついた処理を手軽に試すことができて良いですね。それに最近のJVMは速いのでパフォーマンス的にもあまり心配することはなさそうです。

一方、Scalaがどういう風に型推論するのか見切れないから結局は型を明示してしまうという、僕のようなScala初心者は多いのではないかと思います。ScalaでRubyのようにスラスラ書き下すのは難しいです。。また、ScalaをBetter Javaとして使うだけなら学習コストは低いですが、関数型言語としてしっかり使っていくにはかなり学習が必要だと感じました。

また、ScalaはTwitterでの利用実績があることは有名だと思いますが、中の人がEffective Scala(邦訳)というドキュメントを残しています。Scalaを勉強するなら一度は読んでみると良いと思います。

 

Tags: , ,

OpenCV for Android入門 – カメラ編

ここ最近はAndroidアプリ開発の勉強をしています。今回はOpenCVでデバイスのカメラを利用した動画像処理を試してみました。

技術Wikiの方にもAndroid関連のメモを残しています。
* Android - Tech Note
* OpenCV for Android - Tech Note

Portions of this page are modifications based on work created and shared by Google and used according to terms described in the Creative Commons 3.0 Attribution License.

環境

* OpenCV4Android SDK 2.4.4 (今回はJava APIのみ使用)
* 検証デバイス: Galaxy S2 LTE(Android 2.3.6), AQUOS PHONE ZETA SH-02E(Android 4.0.4)
* 開発機: MacBook Air Mid 2012

基本的にAndroid 2.2(API Level 8)でも動作するように確認しながら進めていきたいと思います。

環境構築とサンプルアプリのビルドまでの手順は、公式サイトにスクリーンショット付きで丁寧に説明してくれています。Windows 7用のガイドですが、ほぼEclipse上での手順説明なのでMacでもパスを読み替える程度で簡単にできました。僕の場合はSDKを ~/Library/OpenCV2.4.4 以下にインストールしておきました。
* OpenCV4Android SDK — OpenCV 2.4.4 documentation

OpenCV Managerについて

OpenCV4Android SDK 2.4.2からライブラリ自体はOpenCV Managerというアプリで管理する構成に変更されました。これまでのようにアプリのバイナリにライブラリをコピーするのは無駄だからというのが主な理由とのことです。デバイスのアーキテクチャをOpenCV Managerが判別して適切なライブラリをインストールしてくれます。

using OpenCV Manager

using OpenCV Manager, feel the difference


確かにサイズは大きく減っていますね。ただ、アプリのユーザーにOpenCVを意識させることになるので(OpenCV Managerを別途インストールしてもらう必要がある)その辺りは微妙な気はしますけど。実際にAndroid端末にOpenCV ManagerをGoogle Playからインストール、実行してみました。ライブラリのインストールが成功すると以下のような画像が見られます。
opencv_manager_aquosphone

カメラアクセスの許可

OpenCVの使い方の前に、けっこう忘れがちなカメラアクセスの許可設定を行っておきます。AndroidManifest.xml ファイルを以下のように編集します。

<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>

以下、OpenCVでデバイスのカメラを利用した動画像処理を行う際の基本フローを整理していきます。

1. レイアウトの定義

OpenCVではカメラを扱う2つのクラス JavaCameraViewNativeCameraView が提供されています(両者の違いは後述)。これのどちらかをビューとしてXMLレイアウトファイル(res/layout/*.xml)に配置します。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:opencv="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:keepScreenOn="true"
    tools:context=".MainActivity" >
    
  <org.opencv.android.JavaCameraView
    android:id="@+id/camera_view"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:visibility="visible"
    opencv:camera_id="any"
    opencv:show_fps="true" />

</RelativeLayout>

ここでは JavaCameraView を配置しています。利用できる属性は camera_id (any/front/back) と show_fps (true/false) です。camera_id には利用するカメラ(フロント/バックカメラ)を指定するのですが、any を指定しておけば自動的にカメラを判別してくれます。show_fps を true にすると画面左上にFPSと画角が表示されます。デバッグ時は有効にしておくと良いです。

2. OpenCVライブラリの読み込みと初期化

OpenCV Managerを使う場合、Activityの中でOpenCVライブラリの読み込みを行う OpenCVLoader.initAsync メソッドを呼び出す必要があります。このメソッドは非同期で実行されるので、読み込み完了後にUIスレッドで呼ばれるコールバックメソッドも併せて実装しておきます。

public class MainActivity extends Activity implements CvCameraViewListener {
    // カメラビューのインスタンス
    // CameraBridgeViewBase は JavaCameraView/NativeCameraView のスーパークラス
    private CameraBridgeViewBase mCameraView;

    // ライブラリ初期化完了後に呼ばれるコールバック (onManagerConnected)
    // public abstract class BaseLoaderCallback implements LoaderCallbackInterface
    private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {
        @Override
        public void onManagerConnected(int status) {
            switch (status) {
            // 読み込みが成功したらカメラプレビューを開始
            case LoaderCallbackInterface.SUCCESS:
                mCameraView.enableView();
                break;
            default:
                super.onManagerConnected(status);
                break;
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // カメラビューのインスタンスを変数にバインド
        mCameraView = (CameraBridgeViewBase) findViewById(R.id.camera_view); 
        // リスナーの設定 (後述)
        mCameraView.setCvCameraViewListener(this);
    }

    @Override
    protected void onResume() {
        super.onResume();
        // 非同期でライブラリの読み込み/初期化を行う
        // static boolean initAsync(String Version, Context AppContext, LoaderCallbackInterface Callback)
        OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_2_4_4, this, mLoaderCallback);
    }

JavaCameraViewとNativeCameraViewの違い

JavaCameraView と NativeCameraView は両方とも CameraBridgeViewBase のサブクラスです。
opencv4android_cameraview
JavaCameraView は android.hardware.Camera を利用した実装、NativeCameraView はOpenCVの VideoCapture を利用した実装という違いがあります。安定性という観点ではAndroid SDKの機能を利用した JavaCameraView の方が安定しているかと思われますが、そんなに気にしなくて大丈夫でしょう。

CvCameraViewListenerインタフェースの実装

CvCameraViewListener または CvCameraViewListener2 インタフェースの以下の3つのメソッドを実装します。
* onCameraViewStarted
* onCameraViewStopped
* onCameraFrame
3つめの onCameraFrame メソッドに任意の画像処理を実装することになります。また、CvCameraViewListener と CvCameraViewListener2 の違いは onCameraFrame の引数のみで、onCameraViewStarted および onCameraViewStopped は共通です。

@Override
public void onCameraViewStarted(int width, int height) {
// カメラプレビュー開始時に呼ばれる
}

@Override
public void onCameraViewStopped() {
// カメラプレビュー終了時に呼ばれる
}
    
// CvCameraViewListener の場合
@Override
public Mat onCameraFrame(Mat inputFrame) {
// フレームをキャプチャする毎(30fpsなら毎秒30回)に呼ばれる
}
    
// CvCameraViewListener2 の場合
@Override
public Mat onCameraFrame(CvCameraViewFrame inputFrame) {
}

CvCameraViewFrame インタフェースは以下のようになっています。CvCameraViewListener2.onCameraFrame ではこのインタフェースを実装した JavaCameraFrame/NativeCameraFrame のインスタンスが渡されます。

public interface CvCameraViewFrame {
    // 4チャンネルRGBAカラーのMatインスタンスを返す
    public Mat rgba();

    // 1チャンネルグレースケールのMatインスタンスを返す
    public Mat gray();
};

キャプチャしたフレーム画像をそのままカメラプレビューに表示させるには、CvCameraViewListener.onCameraFrame の場合は引数の inputFrame をそのまま return し、CvCameraViewListener2.onCameraFrame の場合は inputFrame.rgba() を return します。

OpenCVでカメラを利用した動画像処理を行う際の基本フローは以上になります。ここまでのまとめとして、カメラから取得したフレーム画像を線画風に変換する処理のサンプルコードを載せておきます。

// package, import文は省略

public class MainActivity extends Activity implements CvCameraViewListener2 {
    private CameraBridgeViewBase mCameraView;
    private Mat mOutputFrame;

    private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {
        @Override
        public void onManagerConnected(int status) {
            switch (status) {
            case LoaderCallbackInterface.SUCCESS:
                mCameraView.enableView();
                break;
            default:
                super.onManagerConnected(status);
                break;
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mCameraView = (CameraBridgeViewBase)findViewById(R.id.camera_view);
        mCameraView.setCvCameraViewListener(this);
    }
    
    @Override
    public void onPause() {
        if (mCameraView != null) {
            mCameraView.disableView();
        }
        super.onPause();
    }

    @Override
    protected void onResume() {
        super.onResume();
        OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_2_4_4, this, mLoaderCallback);
    }
    
    @Override
    public void onDestroy() {
        super.onDestroy();
        if (mCameraView != null) {
            mCameraView.disableView();
        }
    }
    
    @Override
    public void onCameraViewStarted(int width, int height) {
        // Mat(int rows, int cols, int type)
        // rows(行): height, cols(列): width
        mOutputFrame = new Mat(height, width, CvType.CV_8UC1);
    }

    @Override
    public void onCameraViewStopped() {
        mOutputFrame.release();
    }

    @Override
    public Mat onCameraFrame(CvCameraViewFrame inputFrame) {
        // Cannyフィルタをかける
        Imgproc.Canny(inputFrame.gray(), mOutputFrame, 80, 100);
        // ビット反転
        Core.bitwise_not(mOutputFrame, mOutputFrame);
        return mOutputFrame;
    }
}

opencv4android_linedraw

opencv4android line drawing


Galaxy S2 LTEでだいたい15fpsほどで動作しました。スマートフォンでもこの速度が出せるのには驚きました。

今回はAndroidでOpenCVを使ったアプリ開発を試してみましたが、特に難しいところもなくスムーズに進めることができました。OpenCVがAndroidをサポートした当初はバグだらけで大変だったらしいですが、現在はとても使いやすくなっているので昔挫折してしまった人はリベンジしてみてください。これからも引き続きいろいろ試していきたいです。

* 参考
opencv.org - Home > PLATFORMS > ANDROID
Android | Blog はじめました

 

Tags: , ,

NginxとFFmpegを利用したHTTP Live Streaming配信

HTTP Live Streaming(HLS)配信の基本的な手順をまとめます。

去年の記事 「NginxのHTTP Pseudo-Streamingを試す」 ではNginxの疑似ストリーミング配信モジュールを試してみましたが、機能不足のため実サービスで使うのは難しいです。そのためWebサーバでストリーミング配信を行いたい場合は今回紹介するHLSなどの利用が推奨されます。

HTTP Live Streaming(HLS)とは

Apple公式のドキュメントを読む方が理解は進むと思いますが、一応ここでも簡単に概要を。

HTTP Live Streaming (also known as HLS) is an HTTP-based media streaming communications protocol implemented by Apple Inc.

HTTP Live Streaming(HLS)はHTTPベースのストリーミング配信プロトコルでAppleが開発しました。iOS 3.0/Android 4.0以降を搭載しているデバイスで利用可能です。

http live streaming

HTTP Live Streamingのアーキテクチャ概要


アーキテクチャは上図のように"Server"/"Distribution"/"Client"の3つで構成されています。まず、"Server"コンポーネントで元素材をMPEG-2 TS形式にエンコードし、ストリームセグメンタと呼ばれるツールを使って細かく分割されたメディアセグメントファイル(一連の低容量TSファイル)とインデックスファイル(M3U8プレイリスト)を生成します。そのファイル群を"Distribution"コンポーネント(一般的なWebサーバ)にデプロイします。ここまでで配信準備は完了です。あとはHLSに対応している"Client"ソフトウェア(動画再生プレイヤー)が"Distribution"に設置されたインデックスファイルにアクセスすれば配信が開始されます。

インデックスファイル(M3U8プレイリスト)

HLSのインデックスファイルはM3U形式(MP3プレイリスト)を拡張したM3U8プレイリストです。簡単な例を以下に示します。これはストリーム全体が各10秒の3つのメディアファイルに分割されているプレイリストになります。

#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:1
#EXTINF:10,
media/segment0.ts
#EXTINF:10,
media/segment1.ts
#EXTINF:10,
media/segment2.ts
#EXT-X-ENDLIST

HLSはRTSPやMMSなどのストリーミング用プロトコルではなくHTTPを使うため、特別なストリーミングサーバを調達する必要がなく、ApacheやNginxなどの一般的なWebサーバで動画像を配信できるのでコスト面でも大きなメリットがあります。

HLS配信対応のよくあるシチュエーション

これまでPC向けにFlash Videoによる映像配信サービスを提供していたが、今の時代、スマートフォンやタブレットPCへも確実にリーチする必要がある。そのため、Flashを再生できないiOS/Android端末向けにはHTTP Live Streamingによる配信を行うことになった。

環境

CentOS 5.8 (x86_64, 仮想2コア) さくらのVPS 1Gプラン
GCC 4.4.7 (rpm: gcc44-4.4.7-1.el5/gcc44-c++-4.4.7-1.el5)
FFmpeg 1.1.1
Nginx 1.3.0
検証デバイス: iPhone 5(iOS 6.1), iPad 2(iOS 6.0), AQUOS PHONE ZETA SH-02E(Android 4.0.4)

CentOSのバージョンは5.x系としました。大規模配信インフラを運用している現場だと6.x系への移行はまだ難しいのでは、という判断からです。そう考えるとGCC4.4.7も新しいですけど、最適なバイナリを作るために一部のコンパイルオプションをどうしても使いたかったので4.4.7としました。また、動画像の配信を行うHTTPサーバはなんでも良いです。ここではNginxを使いますが、もちろんApacheでも問題なくHLS対応可能です。

FFmpegのインストール

MPEG-2 TSファイルへの変換やセグメント分割、プレイリスト(M3U8)作成にはFFmpegを利用しました。以前はオープンソースのストリームセグメンタ(segmenter)を併用することが多かったですが、現在ではFFmpeg単体でプレイリスト作成まで可能です。

yumでインストールする場合

yumでインストールする場合はRPMForgeリポジトリを追加しておきます。

## もし追加していなければRPMForgeリポジトリを追加
$ wget http://pkgs.repoforge.org/rpmforge-release/rpmforge-release-0.5.2-2.el5.rf.x86_64.rpm
$ rpm -ivh rpmforge-release-0.5.2-2.el5.rf.x86_64.rpm
## ffmpeg, ffmpeg-develのインストール
$ yum install --enablerepo=rpmforge ffmpeg.x86_64 ffmpeg-devel.x86_64
$ ffmpeg
FFmpeg version 0.6.5, Copyright (c) 2000-2010 the FFmpeg developers
  built on Jan 29 2012 23:55:02 with gcc 4.1.2 20080704 (Red Hat 4.1.2-51)
  configuration: --prefix=/usr --libdir=/usr/lib64 --shlibdir=/usr/lib64 --mandir=/usr/share/man --incdir=/usr/include --disable-avisynth --extra-cflags='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector --param=ssp-buffer-size=4 -m64 -mtune=generic -fPIC' --enable-avfilter --enable-avfilter-lavf --enable-libdirac --enable-libfaac --enable-libfaad --enable-libfaadbin --enable-libgsm --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libx264 --enable-gpl --enable-nonfree --enable-postproc --enable-pthreads --enable-shared --enable-swscale --enable-vdpau --enable-version3 --enable-x11grab
  libavutil     50.15. 1 / 50.15. 1
  libavcodec    52.72. 2 / 52.72. 2
  libavformat   52.64. 2 / 52.64. 2
  libavdevice   52. 2. 0 / 52. 2. 0
  libavfilter    1.19. 0 /  1.19. 0
  libswscale     0.11. 0 /  0.11. 0
  libpostproc   51. 2. 0 / 51. 2. 0
Hyper fast Audio and Video encoder
usage: ffmpeg [options] [[infile options] -i infile]... {[outfile options] outfile}...

バージョンが0.65と古いです。ストリームセグメンタも実装されていないので前述のセグメンタを別途インストールする必要があります。

## segmenterのコンパイル/インストール
## (古いバージョンの libavformat に対応しているので注意)
$ svn co http://svn.assembla.com/svn/legend/segmenter
$ cd segmenter
$ make
$ sudo make install

ソースからコンパイルしてインストールする場合

最新安定版のFFmpegを使いたい場合はソースコードからコンパイルしてインストールします。せっかく自前でコンパイルするなら環境に合わせて最適なバイナリを作ると良いです。また、ディスクスペースは100MB以上必要になりますので注意してください。

## FFmpegのソースコードを取得
$ wget http://ffmpeg.org/releases/ffmpeg-1.1.1.tar.bz2
$ tar jxf ffmpeg-1.1.1.tar.bz2
$ cd ffmpeg-1.1.1
## CentOSでgcc4.4系を使いたい場合は --cc=gcc44 の指定を忘れずに
## 最適化オプションは各自環境に合わせてください
$ ./configure --prefix=/usr/local --cc=gcc44 --extra-cflags='-march=native -mfpmath=sse -msse2' --optflags='-O2 -finline-functions' --d
isable-avisynth --enable-avfilter --enable-libfaac --enable-libgsm --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb
 --enable-libtheora --enable-libvorbis --enable-libx264 --enable-postproc --enable-pthreads --enable-shared --enable-swscale --enable-vdpau
--enable-x11grab --enable-gpl --enable-nonfree --enable-version3
$ make -j2
$ sudo make install
$ ffmpeg
ffmpeg version 1.1.1 Copyright (c) 2000-2013 the FFmpeg developers
  built on Feb  3 2013 12:56:16 with gcc 4.4.7 (GCC) 20120313 (Red Hat 4.4.7-1)
  configuration: --prefix=/usr/local --cc=gcc44 --extra-cflags='-march=native -mfpmath=sse -msse2' --optflags='-O2 -finline-functions' --disable-avisynth --enable-avfilter --enable-libfaac --enable-libgsm --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libtheora --enable-libvorbis --enable-libx264 --enable-postproc --enable-pthreads --enable-shared --enable-swscale --enable-vdpau --enable-x11grab --enable-gpl --enable-nonfree --enable-version3
  libavutil      52. 13.100 / 52. 13.100
  libavcodec     54. 86.100 / 54. 86.100
  libavformat    54. 59.106 / 54. 59.106
  libavdevice    54.  3.102 / 54.  3.102
  libavfilter     3. 32.100 /  3. 32.100
  libswscale      2.  1.103 /  2.  1.103
  libswresample   0. 17.102 /  0. 17.102
  libpostproc    52.  2.100 / 52.  2.100
Hyper fast Audio and Video encoder
usage: ffmpeg [options] [[infile options] -i infile]... {[outfile options] outfile}...

最新版のFFmpegではストリームセグメンタが実装されているので、セグメンタの別途インストールは不要です。

上記オプションで作られたバイナリは libfaac を組み込んでいるため再配布はできません。
配布を考えないのであれば --march=native をつけてコンパイルして良いかと思います。これを付けると環境に合わせて最適なオプションに展開してくれます。ただし、古いGCCだとこのオプションは効かないので注意してください。ちなみに僕の環境(さくらのVPS 1Gプラン)では以下のように展開されるようです。

-march=core2 -msahf --param l1-cache-size=32 --param l1-cache-line-size=64 --param l2-cache-size=4096 -mtune=core2
各種コーデック/コンテナライブラリの紹介

FFmpegに別途組み込んだコーデック/コンテナライブラリと各プロダクトへのリンクを載せておきます。基本的に ./configure make make install でインストールできます。
参考: Install FFMPEG & x264 on CentOS

x264VideoLAN - x264, the best H.264/AVC encoder
OggXiph.org: Downloads
TheoraXiph.org: Downloads
VorbisXiph.org: Downloads
LAMELAME MP3 Encoder :: Software Downloads
FAACAudioCoding.com - Downloads
OpenCore AMRopencore-amr - Project Web Hosting - Open Source Software

FFmpegは大きなソフトウェアなのでスムーズにコンパイルできないこともよくあります。僕の環境で発生したエラーとその対応方法を載せておきます。

* 「yasm not found, use --disable-yasm for a crippled build」 とエラーが出る場合
–disable-yasm は使わずにちゃんとyasmをインストールしておきます。yumで入るものは古すぎてx264のビルドができないため、こちらもソースからコンパイルして入れます。また、yasmのビルドにはCMakeを使うので、もし入っていなければ予めインストールしておきます(最近はCMake対応しているプロダクトが増えてきましたね)。

## もしCMakeが入っていなければインストール
$ sudo yum install cmake.x86_64
## yasmのソースコードを取得
$ git clone git://github.com/yasm/yasm.git
$ cd yasm
## CentOSでgcc4.4系を使いたい場合は gcc44/g++44 をオプション指定する
$ cmake -DCMAKE_C_COMPILER=gcc44 -DCMAKE_CXX_COMPILER=g++44 .
$ make
$ sudo make install
$ /usr/local/bin/yasm --version
yasm 1.2.0.22.g607fe
Compiled on Jan 19 2013.
Copyright (c) 2001-2011 Peter Johnson and other Yasm developers.
Run yasm --license for licensing overview and summary.

* 「Unknown option "--enable-XXXX".」とエラーが出る場合
古いバージョンのFFmpegで使われていたオプションを指定している可能性が高いです。configureオプションから外しておきます。

* 「ERROR: libx264 must be installed and version must be >= XXXX」とエラーが出る場合
x264のバージョンが古いです。もしyumでインストールしてしまっている場合は削除して、こちらもソースからコンパイルして入れます。

$ git clone git://git.videolan.org/x264.git
$ cd x264
## CentOSでgcc4.4系を使いたい場合はconfigureファイルを編集する
$ vim configure  ## 変数CCに gcc44 を指定
414c414
< CC="${CC-${cross_prefix}gcc}"
---
> CC="${CC-${cross_prefix}gcc44}"
$ ./configure --enable-shared --enable-pic --extra-cflags="-march=native -mfpmath=sse -msse2"
$ make
$ sudo make install
$ /usr/local/bin/x264 --version                                          
x264 0.129.2245M bc13772
built on Jan 19 2013, gcc: 4.4.7 20120313 (Red Hat 4.4.7-1)
configuration: --bit-depth=8 --chroma-format=all
x264 license: GPL version 2 or later

--extra-cflags オプションは各自環境に合わせて指定してください。

エンコード/プレイリスト作成

FFmpegを利用してHLS配信用に動画像ファイルのエンコードとプレイリストの作成を行います。

* MPEG-2 TS 1280x720(16:9) 映像: H.264/AVC [email protected] 24fps 256kbps, 音声:AAC 128kbps
* HLS: VOD, 10秒毎に分割指定 (segment_time), キャッシュ許可

## セグメントファイル群を置くディレクトリ作成
$ mkdir streamfiles
$ ls
streamfiles/ test.mp4
## エンコード、プレイリスト作成
$ ffmpeg -i test.mp4 -threads 2 -codec:v libx264 -s:v 1280x720 -aspect:v 16:9 -b:v 256k -re -fpre:v libx264-hls.ffpreset -codec:a libfaac -ar:a 44100 -b:a 128k -ac:a 2 -map 0 -f segment -segment_format mpegts -segment_time 10 -segment_list stream.m3u8 streamfiles/stream%03d.ts

x264のプリセットは以下のように品質重視の設定にしました。今回の検証デバイスは全てHigh Profileに対応してるのですが、ここではMain Profile Level 3.1で作っています。詳細なエンコードパラメータについてはこれから煮詰めていこうと思いますが、実際にはエンコードサーバの資源(CPU/RAMなど)や配信開始までの猶予時間、再生保障するデバイスなどいろいろ折り合いがあるでしょうし、なかなか難しいところです。。

* libx264-hls.ffpreset

vprofile=main
coder=1
level=31
crf=22
qcomp=0.6
qmin=10
qmax=51
qdiff=4
i_qfactor=0.71
maxrate=14000k
bufsize=14000k
g=250
keyint_min=25
sc_threshold=40
me_method=umh
me_range=16
subq=6
refs=4
trellis=1
bf=16
b_strategy=1
partitions=+pi8x8+pi4x4+pp8x8+pb8x8
cmp=chroma
flags=+loop-global_header
deblock=0:0

生成されたM3U8プレイリストは以下のような内容になりました。FFmpegで指定できる分割時間は目安程度のものなんでしょうか? ここでは10秒指定にしたのですがTARGETDURATIONは15秒になりました。

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-ALLOWCACHE:1
#EXTINF:14.750000,
streamfiles/stream000.ts
#EXTINF:8.791667,
streamfiles/stream001.ts
#EXTINF:9.500000,
streamfiles/stream002.ts
#EXTINF:7.000000,
streamfiles/stream003.ts
#EXTINF:10.750000,
streamfiles/stream004.ts
#EXTINF:9.875000,
streamfiles/stream005.ts
#EXTINF:10.000000,
streamfiles/stream006.ts
... 省略
#EXTINF:9.916667,
streamfiles/stream095.ts
#EXTINF:9.500000,
streamfiles/stream096.ts
#EXTINF:1.834956,
streamfiles/stream097.ts
#EXT-X-TARGETDURATION:15
#EXT-X-ENDLIST

Nginxの設定

mime.typesファイルにMIME Typeを以下のように2つ追加しておきます。

* mime.types

types {
... 省略
    application/x-mpegURL                 m3u8;
    video/MP2T                            ts;

これだけ、簡単ですね。Apacheの場合も同様にMIME Typeの設定だけでOKです。

## Apache2.xの場合
AddType application/x-mpegURL .m3u8
AddType video/MP2T            .ts

クライアントWebページの作成

HTML5 videoタグを利用してクライアントWebページ(プレイヤー)を作ります。videoタグのsrc属性にはプレイリストファイルを指定してください。

* /test/hls/index.html

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no">
    <title>HTTP Live Streaming Test</title>
  </head>
  <body>
    <header>
      <h1>HTTP Live Streaming Test</h1>
    </header>
    <div>
      <video width="1280" height="720" src="stream.m3u8" preload="none" onclick="this.play()" controls />
    </div>
  </body>
</html>

再生試験

* 構成

test/hls
|-- stream.m3u8
|-- index.html
`-- streamfiles
    |-- stream000.ts
    |-- stream001.ts
    |-- stream002.ts
... 省略
    |-- stream095.ts
    |-- stream096.ts
    `-- stream097.ts

あとは、/test/hls/index.html にアクセスしてプレイヤーの再生ボタンを押すだけです。無事にコンテンツが再生されたら成功。

* アクセスログ例
Nginxのアクセスログを確認。クライアントは約10秒ごとに対応したTSファイルをリクエストしていることがわかります(以下のログはNginxのデフォルトフォーマットのログをIPなど一部情報を削って載せています)。

[27/Jan/2013:20:49:35 +0900] "GET /test/hls/streamfiles/stream013.ts HTTP/1.1" 200 586936 "-" "stagefright/1.2 (Linux;Android 4.0.4)"
[27/Jan/2013:20:49:44 +0900] "GET /test/hls/streamfiles/stream014.ts HTTP/1.1" 200 599720 "-" "stagefright/1.2 (Linux;Android 4.0.4)"
[27/Jan/2013:20:49:55 +0900] "GET /labs/hls/streamfiles/stream015.ts HTTP/1.1" 200 678116 "-" "stagefright/1.2 (Linux;Android 4.0.4)"

今回使った検証デバイスは新しい(2013/02現在)デバイスということもあり、高品質な設定でエンコードしたHD動画でもスムーズに再生してくれました(^^ エンコードパラメータの調整には時間をかけましたが、再生確認までは特につまづくところもありませんでした。あとは暗号化によるコンテンツ保護や帯域制御などを試していきたいと思います。

HTTP Live Streaming Test

HTTP Live Streaming Test

余談 「ITU、次世代コーデックのHEVC/H.265を承認」

つい先日、次世代ビデオコーデックのHEVC(High Efficiency Video Coding)/H.265がITUに承認されました。今ではインターネット業界でのサービス開発はスマートフォン向けにシフトしましたが、低帯域のモバイルネットワークでも高品質の映像をユーザーに届けられるというのは素晴らしいと思います。以前AdobeがFlashは「プレミアムビデオとゲーム市場にフォーカスする」と言ってましたが(参照: Flashランタイムのロードマップ)、もし本当ならいち早く対応してほしいなぁと願うところです。

* 参考:
HTTP Live Streaming Resources - Apple Developer
Install FFMPEG & x264 on CentOS
[iPhone] HTTP Live Streaming
HTTP Live Streaming(Encrypt)

 

Tags: , , ,