YUV⇒RGB変換(WebGL編)

Canvas 2D Contextによる描画

WebGLによる描画を行う前に、先ずは<canvas>要素に”2D Context”を使用して描画を行ってみます。
その過程でローカルに存在するYUVファイルからのデータの読み込みやYUV⇒RGB変換等のコードを準備します。

index.htmlへの各要素の配置

先ずは、“index.html”を”dst”ディレクトリ下に準備します。

ページ内には、とりあえず以下の要素を配置します。

要素 type id 概要
canvas canvas2D 画像表示用 CIFサイズ
button button btnOpenFile ファイルオープン
button button btnPlay 再生開始
input text txtFileName ファイル名表示 readonly
input file fileOpenDialog ファイル選択 hidden

なお、<canvas>要素を使用しますのでHTML5は必須です。
ですので、先頭のドキュメントタイプ宣言は<!doctype html>とする必要があります。

<!doctype html>
<html lang="jp">

<head>
  <title>画像表示</title>
  <!-- Required meta tags -->
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

  <!-- Bootstrap CSS v5.2.1 -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css" rel="stylesheet"
    integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT" crossorigin="anonymous">

</head>

<body>
  <main>
    <h1>CIFサイズのYUV画像を表示</h1>
    <h2>&ltcanvas&gtにYUV⇒RGB変換後の画像を表示</h2>
    <div class="d-flex bd-highlight mb-3 mt-5 justify-content-center">
      <canvas class="border border-primary" id="canvas2D" width="352" height="288">
        CIFサイズのYUV画像を表示します。
      </canvas>
    </div>
    <div class="d-flex bd-highlight mb-3 justify-content-center">
      <button type="button" class="btn btn-primary mx-2" disabled id="btnPlay">Play</button>
      <button type="button" class="btn btn-primary mx-1" id="btnOpenFile">File...</button>
      <input type="text" id="txtFileName" readonly>
    </div>

    <!-- ファイルオープンダイアログのためのボタン(見えない) -->
    <input type="file" name="file-open" id="fileOpenDialog" accept=".yuv" hidden>

  </main>

  <!-- Bootstrap JavaScript Libraries -->
  <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"
    integrity="sha384-oBqDVmMz9ATKxIep9tiCxS/Z9fNfEXiDAYTujMAeBAsjFuCZSmKbSSUnQlmh/jp3" crossorigin="anonymous">
  </script>

  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/js/bootstrap.min.js"
    integrity="sha384-7VPbUDkoPSGFnVtYi0QogXtr74QeVeeIs99Qfg5YCF+TidwNdjvaKZX19NZ/e6oz" crossorigin="anonymous">
  </script>
</body>

</html>
    

なお、各要素の配置を簡単にするためBootstrap5を使用しています。
必須ではありませんので、CSS等での指定の方が好ましい場合には、そちらを採用しても問題ありません。

プレイボタン(id=“btnPlay”)を”disabled”属性により無効化しているのは、ファイルをオープンする前は、クリックしても正常動作せず、意味を持たないからです。
また、ファイル名を表示する領域(id=“txtFileName”)を”readonly”属性により書き込みを禁止しているのは、ファイル名を直接指定しないようにするためです。

type="file"<input>要素(id=“fileOpenDialog”)は、ファイル選択ダイアログを表示してローカルのYUVファイルを選択するために配置します。
本来であればファイルオープンボタン(id=“btnOpenFile”)やファイル名を表示する領域(id=“txtFileName”)を配置せず、type="file"<input>要素(id=“fileOpenDialog”)のみを使用すればOKです。
ただ、type="file"<input>要素は下記のように標準のデザインがダサく、CSS等を使用して外見を整えるのも大変な労力が必要となります。ファイル選択用<input data-src=" width="283″ height="40″ class="aligncenter size-full wp-image-591″>

そのため、type="file"<input>要素(id=“fileOpenDialog”)を非表示にしています。

代わりにBootStrapでそれなりのデザインを選択できる、ファイルオープンボタン(id=“btnOpenFile”)とファイル名を表示する領域(id=“txtFileName”)を表示します。

なおファイル選択ダイアログの表示については、ファイルオープンボタン(id=“btnOpenFile”)をクリックした際に、TypeScriptのコードでtype="file"<input>要素(id=“fileOpenDialog”)をクリックするメソッドをコールする事で実現しています。

