続・玩式草子 ―戯れせんとや生まれけん―

第47回『らじる★らじる』聴き逃がしサービス(3)

前回、聴き逃しサービスの番組へのリンクからJSONデータを取り出し、そこに記されたタイトルや配信元URLを使ってffmpegで番組をダウンロードするためのスクリプトを書いてみました。

このスクリプトでいくつかの番組をダウンロードしてみたところ、スクリプト自体はそれなりに動いて必要な情報を取れてはいるものの、番組のダウンロードはしばしば失敗してしまいます。

$ python ./json_01.py 'p=0308_01_3844917'
ffmpeg version 4.3.3 Copyright (c) 2000-2021 the FFmpeg developers
...
Input #0, hls, from 'https://vod-stream.nhk.jp/radioondemand/r/308/s/
  stream_308_5725b4a0be55d9c3c4d52e4c954d77c4/index.m3u8':
  Duration: 00:10:00.06, start: 1.999989, bitrate: 0 kb/s
  Program 0 
    Metadata:
      variant_bitrate : 48601
    Stream #0:0: Audio: aac (HE-AAC), 48000 Hz, stereo, fltp, 47 kb/s
... 
Output #0, mp4, to '名曲スケッチ「悲愴交響曲_第2楽章」_「“眠りの森の美女”
  から“アダージョ”」.mp4':
  Metadata:
    encoder         : Lavf58.45.100
    Stream #0:0: Audio: aac (HE-AAC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 47 kb/s
...
[aac_adtstoasc @ 0x16ffa80] Multiple RDBs per frame with CRC is not
  implemented. Update your FFmpeg version to the newest one from Git.
  If the problem still occurs, it means that your file has a feature
  which has not been implemented.
[mp4 @ 0x1734080] Error applying bitstream filters to an output packet
  for stream #0: Not yet implemented in FFmpeg, patches welcome
av_interleaved_write_frame(): Not yet implemented in FFmpeg, patches welcome
size=     278kB time=00:00:48.12 bitrate=  47.3kbits/s speed=74.9x    
video:0kB audio:283kB subtitle:0kB other streams:0kB global headers:0kB
  muxing overhead: unknown
Conversion failed!

あれこれオプションを指定したり、ffmpegの最新版を試してみても結果は同じだったので、どうやらエラーメッセージにあるようにffmpegが想定しているHLSの仕様(データフレームの構造?)が聴き逃しサービスの提供しているHLSと異なっていることが原因のようです。そのためffmpeg以外の方法を考えることにしました。

マルチメディア・フレームワーク GStreamer

ffmpeg以外のマルチメディアツールは…… と考えて、思いついたのがGStreamerです。GStreamerは「マルチメディア・フレームワーク」と称し、さまざまな種類の動画・音声ファイルを処理する多数のライブラリ(モジュール)から構成されたソフトウェアです。

GStreamerのホームページ
GStreamerのホームページ

GStreamerでは、FLAC形式の音声データを扱う"libgstflac.so"やMP4形式のコンテナを扱う"libgstisomp4.so"のように、動画や音声のコーデックやコンテナごとにその機能を担当するモジュールが用意され、あたかもUNIXの「ソフトウェア・ツールズ」のように、それらモジュールをパイプでつないで複雑な変換や加工処理ができるように設計されています。

GStreamerでどのような機能を使えるかはgst-inspect-1.0コマンドで調べます。引数を指定せずに"gst-insepect-1.0"を起動すると、インストール済みのGStreamerモジュールをチェックして、その機能を一覧表示します。

$ gst-inspect-1.0
vaapi:  vaapijpegdec: VA-API JPEG decoder
vaapi:  vaapimpeg2dec: VA-API MPEG2 decoder
vaapi:  vaapih264dec: VA-API H264 decoder
...
staticelements:  bin: Generic bin
staticelements:  pipeline: Pipeline object

Total count: 239 plugins (1 blacklist entry not shown), 1484 features

さて、まずはGStreamerで聴き逃しサービスが利用できるか試してみましょう。あるファイルやURLがGStreamerで利用できるかはgst-play-1.0コマンドで確認できます。前回のスクリプトで得られたURLを引数に"gst-play-1.0"を起動したところ、問題なくスピーカーから音声が再生できました。

$ gst-play-1.0 https://vod-stream.nhk.jp/radioondemand/r/308/s/stream_308_5725b4a0be55d9c3c4d52e4c954d77c4/index.m3u8
Press 'k' to see a list of keyboard shortcuts.
Now playing https://vod-stream.nhk.jp/radioondemand/r/308/s/stream_308_5725b4a0be55d9c3c4d52e4c954d77c4/index.m3u8
Redistribute latency...
0:00:08.3 / 0:10:00.0

