CASTA — 要件定義書 兼 Claude Code 実装指示書
版数 1.0(DBレス構成 / 記事埋め込み型デモ)/ 作成日 2026-08-17 / 発行 ソウゾウ合同会社 用途 SEO記事「【デモで試せる】Claude Codeで動画配信アプリを開発する方法と費用|動画つき資料【2026年】」に組み込む実演デモ
このドキュメントの使い方(Claude Code へ)
これ1枚が、規約・仕様・タスクのすべて。リポジトリ直下に CLAUDE.md として置き、毎回読み込むこと。
- 実装は「13. 実装タスク」の順に進める。フェーズを飛ばさない。
- 各タスクの括弧内は要件ID。着手前に該当章を読むこと。
- 仕様がここに書かれていない場合は、推測で実装せず質問する。
- スコープ外(4章 Won't have)は、思いついても実装しない。
- 「デモだから」を理由に品質を落とす判断はしない。
姉妹プロジェクトとの関係 HIREBASE(求人サイト)/ RELATE(顧客管理)に続く3本目。3つ並べて「同じテンプレ」と思われた時点で失敗。 HIREBASE=明朝・余白・静けさ(読ませる)/ RELATE=ゴシック・高密度・等幅数字(操作させる)/ CASTA=ダーク・映像が主役・UIが消える(見せる)。 9章のデザイン要件は意図的に第3の方向に振っている。トークンを流用しないこと。
目次
- プロジェクト概要
- 記事への組み込み設計
- アーキテクチャ方針
- スコープ定義
- ロールとデモ切替
- 画面一覧とユーザーフロー
- 機能要件
- データ設計
- デザイン要件
- 技術要件・ディレクトリ構成
- 非機能要件
- 費用設計
- 実装タスク
- 受入基準
1. プロジェクト概要
背景と目的
対象記事のキーワード意図は「動画配信アプリを作りたいが、作り方と費用が分からない」。この検索意図に対し、テキストの説明だけでは以下2点が伝わらない。
- どこまでのものが作れるのか — プレーヤーの完成度、会員制の作り込み、管理画面の実用性
- なぜ費用が読みにくいのか — 動画配信は開発費より運用費(配信コスト)の設計が本質。ここを説明できるかが専門性の証明になる
そこで記事内に、実際に動画が再生できるデモを組み込む。動画配信アプリは「モックでは絶対に伝わらない」領域であり、実際に再生が始まった瞬間に信頼が生まれる。
| 目的 | 内容 |
|---|---|
| 実装力の証明 | HLS配信・画質切替・レジューム再生・字幕・チャプターまで実装した本物のプレーヤーを見せる |
| ビジネス機能の証明 | 会員制ペイウォール、プラン管理、視聴分析。「収益化まで作れる」ことを示す |
| 費用説明の裏付け | デモ自体の配信コストを実測値として記事に載せる。自分たちが払っている数字で語る |
| リード獲得 | デモ内から記事・問い合わせへの導線を常設し、GA4で計測する |
プロダクト定義
| 項目 | 内容 |
|---|---|
| プロダクト名 | CASTA(キャスタ)※仮称 |
| 一言定義 | 会員制サブスクリプション型の動画配信プラットフォーム |
| 想定業態 | オンラインスクール、社内研修、士業・コンサルの会員制コンテンツ、専門メディア |
| 提供形態 | レスポンシブWebアプリ。フロントエンド完結(サーバー側の永続化なし)+ 静的動画配信 |
| 主戦場 | 記事からの流入。モバイル比率が高い前提で設計する(SEO記事の読者はスマホが主) |
| 想定利用者 | 記事の読者。登録なしで即座に再生でき、会員機能もロール切替で体験できる |
プロダクトコンセプト
「アプリを見せない。動画を見せる。」
動画配信サービスのUIは、うまくいっているとき存在に気づかれない。CASTA は、画面を暗く落とし、装飾を削り、サムネイルと映像だけに光を渡す。操作は身体が覚えているもの(スペースで再生、矢印でシーク)をそのまま採用する。
デモとしての成功条件
- 記事を読んでいる人が、3秒以内に再生を開始できる
- プレーヤーが本物である — 画質切替、速度、字幕、チャプター、レジューム。ここが安っぽいと全部が嘘になる
- 有料ゲートが体験できる — 無料会員で有料コンテンツを開くと、プレビュー後にペイウォールが出る
- 管理側も見られる — 配信管理と視聴分析まで、ロール切替で確認できる
- 記事に戻れる/問い合わせできる — デモが行き止まりにならない
2. 記事への組み込み設計
この章が本プロジェクト固有の最重要要件。 単体で作って後から埋め込むのではなく、埋め込み前提で設計する。
配置方針
記事内で2段構えにする。
| 段 | 配置 | 中身 | 目的 |
|---|---|---|---|
| ① 記事内インライン | 記事の冒頭〜中盤、<iframe> |
プレーヤー単体の軽量版(1本の動画 + 主要操作) | スクロール中に「動く」ものを見せ、離脱を防ぐ |
| ② 全画面デモ | 「デモを開く」カード → 別タブ | アプリ全体(カタログ・会員・管理画面) | じっくり触ってもらう |
アプリ全体を記事内 iframe に入れない。 理由は3つ。
- 動画を含むアプリ全体は初期ロードが重く、記事本体のCore Web Vitalsを悪化させる
- iframe 内のスクロールと記事のスクロールが競合し、モバイルで操作不能になる
- アプリ内で画面遷移すると、読者が記事のどこにいたか分からなくなる
埋め込みモード(?embed=1)
同一アプリを、URLパラメータで2つのモードで動作させる。別リポジトリに分けない。
| 通常モード | 埋め込みモード ?embed=1 |
|
|---|---|---|
| ヘッダー・サイドバー | 表示 | 非表示 |
| ロール切替バー | 表示 | 非表示 |
| 表示内容 | アプリ全体 | 指定された1動画のプレーヤーのみ |
| 動画の自動再生 | しない | しない(音声ありの自動再生は禁止) |
| 高さ | 100vh | 親に合わせる(16:9 + 操作バー分) |
| 遷移 | 通常 | アプリ内遷移を禁止。 操作は再生関連のみ。「全画面で開く」ボタンは target="_blank" |
<iframe
src="https://casta.souzoh-demo.com/watch/ep-001?embed=1"
width="100%" style="aspect-ratio:16/9; border:0"
loading="lazy"
allow="fullscreen; picture-in-picture"
title="CASTA 動画プレーヤーのデモ"></iframe>
記事本体のパフォーマンスを壊さない
| ID | 要件 |
|---|---|
| EMB-01 | 記事内 iframe は loading="lazy" とし、ビューポート接近まで読み込まないこと |
| EMB-02 | 埋め込みモードの初期ロードは、ポスター画像1枚 + 再生ボタンのみ。動画のマニフェスト取得は再生ボタン押下後に開始すること |
| EMB-03 | 埋め込みモードのJS初期バンドルは 80KB以下(gzip後)とすること。プレーヤー本体(hls.js)は再生ボタン押下時に動的import すること |
| EMB-04 | 埋め込み iframe が記事の LCP 要素にならないこと。ポスター画像は記事側で preload しない |
| EMB-05 | 自動再生は行わない。音声ありの自動再生は明確に禁止 |
「動画つき資料」との一体化(重要な設計判断)
記事タイトルの「動画つき資料」を、別途用意するのではなくデモの中身そのものにする。
- デモに投入するシリーズの1本目を「Claude Codeで動画配信アプリを作る」の解説動画にする
- そのエピソードに、資料PDF(設計図・費用試算シート)を添付ファイルとして紐付ける(FR-208)
- 読者は「デモを触る」と「資料を見る」を1つの体験として行う
これにより、デモ機能(添付資料ダウンロード)と記事コンテンツ(動画つき資料)が同じ実装で成立する。 二重に作らない。
導線と計測
| ID | 要件 |
|---|---|
| EMB-10 | 全画面デモの上部に、元の記事に戻るリンクを常設すること |
| EMB-11 | デモ内の適切な位置(カタログ下部、ペイウォール内、管理画面)に問い合わせ導線を配置すること。再生中のプレーヤー上には出さない |
| EMB-12 | GA4 で以下を計測すること:demo_open(デモを開く)/video_play/video_progress(25/50/75/100%)/paywall_view/role_switch/form_click/line_click |
| EMB-13 | form_click と line_click は、既存メディアのキーイベント名と揃えること(媒体横断で集計するため) |
SEO上の扱い
| ID | 要件 |
|---|---|
| EMB-20 | デモは記事とは別ドメイン/サブドメインで配信し、記事の評価と競合しないよう 固有の title / description を持つこと |
| EMB-21 | デモ本体は index 可とするが、埋め込みモードのURL(?embed=1)は noindex とすること |
| EMB-22 | デモの各ページから記事へのリンクを持ち、記事側にも rel="nofollow" を付けずリンクすること |
| EMB-23 | 記事側に VideoObject 構造化データを設置すること(name / description / thumbnailUrl / uploadDate / duration / contentUrl) |
3. アーキテクチャ方針
基本方針
バックエンドとデータベースを持たない。ただし動画ファイルだけは実在させる。
| 一般的な構成 | 本プロジェクト |
|---|---|
| PostgreSQL / Supabase | シードデータ(TypeScript)+ ブラウザ内ストア |
| 認証・会員管理 | デモ用ロール切替(ワンクリック) |
| 決済(Stripe) | プラン選択〜完了画面まで再現。課金処理なし |
| 動画アップロード・エンコード | UIとエンコード進捗を再現。実ファイルは事前に用意した固定素材 |
| 署名付きURL・DRM | 実装しない。ペイウォールはUI上のゲートとして再現 |
| 動画の配信 | これだけは本物。 静的オブジェクトストレージ + CDN から HLS で配信する |
動画だけは実在させる理由
動画配信アプリのデモで動画が再生できないのは、求人サイトのデモで求人が0件なのと同じ。再生が始まるかどうかが、このデモの成否そのもの。
ただし「実在させる」のは配信であって、バックエンドではない。HLS は .m3u8 プレイリストと .ts セグメントというただの静的ファイルなので、オブジェクトストレージに置くだけで配信できる。サーバーは不要。
動画素材の方針
| ID | 要件 |
|---|---|
| VID-01 | 動画素材はソウゾウが権利を持つものを使う。既存YouTubeチャンネルの動画を再エンコードして流用することを第一候補とする |
| VID-02 | 自社素材で埋まらない分は、商用利用可のライセンスが明示された素材のみを使う。取得元とライセンス種別を docs/assets-license.md に記録すること |
| VID-03 | 本数は 12〜16本。1本あたり 2〜8分。合計 60分以内に収める |
| VID-04 | 各動画に 3段階の画質(1080p / 720p / 360p)を用意し、画質切替を実演できるようにすること |
| VID-05 | 少なくとも3本に**日本語字幕(WebVTT)**を付けること。うち1本は英語字幕も付け、字幕切替を実演できるようにすること |
| VID-06 | 少なくとも5本に**チャプター(WebVTT chapters)**を設定すること |
| VID-07 | 1本目は「Claude Codeで動画配信アプリを作る」の解説動画とし、資料PDFを添付すること(2章参照) |
エンコード仕様
入力: 1080p mp4
出力: HLS(fMP4 / CMAF)
├ 1080p H.264 High 3,500kbps ─┐
├ 720p H.264 Main 1,800kbps ├ master.m3u8 に3レンディション
└ 360p H.264 Base 600kbps ─┘
セグメント長 4秒 / GOP 2秒 / 音声 AAC 128kbps
サムネイル: 各動画から3枚(ポスター / ホバープレビュー用スプライト / OGP用)
エンコードは ffmpeg のスクリプト(scripts/encode.sh)としてリポジトリに含める。これ自体が記事の「方法」パートの説明材料になる。
配信先
| 候補 | 判断 |
|---|---|
| Cloudflare R2 + Cloudflare CDN | 採用。 エグレス(転送量)課金がないため、デモが伸びてもコストが跳ねない |
| Vercel から静的配信 | 不採用。転送量課金が視聴時間に比例して増える |
| Cloudflare Stream / Mux 等の専用サービス | 不採用(本デモでは)。ただし12章で比較対象として扱い、記事の費用パートで解説する |
差し替え可能性の担保
データアクセスは必ず lib/repo/ のリポジトリ層を経由すること。 コンポーネントからストアを直接触らない。
[ コンポーネント ]
↓ 呼ぶのはこの層だけ
[ lib/repo/*.ts ] ← 全メソッドを async
↓
[ lib/store/*.ts ] ← Zustand + persist(localStorage)
↓
[ lib/seed/*.ts ] ← 初期データ(動画URLはR2を指す)
リポジトリの各メソッドは、中身が同期処理でも async で定義する。実案件へ転用する際、リポジトリの実装だけをAPI呼び出しに差し替えれば、UI層は変更不要。
永続化の範囲
| 対象 | 挙動 |
|---|---|
| シードデータ(シリーズ・エピソード・会員) | 初期投入。管理画面から編集可能 |
| 視聴進捗・視聴履歴 | localStorage に保存。リロードしても「続きから」再生できる |
| マイリスト、プラン契約状態、通知既読 | localStorage に保存 |
| アップロードした動画 | ファイル名とメタデータのみ保持。再生は既存素材で代替(FR-403) |
| リセット | ヘッダーの「デモをリセット」で localStorage をクリア |
擬似ディレイ は 150〜400ms。ただし再生開始に関わる操作にはディレイを入れない(動画配信で待たされるのは致命的な体験のため)。
4. スコープ定義
視聴者向け機能
| ID | 機能 | 概要 | 優先度 |
|---|---|---|---|
| F-V01 | 動画プレーヤー | HLS再生、画質切替、再生速度、音量、全画面、PiP、キーボード操作 | Must |
| F-V02 | レジューム再生 | 前回の停止位置から再開。「最初から見る」も選べる | Must |
| F-V03 | 字幕 | WebVTT。日本語/英語/オフの切替、表示サイズ・背景の調整 | Must |
| F-V04 | チャプター | シークバー上のチャプター区切り、チャプター一覧からのジャンプ | Must |
| F-V05 | シークバープレビュー | シークバーのホバー/ドラッグでサムネイルを表示 | Should |
| F-V06 | 次のエピソード | 終了30秒前にカード表示、自動再生(設定でOFF可) | Should |
| F-V07 | トップページ | ヒーロー、続きから見る、カテゴリ別レーン、ランキング、新着 | Must |
| F-V08 | シリーズ詳細 | エピソード一覧、各話の視聴進捗、シリーズ全体の進捗率 | Must |
| F-V09 | 検索・絞り込み | フリーワード、カテゴリ、タグ、再生時間、視聴状態(未視聴/視聴中/視聴済) | Must |
| F-V10 | マイリスト | シリーズ・エピソードの保存 | Should |
| F-V11 | 視聴履歴 | 時系列の履歴、進捗率、履歴の削除 | Should |
| F-V12 | 添付資料 | エピソードに紐づくPDF・スライドの閲覧とダウンロード | Must |
| F-V13 | ペイウォール | 有料コンテンツはプレビュー(既定3分)後にゲート表示 | Must |
| F-V14 | プラン・契約 | プラン比較、選択、決済フォーム、完了画面(課金処理なし) | Must |
| F-V15 | アカウント設定 | 再生設定(既定画質・速度・自動再生・字幕)、通知設定 | Should |
| F-V16 | メモ機能 | 再生中にタイムスタンプ付きメモを残し、クリックでその位置へジャンプ | Could |
配信管理(運営)向け機能
| ID | 機能 | 概要 | 優先度 |
|---|---|---|---|
| F-A01 | 管理ダッシュボード | 本日の再生数、公開待ち、人気コンテンツ、会員数推移 | Must |
| F-A02 | 動画アップロード | ドロップ、エンコード進捗の再現(待機→変換→完了)、失敗ケースの再現 | Must |
| F-A03 | エピソード編集 | タイトル、説明、サムネイル選択、公開設定、公開予約、無料/有料区分 | Must |
| F-A04 | シリーズ管理 | シリーズの作成、エピソードの並び替え、カテゴリ・タグ付与 | Must |
| F-A05 | 字幕編集 | 字幕ファイルのアップロード、タイムライン上での文言修正、プレビュー | Should |
| F-A06 | チャプター編集 | 再生位置を見ながらチャプターを追加・編集 | Should |
| F-A07 | 添付資料管理 | エピソードへのファイル紐付け、公開範囲設定 | Must |
| F-A08 | 会員管理 | 会員一覧、プラン、視聴状況、停止 | Should |
| F-A09 | プラン管理 | プランの作成・編集、価格、視聴可能範囲 | Should |
| F-A10 | 視聴分析 | 再生数・視聴完了率・離脱ヒートマップ・デバイス別・流入元 | Must |
| F-A11 | 一括操作 | 複数エピソードの公開/非公開、カテゴリ付与 | Should |
共通・基盤
| ID | 機能 | 概要 | 優先度 |
|---|---|---|---|
| F-S01 | ロール切替 | 未ログイン/無料会員/有料会員/運営 をワンクリック切替 | Must |
| F-S02 | 埋め込みモード | ?embed=1 でプレーヤー単体表示(2章) |
Must |
| F-S03 | 記事への導線 | 元記事へのリンク、問い合わせ導線 | Must |
| F-S04 | デモリセット | localStorage をクリアして初期状態へ | Must |
| F-S05 | 通知センター | 新着エピソード、シリーズ更新 | Could |
| F-S06 | ガイドツアー | 初回訪問時に「何を試せるか」を3ステップで案内 | Should |
対象外(Won't have)
| 項目 | 理由 |
|---|---|
| データベース・バックエンドAPI | 3章の方針に基づく |
| 本物の認証・実際の課金処理 | ロール切替と完了画面で代替 |
| DRM・署名付きURL・視聴制限の実効化 | デモでは実装しない。 ただし12章で必要性とコストに触れる |
| ライブ配信・リアルタイム配信 | VODに限定。要望が出た場合は別フェーズ |
| 実際のトランスコード処理 | 進捗UIの再現に留める。素材は事前エンコード |
| コメント・レビュー・コミュニティ機能 | 対象外 |
| ダウンロード視聴(オフライン再生) | 対象外 |
| 多言語UI | 日本語のみ(字幕の多言語は対象) |
| ネイティブアプリ・TVアプリ | レスポンシブWebで対応 |
スコープリスク: 動画配信は「ライブ配信も」「DRMも」と広がりやすい。どちらも単体で数週間〜の領域。要望が出た場合は12章の費用表を使って別見積として提示し、この文書のスコープは変えない。
5. ロールとデモ切替
ロール定義
| ロールID | 名称 | 視聴できる範囲 | 特徴 |
|---|---|---|---|
guest |
未ログイン | 無料コンテンツのみ。有料はプレビュー3分でゲート | 会員登録の導線を体験できる |
free |
無料会員 | 無料コンテンツ全編 + マイリスト・視聴履歴 | 有料コンテンツでペイウォールに当たる |
premium |
有料会員 | 全コンテンツ。添付資料もすべて取得可 | 制限なしの体験 |
admin |
運営 | 全コンテンツ + 管理画面 | 配信管理・分析 |
ロールを跨ぐ体験(必ず動くようにする)
- 未ログインで有料エピソードを再生 → 3分でペイウォール → 有料会員に切替 → 同じ位置から続きが再生される
- 無料会員で途中まで視聴して離脱 → リロード → 「続きから見る」に表示され、停止位置から再開する
- 運営でエピソードを「非公開」に変更 → 視聴者に切替 → カタログから消えている
- 運営で新規エピソードを公開予約(1分後) → 待つ → 視聴者側のカタログに出現する
- 視聴した動画が、運営の視聴分析にカウントされ、離脱ヒートマップに反映される
デモ切替バー(F-S01)
画面上部に常時表示する固定バー。ただし埋め込みモードでは非表示。
- 現在のロール表示と4ロールの切替
- 「デモをリセット」ボタン(確認ダイアログ付き)
- 元記事へ戻るリンク
- 「これはデモです」の明示
デザイン上の扱い: アプリ本体がダークテーマなので、切替バーは逆に明るいグレーにして区別する。プロダクトのトークンを使わない。
6. 画面一覧とユーザーフロー
全28画面。画面IDはディレクトリ構成と 1:1 で対応させる。
視聴者画面
| 画面ID | 画面名 | パス | 主要要素 |
|---|---|---|---|
| SC-001 | トップ | / |
ヒーロー(自動再生なしの背景動画)、続きから見る、カテゴリ別レーン、ランキング、新着 |
| SC-002 | 視聴(プレーヤー) | /watch/[episodeId] |
プレーヤー、エピソード情報、チャプター一覧、添付資料、次の話、同シリーズ |
| SC-003 | 埋め込みプレーヤー | /watch/[episodeId]?embed=1 |
プレーヤーのみ。全画面デモへのリンク |
| SC-004 | シリーズ詳細 | /series/[seriesId] |
シリーズ概要、エピソード一覧(進捗付き)、全体進捗率、マイリスト追加 |
| SC-005 | カタログ | /browse |
カテゴリ絞り込み、並び替え、グリッド表示 |
| SC-006 | 検索結果 | /search |
フリーワード、絞り込みチップ、シリーズ/エピソードのタブ |
| SC-007 | カテゴリ一覧 | /browse/[categorySlug] |
カテゴリ別のシリーズ一覧 |
| SC-010 | マイページ | /my |
続きから見る、マイリスト、視聴履歴のタブ |
| SC-011 | 視聴履歴 | /my/history |
時系列、進捗率、削除 |
| SC-012 | マイリスト | /my/list |
保存したシリーズ・エピソード |
| SC-013 | メモ一覧 | /my/notes |
タイムスタンプ付きメモ。クリックで該当位置へ |
| SC-014 | 再生設定 | /my/settings |
既定画質・速度・自動再生・字幕表示設定 |
| SC-020 | プラン比較 | /plans |
3プランの比較表、視聴可能範囲、FAQ |
| SC-021 | 申込フォーム | /plans/[planId]/checkout |
会員情報、支払い方法(入力UIのみ)、確認 |
| SC-022 | 申込完了 | /plans/complete |
完了、視聴開始への導線 |
| SC-023 | 契約状況 | /my/subscription |
現在のプラン、変更、解約(デモ上での状態変更) |
管理画面
| 画面ID | 画面名 | パス | 主要要素 |
|---|---|---|---|
| SC-100 | 管理ダッシュボード | /admin |
本日の再生数、公開待ち、人気コンテンツ、会員数推移 |
| SC-110 | エピソード一覧 | /admin/episodes |
ステータス別タブ、一括操作、絞り込み |
| SC-111 | アップロード | /admin/episodes/upload |
ドロップ、エンコード進捗の再現、失敗ケース |
| SC-112 | エピソード編集 | /admin/episodes/[id] |
基本情報、サムネイル選択、公開設定、公開予約、無料/有料 |
| SC-113 | 字幕編集 | /admin/episodes/[id]/captions |
タイムライン、行ごとの文言修正、プレビュー |
| SC-114 | チャプター編集 | /admin/episodes/[id]/chapters |
再生位置を見ながら追加・編集 |
| SC-115 | 添付資料管理 | /admin/episodes/[id]/files |
ファイル紐付け、公開範囲 |
| SC-120 | シリーズ管理 | /admin/series |
シリーズCRUD、エピソードの並び替え |
| SC-130 | 会員管理 | /admin/members |
一覧、プラン、視聴状況、停止 |
| SC-131 | プラン管理 | /admin/plans |
プランCRUD、価格、視聴可能範囲 |
| SC-140 | 視聴分析 | /admin/analytics |
再生数推移、視聴完了率、離脱ヒートマップ、デバイス別、流入元 |
主要フロー
フローA:記事の読者が動画に触れる(最重要)
SEO記事を読んでいる
↓
記事内の埋め込みプレーヤー(ポスター画像 + 再生ボタンのみ表示、まだ何も読み込んでいない)
↓
再生ボタンを押す ── ここで初めて hls.js とマニフェストを取得
↓
再生開始(3秒以内)── 画質切替・速度・字幕を触ってみる
↓
「全画面でデモを開く」→ 別タブでアプリ本体へ
↓
SC-001 トップ ── ガイドツアーが3ステップで「試せること」を案内
↓
SC-004 シリーズ詳細 → SC-002 視聴
↓
デモ切替バーで「運営」に切替 → SC-140 視聴分析
↓ ★ たった今自分が見た再生が、グラフに反映されている
問い合わせ導線 or 元記事に戻る
フローB:ペイウォールの体験
SC-001 トップ(未ログイン)── 有料バッジ付きのエピソードを選ぶ
↓
SC-002 視聴 ── 再生開始。プレーヤー上部に「無料プレビュー 残り2:47」
↓
3分経過 ── 再生が停止し、ペイウォールがオーバーレイ表示
「続きを見るには有料プランへの登録が必要です」+ プラン概要 + CTA
↓
SC-020 プラン比較 → SC-021 申込フォーム → SC-022 完了
↓ ※ 決済処理は行わない。ロールが premium に変わる
SC-002 に戻る ── ★ 停止した3分00秒の位置から、そのまま続きが再生される
フローC:運営がエピソードを公開する
SC-111 アップロード ── ファイルをドロップ
↓
エンコード進捗の再現
待機中 → 変換中(0〜100%、解像度ごとに段階表示)→ サムネイル生成 → 完了
※ 一定確率で「変換失敗」を再現し、リトライ導線を見せる
↓
SC-112 エピソード編集
タイトル・説明・サムネイル選択(3候補から)
シリーズへの割当、無料/有料の区分
公開設定:下書き/即時公開/公開予約(日時指定)
↓
SC-114 チャプター編集 ── 再生しながらチャプターを打つ
↓
SC-115 添付資料 ── PDFを紐付け、公開範囲を「有料会員のみ」に
↓
公開
↓
【ロール切替:無料会員】
SC-001 トップの「新着」に出現。★ 添付資料はロックされている
↓
【ロール切替:有料会員】
★ 添付資料がダウンロード可能になっている
7. 機能要件
7.1 プレーヤー(このデモの心臓部)
| ID | 要件 | 優先 |
|---|---|---|
| FR-101 | HLS(.m3u8)を再生できること。ネイティブHLS対応環境(Safari)ではネイティブを使い、非対応環境では hls.js を使うこと |
P1 |
| FR-102 | 再生/一時停止/シーク/音量/ミュート/全画面/PiP を操作できること | P1 |
| FR-103 | 画質を「自動/1080p/720p/360p」から選択でき、切替時に再生位置と再生状態を維持すること。現在の実効画質を表示すること | P1 |
| FR-104 | 再生速度を 0.75 / 1.0 / 1.25 / 1.5 / 2.0 倍から選択できること | P1 |
| FR-105 | 字幕を「オフ/日本語/英語」から選択できること。表示サイズ・文字色・背景の不透明度を設定できること | P1 |
| FR-106 | チャプターがある場合、シークバー上に区切りを表示し、現在のチャプター名をバー上部に表示すること | P1 |
| FR-107 | チャプター一覧からクリックで該当位置へジャンプできること | P1 |
| FR-108 | キーボード操作:Space/K 再生停止、←→ 5秒シーク、J/L 10秒シーク、↑↓ 音量、F 全画面、M ミュート、C 字幕、0-9 位置ジャンプ、? ショートカット一覧 |
P1 |
| FR-109 | 視聴位置を5秒ごとに保存し、次回アクセス時にその位置から再開できること。「最初から見る」も選べること | P1 |
| FR-110 | 視聴完了(95%以上)した場合、進捗を「視聴済」とし、次回は最初から再生すること | P1 |
| FR-111 | シークバーのホバー/ドラッグ中に、その位置のサムネイルプレビューを表示すること | P2 |
| FR-112 | 終了30秒前に「次のエピソード」カードを表示し、カウントダウン後に自動再生すること。自動再生は設定でOFFにでき、カードからキャンセルできること | P2 |
| FR-113 | 通信状況の悪化時にバッファリング表示を出し、自動で下位画質へ切り替わること | P1 |
| FR-114 | 再生に失敗した場合、原因(ネットワーク/非対応形式/読み込み失敗)を区別したメッセージと再試行ボタンを表示すること。無言で止まらないこと | P1 |
| FR-115 | 3秒間操作がない場合、コントロールを自動的に隠すこと。ただし一時停止中は隠さない | P1 |
| FR-116 | モバイルではダブルタップで前後10秒シークできること。ピンチで全画面に入れること | P2 |
| FR-117 | 再生中にタイムスタンプ付きメモを残せること。メモ一覧からクリックでその位置へジャンプできること | P3 |
7.2 埋め込みモード
| ID | 要件 | 優先 |
|---|---|---|
| FR-151 | ?embed=1 でヘッダー・サイドバー・ロール切替バーを非表示にし、プレーヤーのみを表示すること |
P1 |
| FR-152 | 埋め込みモードの初期表示はポスター画像と再生ボタンのみとし、hls.js とマニフェストの取得を再生ボタン押下まで遅延させること | P1 |
| FR-153 | 埋め込みモードではアプリ内の画面遷移を発生させないこと。「全画面で開く」は target="_blank" とすること |
P1 |
| FR-154 | 埋め込みモードで音声ありの自動再生を行わないこと | P1 |
| FR-155 | 埋め込みモードのプレーヤーは、親要素の幅に追従し、16:9 を維持すること | P1 |
7.3 カタログ・視聴体験
| ID | 要件 | 優先 |
|---|---|---|
| FR-201 | トップに「続きから見る」を最上部に配置し、進捗率のバー付きで表示すること。視聴中の作品がない場合はこのレーンを表示しないこと | P1 |
| FR-202 | カテゴリ別レーンを横スクロールで表示し、キーボードの矢印キーでも移動できること | P1 |
| FR-203 | サムネイルのホバー(デスクトップのみ)で、数秒のプレビューを再生すること。モバイルでは行わないこと | P2 |
| FR-204 | シリーズ詳細で、各エピソードの視聴状態(未視聴/視聴中N%/視聴済)とシリーズ全体の進捗率を表示すること | P1 |
| FR-205 | 検索はタイトル・説明・シリーズ名・タグを対象とし、表記ゆれ(ひらがな/カタカナ/半角全角)を吸収すること | P1 |
| FR-206 | 絞り込みとして、カテゴリ、タグ、再生時間(〜5分/5〜15分/15分〜)、視聴状態、無料/有料 を提供すること | P1 |
| FR-207 | 絞り込み条件をURLクエリに反映し、リロード・共有で再現できること | P1 |
| FR-208 | エピソードに添付資料(PDF等)を紐付け、視聴画面から閲覧・ダウンロードできること。公開範囲(全員/会員/有料会員)に応じてロック表示すること | P1 |
| FR-209 | シリーズ・エピソードをマイリストに保存でき、マイページから確認できること | P2 |
| FR-210 | 視聴履歴を時系列で表示し、個別削除・全削除ができること | P2 |
| FR-211 | 再生設定(既定画質・既定速度・自動再生・字幕の既定表示)を保存し、次回以降の再生に適用すること | P2 |
7.4 会員・ペイウォール
| ID | 要件 | 優先 |
|---|---|---|
| FR-301 | エピソードは「無料/有料」の区分を持ち、カタログ上でバッジ表示すること | P1 |
| FR-302 | 未ログイン・無料会員が有料エピソードを再生した場合、既定3分のプレビュー後に再生を停止し、ペイウォールをオーバーレイ表示すること | P1 |
| FR-303 | プレビュー中、残り時間をプレーヤー上部に常時表示すること(「無料プレビュー 残り2:47」) | P1 |
| FR-304 | ペイウォールには、プラン概要・視聴できるようになる内容・CTA を含めること。閉じて他の作品を探す導線も置くこと | P1 |
| FR-305 | プラン加入後、プレビューが停止した位置から続きを再生できること | P1 |
| FR-306 | プラン比較画面で3プラン(無料/スタンダード/プレミアム)の視聴可能範囲を表で示すこと | P1 |
| FR-307 | 申込フォームは会員情報と支払い方法の入力UIを持つこと。入力値の検証は実装し、送信時に課金処理は行わないこと。デモである旨を画面上に明示すること | P1 |
| FR-308 | 申込完了でロールが premium に変わり、切替バーの表示も更新されること |
P1 |
| FR-309 | 契約状況画面からプラン変更・解約ができ、解約すると視聴範囲が即座に制限されること | P2 |
| FR-310 | プレビュー時間はプラン設定・エピソード設定で変更でき、管理画面から編集できること | P3 |
7.5 配信管理
| ID | 要件 | 優先 |
|---|---|---|
| FR-401 | 動画ファイルをドラッグ&ドロップでアップロードできること。対応形式・最大サイズを事前に明示すること | P1 |
| FR-402 | アップロード後、エンコード進捗を段階表示で再現すること:アップロード中(%)→ 待機中 → 変換中(解像度ごと)→ サムネイル生成 → 完了 | P1 |
| FR-403 | エンコード完了後の再生は、事前に用意した素材で代替すること。この挙動をユーザーに誤解させないよう、デモである旨を管理画面上に明示すること | P1 |
| FR-404 | 一定確率で「変換失敗」を再現し、原因表示とリトライ導線を見せること | P2 |
| FR-405 | エピソード編集で、タイトル・説明・シリーズ割当・カテゴリ・タグ・無料/有料区分 を設定できること | P1 |
| FR-406 | サムネイルを3候補から選択、または任意の画像をアップロードして設定できること | P1 |
| FR-407 | 公開設定として「下書き/即時公開/公開予約(日時指定)/非公開」を選べること | P1 |
| FR-408 | 公開予約した日時が到来すると、視聴者側のカタログに自動的に出現すること(クライアント側のタイマーで判定) | P1 |
| FR-409 | 非公開にしたエピソードが、視聴者側のカタログ・検索結果から即座に消えること | P1 |
| FR-410 | 字幕ファイル(WebVTT)をアップロードでき、タイムライン上で行ごとに文言を修正できること。修正結果をプレーヤーでプレビューできること | P2 |
| FR-411 | チャプターを、再生位置を確認しながら追加・編集・削除できること。「現在位置で追加」ができること | P2 |
| FR-412 | エピソードに添付資料を紐付け、公開範囲(全員/会員/有料会員)を設定できること | P1 |
| FR-413 | シリーズを作成し、エピソードをドラッグで並び替えられること | P1 |
| FR-414 | 複数エピソードを選択して、公開/非公開・カテゴリ付与を一括実行できること | P2 |
| FR-415 | 会員一覧で、プラン・登録日・最終視聴日・視聴本数を確認でき、停止できること | P2 |
| FR-416 | プランを作成・編集でき、価格と視聴可能範囲を設定できること | P2 |
7.6 視聴分析
| ID | 要件 | 優先 |
|---|---|---|
| FR-501 | 再生数・総視聴時間・平均視聴完了率を、期間指定(今日/7日/30日/任意)で表示すること | P1 |
| FR-502 | 離脱ヒートマップ:エピソードの再生位置ごとの視聴者残存率を折れ線で表示し、離脱が大きい区間を強調すること | P1 |
| FR-503 | 視聴完了率をエピソード別にランキング表示すること | P2 |
| FR-504 | デバイス別(PC/スマホ/タブレット)、画質別の再生比率を表示すること | P2 |
| FR-505 | シリーズごとの継続率(第1話→第2話→…の視聴継続)をファネルで表示すること | P2 |
| FR-506 | デモ中に視聴した実績が、これらの分析に反映されること(シードの統計に加算される) | P1 |
| FR-507 | 分析データをCSVでエクスポートできること | P3 |
7.7 デモ基盤
| ID | 要件 | 優先 |
|---|---|---|
| FR-601 | 4ロール(未ログイン/無料会員/有料会員/運営)をワンクリックで切り替えられること | P1 |
| FR-602 | 「デモをリセット」で localStorage をクリアし、確認ダイアログを経て初期状態に戻ること | P1 |
| FR-603 | 視聴進捗・マイリスト・契約状態がリロード後も保持されること | P1 |
| FR-604 | 元記事へ戻るリンクを常設すること | P1 |
| FR-605 | 初回訪問時に3ステップのガイドツアーを表示し、「今後表示しない」を選べること | P2 |
| FR-606 | ロール権限外の画面(無料会員で /admin)にアクセスした場合、案内画面からロール切替できること。素の404を出さないこと |
P1 |
| FR-607 | 権限で不可の操作はボタンを非活性にし、理由をツールチップで示すこと。実行してからエラーを出さないこと | P1 |
| FR-608 | データスコープは lib/repo/_scope.ts に集約し、全リポジトリメソッドが通すこと |
P1 |
8. データ設計
型定義(lib/types/)
// ---- コンテンツ ----
type Series = {
id: string
title: string
slug: string
description: string
categoryId: string
tagIds: string[]
coverUrl: string // 縦長(カタログ用)
heroUrl: string // 横長(トップのヒーロー用)
episodeIds: string[] // 並び順を保持
isPublished: boolean
publishedAt: string
createdAt: string
}
type Episode = {
id: string
seriesId: string
number: number // 第N話
title: string
description: string
durationSec: number
// --- 配信情報(実在するファイルを指す) ---
hlsUrl: string // master.m3u8 のURL
posterUrl: string
spriteUrl: string // シークプレビュー用スプライト
spriteMeta: { cols: number; rows: number; interval: number; w: number; h: number }
captions: Caption[]
chapters: Chapter[]
// --- 公開・課金 ---
access: 'free' | 'paid'
previewSec: number // 有料時のプレビュー秒数(既定180)
status: 'draft' | 'scheduled' | 'published' | 'unlisted' | 'private'
scheduledAt?: string // 公開予約日時(FR-408)
publishedAt?: string
// --- 付随 ---
attachmentIds: string[]
tagIds: string[]
createdAt: string
updatedAt: string
}
type Caption = { id: string; lang: 'ja' | 'en'; label: string; vttUrl: string; isDefault: boolean }
type Chapter = { id: string; startSec: number; title: string }
type Attachment = {
id: string
episodeId: string
name: string
fileType: 'pdf' | 'slide' | 'zip' | 'other'
sizeBytes: number
url: string
visibility: 'public' | 'member' | 'premium' // FR-412
}
type Category = { id: string; name: string; slug: string; sortOrder: number }
type Tag = { id: string; name: string }
// ---- 会員・プラン ----
type Plan = {
id: string
name: string // 無料 / スタンダード / プレミアム
priceMonthly: number
description: string
features: string[]
accessScope: 'free_only' | 'all'
sortOrder: number
}
type Member = {
id: string
name: string
email: string
avatarUrl: string
role: 'guest' | 'free' | 'premium' | 'admin'
planId: string
subscribedAt?: string
status: 'active' | 'suspended' | 'cancelled'
lastWatchedAt?: string
watchedCount: number
createdAt: string
}
// ---- 視聴 ----
type WatchProgress = {
episodeId: string
memberId: string
positionSec: number // 5秒ごとに更新(FR-109)
durationSec: number
percent: number // 派生値だが検索性のため保持
isCompleted: boolean // 95%以上(FR-110)
lastWatchedAt: string
}
type WatchEvent = { // 分析の元データ
id: string
episodeId: string
memberId: string
type: 'start' | 'progress' | 'complete' | 'abandon' | 'quality_change' | 'seek'
positionSec: number
quality?: '1080p' | '720p' | '360p'
device: 'desktop' | 'mobile' | 'tablet'
occurredAt: string
}
type Note = { id: string; episodeId: string; memberId: string; positionSec: number; body: string; createdAt: string }
type ListItem = { id: string; memberId: string; targetType: 'series' | 'episode'; targetId: string; addedAt: string }
// ---- 分析(集計値。ストアには保存せず lib/calc で算出) ----
type RetentionPoint = { positionSec: number; retainedRatio: number } // 離脱ヒートマップ(FR-502)
type EpisodeStats = {
episodeId: string
plays: number
uniqueViewers: number
totalWatchSec: number
avgCompletionRate: number
retention: RetentionPoint[]
byDevice: Record<'desktop'|'mobile'|'tablet', number>
byQuality: Record<'1080p'|'720p'|'360p', number>
}
// ---- アップロード(擬似) ----
type UploadJob = {
id: string
fileName: string
sizeBytes: number
stage: 'uploading' | 'queued' | 'transcoding' | 'thumbnailing' | 'done' | 'failed'
progress: number // 0-100
transcodeProgress: Record<'1080p'|'720p'|'360p', number>
errorReason?: string
linkedEpisodeId?: string // 完了時に既存素材へ紐付け(FR-403)
createdAt: string
}
シードデータ(lib/seed/)
| ファイル | 内容 | 件数 |
|---|---|---|
categories.ts tags.ts |
カテゴリ6、タグ24 | — |
series.ts |
シリーズ | 5本 |
episodes.ts |
エピソード(実在する動画URLを指す) | 12〜16本 |
attachments.ts |
添付資料(公開範囲を分散) | 10件 |
plans.ts |
プラン | 3件 |
members.ts |
会員(無料/有料/停止中を分散、過去90日で登録日を散らす) | 240名 |
watchEvents.ts |
視聴イベント(過去90日分。離脱ヒートマップが意味を持つ分布で生成) | 12,000件 |
progress.ts |
操作用ペルソナの視聴進捗(視聴中2本、視聴済3本) | — |
シード作成のルール(品質を左右する)
- シリーズ構成は実在しそうな講座にする。 例:「Claude Code入門」「業務システムの作り方」「AI活用の基礎」等。タイトルの使い回し禁止
- エピソードの説明文を書き分ける。 「第N話の説明です」のようなダミーを残さない
- 視聴イベントは自然な離脱曲線で生成する。 冒頭30秒で15%落ち、以降なだらかに減衰、終盤で微増(まとめ部分)。均等分布だと分析画面が無意味になる
- エピソードごとに離脱パターンを変える。 途中に大きな離脱がある回を意図的に作る(分析の価値が伝わる)
- 日付は現在日時からの相対で生成する。固定日付を埋め込まない
- 「続きから見る」が最初から表示されるよう、ペルソナに視聴途中のエピソードを持たせる。 トップが空だとデモとして成立しない
- 公開予約されたエピソードを1本含める(フローCの実演用)
ストアとリポジトリ
lib/
├── types/ # 上記の型定義
├── seed/
├── store/
│ ├── session.ts # ロール、操作中の会員
│ ├── content.ts # series / episodes / attachments / categories
│ ├── watch.ts # progress / events / notes / list
│ └── ui.ts # 再生設定、ガイドツアー表示済みフラグ
└── repo/ # ★ コンポーネントが触るのはここだけ
├── _delay.ts _scope.ts
├── episodes.ts series.ts attachments.ts
├── watch.ts members.ts plans.ts uploads.ts analytics.ts
リポジトリ層の規約(厳守)
- 全メソッドを
asyncで定義。中身が同期でも例外なく - 戻り値は
{ ok: true; data: T } | { ok: false; error: string }に統一 - コンポーネントから Zustand ストアを直接参照しない
- 視聴可否の判定(
canWatch(episode, role))はlib/repo/_scope.tsに集約する。 コンポーネント側でrole === 'premium'のような分岐を書かない(FR-608) - 派生値(進捗率、離脱率、完了率)はストアに保存せず
lib/calc/で算出する - 再生開始に関わるメソッドには擬似ディレイを入れない
// lib/repo/_scope.ts — 実案件では署名付きURL発行・DRM に置き換わる箇所
export function resolveAccess(ep: Episode, role: Role): AccessResult {
if (ep.access === 'free') return { kind: 'full' }
if (role === 'premium' || role === 'admin') return { kind: 'full' }
return { kind: 'preview', limitSec: ep.previewSec } // guest / free
}
9. デザイン要件
アートディレクション
「画面を暗くして、映像に光を渡す」
動画配信のUIは、うまくいっているとき存在に気づかれない。CASTA は暗い背景に沈み、サムネイルと再生中の映像だけが明るい。装飾・グラデーション・不要な枠線を持たない。
- ダークテーマを既定とする。 ライトテーマは実装しない(切替を作らない)
- サムネイルが唯一の色。 UI自体はほぼ無彩色。彩度はコンテンツ側にある
- アクセントは1色だけ。 再生・進捗・現在地にのみ使う
- コントロールは消える。 3秒で自動的に隠れ、必要なときだけ現れる
HIREBASE / RELATE との差別化(重要) HIREBASE:明朝・白・広い余白(読ませる) RELATE:ゴシック・白・高密度・等幅数字(操作させる) CASTA:ダーク・映像優先・UIが消える(見せる) 3つ並べたとき、別の会社が作ったように見えることが理想。トークンをコピーしない。
カラートークン
:root {
/* 背景は3段階の黒で奥行きを作る */
--bg-base: #0A0B0D; /* ページ背景 */
--bg-raised: #131519; /* カード・パネル */
--bg-overlay: #1C1F25; /* モーダル・ドロップダウン */
--fg-high: #F2F3F5; /* 見出し・本文 */
--fg-mid: #A0A6B0; /* 補助テキスト */
--fg-low: #6B7280; /* ラベル・非活性 */
--accent: #E8503A; /* 再生・進捗・現在地。★これ以外の色を持ち込まない */
--accent-dim: rgba(232,80,58,.18);
--border: #24272E; /* 罫線。極めて控えめに */
--border-hi: #343943; /* フォーカス時 */
/* シグナル(管理画面のみで使用) */
--ok: #3FA37A; --warn: #C8933A; --danger: #D0544A;
/* プレーヤー内グラデーション(コントロールの視認性確保) */
--scrim: linear-gradient(to top, rgba(0,0,0,.85) 0%, rgba(0,0,0,0) 100%);
}
配色ルール
- アクセント
--accentは「再生・進捗・現在地」にのみ使う。 有料バッジ・エラー・装飾に使わない - 有料バッジは色ではなく枠線とアイコンで表現する。カタログが赤いバッジだらけになると台無しになる
- カード同士は影ではなく背景の明度差で分ける(
--bg-baseの上に--bg-raised) - プレーヤーのコントロールは、映像の明暗に負けないよう必ず
--scrimの上に置く
タイポグラフィ
| 役割 | 書体 | 用途 |
|---|---|---|
| 見出し | Noto Sans JP 700 | 作品タイトル、セクション見出し。字間を詰める(letter-spacing: -.01em) |
| 本文 | Noto Sans JP 400 | 説明文、UI |
| 時間表示 | Roboto Mono 500(tabular-nums) |
再生時間、経過時間、チャプター時刻 |
| トークン | デスクトップ | モバイル | 用途 |
|---|---|---|---|
| hero | 36px / 700 | 24px | ヒーローの作品タイトル |
| title | 20px / 700 | 18px | 作品タイトル |
| card | 14px / 500 | 13px | カード内タイトル(2行クランプ) |
| body | 14px / 400 | 14px | 説明文 |
| meta | 12px / 400 | 12px | 再生時間、話数、カテゴリ |
| time | 13px / 500 Mono | 12px | プレーヤーの時間表示 |
ダーク背景では文字が太って見える。 ライト背景と同じウェイトを使わず、本文は 400、見出しは 700 で明確に差をつける(500・600 を多用しない)。
レイアウトとスペーシング
| 項目 | 定義 |
|---|---|
| スペーシング | 4pxベース:4 / 8 / 12 / 16 / 24 / 32 / 48 / 64 |
| ヘッダー | 64px。スクロールで背景が透明→不透明に変化(トップのヒーローを活かすため) |
| コンテンツ幅 | 最大1440px。レーンは画面端まで伸ばし、見切れを作る(横スクロールできることを示すため) |
| カード比率 | サムネイル 16:9、シリーズカバー 2:3 |
| レーンのカード幅 | デスクトップ 280px / タブレット 220px / モバイル 160px |
| 角丸 | 6px(カード・サムネイル)/4px(ボタン・バッジ)/999px(アバター・チップ) |
| 影 | 使わない。 明度差で階層を表現する。例外はモーダルのみ |
| プレーヤー | 16:9 を維持。デスクトップ最大幅1280px、モバイルは画面幅いっぱい |
プレーヤーUI仕様(最重要コンポーネント)
┌──────────────────────────────────────────────┐
│ [無料プレビュー 残り2:47] ← 有料時のみ、上部左 │
│ │
│ 映像 │
│ │
│ ┌──── scrim グラデーション ────────────────┐ │
│ │ ● チャプター名(現在地) │ │
│ │ ━━━━━━━╋━━━━┃━━━━━━━━━━━━━━ ← シークバー│ │
│ │ ▶ ⏭ 🔊━━ 1:24 / 8:15 ⚙ CC ⛶ ⧉ │ │
│ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
| 要素 | 仕様 |
|---|---|
| シークバー | 高さ通常4px、ホバーで6px。バッファ済みは --fg-low の30%、再生済みは --accent。チャプター区切りは2pxの隙間で表現 |
| シークプレビュー | ホバー位置の上にサムネイル(160×90)+ 時刻。チャプター名も併記 |
| 再生ボタン | 中央の大型ボタンは初回のみ。以降は下部コントロールに集約 |
| 設定メニュー(⚙) | 画質/速度の2階層。現在値を親メニューに併記する(「画質 自動(720p)」) |
| 字幕ボタン(CC) | オン時はアクセント色。長押し/右クリックで字幕設定へ |
| コントロール自動非表示 | 3秒。一時停止中・設定メニュー展開中は隠さない(FR-115) |
| バッファリング | 中央に控えめなインジケータ。映像を暗転させない |
| エラー | 中央にメッセージ + 再試行ボタン。原因を区別して表示(FR-114) |
| モバイル | コントロールを大きめに。ダブルタップシークは左右に波紋アニメーション |
主要コンポーネント仕様
| コンポーネント | 仕様 |
|---|---|
| コンテンツカード | サムネイル16:9/再生済みは右下にチェック/視聴中は下端に進捗バー(--accent、高さ3px)/有料は左上に枠線バッジ |
| ホバープレビュー | デスクトップのみ。600ms 停留で拡大 + 無音プレビュー再生。モバイルでは実装しない(FR-203) |
| 横スクロールレーン | 端で見切れを作る。左右の矢印はホバーで出現。キーボード矢印でも移動 |
| ヒーロー | 背景に静止画(動画背景は使わない — 帯域とバッテリーを浪費するため)。タイトル・説明2行・再生ボタン・詳細ボタン |
| エピソードリスト行 | サムネイル/話数・タイトル/再生時間/進捗バー/有料バッジ/説明1行 |
| ペイウォール | 映像の上にオーバーレイ。暗転 + ブラー。プラン概要・CTA・閉じる導線を必ず持つ |
| 添付資料カード | ファイル名・種別アイコン・サイズ・ダウンロードボタン。ロック時は鍵アイコン + 必要プラン名 |
| アップロード進捗 | 段階を横並びのステップで表示。現在の段階のみ進捗%を表示 |
| 離脱ヒートマップ | 横軸=再生位置、縦軸=残存率の折れ線。大きな離脱区間を --danger で塗り、注釈を出す |
| 空状態 | 全一覧に専用の空状態。ダーク背景に合う線画イラスト |
| スケルトン | カード形状。ダーク背景では明るいシマー(--bg-raised → --bg-overlay) |
| デモ切替バー | アプリ本体がダークなので、切替バーは明るいグレーにして区別する |
モーション
| 対象 | duration | 内容 |
|---|---|---|
| コントロールの表示・非表示 | 200ms | フェード。位置は動かさない |
| ホバープレビューの拡大 | 300ms(600ms 停留後) | scale 1.0 → 1.12 + z-index 上げ |
| レーンの横スクロール | 400ms | ease-out |
| モーダル・ペイウォール | 240ms | フェード + ブラー |
| 進捗バーの更新 | なし | **アニメーションさせない。**実時間を表すため |
| 次のエピソードのカウントダウン | — | 円形プログレスで残り時間を可視化 |
prefers-reduced-motion: reduce 時は全アニメーションを無効化し、ホバープレビューの自動再生も停止すること。
アクセシビリティ(WCAG 2.1 AA)
動画プレーヤーは a11y の要求が特に高い領域。ここを作り込めることが差別化になる。
| ID | 要件 |
|---|---|
| A11Y-01 | ダーク背景でのコントラスト比は通常4.5:1以上、18px以上は3:1以上。--fg-mid の使用箇所を実測で検証すること |
| A11Y-02 | プレーヤーの全操作をキーボードで完遂できること(FR-108) |
| A11Y-03 | 字幕を必ず提供すること。字幕のサイズ・背景不透明度をユーザーが調整できること |
| A11Y-04 | 音声のみでは伝わらない情報がある動画に、説明を本文で補うこと |
| A11Y-05 | 音声ありの自動再生を行わないこと(FR-154) |
| A11Y-06 | 3回/秒を超える点滅を含むコンテンツを使用しないこと |
| A11Y-07 | フォーカスリングは2px・オフセット2pxで常時可視。映像の上でも視認できる色にすること |
| A11Y-08 | シークバーは role="slider" とし、aria-valuenow / aria-valuetext(「1分24秒」)を更新すること |
| A11Y-09 | 再生状態の変化を aria-live="polite" で通知すること |
| A11Y-10 | ペイウォール表示時にフォーカスをトラップし、Escで閉じられること |
| A11Y-11 | 情報を色のみで伝えない。視聴済み・有料はアイコンとテキストを併記すること |
ライティング規約
| 原則 | 例 |
|---|---|
| 再生状態は簡潔に | ○「無料プレビュー 残り2:47」/×「あなたは現在無料プレビューを視聴しています」 |
| ペイウォールは責めない | ○「続きは有料プランでご覧いただけます」/×「視聴権限がありません」 |
| エラーは原因と対処 | ○「通信が不安定なため再生できません。再試行するか、画質を下げてお試しください」/×「エラーが発生しました」 |
| ロックは必要条件を書く | ○「この資料はプレミアムプランでダウンロードできます」/×「ダウンロードできません」 |
| 空状態は誘導する | ○「まだ視聴履歴がありません。おすすめから探してみましょう」+ ボタン |
| システム語を使わない | ○「公開を予約する」/×「statusをscheduledに設定」 |
10. 技術要件・ディレクトリ構成
技術スタック(固定・勝手に変更しない)
| レイヤ | 技術 | 備考 |
|---|---|---|
| フレームワーク | Next.js 15(App Router)/ TypeScript strict | |
| スタイリング | Tailwind CSS + CSS Variables | トークンは CSS 変数で定義 |
| UIコンポーネント | shadcn/ui(Radix UI基盤) | ダークテーマに合わせて上書き |
| 状態管理 | Zustand + persist | localStorage に永続化 |
| HLS再生 | hls.js(動的import) | Safari はネイティブHLSを使う(FR-101) |
| プレーヤーUI | 自前実装 | video.js / Plyr を使わない。既製プレーヤーを貼るとデモの意味がなくなる |
| 字幕 | ネイティブ <track> + WebVTT |
|
| D&D | dnd-kit | エピソード並び替え。キーボード対応必須 |
| グラフ | Recharts | 分析画面(動的import) |
| フォーム | React Hook Form + Zod | |
| 日付 | date-fns(ja locale) | |
| アイコン | lucide-react | |
| ホスティング | Vercel | アプリ本体 |
| 動画配信 | Cloudflare R2 + CDN | 3章参照。エグレス無料 |
| エンコード | ffmpeg(scripts/encode.sh) |
ローカル実行。CIには含めない |
| テスト | Vitest / Playwright / axe-core |
上記以外のライブラリを入れる前に必ず提案し、承認を得ること。特にプレーヤーライブラリの導入は禁止。
ディレクトリ構成
/
├── CLAUDE.md
├── app/
│ ├── layout.tsx
│ ├── page.tsx # SC-001 トップ
│ ├── watch/[episodeId]/page.tsx # SC-002 / SC-003(?embed=1)
│ ├── series/[seriesId]/page.tsx # SC-004
│ ├── browse/ # SC-005, 007
│ ├── search/ # SC-006
│ ├── my/ # SC-010〜014
│ ├── plans/ # SC-020〜022
│ ├── admin/ # SC-100〜140
│ └── dev/components/ # コンポーネントカタログ
├── components/
│ ├── player/ # ★ 最重要
│ │ ├── VideoPlayer.tsx # ルート。hls.js の生存管理
│ │ ├── ControlBar.tsx
│ │ ├── SeekBar.tsx # チャプター区切り・プレビュー
│ │ ├── SettingsMenu.tsx # 画質・速度
│ │ ├── CaptionMenu.tsx
│ │ ├── ChapterList.tsx
│ │ ├── NextEpisodeCard.tsx
│ │ ├── Paywall.tsx
│ │ └── useHls.ts / useKeyboardShortcuts.ts / usePlaybackProgress.ts
│ ├── catalog/ # ContentCard, Lane, Hero, HoverPreview
│ ├── domain/ # EpisodeRow, AttachmentCard, UploadStepper, RetentionChart
│ ├── demo/ # RoleSwitcher, ResetButton, GuideTour, BackToArticle
│ └── layout/
├── lib/
│ ├── types/ seed/ store/ repo/
│ ├── calc/ # 進捗率、離脱率、完了率、継続率
│ ├── query/ # 検索・絞り込み
│ ├── validation/
│ └── utils/
├── scripts/
│ └── encode.sh # ffmpeg エンコードスクリプト
├── docs/
│ └── assets-license.md # ★ 動画・画像素材の取得元とライセンス(VID-02)
├── e2e/
└── public/
コーディング規約
- TypeScript strict。
any禁止 - ファイル名は kebab-case、コンポーネントは PascalCase
- コンポーネントから Zustand ストアを直接参照しない。必ず
lib/repo/経由 - 視聴可否の判定を
role === 'premium'のようにコンポーネントに書かない。resolveAccess()を使う - 計算ロジックは
lib/calc/に置き、コンポーネントに埋め込まない - hls.js のインスタンスは必ず破棄する。 エピソード遷移・アンマウント時に
hls.destroy()を呼ぶ。メモリリークはこのアプリで最も起きやすい不具合 <video>のイベントリスナーも同様に確実に解除する- コミットは1タスクごと。メッセージに要件IDを含める
例:
feat(player): 画質切替時の再生位置維持を実装 (FR-103)
11. 非機能要件
| ID | 要件 | 目標値 |
|---|---|---|
| NFR-01 | 再生開始までの時間(再生ボタン押下→最初のフレーム表示) | 1.5秒以下 |
| NFR-02 | LCP(トップ・モバイル4G相当) | 2.5秒以下 |
| NFR-03 | INP | 200ms以下 |
| NFR-04 | CLS | 0.1以下。プレーヤー領域は再生前からアスペクト比を確保すること |
| NFR-05 | 埋め込みモードの初期JSバンドル(gzip後) | 80KB以下(EMB-03) |
| NFR-06 | アプリ本体の初期JSバンドル(gzip後) | 200KB以下(hls.js・Recharts は動的import) |
| NFR-07 | シークバー操作中のフレームレート | 60fps維持 |
| NFR-08 | 5分間の連続再生でメモリ使用量が単調増加しないこと | — |
| NFR-09 | Lighthouse | Performance 90 / Accessibility 95 以上 |
| NFR-10 | 対応ブラウザ | Chrome / Edge / Firefox 最新2バージョン、Safari 16以降、iOS Safari 16以降、Android Chrome |
| NFR-11 | 動画配信のオリジンに適切なCORS・Cache-Controlを設定すること(セグメントは長期キャッシュ、マニフェストは短期) | — |
| NFR-12 | localStorage の容量上限に達した場合、古い視聴イベントから削除し、データを破損させないこと | — |
セキュリティ・権利(実案件への注記を含む)
| ID | 要件 |
|---|---|
| SEC-01 | 動画・画像・字幕素材の取得元とライセンスを docs/assets-license.md に記録すること(VID-02) |
| SEC-02 | 第三者の著作物(音楽・映像・キャラクター)を含む素材を使用しないこと |
| SEC-03 | ユーザー入力(メモ・管理画面の説明文)をサニタイズすること |
| SEC-04 | 申込フォームで入力された情報をどこにも送信しない。 ブラウザ内で完結し、リセットで消えることを画面上に明示すること |
実案件への転用時の注意(デモ本体の要件ではない。商談で聞かれた際に説明できるよう記録)
- 本デモの動画URLは公開されており、URLを知る第三者が直接再生できる。実サービスでは署名付きURL(有効期限付き)またはDRMが必要
- 有料コンテンツを扱う場合、特定商取引法に基づく表記、解約条件の明示、資金決済法の観点(前払式支払手段に該当するか)の確認が必要
- 会員情報は個人情報保護法の対象。利用目的の明示と安全管理措置が必要
- 本デモは会員情報をサーバーに送信しないため、これらの義務は発生しない。「デモである」旨は画面上に常時明示する
12. 費用設計
記事の主題の半分は「費用」。この章は実装要件であると同時に、記事本文の原稿素材でもある。
費用は3層に分けて説明する
| 層 | 内容 | 特徴 |
|---|---|---|
| ① 初期開発費 | 設計・実装・テスト | 一度きり。機能スコープで決まる |
| ② 固定運用費 | ホスティング、ドメイン、監視 | 視聴量に関係なくかかる。小さい |
| ③ 変動運用費(配信コスト) | 転送量・ストレージ・エンコード | 視聴時間に比例して増える。ここが読めないと事業計画が壊れる |
動画配信アプリの費用相談で最も多い失敗は、③を計算せずに始めること。 記事ではここを主軸に据える。
変動運用費の計算式(記事に載せる中核)
月間転送量(GB) = 平均ビットレート(Mbps) ÷ 8 × 平均視聴秒数 × 月間再生数 ÷ 1024
例:720p(1.8Mbps)/ 平均視聴 6分(360秒)/ 月間 10,000再生
= 1.8 ÷ 8 × 360 × 10,000 ÷ 1024
≈ 791 GB/月
この 791GB に、各サービスの単価を掛けたものが月額の配信コストになる。
| 規模 | 月間再生数 | 概算転送量 | 備考 |
|---|---|---|---|
| 小(社内研修・小規模スクール) | 1,000 | 約 79 GB | ほぼ無視できる規模 |
| 中(会員制メディア) | 10,000 | 約 791 GB | ここから配信先の選択が効いてくる |
| 大(成長したサブスク) | 100,000 | 約 7.7 TB | 転送量課金のサービスだと月額が跳ねる |
配信先の選択が費用構造を決める
| 方式 | 課金の考え方 | 向くケース | 注意点 |
|---|---|---|---|
| オブジェクトストレージ + エグレス無料CDN(Cloudflare R2 等) | ストレージ容量 + 操作回数。転送量に課金されない | 視聴時間が読めない/伸びる可能性がある | エンコードは自前。DRM・視聴制限は別途実装 |
| 汎用クラウドストレージ + CDN(S3+CloudFront 等) | ストレージ + 転送量 | 視聴量が小さく安定している | 伸びるとコストが線形に増える |
| 動画専用サービス(Cloudflare Stream / Mux 等) | 配信分数または保存分数 + 配信分数 | エンコード・DRM・分析まで任せたい | 単価は高いが、実装工数が大幅に減る |
| 汎用プラットフォームからの静的配信(Vercel 等) | 転送量 | 検証・小規模のみ | 本番の動画配信には向かない |
判断基準はシンプル: 実装工数を払って変動費を抑えるか、変動費を払って実装工数を抑えるか。月間視聴時間が読めない初期は前者、事業として伸びてDRMや詳細分析が必要になったら後者、という移行が現実的。
記事に載せる際の注意: 各サービスの単価は変動するため、この文書に金額を書き込まない。 記事執筆時点で公式の料金ページを確認し、確認日を明記して掲載すること。古い単価を載せた記事は、それだけで信頼を失う。
開発費の目安(機能スコープ別)
| スコープ | 含まれるもの | 目安 |
|---|---|---|
| 最小構成 | 動画一覧・再生(既製プレーヤー)・会員なし | 小 |
| 標準構成(本デモ相当) | 自前プレーヤー(画質切替・字幕・チャプター・レジューム)/会員制ペイウォール/配信管理CMS/視聴分析 | 中 |
| 拡張構成 | 上記 + DRM・署名付きURL・ライブ配信・アプリ配信 | 大 |
「動画配信アプリ」という言葉の幅が非常に広いことが、費用が伝わらない最大の原因。 記事ではこの3段階を明示し、デモは「標準構成」の実物であることを示す。具体的な金額はLPの価格体系に接続する。
デモ自体の運用コストを実測して記事に載せる
| ID | 要件 |
|---|---|
| COST-01 | デモ公開後、月次で「再生数・総視聴時間・転送量・実際にかかった費用」を記録すること |
| COST-02 | 記録した実測値を記事に掲載し、確認日を明記すること |
| COST-03 | 転送量が想定を大きく超えた場合に気づけるよう、上限アラートを設定すること |
**「私たちのデモは、月にこれだけ再生されて、これだけかかっています」**という一文が、どんな料金表よりも説得力を持つ。
13. 実装タスク
この順に進める。フェーズを飛ばさない。 完了時は [x] に更新する。
Phase 0 — 基盤と動画素材(3日)
0-1. 初期化
- Next.js 15 / TypeScript strict / App Router / Tailwind で初期化
- ESLint / Prettier / husky(pre-commit で lint + typecheck)
- 10章のディレクトリ構成を作成
0-2. 動画素材の準備(先にこれを終わらせる。素材がないとプレーヤーが作れない)
- 使用する動画を確定する(自社YouTube素材を第一候補)(VID-01)
-
scripts/encode.shを作成(HLS 3レンディション、fMP4、4秒セグメント) - 12〜16本をエンコードし、サムネイル(ポスター/スプライト/OGP)を生成
- 字幕(WebVTT)を3本以上、うち1本は日英2言語 (VID-05)
- チャプター(WebVTT chapters)を5本以上 (VID-06)
- Cloudflare R2 にアップロードし、CDN経由で配信できることを確認
- CORS と Cache-Control を設定(セグメントは長期、マニフェストは短期)(NFR-11)
-
docs/assets-license.mdに取得元とライセンスを記録 (VID-02, SEC-01) - 1本目に解説動画と資料PDFを配置(2章の「動画つき資料」との一体化)
0-3. 型とシード
-
lib/types/に8章の型定義をすべて実装 -
lib/seed/の全ファイル。エピソードの説明文を書き分ける。ダミー文言を残さない -
watchEvents.tsを自然な離脱曲線で生成(冒頭落ち→なだらか減衰→終盤微増)。エピソードごとにパターンを変える - ペルソナに視聴途中のエピソードを持たせる(「続きから見る」が最初から表示されるように)
- 公開予約されたエピソードを1本含める
0-4. ストアとリポジトリ
-
lib/store/{session,content,watch,ui}.tsを Zustand + persist で実装 -
lib/repo/_scope.tsにresolveAccess()を実装(FR-608) -
lib/repo/の全モジュール。全メソッド async、Result型、再生系にはディレイを入れない -
lib/calc/に 進捗率・離脱率・完了率・継続率 を実装 -
resolveAccess()とcalcの単体テストをこの時点で書く
0-5. デザインシステム
-
app/globals.cssにカラートークンを CSS 変数で定義(ダークテーマのみ) -
tailwind.config.tsから CSS 変数を参照するよう theme を拡張 - フォント(Noto Sans JP / Roboto Mono)。時間表示に
tabular-nums - shadcn/ui を導入し、ダークトークンに合わせて上書き
- 共通コンポーネント:
ContentCard/Lane/Hero/EmptyState/Skeleton(ダーク用シマー)/Badge -
prefers-reduced-motionの全体対応 -
/dev/componentsにコンポーネントカタログを作成
0-6. デモ基盤
-
RoleSwitcher(4ロール)(FR-601) - デモ切替バー。アプリがダークなので切替バーは明るいグレー
- 「デモをリセット」(FR-602)
- 元記事へ戻るリンク (FR-604)
- ロール権限外アクセス時の案内画面 (FR-606)
Phase 0 完了チェック
- R2 の動画URLをブラウザで直接開いて再生できる
-
/dev/componentsで全コンポーネントがトークン通りに表示される -
resolveAccess()のテストが全パターン通る
Phase 1 — プレーヤー(5日・最重要フェーズ)
1-1. 再生基盤
-
useHls.ts:hls.js の初期化・破棄。Safari はネイティブHLS (FR-101) -
hls.destroy()とイベントリスナー解除を確実に行う(メモリリーク対策) -
VideoPlayer.tsx:再生/一時停止/シーク/音量/ミュート/全画面/PiP (FR-102) -
ControlBar.tsx:scrim グラデーション上に配置 - 自動非表示(3秒。一時停止中・メニュー展開中は隠さない)(FR-115)
- バッファリング表示(映像を暗転させない)(FR-113)
- エラー表示(原因を区別 + 再試行)(FR-114)
1-2. シークと進捗
-
SeekBar.tsx:再生済み/バッファ済み/チャプター区切り -
role="slider"とaria-valuetext(「1分24秒」)(A11Y-08) - シークプレビュー(スプライトからサムネイル抽出 + 時刻 + チャプター名)(FR-111)
-
usePlaybackProgress.ts:5秒ごとに進捗保存 (FR-109) - レジューム再生と「最初から見る」(FR-109)
- 95%以上で「視聴済」判定 (FR-110)
1-3. 設定・字幕・チャプター
-
SettingsMenu.tsx:画質(自動/1080/720/360)。切替時に再生位置と再生状態を維持 (FR-103) - 現在の実効画質の表示(「画質 自動(720p)」)
- 再生速度 5段階 (FR-104)
-
CaptionMenu.tsx:オフ/日本語/英語、サイズ・背景の調整 (FR-105, A11Y-03) - チャプター区切りの表示と現在チャプター名 (FR-106)
-
ChapterList.tsx:クリックでジャンプ (FR-107)
1-4. キーボードとモバイル
-
useKeyboardShortcuts.ts:Space/K/←→/J/L/↑↓/F/M/C/0-9/?(FR-108) - ショートカット一覧のオーバーレイ
- モバイル:ダブルタップシーク(波紋アニメーション)、大きめコントロール (FR-116)
- 映像の上でも視認できるフォーカスリング (A11Y-07)
1-5. 視聴画面と埋め込み
- SC-002 視聴画面(エピソード情報、チャプター、添付資料、次の話、同シリーズ)
-
NextEpisodeCard.tsx:終了30秒前に表示、カウントダウン、キャンセル可 (FR-112) - SC-003 埋め込みモード (FR-151〜155)
- ポスター + 再生ボタンのみの初期表示、hls.js の動的import (FR-152, EMB-02, EMB-03)
- 埋め込みモードのバンドルサイズを実測し 80KB以下を確認
Phase 1 完了チェック
- 再生開始まで1.5秒以下(NFR-01)
- 画質を切り替えても再生位置と再生状態が維持される
- キーボードのみでプレーヤーの全操作ができる
- エピソードを10回続けて切り替えてもメモリが単調増加しない(NFR-08)
- 埋め込みモードの初期バンドルが80KB以下
Phase 2 — カタログと会員(4日)
2-1. カタログ
- SC-001 トップ(ヒーロー、続きから見る、カテゴリ別レーン、ランキング、新着)(FR-201)
-
Lane.tsx:横スクロール、端で見切れ、矢印、キーボード移動 (FR-202) -
HoverPreview:デスクトップのみ、600ms停留で拡大 + 無音プレビュー (FR-203) - SC-004 シリーズ詳細(エピソード一覧、各話の進捗、全体進捗率)(FR-204)
- SC-005 カタログ / SC-007 カテゴリ一覧
- SC-006 検索(表記ゆれ吸収)(FR-205)
- 絞り込み(カテゴリ/タグ/再生時間/視聴状態/無料有料)+ URL同期 (FR-206, FR-207)
2-2. マイページ
- SC-010 マイページ / SC-011 視聴履歴 (FR-210) / SC-012 マイリスト (FR-209)
- SC-014 再生設定(既定画質・速度・自動再生・字幕)と再生への適用 (FR-211)
2-3. ペイウォール(デモの見せ場)
- 有料バッジ表示(色ではなく枠線とアイコン)(FR-301)
- プレビュー制限:既定3分で停止 (FR-302)
- 残り時間の常時表示 (FR-303)
-
Paywall.tsx:オーバーレイ、プラン概要、CTA、閉じる導線、フォーカストラップ (FR-304, A11Y-10) - SC-020 プラン比較 (FR-306) / SC-021 申込フォーム (FR-307) / SC-022 完了 (FR-308)
- 加入後、停止位置から続きを再生 (FR-305)
- SC-023 契約状況、変更・解約 (FR-309)
2-4. 添付資料
-
AttachmentCard:公開範囲に応じたロック表示 (FR-208, FR-412) - ロック時は必要プラン名を明示
Phase 2 完了チェック
- フローBが完走する(プレビュー → ペイウォール → 加入 → 続きから再生)
- 5章「ロールを跨ぐ体験」の1が動作する
- 有料バッジがカタログを赤くしていない(9章の配色ルールを満たす)
Phase 3 — 配信管理(4日)
- SC-100 管理ダッシュボード
- SC-111 アップロード:ドロップ、エンコード進捗の段階再現 (FR-401, FR-402)
- 完了後は既存素材に紐付け。デモである旨を明示 (FR-403)
- 変換失敗の再現とリトライ導線 (FR-404)
- SC-112 エピソード編集(基本情報、サムネイル3候補、無料/有料)(FR-405, FR-406)
- 公開設定(下書き/即時/予約/非公開)(FR-407)
- 公開予約日時の到来でカタログに出現(クライアントタイマー)(FR-408)
- 非公開で視聴者側から即座に消える (FR-409)
- SC-113 字幕編集(アップロード、行ごと修正、プレビュー)(FR-410)
- SC-114 チャプター編集(再生位置を見ながら、「現在位置で追加」)(FR-411)
- SC-115 添付資料管理(紐付け、公開範囲)(FR-412)
- SC-120 シリーズ管理(dnd-kit で並び替え、キーボード対応)(FR-413)
- 一括操作(公開/非公開、カテゴリ付与)(FR-414)
- SC-130 会員管理 (FR-415) / SC-131 プラン管理 (FR-416)
Phase 3 完了チェック
- フローCが完走する(アップロード → 編集 → チャプター → 資料 → 公開 → 視聴者側に出現)
- 5章「ロールを跨ぐ体験」の3と4が動作する
Phase 4 — 視聴分析(2日)
- SC-140 視聴分析:再生数・総視聴時間・平均完了率、期間指定 (FR-501)
-
RetentionChart:離脱ヒートマップ。大きな離脱区間を強調 + 注釈 (FR-502) - 完了率のエピソード別ランキング (FR-503)
- デバイス別・画質別の比率 (FR-504)
- シリーズ継続率ファネル (FR-505)
- デモ中の視聴が分析に反映されること (FR-506)
- CSVエクスポート (FR-507)
Phase 4 完了チェック
- 自分が今見た動画の視聴が、離脱ヒートマップに反映される(5章の体験5)
- シードの離脱曲線が自然で、分析画面が意味を持って見える
Phase 5 — 記事統合と仕上げ(3日)
5-1. 記事統合
- 記事側の埋め込みコードを作成し、実際の記事ページで動作確認 (EMB-01〜05)
- 埋め込みが記事の LCP 要素になっていないことを確認 (EMB-04)
- 記事の Core Web Vitals を埋め込み前後で比較し、悪化していないことを確認
- 「デモを開く」カードの設置
- 元記事へ戻るリンク、問い合わせ導線 (EMB-10, EMB-11)
- GA4 イベント設定(
demo_open/video_play/video_progress/paywall_view/role_switch/form_click/line_click)(EMB-12, EMB-13) -
?embed=1をnoindexに (EMB-21) - デモ本体の title / description / OGP (EMB-20)
- 記事側に
VideoObject構造化データ (EMB-23)
5-2. 体験の総点検
- 5章「ロールを跨ぐ体験」の5パターンをすべて手動で確認
- 全28画面を開き、空の画面が1つもないことを確認
- 空状態・ローディング・エラー状態を全画面で確認
- ガイドツアーを実装 (FR-605)
- localStorage 上限時の処理 (NFR-12)
5-3. アクセシビリティ監査
- axe-core を全画面に実行し、Critical / Serious を0件に
- ダーク背景でのコントラスト比を実測(特に
--fg-mid)(A11Y-01) - キーボードのみで全画面・全プレーヤー操作を完遂
- スクリーンリーダーでプレーヤーの状態変化が読み上げられることを確認 (A11Y-09)
5-4. パフォーマンス
- hls.js / Recharts を動的import に切り出し、初期バンドルを200KB以下に (NFR-06)
- 再生開始1.5秒以下を実測 (NFR-01)
- シークバー操作の60fpsを実測 (NFR-07)
- 5分連続再生でのメモリ推移を計測 (NFR-08)
- Lighthouse で Performance 90 / Accessibility 95 (NFR-09)
5-5. 公開と記録
- Playwright で フローA・B・C の E2E
- Vitest:
_scopecalcqueryのカバレッジ80%以上 -
/dev/componentsを本番で非公開に - Vercel へデプロイ、R2 の転送量アラートを設定 (COST-03)
- 転送量・費用の記録を開始 (COST-01)
- 各フェーズのキャプチャを整理し、記事の「方法」パートの素材にまとめる
合計 21営業日(約4週間)/1名専任 + レビュー体制。 Phase 1(プレーヤー)に5日を割いているのは、ここの完成度がデモの成否そのものだから。既製プレーヤーを貼れば1日で済むが、それでは「Claude Codeで動画配信アプリが作れる」の証明にならない。
14. 受入基準
- 7章の優先度 P1 の要件がすべて実装されていること
- 6章の主要フローA・B・Cが、エンドツーエンドで完走すること
- 5章「ロールを跨ぐ体験」の5パターンがすべて動作すること
- 再生ボタン押下から1.5秒以内に最初のフレームが表示されること
- 画質・速度・字幕・チャプターのすべてが動作し、画質切替で再生位置と再生状態が維持されること
- プレーヤーの全操作がキーボードのみで完遂できること
- レジューム再生が動作し、リロード後も停止位置から再開できること
- ペイウォールが動作し、加入後に停止位置から続きが再生されること
- エピソードを10回連続で切り替えてもメモリが単調増加しないこと
- 埋め込みモードの初期バンドルが80KB以下で、記事のCore Web Vitalsを悪化させないこと
- デモ中の視聴が視聴分析に反映され、離脱ヒートマップが意味のある形状で表示されること
- 全28画面のいずれにも空の状態がなく、リアルなコンテンツが表示されていること
- axe-core で Critical / Serious の指摘が0件であること
- 動画・画像素材のライセンスが
docs/assets-license.mdにすべて記録されていること - データアクセスがすべて
lib/repo/を経由し、視聴可否の判定がresolveAccess()に集約されていること - HIREBASE / RELATE と並べたときに、明確に別のプロダクトとして見えること
判断に迷ったときのルール
- 仕様がこの文書にない場合は、実装せずに質問する。 推測で作らない
- プレーヤーの品質と、キーボード操作、空の画面を作らないことは削減対象外
- 既製のプレーヤーライブラリ(video.js / Plyr 等)を導入しない。 自前実装であることがこのデモの価値
- スコープ外(4章 Won't have)は、思いついても実装しない。特にライブ配信とDRM
- 「デモだから」を理由に品質を落とす判断はしない
- hls.js とイベントリスナーの破棄を忘れない。このアプリで最も起きやすい不具合はメモリリーク
- HIREBASE / RELATE のトークン・コンポーネントをコピーしない