この時点で”index.html”をブラウザに表示すると、キャンバスと2つのボタン、空のテキスト領域が表示されるはずです。

HTML design

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

ローカルファイルを読み込む

最初に、ローカルに存在するファイルakiyo_cif.yuvからYUVデータを読み込むクラス、YUVFileを作成します。

なおYUVファイルはアリゾナ州立大学のライブラリXiph.orgのホームページ等からダウンロードして下さい。

先ず、“src”ディレクトリ下に”yuvfile.ts”ファイルを作成します。
“yuvfile.ts”には以下のようにYUVFileクラスを作成します。

export class YUVFile {
    static readonly CIF_WIDTH = 352;
    static readonly CIF_HEIGHT = 288;
    static readonly Y_SIZE = YUVFile.CIF_WIDTH * YUVFile.CIF_HEIGHT;
    static readonly UV_SIZE = (YUVFile.CIF_WIDTH / 2) * (YUVFile.CIF_HEIGHT / 2);

    private file: File | undefined;
    private _y: Uint8Array | undefined;
    private _u: Uint8Array | undefined;
    private _v: Uint8Array | undefined;
    private start: number = 0;
    private _available: boolean = false;

    public get y(): Uint8Array | undefined {
        return this._y;
    }

    public get u(): Uint8Array | undefined {
        return this._u;
    }

    public get v(): Uint8Array | undefined {
        return this._v;
    }

    public get available(): boolean {
        return this._available;
    }

    constructor(file: File) {
        this.file = file;
    }

    protected async readFile(size: number): Promise<Uint8Array | undefined> {
        let array: Uint8Array | undefined;
        if (this.file != undefined) {
            const end: number = this.start + size;
            const arraybuffer: Promise<ArrayBuffer> = this.file.slice(this.start, end, this.file.type).arrayBuffer();
            array = new Uint8Array(await arraybuffer);
            if (array.byteLength != size) {
                array = undefined;
            }
            this.start = end;
        }
        return array;
    }

    protected async readYUV() {
        this._y = await this.readFile(YUVFile.Y_SIZE);
        this._u = await this.readFile(YUVFile.UV_SIZE);
        this._v = await this.readFile(YUVFile.UV_SIZE);
        this._available = (this._v != undefined);
    }

    public rewind() {
        this.start = 0;
    }
}
    

YUVFileクラスは他のファイルで読み込みますので、クラス名の前にexport修飾子を付加しておきます。

次に、CIFサイズ(“CIF_WIDTH”, “CIF_HEIGHT”)およびYUVのデータサイズ(“Y_SIZE”, “UV_SIZE”)を定数(static readonly)としてセットしておきます。

YUVFileクラスのコンストラクタ

YUVFileクラスのコンストラクタでは、引数にFileインターフェースのオブジェクトを取ります。
渡されたFileオブジェクトは他のメソッドで使用するためプライベートフィールド”file”に保持しておきます。

更に、データの読み取り位置を示すプライベートフィールド”start”は宣言と同時に”0”クリアしておきます。
また、YUVデータが取得できるかどうかを示すプライベートフィールド”_available”を”false”にしておくと同時にgetteravailableを追加しておきます。

ファイルからのデータ読み込み

YUVFileクラスにYUVデータを読み込むメソッドreadFileを作成します。
引数としては読み込むデータのサイズを指定します。

なお、JavaScriptのFileクラスはC / C++Java等と異なり、“Open”する必要はないようです。
と云うより、“Open”に相当するメソッド自体が存在しません。
そのままarrayBufferメソッド等をコールする事でデータ全体を配列として読み込む事ができます。

なお、基本的にはファイル全体のデータを一括して読み込む事になります。
勿論、ファイル全体を一括して読み込み、1画面分のY, U, Vのデータに切り出す事もできます。
ただ、ファイルサイズが大きい場合には、ファイル全体を読み込む際に時間がかかったり、メモリを多く使用する等のデメリットがあります。

これらのデメリットを避けるため、ファイルのデータの一部を指定するサブセットをsliceメソッドにより作成してから読み込みます。
なお、サブセットにおけるデータの開始位置 / 終了位置はプライベートフィールド”start”と引数”size”から取得します。

