PSPのインターネットラジオ機能で遊んでみた

昨日ふとファームをアップデートしたら、インターネットラジオ機能が追加されていた。試してみたところ、どうやらブラウザ上で動いているようだ。「□」ボタンを押すと、アドレスバーにURLも表示される。

アドレスバーにある URL のページにアクセスしソースを読んでみたら、ラジオの再生には PSP ブラウザの拡張機能を利用していることが分かったので、API を解読して、その成果を XMLHTTPRequest もどきのインターフェイスにしてみた。

f:id:moriyoshi:20080119145112j:image:w320

参考資料:「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>

ちなみに、こいつはそのまま「インターネットブラウザ」機能で読み込んでも動いてくれない。
あくまで「インターネットラジオ」メニューから開かなくてはいけない。

  1. メモリースティックをマウントする。
  2. PSP/RADIOPLAYER/InternetRadioPlayerI.prs があることを確認する。
  3. 2. の InternetRadioPlayerI.prs を複製し、InternetRadioPlayerII.prs (拡張子が .prs ならなんでもよい) を同じディレクトリ配下に作成する。
  4. バイナリエディタで 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