Radiko(ESP32S3編)

最近ESP32S3で遊び始めたのですが、以前製作したRadikoを聞くがESP32-audioI2Sというライブラリーを使用すると非常に簡単に出来る事が分かりました。ESP32-audioI2S の説明にホストにつないで再生する例として

// audio.connecttohost(“http://26373.live.streamtheworld.com:3690/XHQQ_FMAAC/HLSTS/playlist.m3u8”);

とm3u8形式が有ります。Radikoもこの形式です。Radikoを聞くを製作当時、m3u8形式をESP32で再生する方法が分からず、仕方なくAACファイルの再生で対応していました。

ハードウェア

ESP32-audioI2SはデジタルオーディオDACデコーダとしてI2Sモジュールを使用します。よって最小構成は本体とI2Sモジュールの2つでOKです。

PCM5102の表と裏のパッドを説明通りにショートすれば下記の様に3本配線するだけでOKです。またI2Sのポートはソフトで指定出来ます。今回はGPIO18,17,08を使用していますが変更も可能です。

PCM5102A ESP32S3 
LCKGPIO17
DINGPIO18
BCKGPIO08

この構成で音はPCM5102のイヤフォンジャックから確認出来ます。もしスピーカーが良いならダイソーで売っているミニスピーカーをイヤフォンジャックにさせばスピーカーから聞けます。

ソフトウェア

先ずはライブラリー ”esp32-audioi2s” を追加して下さい。Arduino->ツールー>ライブラリの管理と進み、検索欄に”esp32-audioi2s”と入力して下さい。esp32-audioi2sが表示されるのでそれをインストール。

ESP32-audioI2Sの例を今回の回路に合わせて書き換えて実行するとサイトにつながって再生が始まりました。(実は拡張子m3u8のURLにはアクセス出来ませんでした。でもm3uもほとんどm3u8と同じ。サンプルのm3u8サイトはもう存在しない為にアクセス出来なかったと判断)

test.ino

#include "Arduino.h"
#include "Audio.h"
#include "WiFi.h"

//----------PCM5102 (I2S) ------------
#define   I2S_BCLK    8   
#define   I2S_LRC    17  
#define   I2S_DOUT   18  

Audio audio;

String ssid =     "xxxxxxx";
String password = "yyyyyyy";

void setup()
{
    Serial.begin(115200);

    WiFi.disconnect();
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid.c_str(), password.c_str());
    while (WiFi.status() != WL_CONNECTED) delay(1500);
    
    audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
    audio.setVolume(12); // default 0...21
    audio.connecttohost("http://www.wdr.de/wdrlive/media/einslive.m3u");
}

void loop() {
    audio.loop();
    vTaskDelay(1);
}

これでconnecttohost()関数にURLを指定するればそのサイトにアクセス出来る事が分かりました。しかしRadikoは、アクセス時ヘッダーに”X-Radiko-AuthToken: 認証キー”を追加する必要が有ります。これを追加する方法は無いかと探しているとconnecttohost()関数に相手左記にユーザIDとPassWordを送る機能が有ることが分かりました。その場合の書式は以下の通り。

bool Audio::connecttohost(const char* host, const char* user, const char* pwd)

第2,3引数にユーザID、Passwordを文字列で渡す様です。この処理はAudio.ccpの約433行目の下記で行われています。

Audio.ccp

