woshidan's blog

あいとゆうきとITと、とっておきの話。

Heartbeatとブロックデバイスについて

短いですが、一応何かしら書くぞということで、本を読んでいたら「Heartbeatがスタンバイしたサーバを有効にする」「ブロックデバイスレプリケーション」と出てきたのが意味がわからなかったのでメモ。 

Heartbeat関連

  • クラスタに関することでハートビートといえばHAクラスタを実現する上で利用される仕組み
    • HA(High Availability)クラスタクラスタリングの中の一種で、あるサービスを提供するサーバが落ちたら代替機がそのサービスを引き継ぐようにするようなクラスタ
    • サーバ間で一定の間隔でパケットを送りあってお互いが生きているか死活監視している
    • 一定以上他のノードからパケットが届かなくなったらフェイルオーバーを作動させる
  • クラスタの種類
  • ハートビート色々
    • http://www.atmarkit.co.jp/ait/articles/0711/13/news139_2.html の記事では
      • Heartbeat … オープンソースソフトウェア「Heartbeat」全体
      • heartbeat … 上記のHeartbeatで提供されるプログラム、もしくはそれを起動したときのプロセス
      • ハートビート … HAクラスタを構成するPCが相互に状態監視をする際のネットワーク構成、およびその通信を指すものとします
        • 数ページしか調べてない時点で上記とちょっと違う定義が出て来ていて、一番上の行にあるのがそれ。要注意単語。
  • ハートビートの通信をどう行うか
    • シリアル通信や、ハートビート用に確保したLANを利用したり。
    • 監視対象のサーバに外部からのリクエストがたくさん来て高負荷のときに、輻輳に巻き込まれてハートビートのパケットが失われないように、アプリケーション本体の動作に使われるネットワークを避けたほうがいいらしい

Linux系OSのブロックデバイスとキャラクタデバイス

  • Linux系OSでは、デバイスはコンピュータに接続された周辺機器のことをデバイスと呼び、 /dev 以下に作成されるデバイスファイルが周辺機器のハードウェアへのインタフェースとなっている
  • ブロックデバイスとは、ある一定の量(通常512~2048バイト)の読み書きをランダムアクセスで行えるデバイスで、ディスク装置全般が該当する
  • キャラクタデバイスはデータの入出力をバイト(1文字)単位で扱うデバイスで、データの読み出しや書き込みがシーケンシャルアクセスとなる
    • 一度読み書きしたデータを後戻りして再び読み書きすることはできない
    • キーボードやマウスなどが該当する

参考

ウェブオペレーションまとめ用に読み直してるんですが、真面目に読むと11章や12章がとても重くて少しいやですね。わかんないものはわかんないで仕方ないですが。

*1:ここのwebアプリケーションサーバをEC2で調達したりする話を聞いたことが多いので、なんとなくクラスタ=クラウド感がありました

Amazon Athenaについて少し調べたのでメモ

まとまってないですが、忘れる前にメモ。

Amazon Athena とは

Amazon S3に入ったCSVJSONをAthena上で定義したスキーマに沿ってSQLで検索できるようにしてくれるクエリサービスです。

Athenaはサーバーレスのサービスで、利用するまでに最低限必要なステップは

  1. ファイルの入っているS3バケットの指定を含むスキーマの定義
  2. クエリを投げる

だけです。利用開始までにデータの抽出や変換、ロード(ETL)は必要なくなっています。ログ分析基盤などでやるようなアドホック分析に活用することができます。

スキーマというかテーブルの定義について

データベースはAthenaの管理画面の[Catalog Manager]などから作成可能です。 データベースごとにテーブルが100個まで作成できます*1

チュートリアルなどではテーブルをクエリエディタで定義していますが、この場合Hive互換のDDLを使って定義できます。

たとえばこういう形式のJSONが詰まったS3のバケットの中のファイルを検索させるためのテーブルを定義したい場合

{ "user_id": 12345, "data": { "imp_ids": [1,2,3,4,5] } }
{ "user_id": 23456, "data": { } }

このようにテーブルを定義することが可能です。

CREATE EXTERNAL TABLE IF NOT EXISTS array_tests (
  user_id String,
  data struct<imp_ids:array<INT>>
  )           
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
LOCATION 's3://exmaple-woshidan-test/sample';

