共有メニュー追加2017/06/13

Androidカメラアプリ

に、画像の共有機能を追加しました。

撮った画像や動画を、すぐにTwitterに投稿したりメールに添付したりすることができます。


共有メニュー

「静止画キャプチャ」「動画キャプチャ」後の確認画面、タイトルバーに共有ボタン(共有メニューボタン)が追加されています。

共有メニュー

ボタンを押すと共有先のメニューが現れます。端末にインストールされているアプリによってメニューの内容は変わります。

Twitter共有

例えば Twitterを選択すると、このように投稿画面に遷移します。
ここからそのまま画像を Twitterに投稿することができます。

撮った画像の共有はアルバム等、別アプリで行えば良いと思っていましたが、調べてみたらそう難しいものではなかったので機能を追加してみました。


ということで、アプリに共有機能を追加する手順をごく簡単に挙げてみます。
詳しくはここにあるキーワードで検索して、ちゃんと解説しているサイトをご覧ください。

・共有メニュー
リソースにメニューを追加して、onCreateOptionsMenu()で設定します。
任意のタイミングで共有ボタンを追加・削除するときは invalidateOptionsMenu()を呼びます。

・共有データ
画像や動画を共有する場合 Intentにそのファイルの Urlを設定しますが、Android7.0からセキュリティの強化でちょっと準備が必要になりました。
 共有ディレクトリをリソースで指定
 AndroidManifest.xmlで FileProviderタグを追加
 FileProvider.getUriForFileで Urlを取得
という手順になります。


カメラ映像で顔検出2016/08/04

前回の記事の通り、Androidアプリ「おかしな(ヵ_ォ)カメラ」では顔検出に Google Play Serivcesの FaceDetectorを使っています。

このAPIの基本的な使い方はチュートリアル等にありますが、今回はカメラの映像で顔検出をする場合の手順と、引っかかった所や気づいたところを解説してみます。

参考にするのは提供されているサンプル
その中(FaceTrackerの下じゃないけど)でも、カメラの映像を顔検出APIに送っているのは
あたりです。


◇手順
・Andoroidでのカメラ画像の表示
・FaceDetectorのプロジェクトへの組み込み
については省略します。

FaceDetectorの生成

FaceDetector detector = new FaceDetector.Builder(getApplicationContext()).build();

こんな感じで顔検出用のクラスを生成します。
このとき、いくつかのパラメータを設定することができますが、その中で使用してみて注意点があったものを挙げてみます。

・setMode(int)
 FAST_MODEで速度重視、ACCURATE_MODEで精度重視となります。
 顔の範囲やZ軸回転(首をかしげる)はどちらでも取得できますが、FAST_MODEではY軸回転(横を向く)が取れなくなりました(常に0が返る)。ちなみにX軸回転(うつむく、見上げる)は元々取得するメンバがありません。何故?

・setMinFaceSize(float)
 検出する顔の最小値(画面に対する比率)を設定します。
 ただこの関数で大きめの値を設定すると、Face.getWidth()や Face.getHeight()で得られる顔の大きさにズレが生じます(画面上の顔が小さくなるほど、実際の顔より大きめの値が返ってくる)。
 設定するとズレるというより設定する値が大きいとズレが顕著になっているようなのですが、残念ながら対処法はわかりません。

・setTrackingEnabled(boolean)
 リアルタイムの映像で検出を行う場合は trueに設定するのですが、ちょっと気を付けなければいけない点があります。それについては後述します。


カメラ映像の取得
このへんは顔検出特有の部分では無いので簡単に済ませます。

カメラ入力画像は、Camera.setPreviewCallbackWithBuffer(Camera.PreviewCallback)と Camera.addCallbackBuffer(byte[])でコールバックを設定して取得します。
CameraSourceでは createCamera()の最後の部分、そしてコールバック関数自体は CameraPreviewCallbackクラスの onPreviewFrame(byte[], Camera)です。
コールバック関数にデータが来る → 顔検出 の繰り返しとなります。

