# リージョンについて

## リージョンの概要

**リージョン**は、Hisui の映像合成における基本的な配置単位です。
一つのリージョン内では、複数の映像ソースを格子状（グリッド）に配置して表示できます。
また複数のリージョンを活用することで、複雑な映像のレイアウトも表現することができます。

レイアウト JSON では以下のように `video_layout` を使ってリージョンを定義します。

```json
{
  "video_layout": {
    "main": {
      "video_sources": ["archive-*.json"],
      "width": 640,
      "height": 480,
      "max_columns": 3,
      "max_rows": 2
    }
  }
}
```

この例では `main` というリージョンが定義されています。

以降では、リージョンの基礎概念や詳細の仕様について記述しています。

なお JSON 内の個々の項目についての説明は [レイアウトの仕様](./layout_spec.md) が詳しいため、そちらも参照してください。

## 基本概念

### リージョン

映像合成において、特定の位置とサイズを持つ長方形の映像表示領域です。各リージョンは以下の属性を持ちます。

- **位置**: X 座標、Y 座標で指定される配置位置
  - 座標系の原点は画面の**左上角**です。
  - X 座標は右方向、Y 座標は下方向が正の値となります。
- **サイズ**: 幅と高さ（ピクセル単位）
- **重ね順**: Z 座標による前後関係の指定
  - Z 座標は、複数のリージョンが重なり合う場合の描画順序を決定します
  - 値が小さいリージョンほど奥（背景側）に、値が大きいリージョンほど手前（前景側）に描画されます。
- **映像ソース一覧**: そのリージョンに表示する映像ファイルの指定

### セル

リージョン内のグリッドを構成する個々の区画です。
各セルには一つの映像ソースが割り当てられ、映像が表示されます。
通常、セルのサイズは、リージョンのサイズとグリッドの行列数から自動的に計算されます。

グリッドの各セルには、**左上から右下に向かって行ごとに順番に** 0 始まりのインデックス番号が振られます。

例: 2 行 × 3 列のグリッドの場合

```text
┌─────┬─────┬─────┐
│  0  │  1  │  2  │
├─────┼─────┼─────┤
│  3  │  4  │  5  │
└─────┴─────┴─────┘
```

このインデックス番号は、`cells_excluded` でセルを除外する際に使用されます。

### グリッド

リージョン内でのセルの配置パターンです。
行数と列数で構成され、映像ソースの数や `max_rows`、`max_columns` の設定に基づいて自動的に決定されます。

### 映像ソース

合成の入力として使われる映像のメタデータファイル（JSON）とメディアファイルのことを指します。
レイアウト JSON の仕様の文脈では、
簡単のために「メタデータファイルのパス」のことを単に「映像ソース」と呼称することもあります。

メタデータファイルとしては、
Sora が各コネクションの録画毎に生成する `archive-*.json` を想定していますが、
Hisui はその中の、以下の情報を参照しています:

- **コネクション ID**: 配信者を識別するための ID
  - Sora の分割録画の際には、この ID が同一のメタデータファイルが複数生成されます
  - それらは自動的に連結して、同一のソースとして処理されます
- **開始時刻・終了時刻**: 映像が表示される時間範囲
  - 合成では録画開始時刻からのオフセット時間が使用されます
- **メディア形式**: メディアファイルのコンテナ形式（`webm` または `mp4`）
- **音声・映像の有無**: そのソースが音声や映像を含むかどうかの情報

レイアウト JSON で指定するのは、メタデータファイルのパスで、
それに対応するメディアファイルのパスは「メタデータファイルの拡張子を **メディア形式** で置き換えたもの」として決定されます。

以下はメタデータファイルの例です。

```json
{
  "connection_id": "M4WMN55P8QYD1BKF0MVWARVKXN",
  "format": "webm",
  "audio": true,
  "video": true,
  "start_time_offset": 0,
  "stop_time_offset": 600,
  ... 他の項目は Hisui は参照しないので省略 ...
}
```