一度スキーマを定義した後にS3にあるファイルに変更を加えることなく後からスキーマを変更することも可能です。

ただし、Athenaで利用するS3のバケットの中のファイルはすべてスキーマの定義に沿った形式である必要があったり、同じバケットの中で違う形式のファイルを検索できるようにすることはできないので、ログ収集時に決めた部分を後から違うフォーマットで検索可能にすることは難しいです。

同じバケットの中でテーブルの定義にそぐわないフォーマットのファイルが見つかった場合は、そぐわない部分の列だけ空白の行が返ってきます。 アドホック分析なので途中から検索方法が違っても大丈夫、ということなのかもしれないです。

また、テーブルのパーティションについてはS3のバケットのパスに依存するので、こちらも後から変更することは難しい場合があります。

テーブルの変更はテーブルのドロップ -> 再作成という手順で行います。

RDBMSのテーブルと異なり、Athenaのスキーマ定義はS3の中のファイルを走査する際に利用されるメタデータのようなものなだけで、保存されているファイルのフォーマットがAthenaのスキーマ定義に影響を受けることはありません。

データの検索

データの検索もSQLのように行うことができます。 Athenaの検索の実行はPrestoを使って行われ、検索クエリを書く際はPrestoで利用できるキーワードの一部が利用可能となっています。

先ほどの例を用いていくらかクエリと実行結果を書いてみます。

SELECT * FROM mydatabase."array_tests" limit 10;
user_id data
1 12345 {imp_ids=[1, 2, 3, 4, 5]}
2 23456 {imp_ids=null}
SELECT * FROM mydatabase."array_tests" limit 10;
SELECT data.imp_ids FROM mydatabase."array_tests" WHERE user_id = '12345';
imp_ids
1 [1, 2, 3, 4, 5]

配列の検索は UNNESTCROSS JOIN を使って、配列の行を展開した行を生成することで可能なようです。パフォーマンスとかはあんまりわかってません。。

SELECT * FROM mydatabase."array_tests" cross join UNNEST (data.imp_ids) AS t (imp_id);
user_id data imp_id
12345 {imp_ids=[1, 2, 3, 4, 5]} 1
12345 {imp_ids=[1, 2, 3, 4, 5]} 2
12345 {imp_ids=[1, 2, 3, 4, 5]} 3
12345 {imp_ids=[1, 2, 3, 4, 5]} 4
12345 {imp_ids=[1, 2, 3, 4, 5]} 5
SELECT * FROM mydatabase."array_tests" cross join UNNEST (data.imp_ids) AS t (imp_id) WHERE imp_id = 3;
user_id data imp_id
12345 {imp_ids=[1, 2, 3, 4, 5]} 3

その他

Athenaでクエリを実行する先のS3に上げるJSONは1行

Athenaのクエリ実行先として指定するS3の中にあげるJSONは1行にminifyしておく必要があります。

