目次

目次

楽曲ストリーミング配信のCDN移行を行った話

アバター画像
山本紘一
アバター画像
山本紘一
最終更新日2025/11/26 投稿日2025/06/06

はじめに

はじめまして。 システム開発推進部第2Gの山本です。 フロントエンドやバックエンドの開発を主に行っております。 今回は担当している音楽配信サービスの楽曲配信のCDN移行を行った話について記載します。

弊社では音楽配信サービスを提供しており、HLS (HTTP Live Streaming) を用いた楽曲のストリーミング配信を行っています。 これまでストリーミング配信時に利用していたCDN事業者のCDNサービスが終了するとの通知があったため、急ぎで新たなCDNの選定が必要になりました。

当初、CDNの乗り換え先としてAWS CloudFront を候補として検討しましたが、初回リクエスト時の HLS変換 に時間がかかることが課題となり、最終的に Fastly CDNを採用することになりました。 本記事では、CloudFrontを利用したHLS変換の仕組みを説明し、最終的にFastly CDNへ移行するまでの流れを紹介します。

旧CDNからのHLS配信について

今まで使用していたCDNでは以下のフローでHLS配信を行っていました。

旧CDNでの配信フロー

  1. ユーザーが CDN に楽曲をリクエスト
  2. CDN がキャッシュを確認し、キャッシュがあればそのまま返却
  3. キャッシュがない場合は、楽曲取得API へリクエストを送信し m4a 形式の楽曲を取得
  4. CDN が m4a を HLS (m3u8 + TS) に変換
  5. HLS変換後の m3u8 + TS をユーザーへ返却
  6. CDN が変換済みの HLS ファイルをキャッシュ

新たなCDNでも同様の配信フローを維持しつつ、HLS 変換を適切に処理できるような仕組みを構築する必要がありました。

CloudFront + Lambda@Edge + MediaConvert + S3を利用した楽曲配信

CloudFront + Lambda@Edge + MediaConvert + S3を利用したHLS変換・配信の実装について詳しく解説していきます。 AWS公式のブログを参考にさせていただきました。

弊社のシステムは AWS 上で構築されているため、以下のAWSリソースを採用することとしました。 • CloudFront:CDNとして利用。キャッシュがない場合はLambdaをトリガー。 • Lambda (Lambda@Edge):CloudFrontのオリジンリクエスト時に動作し、S3やAPIをチェック。 • MediaConvert:m4aをHLS形式(m3u8 + AAC)に変換。 • S3:変換済みのHLSファイル(m3u8 + AAC)を保存。

CloudFront経由のHLS配信フローとAWS構成図

今回構築した配信フローとAWS構成図となります。

  1. ユーザー が CloudFront にリクエスト
  2. CloudFront がキャッシュを確認、キャッシュがあればそのまま返却
  3. キャッシュがない場合、Lambda@Edge をトリガー
  4. Lambda@Edge がS3をチェックし、HLSファイル(m3u8)があれば返却
  5. S3にHLSファイルがない場合、楽曲取得API にリクエストし m4a ファイルを取得
  6. 取得した m4a を MediaConvert で HLS (m3u8 + AAC) に変換
  7. HLS変換後の m3u8 + AAC ファイルを S3 に保存
  8. HLS変換後の m3u8 + AAC をユーザーへ返却
  9. CloudFront が変換後の HLS ファイルをキャッシュ
AWS構成図.jpg

Lambda@Edgeのコード