この映像ソースは、コネクション ID `M4WMN55P8QYD1BKF0MVWARVKXN` で識別される配信者による、開始時刻 0 秒から終了時刻 600 秒（10 分間）の webm 形式の録画で、音声と映像の両方を含んでいることを示しています。

## リージョン処理の全体の流れ

各リージョン処理の概要は以下の通りです。

1. 映像ソース一覧と表示時間範囲の決定
2. グリッド構成の決定
3. 各映像ソースのセルへの割り当て
4. 各種解像度の決定
5. 合成処理

以降では、これらの手順について詳しく説明します。

## 映像ソース一覧と表示時間範囲の決定方法

リージョンで使われる映像ソース一覧の決定は、以下の手順で行われます。

### 1. `video_sources` のワイルドカード解決

リージョンの `video_sources` で指定されたパスがワイルドカードを含む場合は、まずそれが解決されます。

- ファイル名部分に `*` を含むパスは、マッチする全てのファイルに展開されます
  - 例：`archive-*.json` は `archive-connection-id1.json`, `archive-connection-id2.json` 等にマッチします
- 展開後に重複するパスが存在する場合には、重複分は自動的に除去されます
- ワイルドカードを含まないパスは、そのままのパスとして扱われます
- 対応するメディアファイルが存在しないメタデータファイルは、ワイルドカード展開の結果から除外されます
  - 「対応するメディアファイル」とは、メタデータファイルの拡張子を `.webm` または `.mp4` に変更したファイルのことを指します
  - なお、ワイルドカードではなく直接指定したメタデータファイルに対応するメディアファイルが存在しない場合はエラーとなります

### 2. `video_sources_excluded` のワイルドカード解決と除外処理

除外パターンも同様にワイルドカード展開された後、`video_sources` から除外されます。

例えば、次のような指定の場合は、
全ての `archive-*.json` ファイルから `archive-presenter.json` を除いたファイルが対象として残ります。

```json
{
  "video_sources": ["archive-*.json"],
  "video_sources_excluded": ["archive-presenter.json"]
}
```

### 3. 分割録画の考慮

`video_sources` や `video_sources_excluded` に、同じ `connection_id` を持つ複数のメタデータファイルが存在する場合には、
一つのソースに属するものとして扱われます。

例えば、以下のような分割録画ファイルがある場合

```
archive-user1-001.json (connection_id: USER1, 0-300秒)
archive-user1-002.json (connection_id: USER1, 300-600秒)
archive-user1-003.json (connection_id: USER1, 600-900秒)
```

`video_sources` で `["archive-user1-*.json"]` と指定されていたら、
これらは同じ `connection_id` を持つため、一つのソース（0-900 秒の連続した映像）として扱われます。

### 4. 映像ソースの表示開始時刻と終了時刻の決定

各ソースの表示時間範囲は、以下のように決定されます。

- 一括録画の場合: メタデータファイルの `start_time_offset` と `stop_time_offset` の値をそのまま採用
- 分割録画の場合: コネクション ID が同じ全てのメタデータファイルの中で、最も早い開始時刻と最も遅い終了時刻を採用

## グリッドの構成（行列サイズ）の決定方法

各リージョン内でのグリッドの行数と列数は、映像ソースの数と制約設定に基づいて自動的に決定されます。
決定処理は以下の手順で行われます。

### 1. リージョンで必要なセル数を算出

まず、そのリージョンの映像ソースを全て表示する場合に必要となるセルの数を、以下のように決定します。

1. セルの再利用の有無（`reuse` 設定）に応じて、全てのソースを表示するために「必要なセル数」を計算
   - 再利用なしの場合は、単純に `必要なセル数 = ソース数` となる（各ソースに専用セルを割り当て）
   - 再利用ありの場合は、次のように、各時刻で同時に表示されるソースの最大数を計算する
      - 各映像ソースの開始時刻と終了時刻を元に、各時刻で同時に存在（表示）するソース数を計算
      - 「各時刻で同時に存在するソース数」の最大値を求めて「必要なセル数」とする
