We use cookies to enhance your experience on the site
CodeWorlds

Duet, Stitch, and Video Editing

TikTok's collaborative features - duet (split screen) and stitch (react to a clip) - are key to viral content!

Duet Feature

1interface DuetVideo {
2  id: string;
3  originalVideoId: string;
4  originalVideo: Video;
5  duetVideoUrl: string; // right side video
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// Create duet
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}

Duet Recorder

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    // Start both videos simultaneously
22    originalVideoRef.current?.play();
23    setIsRecording(true);
24    // Record user video...
25  };
26
27  return (
28    <div className="h-screen bg-black flex flex-col">
29      {/* Layout Selector */}
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        >
39Side by Side
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        >
49Top & Bottom
50        </button>
51      </div>
52
53      {/* Video Preview */}
54      <div className={`flex-1 flex ${
55        layout === 'side-by-side' ? 'flex-row' : 'flex-col'
56      }`}>
57        {/* Original Video */}
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 bg-opacity-50 px-3 py-2 rounded-full text-white text-sm">
65            Original
66          </div>
67        </div>
68
69        {/* User Video */}
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 bg-opacity-50 px-3 py-2 rounded-full text-white text-sm">
79            You
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      {/* Controls */}
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}

Stitch Feature

1interface StitchVideo {
2  id: string;
3  originalVideoId: string;
4  originalClip: {
5    videoUrl: string;
6    startTime: number; // which second to start
7    duration: number; // 1-5 seconds
8  };
9  reactionVideoUrl: string; // your reaction
10
11  createdAt: Date;
12}
13
14// Stitch allows using 1-5 second clip from original video,
15// then adding your reaction

Stitch Creator

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">Select a clip (1-5 seconds)</h2>
23
24          {/* Video Preview */}
25          <video
26            src={originalVideo.videoUrl}
27            className="w-full rounded-lg mb-4"
28            controls
29          />
30
31          {/* Timeline Selector */}
32          <div className="mb-6">
33            <div className="flex items-center justify-between mb-2 text-sm">
34              <span>Start: {clipStart.toFixed(1)}s</span>
35              <span>Duration: {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            Next: Record Reaction
63          </button>
64        </>
65      )}
66
67      {phase === 'record-reaction' && (
68        <>
69          <h2 className="text-2xl font-bold mb-4">Record your reaction</h2>
70
71          <div className="space-y-4 mb-6">
72            {/* Selected Clip Preview */}
73            <div className="bg-gray-900 rounded-lg p-4">
74              <p className="text-sm text-gray-400 mb-2">Your selected clip:</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 bg-opacity-30 rounded-lg p-4 border border-blue-500">
82              <p className="text-sm">
83                📹 The clip will play first, then you can record your reaction
84              </p>
85            </div>
86          </div>
87
88          <button className="w-full py-4 bg-red-500 rounded-full font-bold">
89            Start Recording
90          </button>
91        </>
92      )}
93    </div>
94  );
95}

Video Trimmer & Editor

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">Edit Video</h2>
19
20      {/* Video Preview */}
21      <video
22        src={videoUrl}
23        className="w-full rounded-lg mb-6"
24        controls
25      />
26
27      {/* Trim Timeline */}
28      <div className="mb-6">
29        <h3 className="font-bold mb-2">Trim</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      {/* Filters */}
55      <div className="mb-6">
56        <h3 className="font-bold mb-2">Filters</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      {/* Caption */}
70      <div className="mb-6">
71        <h3 className="font-bold mb-2">Caption</h3>
72        <textarea
73          value={caption}
74          onChange={(e) => setCaption(e.target.value)}
75          placeholder="Write a caption..."
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 characters
81        </p>
82      </div>
83
84      {/* Post Button */}
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        Post
90      </button>
91    </div>
92  );
93}

Summary 🎓

Duet - split screen with original video ✅ Duet layouts - side-by-side, top-bottom ✅ Audio mix - balance between original & duet ✅ Stitch - use 1-5s clip + your reaction ✅ Clip selector - choose a moment from original video ✅ Video trimmer - cut start/end ✅ Filters - vintage, B&W, warm, cool effects ✅ Caption editor - text + hashtags ✅ Simultaneous playback - original plays while recording duet

You now have a complete TikTok clone with vertical feed, sounds, duet/stitch, and editing! 🎬

See you soon! 🚀

Go to CodeWorlds