因みにarrayBufferメソッドは非同期関数のため、データ読み込み完了まで待つための工夫が必要です。

YUVデータの読み込み

Y, U, VデータはUint8Array型とし、プライベートフィールド”_y”, “_u”, “_v”にセットします。
なお、Y, U, Vデータ其々にgetterを追加しておきます。

YUVFileのリワインド

YUVファイルのデータの読み出し位置を最初に戻すためのメソッドrewindを追加します。
データ読み出し位置のプライベートフィールド”start”を”0”クリアするだけです。

描画用ビットマップ作成

YUVFileオブジェクトで読み込んだYUVデータをRGBデータに変換し、JavaScriptのビットマップを扱うクラスImageBitmapのオブジェクトを取得できるようにします。

そのためのメソッドをYUVFileクラスに追加したいのですが、後にWebGLのテクスチャ作成時にも利用する事を考慮して、別途、YUVFileクラスを継承したRGBImageクラスを作成する事にします。

“src”ディレクトリ下に”rgbimage.ts”ファイルを作成し、以下のコードを記載します。

import { YUVFile } from "./yuvfile";

export class RGBImage extends YUVFile {
    private bitmap: ImageBitmap | undefined;

    constructor(file: File) {
        super(file);
    }

    public finalize() {
        this.close();
    }

    protected close() {
        if (this.bitmap != undefined) {
            this.bitmap.close();
            this.bitmap = undefined;
        }
    }

    protected transYUV2RGB(): Uint8ClampedArray {
        const rgba = new Uint8ClampedArray(YUVFile.CIF_WIDTH * YUVFile.CIF_HEIGHT * 4);
        if ((this.y != undefined) && (this.u != undefined) && (this.v != undefined)) {
            let i: number = 0;
            for (let h: number = 0; h < YUVFile.CIF_HEIGHT; h++) {
                const y_pos: number = h * YUVFile.CIF_WIDTH;
                const uv_pos: number = Math.floor(h / 2) * (YUVFile.CIF_WIDTH / 2);

                for (let w: number = 0; w < YUVFile.CIF_WIDTH; w++) {
                    const yi: number = this.y[y_pos + w];
                    const ui: number = this.u[uv_pos + Math.floor(w / 2)];
                    const vi: number = this.v[uv_pos + Math.floor(w / 2)];

                    if ((yi != undefined) && (ui != undefined) && (vi != undefined)) {
                        const y16: number = yi - 16;
                        const u128: number = ui - 128;
                        const v128: number = vi - 128;

                        rgba[i++] = (1.164 * y16) + (0.0 * u128) + (1.596 * v128);
                        rgba[i++] = (1.164 * y16) + (-0.392 * u128) + (-0.813 * v128);
                        rgba[i++] = (1.164 * y16) + (2.017 * u128) + (0.0 * v128);
                        rgba[i++] = 255;
                    }
                }
            }
        }
        return rgba;
    }

    public async getNextBitmap(): Promise<ImageBitmap | undefined> {
        this.close();

        await this.readYUV();
        if (this.available) {
            const rgba: Uint8ClampedArray = this.transYUV2RGB();
            const image = new ImageData(rgba, YUVFile.CIF_WIDTH);
            this.bitmap = await window.createImageBitmap(image);
        } else {
            this.bitmap = undefined;
        }

        return this.bitmap;
    }

}
    

RGBImageクラスのコンストラクタ

RGBImageクラスのコンストラクタでは、引数にFileインターフェースのオブジェクトを取ります。
コンストラクタ内では、単に親クラスであるYUVFileクラスのコンストラクタを呼び出しているだけです。

RGBImageクラスのファイナライザ

TypeScriptのクラスには本来デストラクタは必要ないはずなので、デストラクタが明示的に定義されていません。
ただ、幾つかのオブジェクトでは、明示的に破棄しないといけない場合があります。
JavaScriptのビットマップを扱うクラスImageBitmapcloseメソッドをコールして明示的に破棄する必要があるオブジェクトです。

RGBImageオブジェクトを破棄する前にImageBitmapオブジェクトを破棄できるよう、デストラクタの代わりにファイナライザとしてfinalizeメソッドを定義しておきます。

finalizeメソッド中では、プライベートフィールド”bitmap”に保管しておいたImageBitmapオブジェクトをcloseメソッドにより明示的に破棄しています。