3. `cells_excluded` が指定されている場合には、そこで除外されたセルの分を「必要なセル数」に加算する

- つまり `cells_excluded` で指定されたセルには「何も表示しない空ソース」が存在するものとして扱われる
- 例：4 つの同時表示ソースがあり、2 つのセルが除外されている場合、6 つのセルが必要と判断される

なお `cells_excluded` で指定されたセルのインデックスが、実際のグリッドのセル数よりも大きい場合、
そのインデックスは単に無視されます。

例えば、以下のようなレイアウトを考えてみます。

```json
{
  "video_layout": {
    "main": {
      "video_sources": ["user1.json", "user2.json", "user3.json"],
      "max_rows": 2,
      "max_columns": 2,
      "cells_excluded": [1, 5, 8]
    }
  }
}
```

ここで、全ての映像ソースの表示時刻範囲が等しいものとすると、グリッドの構成は次のようになります。

```
┌──────┬──────┐
│user1 │ 除外 │  ← インデックスが 1 のセルだけが除外されている
├──────┼──────┤
│user2 │user3 │
└──────┴──────┘
```

インデックスが `5` および `8` のセルは、グリッドのセル数である 4 を超過しているため、
このレイアウトでは単に無視されます。

つまり、除外セルの指定によってグリッドサイズが自動的に拡張されることはありません。

### 2. グリッドサイズの決定

グリッドサイズの決定は、`max_rows` および`max_columns` による制約の指定状況に応じて以下の方針で行われます。

#### 制約が全く指定されていない場合

`max_rows` と `max_columns` の両方が未指定の場合、以下の方針で行列数を決定します。

- 上で求めた「全ての映像ソースを表示するために必要なセル数」を全て配置できるグリッドにする
- できるだけ正方形に近い配置にする
- ただし、列数は行数よりも1つ多くても許容する

**例**: 5つのソースがある場合

```
理想的な配置: 2 行 × 3 列 = 6 セル (5 つのソースを収容可能)

┌─────┬─────┬─────┐
│Src1 │Src2 │Src3 │
├─────┼─────┼─────┤
│Src4 │Src5 │Empty│
└─────┴─────┴─────┘
```

#### 制約が一方のみ指定されている場合

制約が一方のみ指定されている場合、指定された制約値を基準として処理されます。

**`max_columns` のみが指定されている場合**

- 列数を必要なソース数と `max_columns` の小さい方に設定
- 行数を必要なセル数に基づいて計算（`必要なセル数 ÷ 列数` を切り上げ）

**例**: 5つのソースで `max_columns = 4` が指定されている場合

```
グリッド: 2 行 × 4 列 = 8 セル
列数: min(5, 4) = 4
行数: ceil(5 / 4) = 2

┌─────┬─────┬─────┬─────┐
│Src1 │Src2 │Src3 │Src4 │
├─────┼─────┼─────┼─────┤
│Src5 │Empty│Empty│Empty│
└─────┴─────┴─────┴─────┘
```

**`max_rows` のみが指定されている場合**

- 行数を必要なソース数と `max_rows` の小さい方に設定  
- 列数を必要なセル数に基づいて計算（`必要なセル数 ÷ 行数` を切り上げ）

**例**: 5つのソースで `max_rows = 4` が指定されている場合

```
グリッド: 4 行 × 2 列 = 8 セル
行数: min(5, 4) = 4
列数: ceil(5 / 4) = 2

┌─────┬─────┐
│Src1 │Src2 │
├─────┼─────┤
│Src3 │Src4 │
├─────┼─────┤
│Src5 │Empty│
├─────┼─────┤
│Empty│Empty│
└─────┴─────┘
```

#### 両方の制約が指定されている場合

`max_rows` と `max_columns` の両方が指定されている場合は、より小さい制約値を持つ軸を優先して処理されます。

- `max_columns <= max_rows` の場合
  - 「`max_columns` のみが指定されている場合」と同じ方法でグリッドサイズを決定する
  - ただし、行のサイズは最大でも `max_rows` とする

