phonypianistのメモ

調査したことなどをメモ代わりに書いています

UPnPアイテムのIDを指定してのメディアの詳細確認

前回は、ディレクトリ階層の末端にあるアイテムの一覧を取得したが、それぞれのアイテムのIDから、その情報を取ることもできる。

アイテムのIDがわかっていると、それをObjectIDに指定して、BrowseFlagを「BrowseMetadata」にして電文を送信すればよい。

例えば、アイテムのIDが「AUDIO-0-0-1950-PARENT-112-0-0-0-0」の情報を取得するには、次の電文を送信する。

<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <s:Body>
    <u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
      <ObjectID>AUDIO-0-0-1950-PARENT-112-0-0-0-0</ObjectID>
      <BrowseFlag>BrowseMetadata</BrowseFlag>
      <Filter>*</Filter>
      <StartingIndex>0</StartingIndex>
      <RequestedCount>0</RequestedCount>
      <SortCriteria></SortCriteria>
    </u:Browse>
  </s:Body>
</s:Envelope>

すると、次のような電文を返してくる。
Resultタグの中身のみ抜粋して掲載している。

<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:pxn="urn:schemas-panasonic-com:pxn">
<item id="AUDIO-0-0-1950-PARENT-112-0-0-0-0" parentID="AUDIO-1-0-112-PARENT-0-0-0-0-0" refID="AUDIO-0-0-279" restricted="1">
  <dc:title>xxxxxxxxxx</dc:title>
  <dc:creator>xxxxxxxxxx</dc:creator>
  <upnp:writeStatus>NOT_WRITABLE</upnp:writeStatus>
  <upnp:recordable>0</upnp:recordable>
  <upnp:class name="audioItem">object.item.audioItem</upnp:class>
  <res protocolInfo="http-get:*:audio/x-flac:DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01500000000000000000000000000000" duration="0:08:00" size="38572329">http://xxx.xxx.xxx.25:60014/AUDIO-0-0-1950-PARENT-112-0-0-0-0_BDY_1.flac</res>
  <upnp:originalTrackNumber>1</upnp:originalTrackNumber>
  <upnp:genre>xxxxxxxxxx</upnp:genre>
  <upnp:album>xxxxxxxxxx</upnp:album>
  <upnp:artist>xxxxxxxxxx</upnp:artist>
  </item>
</DIDL-Lite>

前回試した、アイテム一覧を取得するときと同じ情報が取れる。

が、プログラム側としては、アイテムのIDさえ保持しておけば、DLNAサーバからいつでもリクエストを送信してメタ情報が取れるので、他の情報をすべて常に保持しておく必要はない。

もちろん、通信が伴うこともあり多少処理に時間がかかるため、作成するプログラムの特性に応じて、メタ情報をキャッシュするのか都度取得するかを使い分ければよい。

UPnP コンテンツのURIの取得

前回は、DLNAサーバのコンテンツの辿り方について確認した。 今回はファイルのURIを取得する。

といっても、前回のように辿っていけば、いずれResultタグの中身が次のようなものを返してくる。

<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:dlna="urn:schemas-dlna-org:metadata-1-0/" xmlns:pxn="urn:schemas-panasonic-com:pxn" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/ http://www.upnp.org/schemas/av/didl-lite-v2-20060531.xsd urn:schemas-upnp-org:metadata-1-0/upnp/ http://www.upnp.org/schemas/av/upnp-v2-20060531.xsd">
<item id="AUDIO-0-0-1590-PARENT-89-0-0-0-0" parentID="AUDIO-1-0-89-PARENT-0-0-0-0-0" refID="AUDIO-0-0-316" restricted="1">
  <dc:title>xxxxxxxxxx</dc:title>
  <dc:creator>xxxxxxxxxx</dc:creator>
  <upnp:writeStatus>NOT_WRITABLE</upnp:writeStatus>
  <upnp:recordable>0</upnp:recordable>
  <upnp:class name="audioItem">object.item.audioItem</upnp:class>
  <res protocolInfo="http-get:*:audio/x-flac:DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01500000000000000000000000000000" duration="0:02:02" size="8428596">http://xxx.xxx.xxx.25:60014/AUDIO-0-0-1590-PARENT-89-0-0-0-0_BDY_1.flac</res>
  <upnp:originalTrackNumber>1</upnp:originalTrackNumber>
  <upnp:genre>xxxxxxxxxx</upnp:genre>
  <upnp:album>xxxxxxxxxx</upnp:album>
  <upnp:albumArtURI dlna:profileID="JPEG_TN">http://xxx.xxx.xxx.25:60014/AUDIO-6-0-1590_BDY.jpg</upnp:albumArtURI>
  <upnp:artist>Vladimir Ashkenazy</upnp:artist>