Hive_CURSOR_ERROR: Row is not a valid JSON Object ... というエラーが出てよく見たら1行目の{あたりから怒られているような場合はこれが原因です。

参考

現場からは以上です。

*1:このあたりの制限は上限緩和申請が可能

「ライブ配信のなかみ[あれくま]」を読んだ

春の技術書典で手に取った時から、勝手にこの本は自分のための本だ、と思っていたのですが、仕事でバタバタしていまして昨日ようやく読みました。

読み終えてやっぱり自分にとってめちゃくちゃ面白かったので自己満足に感想をおいておきます。

この本を読む以前に関わっていたサービスでChromeFlashを使うと警告が出るようになるのを避けるため、動画再生画面で使うファイルの形式をflvからmp4に切り替えるという対応をしたことがあります。

その時、flvのファイルとFlashのプレイヤーの組み合わせだとストリーミング再生ができるのに、mp4とHTML5のvideo要素の組み合わせに切り替えると全てよみこむまで再生できないのをどうにかしたいのだけれど、どこをどう調べたらいいかわからなくてずっともやもやしていました*1

その辺りがこの本で触れられていて、めちゃくちゃ無知に対する恥ずかしさと嬉しさで悶えて死ぬかな、という感じです。

やっぱり感想うまくまとまらなかったので残りの感想は箇条書きにしておきます。。

  • ストリーミング配信のシークバーを実装する際に必要になるデータ形式の雰囲気から具体的なコーデックまで順を追って説明してもらえるのでめちゃくちゃわかりやすかった
    • 配信の遅延の原因でバッファにデータを貯める時間の話が面白かった
  • コーデックとコンテナの違いはこの本を読むまで知らず、動画のファイル形式、JavaScriptで二つ取れるなーみたいな感じで圧倒的無知で死にたさすらある
    • H.262のHが何由来か知らなかったので、MPEG-2 Videoと一緒なのを知った時嬉しさ
    • 各種コンテナ/コーデックの違いについて、たとえばネットワークの本であったりの一部として箇条書き一行ずつくらいで出てくるんだけど、全然覚えられなくて、この本見たら思い出話みたいに語ってあって全然違うものの話みたいで*2楽しい!!!
  • MicrosoftAdobeが独自規格をあきらめていくのにAppleお前ときたら…!
  • HTTPのプログレッシブダウンロードでは落としきっていないファイルを途中まででも無理やり再生プレイヤーが必要という話を聞いて、Flashのプレイヤー強い… 感がありました
  • 動画の配信をするメディアサーバの話をあまり聞いたことがないので新鮮でした

具体的な内容についてはぜひ下記リンクから買って確かめてくださいませ(ダイレクトマーケティング)。

www.dlsite.com

動画を扱うサービスのRails書くWebエンジニアやアプリ作るエンジニアでちょっと動画に興味があって調べたことある人ならきっとめちゃくちゃ面白いと思います。

現場からは以上です。

*1:結局動画ファイル全体の1%もないし、一旦置いておこうか、となった苦い記憶

*2:同じものだと思われます

インテントフラグとActivityStackのふるまいについていくらか

AndroidActivity のスタックの振る舞いを設定するものとして、

  • AndroidManifest.xml に記述する activity 要素の launchMode 属性
  • Activity を立ち上げる時に使う Intent に付与するインテントフラグ

があります。先日、launchModeの話をしたので、今日はインテントフラグの実験を少ししようかと思います。

今回インテントフラグがどんなものか確認する際に取り上げる具体例としては、

  • FLAG_ACTIVITY_SINGLE_TOP
  • FLAG_ACTIVITY_CLEAR_TOP
  • FLAG_ACTIVITY_NO_HISTORY

あたりにします。今回の実験に使ったコードの全体像はこちらで適宜、/* some flags */ 周辺のコードを編集した分を実行結果と一緒に紹介します。

TL;DR

長くなったので。

  • FLAG_ACTIVITY_SINGLE_TOP
    • launchModesingleTop と同じ
    • このフラグをつけて起動したActivityと同じActivityがスタックの一番上にあるなら、既存のインスタンスonNewIntent()を呼びだす
  • FLAG_ACTIVITY_CLEAR_TOP
    • このフラグをつけて起動した Activity よりスタックの上にある Activity を破棄して、フラグをつけた Activity を呼び出す
    • FLAG_ACTIVITY_SINGLE_TOP との併用している、または、起動した ActivitylaunchModesingleTop の場合は既存のインスタンスが再利用される
    • そうでない場合は、既存の Activityインスタンスを破棄してから新しい Activityインスタンスを生成して起動する
  • FLAG_ACTIVITY_NO_HISTORY
    • このフラグをつけて起動した ActivityActivity のスタックに積まれない
    • = onActivityResult が決して呼び出されない

FLAG_ACTIVITY_SINGLE_TOP

まず、MainActivity を起動するときだけ、FLAG_ACTIVITY_SINGLE_TOP を利用してどのような挙動をするか見てみましょう。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, MainActivity.class);
                intent.putExtra("KEY_MESSAGE", "sent new intent");
                intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
                startActivity(intent);
            }
        });

        findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, NextActivity.class);
                startActivity(intent);
            }
        });
    }
public class NextActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_next);

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(NextActivity.this, MainActivity.class);
                intent.putExtra("KEY_MESSAGE", "sent new intent");
                intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
                startActivity(intent);
            }
        });

        findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(NextActivity.this, NextActivity.class);
                startActivity(intent);
            }
        });
    }

