所沢市議会の議会中継サイトで視聴できる録画映像をローカル保存した話

■何をやったのか

 所沢市議会の 議会中継サイト で視聴できる録画映像をパソコンにファイルとしてローカル保存する方法を確立した。このファイルをあらかじめ自宅でスマホに転送しておけば、外出時にも通信容量を気にせずに市議会 (の録画映像) を楽しめるって寸法よ。

▲パソコンにローカル保存した録画映像。

■なぜやったのか

 所沢市議会の議会中継サイトでは、ライブ映像や録画映像を、パソコンやスマホのブラウザでストリーミング映像として視聴できる。画質はそれほど高くないものの、映像ということもあって、それなりの通信容量を消費する。実際にスマホで通信容量を測ってみたところ1分間の視聴で2~3MBであった。映像が数分で終わるなら特に気にすることもないのだが実際にはそんなはずもなく、特に市政一般質問など会議が数時間にもおよぶような日には通信容量を数百MBも消費することになる。

 できれば通信容量を気にすることなく議会中継を見たい。ライブ中継は仕方ないにしても、せめて録画映像はあらかじめ自宅のWiFi環境でスマホに映像ファイルをダウンロードしておき、外出時には通信不要で再生する、といったことができないか。ストリーミングとはいえブラウザとサーバの間で映像のリクエストとレスポンスを交わしているはずであり、その中の映像データをローカルのファイルに固定化できないだろうか。

▲所沢市議会の 議会中継サイト で録画映像を視聴しているところ。

■どのようにやったのか

 ブラウザ (Windows 10 上の Google Chrome 75.0.3770.100) が議会中継のサイトで送受信しているリクエストとレスポンスを調べたところ、各日程のページをロードした際に (1) 映像一覧情報の取得 をしており、また再生ボタンを押した際には (2) playlist.m3u8 の取得、(3) chunklist.m3u8 の取得、(4) 映像ファイル (.ts) の取得 をしていることが分かった。実際のリクエストとレスポンスをこの記事の末尾に付録する。

 これらの内容を精査して調べたところ、所沢市議会の議会中継サイト (の録画映像を Chrome で視聴する場合) は HTTP Live Streaming (HLS) を用いているようだ、ということが分かった。私の理解をざっくり述べると、HLSはストリーミング映像プロトコルのひとつであり、細切れになった映像ファイルの一覧を取得した後、それらの細切れを次々に取得・再生していく もののようだ。また、映像の形式は MPEG-2 TS である。

 だとすれば、ブラウザが送っているリクエストを真似して細切れの映像ファイルを取得し、再生する代わりに ファイルとしてローカル保存すれば、やりたいことができるのではないか。これは curl と jq を使用すればシェルスクリプトで比較的お手軽に実現できそうだ。さらに、それらの細切れを結合してひとつのファイルにすれば、取り回しやすくなるのではないか。映像の形式は広く普及しているものだし、編集・再生ソフトはいくらでもありそうだ。

 そんなこんなで映像ファイル取得スクリプトを作成した。シェルスクリプトで100行に満たない程度だ。サーバに負荷をかけすぎないよう、取得のリクエスト間には待ち時間を15秒設け、失敗によるリトライ時にはこれを倍々にしていくようにした。実際の細切れ映像の再生長が十数秒なので、負荷はブラウザによるものと同程度以下になっているだろう。たぶん。

 このスクリプトをパソコン上で実行して細切れの映像ファイルを取得し、映像演習ソフトを用いて結合する。結合したファイルをスマホ (私の場合は Android 機) にUSB接続なり Dropbox 経由なりで送り、映像再生アプリで再生する。編集ソフトには LosslessCut を、再生アプリには VLC for Android を使用することにした。それほど多くのソフトやアプリを比較したわけではないが、LosslessCut はGUIによる結合作業が軽快に行える点が、VLC for Android は音声のみをバックグラウンドで再生できる点が気に入った。

 開会中の令和元(2019)年6月定例会の録画映像で試したところ、取得も結合も再生もうまくいっているようだ。やったぜ! これでもっと市議会をエンジョイしたい。

▲スクリプトでダウンロードした細切れの映像ファイルを――

▲LosslessCut に Ctrl+A でつっこんで――

▲結合する。その後、リネームして整理整頓し、スマホに送る。

▲VLC for Android の画面。ファイル名の先頭部分が共通する映像ファイルをまとめて表示してくれるようだ。

■付録:リクエストとレスポンス

 平成31年度第1回定例会の「開会・開議〔…〕」のページをロードした際および再生ボタンを押した際のリクエストおよびレスポンスを以下に示す。(ハイライトが妙なことになっているが、Quote にするのもそれはそれで見づらくなってしまうので、このままでご容赦いただきたい。)

(1) 映像一覧情報の取得

GET /dvsapi/councilrd/search?tenant_id=136&keywords=&logical_op=OR&from=&to=&group_id=&speaker_id=0 HTTP/1.1
Host: smart.discussvision.net:443
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36
X-Requested-With: XMLHttpRequest

