hisui 2025.2.0

Recording Composition Tool Hisui
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
# リージョンについて

## リージョンの概要

**リージョン**は、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` の地点」となります。