動画サービスをFirestore+CloudFunctionsで勝手に設計してみる ~ NobodySurf編


こんにちは。もぐめっとです。

最近本当に自分の写真のネタがなさすぎるので昔の写真を引っ張り出したりしてます。普通に滑ってるように見えますが、実は浮いてます。目の錯覚ショットですね。

今回は、昨日apple storeをみたらTodayで紹介されていたNobodySurfという超イケてるサーフィン動画をたくさん見れる超イケてるアプリが掲載されていたので、こちらのアプリをもしfirestoreでシステム構築したらどんな構成になるのか?という題材で勝手に設計してみたので僕が考えた設計を紹介します。

アプリ概要

アプリについて紹介します。
トップからは主にカテゴリ別の動画が見れ、動画詳細で動画をチェックし、気に入れば自分のプレイリストを作って保存することができます。

TOP 動画詳細 マイページ

検索はサーファー名、カテゴリ、フィン、波の大きさ、スタンス、性別などなどいろんな角度から検索をすることができます。

検索TOP 検索結果

Firestore設計

こんな構造を考えてみました

  • movies: サーフィン動画
  • surfers: サーファー一覧
  • news: お知らせ
  • users: アプリを使うユーザ情報
    • playlists: ユーザ保存したプレイリスト情報
      • movies: 実際に保存したmovieのコピー
    • billingTransactions: 購入したときのレシートを保管
    • subscriptions: ユーザのサブスク状況

movies

movie情報です。
movieのidからストリーミングするURLは生成することとします。

/movies/{movieId}
movieId: 自動生成
Field Type Description 関連CloudFunctions
createdAt Timestamp 登録日
updatedAt Timestamp 更新日
movieTitle String 動画タイトル
surfers Map[String: DocumentReference] サーファー名とそのリファレンス
filmers Map[String: DocumentReference] 撮影者名とそのリファレンス
editors Map[String: DocumentReference] 編集者名とそのリファレンス
filmEditors Map[String: DocumentReference] 撮影者兼編集者名とそのリファレンス
presenters Map[String: DocumentReference] プレゼンター名とそのリファレンス
producers Map[String: DocumentReference] プロデューサ名とそのリファレンス
supporters Map[String: DocumentReference] サポーター名とそのリファレンス
narrators Map[String: DocumentReference] ナレーション名とそのリファレンス
writers Map[String: DocumentReference] ライター名とそのリファレンス
musics Array[String] 使われてる音楽名
droneFootager [String] ドローン映像名
countries [String] 撮影された国
locations [String] 撮影された場所
series String シリーズ名
runTime Int 動画時間
years Array[Int] 撮影年代
surfboards Array[String] enum: shortboard, longboard, midLength, fish, softTop, gun
fins Array[String] enum: finless, singleFin, twinFin, triFin, Bonzer
waveSizes Array[String] enum: small, chestHead, overhead, big, huge
stances Array[String] enum: regular, goofy
genders Array[String] enum: man, woman, unknown
otherCategories Array[String] enum: youth, legend
relatedLinks Map[String: String] リンク名とURL

surfers

サーファー情報の管理です。

/surfers/{surferId}
movieId: 自動生成
Field Type Description 関連CloudFunctions
createdAt Timestamp 登録日
updatedAt Timestamp 更新日
surferName String 名前
surferTypes Array[String] サーファーだったり、撮影者だったり様々な肩書があるので配列で持ちます。enum: surfer, filmer, editor, fimEditor, presenter, producer, supporter, narrator, writer
surfboards Array[String] enum: shortboard, longboard, midLength, fish, softTop, gun
fins Array[String] enum: finless, singleFin, twinFin, triFin, Bonzer
waveSizes Array[String] enum: small, chestHead, overhead, big, huge
stances Array[String] enum: regular, goofy
genders Array[String] enum: man, woman, unknown
otherCategories Array[String] enum: youth, legend
relatedLinks Map[String: String] リンク名とURL

news

新着情報の表示用

/news/{newsId}
newsId: 自動生成
Field Type Description 関連CloudFunctions
createdAt Timestamp 登録日
updatedAt Timestamp 更新日
deletedAt Timestamp nullの場合は表示しない
newsTitle String お知らせのタイトル
movieTitle String? 動画タイトル
playlistTitle String? プレイリストタイトル
playlistReference DocumentReference? プレイリストのリファレンス
message String? bannerの場合に表示するメッセージ
url String? bannerの場合に遷移するURL
viewType String? enum: card, banner どんな形式でお知らせを見せるか

newsの取得は deletedAt != nullで表示するものだけ取得します。

users

アプリを使うユーザ管理
下記はそれぞれFirebase Authenticationで解決できるのでここでは管理しません。
認証方法: Firebase Authenticationで管理
ユーザロール: Firebase Authenticationのcustom claimsで管理

Firestore側でなるべく個人情報などのsecureな情報は持たないように設計します。

/users/{userId}
userId: firebase authenticationで認証したuid
Field Type Description 関連CloudFunctions
createdAt Timestamp 登録日
updatedAt Timestamp 更新日
needsNewsNotification Boolean お知らせ通知を送るかどうか
favoriteSurfStyle String 好みのサーフスタイル enum: shortboard, longboard, alternative
fcmTokens Array[String] 通知用のfcmToken配列。これを使ってpush通知を行う

fcmTokenは今回はユーザ同士の読み取りがないためusersにもたせていますが、本来であればsecureな情報にあたるので、交流するようなSNS的なサービスであれば別途collectionを定義してやったほうがいいです。

