Use Cases

Building a Custom Video Player with Claude Code

Learn about building a custom video player using Claude Code. Includes practical code examples.

カスタム動画プレーヤーをClaude Codeで作る

デフォルトのブラウザ動画プレーヤーではデザインの統一が難しく、機能も限定されます。Claude Codeを使えば、ブランドに合ったデザインで、再生速度変更・字幕・ピクチャーインピクチャーなどの機能を備えたカスタムプレーヤーを構築できます。

プレーヤーの要件

> HTML5 Videoベースのカスタム動画プレーヤーを作って。
> 再生/一時停止、シークバー、音量、再生速度変更、
> フルスクリーン、ピクチャーインピクチャー、字幕表示に対応して。

メインプレーヤーコンポーネント

// src/components/VideoPlayer.tsx
'use client';
import { useRef, useState, useEffect } from 'react';

interface VideoPlayerProps {
  src: string;
  poster?: string;
  subtitles?: { src: string; label: string; lang: string }[];
}

export function VideoPlayer({ src, poster, subtitles }: VideoPlayerProps) {
  const videoRef = useRef<HTMLVideoElement>(null);
  const [isPlaying, setIsPlaying] = useState(false);
  const [currentTime, setCurrentTime] = useState(0);
  const [duration, setDuration] = useState(0);
  const [volume, setVolume] = useState(1);
  const [playbackRate, setPlaybackRate] = useState(1);
  const [showControls, setShowControls] = useState(true);

  const video = videoRef.current;

  useEffect(() => {
    if (!video) return;
    const handleTimeUpdate = () => setCurrentTime(video.currentTime);
    const handleLoadedMetadata = () => setDuration(video.duration);
    video.addEventListener('timeupdate', handleTimeUpdate);
    video.addEventListener('loadedmetadata', handleLoadedMetadata);
    return () => {
      video.removeEventListener('timeupdate', handleTimeUpdate);
      video.removeEventListener('loadedmetadata', handleLoadedMetadata);
    };
  }, [video]);

  const togglePlay = () => {
    if (!video) return;
    if (video.paused) {
      video.play();
      setIsPlaying(true);
    } else {
      video.pause();
      setIsPlaying(false);
    }
  };

  const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (!video) return;
    const time = Number(e.target.value);
    video.currentTime = time;
    setCurrentTime(time);
  };

  const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (!video) return;
    const vol = Number(e.target.value);
    video.volume = vol;
    setVolume(vol);
  };

  const changePlaybackRate = (rate: number) => {
    if (!video) return;
    video.playbackRate = rate;
    setPlaybackRate(rate);
  };

  const toggleFullscreen = () => {
    const container = videoRef.current?.parentElement;
    if (!container) return;
    if (document.fullscreenElement) {
      document.exitFullscreen();
    } else {
      container.requestFullscreen();
    }
  };

  const togglePiP = async () => {
    if (!video) return;
    if (document.pictureInPictureElement) {
      await document.exitPictureInPicture();
    } else {
      await video.requestPictureInPicture();
    }
  };

  const formatTime = (seconds: number) => {
    const m = Math.floor(seconds / 60);
    const s = Math.floor(seconds % 60);
    return `${m}:${s.toString().padStart(2, '0')}`;
  };

  return (
    <div
      className="relative group bg-black rounded-xl overflow-hidden"
      onMouseEnter={() => setShowControls(true)}
      onMouseLeave={() => isPlaying && setShowControls(false)}
    >
      <video
        ref={videoRef}
        src={src}
        poster={poster}
        onClick={togglePlay}
        className="w-full cursor-pointer"
      >
        {subtitles?.map((sub) => (
          <track key={sub.lang} kind="subtitles" src={sub.src} label={sub.label} srcLang={sub.lang} />
        ))}
      </video>

      {/* コントロールバー */}
      <div className={`absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-4 transition-opacity ${showControls ? 'opacity-100' : 'opacity-0'}`}>
        {/* シークバー */}
        <input
          type="range"
          min={0}
          max={duration}
          value={currentTime}
          onChange={handleSeek}
          className="w-full h-1 mb-3 accent-blue-500"
        />
        <div className="flex items-center justify-between text-white text-sm">
          <div className="flex items-center gap-3">
            <button onClick={togglePlay} className="text-xl">
              {isPlaying ? '⏸' : '▶'}
            </button>
            <span>{formatTime(currentTime)} / {formatTime(duration)}</span>
            <input
              type="range"
              min={0}
              max={1}
              step={0.1}
              value={volume}
              onChange={handleVolumeChange}
              className="w-20 h-1 accent-white"
            />
          </div>
          <div className="flex items-center gap-3">
            <select
              value={playbackRate}
              onChange={(e) => changePlaybackRate(Number(e.target.value))}
              className="bg-transparent text-white text-sm"
            >
              {[0.5, 0.75, 1, 1.25, 1.5, 2].map((rate) => (
                <option key={rate} value={rate} className="text-black">
                  {rate}x
                </option>
              ))}
            </select>
            <button onClick={togglePiP} title="ピクチャーインピクチャー">🖼</button>
            <button onClick={toggleFullscreen} title="フルスクリーン">⛶</button>
          </div>
        </div>
      </div>
    </div>
  );
}

キーボードショートカット

// src/hooks/useVideoShortcuts.ts
import { useEffect } from 'react';

export function useVideoShortcuts(videoRef: React.RefObject<HTMLVideoElement>) {
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      const video = videoRef.current;
      if (!video) return;

      switch (e.key) {
        case ' ':
          e.preventDefault();
          video.paused ? video.play() : video.pause();
          break;
        case 'ArrowRight':
          video.currentTime = Math.min(video.currentTime + 10, video.duration);
          break;
        case 'ArrowLeft':
          video.currentTime = Math.max(video.currentTime - 10, 0);
          break;
        case 'f':
          document.fullscreenElement
            ? document.exitFullscreen()
            : video.parentElement?.requestFullscreen();
          break;
        case 'm':
          video.muted = !video.muted;
          break;
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [videoRef]);
}

使い方の例

<VideoPlayer
  src="/videos/demo.mp4"
  poster="/images/poster.jpg"
  subtitles={[
    { src: '/subtitles/ja.vtt', label: '日本語', lang: 'ja' },
    { src: '/subtitles/en.vtt', label: 'English', lang: 'en' },
  ]}
/>

関連記事

メディア関連ではオーディオプレーヤーの実装、アクセシビリティ対応はアクセシビリティ実装ガイドもご覧ください。

Web動画のフォーマットやエンコーディングについてはMDN Web Docs(developer.mozilla.org)が詳しいです。

#Claude Code #動画プレーヤー #HTML5 #React #メディア