Cloudflare StreamでVOD動画の署名付きURL配信をしてみた

Cloudflare Streamの動画コンテンツはデフォルト状態では誰もが視聴できるパブリックな状態ですが、署名付きURL機能を有効化することで再生時にトークンを要求するように設定できます。トークンの生成含め実際に動作を確認してみました。
2022.06.29

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

清水です。Cloudflareの動画配信プラットフォームのサービスCloudflare Streamを試しています。Cloudflare Streamでは動画コンテンツ保護の機能の1つとして署名付きURLがサポートされています。デフォルト状態ではビデオIDがわかれば誰もが動画を見ることのできるパブリックな状態ですが、署名付きURLを有効にすることで動画再生にトークンが必要になります。本エントリではVOD動画に対してこの署名付きURLを使った配信をしてみたのでまとめてみたいと思います。

署名付きURLを有効にする

まずは署名付きURL機能を有効にします。署名付きURL機能はビデオごとに設定していきます。ダッシュボードでビデオの詳細ページに進み、「設定」タブの「署名付きURLが必要」の項目のをチェックします。

なお署名付きURL機能を有効にした場合、ダッシュボード内のプレビュープレイヤーでの再生が無効になりますので注意しましょう。

APIからも署名付きURLの有効化が可能です。以下の形式でJSONデータをPOSTします。

curl -X POST \
     -H "Authorization: Bearer ${TOKEN}" \
     --data "{\"uid\":\"${VIDEO_UID}\", \"requireSignedURLs\":true}" \
     "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT}/stream/${VIDEO_UID}"

以下が実際の実行例です。設定後、"requireSignedURLs": trueとなっていることが確認できます。

##### 設定前の状態を確認
 % curl -X GET -H "Authorization: Bearer ${TOKEN}" "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT}/stream/${VIDEO_UID}"

{
  "result": {
    "uid": "c409xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "creator": "user-odawara",
    "thumbnail": "https://cloudflarestream.com/c409xxxxxxxxxxxxxxxxxxxxxxxxxxxx/thumbnails/thumbnail.jpg",
    "thumbnailTimestampPct": 0,
    "readyToStream": true,
    "status": {
      "state": "ready",
      "pctComplete": "100.000000",
      "errorReasonCode": "",
      "errorReasonText": ""
    },
    "meta": {
      "filename": "IMG_5908.MOV",
      "filetype": "video/quicktime",
      "name": "小田原の海",
      "relativePath": "null",
      "type": "video/quicktime"
    },
    "created": "2022-04-30T09:57:30.832975Z",
    "modified": "2022-06-29T07:56:28.246831Z",
    "size": 382466674,
    "preview": "https://watch.videodelivery.net/c409xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "allowedOrigins": [],
    "requireSignedURLs": true,
    "uploaded": "2022-04-30T09:57:30.832929Z",
    "uploadExpiry": "2022-05-01T09:57:30.832922Z",
    "maxSizeBytes": null,
    "maxDurationSeconds": null,
    "duration": 60.9,
    "input": {
      "width": 3840,
      "height": 2160
    },
    "playback": {
      "hls": "https://cloudflarestream.com/c409xxxxxxxxxxxxxxxxxxxxxxxxxxxx/manifest/video.m3u8",
      "dash": "https://cloudflarestream.com/c409xxxxxxxxxxxxxxxxxxxxxxxxxxxx/manifest/video.mpd"
    },
    "watermark": null
  },
  "success": true,
  "errors": [],
  "messages": []
}

##### 署名付きURL機能を有効に設定
 % curl -X POST \
     -H "Authorization: Bearer ${TOKEN}" \
     --data "{\"uid\":\"${VIDEO_UID}\", \"requireSignedURLs\":true}" \
     "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT}/stream/${VIDEO_UID}"
{
  "result": {
    "uid": "c409xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "creator": "user-odawara",
    "thumbnail": "https://cloudflarestream.com/c409xxxxxxxxxxxxxxxxxxxxxxxxxxxx/thumbnails/thumbnail.jpg",
    "thumbnailTimestampPct": 0,
    "readyToStream": true,
    "status": {
      "state": "ready",
      "pctComplete": "100.000000",
      "errorReasonCode": "",
      "errorReasonText": ""
    },
    "meta": {
      "filename": "IMG_5908.MOV",
      "filetype": "video/quicktime",
      "name": "小田原の海",
      "relativePath": "null",
      "type": "video/quicktime"
    },
    "created": "2022-04-30T09:57:30.832975Z",
    "modified": "2022-06-29T08:04:21.104628Z",
    "size": 382466674,
    "preview": "https://watch.videodelivery.net/c409xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "allowedOrigins": [],
    "requireSignedURLs": true,
    "uploaded": "2022-04-30T09:57:30.832929Z",
    "uploadExpiry": "2022-05-01T09:57:30.832922Z",
    "maxSizeBytes": null,
    "maxDurationSeconds": null,
    "duration": 60.9,
    "input": {
      "width": 3840,
      "height": 2160
    },
    "playback": {
      "hls": "https://cloudflarestream.com/c409xxxxxxxxxxxxxxxxxxxxxxxxxxxx/manifest/video.m3u8",
      "dash": "https://cloudflarestream.com/c409xxxxxxxxxxxxxxxxxxxxxxxxxxxx/manifest/video.mpd"
    },
    "watermark": null
  },
  "success": true,
  "errors": [],
  "messages": []
}