また、顔検出処理は時間がかかるので別スレッドで行います。
CameraSourceでは、FrameProcessingRunnableクラスでその処理を管理しています。


顔検出準備
取得した画像データから、Frameクラスを生成します。

Frame frame = new Frame.Builder().setImageData(data, width, height, ImageFormat.NV21).build();

YUV形式に対応していますのでカメラから取得したデータをそのまま使えます。

・画像の向き
 顔検出APIは、検出する顔と画像の上下が合っていることが前提となっています。
 カメラ画像は横向きなので、端末を縦に使う場合等は画像を回転する必要があります。

Frame frame = new Frame.Builder().setImageData(data, width, height, ImageFormat.NV21).setRotation(rotation).build();

 setRotation(int)によって回転角を指定します。
 引数は ROTATION_0 ~ ROTATION_270と、90°単位です。
 CameraSource(というよりFaceTracker)では、screenOrientationを fullSensorにして getDefaultDisplay().getRotation()から計算しています(大雑把に言うと)。

 さて前述の setTrackingEnabled(true)での注意点ですが、
 「おかしな(ヵ_ォ)カメラ」のようなリアルタイムで端末の向きを変えられるアプリの場合、setTrackingEnabled(true)としていると向きが変わったときにエラーが出て顔の検出ができなくなってしまいます。画像としては不連続になるからなんだとは思いますが。
 そのため端末の向きが変わったときは FaceDetectorを一旦 release()して生成し直す必要があります。


顔検出
 あとは FaceDetectorに Frameを放り込んで顔検出情報 Faceの配列を取得するだけです。

SparseArray <Face> faces = detector.detect(frame);

 CameraSourceは顔検出専用ではないので、汎用的にDetectorクラスの receiveFrame(Frame)を使っています。ご注意を。

 個々の検出情報は、valueAt(int)で取り出します。

for (int i = 0; i < faces.size(); i++) {
    Face face = faces.valueAt(i);

    // 顔で遊ぼう!
}

 Faceで得られる情報は、リファレンスからどうぞ。



TextureViewで画面描画2016/05/10

画面描画に TextureViewを使用した Androidアプリのサンプルです。

・メインの処理は native
・描画は OpenGL ES
・フレーム毎の処理
という、ゲーム作成を前提とした仕様となっています。

プロジェクト一式は、こちらから。


今更な感もありますが、今まで GLSurfaceViewを使用していたのを TextureViewに変えてみました。
APIレベル14(Android4.0)未満では、TextureViewが使えないので SurfaceViewを使用するようになっています。

BaseActivity.java
package sys;

import android.app.KeyguardManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.AssetManager;
import android.graphics.SurfaceTexture;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
import android.view.WindowManager;
import android.widget.FrameLayout;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.egl.EGLDisplay;
import javax.microedition.khronos.egl.EGLSurface;


/********************
    アクティビティ
 ********************/
public class BaseActivity extends FragmentActivity implements Runnable
{
    static {
        System.loadLibrary("native");
    }


    private final static int    NATIVE_PRIORITY = android.os.Process.THREAD_PRIORITY_MORE_FAVORABLE;        // nativeスレッド優先度

    public final static int     KEY_BACK = 1;                   // バックキー
    public final static int     KEY_YES  = 2;                   // ダイアログ用
    public final static int     KEY_NO   = 3;

    private final static int    PHASE_RUN      = 0;             // 実行中
    private final static int    PHASE_INIT     = 1;             // 初期化
    private final static int    PHASE_CONTINUE = 2;             // 再開
    private final static int    PHASE_STOP     = 3;             // 中断
    private final static int    PHASE_FINISH   = 4;             // 終了


    protected FrameLayout   base_layout;                        // ベースレイアウト
    protected BaseView      base_view;                          // ビュー
    private int             phase;                              // 実行段階
    protected int           screen_width, screen_height;        // 画面の大きさ
    private final Object    sync_native = new Object();

    private ScheduledExecutorService    executor;               // 定期実行管理
    private ScheduledFuture<?>          future;
    private long            time0, time1;
    private int             frame_rate;                         // フレームレート

