Cours Vibe coding avec l'IA · Module 10: Clone de TikTok

Bibliothèque de sons et création de vidéos

6 min de lecture
Dans cette leçon5

La force de TikTok réside dans les sons - des pistes audio utilisées par des millions de vidéos. Plus une création de vidéos avancée avec des effets !

Bibliothèque de sons

1interface Sound {
2  id: string;
3  name: string;
4  artist: string;
5  duration: number;
6  thumbnailUrl: string;
7
8  // Statistiques
9  useCount: number; // combien de vidéos utilisent ce son
10  favorites: number;
11  isTrending: boolean;
12
13  // Catégories
14  category: 'original' | 'music' | 'trending' | 'effects';
15  tags: string[];
16
17  createdAt: Date;
18}
19
20interface SoundUsage {
21  soundId: string;
22  startTime: number; // à quelle seconde du son démarrer (base 0)
23  volume: number; // 0-1
24}

Page de découverte des sons

1'use client';
2
3import { useState } from 'react';
4
5interface SoundsPageProps {
6  sounds: Sound[];
7  onUseSound: (soundId: string) => void;
8  onFavorite: (soundId: string) => void;
9}
10
11export default function SoundsPage({
12  sounds,
13  onUseSound,
14  onFavorite
15}: SoundsPageProps) {
16  const [activeTab, setActiveTab] = useState<'trending' | 'favorites' | 'discover'>('trending');
17  const [searchQuery, setSearchQuery] = useState('');
18
19  const filteredSounds = sounds.filter(sound => {
20    if (searchQuery) {
21      return sound.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
22             sound.artist.toLowerCase().includes(searchQuery.toLowerCase());
23    }
24
25    if (activeTab === 'trending') return sound.isTrending;
26    if (activeTab === 'favorites') return sound.favorites > 1000;
27    return true;
28  });
29
30  return (
31    <div className="min-h-screen bg-black text-white">
32      {/* En-tête */}
33      <div className="sticky top-0 bg-black z-10 border-b border-gray-800">
34        <div className="p-4">
35          <h1 className="text-2xl font-bold mb-4">Sons</h1>
36
37          {/* Recherche */}
38          <input
39            type="text"
40            value={searchQuery}
41            onChange={(e) => setSearchQuery(e.target.value)}
42            placeholder="Rechercher des sons..."
43            className="w-full bg-gray-900 text-white px-4 py-3 rounded-lg outline-none"
44          />
45        </div>
46
47        {/* Onglets */}
48        <div className="flex border-b border-gray-800">
49          {['trending', 'favorites', 'discover'].map(tab => (
50            <button
51              key={tab}
52              onClick={() => setActiveTab(tab as any)}
53              className={`flex-1 py-3 text-sm font-semibold capitalize ${
54                activeTab === tab
55                  ? 'border-b-2 border-white'
56                  : 'text-gray-500'
57              }`}
58            >
59              {tab}
60            </button>
61          ))}
62        </div>
63      </div>
64
65      {/* Liste des sons */}
66      <div className="p-4 space-y-3">
67        {filteredSounds.map(sound => (
68          <SoundCard
69            key={sound.id}
70            sound={sound}
71            onUse={() => onUseSound(sound.id)}
72            onFavorite={() => onFavorite(sound.id)}
73          />
74        ))}
75      </div>
76    </div>
77  );
78}
79
80function SoundCard({
81  sound,
82  onUse,
83  onFavorite
84}: {
85  sound: Sound;
86  onUse: () => void;
87  onFavorite: () => void;
88}) {
89  const [isPlaying, setIsPlaying] = useState(false);
90
91  return (
92    <div className="bg-gray-900 rounded-lg p-4 flex items-center gap-4">
93      {/* Miniature */}
94      <button
95        onClick={() => setIsPlaying(!isPlaying)}
96        className="relative w-16 h-16 rounded-lg overflow-hidden shrink-0"
97      >
98        <img
99          src={sound.thumbnailUrl}
100          alt={sound.name}
101          className={`w-full h-full object-cover ${isPlaying ? 'animate-pulse' : ''}`}
102        />
103        <div className="absolute inset-0 flex items-center justify-center bg-black/30">
104          <span className="text-white text-2xl">
105            {isPlaying ? '⏸' : '▶'}
106          </span>
107        </div>
108      </button>
109
110      {/* Infos */}
111      <div className="flex-1 min-w-0">
112        <h3 className="font-bold text-white truncate">{sound.name}</h3>
113        <p className="text-sm text-gray-400 truncate">{sound.artist}</p>
114
115        <div className="flex items-center gap-3 mt-2 text-xs text-gray-500">
116          <span>{formatCount(sound.useCount)} vidéos</span>
117          {sound.isTrending && (
118            <span className="flex items-center gap-1 text-pink-500">
119              Tendance
120            </span>
121          )}
122        </div>
123      </div>
124
125      {/* Actions */}
126      <div className="flex flex-col gap-2">
127        <button
128          onClick={onUse}
129          className="px-4 py-2 bg-white text-black rounded-full text-sm font-semibold hover:bg-gray-200"
130        >
131          Utiliser le son
132        </button>
133        <button
134          onClick={onFavorite}
135          className="p-2 hover:bg-gray-800 rounded-full"
136        >
137          Favori
138        </button>
139      </div>
140    </div>
141  );
142}
143
144function formatCount(count: number): string {
145  if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
146  if (count >= 1000) return `${(count / 1000).toFixed(1)}K`;
147  return count.toString();
148}