- `max_columns > max_rows` の場合
  - 「`max_rows` のみが指定されている場合」と同じ方法でグリッドサイズを決定する
  - ただし、列のサイズは最大でも `max_columns` とする

## 各映像ソースのセルへの割り当て

グリッドの構成が決定された後は、各映像ソースの具体的なセルへの割り当てが行われます。
ここでは、映像ソースの表示時間範囲とセルの再利用設定（`reuse`）を考慮して、ソースを配置するセルが決定されます。

なお、ソースとセルの対応は固定で、途中で変更されることはありません。

### 基本的な割り当て方針

映像ソースのセルへの割り当ては、以下の手順で行われます。

1. セルの初期状態を設定
   - 利用可能なセルは「空き」状態に、除外されたセルは「除外」状態に設定
2. 映像ソースを開始時刻が早い順に処理する
   - 新規映像ソースは、その時点で一番インデックスが小さい「空き」状態のセルに割り当てる
   - 終了時刻に達したソースは、セルへの割り当てを解除して、セルを「空き」状態に戻す
     - ただし `reuse` の値が `none` の場合には「空き」には戻さず、そのセルが再利用されないようにする
3. 「空き」状態のセルがない時には `reuse` 設定に従って新規映像ソースの扱いを決定する

### 十分な空きセルがある場合の動作例

以下は 4 つのセル（2×2）に 4 つの映像ソースがある場合の基本的な動作例です。

```
時刻:     0----5----10---15---20
ソース A:      [==== A ==]
ソース B:       [=== B ===]
ソース C: [== C ==]
ソース D:           [== D ==]

[セルの割り当て状態変化]
時刻 0:
┌──────┬──────┐
│  C   │      │
├──────┼──────┤
│      │      │
└──────┴──────┘

時刻 5:
┌──────┬──────┐
│  C   │  A   │
├──────┼──────┤
│      │      │
└──────┴──────┘

時刻 6:
┌──────┬──────┐
│  C   │  A   │
├──────┼──────┤
│  B   │      │
└──────┴──────┘

時刻 8:
┌──────┬──────┐
│      │  A   │  ← ソース C が終了してセル 0 が空きに
├──────┼──────┤
│  B   │      │
└──────┴──────┘

時刻 10:
┌──────┬──────┐
│  D   │  A   │  ← ソース D が開始して最初の空きセル(0) に割り当て
├──────┼──────┤
│  B   │      │
└──────┴──────┘

時刻 15:
┌──────┬──────┐
│  D   │      │  ← ソース A が終了してセル 1 が空きに
├──────┼──────┤
│  B   │      │
└──────┴──────┘

時刻 20:
┌──────┬──────┐
│      │      │  ← ソース B と D が終了して全セルが空きに
├──────┼──────┤
│      │      │
└──────┴──────┘
```

この例では、セルが十分にあるため、どのソースも競合することなく表示されています。

### `reuse` 設定による動作の違い

`max_rows` および `max_columns` の両方が指定されている場合には、
全ての映像ソースを表示するために必要なセル数が足りなくなる可能性があります。

その場合には `reuse` の値に従って、割り当ての挙動が決まります。

`reuse` で指定可能な値は以下の通りです。

- `none`: セルを再利用せず、空きセルがない時に開始された映像ソースは表示対象から除外される
- `show_oldest`: セルを再利用し、競合時は開始時刻が早いソースを優先
- `show_newest`: セルを再利用し、競合時は開始時刻が遅いソースを優先

デフォルトの挙動は `show_oldest` です。

以降では、それぞれの挙動の詳細を、次のようなレイアウトとソースの例を使って説明します。

**レイアウト JSON**

```json
{
  "video_layout": {
    "main": {
      "video_sources": ["*.json"],
      "max_rows": 1,
      "max_columns": 2,
      "reuse": ...以降ではこの値を変更する...
    }
  }
}
```

**映像ソース**