YUV⇒RGB変換のメソッド

YUV⇒RGB変換のメソッドtransYUV2RGBについては、前回以前のYUV⇒RGB変換と殆ど変わりありません。

ただ、変換後のRGBAデータを保持する配列Uint8ClampedArrayは少々特殊です。
基本的に8ビットの符号なし整数値の配列Uint8Arrayと同様ですが、以下のような追加の性質があります。

  • 整数以外の数値、例えば浮動小数点数等を代入すると、最も近い整数に変換して代入される
  • [0, 255]の範囲外の整数を代入すると、負数なら0が、255より大きな値であれば255が代入される

前回以前のYUV⇒RGB変換であれば代入時にMath.roundメソッド等で浮動小数点数を整数に変換後、[0, 255]の範囲にクリップするメソッド等を必要とします。
ですが、Uint8ClampedArrayオブジェクトへの代入に関しては、それらを必要としません。

ImageBitmap作成

YUVデータをRGBデータに変換後は、RGBデータを使ってImageBitmapオブジェクトを作成していきます。

ImageBitmapオブジェクトを取得するメソッドgetNextBitmapでは、以下のような処理を行います。

  1. 前にImageBitmapオブジェクトを作成していれば、closeメソッドで破棄
  2. 次のフレームのYUVデータをreadYUVメソッドで読み込み
  3. 読み込んだYUVデータをtransYUV2RGBメソッドでYUV⇒RGB変換
  4. YUV⇒RGB変換で得られたUint8ClampedArrayオブジェクトのRGBデータと画像の幅を引数にImageDataオブジェクトを作成
  5. ImageDataオブジェクトを引数にcreateImageBitmapメソッドでImageBitmapオブジェクトを作成

なおreadYUVメソッドとcreateImageBitmapメソッドは非同期関数のため、awaitで処理の完了を待ちます。
当然、getNextBitmapメソッドも非同期関数となります。

HTMLの要素のイベント対応

“index.html”に記載したHTML要素に対し、クリックや値の変更等のイベントが発生した際の動作を記述します。
ただ、後にWebGLを使用した描画を行う<canvas><button>, <input>要素等も”index.html”に追加する予定ですし、それらのイベント対応も今回と概ね同様となる予定です。
なので、共通化できる部分を抽象クラスEvents4Elementsとして纏めます。

非常に長いコードですので、先ずはコンストラクタまでを以下に示します。

export type Element = 'canvas' | 'filebtn' | 'playbtn' | 'filetxt' | 'dialog';

export abstract class Events4Elements {
    private canvasID: string | undefined;
    private fileBtnID: string | undefined;
    private playBtnID: string | undefined;
    private fileTxtID: string | undefined;
    private dialogID: string | undefined;

    private timerID: number | undefined;

    constructor(ids: Map<Element, string>) {
        this.canvasID = ids.get('canvas');
        this.fileBtnID = ids.get('filebtn');
        this.playBtnID = ids.get('playbtn');
        this.fileTxtID = ids.get('filetxt');
        this.dialogID = ids.get('dialog');

        /* Add event listener for buttons */
        if (this.fileBtnID != undefined) {
            const btnOpenFile = document.getElementById(this.fileBtnID) as HTMLButtonElement;
            btnOpenFile.addEventListener('click', this.openFileDialog.bind(this));
        }
        if (this.playBtnID != undefined) {
            const btnPlay = document.getElementById(this.playBtnID) as HTMLButtonElement;
            btnPlay.addEventListener('click', this.play.bind(this));
        }

        /* Add event listener for file dialog */
        if (this.dialogID != undefined) {
            const fileOpenDialog = document.getElementById(this.dialogID) as HTMLInputElement;
            fileOpenDialog.addEventListener('change', this.setFileInfo.bind(this));
        }
    }
        :
        :
}
    

Events4Elementsクラスのコンストラクタ

index.html内に配置した要素のID取得

Events4Elementsクラスのコンストラクタでは、引数として”index.html”に配置した<canvas><button>, <input>要素等の”id”属性の値を受け取ります。
なお、要素と”id”の対応が判るように、コンストラクタの引数はMapオブジェクトとし、キーにはUnion型で定義した”Element”を使用します。

