はじめに
この記事では、Next.jsを使用して本格的な動画配信サービスを構築する方法を解説します。Netflix、YouTube、Huluのような動画配信プラットフォームの基盤となる技術を学び、実際に動作するコードを通じて理解を深めていきます。
この記事で学べること
- ストリーミング技術の基礎: HLS/DASHプロトコルの仕組みと違い
- 動画プレイヤーの実装: video.jsを使った高機能プレイヤーの構築
- クラウドインフラの活用: AWS Media ServicesとCloudFrontの設定
- セキュリティ対策: 署名付きURLとDRMによるコンテンツ保護
ストリーミング技術の基礎
プログレッシブダウンロードとストリーミングの違い
動画配信には主に2つの方式があります。
| 方式 | 特徴 | ユースケース |
|---|---|---|
| プログレッシブダウンロード | 動画ファイルを先頭から順次ダウンロード | 短い動画、シンプルな実装 |
| ストリーミング | 動画を小さなセグメントに分割して配信 | 長時間の動画、ライブ配信 |
HLS(HTTP Live Streaming)とは
HLSはAppleが開発したストリーミングプロトコルです。動画を数秒単位のセグメント(.tsファイル)に分割し、マニフェストファイル(.m3u8)で管理します。
# HLSの構成例
video/
├── playlist.m3u8 # マスタープレイリスト
├── 720p/
│ ├── playlist.m3u8 # 720p用プレイリスト
│ ├── segment_001.ts
│ ├── segment_002.ts
│ └── ...
├── 480p/
│ ├── playlist.m3u8 # 480p用プレイリスト
│ └── ...
└── 360p/
└── ...
DASH(Dynamic Adaptive Streaming over HTTP)とは
DASHは国際標準規格(ISO/IEC 23009-1)として策定されたストリーミングプロトコルです。マニフェストファイルには.mpd(Media Presentation Description)形式を使用します。
| 項目 | HLS | DASH |
|---|---|---|
| 開発元 | Apple | MPEG |
| マニフェスト | .m3u8 | .mpd |
| セグメント形式 | .ts, .fmp4 | .m4s, .mp4 |
| DRM対応 | FairPlay | Widevine, PlayReady |
| iOS対応 | ネイティブ | 要対応 |
プロジェクトのセットアップ
必要なパッケージのインストール
# Next.jsプロジェクトの作成
npx create-next-app@latest video-streaming-app --typescript --tailwind --app
# 動画プレイヤー関連のパッケージ
cd video-streaming-app
npm install video.js @types/video.js
npm install hls.js
# AWS SDK(動画管理用)
npm install @aws-sdk/client-s3 @aws-sdk/client-cloudfront
npm install @aws-sdk/cloudfront-signer
プロジェクト構成
video-streaming-app/
├── src/
│ ├── app/
│ │ ├── page.tsx
│ │ ├── videos/
│ │ │ └── [id]/
│ │ │ └── page.tsx
│ │ └── api/
│ │ └── videos/
│ │ └── [id]/
│ │ └── route.ts
│ ├── components/
│ │ └── VideoPlayer.tsx
│ ├── lib/
│ │ ├── aws.ts
│ │ └── video.ts
│ └── types/
│ └── video.ts
└── public/
└── videos/
動画プレイヤーの実装
video.jsを使った基本的なプレイヤー
まず、video.jsを使用した再利用可能なプレイヤーコンポーネントを作成します。
// src/components/VideoPlayer.tsx
'use client';
import { useEffect, useRef, useState } from 'react';
import videojs from 'video.js';
import Player from 'video.js/dist/types/player';
import 'video.js/dist/video-js.css';
// video.jsのプレイヤーオプションの型定義
interface VideoPlayerProps {
src: string;
type?: 'application/x-mpegURL' | 'application/dash+xml' | 'video/mp4';
poster?: string;
autoplay?: boolean;
onReady?: (player: Player) => void;
onTimeUpdate?: (currentTime: number) => void;
onEnded?: () => void;
}
export default function VideoPlayer({
src,
type = 'application/x-mpegURL', // デフォルトはHLS
poster,
autoplay = false,
onReady,
onTimeUpdate,
onEnded,
}: VideoPlayerProps) {
const videoRef = useRef<HTMLDivElement>(null);
const playerRef = useRef<Player | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// プレイヤーが既に初期化されている場合はスキップ
if (!videoRef.current || playerRef.current) return;
// video要素を動的に作成
const videoElement = document.createElement('video-js');
videoElement.classList.add('vjs-big-play-centered', 'vjs-16-9');
videoRef.current.appendChild(videoElement);
// video.jsプレイヤーの初期化
const player = videojs(videoElement, {
controls: true, // コントロールバーを表示
responsive: true, // レスポンシブ対応
fluid: true, // 親要素に合わせてサイズ調整
autoplay: autoplay,
preload: 'auto', // 動画のプリロード
poster: poster, // サムネイル画像
playbackRates: [0.5, 1, 1.25, 1.5, 2], // 再生速度オプション
html5: {
vhs: {
// HLS.jsの設定
overrideNative: true, // ネイティブHLSを上書き
enableLowInitialPlaylist: true, // 低帯域から開始
},
},
sources: [{ src, type }],
});
playerRef.current = player;
// イベントリスナーの設定
player.on('ready', () => {
setIsLoading(false);
console.log('Video player is ready');
onReady?.(player);
});
player.on('timeupdate', () => {
const currentTime = player.currentTime();
if (currentTime !== undefined) {
onTimeUpdate?.(currentTime);
}
});
player.on('ended', () => {
console.log('Video playback ended');
onEnded?.();
});
player.on('error', () => {
const error = player.error();
setError(error?.message || '動画の読み込みに失敗しました');
setIsLoading(false);
});
// クリーンアップ
return () => {
if (playerRef.current) {
playerRef.current.dispose();
playerRef.current = null;
}
};
}, [src, type, poster, autoplay, onReady, onTimeUpdate, onEnded]);
// ソースが変更された場合の処理
useEffect(() => {
const player = playerRef.current;
if (player && !player.isDisposed()) {
player.src({ src, type });
}
}, [src, type]);
if (error) {
return (
<div className="bg-gray-900 rounded-lg p-8 text-center">
<p className="text-red-500">{error}</p>
<button
onClick={() => window.location.reload()}
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
再読み込み
</button>
</div>
);
}
return (
<div className="relative">
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-900 rounded-lg">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
</div>
)}
<div ref={videoRef} className="rounded-lg overflow-hidden" />
</div>
);
}
HLS.jsを直接使用する実装
より細かい制御が必要な場合は、HLS.jsを直接使用します。
// src/components/HLSPlayer.tsx
'use client';
import { useEffect, useRef, useState } from 'react';
import Hls from 'hls.js';
interface HLSPlayerProps {
src: string;
poster?: string;
}
interface QualityLevel {
height: number;
bitrate: number;
}
export default function HLSPlayer({ src, poster }: HLSPlayerProps) {
const videoRef = useRef<HTMLVideoElement>(null);
const hlsRef = useRef<Hls | null>(null);
const [qualityLevels, setQualityLevels] = useState<QualityLevel[]>([]);
const [currentQuality, setCurrentQuality] = useState<number>(-1); // -1 = auto
useEffect(() => {
const video = videoRef.current;
if (!video) return;
// HLS.jsがサポートされているか確認
if (Hls.isSupported()) {
const hls = new Hls({
// バッファ設定
maxBufferLength: 30, // 最大バッファ長(秒)
maxMaxBufferLength: 60, // 絶対最大バッファ長
// 再試行設定
manifestLoadingMaxRetry: 3, // マニフェスト読み込みリトライ回数
levelLoadingMaxRetry: 4, // レベル読み込みリトライ回数
fragLoadingMaxRetry: 6, // フラグメント読み込みリトライ回数
// 品質切り替え設定
startLevel: -1, // -1 = 自動選択
abrEwmaDefaultEstimate: 500000, // 初期帯域推定値(500kbps)
});
hlsRef.current = hls;
hls.loadSource(src);
hls.attachMedia(video);
// マニフェスト解析完了時のイベント
hls.on(Hls.Events.MANIFEST_PARSED, (event, data) => {
console.log(`${data.levels.length}種類の品質レベルが利用可能`);
// 利用可能な品質レベルを取得
const levels = data.levels.map((level) => ({
height: level.height,
bitrate: level.bitrate,
}));
setQualityLevels(levels);
});
// 品質レベル切り替え時のイベント
hls.on(Hls.Events.LEVEL_SWITCHED, (event, data) => {
console.log(`品質レベルが${data.level}に切り替わりました`);
setCurrentQuality(data.level);
});
// エラーハンドリング
hls.on(Hls.Events.ERROR, (event, data) => {
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
console.error('ネットワークエラー、リカバリーを試行...');
hls.startLoad();
break;
case Hls.ErrorTypes.MEDIA_ERROR:
console.error('メディアエラー、リカバリーを試行...');
hls.recoverMediaError();
break;
default:
console.error('致命的なエラー、再初期化が必要');
hls.destroy();
break;
}
}
});
return () => {
hls.destroy();
hlsRef.current = null;
};
}
// Safari等、ネイティブHLSをサポートするブラウザ
else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = src;
}
}, [src]);
// 品質レベルの手動切り替え
const handleQualityChange = (level: number) => {
if (hlsRef.current) {
hlsRef.current.currentLevel = level; // -1 で自動に戻す
}
};
return (
<div className="relative">
<video
ref={videoRef}
poster={poster}
controls
className="w-full rounded-lg"
playsInline // モバイル対応
/>
{/* 品質選択UI */}
{qualityLevels.length > 0 && (
<div className="absolute bottom-16 right-4 bg-black/80 rounded p-2">
<select
value={currentQuality}
onChange={(e) => handleQualityChange(Number(e.target.value))}
className="bg-transparent text-white text-sm"
>
<option value={-1}>自動</option>
{qualityLevels.map((level, index) => (
<option key={index} value={index}>
{level.height}p ({Math.round(level.bitrate / 1000)}kbps)
</option>
))}
</select>
</div>
)}
</div>
);
}
動画一覧ページの実装
型定義
// src/types/video.ts
export interface Video {
id: string;
title: string;
description: string;
thumbnailUrl: string;
duration: number; // 秒
hlsUrl: string;
dashUrl?: string;
createdAt: Date;
views: number;
category: string;
}
export interface VideoListResponse {
videos: Video[];
totalCount: number;
hasMore: boolean;
nextCursor?: string;
}
動画一覧ページ
// src/app/page.tsx
import Link from 'next/link';
import Image from 'next/image';
import { Video } from '@/types/video';
// サンプルデータ(実際はAPIから取得)
const sampleVideos: Video[] = [
{
id: '1',
title: 'Next.js入門講座',
description: 'Next.jsの基本を学びましょう',
thumbnailUrl: '/thumbnails/nextjs-intro.jpg',
duration: 1800, // 30分
hlsUrl: 'https://example.com/videos/1/playlist.m3u8',
createdAt: new Date('2024-01-15'),
views: 15000,
category: 'プログラミング',
},
// ... 他の動画
];
// 秒数を "MM:SS" 形式に変換するユーティリティ関数
function formatDuration(seconds: number): string {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
}
// 視聴回数をフォーマットする関数(1000以上は "1.5K" のように表示)
function formatViews(views: number): string {
if (views >= 1000000) {
return `${(views / 1000000).toFixed(1)}M`;
}
if (views >= 1000) {
return `${(views / 1000).toFixed(1)}K`;
}
return views.toString();
}
export default function HomePage() {
return (
<main className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">動画一覧</h1>
{/* カテゴリーフィルター */}
<div className="flex gap-4 mb-8 overflow-x-auto pb-2">
{['すべて', 'プログラミング', 'デザイン', 'ビジネス'].map((category) => (
<button
key={category}
className="px-4 py-2 bg-gray-800 text-white rounded-full hover:bg-gray-700 whitespace-nowrap"
>
{category}
</button>
))}
</div>
{/* 動画グリッド */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{sampleVideos.map((video) => (
<Link
key={video.id}
href={`/videos/${video.id}`}
className="group block"
>
{/* サムネイル */}
<div className="relative aspect-video bg-gray-800 rounded-lg overflow-hidden">
<Image
src={video.thumbnailUrl}
alt={video.title}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
/>
{/* 再生時間バッジ */}
<span className="absolute bottom-2 right-2 bg-black/80 text-white text-xs px-2 py-1 rounded">
{formatDuration(video.duration)}
</span>
</div>
{/* 動画情報 */}
<div className="mt-3">
<h2 className="font-semibold line-clamp-2 group-hover:text-blue-400 transition-colors">
{video.title}
</h2>
<p className="text-sm text-gray-400 mt-1">
{formatViews(video.views)}回視聴 ・ {video.category}
</p>
</div>
</Link>
))}
</div>
</main>
);
}
動画詳細ページ
// src/app/videos/[id]/page.tsx
import { Suspense } from 'react';
import { notFound } from 'next/navigation';
import VideoPlayer from '@/components/VideoPlayer';
import { Video } from '@/types/video';
// 動画データを取得する関数(実際はデータベースやAPIから取得)
async function getVideo(id: string): Promise<Video | null> {
// 実際の実装ではデータベースやAPIから取得
// ここではサンプルとして固定データを返す
const videos: Record<string, Video> = {
'1': {
id: '1',
title: 'Next.js入門講座',
description: 'Next.jsの基本的な使い方を学びます。App Router、Server Components、データフェッチングなど、最新のNext.js機能を解説します。',
thumbnailUrl: '/thumbnails/nextjs-intro.jpg',
duration: 1800,
hlsUrl: 'https://d2zihajmogu5jn.cloudfront.net/bipbop-advanced/bipbop_16x9_variant.m3u8', // サンプルHLS
createdAt: new Date('2024-01-15'),
views: 15000,
category: 'プログラミング',
},
};
return videos[id] || null;
}
// メタデータの生成
export async function generateMetadata({ params }: { params: { id: string } }) {
const video = await getVideo(params.id);
if (!video) {
return { title: '動画が見つかりません' };
}
return {
title: video.title,
description: video.description,
openGraph: {
title: video.title,
description: video.description,
images: [video.thumbnailUrl],
type: 'video.other',
},
};
}
// ローディングスケルトン
function VideoSkeleton() {
return (
<div className="animate-pulse">
<div className="aspect-video bg-gray-800 rounded-lg" />
<div className="mt-4 h-8 bg-gray-800 rounded w-3/4" />
<div className="mt-2 h-4 bg-gray-800 rounded w-1/2" />
</div>
);
}
export default async function VideoPage({ params }: { params: { id: string } }) {
const video = await getVideo(params.id);
if (!video) {
notFound();
}
return (
<main className="container mx-auto px-4 py-8">
<div className="max-w-5xl mx-auto">
{/* 動画プレイヤー */}
<Suspense fallback={<VideoSkeleton />}>
<VideoPlayer
src={video.hlsUrl}
type="application/x-mpegURL"
poster={video.thumbnailUrl}
/>
</Suspense>
{/* 動画情報 */}
<div className="mt-6">
<h1 className="text-2xl font-bold">{video.title}</h1>
<div className="flex items-center gap-4 mt-2 text-gray-400">
<span>{video.views.toLocaleString()}回視聴</span>
<span>・</span>
<span>{video.createdAt.toLocaleDateString('ja-JP')}</span>
</div>
</div>
{/* 説明文 */}
<div className="mt-6 p-4 bg-gray-800 rounded-lg">
<p className="whitespace-pre-wrap">{video.description}</p>
</div>
</div>
</main>
);
}
AWS Media Servicesの活用
AWSサービス構成
動画配信に必要なAWSサービスの構成を説明します。
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ S3 (入力) │────▶│ MediaConvert │────▶│ S3 (出力) │
│ 動画アップロード│ │ トランスコーディング │ │ HLS/DASHファイル│
└─────────────────┘ └──────────────────┘ └────────┬────────┘
│
┌──────────────────┐ │
│ CloudFront │◀─────────────┘
│ CDN配信 │
└────────┬─────────┘
│
┌────────▼─────────┐
│ ユーザー │
│ 動画視聴 │
└──────────────────┘
AWS SDKを使った署名付きURL生成
// src/lib/aws.ts
import {
CloudFrontClient,
CreateInvalidationCommand,
} from '@aws-sdk/client-cloudfront';
import { getSignedUrl } from '@aws-sdk/cloudfront-signer';
// CloudFrontの設定
const CLOUDFRONT_DOMAIN = process.env.CLOUDFRONT_DOMAIN!;
const CLOUDFRONT_KEY_PAIR_ID = process.env.CLOUDFRONT_KEY_PAIR_ID!;
const CLOUDFRONT_PRIVATE_KEY = process.env.CLOUDFRONT_PRIVATE_KEY!;
/**
* CloudFront署名付きURLを生成する
* 期限付きのアクセスを許可し、不正なアクセスを防止
*/
export function generateSignedUrl(
path: string,
expiresInSeconds: number = 3600 // デフォルト1時間
): string {
const url = `https://${CLOUDFRONT_DOMAIN}/${path}`;
const expiresAt = new Date(Date.now() + expiresInSeconds * 1000);
return getSignedUrl({
url,
keyPairId: CLOUDFRONT_KEY_PAIR_ID,
privateKey: CLOUDFRONT_PRIVATE_KEY,
dateLessThan: expiresAt.toISOString(),
});
}
/**
* 動画ファイル用の署名付きURLを生成
* HLSのマスタープレイリストとすべてのセグメントにアクセス可能なワイルドカードポリシーを使用
*/
export function generateVideoSignedUrl(videoId: string): {
playlistUrl: string;
policy: string;
} {
const basePath = `videos/${videoId}`;
const playlistPath = `${basePath}/playlist.m3u8`;
// ワイルドカードポリシーを使用して、関連するすべてのセグメントへのアクセスを許可
const policyStatement = {
Statement: [
{
Resource: `https://${CLOUDFRONT_DOMAIN}/${basePath}/*`,
Condition: {
DateLessThan: {
'AWS:EpochTime': Math.floor(Date.now() / 1000) + 7200, // 2時間有効
},
},
},
],
};
const policy = JSON.stringify(policyStatement);
const playlistUrl = generateSignedUrl(playlistPath, 7200);
return { playlistUrl, policy };
}
// CloudFrontのキャッシュ無効化
const cloudFrontClient = new CloudFrontClient({
region: process.env.AWS_REGION || 'ap-northeast-1',
});
export async function invalidateCache(
distributionId: string,
paths: string[]
): Promise<void> {
const command = new CreateInvalidationCommand({
DistributionId: distributionId,
InvalidationBatch: {
CallerReference: Date.now().toString(),
Paths: {
Quantity: paths.length,
Items: paths,
},
},
});
await cloudFrontClient.send(command);
}
動画API
// src/app/api/videos/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { generateVideoSignedUrl } from '@/lib/aws';
// 動画情報を取得するAPI
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const videoId = params.id;
// ここで認証チェックを行う(例:JWTトークンの検証)
const authHeader = request.headers.get('authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return NextResponse.json(
{ error: '認証が必要です' },
{ status: 401 }
);
}
try {
// 署名付きURLを生成
const { playlistUrl } = generateVideoSignedUrl(videoId);
return NextResponse.json({
id: videoId,
playlistUrl,
expiresIn: 7200, // クライアントに有効期限を通知
});
} catch (error) {
console.error('Error generating signed URL:', error);
return NextResponse.json(
{ error: '動画URLの生成に失敗しました' },
{ status: 500 }
);
}
}
セキュリティ対策
視聴制限の実装
// src/lib/video.ts
interface ViewPermission {
canView: boolean;
reason?: string;
}
/**
* ユーザーが動画を視聴できるか確認する
*/
export async function checkViewPermission(
userId: string | null,
videoId: string
): Promise<ViewPermission> {
// 未認証ユーザーはプレビューのみ許可
if (!userId) {
return {
canView: false,
reason: 'ログインが必要です',
};
}
// 有料コンテンツの購入確認(実際はDBから取得)
const hasPurchased = await checkPurchaseStatus(userId, videoId);
if (!hasPurchased) {
return {
canView: false,
reason: 'この動画を視聴するには購入が必要です',
};
}
// 地域制限のチェック
const isAllowedRegion = await checkRegionRestriction(videoId);
if (!isAllowedRegion) {
return {
canView: false,
reason: 'この動画はお住まいの地域ではご利用いただけません',
};
}
return { canView: true };
}
// 購入状態の確認(サンプル実装)
async function checkPurchaseStatus(
userId: string,
videoId: string
): Promise<boolean> {
// 実際はデータベースで確認
return true;
}
// 地域制限の確認(サンプル実装)
async function checkRegionRestriction(videoId: string): Promise<boolean> {
// 実際はリクエストのIPアドレスから地域を判定
return true;
}
DRM(Digital Rights Management)の概要
プレミアムコンテンツを保護するためには、DRMの実装が必要です。主要なDRMシステムには以下があります。
| DRMシステム | 対応プラットフォーム | 提供元 |
|---|---|---|
| Widevine | Chrome, Firefox, Android | |
| FairPlay | Safari, iOS | Apple |
| PlayReady | Edge, Windows | Microsoft |
// DRM設定の例(video.jsとの統合)
const drmConfig = {
keySystems: {
// Widevine
'com.widevine.alpha': {
url: 'https://license-server.example.com/widevine',
},
// FairPlay
'com.apple.fps.1_0': {
certificateUri: 'https://license-server.example.com/fairplay/cert',
licenseUri: 'https://license-server.example.com/fairplay',
},
},
};
パフォーマンス最適化
動画のプリロード戦略
// src/components/VideoPreloader.tsx
'use client';
import { useEffect } from 'react';
interface VideoPreloaderProps {
videoUrls: string[];
}
/**
* 次の動画をプリロードしてスムーズな再生を実現
*/
export function VideoPreloader({ videoUrls }: VideoPreloaderProps) {
useEffect(() => {
// 接続が遅い場合はプリロードをスキップ
const connection = (navigator as Navigator & { connection?: { effectiveType: string } }).connection;
if (connection?.effectiveType === 'slow-2g' || connection?.effectiveType === '2g') {
return;
}
// link要素でプリロード
videoUrls.forEach((url) => {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = url;
link.as = 'fetch';
document.head.appendChild(link);
});
return () => {
// クリーンアップ
document.querySelectorAll('link[rel="prefetch"]').forEach((el) => {
el.remove();
});
};
}, [videoUrls]);
return null;
}
Intersection Observerを使った遅延読み込み
// src/components/LazyVideoPlayer.tsx
'use client';
import { useEffect, useRef, useState } from 'react';
import VideoPlayer from './VideoPlayer';
interface LazyVideoPlayerProps {
src: string;
poster: string;
}
export function LazyVideoPlayer({ src, poster }: LazyVideoPlayerProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
});
},
{
rootMargin: '100px', // 100px手前で読み込み開始
threshold: 0.1,
}
);
if (containerRef.current) {
observer.observe(containerRef.current);
}
return () => observer.disconnect();
}, []);
return (
<div ref={containerRef} className="aspect-video bg-gray-800 rounded-lg">
{isVisible ? (
<VideoPlayer src={src} poster={poster} />
) : (
<img
src={poster}
alt="Video thumbnail"
className="w-full h-full object-cover rounded-lg"
/>
)}
</div>
);
}
まとめ
この記事では、Next.jsを使った動画配信サービスの構築方法について解説しました。
学んだ内容
- ストリーミング技術: HLSとDASHの仕組みと違い
- 動画プレイヤー: video.jsとHLS.jsを使った実装
- クラウドインフラ: AWS Media ServicesとCloudFrontの活用
- セキュリティ: 署名付きURLとDRMによるコンテンツ保護
- パフォーマンス: プリロードと遅延読み込みの最適化
次のステップ
- ライブストリーミング機能の追加
- 視聴履歴・レコメンデーション機能
- 字幕・多言語対応
- アナリティクス(視聴分析)の実装
動画配信サービスの開発は複雑ですが、適切な技術スタックを選択し、段階的に機能を追加していくことで、高品質なサービスを構築できます。
参考文献
- Next.js Documentation - Next.js公式ドキュメント
- video.js - オープンソース動画プレイヤー
- HLS.js - JavaScript HLSクライアント
- AWS Elemental MediaConvert - AWS動画変換サービス
- Amazon CloudFront - AWSコンテンツ配信ネットワーク
- HTTP Live Streaming (HLS) - Apple Developer - Apple HLS仕様
- DASH Industry Forum - DASH国際標準