YUV⇒RGB変換

前回、JavaにOpenGLのラッパーであるJOGLを使用してYUVデータを表示しました。

今回はAndroid端末にYUVデータを表示使用と思います。

従来のAndroid OSでは元々OpenGL ES 1.0をサポートしていました。
画像の描画については他の方法よりも高速な方法でした。

ただ現状ではOpenGL ESを使用しなくても、描画はハードウェアアクセラレーションの恩恵を受けていますので、OpenGL ESの複雑なAPIで下手なプログラムを作成するよりも、通常の描画方法を採用した方が高速になる場合もあります。

現在はAndroid OSもOpenGL ES 2.0や3.0等をサポートするようになり、デフォルトでGLSLを使用したシェーダープログラムを使用するようになりました。
その結果、従来、CPU側で行っていた画像計算をGPU側で行うことができるようになり、その分、高速化できる可能性が出てきました。

以下、Android端末を使用して単にYUVファイルを読み込んで画像を表示する事から、OpenGL ESとGLSLを使用してGPUでYUV⇒RGB変換を行って画像を表示するまでをステップバイステップで説明していきます。

Androidの開発環境

先ず、Androidの開発環境を準備します。
詳細は他のホームページに任せますが、今回は以下の開発環境を使用します。

Android Studioのダウンロード / インストール

Androidの開発を行うためのIDEは、EclipseやVisual Studio等、様々な物があります。
今回は、Googleが提供するAndroid Studioを使用します。

Android Studioはこちらのダウンロードページからダウンロードして下さい。

インストールについてはこちらのインストールガイドを参照して下さい。

基本的にはダウンロードしたファイル”android-studio-XXXX.Y.Z.WW-windows.exe”をダブルクリックでインストールが開始します。

Android Studioの設定

デフォルトの設定でOKです。
但し、途中android-sdk-licenseintel-android-extra-licenseに対して"Accept"をチェックする必要があります。

エラー対応

設定後にインストールが継続しますが、最後に

Intel HAXM installation failed!

と云うエラーが発生する場合があります。

この場合、以下のようにWHPXが有効であれば問題ないのでスルーします。

WHPX

Hyper-Vによる仮想化技術です。
有効化する場合は以下のようにします

  1. “Windowsの機能の有効化または無効化”ダイアログボックスを開く
  2. “Hyper-V”をチェック
  3. “Windows ハイパーバイザープラットフォーム”をチェック

詳細についてはAndroid Emulator のハードウェア アクセラレーションを設定するを参照して下さい。

使用言語

前回Javaを使用しましたので、今回もJavaを使用します。

Googleの推奨はKotlinですし、最近のAndroidプログラミングの解説ページはKotlinでの記述が増えてきています。
ただ、KotlinはAndroidプログラミング以外で利用される割合が低いですし、JavaからKotlinへの乗り換えは意外と簡単だと云われています。

なので、Javaを採用します。

とりあえずアプリケーションを作成

先ず最初は、Android Studioでプロジェクトを作成し、表示領域とボタン類を配置していきます。

プロジェクト作成

Android Studioででのプロジェクト作成は以下のようにします。

  1. Android StudioのiconをダブルクリックWelcome to Android Studio
  2. “New Project”ボタンをクリック
  3. エディター画面が既に表示されている場合にはメニューから”File”→“New”->“New Project”を選択
  4. “New Project”ウィンドウが表示される
    New Project / Empty Activity
  5. “Empty Activity”が選択されている事を確認後”Next”ボタンをクリック
  6. プロジェクト名等を設定するウィンドウが表示される
    New Project / Setting
  7. 今回は”Name”を”ShowYUVonAndroid”とする(他の名称でもOK)
  8. “Language”は”Java”を選択
  9. “Package name”や”Minimum SDK”はデフォルトでOK
  10. “Finish”ボタンをクリック

“MainActivity.java”ファイルには以下のように空のActivityが作成されています。

public class MainActivity extends AppCompatActivity {

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

描画領域とボタンの作成

画像サイズの指定

今回もYUVファイルとしてCIFサイズの”akiyo_cif.yuv”を使用しますので、最初に画像サイズを指定しておきます。
以下のようにしてリソースに追加します。

