TikTok's collaborative features - duet (split screen) i stitch (react to clip) - są kluczowe dla viral content!
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}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 >
39 ⬌ Side 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 >
49 ⬍ Top & 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}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 reaction1'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}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="Napisz podpis..."
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}✅ Duet - split screen z original video ✅ Duet layouts - side-by-side, top-bottom ✅ Audio mix - balance between original & duet ✅ Stitch - use 1-5s clip + your reaction ✅ Clip selector - wybierz moment z 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
Masz teraz kompletny TikTok clone z vertical feed, sounds, duet/stitch i editing! 🎬
Do zobaczenia! 🚀