bool Audio::connecttohost(const char* host, const char* user, const char* pwd) { // user and pwd for authentification only, can be empty

    bool     res           = false; // return value
    char*    c_host        = NULL;  // copy of host
    uint16_t lenHost       = 0;     // length of hostname
    uint16_t port          = 0;     // port number
    uint16_t authLen       = 0;     // length of authorization
    int16_t  pos_slash     = 0;     // position of "/" in hostname
    int16_t  pos_colon     = 0;     // position of ":" in hostname
    int16_t  pos_ampersand = 0;     // position of "&" in hostname
    uint32_t timestamp     = 0;     // timeout surveillance
    uint16_t hostwoext_begin = 0;

    // char*    authorization = NULL;  // authorization
    char*    rqh           = NULL;  // request header
    char*    toEncode      = NULL;  // temporary memory for base64 encoding
    char*    h_host        = NULL;

//    https://edge.live.mp3.mdn.newmedia.nacamar.net:8000/ps-charivariwb/livestream.mp3;&user=ps-charivariwb;&pwd=ps-charivariwb-------
//        |   |                                     |    |                              |
//        |   |                                     |    |                              |             (query string)
//    ssl?|   |<-----host without extension-------->|port|<----- --extension----------->|<-first parameter->|<-second parameter->.......

    xSemaphoreTakeRecursive(mutex_playAudioData, 0.3 * configTICK_RATE_HZ);

    // optional basic authorization
    if(user && pwd) authLen = strlen(user) + strlen(pwd);
    char authorization[base64_encode_expected_len(authLen + 1) + 1];
    authorization[0] = '\0';
    if(authLen > 0) {
        char toEncode[authLen + 4];
        strcpy(toEncode, user);
        strcat(toEncode, ":");
        strcat(toEncode, pwd);
        b64encode((const char*)toEncode, strlen(toEncode), authorization);
    }

    if (host == NULL)              { AUDIO_INFO("Hostaddress is empty");     stopSong(); goto exit;}
    if (strlen(host) > 2048)       { AUDIO_INFO("Hostaddress is too long");  stopSong(); goto exit;} // max length in Chrome DevTools

    c_host = x_ps_strdup(host); // make a copy
    h_host = urlencode(c_host, true);

    trim(h_host);  // remove leading and trailing spaces
    lenHost = strlen(h_host);

    if(!startsWith(h_host, "http")) { AUDIO_INFO("Hostaddress is not valid"); stopSong(); goto exit;}

    if(startsWith(h_host, "https")) {m_f_ssl = true;  hostwoext_begin = 8; port = 443;}
    else                            {m_f_ssl = false; hostwoext_begin = 7; port = 80;}

    // In the URL there may be an extension, like noisefm.ru:8000/play.m3u&t=.m3u
    pos_slash     = indexOf(h_host, "/", 10); // position of "/" in hostname
    pos_colon     = indexOf(h_host, ":", 10); if(isalpha(c_host[pos_colon + 1])) pos_colon = -1; // no portnumber follows
    pos_ampersand = indexOf(h_host, "&", 10); // position of "&" in hostname

    if(pos_slash > 0) h_host[pos_slash] = '\0';
    if((pos_colon > 0) && ((pos_ampersand == -1) || (pos_ampersand > pos_colon))) {
        port = atoi(c_host + pos_colon + 1);   // Get portnumber as integer
        h_host[pos_colon] = '\0';
    }
    setDefaults();
    rqh = x_ps_calloc(lenHost + strlen(authorization) + 300, 1); // http request header
    if(!rqh) {AUDIO_INFO("out of memory"); stopSong(); goto exit;}

                       strcat(rqh, "GET /");
    if(pos_slash > 0){ strcat(rqh, h_host + pos_slash + 1);}
                       strcat(rqh, " HTTP/1.1\r\n");
                       strcat(rqh, "Host: ");
                       strcat(rqh, h_host + hostwoext_begin);
                       strcat(rqh, "\r\n");
                       strcat(rqh, "Icy-MetaData:1\r\n");
                       strcat(rqh, "Icy-MetaData:2\r\n");
                       strcat(rqh, "Accept:*/*\r\n");
                       strcat(rqh, "User-Agent: VLC/3.0.21 LibVLC/3.0.21\r\n");
    if(authLen > 0) {  strcat(rqh, "Authorization: Basic ");
                       strcat(rqh, authorization);
                       strcat(rqh, "\r\n"); }
                       strcat(rqh, "Accept-Encoding: identity;q=1,*;q=0\r\n");
                       strcat(rqh, "Connection: keep-alive\r\n\r\n");

    if(m_f_ssl) { _client = static_cast(&clientsecure);}
    else        { _client = static_cast(&client); }

    timestamp = millis();
    _client->setTimeout(m_f_ssl ? m_timeout_ms_ssl : m_timeout_ms);
  • 459から468行: ユーザIDとPassWordがBase64変換されauthorization配列に代入される。
  • 495行: ヘッダー用配列を定義
  • 508から510行: ここで、 authorizationがヘッダーに追加される。

そこで、

  • 495行: ヘッダー用配列を念の為 200増やす
  • 508から518行: ユーザーIDに”ー”が指定された場合、ヘッダーに"X-Radiko-AuthToken: pwd"が追加され
              それ以外は通常通り。
Audio.ccp

    rqh = x_ps_calloc(lenHost + strlen(authorization) + 300 + 200, 1); // http request header
    if(!rqh) {AUDIO_INFO("out of memory"); stopSong(); goto exit;}

                       strcat(rqh, "GET /");
    if(pos_slash > 0){ strcat(rqh, h_host + pos_slash + 1);}
                       strcat(rqh, " HTTP/1.1\r\n");
                       strcat(rqh, "Host: ");
                       strcat(rqh, h_host + hostwoext_begin);
                       strcat(rqh, "\r\n");
                       strcat(rqh, "Icy-MetaData:1\r\n");
                       strcat(rqh, "Icy-MetaData:2\r\n");
                       strcat(rqh, "Accept:*/*\r\n");
                       strcat(rqh, "User-Agent: VLC/3.0.21 LibVLC/3.0.21\r\n");
    if(authLen > 0) {  
      if(user == "-"){
                       strcat(rqh, "X-Radiko-AuthToken: ");
                       strcat(rqh, pwd);
      }
      else{
                       strcat(rqh, "Authorization: Basic ");
                       strcat(rqh, authorization);}
      }                   
                       strcat(rqh, "\r\n"); 
    }
                       strcat(rqh, "Accept-Encoding: identity;q=1,*;q=0\r\n");
                       strcat(rqh, "Connection: keep-alive\r\n\r\n");

    if(m_f_ssl) { _client = static_cast(&clientsecure);}
    else        { _client = static_cast(&client); }

    timestamp = millis();
    _client->setTimeout(m_f_ssl ? m_timeout_ms_ssl : m_timeout_ms);