MainActivityが一番上にあるときはその上にActivityが積まれず

NextActivityが一番上にあるときは新しいMainActivityが積まれます。

launchModesingleTop と同じ振る舞いですね。

FLAG_ACTIVITY_CLEAR_TOP

MainActivityを表示しようとしたとき、FLAG_ACTIVITY_CLEAR_TOPインテントフラグを付与することにします。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Log.d("MainActivity", "MainActivity " + MainActivity.this.toString());

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, MainActivity.class);
                intent.putExtra("KEY_MESSAGE", "sent new intent");
                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
                startActivity(intent);
            }
        });

        findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, NextActivity.class);
                startActivity(intent);
            }
        });
    }
public class NextActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_next);

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(NextActivity.this, MainActivity.class);
                intent.putExtra("KEY_MESSAGE", "sent new intent");
                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
                startActivity(intent);
            }
        });

        findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(NextActivity.this, NextActivity.class);
                startActivity(intent);
            }
        });
    }

MainActivityが一番上にあるとき、すでにあるActivityが破棄されてから別のActivityが作り直されて一番上に積まれ直します。

Log.d("MainActivity", "MainActivity " + MainActivity.this.toString());インスタンスが変わっているというログがこちらです。

D/MainActivity: MainActivity com.example.woshidan.intentflagtest.MainActivity@3a2375cd
D/MainActivity: MainActivity com.example.woshidan.intentflagtest.MainActivity@21a00ad7
D/MainActivity: MainActivity com.example.woshidan.intentflagtest.MainActivity@ac67751

何枚か NextActivity をスタックに積んでから MainActivity を起動し、その後バックキーを押すとアプリが終了します。

この場合のLog.d("MainActivity", "MainActivity " + MainActivity.this.toString());インスタンスが変わっているというログがこちらです。

D/MainActivity: MainActivity com.example.woshidan.contexttest.MainActivity@2007c35
D/MainActivity: MainActivity com.example.woshidan.contexttest.MainActivity@25e3619b

もう少し別の条件で検証するとわかりやすいですが、起動しようとした MainActivity より上のスタックにある NextActivity と古い MainActivityを捨て、MainActivity を作り直して、呼び出していることがわかります。

ドキュメントのフラグ一覧のところの FLAG_ACTIVITY_CLEAR_TOP の欄には、

If set, and the activity being launched is already running in the current task, then instead of launching a new instance of that activity, all of the other activities on top of it will be closed and this Intent will be delivered to the (now on top) old activity as a new Intent.

とあり、これは上記の挙動と異なるように見えます。

しかし、これは仕様どおりで、さらに FLAG_ACTIVITY_CLEAR_TOP単体の項目のドキュメントを読むと、FLAG_ACTIVITY_SINGLE_TOP との併用か、 ActivitylaunchModesingleTop に設定されていなければ、既存のインスタンスへちょっとIntentが送られないことがあることがわかります。

それでは、 FLAG_ACTIVITY_CLEAR_TOPFLAG_ACTIVITY_SINGLE_TOP との併用した場合の挙動を

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Log.d("MainActivity", "MainActivity " + MainActivity.this.toString());

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, MainActivity.class);
                intent.putExtra("KEY_MESSAGE", "sent new intent");
                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
                startActivity(intent);
            }
        });

        findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, NextActivity.class);
                startActivity(intent);
            }
        });
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        Log.d("MainActivity", "onNewIntent");
        Toast.makeText(this, intent.getStringExtra("KEY_MESSAGE"), Toast.LENGTH_SHORT).show();
    }
public class NextActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_next);

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(NextActivity.this, MainActivity.class);
                intent.putExtra("KEY_MESSAGE", "sent new intent");
                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
                startActivity(intent);
            }
        });

        findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(NextActivity.this, NextActivity.class);
                startActivity(intent);
            }
        });
    }
}

で試してみましょう。今度は、一覧の部分にあるように既存のMainActivityのインスタンスにIntentが送られるようになったみたいです。

FLAG_ACTIVITY_NO_HISTORY