users/playlists

ユーザが保存したプレイリスト

/users/{userId}/playlists/{playlistId}
playlistId: 自動生成
Field Type Description 関連CloudFunctions
createdAt Timestamp 登録日
updatedAt Timestamp 更新日
playlistTitle String プレイリスト名
movieCount Number 保存した動画数 onCreatePlaylistMovie, onDeletePlaylistMovie

users/playlists/movies

保存した動画一覧。内容はmoviesと一緒

/users/{userId}/playlists/{playlistId}/movies/{movieId}
movieId: 元のmovieのidと同等

users/billingTransactions

ユーザが課金処理をしてwebhookからレシートを発行されたら保管する。
処理に失敗しても辿れるように必ず最初に保存だけしておく。

/users/{userId}/billingTransactions/{billingTransactionId}
- billingTransactionId: Apple: originalTransactionId, Google: orderId
Field Type Description 関連CloudFunctions
createdAt Timestamp 登録日 onRequestSubscriptionApple,onRequestSubscriptionGoogle
updatedAt Timestamp 更新日 onRequestSubscriptionApple,onRequestSubscriptionGoogle
purchasedAt Timestamp 購入日 onRequestSubscriptionApple,onRequestSubscriptionGoogle
platform String 購入したプラットフォームenum: apple, google onRequestSubscriptionApple,onRequestSubscriptionGoogle
receipt String Apple: レシート, Google: json onRequestSubscriptionApple,onRequestSubscriptionGoogle
productId String 商品ID onRequestSubscriptionApple,onRequestSubscriptionGoogle
userReference DocumentReference コレクショングループで調べるよう onRequestSubscriptionApple,onRequestSubscriptionGoogle
validatedData Map[String: any] レシートの検証結果の情報をいれておく onRequestSubscriptionApple,onRequestSubscriptionGoogle

users/subscriptions

/users/{userId}/subscriptions/{subscriptionId}
- subscriptionId: Apple: originalTransactionId, Google: orderId

platformからのwebhookを受け取ってサブスクリプション情報を更新する。
stripeのこの辺を参考にしてだいたい一緒だろうというのりで作った。

Field Type Description 関連CloudFunctions
createdAt Timestamp 登録日 onRequestSubscriptionApple,onRequestSubscriptionGoogle
updatedAt Timestamp 更新日 onRequestSubscriptionApple,onRequestSubscriptionGoogle
cancelAt Timestamp? キャンセル日 onRequestSubscriptionApple,onRequestSubscriptionGoogle
canceledAt Timestamp? キャンセルした日 onRequestSubscriptionApple,onRequestSubscriptionGoogle
currentPeriodStart Timestamp サブスク開始日 onRequestSubscriptionApple,onRequestSubscriptionGoogle
currentPeriodEnd Timestamp サブスク終了日 onRequestSubscriptionApple,onRequestSubscriptionGoogle
endedAt Timestamp? サブスクを終了した日 onRequestSubscriptionApple,onRequestSubscriptionGoogle
trialStart Timestamp? お試し開始日 onRequestSubscriptionApple,onRequestSubscriptionGoogle
trialEnd Timestamp? お試し終了日 onRequestSubscriptionApple,onRequestSubscriptionGoogle
price Number 金額 onRequestSubscriptionApple,onRequestSubscriptionGoogle
productId String 商品ID onRequestSubscriptionApple,onRequestSubscriptionGoogle
status String enum: active, canceled, incomplete, incompleteExpired, pastDue, trialing, unpaid onRequestSubscriptionApple,onRequestSubscriptionGoogle

ただ、status周りはストアごとにもあったりするのでもう少し精査の必要性あり

CloudFunctions設計

onRequestSubscriptionApple / onRequestSubscriptionGoogle

Requestで実装。
Apple/Googleからsubscriptionの更新があった時に発火されるwebhook先のURL。
受け取ったレシートを検証し、subscriptionsやbillingTransactionsレコードを管理する
また、userのclamsもサブスクの状態を見て更新します。

onCreatePlaylistMovie, onDeletePlaylistMovie

ユーザがプレイリストを追加したり、削除した時に発火。プレイリスト数を増減させる。

ユースケースを考える

動画のストリーミングについて

動画はcloud storageに保管すればストリーミング再生ができそうな雰囲気は醸し出してます。

検索について

検索条件がたくさんあるので全部algoriaにお任せです。
ただ、検索キーとして、movieのsurfersやfilmersなどで使ってる名前を別途配列として定義してしまえばalgoriaを使わなくても検索をすることは可能です。(フィールドは検索用にたくさん増えてはしまいますが、algoria使わなくていいので実装コストは下がる)

ただ、最近extensionも出たので、algoriaの実装難易度も大分下がりました。

まとめ

ユーザ同士の交流といった機能はないので、実装コストは大きく削れそうです。
そう考えるとユーザ交流させるサービスのコストって結構高いんだなぁと改めて感じました。

私はスノボが好きなのですが、いずれNobody Snowみたいな雪山版のイケてるサービスも作れたらいいなと密かに思ったりしています。

最後に、ワンナイト人狼オンラインというゲームを作ってます!よかったら遊んでね!

他にもCameconOffchaといったサービスも作ってるのでよかったら使ってね!

また、チームビルディングや技術顧問、Firebaseの設計やアドバイスといったお話も受け付けてますので御用の方は弊社までお問い合わせください。