Arduinoライブラリーの中に有る”Audio.ccp”を上記の様に変更すれば connecttohost(“radiko_URL”, “-“, “認証番号”)でRadikoにアクセス出来ます。

Radiko認証キーの取得はRadikoを聞く(Arduino編)ーRadiko Playerの製作ーと同じです。radiko_02.inoの212行にRadiko(byte flg, String ID)関数が有ります。このpart3まで実行すると、認証キーとアクセス可能なラジオ局のリストが作成されます。

今回の回路に合わせて前回のプログラムを書き換えると下記の様になります。

radiko_s3.ino

#include <Arduino.h>
#include <WiFi.h>
#include <string.h>
#include "mbedtls/base64.h"
#include <HTTPClient.h>

#include <WebServer.h>
#include "SPIFFS.h"

#include "Audio.h"
Audio audio;

#define BUFF_MAX      34000

//----------PCM5102 (I2S) ------------
#define   bclk    8   
#define   wclk    17  
#define   dout    18  

// Enter your WiFi setup here:
const char *SSID = "xxxxxxxx";
const char *PASSWORD = "yyyyyyyy";

IPAddress ip(192, 168, 3, 250);           // IP Address
IPAddress gateway(192,168, 3, 1);         // Gateway Address
IPAddress dns(192,168, 3, 1);             // Gateway Address
IPAddress subnet(255, 255, 255, 0);       // Subnet Address

WebServer _server(80);
String _index_top =
                  "<!DOCTYPE html>\n<html>\n<head>\n<meta charset='utf-8'>\n<meta name='viewport' content='width=device-width,initial-scale=1'>\n"
                  "<link rel='stylesheet' type='text/css' href='/radiko.css' >\n<title>Radiko</title>\n</head>\n"
                  "<body>\n<center>\n<div class='b_frame'>\n<br>\n<div class='t_font'><u>Radiko 1.0</u></div>\n<br>\n"
                  "<form method='get' id='abc'>\n<input name='1' id='123' style='display:none'>\n</form>\n"
                  "<form oninput='op.value=ab.value'>\n<span class='v_font'>Volume</span><br>\n"
                  "<input style='width:220px;' type='range' value='";
                  