以下、Lambda の実装の中で重要な部分を抜粋して解説します。

  1. HLSマニフェストのレスポンス生成 HLSマニフェストファイルを適切にレスポンスさせる。
    function createManifestWrapper(manifestBody) {
    return {
        status: '200',
        statusDescription: 'OK',
        headers: {
            'access-control-allow-origin': [{ value: '*' }],
            'content-type': [{ value: 'application/vnd.apple.mpegurl' }],
            'cache-control': [{ value: 'max-age=3' }],
        },
        body: manifestBody,
    };
    }
    
  2. S3からHLSマニフェストを取得 S3に変換済みのHLSファイルがあるかチェック。 HLSファイルがあればファイルを返却。 HLSファイルがなければエラーをスロー。
    async function getManifest(qsParams) {
    const s3Path = `${qsParams.musicId}/${qsParams.musicId}.m3u8`;
    const bucketParams = {
        'Bucket': 'yourHlsBucket',
        'Key': s3Path
    };
    
    try {
        const manifest = await s3.send(new GetObjectCommand(bucketParams));
        return manifest;
    } catch (err) {
        err.statusCode = 404;
        throw err;
    }
    }
    
  3. MediaConvertジョブの作成 S3にファイルがない場合、MediaConvert を利用して m4a から HLS (m3u8 + AAC) に変換。 SegmentLength: 10 により10秒単位でHLSセグメントを生成。 変換後は s3://${hlsS3bucket}/${qsParams.musicId}/ に保存される。
    async function createMediaConvertJob(musicUrl, qsParams) {
    const s3Path = `s3://${hlsS3bucket}/${qsParams.musicId}/`;
    const mediaConvertJobParams = {
        "Role": 'yourHlsMediaConvertJobIamRole',
        "Settings": {
            "OutputGroups": [{
                "Name": "Apple HLS",
                "OutputGroupSettings": {
                    "Type": "HLS_GROUP_SETTINGS",
                    "HlsGroupSettings": {
                        "SegmentLength": 10,
                        "Destination": s3Path
                    }
                }
            }],
            "Inputs": [{
                "FileInput": musicUrl,
                "AudioSelectors": {
                    "Audio Selector 1": { "DefaultSelection": "DEFAULT" }
                }
            }]
        }
    };
    
    const command = new CreateJobCommand(mediaConvertJobParams);
    return await mediaConvert.send(command);
    }
    
  4. Lambda@Edge のメインハンドラー exports.handler ではユーザーリクエストを処理する流れを定義する。
    exports.handler = async (event) => {
    const request = event.Records[0].cf.request;
    // クエリ文字列から各キーのバリデーションチェックを行い、オブジェクトとして返却
    const qsParams = parseQueryString(request.querystring, request.uri);
    
    try {
        const manifest = await getManifest(qsParams);
        return createManifestWrapper(manifest.Body);
    } catch (err) {
        if (err.statusCode == 404) {
            // APIから音源取得
            const musicData = await getMusicData(qsParams);
            // MediaConvertジョブ作成
            await createMediaConvertJob(musicData.url, qsParams);
            // HLSファイル変換中にプレイヤー側が止まらないためにイントロ用マニフェストを返却
            return createIntroManifest();
        }
    }
    };
    

CloudFront + Lambda@Edge + MediaConvert + S3を利用した配信の課題と解決策

課題

CloudFront + Lambda@Edge + MediaConvert + S3を利用した楽曲配信の構築は完了しましたが、初回アクセス時にHLS変換が必要になるため、初回再生時には時間がかかるという課題が浮上してきました。 CDN移行後のアクセスは、すべて初回アクセス扱いになるため、全楽曲がこの問題に直面しました。

  1. 初回アクセス時のHLS変換 初回アクセス時には S3 バケットに HLS 変換済みのファイル (m3u8 + AAC) が存在しません。 そのため、初回のみ m4a を HLS に変換する処理が必要になります。 HLS変換には 楽曲の長さに関わらず約10秒前後 かかり、この間ユーザーは再生を開始できないようになります。
  2. 変換中の再生 変換中でも一部のデータを返して再生を開始する設定もありますが、シークバーを動かすと変換が完了していない部分は再生できない という問題が発生しました。 このため、最終的にHLS変換が完了してから再生を開始する方式にしました。

  3. MediaConvertのコスト CDN移行前にHLSファイルを事前に作成し、初回アクセス時の変換を不要にすることで、再生遅延を解消することにしました。 しかし、配信楽曲数が多いため、MediaConvertを使用するとコストが大幅に増加することが判明しました。

解決策: EC2 + FFmpeg による事前HLS変換

上記の課題を解決するため、MediaConvertを使用せず、EC2上でFFmpegを利用したバッチ処理を構築しました。 1. EC2上でFFmpegを構築 2. 対象楽曲の音源ファイルを取得 3. 音源ファイルをFFmpegでHLSファイル (m3u8 + AAC) に変換 4. 変換後のHLSファイルをS3に保存 これにより、初回アクセス時はMediaConvertを使用した変換処理を不要にし、すぐに再生できるように準備しました。 ただ、CDN移行まで全楽曲分バッチ処理を流すほどの時間がなかったため、直近聞かれている曲に限定してバッチ処理を行なっており、マイナー楽曲に対しては事前変換を行なっておりませんでした。 また、新規で配信される楽曲に対しては考慮しておらず、楽曲公開前に事前変換するといった対策ができておりませんでした。 そのため、マイナー楽曲や新曲に対しては依然として、初回アクセス時のHLS変換による再生遅延の問題がありました。

アバター画像

山本紘一

目次