@danything/arib 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # @danything/arib
2
2
 
3
- 日本のデジタル放送 (ISDB-T / ISDB-S、ARIB 規格) の MPEG-TS を TypeScript で読むライブラリです。
3
+ 日本のデジタル放送 (ISDB-T / ISDB-S / ISDB-C、ARIB 規格) の MPEG-TS を TypeScript で読むライブラリです。
4
4
  録画サーバ [denpa](https://github.com/danything/denpa) の中で使っているものを切り出しました。
5
5
 
6
- - PSI (`psi`): パケットの切り出し、セクションの組み立て、PAT / PMT / SDT / NIT。衛星はネットワークの全部の TS の NIT と、ほかの TS の SDT もそろえて読む
6
+ - PSI (`psi`): パケットの切り出し、セクションの組み立て、PAT / PMT / SDT / NIT。衛星はネットワークの全部の TS の NIT と、ほかの TS の SDT もそろえて読む。ケーブルは SDT だけで局をそろえる
7
7
  - 映像・音声の PES と ADTS を取り出す (`pes`)
8
8
  - 番組表の EIT[p/f] と EIT[schedule] を、ジャンル・音声・拡張形式イベント・シリーズ記述子まで読む。放送の時刻 (TDT / TOT) もここ (`eit`)
9
9
  - ARIB STD-B24 の 8 単位符号 (JIS X 0213 の 1 面・2 面まで) と ARIB 外字を文字列にする (`aribtext`、`aribtext-gaiji`)
@@ -299,13 +299,16 @@ export class B24CaptionDecoder {
299
299
  return;
300
300
  const found = [];
301
301
  for (let i = 0; i < languages; i++) {
302
- if (at + 6 > data.length)
302
+ if (at + 5 > data.length)
303
303
  return;
304
304
  const tag = data[at] >> 5;
305
305
  const dmf = data[at] & 0x0f;
306
306
  at += 1;
307
+ // DC (表示モード) が 1 バイト挟まる
307
308
  if (dmf === 0b1100 || dmf === 0b1101 || dmf === 0b1110)
308
309
  at += 1;
310
+ if (at + 4 > data.length)
311
+ return;
309
312
  const code = ascii(data, at, 3);
310
313
  at += 3; // ISO 639
311
314
  const format = data[at] >> 4;
package/dist/eit.js CHANGED
@@ -159,10 +159,12 @@ function readDescriptors(event, body) {
159
159
  for (const [tag, data] of descriptors(body)) {
160
160
  switch (tag) {
161
161
  case DESC_SHORT_EVENT: {
162
+ if (data.length < 4)
163
+ break;
162
164
  const nameLength = data[3];
163
165
  const textAt = 4 + nameLength;
164
166
  event.name = decodeAribText(data.subarray(4, textAt));
165
- const textLength = data[textAt];
167
+ const textLength = data[textAt] ?? 0;
166
168
  event.description = decodeAribText(data.subarray(textAt + 1, textAt + 1 + textLength));
167
169
  break;
168
170
  }
@@ -199,6 +201,9 @@ function readDescriptors(event, body) {
199
201
  break;
200
202
  }
201
203
  case DESC_COMPONENT: {
204
+ // 短いものを読むと、種別 0 の「480i」と出る
205
+ if (data.length < 2)
206
+ break;
202
207
  const streamContent = data[0] & 0x0f;
203
208
  const componentType = data[1];
204
209
  event.video = {
@@ -221,6 +226,9 @@ function readDescriptors(event, body) {
221
226
  * [残り] text_char … **放送が付けた名前**
222
227
  */
223
228
  case DESC_AUDIO_COMPONENT: {
229
+ // 言語の符号まで無いものを読むと、言語が空の音声が 1 本増える
230
+ if (data.length < 9)
231
+ break;
224
232
  const flags = data[5];
225
233
  const multiLingual = (flags & 0x80) !== 0;
226
234
  const langs = [lang(data, 6)];
@@ -245,7 +253,7 @@ function readDescriptors(event, body) {
245
253
  case DESC_CA_CONTRACT_INFO: {
246
254
  if (data.length < 4)
247
255
  break;
248
- const caSystemId = (data[0] << 8) | data[1];
256
+ const caSystemId = u16(data, 0);
249
257
  const numOfComponent = data[2] & 0x0f;
250
258
  let at = 3 + numOfComponent;
251
259
  if (at >= data.length)
package/dist/psi.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * TS の土台 (パケットの切り出し・セクションの組み立て・PAT/PMT) と、局の一覧。
3
3
  * 局の一覧はチャンネルスキャンで「この物理チャンネルに何が入っているか」を知るために使い、
4
- * 読むのは NIT と SDT の 2 つ。
4
+ * 読むのは NIT と SDT の 2 つ。ケーブルでは、局の無い TS を見分けるのに PAT も読む (`ServiceReader`)。
5
5
  *
6
6
  * - NIT (PID 0x0010) … ネットワーク ID と、地上波ならリモコン番号。衛星はネットワークの全部の TS
7
7
  * (TSID・周波数・サービスの一覧) が載っている (`NitReader`)
@@ -48,8 +48,9 @@ export interface TransportInfo {
48
48
  }
49
49
  export interface NetworkInfo {
50
50
  networkId: number;
51
+ /** 最初の TS のリモコン番号。地上波の NIT は自分の TS 1 つだけを載せる */
51
52
  remoteControlKeyId: number | null;
52
- transportStreams: TransportInfo[];
53
+ transportStreams: NetworkTransport[];
53
54
  }
54
55
  /**
55
56
  * PSI セクションを組み立てる。
@@ -182,6 +183,8 @@ export type Delivery = SatelliteDelivery | TerrestrialDelivery;
182
183
  /** NIT の TS の 1 件。サービスの一覧に、どこで流れているかが付く */
183
184
  export interface NetworkTransport extends TransportInfo {
184
185
  delivery: Delivery | null;
186
+ /** その TS のリモコン番号 (TS 情報記述子)。載せていなければ null */
187
+ remoteControlKeyId: number | null;
185
188
  }
186
189
  /** そろった NIT。セクションをつないだもの */
187
190
  export interface Network {
@@ -259,13 +262,26 @@ export interface FoundService extends Service {
259
262
  * Mirakurun のスキャンと同じで、**両方**揃って初めてそのチャンネルを
260
263
  * 「受信できた」とみなす。SDT だけ取れても、どのネットワークのものか
261
264
  * 分からないと設定に書けない。
265
+ *
266
+ * **ケーブル (`requireNit: false`) は SDT[actual] だけで揃ったとみなす。** 1 つのネットワークが
267
+ * 地上波・BS の再送信と自主放送の TS をまとめて流していて、NIT は CATV のネットワークのもの
268
+ * (局の元の放送とは別)。どの放送の局かは SDT の original_network_id で分かる。
269
+ * NIT が先に来ていれば、その TS のリモコン番号だけを使う。
270
+ * PAT に局が 1 つも無い TS (データだけの TS) も揃ったとみなし、局は返さない (`empty`)
262
271
  */
263
272
  export declare class ServiceReader {
264
273
  private readonly nit;
265
274
  private readonly sdt;
275
+ private readonly pat;
266
276
  private readonly packets;
277
+ private readonly requireNit;
267
278
  network: NetworkInfo | null;
268
279
  transport: TransportInfo | null;
280
+ /** PAT に局が 1 つも無かった (`requireNit: false` のときだけ読む) */
281
+ empty: boolean;
282
+ constructor({ requireNit }?: {
283
+ requireNit?: boolean;
284
+ });
269
285
  get complete(): boolean;
270
286
  /** 任意の長さのバイト列を渡す。揃ったら true */
271
287
  feed(chunk: Uint8Array): boolean;
package/dist/psi.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * TS の土台 (パケットの切り出し・セクションの組み立て・PAT/PMT) と、局の一覧。
3
3
  * 局の一覧はチャンネルスキャンで「この物理チャンネルに何が入っているか」を知るために使い、
4
- * 読むのは NIT と SDT の 2 つ。
4
+ * 読むのは NIT と SDT の 2 つ。ケーブルでは、局の無い TS を見分けるのに PAT も読む (`ServiceReader`)。
5
5
  *
6
6
  * - NIT (PID 0x0010) … ネットワーク ID と、地上波ならリモコン番号。衛星はネットワークの全部の TS
7
7
  * (TSID・周波数・サービスの一覧) が載っている (`NitReader`)
@@ -481,11 +481,11 @@ function parseNitSection(section) {
481
481
  at += 6 + loop;
482
482
  const services = [];
483
483
  let delivery = null;
484
+ let remote = null;
484
485
  for (const [tag, descriptor] of descriptors(body)) {
485
- if (tag === DESC_TS_INFORMATION &&
486
- descriptor.length > 0 &&
487
- remoteControlKeyId === null) {
488
- remoteControlKeyId = descriptor[0];
486
+ if (tag === DESC_TS_INFORMATION && descriptor.length > 0) {
487
+ remote ??= descriptor[0];
488
+ remoteControlKeyId ??= remote;
489
489
  }
490
490
  else if (tag === DESC_SERVICE_LIST) {
491
491
  for (let i = 0; i + 3 <= descriptor.length; i += 3) {
@@ -508,13 +508,13 @@ function parseNitSection(section) {
508
508
  originalNetworkId,
509
509
  services,
510
510
  delivery,
511
+ remoteControlKeyId: remote,
511
512
  });
512
513
  }
513
514
  return {
514
515
  other: tableId === TABLE_NIT_OTHER,
515
516
  networkId: u16(section, 3),
516
517
  version: (section[5] >> 1) & 0x1f,
517
- current: (section[5] & 0x01) !== 0,
518
518
  sectionNumber: section[6],
519
519
  lastSectionNumber: section[7],
520
520
  remoteControlKeyId,
@@ -716,15 +716,30 @@ export class SdtReader {
716
716
  * Mirakurun のスキャンと同じで、**両方**揃って初めてそのチャンネルを
717
717
  * 「受信できた」とみなす。SDT だけ取れても、どのネットワークのものか
718
718
  * 分からないと設定に書けない。
719
+ *
720
+ * **ケーブル (`requireNit: false`) は SDT[actual] だけで揃ったとみなす。** 1 つのネットワークが
721
+ * 地上波・BS の再送信と自主放送の TS をまとめて流していて、NIT は CATV のネットワークのもの
722
+ * (局の元の放送とは別)。どの放送の局かは SDT の original_network_id で分かる。
723
+ * NIT が先に来ていれば、その TS のリモコン番号だけを使う。
724
+ * PAT に局が 1 つも無い TS (データだけの TS) も揃ったとみなし、局は返さない (`empty`)
719
725
  */
720
726
  export class ServiceReader {
721
727
  nit = new SectionAssembler(PID_NIT);
722
728
  sdt = new SectionAssembler(PID_SDT);
729
+ pat = new SectionAssembler(PID_PAT);
723
730
  packets = new PacketStream();
731
+ requireNit;
724
732
  network = null;
725
733
  transport = null;
734
+ /** PAT に局が 1 つも無かった (`requireNit: false` のときだけ読む) */
735
+ empty = false;
736
+ constructor({ requireNit = true } = {}) {
737
+ this.requireNit = requireNit;
738
+ }
726
739
  get complete() {
727
- return this.network !== null && this.transport !== null;
740
+ return this.requireNit
741
+ ? this.network !== null && this.transport !== null
742
+ : this.transport !== null || this.empty;
728
743
  }
729
744
  /** 任意の長さのバイト列を渡す。揃ったら true */
730
745
  feed(chunk) {
@@ -739,16 +754,22 @@ export class ServiceReader {
739
754
  this.transport = parseSdt(section) ?? this.transport;
740
755
  }
741
756
  }
757
+ if (!this.requireNit && !this.empty && this.transport === null) {
758
+ for (const section of this.pat.feed(packet)) {
759
+ if (section[0] === TABLE_PAT && parsePat(section).size === 0)
760
+ this.empty = true;
761
+ }
762
+ }
742
763
  }
743
764
  return this.complete;
744
765
  }
745
766
  /** 録るに値するサービスだけ。リモコン番号は NIT のものを配る */
746
767
  services() {
747
- if (this.network === null || this.transport === null)
768
+ const transport = this.transport;
769
+ if (transport === null)
748
770
  return [];
749
- const { networkId, remoteControlKeyId } = this.network;
750
- const { transportStreamId } = this.transport;
751
- return this.transport.services
771
+ const { transportStreamId, originalNetworkId } = transport;
772
+ const found = (networkId, remoteControlKeyId) => transport.services
752
773
  .filter((service) => SERVICE_TYPES.has(service.serviceType))
753
774
  .map((service) => ({
754
775
  ...service,
@@ -756,5 +777,13 @@ export class ServiceReader {
756
777
  transportStreamId,
757
778
  remoteControlKeyId,
758
779
  }));
780
+ if (this.requireNit) {
781
+ if (this.network === null)
782
+ return [];
783
+ return found(this.network.networkId, this.network.remoteControlKeyId);
784
+ }
785
+ const own = this.network?.transportStreams.find((ts) => ts.transportStreamId === transportStreamId &&
786
+ ts.originalNetworkId === originalNetworkId);
787
+ return found(originalNetworkId, own?.remoteControlKeyId ?? null);
759
788
  }
760
789
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danything/arib",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "日本のデジタル放送 (ARIB) の TS を読む: PSI・PES・EIT・ARIB の文字・字幕・データ放送",
5
5
  "license": "MIT",
6
6
  "repository": {