Flux de création de vidéo

1interface VideoCreationState {
2  // Enregistrement
3  recordedChunks: Blob[];
4  isRecording: boolean;
5  recordingDuration: number;
6  maxDuration: number; // 15, 60 ou 180 secondes
7
8  // Montage
9  selectedSound?: Sound;
10  soundVolume: number;
11  selectedEffect?: Effect;
12  filters: Filter[];
13  trimStart: number;
14  trimEnd: number;
15
16  // Légende
17  caption: string;
18  hashtags: string[];
19  mentions: string[];
20
21  // Confidentialité
22  privacy: 'public' | 'friends' | 'private';
23  allowComments: boolean;
24  allowDuet: boolean;
25  allowStitch: boolean;
26}
27
28interface Effect {
29  id: string;
30  name: string;
31  type: 'beauty' | 'ar' | 'green-screen' | 'time';
32  thumbnailUrl: string;
33  isPopular: boolean;
34}
35
36interface Filter {
37  id: string;
38  name: string;
39  intensity: number; // 0-1
40}

Enregistreur de vidéo

1'use client';
2
3import { useState, useRef, useEffect } from 'react';
4
5export default function VideoRecorder({
6  maxDuration = 60,
7  onComplete: (videoBlob: Blob, metadata: any) => void
8}: {
9  maxDuration?: number;
10  onComplete: (videoBlob: Blob, metadata: any) => void;
11}) {
12  const [isRecording, setIsRecording] = useState(false);
13  const [duration, setDuration] = useState(0);
14  const [selectedSound, setSelectedSound] = useState<Sound | null>(null);
15  const [selectedEffect, setSelectedEffect] = useState<Effect | null>(null);
16
17  const videoRef = useRef<HTMLVideoElement>(null);
18  const mediaRecorderRef = useRef<MediaRecorder | null>(null);
19  const chunksRef = useRef<Blob[]>([]);
20
21  useEffect(() => {
22    // Initialiser la caméra
23    navigator.mediaDevices.getUserMedia({ video: true, audio: true })
24      .then(stream => {
25        if (videoRef.current) {
26          videoRef.current.srcObject = stream;
27        }
28      });
29  }, []);
30
31  const startRecording = () => {
32    if (!videoRef.current?.srcObject) return;
33
34    const stream = videoRef.current.srcObject as MediaStream;
35    const mediaRecorder = new MediaRecorder(stream);
36
37    mediaRecorder.ondataavailable = (e) => {
38      if (e.data.size > 0) {
39        chunksRef.current.push(e.data);
40      }
41    };
42
43    mediaRecorder.onstop = () => {
44      const blob = new Blob(chunksRef.current, { type: 'video/webm' });
45      onComplete(blob, {
46        duration,
47        sound: selectedSound,
48        effect: selectedEffect
49      });
50    };
51
52    chunksRef.current = [];
53    mediaRecorder.start();
54    mediaRecorderRef.current = mediaRecorder;
55    setIsRecording(true);
56
57    // Compteur de durée
58    const interval = setInterval(() => {
59      setDuration(d => {
60        if (d >= maxDuration) {
61          stopRecording();
62          clearInterval(interval);
63          return d;
64        }
65        return d + 0.1;
66      });
67    }, 100);
68  };
69
70  const stopRecording = () => {
71    if (mediaRecorderRef.current && isRecording) {
72      mediaRecorderRef.current.stop();
73      setIsRecording(false);
74      setDuration(0);
75    }
76  };
77
78  return (
79    <div className="relative h-screen bg-black flex flex-col">
80      {/* Aperçu de la vidéo */}
81      <div className="flex-1 relative">
82        <video
83          ref={videoRef}
84          autoPlay
85          playsInline
86          muted
87          className="w-full h-full object-cover"
88        />
89
90        {/* Superposition d'effet */}
91        {selectedEffect && (
92          <div className="absolute top-4 left-4 bg-black/50 px-3 py-2 rounded-full text-white text-sm">
93            {selectedEffect.name}
94          </div>
95        )}
96
97        {/* Indicateur de son */}
98        {selectedSound && (
99          <div className="absolute top-4 right-4 bg-black/50 px-3 py-2 rounded-full text-white text-sm flex items-center gap-2">
100            {selectedSound.name}
101          </div>
102        )}
103
104        {/* Indicateur d'enregistrement */}
105        {isRecording && (
106          <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
107            <div className="w-20 h-20 border-4 border-red-500 rounded-full animate-pulse"></div>
108          </div>
109        )}
110      </div>
111
112      {/* Commandes */}
113      <div className="bg-black border-t border-gray-800 p-6">
114        {/* Barre de durée */}
115        <div className="mb-6">
116          <div className="h-1 bg-gray-700 rounded-full overflow-hidden">
117            <div
118              className={`h-full transition-all ${
119                isRecording ? 'bg-red-500' : 'bg-gray-500'
120              }`}
121              style={{ width: `${(duration / maxDuration) * 100}%` }}
122            />
123          </div>
124          <div className="flex justify-between mt-2 text-xs text-gray-400">
125            <span>{duration.toFixed(1)}s</span>
126            <span>{maxDuration}s</span>
127          </div>
128        </div>
129
130        {/* Actions */}
131        <div className="flex items-center justify-around">
132          {/* Effets */}
133          <button className="flex flex-col items-center gap-1">
134            <div className="w-12 h-12 bg-gray-800 rounded-full flex items-center justify-center text-2xl">
135              ◆
136            </div>
137            <span className="text-xs text-white">Effets</span>
138          </button>
139
140          {/* Bouton d'enregistrement */}
141          <button
142            onClick={isRecording ? stopRecording : startRecording}
143            className={`w-20 h-20 rounded-full border-4 flex items-center justify-center transition ${
144              isRecording
145                ? 'border-red-500 bg-red-500'
146                : 'border-white bg-transparent'
147            }`}
148          >
149            {isRecording && (
150              <div className="w-8 h-8 bg-white rounded"></div>
151            )}
152          </button>
153
154          {/* Sons */}
155          <button className="flex flex-col items-center gap-1">
156            <div className="w-12 h-12 bg-gray-800 rounded-full flex items-center justify-center text-2xl">
157              ◉
158            </div>
159            <span className="text-xs text-white">Sons</span>
160          </button>
161        </div>
162      </div>
163    </div>
164  );
165}