    private short[]         touch_status = new short[5*3];      // タッチパネル状態
    protected int           key_status = 0;                     // キー入力状態


    public native int       initNative(boolean _init, AssetManager _mgr);           // native部初期化
    public native void      setScreenNative(int _w, int _h);                        // native部画面サイズ設定
    public native void      quitNative();                                           // native部終了
    public native void      pauseNative();                                          // native部一時停止
    public native boolean   updateNative(boolean draw, short _touch[], int _key);   // native部稼働


    /**********
        開始
     **********/
    @Override
    protected void  onCreate(Bundle _savedInstanceState)
    {
        onCreate2(_savedInstanceState, null, null);
    }

    protected void  onCreate2(Bundle _savedInstanceState, FrameLayout _base)
    {
        onCreate2(_savedInstanceState, _base, null);
    }

    protected void  onCreate2(Bundle _savedInstanceState, FrameLayout _base, int[] _attribs)
    {
        super.onCreate(_savedInstanceState);
        phase = (_savedInstanceState == null) ? PHASE_INIT : PHASE_CONTINUE;

        if ( _attribs == null ) {
            _attribs = (new int[]
                        {                                   // デフォルト 画面アトリビュート
                            EGL10.EGL_RED_SIZE,     8,
                            EGL10.EGL_GREEN_SIZE,   8,
                            EGL10.EGL_BLUE_SIZE,    8,
                            EGL10.EGL_DEPTH_SIZE,   0,
                            EGL10.EGL_STENCIL_SIZE, 0,
                            EGL10.EGL_NONE
                        });
        }
        base_view = new BaseView((Build.VERSION.SDK_INT < 14), _attribs);       // ベースビュー

        if ( _base == null ) {
            base_layout = base_view;
            setContentView(base_view);
        }
        else {
            base_layout = _base;
            base_layout.addView(base_view, 0);
        }
    }

    @Override
    protected void  onStart()
    {
        super.onStart();
        SoundManager.init();                        // サウンド管理初期化
    }

    /**********
        終了
     **********/
    @Override
    public void     finish()
    {
        phase = PHASE_FINISH;
        super.finish();
    }

    @Override
    protected void  onDestroy()
    {
        if ( phase == PHASE_FINISH ) {
            quitNative();                           // native部終了
        }
        super.onDestroy();
    }

    @Override
    protected void  onStop()
    {
        super.onStop();
        SoundManager.quit();                        // サウンド管理終了
    }

