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

Duo, Collage et montage vidéo

5 min de lecture
Dans cette leçon6

Les fonctionnalités collaboratives de TikTok - le duo (écran partagé) et le collage (réagir à un extrait) - sont essentielles pour le contenu viral !

Fonctionnalité Duo

1interface DuetVideo {
2  id: string;
3  originalVideoId: string;
4  originalVideo: Video;
5  duetVideoUrl: string; // vidéo du côté droit
6
7  layout: 'side-by-side' | 'top-bottom' | 'green-screen';
8  audioMix: {
9    originalVolume: number; // 0-1
10    duetVolume: number; // 0-1
11  };
12
13  createdAt: Date;
14}
15
16// Créer un duo
17function createDuet(original: Video, duetRecording: Blob): DuetVideo {
18  return {
19    id: generateId(),
20    originalVideoId: original.id,
21    originalVideo: original,
22    duetVideoUrl: URL.createObjectURL(duetRecording),
23    layout: 'side-by-side',
24    audioMix: {
25      originalVolume: 0.7,
26      duetVolume: 1.0
27    },
28    createdAt: new Date()
29  };
30}

Enregistreur de duo

1'use client';
2
3import { useState, useRef } from 'react';
4
5interface DuetRecorderProps {
6  originalVideo: Video;
7  onComplete: (duetVideo: DuetVideo) => void;
8}
9
10export default function DuetRecorder({
11  originalVideo,
12  onComplete
13}: DuetRecorderProps) {
14  const [isRecording, setIsRecording] = useState(false);
15  const [layout, setLayout] = useState<'side-by-side' | 'top-bottom'>('side-by-side');
16
17  const originalVideoRef = useRef<HTMLVideoElement>(null);
18  const userVideoRef = useRef<HTMLVideoElement>(null);
19
20  const startRecording = () => {
21    // Démarrer les deux vidéos simultanément
22    originalVideoRef.current?.play();
23    setIsRecording(true);
24    // Enregistrer la vidéo de l'utilisateur...
25  };
26
27  return (
28    <div className="h-screen bg-black flex flex-col">
29      {/* Sélecteur de disposition */}
30      <div className="bg-gray-900 p-3 flex gap-2">
31        <button
32          onClick={() => setLayout('side-by-side')}
33          className={`px-4 py-2 rounded ${
34            layout === 'side-by-side'
35              ? 'bg-white text-black'
36              : 'bg-gray-700 text-white'
37          }`}
38        >
39          Côte à côte
40        </button>
41        <button
42          onClick={() => setLayout('top-bottom')}
43          className={`px-4 py-2 rounded ${
44            layout === 'top-bottom'
45              ? 'bg-white text-black'
46              : 'bg-gray-700 text-white'
47          }`}
48        >
49          Haut et bas
50        </button>
51      </div>
52
53      {/* Aperçu de la vidéo */}
54      <div className={`flex-1 flex ${
55        layout === 'side-by-side' ? 'flex-row' : 'flex-col'
56      }`}>
57        {/* Vidéo originale */}
58        <div className="flex-1 relative">
59          <video
60            ref={originalVideoRef}
61            src={originalVideo.videoUrl}
62            className="w-full h-full object-cover"
63          />
64          <div className="absolute top-4 left-4 bg-black/50 px-3 py-2 rounded-full text-white text-sm">
65            Originale
66          </div>
67        </div>
68
69        {/* Vidéo de l'utilisateur */}
70        <div className="flex-1 relative border-l-2 border-white">
71          <video
72            ref={userVideoRef}
73            autoPlay
74            playsInline
75            muted
76            className="w-full h-full object-cover"
77          />
78          <div className="absolute top-4 right-4 bg-black/50 px-3 py-2 rounded-full text-white text-sm">
79            Toi
80          </div>
81
82          {isRecording && (
83            <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
84              <div className="w-16 h-16 border-4 border-red-500 rounded-full animate-pulse"></div>
85            </div>
86          )}
87        </div>
88      </div>
89
90      {/* Commandes */}
91      <div className="bg-black p-6">
92        <div className="flex justify-center">
93          <button
94            onClick={startRecording}
95            className="w-20 h-20 bg-red-500 rounded-full border-4 border-white flex items-center justify-center"
96          >
97            {isRecording ? (
98              <div className="w-8 h-8 bg-white rounded"></div>
99            ) : (
100              <span className="text-white text-3xl">⏺</span>
101            )}
102          </button>
103        </div>
104      </div>
105    </div>
106  );
107}