</item>
<item>
  ...

ディレクトリの場合はcontainerタグだったが、ファイルはitemタグで返ってくる。

あとは簡単。resタグにファイルのURIが入っているので、それを取得するのみ。

resタグのprotocolInfo属性には、ファイルの種類等が「プロトコル:ネットワーク:コンテンツフォーマット:追加情報」の形式で記載されている。 通常、プロトコルは「http-get」、つまりHTTPのGETリクエストで取得可能ということ。 ネットワークは「*」になっていることがほとんど(のはず)。 コンテンツフォーマットはMIME-typeなので、「audio/mp3」のような文字列。Google HomeURIを渡して再生する場合には、このMIME-typeも合わせて渡すことになる。 追加情報は・・・よくわかりません💦

参考:http://upnp.org/specs/av/UPnP-av-ConnectionManager-v1-Service.pdf

次回は、アイテムのIDを指定してのメタ情報取得について。

DLNAサーバ内の探索

前回は、DLNAサーバの一覧を取得した。 今回は、その中からコンテンツを保持するサーバの中を辿ってみる。

サービスの絞り込み

UPnPの応答で、STの項目にContentDirectoryを含むものが、コンテンツを保持するサーバとみなしてよい。 その応答内のLOCATION項目のURIを取得する。

HTTP/1.1 200 OK
CACHE-CONTROL: max-age=1800
DATE: Tue, 29 Jan 2019 15:04:52 GMT
EXT:
LOCATION: http://xxx.xxx.xxx.25:60606/D8AFF1C0AF18/Server0/ddd
SERVER: Linux/4.0 UPnP/1.0 Panasonic-UPnP-MW/1.0
ST: urn:schemas-upnp-org:service:ContentDirectory:2
USN: uuid:4D454930-0100-1000-8000-D8AFF1C0AF18::urn:schemas-upnp-org:service:ContentDirectory:2

上記だと「http://xxx.xxx.xxx.25:60606/D8AFF1C0AF18/Server0/ddd」を取得する。 このURIにGETリクエストを送ると、次の応答が返ってくる。

<?xml version="1.0"?>
<root xmlns="urn:schemas-upnp-org:device-1-0" xmlns:dlna="urn:schemas-dlna-org:device-1-0" xmlns:vli="urn:schemas-panasonic-com:vli" xmlns:hdlnk="urn:schemas-hdlnk-org:device-1-0" xmlns:sptv="urn:schemas-skyperfectv-co-jp:device-1-0" xmlns:jlabs="urn:schemas-jlabs-or-jp:device-1-0" xmlns:viera="urn:schemas-panasonic-com:viera">
  <specVersion>
    <major>1</major>
    <minor>0</minor>
  </specVersion>
  <device>
    <deviceType>urn:schemas-upnp-org:device:MediaServer:2</deviceType>
    <friendlyName>DMR-UBZ2030</friendlyName>
    <manufacturer>Panasonic</manufacturer>
    <modelName>BD/DVD Recorder</modelName>
    <modelNumber>DMR-UBZ2030</modelNumber>

(中略)

    <serviceList>
      <service>
        <serviceType>urn:schemas-upnp-org:service:ContentDirectory:2</serviceType>
        <serviceId>urn:upnp-org:serviceId:ContentDirectory</serviceId>
        <SCPDURL>http://xxx.xxx.xxx.25:60606/Server0/CDS_SCPD</SCPDURL>
        <controlURL>http://xxx.xxx.xxx.25:60606/Server0/CDS_control</controlURL>
        <eventSubURL>http://xxx.xxx.xxx.25:60606/Server0/CDS_event</eventSubURL>
      </service>
      <service>
        <serviceType>urn:schemas-upnp-org:service:ConnectionManager:2</serviceType>
        <serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId>
        <SCPDURL>http://xxx.xxx.xxx.25:60606/Server0/CMS_SCPD</SCPDURL>
        <controlURL>http://xxx.xxx.xxx.25:60606/Server0/CMS_control</controlURL>
        <eventSubURL>http://xxx.xxx.xxx.25:60606/Server0/CMS_event</eventSubURL>
      </service>
      <service>
        <serviceType>urn:schemas-upnp-org:service:ScheduledRecording:1</serviceType>
        <serviceId>urn:upnp-org:serviceId:ScheduledRecording</serviceId>
        <SCPDURL>http://xxx.xxx.xxx.25:60606/Server0/SRS_SCPD</SCPDURL>
        <controlURL>http://xxx.xxx.xxx.25:60606/Server0/SRS_control</controlURL>
        <eventSubURL>http://xxx.xxx.xxx.25:60606/Server0/SRS_event</eventSubURL>
      </service>
      <service>
        <serviceType>urn:schemas-xsrs-org:service:X_ScheduledRecordingExt:1</serviceType>
        <serviceId>urn:xsrs-org:serviceId:X_ScheduledRecordingExt</serviceId>
        <SCPDURL>http://xxx.xxx.xxx.25:60606/Server0/XSRSExt_SCPD</SCPDURL>
        <controlURL>http://xxx.xxx.xxx.25:60606/Server0/XSRSExt_control</controlURL>
        <eventSubURL>http://xxx.xxx.xxx.25:60606/Server0/XSRSExt_event</eventSubURL>
      </service>
    </serviceList>
  </device>