uint8_t  _st_selected, _station_MAX, _volume;
bool _flg_play, _flg_mute, _flg_auto;
char _url[200], _auth[50];
String  st_list[2], _auth_token;
void setup()
{
   //-----------------  part0 ------------------------------------------------------------------------------
    Serial.begin(115200);
    delay(500);

    WiFi.disconnect();
    WiFi.softAPdisconnect(true);
    delay(500);
    
    WiFi.mode(WIFI_STA);
    WiFi.config(ip, gateway, subnet, dns);
    WiFi.begin(SSID, PASSWORD);
    while (WiFi.status() != WL_CONNECTED)
    {
      Serial.println("...Connecting to WiFi");
      delay(1000);
    }
    Serial.println("Connected");
    Serial.println(SSID);
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());

    //-----------------  HTTP Server & SPIFFS ----------------------------------------------------------------------
    _server.on("/", handleRoot);
    _server.onNotFound(handleWebRequests);
    _server.begin();
    Serial.println("HTTP server started");

    Serial.println("SPIFFS started");
    SPIFFS.begin();

    Serial.print("I2S Setup");
    audio.setPinout(bclk, wclk, dout);
    audio.setVolumeSteps(100);
    
    Read_state();
    Radiko_Key(_auth_token, st_list);
    
    _flg_mute = false;
    audio.setVolume(_volume); 
    
    if(_flg_auto) {
      _flg_play = true;
      Radiko_play(_st_selected);
    }
    else _flg_play = false;

}

void loop()
{
    _server.handleClient();

    if(_flg_play){
      if (audio.isRunning()) audio.loop();
      else audio.connecttohost(_url,"-", _auth);
    }
}

void handleRoot()
{
  String cmd,str;
  int a,flg;
  
    flg = 1;
    cmd = _server.argName(0);
    switch(cmd.toInt())
    {
      case 1: // Play, Mute, Auto, Save
                cmd = _server.arg("1");
                str = cmd.substring(2);
                a = str.toInt();
                flg = 0;
                switch(cmd.charAt(0))
                {
                  case '1': // volume data
                            _volume = a;
                            audio.setVolume(_volume); 
                            break;
                            
                  case '2': // station No
                            _st_selected = a;
                            Radiko_stop();
                            delay(50);
                            Radiko_play(_st_selected);
                            break;
                            
                  case '3': // play
                            if(!_flg_play) Radiko_play(_st_selected);
                            break;

                  case '4': // mute
                            _flg_mute = !_flg_mute;
                            a = _volume;
                            if(_flg_mute) a = 5; 
                            audio.setVolume(a);
                            break;

                  case '5': // auto flg
                            _flg_auto = !_flg_auto;
                            break;

                  case '6': // Save
                            Save_state();
                            break;
                }
                break;

      case 2: //  Stop
                Radiko_stop();
                break;
    }

    if(flg)
    {
      str = Make_index(str);
      _server.send(200, "text/html",str);
    }
    else _server.send(204, "text/html","");
}

void Radiko_play(uint8_t _st_selected){
  int a;
  String str;
  
    Radiko_Key(_auth_token, st_list);
    str = "https://f-radiko.smartstream.ne.jp/" + get_st_name(_st_selected, st_list[0]) + "/_definst_/simul-stream.stream/playlist.m3u8";
    for(a = 0; a < str.length(); a ++) _url[a] = str[a];
    _url[a] = 0;
    for(a = 0; a < _auth_token.length(); a ++) _auth[a] = _auth_token[a];
    _auth[a] = 0;
    audio.connecttohost(_url,"-", _auth);
    _flg_play = true;  
}

void Radiko_stop()
{
    if(_flg_play) audio.stopSong();
    _flg_play = false;
}