GStreamerで聴き逃しサービスの配信に対応できそうなので、次はどのモジュールを組み合わせてURIから音声データをダウンロードするかを考えます。そのために配信元の情報をgst-discover-1.0で調べてみます。

$ gst-discover-1.0 https://vod-stream.nhk.jp/radioondemand/r/308/s/stream_308_5725b4a0be55d9c3c4d52e4c954d77c4/index.m3u8
Analyzing https://vod-stream.nhk.jp/radioondemand/r/308/s/stream_308_5725b4a0be55d9c3c4d52e4c954d77c4/index.m3u8
Done discovering https://vod-stream.nhk.jp/radioondemand/r/308/s/stream_308_5725b4a0be55d9c3c4d52e4c954d77c4/index.m3u8

Properties:
  Duration: 0:10:00.064000000
  Seekable: yes
  Live: no
  container: application/x-hls
    container: MPEG-4 AAC
      audio: MPEG-4 AAC
        Stream ID: 04a7001f96985b910726b6f477b215306525d31d14dbf07508e8c8ac47336f3a/src_0
        Language: <unknown>
        Channels: 2 (front-left, front-right)
        Sample rate: 48000
        Depth: 32
        Bitrate: 48617
        Max bitrate: 0

"gst-discover-1.0"は引数として指定したURIやファイルの内容を調べるコマンドで、結果を見ると、今回指定したURIからはHLSプロトコル(application/x-hls)を使って、MPEG-4形式のコンテナで、AACコーデックの2チャンネル、サンプルレート48kHzのデータが送られてくることが分かりました。

GStreamerの使い方

それでは必要なモジュール類を考えてみます。今回はURLを指定してデータをダウンロードするので、まずはHTTPを処理するためのモジュールが必要です。"gst-inspect-1.0"で調べると、下請けにlibcurlを使うcurlhttpsrc、libneonを使うneonhttpsrc、libsoupを使うsouphttpsrcの3種がありました。

$ gst-inspect-1.0 | grep http
curl:  curlhttpsink: Curl http sink
curl:  curlhttpsrc: HTTP Client Source using libcURL
neonhttpsrc:  neonhttpsrc: HTTP client source
soup:  souphttpsrc: HTTP client source
soup:  souphttpclientsink: HTTP client sink

HLS回りを処理するモジュールも確認しておきましょう。こちらはHLS形式で送られてきたデータを処理するhlsdemux一択のようです。

$ gst-inspect-1.0 | grep hls
libav:  avmux_hls: libav Apple HTTP Live Streaming muxer
typefindfunctions: application/x-hls: m3u8
hls:  hlsdemux: HLS Demuxer
hls:  hlssink: HTTP Live Streaming sink
hls:  hlssink2: HTTP Live Streaming sink

pulseaudioやGStreamerといったマルチメディア用のソフトウェアでは、source(src)がデータの入力元、sinkがデータの出力先を意味します。上記例ではcurlhttpsrcがlibcurlを使ってネット上のURLからデータをダウンロードする機能、curlhttpsinkは逆にネット上へデータを送信する機能となります。

一方、mux(MUltipleXer)は動画や音声、字幕データなどをMP4やMatroskaといった形式で1つのファイル(データストリーム)に統合する機能、demux(DE-MUltipleXer)はその逆で1つのデータストリームから動画や音声、字幕データ等を取り出す(分離する)機能を意味します。上記例ではhlsdemuxモジュールがHLS形式で送られてくるデータを動画や音声に分離する機能、avmux_hlsが動画や音声データをHLS形式に統合する機能となります。hlssinkはHLS形式でネット上に配信する機能を担うのでしょう。

GStreamerでは、gst-launch-1.0コマンドでこれらのモジュールを起動し、"!"で処理を繋いでいきます。今回の場合、HTTP(URI)からHLSでデータをダウンロードし、そこから音声データを取り出して適切な形式でファイルに落す、という手順になるので、まずは音声データをダウンロードしてpulseaudioに流しこんで再生できるか確認します。

$ gst-launch-1.0 curlhttpsrc location='https://vod-stream.nhk.jp/radioondemand/r/308/s/stream_308_5725b4a0be55d9c3c4d52e4c954d77c4/index.m3u8' \
! hlsdemux ! decodebin ! audioconvert ! pulsesink
パイプラインを一時停止 (PAUSED) にしています...
Pipeline is PREROLLING ...
Got context from element 'souphttpsrc1': gst.soup.session=context, session=(SoupSession)NULL, force=(boolean)false;
Redistribute latency...
Pipeline is PREROLLED ...
パイプラインを再生中 (PLAYING) にしています...
New clock: GstPulseSinkClock
0:00:05.1 / 0:10:00.0 (0.9 %)