</root>

前半はサーバの名称等の情報、途中省略している部分はエンコードの種類等の情報、後半はサービスのURIが含まれる。 サービスのURIも複数あるが、今回の目的であるコンテンツを管理するサーバを表すものを絞り込む。 serviceTypeにContentDirectoryが含まれるものが該当し、そのcontrolURLを取得すればよい。 上記だと「http://xxx.xxx.xxx.25:60606/Server0/CDS_control」となる。

コンテンツの探索

ここからは、先ほど取得したURIに対してPOSTリクエストを送り、コンテンツを探索することになる。形式はSOAPっぽい。 送るリクエストボディは次の通り。

<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <s:Body>
    <u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:2">
      <ObjectID>0</ObjectID>
      <BrowseFlag>BrowseDirectChildren</BrowseFlag>
      <Filter>*</Filter>
      <StartingIndex>0</StartingIndex>
      <RequestedCount>0</RequestedCount>
      <SortCriteria></SortCriteria>
    </u:Browse>
  </s:Body>
</s:Envelope>

<u:Browse> タグのNamespaceは、先の応答結果のserviceTypeと同じにしておく。

レスポンスは次のようなものになる。

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <s:Body>
    <u:BrowseResponse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:2">
      <Result>&lt;DIDL-Lite xmlns=&quot;urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/&quot; xmlns:dc=&quot;http://purl.org/dc/elements/1.1/&quot; xmlns:upnp=&quot;urn:schemas-upnp-org:metadata-1-0/upnp/&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; xsi:schemaLocation=&quot;urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/ http://www.upnp.org/schemas/av/didl-lite-v2-20060531.xsd urn:schemas-upnp-org:metadata-1-0/upnp/ http://www.upnp.org/schemas/av/upnp-v2-20060531.xsd&quot;&gt;(中略)&lt;/DIDL-Lite&gt;</Result>
      <NumberReturned>4</NumberReturned>
      <TotalMatches>4</TotalMatches>
      <UpdateID>1229</UpdateID>
    </u:BrowseResponse>
  </s:Body>
</s:Envelope>

Resultタグの中がさらにXMLデータになっていて、&lt;やら&gt;やらを変換してXML解釈することで、中身を確認できる。 上記は中略しているが、解釈した内容は次のようになる。

<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/ http://www.upnp.org/schemas/av/didl-lite-v2-20060531.xsd urn:schemas-upnp-org:metadata-1-0/upnp/ http://www.upnp.org/schemas/av/upnp-v2-20060531.xsd">
<container id="HDD" parentID="0" restricted="0">
  <dc:title>HDD</dc:title>
  <upnp:writeStatus>NOT_WRITABLE</upnp:writeStatus>
  <upnp:recordable>0</upnp:recordable>
  <upnp:class name="container">object.container</upnp:class>
</container>
<container id="USB_HDD" parentID="0" restricted="0">
  <dc:title>録画用USB-HDD</dc:title>
  <upnp:writeStatus>NOT_WRITABLE</upnp:writeStatus>
  <upnp:recordable>0</upnp:recordable>
  <upnp:class name="container">object.container</upnp:class>
</container>
<container id="SQV_HDD" parentID="0" restricted="0">
  <dc:title>SeeQVault-HDD</dc:title>
  <upnp:writeStatus>NOT_WRITABLE</upnp:writeStatus>
  <upnp:recordable>0</upnp:recordable>
  <upnp:class name="container">object.container</upnp:class>
</container>
<container id="TUNER" parentID="0" restricted="1">
  <dc:title>チューナ</dc:title><upnp:writeStatus>NOT_WRITABLE</upnp:writeStatus>
  <upnp:recordable>0</upnp:recordable>
  <upnp:class name="container">object.container</upnp:class>
  </container>