Fonctionnalité Collage

1interface StitchVideo {
2  id: string;
3  originalVideoId: string;
4  originalClip: {
5    videoUrl: string;
6    startTime: number; // à quelle seconde démarrer
7    duration: number; // 1-5 secondes
8  };
9  reactionVideoUrl: string; // ta réaction
10
11  createdAt: Date;
12}
13
14// Le collage permet d'utiliser un extrait de 1 à 5 secondes de la vidéo originale,
15// puis d'ajouter ta réaction

Créateur de collage

1'use client';
2
3import { useState } from 'react';
4
5interface StitchCreatorProps {
6  originalVideo: Video;
7  onComplete: (stitch: StitchVideo) => void;
8}
9
10export default function StitchCreator({
11  originalVideo,
12  onComplete
13}: StitchCreatorProps) {
14  const [clipStart, setClipStart] = useState(0);
15  const [clipDuration, setClipDuration] = useState(5);
16  const [phase, setPhase] = useState<'select-clip' | 'record-reaction'>('select-clip');
17
18  return (
19    <div className="min-h-screen bg-black text-white p-4">
20      {phase === 'select-clip' && (
21        <>
22          <h2 className="text-2xl font-bold mb-4">Sélectionne un extrait (1-5 secondes)</h2>
23
24          {/* Aperçu de la vidéo */}
25          <video
26            src={originalVideo.videoUrl}
27            className="w-full rounded-lg mb-4"
28            controls
29          />
30
31          {/* Sélecteur de timeline */}
32          <div className="mb-6">
33            <div className="flex items-center justify-between mb-2 text-sm">
34              <span>Début : {clipStart.toFixed(1)}s</span>
35              <span>Durée : {clipDuration}s</span>
36            </div>
37
38            <input
39              type="range"
40              min={0}
41              max={originalVideo.duration - clipDuration}
42              step={0.1}
43              value={clipStart}
44              onChange={(e) => setClipStart(parseFloat(e.target.value))}
45              className="w-full"
46            />
47
48            <input
49              type="range"
50              min={1}
51              max={5}
52              value={clipDuration}
53              onChange={(e) => setClipDuration(parseInt(e.target.value))}
54              className="w-full mt-2"
55            />
56          </div>
57
58          <button
59            onClick={() => setPhase('record-reaction')}
60            className="w-full py-4 bg-red-500 rounded-full font-bold"
61          >
62            Suivant : enregistrer la réaction
63          </button>
64        </>
65      )}
66
67      {phase === 'record-reaction' && (
68        <>
69          <h2 className="text-2xl font-bold mb-4">Enregistre ta réaction</h2>
70
71          <div className="space-y-4 mb-6">
72            {/* Aperçu de l'extrait sélectionné */}
73            <div className="bg-gray-900 rounded-lg p-4">
74              <p className="text-sm text-gray-400 mb-2">Ton extrait sélectionné :</p>
75              <p className="font-semibold">
76                {clipStart.toFixed(1)}s - {(clipStart + clipDuration).toFixed(1)}s
77              </p>
78            </div>
79
80            {/* Instructions */}
81            <div className="bg-blue-900/30 rounded-lg p-4 border border-blue-500">
82              <p className="text-sm">
83                L'extrait sera d'abord lu, puis tu pourras enregistrer ta réaction
84              </p>
85            </div>
86          </div>
87
88          <button className="w-full py-4 bg-red-500 rounded-full font-bold">
89            Démarrer l'enregistrement
90          </button>
91        </>
92      )}
93    </div>
94  );
95}

Rogneur et éditeur de vidéo