    /**********
        再開
     **********/
    @Override
    protected void  onResume()
    {
        super.onResume();

        executor = Executors.newSingleThreadScheduledExecutor();    // 描画スレッド管理

        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);                   // スリープ禁止
        for (int i = 0; i < 5; i++) {               // タッチパネル状態クリア
            touch_status[i*3] = 0;
        }
        if ( ((KeyguardManager)getSystemService(Context.KEYGUARD_SERVICE)).inKeyguardRestrictedInputMode() ) {
            receiver = new UnLockReceiver();
            registerReceiver(receiver, new IntentFilter(Intent.ACTION_USER_PRESENT));           // スクリーンロック解除待ち
        }
        else {
            start();
        }
    }

    private UnLockReceiver  receiver = null;                        // スクリーンロック解除検知用

    private class UnLockReceiver extends BroadcastReceiver
    {
        @Override
        public void     onReceive(Context context, Intent intent)
        {
            unregisterReceiver(receiver);                           // レシーバー登録を解除
            receiver = null;
            start();
        }
    }

    /**************
        一時停止
     **************/
    @Override synchronized
    protected void  onPause()
    {
        super.onPause();
        if ( phase == PHASE_RUN ) {
            phase = PHASE_STOP;
        }
        if ( future != null ) {                     // 定期実行停止
            future.cancel(false);
            future = null;
        }
        try {
            executor.submit(new Runnable()
            {
                @Override
                public void     run()
                {
                    base_view.quitGL();             // OpenGL終了
                }
            }).get();
        }
        catch (InterruptedException | ExecutionException e) {}
        executor.shutdown();
        executor = null;
        if ( phase >= PHASE_STOP ) {
            pauseNative();                          // native部一時停止
        }
        if ( receiver != null ) {
            unregisterReceiver(receiver);           // レシーバー登録を解除
            receiver = null;
        }
        getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);     // スリープ禁止解除
    }

    /********************************
        開始
            戻り値  フレームレート
     ********************************/
    synchronized
    public void     start()
    {
        if ( (executor == null) || (base_view.native_window == null) ) {
            return;
        }

        try {
            executor.submit(new Runnable()
            {
                @Override
                public void     run()
                {
                    android.os.Process.setThreadPriority(NATIVE_PRIORITY);
                    base_view.initGL()  ;                                                           // OpenGL初期化
                    synchronized (sync_native) {
                        frame_rate = initNative((phase == PHASE_INIT), getAssets());                // native部初期化
                    }
                    phase = PHASE_RUN;

                    time0 = System.currentTimeMillis();
                    time1 = 0;
                }
            }).get();
        }
        catch (InterruptedException | ExecutionException e) {}

        future = executor.scheduleAtFixedRate(this, 0, 1000/frame_rate, TimeUnit.MILLISECONDS);     // 定期実行開始
    }

    /*********
        稼働
     **********/
    @Override
    public void     run()
    {
        if ( phase == PHASE_RUN ) {
            long    _t = System.currentTimeMillis();
            int     _loop;

            time1 += (_t - time0)*frame_rate;
            if ( time1 > 4*1000 - 1 ) {
                time1 = 4*1000 - 1;
            }
            time0 = _t;
            _loop = (int)time1/1000;
            if ( _loop > 0 ) {
                time1 -= _loop*1000;

                base_view.swap();
                synchronized (sync_native) {
                    for (; (_loop > 0) && (phase == PHASE_RUN); _loop--) {
                        int     _key = key_status;

                        key_status = 0;
                        if ( !updateNative((_loop == 1), touch_status, _key) ) {    // native部稼働
                            finish();
                            break;
                        }
                    }
                }
            }
        }
    }

    /********************
        画面サイズ設定
     ********************/
    public void     set_screen(int _width, int _height)
    {
        screen_width  = _width;
        screen_height = _height;
        synchronized (sync_native) {
            setScreenNative(_width, _height);
        }
    }


    /********************
        タッチイベント
     ********************/
    public boolean  onTouchEvent(final MotionEvent event)
    {
        if ( phase != PHASE_RUN ) {
            return  false;
        }

        int     _action = event.getAction();
        int     _index, _id;

        switch ( _action & MotionEvent.ACTION_MASK ) {
          case MotionEvent.ACTION_DOWN :
          case MotionEvent.ACTION_POINTER_DOWN :
            _index  = (_action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
            _id     = event.getPointerId(_index)*3;
            if ( _id < 5*3 ) {
                touch_status[_id + 1] = (short)event.getX(_index);      // X座標
                touch_status[_id + 2] = (short)event.getY(_index);      // Y座標
                touch_status[_id + 0] = 1;                              // タッチ中
            }
            break;

          case MotionEvent.ACTION_MOVE :
            for (_index = 0; _index < event.getPointerCount(); _index++) {
                _id = event.getPointerId(_index)*3;
                if ( _id < 5*3 ) {
                    touch_status[_id + 1] = (short)event.getX(_index);  // X座標
                    touch_status[_id + 2] = (short)event.getY(_index);  // Y座標
                    touch_status[_id + 0] = 1;                          // タッチ中
                }
            }
            break;

          case MotionEvent.ACTION_UP :
          case MotionEvent.ACTION_POINTER_UP :
            _index  = (_action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
            _id     = event.getPointerId(_index)*3;
            if ( _id < 5*3 ) {
                touch_status[_id + 0] = 0;                              // 非タッチ
            }
            break;
        }
        return  true;
    }

    /********************
        バックキー入力
     ********************/
    @Override
    public void     onBackPressed()
    {
        key_status = KEY_BACK;
    }


    /************
        ビュー
     ************/
    class BaseView extends FrameLayout
    {
        public EGL10        mEgl;
        public EGLDisplay   mEglDisplay;
        public EGLContext   mEglContext;
        public EGLSurface   mEglSurface;
        public Object       native_window;
        private int[]       config_attribs;

        private static final int    EGL_OPENGL_ES2_BIT = 4;
        private static final int    EGL_CONTEXT_CLIENT_VERSION = 0x3098;


        /************************************************
            コンストラクタ
                引数    _kind    = true :SurfaceView
                                   false:TextureView
                        _attribs = 画面アトリビュート
         ************************************************/
        public BaseView(boolean _kind, int[] _attribs)
        {
            super(getApplication());
            config_attribs = _attribs;

            if ( _kind ) {                  // SurfaceView
                SurfaceView     _view = new SurfaceView(getApplication());

                _view.getHolder().addCallback(new SurfaceHolder.Callback()
                {
                    @Override
                    public void surfaceCreated(SurfaceHolder holder) {}

                    @Override synchronized
                    public void surfaceDestroyed(SurfaceHolder holder)
                    {
                        native_window = null;
                    }

                    @Override
                    public void surfaceChanged(SurfaceHolder _holder, int format, int _width, int _height)
                    {
                        set_screen(_width, _height);
                        set_surface(_holder);
                    }
                });
                addView(_view);
            }
            else {                          // TextureView
                TextureView     _view = new TextureView(getApplication());

                _view.setSurfaceTextureListener(new TextureView.SurfaceTextureListener()
                {
                    @Override
                    public void     onSurfaceTextureAvailable(SurfaceTexture _surface, int _width, int _height)
                    {
                        set_screen(_width, _height);
                        set_surface(_surface);
                    }

                    @Override
                    public void     onSurfaceTextureSizeChanged(SurfaceTexture surface, int _width, int _height)
                    {
                        set_screen(_width, _height);
                    }

                    @Override synchronized
                    public boolean  onSurfaceTextureDestroyed(SurfaceTexture surface)
                    {
                        native_window = null;
                        return  true;
                    }

                    @Override
                    public void     onSurfaceTextureUpdated(SurfaceTexture surface) {}
                });
                addView(_view);
            }
        }

        /**********
            開始
         **********/
        synchronized
        private void    set_surface(Object _window)
        {
            if ( native_window == null ) {
                native_window = _window;
                start();
            }
        }

        /**********
            稼働
         **********/
        private void    swap()
        {
            if ( !mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext) ) {
                throw new RuntimeException("eglMakeCurrent failed");
            }
            mEgl.eglSwapBuffers(mEglDisplay, mEglSurface);
        }


        /******************
            OpenGL初期化
         ******************/
        private void    initGL()
        {
            mEgl = (EGL10)EGLContext.getEGL();
            mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
            if ( mEglDisplay == EGL10.EGL_NO_DISPLAY ) {
                throw new RuntimeException("eglGetDisplay failed");
            }
            if ( !mEgl.eglInitialize(mEglDisplay, new int[2]) ) {
                throw new RuntimeException("eglInitialize failed");
            }
            if ( (search_config(config_attribs) == null) && (search_config(new int[] {EGL10.EGL_NONE}) == null) ) {
                throw new RuntimeException("eglCreateWindowSurface failed");
            }

            mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext);
        }

        private EGLConfig   search_config(final int[] attribs)
        {
            int[]   num_config = new int[1];

            mEgl.eglChooseConfig(mEglDisplay, attribs, null, 0, num_config);
            if ( num_config[0] <= 0 ) {
                return  null;
            }

            EGLConfig[]     configs = new EGLConfig[num_config[0]];

            mEgl.eglChooseConfig(mEglDisplay, attribs, configs, num_config[0], num_config);

            final int[]     attrib_list = {EGL_CONTEXT_CLIENT_VERSION, 2,   EGL10.EGL_NONE};

            for (EGLConfig config : configs) {
                mEglContext = mEgl.eglCreateContext(mEglDisplay, config, EGL10.EGL_NO_CONTEXT, attrib_list);    // Context作成
                if ( (mEglContext != null) && (mEglContext != EGL10.EGL_NO_CONTEXT) ) {
                    mEglSurface = mEgl.eglCreateWindowSurface(mEglDisplay, config, native_window, null);        // Surface作成
                    if ( (mEglSurface != null) && (mEglSurface != EGL10.EGL_NO_SURFACE) ) {
                        return  config;
                    }
                    mEgl.eglDestroyContext(mEglDisplay, mEglContext);
                }
            }
            return  null;
        }

        /****************
            OpenGL終了
         ****************/
        private void    quitGL()
        {
            if ( mEglContext != EGL10.EGL_NO_CONTEXT ) {
                mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
                mEgl.eglDestroyContext(mEglDisplay, mEglContext);
                mEglContext = EGL10.EGL_NO_CONTEXT;
            }
            if ( mEglSurface != EGL10.EGL_NO_SURFACE ) {
                mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
                mEglSurface = EGL10.EGL_NO_SURFACE;
            }
            if ( mEglDisplay != EGL10.EGL_NO_DISPLAY ) {
                mEgl.eglTerminate(mEglDisplay);
                mEglDisplay = EGL10.EGL_NO_DISPLAY;
            }
        }
    }
}

