【Java】Androidのカメラ映像をMotionJPEGで配信する方法
はじめに
こんにちは。Daddy's Officeの市川です。
長いこと監視カメラソフトの開発を続けていますが、未だによく利用するのがMotionJPEG形式での動画配信です。その理由は、実装が楽で処理負荷も低く、監視カメラ用途では十分な性能が出るためです。
特に、監視カメラでは、各フレーム映像に対して何かしらの処理を行う場合が多いため、任意のフレームを自由に切り出すことができるMotionJPEG形式は、現在でも各社の監視カメラのネットワーク配信フォーマットとして採用されています。
今回は、このMotionJPEGを使用して、Androidのカメラ映像をネットワーク上に動画として配信するアプリケーションの書き方(Java)を説明します。
ソースコード
サンプルアプリケーションの全ソースコードはこちらのGitHubに置いてあります。
サンプルを実行すると、背面カメラの映像を10080ポートから、MotionJPEG形式で配信します。
ChromeかFirefoxで配信しているAndroid端末に接続すると、カメラ映像が確認できます。
MotionJPEGとは
細かい解説はWiKiペディア Motion JPEGを参照してください。
簡単に言うと、JPEG画像の連続フレームで構成されたものがMotionJPEGです。
ただ、そのまとめ方(コンテナ)に関しては、定型フォーマットが存在していません。例えばビデオカメラなどではAVIコンテナにJPEGとLPCMを入れて、MotionJPEGと表記している物もありますし、MOV形式のMotionJPEGもあります。
ネットワークカメラなどのHTTP上のストリーミング形式のものは、Content-Type: multipart/x-mixed-replaceを使用して複数のJPEGを連続して送るフォーマットをMotionJPEGと表記しています。
つまり、JPEG画像の連続フレームで構成されたものは、すべてMotionJPEGと名乗っている状態です。
Panasonicなど、主要メーカ製のネットワークカメラは、ほとんどがHTTP上のmultipartストリーミング形式になっており、私が開発しているLiveCapture3でもこのmultipart方式で配信しています。
Multipart方式のMotionJPEGフォーマット
HTTP上でMotionJPEGを配信するのに使用される、multipart/x-mixed-replaceというのは、Netscape社が提唱した、サーバからのプッシュ配信の為のもので、クライアント側は、新たなpartが送られてきたら、表示中のものを破棄して新しいPartを表示する、という仕様になっています。
MotionJPEGを配信する場合、配信サーバはこのmultipart/mixed-replaceを使用して、クライアントがコネクションを切断するまでJPEG画像を流し続けます。
各JPEG画像の区切りの判断は、通常のmultipart同様、Content-Typeに記載したboundary文字列で判断します。
サーバからのレスポンスを図にすると、こんな感じです。
ただ、このmultipart/mixed-replaceは、あまり広まりませんでした。その為、すべてのブラウザでこのContentTypeが処理できるとは限りません。
私の手元のPC(Windows10)では、ChromeとFireFoxでは再生できましたが、IEとEdgeでは再生できませんでした。※AndroidのChromeでも再生できました。
ということで、この形式のMotionJPEGを表示させるためには、サーバが一方的に送りつけてくるコマ送りJPEGを遅延なく受信⇒デコード⇒表示を繰り返し処理可能な能力をもったクライアントアプリケーションを作る必要があります。
ただ、今回は配信側の説明のみを行いますので、動作確認は上記のMotionJPEG再生可能なブラウザを使ってください。
サンプルソースコード(Java)説明
Androidサンプルの全ソースコード(Java)はこちらのGitHubに置いてあります。
以下、各クラスの説明です。
- CameraPreview カメラの画像を取得するSurfaceを保持したクラス
- MJpegServer MotionJPEGを配信する簡易HTTPサーバクラス
- MainActivity CameraPreviewが保持するプレビュー用Surfaceを張り付けるActivity
- PermissionConfirmActivity カメラ権限確認用Activity
カメラ映像の取得
古い端末でも動くように、カメラ映像の取得にはCamera APIを使用しています。カメラ映像の取得は他にもサンプルがたくさんありますので、詳しくはそちらを参照してください。
ポイントは、カメラ映像フレームの取得処理です。android.hardware.cameraのsetPreviewCallbackWithBufferを使用して、カメラのフレーム映像を取得していますが、取得できるフレーム画像をJPEGに変換する必要があります。取得できるカメラ映像の色空間はYUV系になるので、それをandroid.graphics.YuvImageを使用してJPEGに変換しています。
private byte[] convertYuv2Jpeg(byte[] yuvData, int format, int w, int h) {
byte[] jpegData = null;
if (yuvData != null) {
try {
YuvImage yuvimage = new YuvImage(yuvData, format, w, h, null);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
yuvimage.compressToJpeg(new Rect(0, 0, w, h), 70, baos);
jpegData = baos.toByteArray();
baos.close();
}
catch (Exception e) {
e.printStackTrace();
}
}
return jpegData;
}
MotionJPEG配信サーバ起動
MotionJPEGの配信はHTTPで行いますので、簡易的なHTTPサーバを作っています。
MJpegServerのstartをコールすると、クライアント接続用の待ち受けスレッドが起動します。そのスレッド処理の頭で、サーバソケットを作成し、指定されたポート番号にバインドして、クライアントからの接続を待ちます。
JavaのSocketでは、クライアントの待ち受けを行うacceptを強制停止することができません。そのため、acceptを行うサーバソケットにsetSoTimeoutでタイムアウト値を設定して、定期的にSocketTimeoutExceptionを発生させることで、中断処理を可能にします。
//
// サーバソケットを作成し、指定したポートにバインド
socket = new ServerSocket();
socket.setReuseAddress(true);
socket.setSoTimeout(500); // 500msecでタイムアウト
socket.bind(new InetSocketAddress(mPort));
//
// クライアントの接続を待つ
while (isRunning()) {
Socket clientSock = null;
try {
clientSock = socket.accept();
talkToClient(clientSock);
}
catch (SocketTimeoutException e) {
// ソケットタイムアウト
}
catch (Exception e) {
e.printStackTrace();
}
}
クライアント接続
接続してきた各クライアントとの処理は、別のスレッドを起動して行います。
今回のサンプルでは、HTTP GETリクエストであればすべてOKとし、それ以外はエラーとしています。
private void talkToClient(final Socket socket){
new Thread(new Runnable() {
@Override
public void run() {
try{
//
// リクエストヘッダの解析
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
String line;
if ((line = in.readLine()) == null){
return;
}
String [] commands = line.split(" ");
if (commands.length < 2){
return;
}
//
// GETコマンド以外はエラーにする
if(commands[0].compareToIgnoreCase("GET") == 0) {
responseForVideo(socket);
}
else{
BufferedOutputStream out
= new BufferedOutputStream(socket.getOutputStream());
String response = "HTTP/1.1 400 Bad Request" + CRLF;
out.write(response.getBytes("US-ASCII"));
out.close();
}
}
catch(Exception e){
e.printStackTrace();
}
finally{
try{
socket.close();
}
catch(Exception e){
}
}
}
}).start();
}
MotionJPEG送信
クライアントソケットから、すべてのリクエスト情報を読みだした後に、レスポンスとして、MotionJPEGを返却します。
private void responseForVideo(Socket socket){
BufferedOutputStream out = null;
try{
socket.setTcpNoDelay(true);
socket.setSoTimeout(3000);
//
// レスポンスヘッダーを返却
out = new BufferedOutputStream(socket.getOutputStream());
String header = "HTTP/1.1 200 OK" + CRLF;
header += "Content-Type: multipart/x-mixed-replace; boundary=--myboundary" + CRLF;
header += CRLF;
out.write(header.getBytes("US-ASCII"));
//
// multipartの各パートを生成&返却
while(isRunning()){
//
// パートヘッダー
header = "--myboundary" + CRLF;
header += "Content-Length: " + String.valueOf(mFrame.length) + CRLF;
header += "Content-type: image/jpeg" + CRLF;
header += CRLF;
out.write(header.getBytes("US-ASCII"));
//
// JPEG画像
out.write(mFrame, 0, mFrame.length);
//
// パート終了(\r\n)
out.write(CRLF.getBytes("US-ASCII"));
//
// 少しお休み
Thread.sleep(100);
}
}
catch(Exception e){
e.printStackTrace();
}
finally {
if (out != null){
try{
out.close();
}
catch(Exception e){
}
}
}
}
ブラウザでの動作確認
Windows10上のChromeで、サンプルプログラムを起動したAndroidに接続すると、以下のようにカメラのライブ映像が表示されます。
最後に
ソースコードを見てもらえればわかりますが、MotionJPEGであれば、非常に単純なプログラムで動画配信ができます。
若干のカクツキなどが生じますが、表示するクライアント側のプレイヤーの開発もJPEGを表示するだけでOKなので、用途によってはとても有用なフォーマットだと思います。
また、最近はクラウドを介したサービスが多くなる中、サーバ側の運用費を抑えた動画配信サービスの開発も可能です。
MPEGライセンスなどの問題もなく、編集がやりやすいなど、MotionJPEGはまだまだ使える動画フォーマットだと思いますので、いろいろと試してみてください!
Discussion