void Radiko_Key(String& _auth_token, String* st_list)
{
  char * auth_key = "bcd151073c03b352e1ef2fd66c32209da9ca0afa" ;
  HTTPClient http;
  int key_length, offset;
  char base64_encoded[32],token_p[32];
  size_t base64_len;
  const char *headerKeys[] = {"X-Radiko-AuthToken", "X-Radiko-KeyLength", "X-Radiko-KeyOffset"};
  String str;
  int a,b,c;
  WiFiClient * stream;
  uint8_t* _sound_buff;

    _sound_buff = (uint8_t*)malloc(BUFF_MAX + 5);
    //-----------------  part1 ------------------------------------------------------------------------------
    http.begin("https://radiko.jp/v2/api/auth1");
    http.addHeader("User-Agent", "esp32/4");
    http.addHeader("Accept", "*/*");
    http.addHeader("X-Radiko-App", "pc_html5");
    http.addHeader("X-Radiko-App-Version", "0.0.1");
    http.addHeader("X-Radiko-User", "dummy_user");
    http.addHeader("X-Radiko-Device", "pc");
    http.collectHeaders(headerKeys, 3);
    Serial.println("part1 = " + String(http.GET()));
    
    offset = String(http.header(headerKeys[2])).toInt();
    key_length = String(http.header(headerKeys[1])).toInt();
    _auth_token = http.header(headerKeys[0]);
    http.end();

    token_p[0] = '\0';
    strncpy(token_p, &auth_key[offset], key_length);
    token_p[key_length] = '\0';
    mbedtls_base64_encode((unsigned char *) base64_encoded, sizeof(base64_encoded),&base64_len, (unsigned char *) token_p, strlen(token_p));

    //-----------------  part2 ------------------------------------------------------------------------------
    http.begin("https://radiko.jp/v2/api/auth2");
    http.addHeader("X-Radiko-AuthToken", _auth_token);
    http.addHeader("X-Radiko-Partialkey", base64_encoded);
    http.addHeader("X-Radiko-User", "dummy_user");
    http.addHeader("X-Radiko-Device", "pc");
    Serial.println("part2 = " + String(http.GET()));
    
    str = http.getString();
    str = str.substring(0,str.indexOf(","));
    http.end();
  
    //-----------------  part3 ------------------------------------------------------------------------------
    //    http.begin("https://radiko.jp/v2/station/list/[ID].xml");
    str = "https://radiko.jp/v2/station/list/" + str + ".xml";
    http.begin(str);
    http.addHeader("X-Radiko-AuthToken", _auth_token);
    Serial.println("part3 = " + String(http.GET()));
      
    b = http.getSize();
    c = 0;
    stream = http.getStreamPtr();
    while(b)
    {
      while(!(a = stream->available())) NOP();
      stream->readBytes(&_sound_buff[c], a);
      b -= a;
      c += a;
    }
    _sound_buff[c] = 0;
    str = String((char *)_sound_buff);    

    b = 0;
    st_list[0] = st_list[1] = "";
    while((a = str.indexOf("<id>")) != -1)
    {
      a +=  4;
      str = &str[a];
      a = str.indexOf("</id>");
      st_list[0] += str.substring(0,a);
      st_list[0] += "\n";
      a = a + 16;
      str = &str[a];
      a = str.indexOf("</name>");
      st_list[1] += str.substring(0,a);
      st_list[1] += "\n";
      b ++;
    }
    _station_MAX = b;
    for(a = 0; a < b; a ++)
      Serial.println("No." + String(a) +" ID: " + get_st_name(a, st_list[0]) + " , name: " + get_st_name(a, st_list[1]));
    Serial.println("station_MAX = " + String(b));
    
    http.end();
    free(_sound_buff);
}

String get_st_name(int num, String str){
  int a;

    if(num > -1){
      a = 0;
      while(num){
        while(str[a] != 0xa) a ++;  
        a ++;
        num --;        
      }
      num = a;
      a ++;
      while(str[a] != 0xa) a ++;
      str = str.substring(num,a);
    }
    else str = "";
    return(str);
}

String Make_index(String str)
{
  int a;
  
    str = String(_volume);
    str += "' id='vol' name='ab' onchange='set_data(1)'>\n<output name='op' class='v_font'>";
    str += String(_volume);
    str += "</output>\n<br>\n<form>\n<select id='_title' class='m_font' onchange='set_data(2)'>\n";
    
    for(a = 0; a < _station_MAX; a ++)
    {
      str += "<option value='";
      str += (String(a) + "'");
      if(a == _st_selected) str += " selected>";
      else str += ">";
      str += get_st_name(a, st_list[1]);
      str += "</option>\n";
    }

    str += "</select>\n</form>\n<br>\n<form method='get'>\n<button type='button' id='_play' onclick='set_data(3)' ";
    if(_flg_play) str += "style ='background: red;'";
    str += ">Play</button>\n<button type='button' id='_mute' onclick='set_data(4)' ";
    if(_flg_mute) str += "style ='background: red;'";
    
    str += ">Mute</button>\n<button type='submit' name='2' >Stop</button>\n<br>\n<span class='v_font' style ='margin-left:5%;'>Auto Start :</span>\n";
    str += "<input type='radio' id='_auto' style ='margin-left:5%;' onclick='set_data(5)'";
    if(_flg_auto) str += " checked ";
    str += "/>\n<button type='button' style ='margin-left:16%;' onclick='set_data(6)'";
    str += ">Save</button>\n</form>\n</div>\n</center>\n<script src='./radiko.js'></script>\n</body>\n</html>";

    str = _index_top + str;
    
    return str;
}