/***************** End of File ***************************************************/

シンプルな処理なので、ライフサイクル通りに順を追っていけば何をやっているかはわかると思いますが、一応少しだけ説明。

・executor
定期実行を管理するための ScheduledExecutorServiceです。
また、そのスレッドが OpenGLの描画スレッドとなっています。
自分がそんなに速さを必要とするアプリを作っていないこともあって、計算用のスレッドを分けたりはしていません。

・フレームレートに合わせた処理
scheduleAtFixedRateで実行する処理の間隔とフレームレートにはズレが出てきますので、時刻を見て処理回数を変えて合わせています(268行~)。
ついでに、ある程度の処理落ちにも対応できます。

・SurfaceViewと TextureView
393行からの Viewとそのコールバックの作成で、SurfaceViewと TextureViewの違いを吸収しています。


こちらは、同様に描画を変更した「ひっくり返しパズル かえすがえす」のプロジェクトです。
さすがに、これだけの変更でバージョンアップリリースはしていませんが。

そんなわけで稼働実績は大して無いので、上記の内容は参考程度に。



初音ミクが Lolipopで…2015/08/01

Android端末の更新がきてたので、5.1.1(Lolipop)にアップデートしました。

自作アプリの動作確認をしておこうと「初音演歌」で初音ミクに唄わせてみたところ
なんか、ポツポツとノイズが…

