Cours Vibe coding avec l'IA · Module 10: Clone de TikTok
Bibliothèque de sons et création de vidéos
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. À quoi servent les sons dans TikTok ?
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 :