“Element”には以下の値を持たせるようにします。

  • ‘canvas’: 画像を描画するためのキャンバス
  • ‘filebtn’: ファイルオープンボタン
  • ‘playbtn’: プレイボタン
  • ‘filetxt’: ファイル名を表示するテキスト領域
  • ‘dialog’: ファイル選択のための入力

コンストラクタ内では、先ず、渡されたMapオブジェクトから”index.html”に配置された各要素の”id”の文字列を取得し、プライベートフィールドに保存します。
これら”id”の文字列はdocument.getElementByIdメソッドにより”index.html”に配置された要素を取得する際に使用されます。

ファイルオープンボタンのクリックイベント

ファイルオープンボタンをクリックした際の動作を記述するメソッドopenFileDialogaddEventListenerメソッドで登録します。

なお、addEventListenerメソッドの第2引数にはthis.openFileDialog.bind(this)のように.bind(this)を付加しています。
これは、.bind(this)が無いと、コール先のopenFileDialogメソッド内で自身のオブジェクトを示す”this”が”undefined”になってしまうからです。
回避策は幾つか存在しますが、今回は.bind(this)を付加する事で回避するようにしました。

プレイボタンのクリックイベント

プレイボタンをクリックした際の動作を記述するメソッドplayaddEventListenerメソッドで登録します。

なお、addEventListenerメソッドの第2引数にはthis.play.bind(this)のように.bind(this)を付加しています。
これは、.bind(this)が無いと、コール先のplayメソッド内で自身のオブジェクトを示す”this”が”undefined”になってしまうからです。
回避策は幾つか存在しますが、今回は.bind(this)を付加する事で回避するようにしました。

ファイルインプットの値変更イベント

ファイルインプットの値が変更された際の動作を記述するメソッドsetFileInfoaddEventListenerメソッドで登録します。

なお、addEventListenerメソッドの第2引数にはthis.setFileInfo.bind(this)のように.bind(this)を付加しています。
これは、.bind(this)が無いと、コール先のsetFileInfoメソッド内で自身のオブジェクトを示す”this”が”undefined”になってしまうからです。
回避策は幾つか存在しますが、今回は.bind(this)を付加する事で回避するようにしました。

openFileDialogメソッド

ファイルオープンボタンのクリックに対応したメソッドopenFileDialogは以下の通りです。
Events4Elementsクラスに追加します。

protected openFileDialog() {
    if (this.dialogID != undefined) {
        const fileOpenDialog = document.getElementById(this.dialogID) as HTMLInputElement;
        fileOpenDialog.value = '';
        fileOpenDialog.click();
    }
}
      

メソッド内では、“index.html”内の隠し要素であるファイルインプットの値をクリアした後、クリックします。
これにより、ファイル選択ダイアログが表示され、YUVファイルを選択できるようになります。

結局、ファイルオープンボタンは、隠し要素であるファイルインプットをユーザーがクリックする事ができないので、代わりにクリックを受け付けるだけの存在です。

setFileInfoメソッド

ファイルインプットのクリックによりファイル選択ダイアログが表示され、ファイルが選択されると、ファイルインプットには選択されたファイル名が表示されます。
ファイル名の表示に伴いコールされるメソッドがsetFileInfoです。

setFileInfoメソッドは以下の通りです。

Events4Elementsクラスに追加します。

protected setFileInfo() {
    if (this.dialogID != undefined) {
        const fileOpenDialog = document.getElementById(this.dialogID) as HTMLInputElement;
        if (fileOpenDialog.value != '') {
            const files: FileList | null = fileOpenDialog.files;
            this.setFileName(files);
            this.showNextImage();
        }
    }
}
    :
    :
protected abstract setFileName(files: FileList | null | undefined): void;

protected abstract showNextImage(): void;
      

setFileInfoメソッド内では、取得したファイルリストをsetFileNameメソッドに渡した後、showNextImageメソッドをコールしています。

setFileNameメソッドは、基本的にファイルリストから選択ファイルを取得して、ビットマップやテクスチャを作成するオブジェクトを通してYUVFileオブジェクトにファイルをセットします。
ただ、キャンバスへの描画に対して2Dコンテキストを使用するか、WebGLを使用するか等の違いによりsetFileNameメソッドの内容が異なるため、Events4Elementsクラス内では、抽象メソッドとして宣言します。

