10年前の音楽プレーヤー実装を発掘した
昔作ったWebサイトのサーバーを整理していたら、10年前に実装した音楽プレーヤーのコードが出てきた。某アニメのED曲試聴プレーヤーで、版権物だったから音源の保護にかなり気を使った記憶がある。
当時のコードを見返すと「こんなに大変だったのか」と驚く。今なら1行で済むことに200行以上書いていた。
当時の構成
4ファイル構成だった。
index.html- jQuery 1.8.3ベースのUIapi.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側でやってくれるし、収益化もできる。自前で音声ホスティングする場面って、ポッドキャストか同人音声くらいかもしれない。