技術 約3分で読めます

10年前の音楽プレーヤー実装を発掘した

昔作ったWebサイトのサーバーを整理していたら、10年前に実装した音楽プレーヤーのコードが出てきた。某アニメのED曲試聴プレーヤーで、版権物だったから音源の保護にかなり気を使った記憶がある。

当時のコードを見返すと「こんなに大変だったのか」と驚く。今なら1行で済むことに200行以上書いていた。

当時の構成

4ファイル構成だった。

  • index.html - jQuery 1.8.3ベースのUI
  • api.php - パス難読化 + ワンタイムトークン生成
  • audio.php - トークン検証 + iOS向けRange Request対応
  • audio.js - Blob URL化 + 全イベント監視 + フォールバック処理

解決すべきだった問題

1. 直リンク防止

音源URLを直接叩かれないよう、APIでワンタイムトークンを発行していた。

function get_unique_id($t)
{
    $pin = /* ランダム32文字生成 */;
    $str = $pin.'_'.$t;
    return 'r='.$pin.'&u='.hash_hmac('sha256',$str,false).'&t='.$t;
}

さらに、パスをASCIIコード配列に分解して返していた。

function splitPath($path, $t)
{
    $data = array('a'=>array(), 'b'=>array());
    $strs = preg_split("//u", $path, -1, PREG_SPLIT_NO_EMPTY);
    foreach($strs as $key => $val)
    {
        $data['a'][] = ord($val);  // 1文字ずつASCIIコードに変換
    }
    // ...
    return $data;
}

JS側でこれを復元してからアクセスする仕組み。

// ASCIIコード配列を文字列に復元
var d3 = '';
for(var i = 0; i < d1.length; i++){
    d3 += String.fromCharCode(d1[i]);
}

DevToolsで見れば一発でバレるんだけど、当時はこれで「一応の保護」としていた。

2. ダウンロード防止

音源をXHRでバイナリ取得し、Blob URLに変換していた。

var xhr = new XMLHttpRequest();
xhr.responseType = 'arraybuffer';
xhr.onload = function() {
    var blob = new Blob([new Uint8Array(this.response)], {'type':'audio/mp3'});
    var url = URL.createObjectURL(blob);
    audio.src = url;  // blob://... 形式のURL
};

こうすると、ブラウザの履歴やキャッシュに元のURLが残らない。ページを閉じるとBlob URLも無効になる。

3. iOSで再生できない問題

これが一番厄介だった。当時のiOS Safariは音声再生時に必ずRange Requestを送る仕様だった。

GET /audio.php HTTP/1.1
Range: bytes=0-

普通に200 OKで全データを返すと、なぜか再生できない。206 Partial Contentで返す必要があった。

if(isset($_SERVER['HTTP_RANGE']))
{
    list($dummy, $range) = explode('=', $_SERVER['HTTP_RANGE']);
    list($start, $end) = explode('-', $range);
    if(empty($end)){ $end = filesize($music_path) - 1; }
    $length = $end - $start + 1;

    header('HTTP/1.1 206 Partial Content');
    header('Accept-Ranges: bytes');
    header('Content-Length: ' . $length);
    header('Content-Range: bytes ' . $start . '-' . $end . '/' . filesize($music_path));

    $handle = fopen($music_path, 'rb');
    fseek($handle, $start);
    echo fread($handle, $length);
    fclose($handle);
}

ただし、ダウンローダーツールも分割ダウンロードでRange Requestを使う。そこで「特定のパラメータがないRange Requestは拒否」という分岐も入れていた。

4. ブラウザごとの挙動差異

Audio要素の挙動がブラウザごとに違いすぎて、全イベントを監視してデバッグしていた。

$(this.audio).on({
    'loadstart': function(){ consoleLine('loadstart'); },
    'progress': function(){ consoleLine('progress'); },
    'suspend': function(){ consoleLine('suspend'); },
    'abort': function(){ consoleLine('abort'); },
    'error': function(){ consoleLine('error'); },
    'emptied': function(){ consoleLine('emptied'); },
    'stalled': function(){ consoleLine('stalled'); },
    'loadedmetadata': function(){ consoleLine('loadedmetadata'); },
    'loadeddata': function(){ consoleLine('loadeddata'); },
    'canplay': function(){ consoleLine('canplay'); },
    'canplaythrough': function(){ consoleLine('canplaythrough'); },
    // ... 他にも大量
});

今ならどう書くか

基本的な音楽プレーヤーなら、これだけ。

<audio controls src="/audio/song.mp3"></audio>

カスタムUIが欲しくても、こんな感じで済む。

const audio = document.querySelector('audio');
const btn = document.querySelector('#playBtn');

btn.addEventListener('click', () => {
    audio.paused ? audio.play() : audio.pause();
});

iOSのRange Request問題は?

Webサーバーが自動で対応してくれる。nginx、Apache、Vercel、CloudFlare、どれも静的ファイルに対してRange Requestを適切に処理する。PHPで自前実装する必要はない。

保護が必要な場合は?

正直、クライアントサイドでの保護は限界がある。DevToolsで見れば結局バレる。

現代的なアプローチとしては:

  • 署名付きURL - CloudFront Signed URL等で、一定時間だけ有効なURLを発行
  • HLS/DASHストリーミング - 音源を細切れにして配信。DRMとの組み合わせも可能
  • 諦めて短い試聴版を配信 - 30秒だけ、みたいな割り切り

まとめ

10年前と今の比較:

項目10年前
基本再生jQuery + 複雑なJS<audio controls>
iOS対応PHPでRange Request自前実装サーバーが自動対応
直リンク防止トークン + 難読化署名付きURL or 諦め
カスタムUI大変素のJSで簡単

ブラウザの標準化が進み、サーバー側の対応も自動化された。Web開発、だいぶ楽になったなと実感する。

というか、今どき版権音楽の試聴なら「YouTube埋め込み」が一番楽。著作権管理もYouTube側でやってくれるし、収益化もできる。自前で音声ホスティングする場面って、ポッドキャストか同人音声くらいかもしれない。