TFT_eSPIとAudioI2SでmediaPlayerを作る(6)

今回はRadikoに接続します。Radikoは接続に認証が必要になるので前回のWeb Radioとは若干違った接続方法となります。

Radiko

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

Radikoを聞く(Arduino編)ー 認証と音声データ ーにRadikoへの認証と接続方法の説明が有ります。RadikoサーバからURL ”xxxxx.m3u8” を取得しaudio.connecttohost()関数を使ってサーバに繋げばRadikoを聴く事が出来ます。

Radiko接続に必要な情報は下記のコードで取得します。

Radiko_Key(bool flg)

//----------------------------------------------------------------------------------
//                    Connect Radiko  
//----------------------------------------------------------------------------------

void Radiko_Key(bool flg){
  char * auth_key = "bcd151073c03b352e1ef2fd66c32209da9ca0afa" ;
  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, _chunk_url;
  int a,b,c;
  WiFiClient * stream;
  HTTPClient http;
  uint8_t* _sound_buff;

    _sound_buff = (uint8_t*)malloc(BUFF_MAX + 5);
    //-----------------  part1 ------------------------------------------------------------------------------
    if(flg) print_msg("Connecting to Radiko", MG_COLOR);
    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);
    str = "part1 = " + String(http.GET());
    Serial.println(str);
    if(flg) print_msg(str, MG_COLOR);
    
    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");
    str = "part2 = " + String(http.GET());
    Serial.println(str);
    if(flg) print_msg(str, MG_COLOR);
    
    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);
    str = "part3 = " + String(http.GET());
    Serial.println(str);
    if(flg) print_msg(str, MG_COLOR);

    b = http.getSize();
    c = 0;
    stream = http.getStreamPtr();

    while(b){
      while(!(a = stream->available())) NOP(); // delay(5);
      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 ++;
    }
    pl_st.st_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]));
    str = "station_MAX = " + String(b);
    Serial.println(str);
    if(flg) print_msg(str, MG_COLOR);
    
    http.end();

    free(_sound_buff);
}

この関数を実行すると AuthToken、ラジオ局のID、ラジオ局名を取得出来ます。AuthTokenはRadikoサーバーにアクセスする場合の認証キー。今回、ラジオIDと局名は下記の様になりました。

ID & Name

No.     ID              ラジオ局名
-----------------------------------------
1:      TBS             TBSラジオ
2:      QRR             文化放送
3:      LFR             ニッポン放送
4:      RN1             ラジオNIKKEI第1
5:      RN2             ラジオNIKKEI第2
6:      INT             interfm
7:      FMT             TOKYO FM
8:      FMJ             J-WAVE
9:      JORF            ラジオ日本
10:     BAYFM78         bayfm78
11:     NACK5           NACK5
12:     YFM             FMヨコハマ
13:     IBS             LuckyFM 茨城放送
14:     HOUSOU-DAIGAKU  放送大学
15:     JOAK            NHKラジオ第1(東京)
16:     JOAK-FM         NHK-FM(東京)

ラジオIDで検索用文字列st_list[0]を、ラジオ局名でst_list[1]を作成し前回同様に選択に使用します。ラジオIDとAuthTokenを持ってRadikoのサーバーにアクセスすれば、サーバから”xxxxx.m3u8”を取得出来ます。

認証キーが曲者

”xxxxx.m3u8”を引数に、関数audio.connecttohost()を使えRadikoが聴けるはずだったのですが、audio.connecttohost()でRadikoサーバに接続する場合も認証キーが必要になります。ヘッダーに”X-Radiko-AuthToken: 認証キー”を追加する必要が有り、connecttohost()関数その機能が無いか調べた所、その様な機能は無かったのですがユーザIDとPassWordを送る機能が有ることが分かりました。

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

これを使ってconst char* userを ”-“ と指定した場合、 const char* pwd(これを認証キーとする)を ヘッダーに”X-Radiko-AuthToken:pwd とセットする様ライブラリーの”Audio.ccp”を書き換えました。下記がそれです。”Audio.ccp”のバージョンにより変更箇所が若干違うと思いますが、同様の箇所を修正下さい。

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);

これで、audio.connecttohost(“radiko_URL”, “-“, “認証番号”)でRadikoにアクセス出来ます。

実行

今回のスケッチをここに保存しましたー> player2.ino 

いつもの通り、SDカードにフォントと”url”ファイルを下記の様に保存して下さい。フォント:genshin-regular-20pt.vlw。 ”url.txt”ファイル:url.txt

最後に、player2.inoに有るSSIDとPASSWORDを入力してコンパイル実行して下さい。

  1. Web Radio(左)前回製作、Radiko(中央)今回製作、Player(右)次回製作予定 を選択する画面です。
    真ん中のICON Radiko にタッチします。
  2. これがRadikoの初期画面です。受信出来るラジオ局の最初のラジオ局がデフォルトで選ばれる設定になっています。この画面でTBSラジオにタッチ
  3. ラジオ局選択画面が表示されます。前回のWeb Radioと同じ操作で聞きたいラジオ局を選択します。
  4. 例えば、J-WAVEを選んで OKボタンにタッチ
  5. 初期画面に戻ります。ラジオ局表示欄にJ-WAVEが表示されています。
  6. スタートボタンにタッチすればJ-WAVEが聞けます。
  7. 初期画面に有る3つのボタンで右のボタンにタッチすれば、機器選択画面が表示されます。現在、Web RadioとRadikoは機能します。切り替えて動きを確認下さい。

次回は

次回はSDカードに保存した音楽ファイルを再生する機能を追加して行きます。