</DIDL-Lite>

containerタグが要素を表していて、そのid属性がキーとなる。 さらにこの下を辿るには、先ほど送ったリクエストボディのObjectIDにcontainerタグのid属性値を設定して、再度同じURIにPOSTすればよい。 配下を辿るのは、この繰り返しとなる。

なお、コンテンツが多い場合は一度にすべてのcontainerを取得できない。 StartingIndex + NumberReturned < TotalMatches の場合は、StartingIndexに値を設定して再度POSTすれば、その続きから取得できる。


最初の方のリクエストがややこしいが、コンテンツの一覧さえ取得できれば、あとは繰り返し同じリクエストを使えるため、楽に取得できる。

次回は、コンテンツのURIを取得する。

UPnPによるDLNAサーバの探索

DLNAサーバ(おうちクラウドDIGA)にある音楽ファイルを、スマホで操作してGoogle Homeで再生したい。

スマホDLNAクライアントアプリを入れて、スマホGoogle Homeにキャストしてみたところ、再生はできるものの、アプリのせいなのかわからないが途中で止まる。
またスマホで別の操作をしていると(ゲームとか)、その音がGoogle Homeから流れ出てしまう。

これはよろしくない。

ということで、次のような構成で音楽を再生できるようにすることを目標として、いろいろ調べてみることにした。

f:id:phonypianist:20190130005558p:plain
最終構成(予定)

まずは、音楽データがあるDLNAサーバ(DMS)と通信できる必要がある。

ここの記事を参考にさせてもらい、Python 3.7でローカルネットワーク上のDLNAサーバを探すスクリプトを作って実行してみた。
DLNAってなんじゃらほい? - SSDPを喋ってみる - - 初老のボケ防止日記

import socket

timeout = 10

msearch_request_lines = (
    'M-SEARCH * HTTP/1.1',
    'HOST: 239.255.255.250:1900',
    'MAN: "ssdp:discover"',
    f'MX: {timeout}',
    'ST:ssdp:all',
    '',
    ''
)

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(timeout)
msearch_request_body = '\r\n'.join(msearch_request_lines)
sock.sendto(msearch_request_body.encode('utf-8'), ('239.255.255.250', 1900))
while True:
    try:
        res, device = sock.recvfrom(4096)
        print(f'>>>>>>>>>> from {device} <<<<<<<<<<')
        print(res.decode('utf-8'))
    except socket.timeout:
        break

sock.close()

すると、やたらいっぱい応答が返ってきた。
その一部を載せておく。

>>>>>>>>>> from ('xxx.xxx.xxx.1', 1900) <<<<<<<<<<
HTTP/1.1 200 OK
CACHE-CONTROL: max-age=120
ST: upnp:rootdevice
USN: uuid:bc329e00-1dd8-11b2-8601-84afec3998c1::upnp:rootdevice
EXT:
SERVER: SDK 4.3.0.0 UPnP/1.0 MiniUPnPd/1.6
LOCATION: http://xxx.xxx.xxx.1:57323/WFADeviceDesc.xml


>>>>>>>>>> from ('xxx.xxx.xxx.1', 1900) <<<<<<<<<<
HTTP/1.1 200 OK
CACHE-CONTROL: max-age=120
ST: urn:schemas-wifialliance-org:device:WFADevice:1
USN: uuid:bc329e00-1dd8-11b2-8601-84afec3998c1::urn:schemas-wifialliance-org:device:WFADevice:1
EXT:
SERVER: SDK 4.3.0.0 UPnP/1.0 MiniUPnPd/1.6
LOCATION: http://xxx.xxx.xxx.1:57323/WFADeviceDesc.xml


>>>>>>>>>> from ('xxx.xxx.xxx.25', 1900) <<<<<<<<<<
HTTP/1.1 200 OK
CACHE-CONTROL: max-age=1800
DATE: Tue, 29 Jan 2019 15:04:54 GMT
EXT:
LOCATION: http://xxx.xxx.xxx.25:60607/D8AFF1C0AF18/Server1/ddd
SERVER: Linux/4.0 UPnP/1.0 Panasonic-UPnP-MW/1.0
ST: urn:schemas-upnp-org:service:RenderingControl:1
USN: uuid:4D454930-0300-1000-8000-D8AFF1C0AF18::urn:schemas-upnp-org:service:RenderingControl:1


