Bellagio OpenMAX IL で OpenMAX IL

# 本記事について

- OpenMAX IL についてざっくり説明します
- オープンソースである Bellagio OpenMAX IL を利用し、OpenMAX IL のコンポーネントの一覧取得まで（だけ）コードを書いてみます
- Ubuntu 22.04 x64 上で `docker run --rm -it ubuntu:bionic` した環境で動作確認をしています

# OpenMAX

## OpenMAX とは

https://www.khronos.org/openmax/

- 動画・音声などのマルチメディアを操作するための API
  - インターフェースのみで、実装は提供しない。C言語で言えば、ヘッダファイルのみが提供される。
- プラットフォーム非依存・アプリケーション非依存
  - 動画再生は非常に大きなファイルが高速に処理される必要があり、ハードウェア・プラットフォームの支援が必要不可欠
    - プラットフォームに強く依存したソフトウェアは移植が困難になる。OpenMAX は、処理速度も移植性も犠牲にしないインターフェースを提供する
- Khronos が策定
  - Khronos が策定している API には OpenMAX の他、 OpenGL、Vulkan などがある
- ロイヤリティフリーである
  - ロイヤリティフリーでないインターフェースには、例えば HDMI がある
- 3 つの API が定義されている
  - DL (Develop Layer)
    - メディア処理によく使われる機能の API
    - 例えば高速フーリエ変換・ハフマン符号の展開など
  - IL (Integration Layer)
    - 主にメディアコーデックなどを制御する API
    - 例えば圧縮データのデコードなど
  - AL (Application Layer)
    - 主にメディアプレーヤーなどを制御する API
    - 例えば再生開始・終了の指示など
- 活用例として、Android の内部実装に OpenMAX IL のインターフェースが利用されている[^omx-omx-1]
  - https://source.android.com/docs/core/media?hl=ja

[^omx-omx-1]: 実際の実装はこの辺り https://android.googlesource.com/platform/frameworks/av/+/a8a1b31a1d53ce0e2e854ceb3a154c1991495dc4/media/libstagefright/ACodec.cpp

## OpenMAX IL とは

https://registry.khronos.org/OpenMAX-IL/

OpenMAX IL 1.1.2 Specification clause 1.2

- Component という単位で機能が分割され、機能追加が容易
  - ある Component はデコードを、ある Component はエンコードを、別の Component はカメラ画像を取得…など
- API を利用して Componenet を操作するコードは IL Client と呼ばれる
- Component はComponent 同士を直接つなぐことも可能
  - IL Client がデータを操作する必要がなくなる
    - Component の処理は GPU のメモリ空間で行われる場合が多いため、GPU のメモリ空間で別 Component に直接データを渡すことで、GPU のメモリ空間と CPU のメモリ空間を行き来するコストを削減できる
    - ハードウェアと連携しやすい API 設計になっている
  - その挙動が地中（＝GPU空間）に潜っているように見えることから[^omx-il-1]、トンネルと表現されることがある（multimedia tunneling、communication tunnel）

OpenMAX IL の API について。API は core API と component API の 2 種類がある。

- core API は Component をロードしたり破棄したりする API
- component API は Component に対して操作する API
  - Component の状態を操作する
    - LOADED, IDLE, EXECUTING の 3 つの状態が主軸
    - 状態を変えるには、次の状態を StateSet した後、次の状態の条件を満たす操作をすることで状態が変更される
      - LOAD -> IDLE の場合、StateSet(IDLE) した後、バッファを必要数確保すると、IDLE に遷移する
      - OpenMAX IL 1.1.2 Specification 3.4 Calling Sequences にシーケンス図と共に例が記載されている
  - Component に対するデータの送信（EmptyThisBuffer関数）や受信（FillThisBuffer関数）
    - Empty は、用意したバッファを Component が消費するというイメージ[^omx-il-2]
  - その他オプション設定など

[^omx-il-1]: たぶん
[^omx-il-2]: だと思う

## Bellagio OpenMAX IL とは

https://omxil.sourceforge.net/

- OpenMAX IL インターフェースを実装した OpenMAX IL Core
- Linux で動作する
- オープンソースで、Ubuntu や Debian であれば apt から入手できる
  - 利用可能なコンポーネントは多くない。主にカメラ等の入出力デバイスと、vorbis等の数個のデコーダのみ
  - ソースからビルド[^omx-bomx-1]すれば ffmpeg 実装のビデオデコーダが手に入る。ただし `With some rework to do (bad)` の扱いになっている

[^omx-bomx-1]: 2010 年より前のコードのようです。Ubuntu 10.04 より新しい環境では動作保証されていない。

# 実装

- Bellagio OpenMAX IL を利用して、 OpenMAX IL のコードを書いてみます
- OpenMAX IL のインターフェースは C 言語ですが、C++17 から取り扱うことにします
- 以降記載するコードは、ソースコード量を短縮させるため、エラーを雑に扱っています

## 環境構築

- `libomxil-bellagio-dev` をインストールして Bellagio OpenMAX IL のライブラリと OpenMAX IL のヘッダを取得
- g++ ビルド環境に `build-essential` と `pkg-config` をインストール

```bash
apt update
apt install build-essential pkg-config libomxil-bellagio-dev
```

## core を初期化するだけ

コードは以下の通り。