```
時刻:     0----5----10---15---20
ソースA:    [=== A ===]
ソースB:       [=== B ===]
ソースC:  [======== C ========]
```

この例では、利用可能なセルの数が 2 なのに対して、最大で同時に 3 つのソースを表示する必要があるため、
`reuse` の値に従って、競合の処理が行われます。

#### `reuse: "none"` の場合

`reuse: "none"` を指定すると、セルを再利用しません。
未割り当てのセルがない時に開始された映像ソースは、セルへの割り当てが行われません。

**動作例**

```
[映像ソースのタイムライン（再掲）]
時刻:     0----5----10---15---20
ソースA:    [=== A ===]
ソースB:       [=== B ===]
ソースC:  [======== C ========]

[セルの状態変化]
時刻 0:
┌──────┬──────┐
│  C   │      │  ← ソース C が開始
└──────┴──────┘

時刻 2:
┌──────┬──────┐
│  C   │  A   │  ← ソース A が開始
└──────┴──────┘

時刻 5:
┌──────┬──────┐
│  C   │  A   │  ← ソース B が開始するが未割り当てセルがないため割り当てられない
└──────┴──────┘

時刻 12:
┌──────┬──────┐
│  C   │      │  ← ソース A が終了してセル 1 が空きに
└──────┴──────┘

時刻 20:
┌──────┬──────┐
│      │      │  ← ソース C が終了してセル 0 が空きに
└──────┴──────┘
```

この場合、ソース B は合成結果の映像にいっさい含まれなくなります。

また `none` の場合には、空きセルの再利用が全く行われないので、
仮の B の開始時刻が（A の終了時刻である）12 秒より後であったとしても、
合成結果に含まれることはありません。

#### `reuse: "show_oldest"`（デフォルト）の場合

`reuse: "show_oldest"` を指定すると、セルの再利用が有効になり、
表示時刻が重なる複数のソースが、1 つのセルに割り当てられるようになります。

新規ソースを開始する際に、空き状態のセルがない場合には
「割り当てられている映像ソースの終了時刻が一番早いセル」に割り当てられます。

同じセルに割り当たった映像ソース同士では **開始時刻が早い映像ソース（古いソース）** が優先的に表示されます。

**動作例**

```
[映像ソースのタイムライン（再掲）]
時刻:     0----5----10---15---20
ソースA:    [=== A ===]
ソースB:       [=== B ===]
ソースC:  [======== C ========]

[セルの状態変化]
時刻 0:
┌──────┬──────┐
│  C   │      │  ← ソース C が開始
└──────┴──────┘

時刻 2:
┌──────┬──────┐
│  C   │  A   │  ← ソース A が開始
└──────┴──────┘

時刻 5:
┌──────┬──────┐
│  C   │  A   │  ← ソース B が開始し、セル 1 に割り当てられるが、A の方が早く開始したため A が優先
└──────┴──────┘

時刻 12:
┌──────┬──────┐
│  C   │  B   │  ← ソース A が終了したので、同じセルに割り当たっていた B を表示
└──────┴──────┘

時刻 15:
┌──────┬──────┐
│  C   │      │  ← ソース B が終了
└──────┴──────┘

時刻 20:
┌──────┬──────┐
│      │      │  ← ソース C が終了
└──────┴──────┘
```

この例ではセル 1 が再利用され、ソース A と B の両方が割り当てられていますが、
A の方が開始時刻が早いため、A の表示期間中は A が優先して表示され続けます。

#### `reuse: "show_newest"` の場合

`reuse: "show_newest"` を指定すると、セルの再利用が有効になり、
`"show_oldest"` と同様に、表示時刻が重なる複数のソースが 1 つのセルに割り当てられるようになります。

新規ソースを開始する際に、空き状態のセルがない場合には
「割り当てられている映像ソースの終了時刻が一番早いセル」に割り当てられます。

同じセルに割り当たった映像ソース同士では **開始時刻が遅い映像ソース（新しいソース）** が優先的に表示されます。

**動作例**