>>>>>>>>>> from ('xxx.xxx.xxx.25', 1900) <<<<<<<<<<
HTTP/1.1 200 OK
CACHE-CONTROL: max-age=1800
DATE: Tue, 29 Jan 2019 15:04:54 GMT
EXT:
LOCATION: http://xxx.xxx.xxx.25:60607/D8AFF1C0AF18/Server1/ddd
SERVER: Linux/4.0 UPnP/1.0 Panasonic-UPnP-MW/1.0
ST: urn:schemas-upnp-org:service:ConnectionManager:1
USN: uuid:4D454930-0300-1000-8000-D8AFF1C0AF18::urn:schemas-upnp-org:service:ConnectionManager:1


>>>>>>>>>> from ('xxx.xxx.xxx.25', 1900) <<<<<<<<<<
HTTP/1.1 200 OK
CACHE-CONTROL: max-age=1800
DATE: Tue, 29 Jan 2019 15:04:52 GMT
EXT:
LOCATION: http://xxx.xxx.xxx:60606/D8AFF1C0AF18/Server0/ddd
SERVER: Linux/4.0 UPnP/1.0 Panasonic-UPnP-MW/1.0
ST: urn:schemas-upnp-org:service:ContentDirectory:2
USN: uuid:4D454930-0100-1000-8000-D8AFF1C0AF18::urn:schemas-upnp-org:service:ContentDirectory:2

・・・

xxx.xxx.xxx.1というのは無線ルータ。UPnPで応答するものはすべて何かしら返してくる様子。
音楽データがあるDLNAサーバはxxx.xxx.xxx.25。これからも複数の応答が返ってきている。役割毎に応答を返している様子。

今回はDLNAサーバにある音楽データを取得したいので、応答のUSNにContentDirectoryを含むものが対象。
上記の応答結果だと一番下に記載しているものが該当する。

とりあえずDLNAサーバの検知はできた。

次は、応答のLOCATIONに書かれているURIを辿って、実際の音楽データのURIを取得してみる予定。 →コンテンツの中身の探索

Raspberry PiにPython 3.7をインストール

Raspberry Pi(3 Model B+)にOSが入ったところで、自作プログラムを動かすための準備に取り掛かる。

とりあえずPython3.7をインストールする。

まずは、ビルドに必要なパッケージをインストール。  

sudo apt-get install build-essential tk-dev libncurses5-dev libncursesw5-dev libreadline6-dev libdb5.3-dev libgdbm-dev libsqlite3-dev libssl-dev libbz2-dev libexpat1-dev liblzma-dev zlib1g-dev libffi-dev libc6-dev

Python 3.7.2のダウンロードとビルド、インストール。

cd /tmp
wget https://www.python.org/ftp/python/3.7.2/Python-3.7.2.tar.xz
tar zxvf Python-3.7.2.tgz
cd Python-3.7.2
./configure --prefix=/usr/local
sudo make
sudo make install

ここで sudo make install のときに次のようなエラーメッセージが出た。

"ModuleNotFoundError: No module named '_ctypes'"

どうやら、ビルドに必要なパッケージのインストールでlibffi-devのインストールができていなかったのが問題だった様子。
再度libffi-devをインストールして sudo make install すれば、無事Pythonがインストールできた。

-Vオプションを付けて実行して、インストールできたか確認。

python3 -V
pip3 -V

Pythonのインストールは以上。

Raspberry Pi開封

 

家にDIGAのDLNAサーバがあるので、何かのマシン上でDLNAクライアントを動作させ、Google Homeで音を鳴らすのをやってみたい。

という動機から、今更だが、Raspberry Piを初めて購入してみた。

Raspberry Pi上でDLNAクライアントを動作させることが当面の目標。

備忘録のためにも、ここに記録しておく。

 

2019/1/23現在、正規代理店のサイトであるRS-onlineでBasic Setが完売になってしまっていたため、Amazonで購入。Raspberry Pi 3 Model B+。

www.amazon.co.jp

 

で、届いたのがこれ。 

f:id:phonypianist:20190125224721j:plain

Raspberry Pi

クリアケース付き、ヒートシンク付き、電源付きで、軽く始めるにはちょうどよい。

こう見ると、コネクタがやたらでかく感じる。

f:id:phonypianist:20190125224758j:plain

Raspberry Pi箱の中身

ケースに入れるとこんな感じ。

f:id:phonypianist:20190125224925j:plain

Raspberryをケースに入れる

 

手持ちのmicroSDカードにOSイメージを書き込んで、Raspberry Piに差し込んだら、簡単に起動した。

最初の手順はいろんな記事がすでに世の中にあるので省略。

OSが入ったところで、次の作業に移る。