```cpp
#include <iostream>
#include <OMX_Core.h>

int main() {
  OMX_ERRORTYPE res = OMX_Init();
  if (res != OMX_ErrorNone) {
    std::cerr << "OMX_Init res=" << std::endl;
    abort();
  }
  std::cout << "OMX_Init: ok" << std::endl;

  OMX_ERRORTYPE res = OMX_Deinit();
  if (res != OMX_ErrorNone) {
    std::cerr << "OMX_Deinit res=" << std::endl;
    abort();
  }
  std::cout << "OMX_Deinit: ok" << std::endl;
  return 0;
}
```

gcc を使って上記コードをビルドします。ライブラリの依存の引数は `pkg-config` に任せておきます。

```bash
g++ -std=c++17 -o app1 $(pkg-config --cflags libomxil-bellagio) app1.cpp  $(pkg-config --libs libomxil-bellagio)
```

```
OMX_Init: ok
OMX_Deinit: ok
```

## Component の一覧取得

Component の一覧と、Component ごとの role を取得してみます。
1 つの Component が複数の機能を持っていることがあり、role で区別されます。
よくあるのは、DRM (Digital Rights Management) に対応したものとそうでないものが用意されているケースで、`OMX.Xxx.AVC.Decoder` と `OMX.Xxx.AVC.Decoder.secure` が取得されがちです。

以下にコードを示します。role 名を格納する型と Component 名を格納する型が異なる点を除いては、特に変わっている点はないです。

```cpp
#include <array>
#include <iostream>
#include <vector>
#include <OMX_Component.h>
#include <OMX_Core.h>

#define CHECK_OMX(expr)                                                        \
  {                                                                            \
    OMX_ERRORTYPE __res;                                                       \
    if ((__res = (expr)) != OMX_ErrorNone) {                                   \
      std::cerr << "LINE" << __LINE__ << ": failed `" #expr "` errortype="     \
                << static_cast<int>(__res) << std::endl;                       \
      abort();                                                                 \
    }                                                                          \
  }

std::string
convertRoleToString(const std::array<OMX_U8, OMX_MAX_STRINGNAME_SIZE> &role) {
  return std::string(reinterpret_cast<const char *>(role.data()));
}

std::vector<std::array<OMX_U8, OMX_MAX_STRINGNAME_SIZE>>
getRolesOfComponent(const std::string &component_name) {
  constexpr OMX_U32 kMaxNumRoles = 4;
  // TODO: USE OMX_UUIDTYPE
  std::vector<std::array<OMX_U8, OMX_MAX_STRINGNAME_SIZE>> role_names_buf(
      kMaxNumRoles);
  std::vector<OMX_U8 *> role_names_buf_ptr(kMaxNumRoles);
  OMX_U32 num_roles = kMaxNumRoles;

  for (size_t i = 0; i < (size_t)num_roles; ++i)
    role_names_buf_ptr[i] = role_names_buf[i].data();

  CHECK_OMX(OMX_GetRolesOfComponent(const_cast<char *>(component_name.data()),
                                    &num_roles, role_names_buf_ptr.data()));

  role_names_buf.resize(num_roles);
  return role_names_buf;
}

std::vector<std::string> getAllComponentNames() {
  std::vector<std::string> component_names;
  for (OMX_U32 index = 0;; ++index) {
    char component_name[OMX_MAX_STRINGNAME_SIZE] = {};
    OMX_ERRORTYPE result = OMX_ComponentNameEnum(
        component_name, OMX_MAX_STRINGNAME_SIZE - 1, index);
    if (result == OMX_ErrorNoMore)
      break;
    CHECK_OMX(result);
    component_names.emplace_back(component_name);
  }
  return component_names;
}

int main() {
  CHECK_OMX(OMX_Init());

  for (const auto &component_name : getAllComponentNames()) {
    std::cout << "component name: " << component_name << "\n";
    for (const auto &role_name : getRolesOfComponent(component_name)) {
      std::cout << "- role: " << convertRoleToString(role_name) << "\n";
    }
    std::cout << std::endl;
  }

  CHECK_OMX(OMX_Deinit());
  return 0;
}
```

実行してみると、Component が 1 つも取得できていないと思います。
`libomxil-bellagio-dev` には Component は入っておらず、別のパッケージに分かれているので、別途インストールします。改めて実行すると、インストールしたものが表示されるはず。

```bash
$ g++ -std=c++17 -o app2 $(pkg-config --cflags libomxil-bellagio) app2.cpp  $(pkg-config --libs libomxil-bellagio)
$ ./app2
$ apt search libomxil-bellagio0-components
Sorting... Done
Full Text Search... Done
libomxil-bellagio0-components-alsa/bionic 0.1-2 amd64
  ALSA source/sink components for Bellagio OpenMAX IL

libomxil-bellagio0-components-base/bionic 0.9.3-4 amd64
  components for Bellagio OpenMAX IL
...

$ apt install libomxil-bellagio0-components-fbdevsink
...
$ ./app2 
component name: OMX.st.fbdev.fbdev_sink
- role: fbdev.fbdev_sink

component name: OMX.st.fbdev.fbdev_sink
- role: fbdev.fbdev_sink

```

Component の作成に進みたいところですが、記事が長くなってしまうのでここまで。

# おわり

株式会社ACCESS Advent Calendar 2022 2日目の記事でした。
業務で OpenMAX IL を触れたので、最も手に取りやすい Bellagio を題材にして OpenMAX IL の記事を書きました。

OpenMAX IL に興味を持って触ってみようと思った方は、まずは先に Android の MediaCodec に触れてみることをオススメします。インターフェースはよく似ているし、インターネットにより多くの情報があります。