```
[映像ソースのタイムライン（再掲）]
時刻:     0----5----10---15---20
ソースA:    [=== A ===]
ソースB:       [=== B ===]
ソースC:  [======== C ========]

[セルの状態変化]
時刻 0:
┌──────┬──────┐
│  C   │      │  ← ソース C が開始
└──────┴──────┘

時刻 2:
┌──────┬──────┐
│  C   │  A   │  ← ソース A が開始
└──────┴──────┘

時刻 5:
┌──────┬──────┐
│  C   │  B   │  ← ソース B が開始し、A と同じセル 1 に割り当て。B の方が後に開始したため B が優先
└──────┴──────┘

時刻 12:
┌──────┬──────┐
│  C   │  B   │  ← ソース A が終了（表示内容に変化はなし）
└──────┴──────┘

時刻 15:
┌──────┬──────┐
│  C   │      │  ← ソース B が終了
└──────┴──────┘

時刻 20:
┌──────┬──────┐
│      │      │  ← ソース C が終了
└──────┴──────┘
```

この例ではセル 1 が再利用され、ソース A と B の両方が割り当てられていますが、
B の方が開始時刻が遅いため、競合期間中は B が優先して表示されます。

## 解像度の決定方法

Hisui の映像合成において、最終的な出力映像の解像度は複数の段階を経て決定されます。
解像度は「全体解像度」、「リージョン解像度」、「セル解像度」の 3 つのレベルで考える必要があります。

なお各解像度は、指定値や計算結果が奇数だったとしても、最終的には偶数に丸められて処理されます。

### 全体解像度の決定方法

全体解像度は、合成後の映像全体のサイズを決定します。
以下の優先順位で決定されます。

#### 1. `resolution` フィールドによる明示的指定

レイアウト JSON で `resolution` フィールドが指定されている場合、それが全体解像度として使用されます。

```json
{
  "resolution": "1920x1080",
  "video_layout": {
    "main": {
      "video_sources": ["archive-*.json"]
    }
  }
}
```

#### 2. リージョンの配置からの自動計算

`resolution` が未指定の場合、各リージョンの位置とサイズから必要な解像度が自動計算されます。
具体的には、全てのリージョンが収まる最小の解像度が選択されます。

```json
{
  "video_layout": {
    "main": {
      "video_sources": ["archive-main.json"],
      "width": 640,
      "height": 480, // <- 下端: 50 + 480 = 530
      "x_pos": 100,
      "y_pos": 50
    },
    "sub": {
      "video_sources": ["archive-sub.json"],
      "width": 200,  // <- 右端: 800 + 200 = 1000
      "height": 150,
      "x_pos": 800,
      "y_pos": 300
    }
  }
}
```

上記の例では、`sub` リージョンの右端が `800 + 200 = 1000` ピクセル、下端が `50 + 480 = 530` ピクセルとなるため、全体解像度は `1000×530` ピクセルになります。

### リージョン解像度の決定方法

各リージョンのサイズは、以下の優先順位で決定されます。

#### 1. `width` / `height` による明示的指定

リージョンの `width` および `height` が指定されている場合、それがそのまま使用されます。

```json
{
  "video_layout": {
    "main": {
      "video_sources": ["archive-*.json"],
      "width": 640,
      "height": 480
    }
  }
}
```

#### 2. `cell_width` / `cell_height` からの計算

セルサイズが指定されている場合、リージョンサイズはグリッドの構成から逆算されます。

```json
{
  "video_layout": {
    "main": {
      "video_sources": ["archive-0.json", "archive-1.json", "archive-2.json"],
      "cell_width": 320,
      "cell_height": 240,
      "max_columns": 2,
      "max_rows": 2,
      "border_pixels": 2
    }
  }
}
```

上のレイアウトの場合、セルのみを考慮したリージョンのサイズは `(320*2)x(240*2) = 640x480` となります。

