PSPのインターネットラジオ機能で遊んでみた
昨日ふとファームをアップデートしたら、インターネットラジオ機能が追加されていた。試してみたところ、どうやらブラウザ上で動いているようだ。「□」ボタンを押すと、アドレスバーにURLも表示される。
アドレスバーにある URL のページにアクセスしソースを読んでみたら、ラジオの再生には PSP ブラウザの拡張機能を利用していることが分かったので、API を解読して、その成果を XMLHTTPRequest もどきのインターフェイスにしてみた。
参考資料:「PSP™ (PlayStation® Portable) インターネットブラウザ向け コンテンツ作成ガイドライン Version 3.80」*1
<html> <head> <title>test</title> <script language="JavaScript"> var __psp__mimeType_pluginExt= 'application/x-psp-extplugin'; var __psp__default_userAgent = 'PSP-InternetRadioPlayer/1.00'; (function() { var plugin_stat = navigator.mimeTypes[__psp__mimeType_pluginExt]; if (plugin_stat && plugin_stat.enabledPlugin) { document.write('<object name="__psp__pluginExt" type="' + __psp__mimeType_pluginExt + '"></object>'); } })(); var __psp__initPluginExt = function() { if (!this.__psp__pluginExt) return; this.__psp__pluginExt.sysRadioSetMasterVolume(0); this.__psp__pluginExt.sysRadioSetSubVolume(0); this.__psp__pluginExt.sysRadioBackLightAlwaysOn(0); this.__psp__pluginExt.sysRadioSetDebugMode(1); this.__psp__pluginExt.sysRadioSetDebugLogTextStyle( // RGBA 224,224,224,255, // foreground (1) 255,255,255,255, // foreground (2) 30,30,40,255, // background 1, 0, 1); this.__psp__initPluginExt = null; }; var XMLHTTPRequest = function() { if (window.__psp__initPluginExt) window.__psp__initPluginExt(); this.userAgent = __psp__default_userAgent; this.opened = false; this.bufferSize = 65536; this.async = false; this.readyState = 0; this.status = 0; this.responseText = null; this.onreadystatechange = null; this.onerror = null; this.onprogress = null; this.__error = false; this.__url = null; this.__poller = 0; }; XMLHTTPRequest.prototype = { open: function(method, url, async) { if (!window.__psp__pluginExt) return false; if (this.readyState == 1) // now loading return false; if (this.__poller) // assertion; should be 0 at this time return false; method = method.toUpperCase(); if (method != 'GET') return false; // currently GET is the only supported method this.async = async; this.readyState = 0; // 0 = uninitialized this.responseText = null; this.status = 0; this.__url = url; this.__error = false; this.__poller = 0; return true; }, send: function(body) { if (this.__url == null) return false; if (this.__poller) // assertion; should be 0 at this time return false; this.readyState = 1; // 1 = loading this.__error = false; var self = this; this.__poller = setInterval( function() { switch (window.__psp__pluginExt.sysRadioGetHttpGetStatus()) { case -1: // something went wrong self.readyState = 4; self.__error = 1; // 1 = generic failure clearInterval(self.__poller); self.__poller = 0; window.__psp__pluginExt.sysRadioHttpGetTerminate(); self.status = 500; if (self.onreadystatechange) self.onreadystatechange({ target: self }); if (self.onerror) self.onerror({ target: self }); break; case 0: // loaded self.readyState = 4; // complete self.__error = 0; // 0 = success clearInterval(self.__poller); self.__poller = 0; self.responseText = window.__psp__pluginExt.sysRadioGetHttpGetResult(); window.__psp__pluginExt.sysRadioHttpGetTerminate(); self.status = 200; if (self.onreadystatechange) self.onreadystatechange({ target: self }); if (self.onload) self.onload({ target: self }); break; case 1: // loading if (self.onprogress) { self.onprogress({ target: self, position: -1, totalSize: -1 }); } break; } }, 100 ); // initiate the request window.__psp__pluginExt.sysRadioPrepareForHttpGet( this.__url, this.userAgent, this.bufferSize); if (this.onreadystatechange) this.onreadystatechange(this.readyState); if (!this.async) { while (this.readyState == 1); } return true; }, abort: function() { if (this.readyState != 1 && this.__poller == 0) return false; clearInterval(this.__poller); window.__psp__pluginExt.sysRadioHttpGetTerminate(); this.readyState = 0; this.__poller = 0; this.__error = -1; // -1 = aborted }, setRequestHeader: function(name, val) { return false; // not implemented } }; </script> <script language="JavaScript"> var testXMLHttpRequestAsync = function() { var xhr = new XMLHTTPRequest(); xhr.open('GET', 'http://www.google.com/', true); xhr.onreadystatechange = function(e) { if (xhr.readyState == 4) { if (xhr.status == 200) alert(xhr.responseText); else alert('FAIL'); } }; xhr.onprogress = function(e) { } xhr.send(null); } window.onload = function() { testXMLHttpRequestAsync(); }; </script> </head> <body> test. </body> </html>
ちなみに、こいつはそのまま「インターネットブラウザ」機能で読み込んでも動いてくれない。
あくまで「インターネットラジオ」メニューから開かなくてはいけない。
- メモリースティックをマウントする。
- PSP/RADIOPLAYER/InternetRadioPlayerI.prs があることを確認する。
- 2. の InternetRadioPlayerI.prs を複製し、InternetRadioPlayerII.prs (拡張子が .prs ならなんでもよい) を同じディレクトリ配下に作成する。
- バイナリエディタで InternetRadioPlayerII.prs を開き、URL の部分を適当に書き換える。余った部分は NUL で埋める。
0x00 | BYTE[4] | magic code "PRSF" |
0x04 | DWORD LE | file version? |
0x08 | DWORD LE | size of the header |
0x0c | DWORD LE | size of the descriptor block |
0x10 | DWORD LE | offset to the property block? (from the beginning of the file) |
0x18 | DWORD LE | offset to the property block? (from the beginning of the file) |
0x1c | DWORD LE | size of the property block |
0x20 | DWORD LE | offset to the icon image? (from the beginning of the file) |
0x28 | DWORD LE | offset to the icon image? (from the beginning of the file) |
0x30 | DWORD LE | offset to the icon image? (from the beginning of the file) |
0x34 | DWORD LE | size of the icon image block |
0x38-0x3f | NUL | padding |
0x68 | DWORD LE | size of the icon image |
0x6c | DWORD LE | offset to the title chunk (from the beginning of the property block) |
0x70 | DWORD LE | number of components * 2 + 1 |
0x74 | DWORD LE | offset to the value (string is encoded in UTF-8) |
0x78 | DWORD LE | size of the value (in bytes) |
0x7c | DWORD LE | offset to the information chunk (from the beginning of the property block) |
0x80 | DWORD LE | number of component * 2 + 1 |
0x84 | DWORD LE | 0 |
0x88-0x97 | DWORD LE[4] | -1, -1, -1, -1 |
0x98 | DWORD LE | offset to the comment chunk (from the beginning of the property block) |
0x9c | DWORD LE | number of components * 2 + 1 |
0xa0 | DWORD LE | offset to the value |
0xa4 | DWORD LE | size of the value |
0xa8 | DWORD LE | offset to the copyright chunk (from the beginning of the property block) |
0xac | DWORD LE | number of components * 2 + 1 |
0xb0 | DWORD LE | offset to the value |
0xb4 | DWORD LE | size of the value |
0xb8 | DWORD LE | offset to the author chunk (from the beginning of the property block) |
0xbc | DWORD LE | number of components * 2 + 1 |
0xc0 | DWORD LE | offset to the value |
0xc4 | DWORD LE | size of the value |
0xc8 | DWORD LE | offset to the radioplayer_url chunk (from the beginning of the property block) |
0xcc | DWORD LE | number of components * 2 + 1 |
0xd0 | DWORD LE | offset to the value |
0xd4 | DWORD LE | size of the value |
0xd8 | DWORD LE | offset to the homepage_url chunk (from the beginning of the property block) |
0xdc | DWORD LE | number of components * 2 + 1 |
0xe0 | DWORD LE | offset to the value |
0xe4 | DWORD LE | size of the value |
0xe8 | DWORD LE | offset to the version chunk (from the beginning of the property block) |
0xec | DWORD LE | number of components * 2 + 1 |
0xf0 | DWORD LE | offset to the value |
0xf4 | DWORD LE | size of the value |
0xf8-0xff | NUL | padding |
追記: 一部オフセットに誤りがありました。
*1:どうも正式に公開しているものではなさそうだけど、playstation.comでサイト内検索をやると出てくるのでいいんだろう :p