Androidのカメラ映像をMotionJPEGで配信する

# はじめに

こんにちは。Daddy's Officeの市川です。

長いこと監視カメラソフトの開発を続けていますが、未だによく利用するのがMotionJPEG形式での動画配信です。
その理由は、実装が楽で処理負荷も低く、監視カメラ用途では十分な性能が出るためです。

特に、監視カメラでは、各フレーム映像に対して何かしらの処理を行う場合が多いため、任意のフレームを自由に切り出すことができるMotionJPEG形式は、現在でも各社の監視カメラのネットワーク配信フォーマットとして採用されています。

今回は、このMotionJPEGを使用して、Androidのカメラ映像をネットワーク上に動画として配信する方法を説明します。

# ソースコード

Androidサンプルの全ソースコードは[こちらのGitHub][2]に置いてあります。

サンプルを実行すると、背面カメラの映像を10080ポートから、MotionJPEG形式で配信します。

ChromeかFirefoxで配信しているAndroid端末に接続すると、カメラ映像が確認できます。
(http://xxx.xxx.xxx.xxx:10080という感じで)


# MotionJPEGとは

細かい解説は[WiKiペディア Motion JPEG][1]を参照してください。

簡単に言うと、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文字列で判断します。

サーバからのレスポンスを図にすると、こんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382663/dc78e945-366a-8b25-27f2-2cf0168ed5d6.png)




ただ、このmultipart/mixed-replaceは、あまり広まりませんでした。
その為、すべてのブラウザでこのContentTypeが処理できるとは限りません。

私の手元のPC(Windows10)では、ChromeとFireFoxでは再生できましたが、IEとEdgeでは再生できませんでした。
※AndroidのChromeでも再生できました。

ということで、この形式のMotionJPEGを表示させるためには、サーバが一方的に送りつけてくるコマ送りJPEGを遅延なく受信⇒デコード⇒表示を繰り返し処理可能な能力をもったクライアントアプリケーションを作る必要があります。


ただ、今回は配信側の説明のみを行いますので、動作確認は上記のMotionJPEG再生可能なブラウザを使ってください。


# サンプルソースコード説明
Androidサンプルの全ソースコードは[こちらのGitHub][2]に置いてあります。

以下、各クラスの説明です。

|クラス名|説明|
|:--|:--|
|CameraPreview|カメラの画像を取得するSurfaceを保持したクラス|
|MJpegServer|MotionJPEGを配信する簡易HTTPサーバクラス|
|MainActivity|CameraPreviewが保持するプレビュー用Surfaceを張り付けるActivity|
|PermissionConfirmActivity|カメラ権限確認用Activity|



## カメラ映像の取得

古い端末でも動くように、カメラ映像の取得には[Camera API][5]を使用しています。
カメラ映像の取得は他にもサンプルがたくさんありますので、詳しくはそちらを参照してください。

ポイントは、カメラ映像フレームの取得処理です。
android.hardware.cameraの[setPreviewCallbackWithBuffer][6]を使用して、カメラのフレーム映像を取得していますが、取得できるフレーム画像をJPEGに変換する必要があります。
取得できるカメラ映像の色空間はYUV系になるので、それを[android.graphics.YuvImage][7]を使用してJPEGに変換しています。

```java:CameraPreview.java

    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を発生させることで、中断処理を可能にします。

```Java:MJpegServer.java
        //
        // サーバソケットを作成し、指定したポートにバインド
        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とし、それ以外はエラーとしています。


```java:MJpegServer.java

    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を返却します。

```java:MJpegServer
    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){

                }
            }
        }
    }

```

接続が切れるまで、繰り返しMotioJPEGを送信し続けます。

# ブラウザでの動作確認

Windows10上のChromeで、サンプルプログラムを起動したAndroidに接続すると、以下のようにカメラのライブ映像が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382663/382daf3e-19ca-ab4b-e5da-8029d203e56b.png)

# 最後に

ソースコードを見てもらえればわかりますが、MotionJPEGであれば、非常に単純なプログラムで動画配信ができます。

若干のカクツキなどが生じますが、表示するクライアント側のプレイヤーの開発もJPEGを表示するだけでOKなので、用途によってはとても有用なフォーマットだと思います。

また、最近はクラウドを介したサービスが多くなる中、サーバ側の運用費を抑えた動画配信サービスの開発も可能です。

MPEGライセンスなどの問題もなく、編集がやりやすいなど、MotionJPEGはまだまだ使える動画フォーマットだと思いますので、いろいろと試してみてください！



[1]:https://ja.wikipedia.org/wiki/Motion_JPEG
[2]:https://github.com/daddyYukio/broadcasttest
[3]:https://github.com/daddyYukio/broadcasttest/blob/master/app/src/main/java/com/daddysoffice/sample/broadcasttest/CameraPreview.java
[4]:https://github.com/daddyYukio/broadcasttest/blob/master/app/src/main/java/com/daddysoffice/sample/broadcasttest/MJpegServer.java

[5]:https://developer.android.com/guide/topics/media/camera
[6]:https://developer.android.com/reference/android/hardware/Camera.html#setPreviewCallbackWithBuffer(android.hardware.Camera.PreviewCallback)
[7]:https://developer.android.com/reference/android/graphics/YuvImage