なお、実際のリージョンサイズにはセルの周りに挿入される枠線の分が加算されるため、最終的なサイズは上の値よりも大きくなります。
この例では、内側の枠線（2 ピクセル × 1 本ずつ）と外側の枠線（2 ピクセル × 2 辺ずつ）が加算され、
最終的なリージョンサイズは `646x484` ピクセルとなります。

枠線の扱いについては、セル解像度の決定方法のセクションで詳しく説明しています。

**注意**

- `width` と `cell_width` を同時に指定することはできません（両方指定するとエラーになります）
- 同様に、`height` と `cell_height` も同時指定できません

#### 3. 全体解像度からの自動計算

リージョンサイズやセルサイズが未指定の場合、全体解像度とリージョンの位置から自動計算されます：

```json
{
  "resolution": "1920x1080",
  "video_layout": {
    "main": {
      "video_sources": ["archive-*.json"],
      "x_pos": 100,
      "y_pos": 50
      // width, height ないし cell_width, cell_height は未指定
    }
  }
}
```

上記の例では、リージョンサイズは以下のようになります

- 幅: `1920 - 100 = 1820` ピクセル
- 高さ: `1080 - 50 = 1030` ピクセル

### セル解像度の決定方法

各映像ソースが実際に表示されるセルのサイズは、基本的には次のようにして計算されます

- セルの幅: `リージョンの幅 / グリッドの列数`
- セルの高さ: `リージョンの高さ / グリッドの行数`

ただし、セルの周りには枠線が挿入されることがあるため、
各セルが実際に使用できる領域は、グリッドサイズから、その枠線分を差し引いたものとなります。

枠線には、以下の 2 種類があります

- **内側の枠線**:
  - セル間に挿入される枠線で、幅は `border_pixels` で指定された値となります（デフォルトは 2 ピクセル）
- **外側の枠線**:
  - リージョンの外周に挿入される枠線で、可能な場合は `border_pixels` で指定された値、そうでなければ残りの領域に合わせて調整されます
  - ただし、リージョンが全体解像度と同じサイズの場合、外側の枠線は挿入されません

`border_pixels` はリージョンごとに設定可能で、レイアウト JSON の `video_layout.$REGION_NAME.border_pixels` で指定します。
省略された場合のデフォルト値は 2 ピクセルです。

たとえば、以下のようなレイアウトを考えてみます。

```json
{
  "resolution": "1920x1080",
  "video_layout": {
    "main": {
      "video_sources": ["archive-0.json", "archive-1.json", "archive-2.json", "archive-3.json"],
      "width": 640,
      "height": 480,
      "max_columns": 2,
      "max_rows": 2,
      "border_pixels": 4
    }
  }
}
```

この場合、以下のように計算されます。

1. **グリッド構成**: 2 行 × 2 列 = 4 セル
2. **外側の枠線**: リージョンサイズ (640×480) が全体解像度 (1920×1080) より小さいため、上下左右に 4 ピクセルずつ
3. **内側の枠線**: セル間に 4 ピクセルずつ（水平方向に 1 本、垂直方向に 1 本）
4. **セルサイズの計算**:
   - セル幅: `(640 - 8 - 4) / 2 = 314` ピクセル
   - セル高: `(480 - 8 - 4) / 2 = 234` ピクセル

**枠線を無効化する場合**:

枠線を完全に無効化したい場合は、`border_pixels` に `0` を指定します

```json
{
  "video_layout": {
    "main": {
      "video_sources": ["archive-*.json"],
      "border_pixels": 0,
      "max_columns": 2,
      "max_rows": 2
    }
  }
}
```

この場合、セル間や外周に枠線は挿入されず、リージョン全体がセルで埋め尽くされます。

## 映像の合成処理

ここまでで各リージョンの構成や映像ソースのセルへの割り当てが決定しました。
本セクションでは、それらの情報を使って、どのように合成処理が行われるのかを概説します。

### リージョンの描画

各リージョンは、その位置（`x_pos`および`y_pos`）と解像度に対応する領域に描画されます。

複数のリージョンで重なる領域がある場合には `z_pos` の値によって、以下のように描画順序が決定されます。