どのActivityを起動するときも FLAG_ACTIVITY_NO_HISTORYインテントフラグを付与してがんがんActivityを起動してみます。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, MainActivity.class);
                intent.putExtra("KEY_MESSAGE", "sent new intent");
                intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
                startActivity(intent);
            }
        });

        findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, NextActivity.class);
                intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
                startActivity(intent);
            }
        });
    }
public class NextActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_next);

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(NextActivity.this, MainActivity.class);
                intent.putExtra("KEY_MESSAGE", "sent new intent");
                intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
                startActivity(intent);
            }
        });

        findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(NextActivity.this, NextActivity.class);
                intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
                startActivity(intent);
            }
        });
    }
}

たくさんスタックを積んだはずが、2回バックキーを押すとアプリが終了してしまいました。

最初の一回のバックキーで一番最初に起動した MainActivity へ戻ってしまったようです。

FLAG_ACTIVITY_NO_HISTORYインテントフラグをつけて起動されたActivityは、Activityのスタックに積まれないようです。

インテントフラグはActivityを起動する側が指定するので、使う側の都合によってActivityのスタック上での扱いを決める方法、みたいな感じなんですかね。

現場からは以上です。

参考

Intent | Android Developers

AndroidのCamera2 APIを触ってみた

ちょっと面白そうだったので調べました。

概要

https://developer.android.com/reference/android/hardware/camera2/package-summary.html からざっくり読み取った結果、Camera2 APIでカメラ経由で画像を取得したりする流れをかなり大雑把にいうと

  1. CameraManager を利用し、CameraDevice.StateCallback 経由で CameraDeviceインスタンスを取得
  2. Surface系のViewやMediaCodecなどの出力先とカメラデバイスを利用して CameraDeviceインスタンスから createCaptureSession(List, CameraCaptureSession.StateCallback, Handler) メソッドにより CameraCaptureSession を生成
  3. 1フレーム分の入力を取得するための入力リクエスト( CaptureRequestインスタンス)を作る
  4. CaptureSession にリクエストをセットする
  5. リクエストが処理されて、 TotalCaptureResult (カメラデバイスの現在の状態など)オブジェクトの生成や、出力先への1フレーム分への画像データの送信が行われる

となるようです。それぞれ見ていきましょう。

なお、ただでさえややこしいので、今回のサンプルコードは意図的にtargetSDKVersionを21にして、パーミッション周りやエラー対応の処理はビルドが通るギリギリくらいまで省いています。。

CameraManager を利用し、CameraDevice.StateCallback 経由で CameraDeviceインスタンスを取得

  1. CanmeraManager を利用してそのデバイスで利用できるカメラのIdを取得
  2. カメラのIdと CameraDevice.StateCallbackインスタンスを利用して、CameraManager.openCamera()メソッドで CameraDevice をオープン
  3. CameraDevice.StateCallbackonOpened コールバックで CameraDeviceインスタンスを取得

利用するカメラの種類を制限したかったりする場合はCameraDevice をオープンする前に行います。

また、サンプルによるとプレビュー領域のサイズや向きなどを設定したい場合はプレビューを表示する TextureView に対して行うようです。

        CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
        String selectedCameraId = "";
        try {
            selectedCameraId = manager.getCameraIdList()[0];

            // https://github.com/googlesamples/android-Camera2Basic/blob/5dad16c103715b5e7e3c001cc5f6067f8d23f29e/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.java#L499
            // あたりにあるのですが、顔用カメラを使いたくないなどがあれば、CameraCharacteristicsを経由して確認可能
            //            CameraCharacteristics characteristics
            //                    = manager.getCameraCharacteristics(selectedCameraId);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }

        try {
            manager.openCamera(selectedCameraId, mStateCallback, mBackgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(CameraDevice cameraDevice) {
            mCameraDevice = cameraDevice;
        }

        @Override
        public void onDisconnected(CameraDevice cameraDevice) {
            cameraDevice.close();
            mCameraDevice = null;
        }

        @Override
        public void onError(CameraDevice cameraDevice, int error) {
            cameraDevice.close();
            mCameraDevice = null;
        }

    };

CameraDeviceインスタンスから CameraCaptureSession を生成

Surface系のViewやMediaCodecなどの出力先とカメラデバイスを利用して CameraDeviceインスタンスから createCaptureSession(List, CameraCaptureSession.StateCallback, Handler) メソッドにより CameraCaptureSession を生成します。

  1. 出力先として TextureView を用意する
  2. TextureView から SurfaceTexture を取得して Surface を生成する
  3. 2のSurface をListに入れて、CameraCaptureSession.createCaptureSession() メソッドを呼び出す

2の Surface は、 ImageReaderMediaCodec など他の出力先クラスからも生成/取得が可能です*1

// Activity.onCreate内
        mTextureView = (TextureView) findViewById(R.id.texture);
        mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
            @Override
            public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
                // 先ほどのカメラを開く部分をメソッド化した
                openCamera();
            }

            @Override
            public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {

            }

            @Override
            public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
                return true;
            }

            @Override
            public void onSurfaceTextureUpdated(SurfaceTexture surface) {

            }
        });
   private CameraCaptureSession mCaptureSession = null;