同時に、showNextImageメソッドについても内容が異なるため、抽象メソッドとして宣言します。

playメソッド

プレイボタンのクリックに対応したメソッドplayは以下の通りです。
EventsElementsクラスに追加します。

protected play() {
    if (this.timerID == undefined) {
        this.enableFileButton(false);
        this.switchPlayStopButton();
        this.timerID = window.setInterval(this.showNextImage.bind(this), 66);
    } else {
        window.clearInterval(this.timerID);
        this.timerID = undefined;
        this.enableFileButton(true);
        this.switchPlayStopButton();
    }
}
      

playメソッド内では、enableFileButton, switchPlayStopButtonメソッドでボタン類の表示を変更後、window.setIntervalメソッドでタイマーを起動します。
コールバック関数としては、抽象メソッドshowNextImageを指定します。
なお、コールバック関数内で自身を示す”this”オブジェクトが正しく機能するよう、.bind(this)を付加しておきます。

因みにwindow.setIntervalの代わりにsetIntervalとした場合、正常に動作はしますが、戻り値の型が”number”では無く、“NodeJS.Timeout”等、他の型になってしまうため注意が必要です。

また、動画再生中に再度プレイボタンがクリックされた場合には、window.clearIntervalによってタイマーを止めた後、enableFileButton, switchPlayStopButtonメソッドでボタン類の表示を変更しておきます。

stopPlayメソッド

ファイルから全てのフレームのYUVデータの読み出しが終わったら、動画再生を止める必要があります。
その際にコールされるメソッドがstopPlayです。

protected stopPlay() {
    if (this.timerID != undefined) {
        window.clearInterval(this.timerID);
        this.timerID = undefined;
    }
    this.switchPlayStopButton();
    this.enableFileButton(true);
    this.rewind();
}
    :
    :
protected abstract rewind(): void;
      

先ず、window.clearIntervalメソッドでタイマーを止めた後、enableFileButton, switchPlayStopButtonメソッドでボタン類の表示を変更しておきます。
最後にYUVファイルのデータの読み込み位置を最初に戻すためにrewindメソッドをコールします。

なおrewindメソッドは、キャンバスへの描画に対して2Dコンテキストを使用するか、WebGLを使用するか等の違いによりYUVFileオブジェクトのrewindメソッドへのアクセス方法が異なるようになりますので、抽象メソッドとして宣言します。

enableFileButton / enablePlayButtonメソッド

ファイルオープンボタンやプレイボタンの有効、無効をセットするメソッドenableFileButton, enablePlayButtonを追加します。

protected enableFileButton(enable: boolean) {
    if (this.fileBtnID != undefined) {
        const btnOpenFile = document.getElementById(this.fileBtnID) as HTMLButtonElement;
        btnOpenFile.disabled = (!enable);
    }
}

protected enablePlayButton(enable: boolean) {
    if (this.playBtnID != undefined) {
        const btnPlay = document.getElementById(this.playBtnID) as HTMLButtonElement;
        btnPlay.disabled = (!enable);
    }
}
      

switchPlayStopButtonメソッド

プレイボタンの表示を”Play”と”Stop”で切り替えるためのメソッド”switchPlayStopButton”を追加します。

protected switchPlayStopButton() {
    if (this.playBtnID != undefined) {
        const btnPlay = document.getElementById(this.playBtnID) as HTMLButtonElement;
        btnPlay.textContent = (btnPlay.textContent == 'Play') ? 'Stop' : 'Play';
    }
}
      

getCanvasメソッド

“index.html”に配置した<canvas>要素のHTMLCanvasElementオブジェクトを取得するメソッドを追加します。

protected getCanvas(): HTMLCanvasElement | undefined {
    let canvas: HTMLCanvasElement | undefined;
    if (this.canvasID != undefined) {
        canvas = document.getElementById(this.canvasID) as HTMLCanvasElement;
    }
    return canvas;
}
      

extractFileFromFileListメソッド

ファイルインプットでファイル選択ダイアログによりファイル選択後に取得できるファイルリストから、選択したファイルを取得するため、extractFileFromFileListメソッドを追加します。