void handleWebRequests()
{
  String dataType = "text/plain";
  String path;
  File dataFile;

    path = _server.uri();
    if(path.endsWith(".txt")) dataType = "text/plain";
    else if(path.endsWith(".css")) dataType = "text/css";
    else if(path.endsWith(".js")) dataType = "application/javascript";
    else if(path.endsWith(".ico")) dataType = "image/html";
    
    dataFile = SPIFFS.open(path.c_str(), "r");
    _server.streamFile(dataFile, dataType);
    dataFile.close();
    delay(5);
}

void Save_state()
{
  String str;
  File dataFile;

    str = String(_volume) + "\n" + String(_st_selected) +"\n" + String(_flg_auto) + "\n";
    dataFile = SPIFFS.open("/state.txt", "w");
    dataFile.print(str);
    dataFile.close();
}

void Read_state()
{
  String str;
  File dataFile;

    dataFile = SPIFFS.open("/state.txt", "r");
  
    str = dataFile.readStringUntil('\n');
    _volume = str.toInt();

    str = dataFile.readStringUntil('\n');
    _st_selected = str.toInt();
    
    str = dataFile.readStringUntil('\n');
    _flg_auto = str.toInt();

    dataFile.close();
}
  • Arduinoのスケッチです。
  • 前回、認証キーとラジオ局を取得する関数 Radiko(byte flg, String ID)を Radiko_Key(String& _auth_token, String* st_list)と定義し直しています。
  • Radiko_Key(String& _auth_token, String* st_list) この関数を呼ぶと、引数_auth_tokenに認証キーが、st_listにアクセス可能なラジオ局が文字列となって帰って来ます。
  • RadikoのURLは169行で作成しています。
    • URLは “https://f-radiko.smartstream.ne.jp/ Station ID /_definst/simul-stream.stream/playlist.m3u8″ です。
    • Station IDの箇所に聞きたいラジオ局のIDを指定します。
    • ラジオ局ID取り出し用に関数get_st_name()を作成しています。
  • audio.connecttohost()関数は引数が char*型なので、169行でString型に保存したURLを170,171行でcharの配列_url[]にコピーしています。
  • 同様に認証キーもcharの配列_auth[]にコピー
  • 174行のaudio.connecttohost(_url,”-“, _auth);でサイトにアクセス開始です。
  • 92行のloop()関数
    • audio.isRunning():
      • Ture:実行中。ー>続けて再生 audio.loop()を実行
      • False:最初のm3u8リスト終了。次の演奏リスト取得ー>audio.connecttohost(_url,”-“, _auth);
    • これで連続で再生されます。

後は前回と同じです。プログラムはこれとは別に

  • ホームページで使用する
    • JAVA SCRIPT:   radiko.js
    • CSSファイル:   radiko.css
    • 初期設定ファイル: state.txt
    • faviconファイル:  favicon.ico

が有ります。全部まとめてここに保存しました。

プログラムをコンパイルして実行。ブラウザで192.168.3.250をアクセスすると

この画面が表示されます。使い方は、Radikoを聞く(Arduino編)ーRadiko Playerの製作ーと一緒です。

最後に

Radikoを聞く(Arduino編)ーRadiko Playerの製作ーに比べこれは格段に落ちなくなりました。でもたまに落ちるのですが原因は大体ネット関係です。

  • WiFIの状態を改善する。ー> ルーターとの間の障害物を無くす、 ルータに近づく
  • ネットが遅い。 ー> ネットの空いている時間にもう一度試す

この様な場合、ESP32S3にリセットがかかり再起動します。なので、Auto Startをオンし一度Saveするれば起動時に同じラジオ局を再生するので結果的に続けてラジオが聞けます。

今回のRadiko URL “https://f-radiko.smartstream.ne.jp/〜” はタイムフリー用のURLでした。再生している番組が実際のものより約3から4分おくれるのはその為と思います。また、タイムフリーは継続して1日3時間までという条件が有ります。3時間連続で聞いた事が無いので分かりませんが、多分そこで落ちると思います。