// ...

    private void createCameraPreviewSession() {
            SurfaceTexture texture = mTextureView.getSurfaceTexture();
            texture.setDefaultBufferSize(320, 240); // 自分の手元のデバイスで決めうちしてます
            Surface surface = new Surface(texture);
        try {
            mCameraDevice.createCaptureSession(Arrays.asList(surface), new CameraCaptureSession.StateCallback() {
                @Override
                public void onConfigured(CameraCaptureSession session) {
                    // カメラがcloseされている場合
                    if (null == mCameraDevice) {
                        return;
                    }
                }

                mCaptureSession = session;

                @Override
                public void onConfigureFailed(CameraCaptureSession session) {

                }
            }, null);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

1フレーム分の入力を取得するための入力リクエスト( CaptureRequestインスタンス)を作る

CaptureRequest はカメラが何ができるかなどの情報が必要なため、 CameraDevice クラスが CaptureRequest.Builder のファクトリメソッドを持っていますので利用します。

リクエストには色々ありますが、今回のリクエストは Target に与えた Surface によくあるプレビューの表示を作るための画像データを送ってくれ、というものです(たぶん)。

private CaptureRequest.Builder mPreviewRequestBuilder;
private CaptureRequest mPreviewRequest;
...

// createCameraPreviewSession メソッド内で
        try {
            mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            mPreviewRequestBuilder.addTarget(surface);
            mPreviewRequest = mPreviewRequestBuilder.build();
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }

CaptureSession にリクエストをセットする

先ほど生成したリクエストに加えて、 CameraCaptureSession.CaptureCallback が必要です。 プレビュー表示のいろいろなエラーハンドリングをする場合は結構書く必要があるみたいですが、今回はその辺は考えないのでnullを与えています。

この時点で TextureView を置いた領域にカメラで撮影した範囲が表示されているのが確認できます。

mCameraDevice.createCaptureSession(Arrays.asList(surface), new CameraCaptureSession.StateCallback() {
    @Override
    public void onConfigured(CameraCaptureSession session) {
        // カメラがcloseされている場合
        if (null == mCameraDevice) {
            return;
        }

        mCaptureSession = session;

        try {
            session.setRepeatingRequest(mPreviewRequest, null, null); // `CaptureSession` にリクエストをセットする
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

TotalCaptureResult へ送られてきた1フレーム分への画像データの処理について

前節で TotalCaptureResult へデータが送られてくるところまでは確認しましたが、 プレビューだけ延々と表示しても仕方ないので、ここへ飛んできたデータを利用する方法の一例を書いておしまいにします。

下記のコードでは、ボタンを押すとプレビューの表示を止めて、プレビューに表示している画像をファイルへ保存しています。

        Button capture = (Button) findViewById(R.id.button_capture);
        capture.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {
                    mCaptureSession.stopRepeating(); // プレビューの更新を止める
                    if(mTextureView.isAvailable()) {
                        File file = new File(getFilesDir(), "surface_text.jpg");
                        FileOutputStream fos = new FileOutputStream(file);
                        Bitmap bitmap = mTextureView.getBitmap();
                        bitmap.compress(Bitmap.CompressFormat.JPEG, 50, fos);
                        fos.close();
                    }
                } catch (CameraAccessException e) {
                    e.printStackTrace();
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });

真剣にやろうと思ったらActivityやFragmentのライフサイクルに合わせてリソースの解放とか色々あるんですが今日は流れを把握するためにこんな感じで。

プレビューの表示止めてファイルに書き込む部分まで含めて全体のコードは以下となります。一番初めに書きましたが、今回はCamera2 APIの流れだけを把握しようと例外処理を省くため、targetSDKVersion=21にしているので少々注意してお試しください。

Camera2_API_Sample · GitHub

現場からは以上です。

参考

Activityの起動とlaunchModeについて

ActivityのlaunchMode の違いが気になっていたので、公式で推奨されている standard, singleTop についてざっくり整理しました。

launchMode=“standard"、あるいは指定がない場合

Androidのシステムは常に、ターゲットタスク*1内で新しいActivityのインスタンスを作成します。

つまり、

<activity android:name=".MainActivity"
    android:launchMode="standard"
    >
    <intent-filter> <!-- IntentFilterの部分は本記事の趣旨には関係ない -->
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

となっている場合、

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                startActivity(new Intent(MainActivity.this, MainActivity.class));
            }
        });
    }

と書いたらボタンを押せば何枚でもMainActivityが生成されてスタックに積まれていくわけです。

こんな感じに。

f:id:woshidan:20170901010221g:plain

launchMode=“singleTop"の場合

Androidのシステムは、ターゲットタスク内で

  • 起動しようとしているActiivtyがタスクのトップにある場合は新しいActivityを生成しない
  • 起動しようとしているActiivtyがタスクのトップにない場合は新しいActivityを生成する

というふるまいをします。ややこしいですね。

ちょっと動かしてみましょう。

<activity
    android:name=".MainActivity"
    android:label="FirstActivity"
    android:launchMode="singleTop">
    <intent-filter> <!-- IntentFilterの部分は本記事の趣旨には関係ない -->
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
<activity android:name=".NextActivity"
    android:launchMode="singleTop"
    android:label="SecondActivity"
    >
</activity>
// NextActivityもほぼ同じコード
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                startActivity(new Intent(MainActivity.this, MainActivity.class));
            }
        });

        findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                startActivity(new Intent(MainActivity.this, NextActivity.class));
            }
        });
    }
}