使ったモジュールを簡単に紹介すると、curlhttpsrcは前述のようにlibcurlを使って"location=..."で指定したURIからデータをダウンロードするモジュールで、hlsdemuxでそこから動画や音声を取りだし、decodebinで取り出した動画や音声をGStremaerが使う汎用的な形式(raw data)に変換、audioconvertで出力先(例ではpulsesink)が受け取れる形式に合わせた上でpulsesinkからpulseaudioサーバに送る、という処理になります。なお、それぞれのモジュールを"gst-inspect-1.0"で調べれば、その機能や指定可能なオプションが表示されます。

$ gst-inspect-1.0 curlhttpsrc
Factory Details:
  Rank                     secondary (128)
  Long-name                HTTP Client Source using libcURL
  Klass                    Source/Network
  Description              Receiver data as a client over a network via HTTP using cURL
  ...
  location            : URI of resource to read
                        flags: 読み込み可能, 書き込み可能
                        String. Default: null
 ...
  user-pw             : HTTP location URI password for authentication
                        flags: 読み込み可能, 書き込み可能
                        String. Default: null

上記コマンドで音声データの再生ができたので、後は出力先をファイルにすれば何とかなりそうです。そのためには"pulsesink"の代わりにfilesinkを使い、"location=..."の指定でファイルに落します。その際、"AAC"形式の音声データを"MP4"なコンテナに格納することにします。

"AAC(Advanced Audio Codec)"はMP3の後継となるコーデックで、MP3よりも効率よく音声データを圧縮できます。"MP4"は動画や音声データを格納するためのビデオ形式(コンテナ)で、ISOで標準化されているため、たいていのメディアプレイヤー等で再生できます。

そのためには、音声データをfaacでAAC形式に変換し、mp4muxでMP4形式のコンテナに格納してから、filesinkでファイルに出力することになり、先のコマンドラインの"pulsesink"の部分をこんな風に書き変えました。

$ gst-launch-1.0 curlhttpsrc location='https://vod-stream.nhk.jp/radioondemand/r/308/s/stream_308_5725b4a0be55d9c3c4d52e4c954d77c4/index.m3u8' \
! hlsdemux ! decodebin ! audioconvert ! faac ! mp4mux ! filesink location='testfile.mp4'
パイプラインを一時停止 (PAUSED) にしています...
Pipeline is PREROLLING ...
Got context from element 'souphttpsrc1': gst.soup.session=context, session=(SoupSession)NULL, force=(boolean)false;
Pipeline is PREROLLED ...
パイプラインを再生中 (PLAYING) にしています...
New clock: GstSystemClock
Got EOS from element "pipeline0".
Execution ended after 0:00:08.330292008
Setting pipeline to NULL ...
Freeing pipeline ...

上記コマンドラインでダウンロードしたファイルを調べてみると、正しくMP4形式になっていて、愛用している音楽プレイヤーAudaciousでも再生できました。

$ file testfile.mp4
 testfile.mp4: ISO Media, MP4 v2 [ISO 14496-14]
Audaciousオーディオプレイヤーでの再生
Audaciousオーディオプレイヤーでの再生

後はこのコマンドを前回のスクリプトに組み込んで、番組へのリンクから配信データをダウンロードできるようにすればいいわけですが、だいぶ長くなってしまったので、そのあたりは次回に回しましょう。


本文中でも触れたように、今回取りあげたGStreamerは「単機能のコマンドを組み合わせて複雑な処理を実現する」というUNIXの伝統的な考え方をマルチメディア分野に応用した面白いソフトウェアです。

以前紹介した"ffmpeg"は出力先の拡張子に応じて入力データを自動的に変換してくれるのに対し、GStreamerでは「これをこうして、こうやって」の手順をひとつずつ組み立てていく必要があり、その際にはコーデックやコンテナに関する知識も必要なので、最初のうちは簡単な変換作業をするのも大変でした。

しかし多少慣れてくると、状況や目的に応じたモジュールの組み合わせを考えるのが謎解きパズルのように感じられ、あれこれ試行錯誤するのが楽しくなってきます。このあたりの感覚は「ソフトウェア・ツールズ」に共通するものかも知れません。

おすすめ記事

記事・ニュース一覧