Résumé

Bibliothèque de sons - découvrir, rechercher, sons tendance Carte de son - aperçu, lecture, utilisation du son Nombre d'utilisations - combien de vidéos utilisent le son Enregistreur de vidéo - accès caméra, enregistrement, durée Effets - effets RA, filtres, mode beauté Commandes d'enregistrement - démarrer/arrêter, barre de durée Sélection du son - choisir une piste audio pour la vidéo Limites de durée - options 15 s, 60 s, 180 s

Dans le prochain exercice, nous ajouterons le duo, le collage et le montage avancé !

À bientôt !

Code de cette leçon : App.jsx
1// Superposition d'infos de la vidéo
2const video = {
3  author: { username: 'CaptainJack', avatar: '', isVerified: true },
4  caption: 'J\'ai trouvé le trésor légendaire de Cortez ! Ce fut un périple épique à travers les sept mers',
5  hashtags: ['treasure', 'adventure', 'piratelife'],
6  likes: 125300,
7  comments: 3420,
8  shares: 892,
9  sound: { name: 'Pirate\'s Anthem - Redbeard' },
10  hasLiked: false,
11  hasBookmarked: false,
12  isFollowingAuthor: false
13};
14
15import { useState } from 'react';
16
17export default function App() {
18  const [videoState, setVideoState] = useState(video);
19
20  const formatCount = (count) => {
21    if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
22    if (count >= 1000) return `${(count / 1000).toFixed(1)}K`;
23    return count.toString();
24  };
25
26  return (
27    <div className="h-screen bg-gradient-to-br from-purple-900 to-blue-900 relative flex items-center justify-center">
28      <div className="text-9xl">◆</div>
29
30      {/* Actions à droite */}
31      <div className="absolute right-4 bottom-20 flex flex-col gap-6 items-center">
32        {/* Avatar */}
33        <div className="relative">
34          <div className="text-4xl">{videoState.author.avatar}</div>
35          {!videoState.isFollowingAuthor && (
36            <button
37              onClick={() => setVideoState({...videoState, isFollowingAuthor: true})}
38              className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 w-6 h-6 bg-red-500 rounded-full text-white flex items-center justify-center"
39            >
40              +
41            </button>
42          )}
43          {videoState.author.isVerified && (
44            <div className="absolute -top-1 -right-1 w-4 h-4 bg-blue-500 rounded-full flex items-center justify-center text-white text-xs">
45              √
46            </div>
47          )}
48        </div>
49
50        {/* J’aime */}
51        <button
52          onClick={() => setVideoState({
53            ...videoState,
54            hasLiked: !videoState.hasLiked,
55            likes: videoState.hasLiked ? videoState.likes - 1 : videoState.likes + 1
56          })}
57          className="flex flex-col items-center gap-1"
58        >
59          <div className={`text-sm font-semibold ${videoState.hasLiked ? 'text-red-500' : 'text-white'}`}>
60            {videoState.hasLiked ? 'Aimé' : 'J’aime'}
61          </div>
62          <span className="text-white text-xs font-bold">
63            {formatCount(videoState.likes)}
64          </span>
65        </button>
66
67        {/* Commentaire */}
68        <button className="flex flex-col items-center gap-1">
69          <div className="text-sm font-semibold text-white">Commenter</div>
70          <span className="text-white text-xs font-bold">
71            {formatCount(videoState.comments)}
72          </span>
73        </button>
74
75        {/* Favori */}
76        <button
77          onClick={() => setVideoState({...videoState, hasBookmarked: !videoState.hasBookmarked})}
78          className="flex flex-col items-center gap-1"
79        >
80          <div className={`text-sm font-semibold ${videoState.hasBookmarked ? 'text-yellow-400' : 'text-white'}`}>
81            {videoState.hasBookmarked ? 'Enregistré' : 'Enregistrer'}
82          </div>
83        </button>
84
85        {/* Partager */}
86        <button className="flex flex-col items-center gap-1">
87          <div className="text-sm font-semibold text-white">Partager</div>
88          <span className="text-white text-xs font-bold">
89            {formatCount(videoState.shares)}
90          </span>
91        </button>
92
93        {/* Son */}
94        <button className="w-10 h-10 rounded-full bg-gradient-to-r from-pink-500 to-purple-500 animate-spin-slow flex items-center justify-center text-xl">
95          ◉
96        </button>
97      </div>
98
99      {/* Infos du bas */}
100      <div className="absolute bottom-4 left-4 right-20">
101        <div className="flex items-center gap-2 mb-2">
102          <span className="text-white font-bold">@{videoState.author.username}</span>
103          {videoState.author.isVerified && <span className="text-blue-400">√</span>}
104          {videoState.isFollowingAuthor && (
105            <span className="text-xs bg-gray-700 px-2 py-0.5 rounded text-white">Abonné</span>
106          )}
107        </div>
108
109        <p className="text-white text-sm mb-2 line-clamp-2">{videoState.caption}</p>
110
111        <div className="flex flex-wrap gap-2 mb-3">
112          {videoState.hashtags.map((tag, idx) => (
113            <span key={idx} className="text-white text-sm font-semibold">#{tag}</span>
114          ))}
115        </div>
116
117        <div className="flex items-center gap-2 bg-black/40 px-3 py-1.5 rounded-full max-w-xs">
118          <span className="text-white text-xs">Musique</span>
119          <span className="text-white text-xs truncate">{videoState.sound.name}</span>
120        </div>
121      </div>
122    </div>
123  );
124}

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. À quoi servent les sons dans TikTok ?

  2. 2. Que stocke le champ useCount dans l'interface Sound ?

Voici 2 des 5 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Tri vertical

    Ordonne les onglets de la page Sounds tels qu'ils apparaissent dans le code (activeTab) :

  • Tri horizontal

    Ordonne les étapes de création d'une vidéo TikTok :

  • Éditeur de code

    Découverte des sons

  • Clique dans l'ordre

    Construis l'expression qui vérifie le nom du son en minuscules (clique dans l'ordre) :

  • Clique dans l'ordre

    Construis la lecture d'une catégorie de son dans une condition (clique dans l'ordre) :

  • Tri horizontal

    Ordonne le début de l'appel qui demande l'accès à la caméra et au micro :

  • Tri vertical

    Ordonne les étapes d'enregistrement vidéo dans VideoRecorder (de haut en bas) :

  • Éditeur de code

    Interface d'enregistrement vidéo

  • Clique dans l'ordre

    Construis la création d'un Blob à partir des morceaux enregistrés (clique dans l'ordre) :

  • Tri horizontal

    Ordonne l'expression qui calcule la largeur de la barre de progression de l'enregistrement :

Articles utiles