交互に二つのActivityを起動する場合は、新しいActivityが生成されスタックに積まれていきます。

f:id:woshidan:20170901010056g:plain

同じActivityを連続して起動しようとすると、singleTopの 起動しようとしているActiivtyがタスクのトップにある場合は新しいActivityを生成しない という性質により、新しいActivityのインスタンスは生成されず、スタックにあるActivityは増えません。

f:id:woshidan:20170901010153g:plain

ところで、launchModestandard のアクティビティを利用している場合、前のActivityから渡されたIntentをonCreateで受け取って表示やAPIへのリクエストパラメータに利用します。

それでは、singleTopのActiivtyがスタックの一番上にあるときに、もう一度Activityを起動しようとしたことを検知、あるいは、その際渡されたIntentを元に表示などを変更したい場合、どうしたらいいでしょうか。

公式ドキュメントにも書いてありますが、Activity.onNewIntent(Intent)で可能です。

// 先ほどのMainActivityのコードを少し編集
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, MainActivity.class);
                intent.putExtra("KEY_MESSAGE", "sent new intent");
                startActivity(intent);
            }
        });

        // 略
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        
        Toast.makeText(this, intent.getStringExtra("KEY_MESSAGE"), Toast.LENGTH_SHORT).show();
    }

f:id:woshidan:20170901010031g:plain

ECやメディア系のアプリで起動時にダイアローグを表示して、押されたボタンによっては、トップ画面の表示をデフォルトではなく特定の単語やカテゴリで検索かけた結果を表示するようにしたい、なんていう場合などは覚えておくと便利そうです。

現場からは以上です。

*1:おおむね起動しようとしているActivityの属するアプリのタスク

NoSQLデータベースファーストガイドを読んだ

NoSQLデータベース ファーストガイド

NoSQLデータベース ファーストガイド