- `z_pos` の値が大きいリージョンが前面に描画される
- `z_pos` の値が同じリージョン同士の描画順序は未定義

### 映像ソースのセルへの描画

各セルに割り当てられた映像ソースは、以下の手順で描画されます：

#### 開始時刻と終了時刻による描画制御

映像ソースは、メタデータファイルで指定された `start_time_offset` から `stop_time_offset` までの期間のみ描画されます。

- **開始時刻前**: ソースの開始時刻に達するまで、そのソースは描画されません（セルは黒塗りのまま）
- **表示期間中**: ソースの開始時刻から終了時刻まで、映像が描画されます
- **終了時刻後**: ソースの終了時刻を過ぎると、そのソースは描画されなくなります（セルは黒塗りに戻る）

#### 同じセルに割り当てられた場合の優先順序

複数の映像ソースが同じセルに割り当てられ、表示時刻が重複する場合は、`reuse` 設定に従って優先順序が決定されます。

- **`reuse: "show_oldest"`**: 優先度の値が小さい（開始時刻が早い）ソースが描画される
- **`reuse: "show_newest"`**: 優先度の値が大きい（開始時刻が遅い）ソースが描画される

#### ソースのリサイズ（アスペクト比の維持）

各映像ソースは、セルサイズに合わせて以下の方針でリサイズされます：

1. **アスペクト比の維持**: 元の映像のアスペクト比は常に維持されます
2. **セル内への収納**: リサイズ後の映像がセルの幅・高さを超えないようにスケーリングします
3. **スケーリング倍率の決定**: 幅基準の倍率と高さ基準の倍率のうち、小さい方を採用します
4. **中央配置**: リサイズ後の映像は、セルの中央に配置されます
5. **余白の処理**: 映像がセル全体を覆わない部分は黒塗りされます

**例**: セルサイズが 320×240 ピクセル、元映像が 640×360 ピクセルの場合

- 幅基準の倍率: 320/640 = 0.5
- 高さ基準の倍率: 240/360 ≈ 0.67
- 採用される倍率: 0.5（小さい方）
- リサイズ後のサイズ: 320×180 ピクセル
- 配置: セルの中央に配置され、上下に 30 ピクセルずつの黒帯が入る

#### 描画対象のソースが存在しないセルの扱い（黒塗り）

以下の場合、セルは黒塗りで表示されます：

- **映像ソースが割り当てられていないセル**: そのセルに対応する映像ソースが存在しない
- **除外セル**: `cells_excluded` で指定されたセル
- **表示時刻外**: 割り当てられたソースの表示時刻範囲外の期間

### `trim` の扱い

トリムは、配信者が存在しない期間を合成結果から自動的に除去する機能です。

レイアウト JSON で `trim: true` を指定することで有効になります（省略時は有効扱い）

```json
{
  "trim": true,
  "audio_sources": ["archive-*.json"],
  "video_layout": {
    "main": {
      "video_sources": ["archive-*.json"]
    }
  }
}
```

**`trim: true` 指定時の動作**

- ソースが全く存在しない時間帯が、合成結果に含まれなくなります
  - 合成結果ファイルの尺はその分だけ短くなります
- ソースの内容とは関係なく、**ソースが該当期間に存在するかどうか**のみで判断されます
- 映像ソースだけでなく音声ソースの存在も考慮されます

**例**:

以下のような映像ソースの場合には「10-15 秒の期間」が合成結果から除去され、
実際の出力時間は 20 秒（25 秒 - 5 秒）となります。

```
時刻:      0----5----10---15---20---25
ソース A:  [=== A ===]
ソース B:                  [=== B ===]
```

**`trim: false` 指定時の動作**

上記のトリム処理が原則としてなくなりますが、
例外として「冒頭部分で配信者が存在しない期間のトリム」は常に行われます。

言いかえると、合成結果の時刻の起点は 0 秒地点ではなく、「最初のソースの `start_time_offset` の地点」となります。