「初音ミク」をうまく使いこなしているわけではないので、元々歌声に不自然さはあったのですが、それとは違う今までは無かったノイズが乗るようになっていました。

初音演歌」のプログラムも古いので、基本的な共通部分を最新のものに差し替えてみると
直りました。良かった、良かった。


サウンドの再生は、
データの一部分を再生 → 再生完了コールバックで次の部分を再生
の繰り返しで行われているのですが、このコールバックが呼ばれるのにラグがあったようです。

サウンドはメインを止めないように、コマンドとして一旦キューに貯めて別スレッドで再生などの処理を行っています。このスレッドに AsyncTaskを使用してプライオリティが低かったのが、ラグの原因だと思われます。

しかし再生開始が遅れることはあっても、一度再生されてしまえばコールバックはスムーズに呼ばれると思っていたんですが…
コールバックの仕組みはよくわかりませんが、コールバックの設定か、サウンドを再生したスレッドのプライオリティに依るということなんでしょうか?


そんなわけで無事修正出来ましたので、
初音ミクが即興で演歌を唄うアプリ「初音演歌をバージョンアップしました。

初音演歌


他のアプリも修正しなければいけないのですが、とりあえずサウンドが重要なのを優先ということで…

プログラムで動画作成:Android SDK2015/07/15