春先にRDBMSの勉強を少ししたのでNoSQLの話も少し覗きました。2011年発行とやや古いのですが、その分一歩ずつ各種ソフトウェアの背景を把握しながら前に進んでいけるような内容でよかったです。

  • 紹介されていたのはmemcached, Tokyo Tryant, Redis, MongoDB, MySQL + HandlerSocket
    • 当時のものではありますが、サーバ1台目から、レプリケーションやるところまで手順とログあったのどんな感じなのか感覚がわかりやすくて良かった
    • 3章の事例, 4章のパフォーマンス計測のコードがあるところもありがたい*1
  • memcachedなどのkey-valueストアはテーブルに相当するものがないということを認識して雷を打たれた感じになりました
    • なんていうか、マジックナンバー的なコラムが2つ*2用意されたテーブルみたいです
    • パフォーマンスや使い方は全然違いますが、データが持たせられる構造的に
  • 各NoSQLデータベースについて
    • 各ソフトウェアの見出しは適当につけましたが、使うときは4章および現在の各種ソフトウェアのドキュメント参照のこと
    • memcachedは揮発するけど速い
      • 揮発していい、計算に数分かかるけど一度参照されたらしばしば参照されるようなデータに使いやすい
      • RDBMSなどと組み合わせて使うことが前提
    • Tokyo Triantは書き込む分、memcacedより遅いがRDBより速い
      • ブログのアクセス数みたいに希薄しては困るけどしばしば更新 & アクセスされる、みたいな値に使うのが良さそう
        • 検索条件の都合で、範囲を指定してのデータ取得に弱いので、MySQLなど範囲指定が柔軟な他のDBでサマリテーブル作って組み合わせると良さそう
    • rediesはこれらのいいとこ取りしたような感じ
      • 配列の取り扱いが強いのと、集合演算があるので、フォローしてる人のつぶやき一覧みたいにしばしば更新&参照される割に絞り込みがややこしそうなところで使えそうな感じがありました
      • この例みて少し強くなった気持ちになれたので自分でも似たような例を考えて一回試し書きしてみたい
      • 探され方の分だけデータを挿入するためのキーを用意する、みたいな印象
    • mongo dbはドキュメントの構造の柔軟さがまず魅力
      • embedded ドキュメントのとこは実際データの書き込み・保存とか、その辺よく読み取れなかったのでもう一回触り直ししたいが、ドキュメント型の他のNoSQLのぞいてそっちを触った方が良さそう?
      • コンソール叩いてデータベースやコレクションが書き込み時に必要になってから生成されているの示している部分がよかった
      • mapperのバリデーションはアプリケーション側で値のバリデーションやマッピングに利用するために書いているのであり、mongo的にはいらない
      • BSON(データ形式)のこともうちょっと調べたい
    • MySQL + HandlerSocket
      • 速いな! という気持ちですが、あれから時間も経っていて、いくらかのNoSQLはけっこう枯れてきたと思われるので、NoSQLの仕事はNoSQLに任せた方がいいかなという感触

とりあえず、RDBMSの本を読んで、集合演算やしばしば更新されるテーブルに対してどうしようかなーという感じがあり、具体的には

  • ブログのアクセス数
  • 頻繁にアクセス&更新される一覧の表示

に関して悩んだことがあったので、知りたいことはここにあったのかーという感じでした。自分不勉強ですねぇ。。

また、レプリケーションの話が各ソフトウェアごとに書いてあって、データが分散しているとはどういうことかー、みたいなのが少しだけ感覚つかめた気がして*3よかった。例を浴びるの is よいです。これは不意打ちによかったです。

こういうの、インフラ寄りの人からすると当たり前かもしれないんですが、rediesを導入する機能を書き始めるWebアプリケーションエンジニアからすると触るの自体大変だったりして具体的なログの感じとか、データが保存されているイメージとかつかめなくてやたら難航することがあるような記憶があるので、お手本のログがあるの、正常な状態はこれだよ、と教えてもらえる感じでとてもよかったです。

現場からは以上です。

*1:いま同様の検証やるときの参考にもなると思いました

*2:1つが検索用のキーでもう一つが表示などに使う意味のある値

*3:気だけとかいう可能性はありまくりなので、いつかちょっと何か作りたいですね