##### 設定後の状態を確認
 % curl -X GET -H "Authorization: Bearer ${TOKEN}" "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT}/stream/${VIDEO_UID}"                               
{
  "result": {
    "uid": "c409xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "creator": "user-odawara",
    "thumbnail": "https://cloudflarestream.com/c409xxxxxxxxxxxxxxxxxxxxxxxxxxxx/thumbnails/thumbnail.jpg",
    "thumbnailTimestampPct": 0,
    "readyToStream": true,
    "status": {
      "state": "ready",
      "pctComplete": "100.000000",
      "errorReasonCode": "",
      "errorReasonText": ""
    },
    "meta": {
      "filename": "IMG_5908.MOV",
      "filetype": "video/quicktime",
      "name": "小田原の海",
      "relativePath": "null",
      "type": "video/quicktime"
    },
    "created": "2022-04-30T09:57:30.832975Z",
    "modified": "2022-06-29T08:04:21.104628Z",
    "size": 382466674,
    "preview": "https://watch.videodelivery.net/c409xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "allowedOrigins": [],
    "requireSignedURLs": true,
    "uploaded": "2022-04-30T09:57:30.832929Z",
    "uploadExpiry": "2022-05-01T09:57:30.832922Z",
    "maxSizeBytes": null,
    "maxDurationSeconds": null,
    "duration": 60.9,
    "input": {
      "width": 3840,
      "height": 2160
    },
    "playback": {
      "hls": "https://cloudflarestream.com/c409xxxxxxxxxxxxxxxxxxxxxxxxxxxx/manifest/video.m3u8",
      "dash": "https://cloudflarestream.com/c409xxxxxxxxxxxxxxxxxxxxxxxxxxxx/manifest/video.mpd"
    },
    "watermark": null
  },
  "success": true,
  "errors": [],
  "messages": []
}

トークンを生成して動画を再生する

署名付きURLを有効にできました。この状態だと先ほどのダッシュボードのほか、iframe.videodelivery.netで提供されるStream PlayerやマニフェストURLでも当然再生はできません。

以下のようにStream Playerでは「You don't have permission to view this video」と権限がない旨、表示されてしまいます。

またマニフェストURLにアクセスしようとした場合、以下のように401 unauthorizedが返ってきます。

 % curl https://cloudflarestream.com/c409xxxxxxxxxxxxxxxxxxxxxxxxxxxx/manifest/video.m3u8
401 unauthorized
 % curl -I https://cloudflarestream.com/c409xxxxxxxxxxxxxxxxxxxxxxxxxxxx/manifest/video.m3u8
HTTP/2 401
date: Wed, 29 Jun 2022 08:09:33 GMT
content-type: text/plain
content-length: 16
access-control-allow-origin: *
cache-control: no-cache, no-store, must-revalidate
vary: origin
expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
server: cloudflare
cf-ray: 722d2e419c7d1ee9-NRT

再生するためにはトークンが必要です。トークンの作成方法はドキュメントに記載があります。

トークン作成時、デフォルトでは1時間有効なトークンが作成されます。オプションでトークンの有効期限のほか、再生できる地域や国、IPアドレスなどを指定することが可能です。

またトークンの作成方法についても、api.cloudflare.com/tokenエンドポイントを使って作成する方法、自前でプログラムからトークンを作成する方法があります。前者についてはテスト目的、1日あたり10,000未満のトークンが目安ということですので、本格的な運用の場合には後者の方法を採るようにしましょう。

これらトークン作成方法については、詳細がドキュメントにCloudflare Workerを用いたサンプルコードとともに記載されています。今回はシンプルに/tokenエンドポイントを利用した方法を確認してみます。api.cloudflare.comにビデオIDを指定してデータをPOSTするかたちです。以下の形式となります。