HTTP/1.1 200 OK
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Connection: Keep-Alive
Content-Encoding: gzip
Content-Type: application/json; charset=utf-8
Date: Tue, 25 Jun 2019 08:51:20 GMT
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Keep-Alive: timeout=5, max=93
Pragma: no-cache
Server: Apache
Transfer-Encoding: chunked
Vary: Accept-Encoding

(ここに JSON 形式の映像一覧情報)

(2) playlist.m3u8 の取得

GET /www08/smart/_definst_/mp4:kaiken/tokorozawa/w_h31/0221000101.mp4/playlist.m3u8 HTTP/1.1
Host: smart.hls.wseod.stream.ne.jp:443
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36
X-Requested-With: XMLHttpRequest

HTTP/1.1 200
accept-ranges: bytes
access-control-allow-credentials: true
access-control-allow-headers: Content-Type, User-Agent, If-Modified-Since, Cache-Control, Range
access-control-allow-methods: OPTIONS, GET, POST, HEAD
access-control-allow-origin: *
access-control-expose-headers: Date, Server, Content-Type, Content-Length
cache-control: max-age=43200
content-length: 75
content-type: application/vnd.apple.mpegurl
date: Tue, 25 Jun 2019 08:42:01 GMT
status: 200
via: JSTCDN
x-cache: MISS/S
x-cache-age: 0/43200
x-origin-date: Tue, 25 Jun 2019 08:42:01 GMT

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=494968
chunklist.m3u8

(3) chunklist.m3u8 の取得

GET /www08/smart/_definst_/mp4:kaiken/tokorozawa/w_h31/0221000101.mp4/chunklist.m3u8 HTTP/1.1
Host: smart.hls.wseod.stream.ne.jp:443
Accept: */*
Origin: https://smart.discussvision.net
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36

HTTP/1.1 200
accept-ranges: bytes
access-control-allow-credentials: true
access-control-allow-headers: Content-Type, User-Agent, If-Modified-Since, Cache-Control, Range
access-control-allow-methods: OPTIONS, GET, POST, HEAD
access-control-allow-origin: *
access-control-expose-headers: Date, Server, Content-Type, Content-Length
cache-control: max-age=43200
content-length: 913
content-type: application/vnd.apple.mpegurl
date: Tue, 25 Jun 2019 08:42:01 GMT
status: 200
via: JSTCDN
x-cache: MISS/S
x-cache-age: 0/43200
x-origin-date: Tue, 25 Jun 2019 08:42:01 GMT

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:17
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:16.667,
media_0.ts
#EXTINF:16.666,
media_1.ts
#EXTINF:10.2,
media_2.ts
#EXTINF:16.667,
media_3.ts
#EXTINF:16.667,
media_4.ts
#EXTINF:16.666,
media_5.ts
#EXTINF:16.667,
media_6.ts
#EXTINF:16.667,
media_7.ts
#EXTINF:16.666,
media_8.ts
#EXTINF:16.667,
media_9.ts
#EXTINF:16.667,
media_10.ts
#EXTINF:16.666,
media_11.ts
#EXTINF:16.667,
media_12.ts
#EXTINF:16.667,
media_13.ts
#EXTINF:16.666,
media_14.ts
#EXTINF:15.834,
media_15.ts
#EXTINF:16.666,
media_16.ts
#EXTINF:16.667,
media_17.ts
#EXTINF:16.667,
media_18.ts
#EXTINF:16.666,
media_19.ts
#EXTINF:16.667,
media_20.ts
#EXTINF:16.667,
media_21.ts
#EXTINF:16.666,
media_22.ts
#EXTINF:16.667,
media_23.ts
#EXTINF:16.667,
media_24.ts
#EXTINF:15.4,
media_25.ts
#EXTINF:16.666,
media_26.ts
#EXTINF:16.667,
media_27.ts
#EXTINF:16.333,
media_28.ts
#EXTINF:6.59,
media_29.ts
#EXT-X-ENDLIST

(4) 動画ファイル (.ts) の取得

GET /www08/smart/_definst_/mp4:kaiken/tokorozawa/w_h31/0221000101.mp4/media_0.ts HTTP/1.1
Host: smart.hls.wseod.stream.ne.jp:443
Accept: */*
Origin: https://smart.discussvision.net
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36

HTTP/1.1 200
accept-ranges: bytes
access-control-allow-credentials: true
access-control-allow-headers: Content-Type, User-Agent, If-Modified-Since, Cache-Control, Range
access-control-allow-methods: OPTIONS, GET, POST, HEAD
access-control-allow-origin: *
access-control-expose-headers: Date, Server, Content-Type, Content-Length
cache-control: max-age=43200
content-length: 580544
content-type: video/MP2T
date: Tue, 25 Jun 2019 08:42:01 GMT
status: 200
via: JSTCDN
x-cache: MISS/S
x-cache-age: 0/43200
x-origin-date: Tue, 25 Jun 2019 08:42:01 GMT

(ここに映像ファイル)