前回の記事の通り、アプリ「おかしなカメラ」に、動画を保存する機能を追加しました。

Androidで動画を作成する方法はネットで調べればわかりますが、実際にアプリで機能させるにはそれ以外にも作らなければいけない所があります。
もちろんそれらも調べることはできるのですが、常套手段やどんな手法があるかもわからないと検索するのもままなりません。

そこで今回は検索用のキーワードを挙げる意味でも、簡単に動画作成の流れを追ってみます。


データをエンコードして動画に
動画ファイルの作成には、MediaCodecと MediaMuxerというクラスを使用します。
MediaCodecでデータをエンコード、MediaMuxerでファイルにまとめます。
画像と音声は別データなので、それぞれ MediaCodecが必要です。

MediaCodecは getInputBuffers()で入力用のバッファを取得して、エンコードのためのバイナリデータを送ることができるので、音声の方は AudioRecordで取得したデータを送っています。

ただし、画像の方はこの方法は使いません。
バイナリでやり取りしたのでは重いし、そもそも機種毎に対応している画像フォーマットが違うらしいです。

画像データを送るには、Surfaceを使用する方法が用意されています。
MediaCodecの createInputSurfaceで Surfaceを取得、そこへエンコードする画像を描画します。

Surfaceへの描画というと lockCanvasで Canvasを取得して…という方法がありますが、MediaCodec.javaの createInputSurfaceの所には lockCanvasは使うな、OpenGL ES(の類)を使え、とあります。実際、動画の作成を行うようなアプリが描画に Canvasを使用しているとは思えませんが…。


◇OpenGLによる Surfaceへの描画
こんなことは Androidで OpenGLを使うための基本的な話なんでしょうが、最初から GLSurfaceViewなんて物を使っていると案外知らなかったりします。

それぞれのクラスがどんな役割を持っているか、正確にはわかっていないのですが必要な処理を並べてみます。

・EGL10を取得
・EGLDisplayを取得
・EGLConfigを取得
・EGLContextを作成
・EGLSurfaceを作成
 このとき eglCreateWindowSurfaceの第3引数に、createInputSurfaceで取得した Surfaceを指定します。

後は毎フレーム glMakeCurrentから描画、eglSwapBuffers…とかあるんですが、このあたりはサンプルも含めてちゃんと解説している所を探した方が良いでしょう。

これで OpenGLを使った描画で動画を作成できるようになりました。しかし、まだ問題があります。


◇異なる EGLContext間の描画
実際のアプリでは画面キャプチャ等、画面に表示するテクスチャやフレームバッファで動画を作成する必要が多いのではないかと思います。
ところが、画面描画用の Surface(GLSurfaceView)とエンコード用の Surfaceでは EGLContextが異なっているので、このままではテクスチャを共用できません(テクスチャ等は EGLContextごとに管理されるため)。

一旦テクスチャデータをメモリに落として、エンコード用の Surfaceに切り替えたときにテクスチャを生成すれば描画できなくもありませんが、毎フレームそんな重い処理をするのは避けたいところです。

そこで、shared_contextという手法(実は手法の名前なのか、よくわかっていません)を使って、テクスチャ等を共有します。
具体的には、エンコード用の EGLContextを作成するときの eglCreateContextで、第3引数に画面描画に使用している EGLContextを指定します。
これで、画面に表示するテクスチャやフレームバッファを動画にすることができます。

ちなみに、GLSurfaceViewで使用している EGLContextを取得するには setEGLContextFactoryを使用します。
EGLContextを作成するクラスを作って、createContextの戻り値が必要とする EGLContextとなります。


以上、動画作成部分を実装するまでに調べた所や、つまづいた所を簡単に挙げてみました。
詳しい解説やサンプル等は、それぞれの処理で調べてみてください。

なお、上に挙げた方法がベストとは限りません。また、大体のニュアンスで書いていて細かい所で間違っている表現もあるかもしれないので気を付けてください。