curl -X POST \
     -H "Authorization: Bearer $TOKEN" \
     "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT}/stream/${VIDEO_UID}/token"

以下のようにexpで有効期限を指定することも可能です。(有効期限を指定しない場合は1時間となります。)オプションは「 Create a signed URL token for a video - Cloudflare API v4 Documentation」なども参考にしましょう。

 % curl -X POST \
     -H "Authorization: Bearer $TOKEN" \
     --data '{"exp": 1656491400}' \
     "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT}/stream/${VIDEO_UID}/token"

以下が実際の実行結果です。

 % curl -X POST \
     -H "Authorization: Bearer $TOKEN" \
     "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT}/stream/${VIDEO_UID}/token"
{
  "result": {
    "token": "eyJhxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  },
  "success": true,
  "errors": [],
  "messages": []
}

Stream Playerで再生の際、ビデオIDの代わりにこのトークン文字列を使用します。

  • 署名付きURLが無効の場合
    • https://iframe.videodelivery.net/c409xxxxxxxxxxxxxxxxxxxxxxxxxxxx
  • 署名付きURLが有効な場合
    • https://iframe.videodelivery.net/eyJhxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

マニフェストURLでも同様に、ビデオIDの代わりにトークン文字列を利用すればアクセスできるようになりました。以下、HLSマニフェストURLへアクセスしてみた例です。

 % curl -I https://cloudflarestream.com/eyJhxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/manifest/video.m3u8
HTTP/2 200
date: Wed, 29 Jun 2022 08:16:02 GMT
content-type: application/x-mpegURL
access-control-allow-origin: *
cache-control: public, max-age=600
vary: origin, referer
access-control-allow-headers: range
access-control-expose-header: cf-ray
stream-dw-version: 2022.6.2
expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
server: cloudflare
cf-ray: 722d37beb93a80f5-NRT
 % curl https://cloudflarestream.com/eyJhxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/manifest/video.m3u8
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="group_audio",NAME="und",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,URI="stream_tcecxxxxxxxxxxxxxxxxxxxxxxxxxxxxx_r16xxxx553.m3u8"
#EXT-X-STREAM-INF:RESOLUTION=854x480,CODECS="avc1.4d401f,mp4a.40.2",BANDWIDTH=2764131,AVERAGE-BANDWIDTH=2190192,FRAME-RATE=60.000,AUDIO="group_audio"
stream_t63exxxxxxxxxxxxxxxxxxxxxxxxxxxxx_r16xxxx537.m3u8
#EXT-X-STREAM-INF:RESOLUTION=1920x1080,CODECS="avc1.4d402a,mp4a.40.2",BANDWIDTH=9212192,AVERAGE-BANDWIDTH=7422169,FRAME-RATE=60.000,AUDIO="group_audio"
stream_t63exxxxxxxxxxxxxxxxxxxxxxxxxxxxx_r16xxxx589.m3u8
#EXT-X-STREAM-INF:RESOLUTION=1280x720,CODECS="avc1.4d4020,mp4a.40.2",BANDWIDTH=6762836,AVERAGE-BANDWIDTH=5211825,FRAME-RATE=60.000,AUDIO="group_audio"
stream_t63exxxxxxxxxxxxxxxxxxxxxxxxxxxxx_r16xxxx535.m3u8
#EXT-X-STREAM-INF:RESOLUTION=640x360,CODECS="avc1.4d401f,mp4a.40.2",BANDWIDTH=1524991,AVERAGE-BANDWIDTH=1209509,FRAME-RATE=60.000,AUDIO="group_audio"
stream_t63exxxxxxxxxxxxxxxxxxxxxxxxxxxxx_r16xxxx526.m3u8
#EXT-X-STREAM-INF:RESOLUTION=426x240,CODECS="avc1.42c01e,mp4a.40.2",BANDWIDTH=838965,AVERAGE-BANDWIDTH=624348,FRAME-RATE=60.000,AUDIO="group_audio"
stream_t63exxxxxxxxxxxxxxxxxxxxxxxxxxxxx_r16xxxx464.m3u8

まとめ

Cloudflare Streamで署名付きURLを使った動画コンテンツの限定配信を試してみました。動画のURL自体がトークンを利用したものになるイメージですね。認証したユーザにだけ動画コンテンツを視聴させるといった場合のほか、動画にIP制限やジオロケーション制限を入れる場合も同様にこの署名付きURL機能を利用します。今回はCloudflareの/tokenエンドポイントを利用してトークンを生成する方法でしたが、自前でプログラムからトークンを作成する方法についても試しておきたいなと思いました。