  1. 左のProjectツリーペインの”app”を右クリック
  2. メニューから”New”→“XML”→“Values XML File”を選択
  3. リソースフォルダにXMLファイルを作成するウィンドウが表示される
    New Android Component / dimens.xml
  4. “Values File Name”に”dimens”と入力
  5. “Finish”ボタンをクリック
  6. “app”→“res”→“values”→“dimens.xml”ファイルが作成される

なお、画像サイズ等の寸法を指定するリソースは”dimens.xml”に記載する事が推奨されています。
詳細はアプリのリソースの概要を参照してください。

CIFサイズの幅と高さを指定するため、“dimens.xml”は以下のようにします。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="width">352dp</dimen>
    <dimen name="height">288dp</dimen>
</resources>
      

なお、“dimens.xml”内のリソースは必ず単位を付加します。
付加できる単位は、現状、以下の6つがサポートされています。

単位 意味
dp 密度非依存ピクセル
160dpiの画面を基準にしたピクセル
実際のピクセル数は画面密度により変化
sp スケール非依存ピクセル
dpに近いがフォントサイズ指定の際に使用
pt 72dpiの画面での1/72インチ
px ピクセル
実際の画面の画素数
mm ミリメートル
in インチ

詳細はその他のリソース→ディメンションを参照してください。

ボタン用のテキスト指定

AndroidではUI等に使用するテキストもリソースとして定義します。

左のProjectツリーペインの”app”→“res”→“values”→“strings.xml”ファイルを選択、オープンします。
以下のように”File…“ボタンと”Play”ボタンに表示するテキストを指定します。

<resources>
    <string name="app_name">ShowYUVonAndroid</string>
    <string name="play_button_text">Play</string>
    <string name="file_button_text">File...</string>
</resources>
      

レイアウトの修正

レイアウトを修正する場合には、先ず、左のProjectツリーペインから”app”→“res”→“layout”→“activity_main.xml”ファイルを選択、オープンします。

最初は"Layout Editor"が表示されるはずです。
“Layout Editor"の使い方についてはLayout Editor を使用して UI を作成する等を参照してください。

今回は右上の”View Mode”から”Code”モードを選択し、xmlコードを直接編集します。

最初はConstraintLayout内にTextViewとして”Hello World!“が表示されるようになっています。
今回は先ず、LinearLayout内にViewButtonを配置します。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <View
        android:id="@+id/view"
        android:layout_width="@dimen/width"
        android:layout_height="@dimen/height"
        android:layout_gravity="center_horizontal"
        android:layout_margin="16dp" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">

        <Space
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1" />

        <Button
            android:id="@+id/play_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:enabled="false"
            android:text="@string/play_button_text" />

        <Space
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1" />

        <Button
            android:id="@+id/file_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="@string/file_button_text" />

        <Space
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1" />
    </LinearLayout>

</LinearLayout>
      

因みにViewは仮置きです。
後で色々と変更します。

View.OnClickListenerインターフェースの追加

ボタンを配置しましたので、ボタンクリックに対する処理を実装するため、MainActivityクラスにView.OnClickListenerインターフェースを追加します。
それに伴い、onClickメソッドを追加します。

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.play_button).setOnClickListener(this);
        findViewById(R.id.file_button).setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {

    }
}
      

なおonCreateメソッド内でボタンに対してsetOnClickListenerメソッドでリスナーを設定しておきます。

この状態で”Shift+F10”でアプリケーションを実行すると、デフォルトで作成されているデバイスエミュレーターが起動し以下のように表示されます。

最初の画面

AVD(Android Virtual Device)の作成

基本的にプログラムの実行/デバッグで使用するデバイスは、Android Studioと共にデフォルトで提供されているAVDで問題ないはずですが、以下のように新しく作成する事もできます。

  1. 右端上のタブ”DeviceManager”を選択
  2. “Device Manager”ペインの左上の”CreateDevice”ボタンをクリック
    Device Manager
  3. “Virtual Device Configuration”の”Select Hardware”ウィンドウが表示される
    Virtual Device Configulation / Select Hardweare
  4. 適当なデバイスを選択後、“Next”ボタンをクリック
    例として”Pixel 2”を選択
  5. “Virtual Device Configuration”の”System Image”ウィンドウが表示される Virtual Device Configulation / System Image
  6. 適当な”System Image”を選択して”Next”ボタンをクリック
    基本的にはデフォルトで選択されているImageでOK
  7. “Virtual Device Configuration”の”Android Virtual Device (AVD)“ウィンドウが表示される Virtual Device Configulation / Android Virtual Device
  8. 基本的にデフォルトで問題ないので”Finish”ボタンをクリック
    なお”AVD Name”は適当に変更可

以上で新しいAVDが作成されます。