protected extractFileFromFileList(files: FileList | null | undefined): File | null {
    let file: File | null = null;
    if ((files != undefined) && (files != null) && (files.length > 0)) {
        file = files.item(0);
        if (file != null) {
            const filename: string | undefined = file.name;
            if ((filename != undefined) && (this.fileTxtID != undefined)) {
                const fileNameArea = document.getElementById(this.fileTxtID) as HTMLInputElement;
                fileNameArea.value = filename;
            }
            this.enablePlayButton(true);
        }
    }
    return file;
}
      

なお、ファイルを取得後、ファイル名表示用のテキストエリアにファイル名を表示します。

因みにFileオブジェクト内のファイル名はパスを含みません。
セキュリティの関係でFileオブジェクトからダイレクトにパスを取得するのは難しいようです。

Canvas 2D Contextの要素のイベント対応

canvas要素に2Dコンテキストを使用して画像を描画する際のイベント対応をEvents4Elements2Dクラスとして纏めます。
各イベントに対する動作のテンプレートは抽象クラスEvents4Elementsに纏めていますので、Events4Elements2DクラスはEvents4Elementsクラスを継承するようにします。

import { YUVFile } from "./yuvfile";
import { Element, Events4Elements } from "./events4elements";
import { RGBImage } from "./rgbimage";

export class Events4Elements2D extends Events4Elements {
    static readonly MAP_2D = new Map<Element, string>([
        ['canvas', 'canvas2D'],
        ['filebtn', 'btnOpenFile'],
        ['playbtn', 'btnPlay'],
        ['filetxt', 'txtFileName'],
        ['dialog', 'fileOpenDialog']
    ]);

    private image: RGBImage | undefined;

    constructor() {
        super(Events4Elements2D.MAP_2D);
        const ctx: CanvasRenderingContext2D | undefined = this.getContext2D();
        if (ctx != undefined) {
            ctx.fillStyle = 'green';
            ctx.fillRect(0, 0, YUVFile.CIF_WIDTH, YUVFile.CIF_HEIGHT);
        }
    }

    public finalize() {
        if (this.image != undefined) {
            this.image.finalize();
            this.image = undefined;
        }
    }

    protected getContext2D(): CanvasRenderingContext2D | undefined {
        const canvas: HTMLCanvasElement | undefined = this.getCanvas();
        var ctx: CanvasRenderingContext2D | undefined;
        if (canvas != undefined) {
            ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
        }
        return ctx;
    }

    protected setFileName(files: FileList | null | undefined) {
        if (this.image != undefined) {
            this.finalize();
        }
        const file: File | null = this.extractFileFromFileList(files);
        if (file != null) {
            this.image = new RGBImage(file);
        }
    }

    protected showNextImage() {
        if (this.image != undefined) {
            this.image.getNextBitmap().then((bmp: ImageBitmap | undefined) => {
                if (bmp != undefined) {
                    const ctx: CanvasRenderingContext2D | undefined = this.getContext2D();
                    if (ctx != undefined) {
                        ctx.drawImage(bmp, 0, 0);
                    }
                } else {
                    this.stopPlay();
                }
            });
        }
    }

    protected rewind() {
        if (this.image != undefined) {
            this.image.rewind();
        }
    }
}
    

Events4Elements2Dクラスのコンストラクタ

Events4Elements2Dクラスのコンストラクタでは、最初に”index.htmlに配置した要素の”id”のMapオブジェクトを引数に、親クラスEvents4Elementsのコンストラクタをコールします。

その後、<canvas>要素の2Dコンテキストを取得し、fillRectメソッドで<canvas>要素を緑色に塗りつぶします。

Events4Elements2Dクラスのファイナライザ

Events4Elements2Dオブジェクトを破棄する前に、<canvas>要素に対する描画に必要なRGBImageオブジェクトのファイナライザをコールする必要がありますので、デストラクタの代わりとしてfinalizeメソッドを定義しておきます。

finalizeメソッド中では、プライベートフィールド”image”に保管しておいたRGBImageオブジェクトをfinalizeメソッドにより明示的に破棄します。

getContext2Dメソッド

getContext2D<canvas>要素の2Dコンテキストを取得するためのメソッドです。

HTMLCanvasElementオブジェクトのgetContextメソッドに引数として”2d”を指定する事で取得できます。

Events4Elements2DクラスのsetFileNameメソッド

Events4Elementsクラス内で抽象メソッドとして宣言されていたsetFileNameを実装します。