1'use client';
2
3interface VideoEditorProps {
4  videoBlob: Blob;
5  onSave: (editedVideo: Blob, metadata: any) => void;
6}
7
8export default function VideoEditor({ videoBlob, onSave }: VideoEditorProps) {
9  const [trimStart, setTrimStart] = useState(0);
10  const [trimEnd, setTrimEnd] = useState(60);
11  const [filters, setFilters] = useState<Filter[]>([]);
12  const [caption, setCaption] = useState('');
13
14  const videoUrl = URL.createObjectURL(videoBlob);
15
16  return (
17    <div className="min-h-screen bg-black text-white p-4">
18      <h2 className="text-2xl font-bold mb-4">Monter la vidéo</h2>
19
20      {/* Aperçu de la vidéo */}
21      <video
22        src={videoUrl}
23        className="w-full rounded-lg mb-6"
24        controls
25      />
26
27      {/* Timeline de rognage */}
28      <div className="mb-6">
29        <h3 className="font-bold mb-2">Rogner</h3>
30        <div className="flex items-center gap-4">
31          <input
32            type="range"
33            min={0}
34            max={trimEnd}
35            value={trimStart}
36            onChange={(e) => setTrimStart(parseFloat(e.target.value))}
37            className="flex-1"
38          />
39          <span className="text-sm">{trimStart.toFixed(1)}s</span>
40        </div>
41        <div className="flex items-center gap-4 mt-2">
42          <input
43            type="range"
44            min={trimStart}
45            max={60}
46            value={trimEnd}
47            onChange={(e) => setTrimEnd(parseFloat(e.target.value))}
48            className="flex-1"
49          />
50          <span className="text-sm">{trimEnd.toFixed(1)}s</span>
51        </div>
52      </div>
53
54      {/* Filtres */}
55      <div className="mb-6">
56        <h3 className="font-bold mb-2">Filtres</h3>
57        <div className="flex gap-2 overflow-x-auto">
58          {['None', 'Vintage', 'B&W', 'Warm', 'Cool', 'Vibrant'].map(filter => (
59            <button
60              key={filter}
61              className="px-4 py-2 bg-gray-800 rounded-full whitespace-nowrap"
62            >
63              {filter}
64            </button>
65          ))}
66        </div>
67      </div>
68
69      {/* Légende */}
70      <div className="mb-6">
71        <h3 className="font-bold mb-2">Légende</h3>
72        <textarea
73          value={caption}
74          onChange={(e) => setCaption(e.target.value)}
75          placeholder="Écris une légende..."
76          className="w-full bg-gray-900 text-white p-3 rounded-lg resize-none"
77          rows={3}
78        />
79        <p className="text-xs text-gray-500 mt-1">
80          {caption.length}/150 caractères
81        </p>
82      </div>
83
84      {/* Bouton Publier */}
85      <button
86        onClick={() => onSave(videoBlob, { trimStart, trimEnd, filters, caption })}
87        className="w-full py-4 bg-red-500 rounded-full font-bold"
88      >
89        Publier
90      </button>
91    </div>
92  );
93}

Résumé

Duo - écran partagé avec la vidéo originale Dispositions du duo - côte à côte, haut-bas Mixage audio - équilibre entre l'originale et le duo Collage - utiliser un extrait de 1 à 5 s + ta réaction Sélecteur d'extrait - choisir un moment de la vidéo originale Rogneur de vidéo - couper le début/la fin Filtres - effets vintage, N&B, chaud, froid Éditeur de légende - texte + hashtags Lecture simultanée - l'originale se lit pendant l'enregistrement du duo

Tu as maintenant un clone complet de TikTok avec un fil vertical, des sons, le duo/collage et le montage !

À bientôt !

