外から使う API
画面の外のもの (テレビのアプリ・Home Assistant の連携・スクリプト) 向けの API です。 JSON の形式は保ちます。 足すのが原則で、やむを得ず外したものはその節に書きます。
- 認証は画面と同じです (下記)。
TRUSTED_NETWORKSに入っている相手はそのまま通ります (家の LAN の Home Assistant など)。それ以外はログインか、アクセストークンが要ります - 書き込み (POST・DELETE) には
Content-Type: application/jsonを付けます (本文が無い DELETE でも)。 付けないと、SvelteKit の CSRF の守りが「よそのサイトからのフォーム送信」とみなして 403 を返します (アプリは Origin を付けないため) - URL は denpa の根からの相対で返します (
api/services/…)。denpa を開いている URL に足して使います。 前段の接頭辞 (/denpa/など) の下で動かしていても、そのまま解決できます
局の一覧 GET /api/services
Section titled “局の一覧 GET /api/services”テレビと同じ並び (地上波 → BS → CS、リモコン番号順)。
[{ "id": 3227310008, "type": "GR", "name": "TOKYO MX", "remoteControlKey": 9, "logo": "api/services/3227310008/logo", "live": "api/services/3227310008/live", "now": { "id": 32736103210001, "title": "ニュース", "startAt": 1790000000000, "endAt": 1790001800000, "reserved": false, "recording": false, "audios": [{ "id": "0:main", "stream": 0, "side": "main", "label": "主音声 (日本語)", "main": true }, { "id": "0:sub", "stream": 0, "side": "sub", "label": "副音声 (英語)", "main": true }, { "id": "0:both", "stream": 0, "side": "both", "label": "主+副", "main": true }] } }]logoは局ロゴをまだ取得できていなければnullnowはいま放送中の番組 (時刻は epoch ms)。番組表に無ければnullnow.audiosはその番組で選べる音声 (下記)。ライブの?audio=<id>に渡しますnow.idは番組ID (GET /api/programs/<id>で中身を引けます)now.reservedはその番組を録る予定か (予約が予約済みか録画中。競合で弾かれたものはfalse)、now.recordingは いま録っている最中か (録画中ならreservedもtrue)。番組表のマスの印と同じ物差しです。 録画ボタンの印に使い、押すのはPOST /api/services/<id>/record
音声の一覧 audios
Section titled “音声の一覧 audios”局の now.audios と録画の audios は同じ形式で、画面の音声の切り替えに出るものと同じ一覧です。
番組表が何も言っていなくても、{ "id": "0:both", "stream": 0, "side": "both", "label": "音声" } の1つは必ず入ります。
id… 選ぶときに渡す値 (<stream>:<side>)stream… 何本目の音声か (0 から)。複数の音声 (解説放送など) はここが分かれるside…bothはそのまま、main/subはデュアルモノ (二カ国語。1本の左右に別の音声) の 左だけ・右だけを両耳に配ったもの。デュアルモノは1本からmainsubboth(主+副) の3つが出ますlabel… 画面と同じ名前 (「主音声 (日本語)」「解説ステレオ」)main… 放送が主音声と言っているか。言っていなければキーごと無い。何も選ばなければ、これ (無ければ先頭) をエンコードします
ライブ GET /api/services/<id>/live
Section titled “ライブ GET /api/services/<id>/live”流し続けます。閉じれば止まり、誰も見ていなければチューナーを返します (画面のライブと同じ取り合い。 同じ局・同じ形式を誰かが見ていればそのストリームを共有します)。知らない局は 404。
codec |
中身 | Content-Type | 向いている相手 |
|---|---|---|---|
(なし) / h264 |
H.264 / AAC の fragmented MP4 | video/mp4 |
ブラウザ・テレビのアプリ (いちばん広く再生できる) |
av1 |
AV1 / Opus の fragmented MP4 | video/mp4 |
AV1 をデコードできる新しい端末 |
raw |
エンコードせずに1局に絞っただけの TS (MPEG-2) | video/mp2t |
テレビのアプリ (いちばん軽い。MPEG-2 をハードでデコードできる端末) |
?audio=only で音声だけ (AAC の fragmented MP4、audio/mp4)。音声だけを聴くとき向け。
映像をエンコードしないので軽い。録画の audio と同じ書き方です。codec と一緒に渡すと、audio=only が勝ちます。
?audio=<id> (now.audios の id) でエンコードする音声を選びます。デュアルモノの main / sub は片側を両耳に配ってエンコードします。
知らない・形式の違う id は断らずに主音声でエンコードします (番組が替わって消えた音声など)。選び直すにはリクエストし直します
(同じ局・同じ形式・同じ音声を見ている人とだけストリームを共有する)。raw では効きません (全部の音声が入っているので、受け側が選ぶ)。
録画にチューナーを譲ると閉じます (ライブは録画にだけ譲る。チューナーを取り上げられると、ほかのチューナーが空いていても閉じることがある)。
閉じたら同じ局をリクエストし直してください。入れたらそのまま続きが観られます。入れなかったときは、その少し前に
通知 (GET /api/events) の yielded でどの録画に譲ったかが来ているので、その録画の追っかけ
(GET /api/recordings/<id>/chase?from=<大きな値>。録れているところの少し手前から流れる) に切り替えられます。
画面とテレビのアプリの動きは stream.md。
番組が替わって映像・音声・字幕の並び (PMT) が変わったときも閉じます (音声の本数や字幕の PID が変わった)。エンコードは起きたときの
並びのまま音声・字幕を選んでいて追従しないので、作り直します。閉じたらすぐ同じ局をリクエストし直してください。
raw では閉じません (映像と音声は受け側が PMT を読んで追い、字幕はサーバが写し直します)。
stream.md。
いまの番組を録る POST /api/services/<id>/record
Section titled “いまの番組を録る POST /api/services/<id>/record”その局でいま流れている番組を予約します (画面のライブの録画ボタンと同じ)。本文は要りませんが、
Content-Type: application/json は付けます (上記)。
{ "recorded": "ニュース", "programId": 32736103210001, "reserved": true }- 既に始まっている番組は、数秒後 (次のスケジューラの周期) に録りはじめます
- 何度押しても二重には録りません (予約は番組ごとに1本)。予約済み・録画中でも同じ答えが返ります
reservedがfalseなら予約はあるものの録らない状態です。たいていはチューナーが足りない競合 (どれを録るかは予約どうしの優先度で決まります。手動の予約は 2、ルールの予約はルールの優先度)- 番組表にいまの番組が無ければ
404、予約できなければ (放送が終わったところなど)400。 どちらもmessageに画面に出せる理由が入ります
録画の一覧 GET /api/recordings
Section titled “録画の一覧 GET /api/recordings”観られるもの (録り終えてファイルがあるものと、いま録っているもの) を新しい順に。?limit= と ?offset= で区切れます。
[{ "id": 12, "title": "番組 第1話", "name": "[新]番組 第1話[字]", "serviceId": 3227310008, "serviceName": "TOKYO MX", "startAt": 1790000000000, "endAt": 1790001800000, "durationMs": 1800000, "resumeMs": 754000, "watchedAt": null, "recording": false, "chase": "api/recordings/12/chase", "cmReliable": true, "poster": "api/recordings/12/poster", "files": [ { "source": "encoded", "codec": "av1", "url": "api/recordings/12/file?source=encoded" }, { "source": "alt", "codec": "h264", "url": "api/recordings/12/file?source=alt" }, { "source": "ts", "codec": "mpeg2", "url": "api/recordings/12/file?source=ts" }], "audio": "api/recordings/12/file?audio=only", "audios": [{ "id": "0:both", "stream": 0, "side": "both", "label": "ステレオ (日本語)", "main": true }] }]- 時刻は UNIX ミリ秒。
titleは記号 ([字]など) を落とした名前、nameは放送のままの名前 filesは出せるものだけ。どれを開くかは呼ぶ側が決めます (テレビのアプリなら、その端末がデコードできるもの)。 エンコードしたものは Matroska (video/x-matroska)、元のTSは TS (video/mp2t)。Range に応じます。 再生のためのもので、ダウンロード用ではありません (添付は付けない)audioは主音声だけを AAC (ADTS、audio/aac) で流します。音声だけを聴くとき向け。sourceと一緒に渡せば、元にするファイルを選べますresumeMsは続きから観る位置 (画面の「続き」と同じもの)。観ていない・観終えたものはnull。 エンコードしたもの (sourceがencoded・alt) の時刻で、エンコードしたものが無ければ元のTSの時刻。 エンコードしたものがあるのに元のTS (source=ts) で開くときは、時刻が合わないので使えません (CM を切ったもの・頭を捨てたもの)。durationMsもエンコードし終えると出来上がりの長さになるので、resumeMsと同じ物差しです (観た割合・残りは2つから出せます)watchedAtは末尾まで観た時刻 (ms)。まだならnull。watchedAtもresumeMsもnullなら未視聴 (画面の一覧の印と同じ)。resumeに末尾 (尺の30秒手前より後) を送ると入ります。画面の「未視聴に戻す」でnullに戻ります (「視聴済みにする」「未視聴に戻す」はどちらもresumeMsも消します)posterは一覧のサムネイル (JPEG)。エンコードする前の録画や元のTSしか無いものでは 404recordingはいま録っている最中か。録画中はdurationMsがnull(途中で切れて録り直したものは、それまでに録れた長さ) で、filesは元のTSだけ (伸びている途中)。 録画中・エンコードが終わる前はchase(下) で観るchaseは追っかけ再生のエンドポイント。元のTSがある間だけ入り、無ければnullaudiosは放送の音声の構成 (上記)。追っかけの?audio=<id>に渡します。エンコードしたもの (files) は デュアルモノを左右2本に割ってあるなど作りが違うので、そちらの音声は再生側のトラックの切り替えで選んでくださいcmReliableは CM 飛ばしを観はじめに入れてよいか (画面と同じ決め方)。ロゴで CM を見分けられなかった 録画は無音だけで当てていて外れやすいのでfalse。そのときはオフで始め、押せばオンにする
追っかけ再生 GET /api/recordings/<id>/chase
Section titled “追っかけ再生 GET /api/recordings/<id>/chase”録っている最中 (と、録り終えてエンコードが終わる前) の録画を、伸びている元のTSから流します。
codec |
中身 | Content-Type |
|---|---|---|
(なし) / h264 |
画面の追っかけと同じ再エンコード (H.264 / AAC の fragmented MP4) | video/mp4 |
av1 |
AV1 / Opus の fragmented MP4 | video/mp4 |
raw |
エンコードせずに元のTS (MPEG-2) をそのまま | video/mp2t |
?from=<秒>で頭からの位置を選びます (既定 0)。シークはfromを変えてリクエストし直します?audio=<id>(一覧のaudiosのid) でh264/av1でエンコードする音声を選びます。ライブと同じく、知らないものは主音声、 選び直すにはリクエストし直し、rawでは効きません- 元のTSには時間の索引が無いので、位置は「いまのファイルの大きさ ÷ 録れている秒数」の比例で 当たりを付け、188 バイト (TS パケット) に揃えます。いま書いているところの 5 秒手前より先へは行きません
- 送り込みは実時間の倍速まで。読む側が詰まっている間は止めて待ちます (再エンコードのほうも、 読まれずに溜まったら送り込みを止める)
- 尻まで読んだら、録っている間は書き足されるのを待って読み続け、録り終えて尻まで読んだら閉じます
h264/av1は、番組が替わって映像・音声・字幕の並び (PMT) が変わったところでも閉じます (ライブと同じ理由)。 届いたぶんを観終えてから、観ていた位置 (from+ 再生した秒) をリクエストし直してください。変わったところの 15 秒以内を リクエストされたら、変わったところから流します (秒からバイトへの当たりがずれて、前の番組から読み直さないように)。rawでは閉じません- 録画が無い・まだ何も録れていなければ 404
h264/av1は、上の確かめ (録画がある・何か録れている) を通ったあとで再エンコードを断られると (録画が直前に消された・音声の組み立てで転んだなど)、200 のまま中身が空で、すぐ閉じます。 空で閉じたらリクエストし直すか、rawを使ってください- 録画中は一覧の
durationMsがnullです (長さが決まっていない)。録れている長さは 録り始め (startAt) からの経過で見積もってください h264/av1はその場で変換する本数の枠を 1 つ使います (下記)。 埋まっていれば 503 ({"message":"サーバが混んでいます"})。rawは数えません
録画の観方 GET /api/recordings/<id>/playback
Section titled “録画の観方 GET /api/recordings/<id>/playback”録り終えた録画を、何で観られるか。観る画面 (/watch) が使っています。どれを使うかは受け側が決めます
(端末で再生できるか、人が選んだコーデック)。
{ "duration": 1790.5, "axis": "encoded", "audios": [{ "id": "0", "label": "主音声" }, { "id": "1", "label": "副音声" }], "offers": [{ "codec": "av1", "way": "file", "source": "encoded", "codecs": "av01.0.08M.08,opus" }, { "codec": "h264", "way": "stream", "codecs": "avc1.640029,mp4a.40.2" }, { "codec": "av1", "way": "stream", "codecs": "av01.0.08M.08,opus" }, { "codec": "mpeg2", "way": "raw" }] }axisは時刻の物差し。encodedはエンコード済みのファイルの時刻、tsは元の TS の頭を 0 とした時刻 (エンコード済みが無い録画)。duration・下のfrom・ストリームの時刻・字幕 (captions.json)・チャプターはどれもこれで数えますoffersのway:file… エンコード済みのファイルそのまま (api/recordings/<id>/file?source=<source>)。codecsは入れ物の中身 (読めなければ無い)stream…stream?codec=<codec>で fragmented MP4。codecsは MP4 の中身。エンコード済みと同じコーデックなら詰め替えるだけ (軽い)、違えばエンコード済み (無ければ元の TS) から変換しますraw…stream?codec=mpeg2で元の TS をそのまま。元の TS が残っていて、CM を切らずにエンコードした録画 (か、エンコード済みが無い録画) だけ
audiosのidをstream?audio=に渡します- 録っている最中の録画、エンコード済みも元の TS も無い録画は 404
録画を流す GET /api/recordings/<id>/stream
Section titled “録画を流す GET /api/recordings/<id>/stream”?codec=h264 / av1 / mpeg2 (要る。無い・知らない値は 400)、?from=<秒> (物差しの上の位置。既定 0)、?audio=<id>。シークは from を変えてリクエストし直します。
h264/av1… fragmented MP4 (video/mp4)。塊の時刻 (tfdt) は物差しの時刻のまま書きます (頭を 0 に詰め直さない)。 詰め替えはfromの手前のキーフレームから、変換は元の TS ならfromの 1 秒手前の映像から始まりますmpeg2… 元の TS をfromの 1 秒手前の映像から流します (video/mp2t)。X-Denpa-Originに物差しの 0 にあたる PTS (90kHz) を返します。 物差しの時刻 = (PTS −X-Denpa-Origin) を 33 ビットの一周の中で取って 90000 で割ったもの。audioは効きません (全部の音声が入る)- 変換 (エンコード済みと違うコーデック) はその場で変換する本数の枠を使います。埋まっていれば 503 (
{"message":"サーバが混んでいます"})。 詰め替えとmpeg2は数えません offersに無い形式は 404。読まれないまま 5 分置かれた変換は止めます (枠を返すため)。リクエストし直してください
録画のチャプター GET /api/recordings/<id>/chapters
Section titled “録画のチャプター GET /api/recordings/<id>/chapters”CM 飛ばしの行き先です。観る画面とテレビのアプリが使っています。
{ "chapters": [{ "start": 0, "end": 612.4, "title": "本編" }, { "start": 612.4, "end": 702.4, "title": "CM" }, { "start": 702.4, "end": 1790.5, "title": "本編" }] }start・endは秒で、playbackの物差し (上記) の時刻です。録画の時間そのもので、 エンコード済みのファイルの時刻・streamのfrom・ストリームの時刻と同じ数です。シークでリクエストし直しても、どの形式で流しても、そのまま使えます- エンコード済みがあれば、ファイルに入っているチャプターを返します。まだエンコードしていない録画は、検出した CM の区間から組みます
titleは denpa が書くものは本編とCM。CM かどうかはtitleがCMかで見てください。引き継いだ録画 (EPGStation) は 別の名前のことがあり、名前の無いものは—です。並びはstartの順- チャプターが無ければ空の配列です (CM を切ってエンコードした、CM を見つけられなかった、CM の扱いが「何もしない」、録っている最中)。 ファイルを読めなかったときも空で返します
- CM検出をやり直すと、同じ録画でも中身が変わります (通知の
chapters)。エンコード済みのものはETag付きのno-cacheで返すので、If-None-Matchで確かめれば変わっていないときは 304 です - 録画が無い・消したものは 404
その場で変換する本数
Section titled “その場で変換する本数”録画をエンコード済みと違う形式で流す (stream) のと、追っかけ再生 (chase の h264 / av1。画面の追っかけも) は、
サーバがその場でエンコードします。同時に走らせる本数には上限があり、設定の「録画」の「エンコード」(/settings/recording#encoding) にある「同時に変換する本数」で変えられます
(自動では GPU が無ければ 2 本、あれば 4 本)。ライブは数えず、断りません。
元のTSの字幕 GET /api/services/<id>/captions・GET /api/recordings/<id>/captions
Section titled “元のTSの字幕 GET /api/services/<id>/captions・GET /api/recordings/<id>/captions”元の TS (live?codec=raw・chase?codec=raw・file?source=ts) の字幕を、放送の PTS のまま流します。
ブラウザで元のTSを再生する経路と同じもの (stream.md)。受け側は映像の PTS がそこを過ぎたら重ね、次が来るまで出しておきます。
- 中身は文字の配置 (0x22。下記) です。ブラウザと同じもので、受け側が描きます。
前の画像 (0x20) はやめ、
?formatは見ません (denpa-tv 0.16.0 より前のアプリでは字幕が出ません) - ライブ (
services/<id>/captions) はいま変換せずに流している局のストリームを共有するだけです。live?codec=rawを開いてからリクエストします。 流していなければ 404。映像が閉じれば、こちらも閉じます - 録画 (
recordings/<id>/captions?from=<秒>) は、映像をリクエストしたのと同じfromを渡します (シークしたらリクエストし直す)。fromの 10 秒手前から読むので、いま出ている字幕も来ます。追っかけと同じく倍速までで先へ進み、録り終えた録画は 尻まで読んだら閉じます。元のTSが無ければ 404。同じ録画に同時に開けるのは 3 本までで、超えるといちばん古いものを閉じます (シークでリクエストし直したとき、前の繋ぎが残っていても積もらないように) - 番組が替わって並び (PMT) が変わっても閉じません。サーバが字幕を写し直し、選べる字幕 (0x40) を送り直します
- 本文 (
application/octet-stream) は、WebSocket の1フレーム (stream.md) の頭に長さを付けたものが並びます:
[4: 後ろの長さ (BE)][1: 種別][8: 時刻 (90kHz, BE)][中身]
0x22 字幕の文字の配置 JSON (下記)。前の1枚を丸ごと置き換える (`runs` が空なら消す)0x40 通知 JSON。{"type":"captions","tracks":[{"index":0,"lang":"jpn","label":"字幕 (日本語)"}],"track":0} は選べる字幕 (字幕を持たない放送では来ない。`lang` は放送が名乗っていなければ null)。20 秒おきに {"type":"ping"}。知らない種別・type は読み捨てる- 字幕の言語。 字幕1本に言語が複数載っている放送 (たまに英語が付く) では、
captionsの通知に"languages":[{"index":1,"code":"jpn","label":"日本語"},{"index":2,"code":"eng","label":"英語"}],"language":1が付きます (字幕管理データが届いてから)。?language=<1〜8>で第何言語を流すかを選びます。省けば第1言語。 その言語が載っていない番組では第1言語を流し、通知のlanguageも 1 になります - 時刻は放送の PTS (33 ビットで 26.5 時間ごとに一周する)。比べるときは近いほうへ伸ばしてください
録画の字幕 GET /api/recordings/<id>/captions.json
Section titled “録画の字幕 GET /api/recordings/<id>/captions.json”エンコードしたもの (mkv) に入っている放送の字幕を、文字の配置で丸ごと返します。観る画面はこれを描きます。
{ "v": 1, "pages": [{ "at": 12.345, "page": { "v": 1, "plane": [960, 540], "duration": null, "runs": [ … ] } }], "language": { "index": 1, "code": "jpn", "label": "日本語" }, "others": [{ "index": 2, "code": "eng", "label": "英語", "pages": [ … ] }] }atは動画の頭からの秒。pageは 下記。再生位置を追い越していない中の最後の1枚を出します- まだエンコードしていない録画は元の TS から抜きます。
atは元の TS の頭からの秒 (playbackの物差しts)。 TS を丸ごと読むので、返るまでエンコード済みより時間がかかります pagesは第1言語。languageは第1言語の名前 (字幕管理データが読めなければ省く)。othersは第2言語から後ろで、載っているときだけ 付きます (たいていの番組には無い)。足しただけなのでvは 1 のままです- 字幕を持たない録画は 404。ずっと前にエンコードした録画 (字幕が画像 (PGS) で入っている) も 404 です。元のTSが残っていれば、再エンコードすると放送の字幕が入ります
字幕の文字の配置
Section titled “字幕の文字の配置”放送の字幕 (ARIB STD-B24) を denpa がデコードして、置き場所・大きさ・色まで決めたものです。受け側は言われたとおりに塗るだけで、
放送どおりの所に出ます。ブラウザの描き方は src/lib/components/player/caption-draw.ts。
{ "v": 1, "plane": [960, 540], "duration": 3000, "runs": [{ "x": 250, "y": 450, "w": 40, "h": 60, "fx": 2, "fy": 12, "size": 36, "scaleX": 1, "text": "字幕です", "fg": "#ffffffff", "bg": "#00000080" }, { "x": 410, "y": 450, "w": 40, "h": 60, "fx": 2, "fy": 12, "size": 36, "scaleX": 1, "text": "〓", "drcs": "65569", "fg": "#ffffffff", "bg": "#00000080", "stroke": "#000000ff" }], "drcs": { "65569": { "w": 72, "h": 72, "depth": 16, "bits": 4, "data": "AAAA…", "md5": "0123…" } } }vは形式の版。知らない版は描かないでください (互換の無い変え方をしたら上げます。足すだけなら上げない)- 座標はすべて字幕の面の単位 (
plane。ふつう 960x540)。映像の枠に合わせて縦横それぞれ伸ばします durationは出しておく長さ (ms)。nullなら次の1枚まで。過ぎたら次を待たずに消しますrunsは描く順。1つの run は同じ見た目で横に並ぶ字で、textの1字 (コードポイント) ごとに幅w・高さhの 区画をxから右へ使います- 背景: 区画を
bgで塗る (空白の字も塗る) - 字: 区画の左上から (
fx,fy) ずらした所が字の枠 (高さsize・幅size × scaleX)。字の大きさはsize(em) で、 横だけscaleX倍に縮める (中くらいの全角は 0.5。半角の英数・カナは 1 のまま)。ペンは字の枠の左端。 縦は「永」の墨の上下が字の枠の真ん中に来るように基準線を置きます - 空白 (U+0020・U+3000 など) は字を描かない
strokeがあれば字の外側へ太さ 1.5 (面の単位) で縁取る (字より先に)underlineは区画の幅で字の下に線。boxは区画の縁に字の色の線 (1 下・2 右・4 上・8 左の和、太さは面の 1 単位)flashは点滅 (0.5 秒ずつ出す・消す)。rubyはルビらしい小さな字 (隠したければ隠してよい)。bold/italicは印だけ (描き分けなくてよい)drcsは置き換えられなかった外字。textは「〓」で、drcsの画像を字の枠いっぱいにfgで描く
- 背景: 区画を
drcsの画像は左上から1画素bitsビットずつ上の桁から詰めたもの (base64)。値 v の濃さは v / (depth− 1)。 いまは denpa が放送の白黒の画像 (16〜36 画素) の斜めを丸め、濃淡の画像 (bits4・depth16、一辺 64〜256。18x18 の字なら 72x72 で base64 3.5KB ほど) にして送ります。 濃さをfgの不透明度に掛けて塗り、字の枠いっぱいへなめらかに (双線形などで) 引き伸ばしてください。bitsは 1・2・4・8 のどれでも 読めるようにしておいてください (前に保存した字幕には白黒のbits1 が入っています)。md5は放送の元の画像の MD5 (外字の置き換え表に足すときのキー。描くのには使わない)。 置き換え表で字にできたもの (〓 にならないもの) はここに来ません- 色は
#rrggbbaa。字はデータ放送と同じ丸ゴシック (GET /api/font/denpa-font.woff2。Denpa Font) で描きます。版なしの URL は毎回確かめる (no-cache+ ETag) ので、そのまま取っても古い字は残りません
番組表の番組の中身 GET /api/programs/<id>
Section titled “番組表の番組の中身 GET /api/programs/<id>”<id> は番組ID (局の now.id)。画面の番組の詳細と同じものを返します (テレビのアプリのライブの詳細で使う)。
このエンドポイントだけキーは snake_case (画面と同じ形式をそのまま返している)。
{ "name": "[新]番組 第1話[字]", "service_name": "TOKYO MX", "start_at": 1790000000000, "end_at": 1790001800000, "description": "番組の概要", "extended": { "番組内容": "…", "出演者": "…" }, "genre_detail": [{ "lv1": 7, "lv2": 0 }], "audios": [{ "componentType": 3, "langs": ["jpn"], "main": true }, { "componentType": 3, "langs": ["jpn"], "text": "解説ステレオ" }], "video_type": "mpeg2", "video_resolution": "1080i", "is_free": true, "copy_never": false }nameは放送のままの名前 (記号[字]などを含む)。時刻は UNIX ミリ秒extendedは放送の詳細 (見出し → 本文)、genre_detailはジャンル (ARIB の大分類lv1・中分類lv2)、audiosは番組表の音声 (componentTypeは ARIB の音声の構成。2 がデュアルモノ、3 がステレオ。textは放送が付けた名前)。 どれも番組表に無ければnullvideo_type(mpeg2/h.264など)・video_resolution(1080i/480iなど) も、無ければnullis_freeは無料放送かcopy_neverはコピー禁止の番組か。禁止の番組は録らない (data.md)。POST /api/services/<id>/recordは400と「コピー禁止の番組なので録画しません」を返す- 番組表は終わった番組を消していくので、引けないことがあります (
404)。そのときは呼ぶ側が持っている分 (nowの番組名・時刻など) だけを出してください
検索の候補 GET /api/search?q=<言葉>&limit=<数>
Section titled “検索の候補 GET /api/search?q=<言葉>&limit=<数>”画面のヘッダーの欄が、打つそばから出す候補に使います。読み方は結果の画面 (/search?q=) と同じで
(空白で区切った語をすべて含む。全角/半角・大文字/小文字は区別しない)、節ごとに先頭の limit 件 (既定 4、多くても 10) と、
その先にもあるか (more) だけを返します。件数は数えません。キーは snake_case です。
{ "q": "でんぱ", "programs": { "more": true, "rows": [{ "id": 3227310008123, "name": "でんぱ劇場", "service_name": "でんぱテレビ", "start_at": 1790000000000, "end_at": 1790001800000, "reservation_state": null }] }, "recordings": { "more": false, "rows": [{ "id": 12, "name": "でんぱ劇場 #1", "service_name": "でんぱテレビ", "start_at": 1789000000000, "state": "available", "watch": "watch/12" }] }, "reservations": { "more": false, "rows": [{ "id": 5, "program_id": 3227310008123, "name": "でんぱ劇場", "service_name": "でんぱテレビ", "start_at": 1790000000000, "end_at": 1790001800000, "state": "scheduled", "chase": null }] }, "rules": { "more": false, "rows": [{ "id": 2, "name": "でんぱ", "enabled": true }] } }programsはまだ終わっていない番組 (始まる順)。reservation_stateは予約が立っていればその状態recordingsは消していない録画 (新しい順)。watchは観る画面 (録っている最中は追っかけ再生chase/<id>)。観られなければnullreservationsはこれから録るものと録っている最中のもの。chaseは録っている最中なら追っかけ再生qが空なら、どの節も空で返します。100 字を超えた分は切って探します (結果の画面/search?q=も同じ)
番組の中身 GET /api/recordings/<id>/detail
Section titled “番組の中身 GET /api/recordings/<id>/detail”{ "id": 12, "title": "番組 第1話", "name": "[新]番組 第1話[字]", "description": "番組の概要", "extended": { "出演者": "…", "番組内容": "…" } }descriptionは短い説明、extendedは放送の詳細 (見出し → 本文)。無ければ""/{}- 一覧 (
GET /api/recordings) には入れていない (長くて一覧が重くなる)。テレビのアプリの詳細で使う
観た位置 POST /api/recordings/<id>/resume
Section titled “観た位置 POST /api/recordings/<id>/resume”本文は { "at": 秒, "length": 尺の秒, "source": "encoded" } (length は分からなければ省く)。source は観ているファイル
(files の source と同じ encoded / alt / ts。追っかけは ts)。省くと、エンコードしたものがあれば encoded、無ければ ts とみなします。
at と length はそのファイルの時刻です。画面と同じく、頭の少しと
末尾の 30 秒は「覚えない」(消す) 扱いです。答えは { "resume": 覚えた秒 または null, "edge": 30 }。
15 秒おきくらいに送れば、ほかの端末 (画面・ほかのテレビ) でも続きから観られます。
末尾の 30 秒に入った位置を送ると「観終えた」になり、一覧の watchedAt が入ります (length が要ります)。
再生が終わったときにも位置を送ってください (末尾の CM を飛ばすと、15 秒おきの控えが末尾に届かないため)
サーバは元のTSの時刻に直して覚えます (CM を切ってエンコードしたものは、残した区間をつないだ時刻なので)。
追っかけで観た続きをエンコードしたもので開いても、同じ場面から始まります。切った CM の途中だった位置は、その後の本編の頭になります。
答えの resume は送った source の時刻のままです (v1.55 までに覚えた位置は直していないので、CM を切った録画では少しずれることがあります)
変化の通知 GET /api/events
Section titled “変化の通知 GET /api/events”Server-Sent Events (text/event-stream) で、サーバ側の変化を流し続けます。受け取ったら該当の一覧を読み直してください。
event: recordingsdata: 1
event: encodedata: {"recordingId":12,"percent":0.425,"etaMs":600000,"log":"…"}-
名前:
recordingsservicesprogramstunersencodeほか (画面向けのもの)。知らない名前は読み捨ててください -
programsは番組表を集めたときのほか、ライブで開いている局の番組が放送で替わった・延びたときにも来ます (EIT[p/f] が番組表と違ったとき)。局の一覧 (GET /api/services) のnowを読み直せば、いまの番組に追いつきます -
chaptersは録画のチャプターが書き換わった (CM検出をやり直した) ことの通知。dataは{"recordingId":12}。 その録画を観ていれば チャプター を読み直してください。位置 (秒) の物差しは変わりません -
dataはencodeとyieldedとchaptersだけが中身を運び、ほかは1。encodeはrecordingIdpercentetaMslog。percentは 0〜1 (名前に反して百分率ではない)。etaMsは見積もれなければnull -
yieldedはライブがチューナーを録画に譲った (チューナーを取り上げられた) ことの通知。serviceIdの局を観ていた人だけが読みます。 譲った録画が分かったときだけ来ますevent: yieldeddata: {"serviceId":3239123608,"recording":{"id":12,"title":"ニュース","serviceName":"TOKYO MX","endAt":1790001815000}}recording.endAtは録り終える時刻 (UNIX ミリ秒。終わりのマージン込み)。それまでそのチューナーは空きません。 使い方は ライブ の「録画にチューナーを譲ると閉じます」 -
25 秒おきに
event: pingが来ます。60 秒ほど何も届かなければ繋ぎ直してください (黙って切れた繋ぎを見分けるため) -
繋ぎ直したら一覧を1度読み直してください。切れていた間の通知は送り直しません (
Last-Event-IDは使わない)
| 相手 | 認証 |
|---|---|
信頼するネットワーク (TRUSTED_NETWORKS) の中 |
何も要りません |
| アプリ (テレビなど) | Authorization: Bearer denpa_… (下のペアリングで受け取るアクセストークン)。どこから来ても通ります |
| ブラウザ | OIDC のログイン (Cookie) |
資格の無い API の呼び出しには、いつも同じ 401 の JSON を返します ({"error":"unauthorized"}、
WWW-Authenticate: Bearer realm="denpa")。ログイン画面の HTML へは回しません。アプリはアクセストークンなしでまず API を呼び、
これが返ったときだけペアリングに進めばよい (信頼するネットワークの中なら通るので、ペアリングは要らない)。
無効にした・知らないアクセストークンを出したときは {"error":"invalid_token"} の 401 (WWW-Authenticate: Bearer error="invalid_token") で、
信頼するネットワークの中でも通しません (ペアリングし直しの合図)。仕組みと守りは auth.md。
ログイン方法を何も設定していない denpa は 403 で断り、ペアリングもできません (auth.md)。
ペアリング POST /api/device/code → POST /api/device/token
Section titled “ペアリング POST /api/device/code → POST /api/device/token”OAuth のデバイス認可 (RFC 8628) と同じ方式です。どちらも資格は要りません。
POST /api/device/code (本文 { "name": "居間のテレビ" }。名前は 40 字まで):
{ "deviceCode": "…", "userCode": "ABCD-EFGH", "verificationUri": "device", "verificationUriComplete": "device?code=ABCD-EFGH", "expiresIn": 600, "interval": 5 }- QR にするのは
verificationUriComplete(denpa の根からの相対) です。userCode・verificationUriは RFC の形式に合わせて返すだけで、コードを打ち込む画面はありません (コードは画面に出さなくてよい)。スマホで開くと、 いつものログイン方法 (信頼するネットワークか OIDC のログイン) を通ったうえでそのまま済みます - 有効なコードが多すぎるときは
429 {"error":"too_many_pending"}(しばらくして出し直す)
POST /api/device/token (本文 { "deviceCode": "…" }) を interval 秒おきに:
- 済んでいれば
200 { "token": "denpa_…", "tokenType": "Bearer" }。1度だけ返します - まだなら
400 {"error":"authorization_pending"}、急ぎすぎなら400 {"error":"slow_down"}(間隔を延ばす)、切れたら400 {"error":"expired_token"}(コードの発行からやり直す)、 知らない・使い終えたコードなら400 {"error":"invalid_grant"}。断る手順は無いのでaccess_deniedは返しません
アクセストークンを無効にする POST /api/device/logout
Section titled “アクセストークンを無効にする POST /api/device/logout”Authorization: Bearer で出しているアクセストークンを無効にします (アプリの「サーバーから外す」)。204 を返します。Bearer が無ければ 401 {"error":"unauthorized"}。
ほかの端末のアクセストークンは、設定の「連携」の「テレビのアプリ」(/settings/integrations#tv) から取り消します。
録画の状態 GET /api/recordings/<id>
Section titled “録画の状態 GET /api/recordings/<id>”{ "id": 12, "encoded": false, "state": "recording", "durationMs": null }encodedはエンコード済みのファイルがあるか。「エンコードしない」録画は元の TS を置くだけなので、置き終えてもfalseのまま (観られるかどうかはstateで分かる)。stateは録画の状態 (recordingrecordedavailablefailed)。録画が無い・消したものは404durationMsは一覧のdurationMsと同じもの (録画中はnull)。 追っかけのストリームが終わったとき、録り終えて最後まで観たのかを確かめるのに使えます (一覧を取り直さずに済む)- 追っかけ再生の画面は
recordingsの通知 (上記) のたびにこれを読み、encodedがtrueになったら観る画面への案内を出す
録画を消す DELETE /api/recordings/<id>
Section titled “録画を消す DELETE /api/recordings/<id>”ファイルごと消して 204 を返します。もう消えていても 204、録画中は 409、録画が無ければ 404
(どれも呼ぶ側は「済んだ」か「消せない」と読めばよい。端末の消しておいた分をあとで送り直すため、何度呼んでも同じ結果になる)。
本文は要りませんが、Content-Type: application/json は付けます (上記)。