setFileNameメソッド内では、引数として渡されたファイルリストからextractFileFromFileListメソッドでFileオブジェクトを取得します。
取得したFileオブジェクトを使用し、新たにRGBImageオブジェクトを作成し、プライベートフィールド”image”にセットします。

Events4Elements2DクラスのshowNextImageメソッド

Events4Elementsクラス内で抽象メソッドとして宣言されていたshowNextImageを実装します。

showNextImageメソッドでは、先ずRGBImageオブジェクトのgetNextBitmapメソッドで次のフレームのビットマップであるImageBitmapオブジェクトを取得します。
なおgetNextBitmapは非同期関数ですので、thenメソッドにより処理が終了した際の動作を指定しています。

thenメソッド内では、ビットマップの取得が成功していれば、2Dコンテキストに対してdrawImageメソッドでビットマップの画像を表示します。
ビットマップの取得が失敗していれば、これ以上の画像データが無いとして、stopPlayメソッドによって動画再生を停止します。

Events4Elements2Dクラスのrewindメソッド

Events4Elementsクラス内で抽象メソッドとして宣言されていたrewindを実装します。

RGBImageオブジェクトのrewindメソッドをコールします。

index.tsの作成

“index.html”をブラウザでオープンした際に実行するコードを作成していきます。
先ずは、入り口となるTypeScriptのソースファイル”index.ts”を”src”ディレクトリ下に作成します。

“index.ts”内では、addEventListenerメソッドの第1引数に”DOMContentLoaded”を指定する事でオープン時に実行される関数initを指定します。

import { Events4Elements2D } from "./events4elements2d";

var canvas2D: Events4Elements2D | undefined;

addEventListener('DOMContentLoaded', init);

function init() {
    window.addEventListener('close', finalize);

    canvas2D = new Events4Elements2D();
}

function finalize() {
    if (canvas2D != undefined) {
        canvas2D.finalize();
        canvas2D = undefined;
    }
}
    

init関数

関数init内では、addEventListenerメソッドの第1引数に”close”を指定する事でドキュメントのクローズ時に実行される関数finalizeを指定します。
更に、“index.html”内のキャンバス要素への描画やボタンクリックイベントへの対応を記述したクラスEvents4Elements2Dのオブジェクトを作成しています。

finalize関数

finalize関数内では、Events4Elements2D.finalizeメソッドで、作成したクラスオブジェクト内の不要なオブジェクトを明示的に削除するようにします。

コンパイルと実行

この時点で、“Ctrl-@”等でオープンしたターミナルで以下のコマンドを実行してコンパイルします。

webpack

実行(デバッグ)

VSCodeでのデバッグでは、先ず“.vscode”フォルダ内に”launch.json”ファイルを作成します。

  1. “index.html”を選択して表示 ← 重要
  2. “Ctrl-Shift-D”で”実行とデバッグ”パネルを表示
  3. “launch.jsonファイルを作成します。”をクリック
  4. “デバッガーの選択”で”Web App (Chrome)“もしくは”Web App (Edge)“を選択

以下のような内容の”launch.json”が作成されます。
”Web App (Chrome)“を選択した場合です。

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "chrome",
      "request": "launch",
      "name": "Open index.html",
      "file": "d:\\XXXXXXXX\\ShowYUVwithWebGL\\dst\\index.html"
    }
  ]
}
  

なお”type”は”chrome”以外に”msedge”, “pwa-chrome”, “pwa-msedge”を選べます。
Google Chromeをインストールしていない場合には、“msedge”か”pwa-msedge”の2択となります。

“file”については絶対パスを指定します。

この状態で”F5”キーを押せばブラウザが開いて”index.html”が表示されます。

ブラウザがオープンすれば、VSCode上でブレークポイントを設定したり、変数の値等を確認したりできます。

実行(サーバー)

拡張機能であるLive Serverをインストールしていれば、実際のサーバーを通した確認もできます。

“エクスプローラー”パネルから”index.html”を右クリックし、“Open with Live Server”を選択すると、ブラウザに”index.html”が表示されます。

なお、この状態ではVSCode上でのデバッグはできません。
デバッグはブラウザに搭載されている”デベロッパーツール”のデバッガーを使用します。

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

なお、実際の動作はこちらで確認できます。