Code de cette leçon : App.jsx
1// Page de découverte des sons
2import { useState } from 'react';
3
4const sounds = [
5  { id: 1, name: 'Pirate\'s Anthem', artist: 'Redbeard', useCount: 1250000, isTrending: true, icon: '' },
6  { id: 2, name: 'Sea Shanty', artist: 'Morgan Crew', useCount: 890000, isTrending: true, icon: '' },
7  { id: 3, name: 'Treasure Found', artist: 'Jack Sparrow', useCount: 2340000, isTrending: true, icon: '' },
8  { id: 4, name: 'Battle Theme', artist: 'Blackbeard', useCount: 450000, isTrending: false, icon: '' }
9];
10
11export default function App() {
12  const [activeTab, setActiveTab] = useState('trending');
13  const [searchQuery, setSearchQuery] = useState('');
14
15  const filteredSounds = sounds.filter(sound => {
16    if (searchQuery) {
17      return sound.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
18             sound.artist.toLowerCase().includes(searchQuery.toLowerCase());
19    }
20    if (activeTab === 'trending') return sound.isTrending;
21    return true;
22  });
23
24  const formatCount = (count) => {
25    if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
26    if (count >= 1000) return `${(count / 1000).toFixed(1)}K`;
27    return count.toString();
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          <input
38            type="text"
39            value={searchQuery}
40            onChange={(e) => setSearchQuery(e.target.value)}
41            placeholder="Rechercher des sons..."
42            className="w-full bg-gray-900 text-white px-4 py-3 rounded-lg outline-none"
43          />
44        </div>
45
46        {/* Onglets */}
47        <div className="flex border-b border-gray-800">
48          {['trending', 'favorites', 'discover'].map(tab => (
49            <button
50              key={tab}
51              onClick={() => setActiveTab(tab)}
52              className={`flex-1 py-3 text-sm font-semibold capitalize ${
53                activeTab === tab ? 'border-b-2 border-white' : 'text-gray-500'
54              }`}
55            >
56              {tab}
57            </button>
58          ))}
59        </div>
60      </div>
61
62      {/* Liste des sons */}
63      <div className="p-4 space-y-3">
64        {filteredSounds.map(sound => (
65          <div key={sound.id} className="bg-gray-900 rounded-lg p-4 flex items-center gap-4">
66            {/* Icône */}
67            <div className="w-16 h-16 rounded-lg bg-gradient-to-r from-pink-500 to-purple-500 flex items-center justify-center text-4xl">
68              {sound.icon}
69            </div>
70
71            {/* Infos */}
72            <div className="flex-1">
73              <h3 className="font-bold text-white">{sound.name}</h3>
74              <p className="text-sm text-gray-400">{sound.artist}</p>
75              <div className="flex items-center gap-3 mt-1 text-xs text-gray-500">
76                <span>{formatCount(sound.useCount)} vidéos</span>
77                {sound.isTrending && (
78                  <span className="flex items-center gap-1 text-pink-500">
79                    Tendance
80                  </span>
81                )}
82              </div>
83            </div>
84
85            {/* Actions */}
86            <div className="flex flex-col gap-2">
87              <button className="px-4 py-2 bg-white text-black rounded-full text-sm font-semibold hover:bg-gray-200">
88                Utiliser le son
89              </button>
90              <button className="p-2 hover:bg-gray-800 rounded-full">Favori</button>
91            </div>
92          </div>
93        ))}
94      </div>
95    </div>
96  );
97}

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. Qu'est-ce qu'un Duo sur TikTok ?

  2. 2. Que décrit l'objet audioMix dans un Duo ?

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

Tâches pratiques dans le jeu

  • Tri vertical

    Ordonne les valeurs du type de mise en page du Duo telles qu'elles apparaissent dans la définition (de haut en bas) :

  • Tri vertical

    Ordonne les champs renvoyés par createDuet (tels qu'ils apparaissent dans le code) :

  • Éditeur de code

    Interface d'enregistrement de Duo

  • Tri horizontal

    Ordonne les étapes de création d'un Stitch :

  • Tri vertical

    Ordonne les phases de création d'un Stitch dans StitchCreator (de haut en bas) :

  • Éditeur de code

    Interface de montage vidéo

  • Tri horizontal

    Ordonne la ligne qui crée l'URL d'aperçu à partir de videoBlob :

  • Clique dans l'ordre

    Construis la lecture du nombre de caractères de la légende saisie (clique dans l'ordre) :

  • Tri vertical

    Ordonne les sections de l'éditeur vidéo telles qu'elles apparaissent dans l'interface (de haut en bas) :

  • Clique dans l'ordre

    Construis le réglage de la mise en page du Duo sur side-by-side (clique dans l'ordre) :

  • Tri vertical

    Ordonne le parcours utilisateur complet dans Bobines du Trésor (de haut en bas) :

  • Tri horizontal

    Ordonne l'expression qui calcule la fin de l'extrait de Stitch sélectionné :

  • Clique dans l'ordre

    Construis le titre de la page Sons en JSX (clique dans l'ordre) :

Articles utiles