なお実行/デバッグに使用するデバイスはエディター画面の右上部分で選択できます。AVDの選択

YUVファイル選択

“File…”ボタンをクリックした時にファイル選択のActivityを表示し、ファイルの選択を行えるようにします。

先ずActivityを呼び出す前にファイル選択の結果を受け取るコードを作成しておきます。
Activityから結果を受け取る為にはActivityResultLauncher<Intent>クラスのインスタンスをregisterForActivityResultで作成します。
ファイル選択の結果を受け取るコールバックメソッドはregisterForActivityResultの第2引数に渡すActivityResultCallback<ActivityResult>クラスインスタンスのonActivityResultメソッドに実装します。

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
            :
            :
    ActivityResultLauncher<Intent> filePickerStartForResult = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
            new ActivityResultCallback<ActivityResult>() {
                @Override
                public void onActivityResult(ActivityResult result) {
                    if (result.getResultCode() == Activity.RESULT_OK) {
                        Intent intent = result.getData();
                        Uri uri = intent.getData();
                        Toast.makeText(getApplicationContext(), uri.toString(), Toast.LENGTH_SHORT).show();
                    }
                }
            });
                :
                :
}
    

とりあえず現状はToastを使用してファイルのURIを表示するようにしています。

このインスタンスを使用して、onClickメソッド中でファイル選択のActivityを呼び出すには以下のようにします。

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
            :
            :
   @Override
    public void onClick(View view) {
        if (view.getId() == R.id.file_button) {
            Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
            intent.addCategory(Intent.CATEGORY_OPENABLE);
            intent.setType("application/octet-stream");
            filePickerStartForResult.launch(intent);
        }
    }
}
    
  1. ACTION_OPEN_DOCUMENTを引数にしてIntentのインスタンスを作成
  2. addCategoryメソッドにCATEGORY_OPENABLEを指定して”ファイルオープン”とする
  3. setTypeメソッドにMIMEタイプ”application/octet-stream”を指定してバイナリファイル全般を対象とする
    MIMEタイプとして”video/*“等としたいが、”yuv”はビデオのタイプとして登録されていないのでバイナリファイルで代替
  4. ActivityResultLauncher<Intent>launchインスタンスメソッドでファイル選択のActivityを開始

なおMIMEタイプについてはMIME タイプ (IANA メディアタイプ)が詳しいので、そちらを参照してください。

実際のファイル選択

この時点で実行し”File…“ボタンをクリックすると画面が”ファイル選択のActivity”に切り替わります。

“akiyo_cif.yuv”ファイルはこの時点で未だAVDに送られていません。
ファイルをAVDに送る一番簡単な方法は、AVDの上にファイルをドラッグアンドドロップする事です。

ファイル自体は”ダウンロード”以下に配置されますので、左上”≡“をクリックして”ダウンロード”を選択します。
“akiyo_cif.yuv”ファイルがあるはずですのでクリックします。

Selecty download folder ⇒ Select akiyo_cif.yuv file

ファイル選択後は元のActivityに戻り、下部ToastにURIが表示されます。

ちなみに、もう少し柔軟にADV上のファイルを出し入れしたい場合には、メニューから”View”→“Tool Windows”→“Device File Explorer”を選択します。
“Device File Explorer”ペインが表示されますので、ADV上のファイルを操作する事ができます。

YUV⇒RGB変換用クラスの作成

上記でファイルの選択を行ったので、続けてYUVファイルをオープンし、データを読み込んだ後、YUV⇒RGB変換を行うクラスを作成しておきます。

  1. 左のprojectツリーペインの”app”→“java”→“com.example.showyuvonandroid”を右クリック
  2. メニューから”New”→“Java Class”を選択
  3. “New Java Class”ウィンドウが表示される
    New Java Class / ShowYUVImage
  4. クラス名として”ShowYUVImage”を入力、“Class”を選択しリターン

YUV⇒RGB変換用クラスのコンストラクタ作成

ShowYUVImageクラスが作成されますので、Contextクラスを引数とするコンストラクタを追加します。
インスタンス変数としては、画像サイズやファイルストリーム等を記載します。
なお、YUV⇒RGB変換も行いますので、“Y”, “U”, “V”, “ARGB”其々のデータを保持するインスタンス変数も追加しておきます。

public class ShowYUVImage {

    private int width;
    private int height;
    private int ySize;
    private int uvSize;

    private byte[] y;
    private byte[] u;
    private byte[] v;
    private int[] argb;

    private InputStream yuvFile;

    public ShowYUVImage(Context context) {
        int scale = context.getResources().getDisplayMetrics().densityDpi;
        int w = context.getResources().getDimensionPixelSize(R.dimen.width);
        int h = context.getResources().getDimensionPixelSize(R.dimen.height);
        width = w * 160 / scale;
        height = h * 160 / scale;
        ySize = width * height;
        uvSize = (width / 2) * (height / 2);
        y = new byte[ySize];
        u = new byte[uvSize];
        v = new byte[uvSize];
        argb = new int[ySize];
    }
}
      

なお、コンストラクタの最初の5行目、“height”の値を計算するまでは、“dp”で指定した画面サイズを”pixel”単位に変換する為の操作です。
これにより、“width”と”height”はCIFサイズ352 x 288にセットされます。

YUV⇒RGB変換用クラスでのファイルオープンとデータの読み込み

YUV⇒RGB変換用クラスShowYUVImageではファイルからYUVデータを読み込みます。
そのため、ファイルのURIを受け取ってオープンし、YUVデータを読み込むメソッドが必要となります。

まず、ファイルのURIを受け取ってオープンするメソッドとして以下を追加します。

private boolean available = false;

public boolean isAvailable() {
    return available;
}

public void setYUVFileURL(ContentResolver contentResolver, Uri uri) {
    try {
        yuvFile = contentResolver.openInputStream(uri);
        available = true;
        readYUV();    // YUVデータを読み込むメソッド
    }
    catch (Exception e) {
        Log.e("SetFile", e.toString());
        available = false;
    }
}
      

URIを指定してファイルをオープンするためには、ContentResolverクラスのopenInputStreamメソッドを使用します。
状況により例外が発生した場合にはLogCatに内容を出力してリターンします。

なお、インスタンス変数”available”はYUVデータが正常に読み込まれたことを示すフラグです。
“available”にはゲッターを作成しておきます。

YUVファイルが正常にオープンできましたら、YUVデータを読み込んでおきます。

protected void readYUV() {
    if (available) {
        try {
            yuvFile.read(y);
            yuvFile.read(u);
            int size = yuvFile.read(v);
            if (size < uvSize) {
                available = false;
            }
        } catch (IOException e) {
            Log.e("ReadFile", e.toString());
            available = false;
        }
    }
}
      

なお、“V”データを読み込んだ際にデータが足りない、もしくは読み込めなかった場合には読み込み失敗としてフラグavailableを"false"にセットします。

YUV⇒RGB変換メソッドの追加

YUVファイルからデータを読み込むことができましたら、次はYUV⇒RGB変換です。
基本的に使用する計算式は今までと同じですので、YUV⇒RGB変換(JOGL編)と概ね同じコードを使用できます。

protected int clip(int n) {
    return (n <= 0) ? 0 : ((n >= 255) ? 255 : n);
}

protected void transYUV2RGB() {
    for (int h = 0; h < height; h++) {
        int y_pos = h * width;
        int uv_pos = (h / 2) * (width / 2);

        for (int w = 0; w < width; w++) {
            int yi = y[y_pos + w] & 0x0FF;
            int ui = u[uv_pos + (w / 2)] & 0x0FF;
            int vi = v[uv_pos + (w / 2)] & 0x0FF;

            double y16 = yi - 16.0;
            double u128 = ui - 128.0;
            double v128 = vi - 128.0;

            int r = (int) ((1.164 * y16) + (0.0 * u128) + (1.596 * v128));
            int g = (int) ((1.164 * y16) + (-0.392 * u128) + (-0.813 * v128));
            int b = (int) ((1.164 * y16) + (2.017 * u128) + (0.0 * v128));
            r = clip(r);
            g = clip(g);
            b = clip(b);
            argb[y_pos + w] = (0xFF << 24) | (r << 16) | (g << 8) | b;
        }
    }
}
      

YUV⇒RGB変換クラスでのBitmapの作成

画像データをViewクラスに描画するためにはBitmap形式の画像としてViewクラスに渡す必要があります。
なので、作成したARGB形式のデータをBitmap形式に変換して返すメソッドを追加しておきます。

public Bitmap getNextBitmap() {
    if (available) {
        transYUV2RGB();
        Bitmap bitmap = Bitmap.createBitmap(argb, width, height, Bitmap.Config.ARGB_8888);
        readYUV();
        return bitmap;
    }
    return null;
}
      

ARGB形式のデータからBitmap形式の画像を作成するためにはcreateBitmapクラスメソッドを使用します。
Bitmap形式の画像作成後は次のYUVデータを読み込んでおきます。

Viewの修正

YUVファイルからデータを読み込んでBitmap形式の画像を作成できるようになりましたので、次はViewクラスを使って画像の表示を行う事にします。

Viewクラスは基本のクラスですし、描画のコードもonDrawメソッドに集約すれば良いので非常に手軽に使用できます。

Viewのサブクラスの作成

先ず、Viewのサブクラスを作成します。

  1. 左のprojectツリーペインの”app”→“java”→“com.example.showyuvonandroid”を右クリック
  2. メニューから”New”→“Java Class”を選択
  3. “New Java Class”ウィンドウが表示される
    New Java Class / ShowYUVView
  4. クラス名として”ShowYUVView”を入力、“Class”を選択しリターン

新たに”ShowYUVView.java”ファイルが作成されますので、ShowYUVViewクラスがViewクラスを継承するように、クラス名の後にextends Viewを追加しておきます。

ShowYUVViewクラスへのコンストラクタの追加

ShowYUVViewクラスは”activity_main.xml”ファイル内で”View”タグを置き換えます。
そのため、Viewクラスが実装しているコンストラクタは全て実装する事が推奨されます。

なお、ShowYUVViewクラスは描画のためにShowYUVImageクラスのインスタンスを必要としますので、インスタンス変数としてコンストラクタ内で作成しておきます。

public class ShowYUVView extends View {

    private ShowYUVImage showYUVImage;

    public ShowYUVView(Context context) {
        super(context);
        showYUVImage = new ShowYUVImage(context);
    }

    public ShowYUVView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        showYUVImage = new ShowYUVImage(context);
    }

    public ShowYUVView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        showYUVImage = new ShowYUVImage(context);
    }

    public ShowYUVView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        showYUVImage = new ShowYUVImage(context);
    }
}
      

ShowYUVViewクラスのonDrawメソッドのオーバーライド

UIスレッドでShowYUVViewクラスのinvalidateメソッドがコールされると、onDrawメソッドがコールされます。
Viewクラスのサブクラスで描画を行うためには、ViewクラスのonDrawメソッドをオーバーライドします。

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    Bitmap bmp = showYUVImage.getNextBitmap();
    if (bmp != null) {
        Rect dst = new Rect(0, 0, canvas.getWidth() - 1, canvas.getHeight() - 1);
        canvas.drawBitmap(bmp, null, dst, null);
    }
    else {
        canvas.drawARGB(0xFF, 0x00, 0x00, 0xFF);
    }
}
      

最初にViewクラスのonDrawメソッドをコールしておきます。
次にBitmap形式の画像を取得し、CanvasクラスのdrawBitmapメソッドで描画を行います。
なお、Bitmap形式の画像の取得に失敗した場合には、drawARGBメソッドで画面全体を赤色に塗り潰すようにしています。

因みに描画領域を示す”dst”を作成する際にRectクラスのコンストラクタの第3引数と第4引数は幅と高さではありません
右と下の画素の座標を指定する事になっていますので、描画領域全体を指定するには、幅および高さから1を引く必要があります。

ShowYUVViewクラスのその他のメソッド

ShowYUVImageクラスのインスタンスはShowYUVViewで保持するようにしましたので、ファイルのURIのセットやYUVデータが準備できているかの問い合わせはShowYUVViewクラスを通して行う必要があります。
そのためのメソッドを追加しておきます。

    public void setYUVFileURL(ContentResolver contentResolver, Uri uri) {
        showYUVImage.setYUVFileURL(contentResolver, uri);
    }

    public boolean isAvailable() {
        return showYUVImage.isAvailable();
    }
      

ViewのサブクラスへのYUVデータの表示

以上で準備ができましたので、以下、ViewのサブクラスをUIにセットし、描画を行うようにします。

actibity_main.xmlへのShowYUVViewの追加

“activity_main.xml”をオープンし、コードを表示します。
以下のように”View”タグ部分を”com.example.showyuvonandroid.ShowYUVView”に変更します。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <com.example.showyuvonandroid.ShowYUVView
        android:id="@+id/view"
        android:layout_width="@dimen/width"
        android:layout_height="@dimen/height"
        android:layout_gravity="center_horizontal"
        android:layout_margin="16dp" />

    <LinearLayout
        :
        :
    </LinearLayout>

</LinearLayout>
      

因みにレイアウト用のXML中で独自に作成したクラスをタグの要素名として指定する場合には、完全修飾クラス名を指定する必要があります。

ShowYUVViewでのファイル名設定と表示

ファイル名のセットとShowYUVViewへの描画を行うため、“MainActivity.java”を修正します。
MainActivityのインスタンス変数”filePickerStartForResult”中のコールバックメソッドonActivityで以前にToastでURIを表示している部分にコードを追加します。

ActivityResultLauncher<Intent> filePickerStartForResult = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
        new ActivityResultCallback<ActivityResult>() {
            @Override
            public void onActivityResult(ActivityResult result) {
                if (result.getResultCode() == Activity.RESULT_OK) {
                    Intent intent = result.getData();
                    Uri uri = intent.getData();
                    Toast.makeText(getApplicationContext(), uri.toString(), Toast.LENGTH_SHORT).show();
                    ShowYUVView view = findViewById(R.id.view);    // 以下追加
                    view.setYUVFileURL(getContentResolver(), uri);
                    view.invalidate();
                    findViewById(R.id.play_button).setEnabled(true);
                }
            }
        });
      

この時点で実行し”File…“ボタンをクリックして”akiyo_cif.yuv”ファイルを選択すると、以下のように表示されるはずです。Viewクラスによる最初の画面

なお次で動画再生をするため、最後の部分で”Play”ボタンを有効化しておきます。

動画対応

定期的に画像を更新してYUVデータを動画として表示するため、タイマーを使用します。

タイマータスクの作成

タイマーにより定期的に実行されるタスクを記述するため、先ず、TimerTaskクラスを継承するクラスを作成します。
TimerTaskのサブクラスでは、定期的に実行されるコードはrunメソッド内に記述します。
とりあえずサブクラスの名前をShowYUVTimerTaskとし、MainActivityクラスの内部クラスとして実装しておきます。

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    Handler handler = new Handler();
    Timer timer;
    TimerTask timerTask;
        :
        :
    protected class ShowYUVTimerTask extends TimerTask {

        @Override
        public void run() {
            handler.post(new Runnable() {
                @Override
                public void run() {
                    ShowYUVView view = findViewById(R.id.view);
                    if (view.isAvailable()) {
                        findViewById(R.id.view).invalidate();
                    }
                    else {
                        timer.cancel();
                        findViewById(R.id.file_button).setEnabled(true);
                    }
                }
            });
        }
    }
}
      

TimerTaskrunメソッドは基本的にUIスレッドとは別のスレッドでコールされます。
ですので、そのままViewでの描画等を行ってしまいますと色々と不具合を生じます。
それを避けるため、Handlerでメッセージキューにコードをポストし、UIスレッドで実行させるようにします。

Playボタンでタイマー開始

“Play”ボタンをクリックした時にタイマーを開始するコードを追加します。
MainActivityクラスのonClickメソッドを以下のように修正します。

public void onClick(View view) {
    if (view.getId() == R.id.file_button) {
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType("application/octet-stream");
        filePickerStartForResult.launch(intent);
    }
    // 以下を追加
    else if (view.getId() == R.id.play_button) {
        timerTask = new ShowYUVTimerTask();
        timer = new Timer(true);
        long period = getResources().getInteger(R.integer.period);
        timer.schedule(timerTask, period, period);
        view.setEnabled(false);
        findViewById(R.id.file_button).setEnabled(false);
    }
}
      

因みに”R.integer.period”はリソースとして定義します。

タイマー間隔の指定

タイマーの間隔は66msecですが、リソースとして指定しておきます。

  1. 左のProjectツリーペインの”app”を右クリック
  2. メニューから”New”→“XML”→“Values XML File”を選択
  3. リソースフォルダにXMLファイルを作成するウィンドウが表示される
    New Android Component / constants.xml
  4. “Values File Name”に”constants”と入力
  5. “Finish”ボタンをクリック
  6. “app”→“res”→“values”→“constants.xml”ファイルが作成される

なお、整数の定数等を指定するリソースは”constants.xml”に記載する事が決まっている訳ではないようで、“integers.xml”等でもOKです。

タイマー間隔指定するため、“constants.xml”は以下のようにします。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <integer name="period">66</integer>
</resources>
      

タグの要素名は”integer”です。

この時点で実行し”File…“ボタンをクリックして”akiyo_cif.yuv”を選択、“Play”ボタンをクリックすると動画が表示されます。

ここまでのコードはここからダウンロードできます。