Vibe coding with AI course Β· Module 10: TikTok Clone

Sounds Library and Video Creation

6 min read
In this lesson5

TikTok's power lies in sounds - audio tracks used by millions of videos. Plus advanced video creation with effects!

Sounds Library

interface Sound {
  id: string;
  name: string;
  artist: string;
  duration: number;
  thumbnailUrl: string;

  // Stats
  useCount: number; // how many videos use this sound
  favorites: number;
  isTrending: boolean;

  // Categories
  category: 'original' | 'music' | 'trending' | 'effects';
  tags: string[];

  createdAt: Date;
}

interface SoundUsage {
  soundId: string;
  startTime: number; // which second of sound to start (0-based)
  volume: number; // 0-1
}

Sounds Discovery Page

'use client';

import { useState } from 'react';

interface SoundsPageProps {
  sounds: Sound[];
  onUseSound: (soundId: string) => void;
  onFavorite: (soundId: string) => void;
}

export default function SoundsPage({
  sounds,
  onUseSound,
  onFavorite
}: SoundsPageProps) {
  const [activeTab, setActiveTab] = useState<'trending' | 'favorites' | 'discover'>('trending');
  const [searchQuery, setSearchQuery] = useState('');

  const filteredSounds = sounds.filter(sound => {
    if (searchQuery) {
      return sound.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
             sound.artist.toLowerCase().includes(searchQuery.toLowerCase());
    }

    if (activeTab === 'trending') return sound.isTrending;
    if (activeTab === 'favorites') return sound.favorites > 1000;
    return true;
  });

  return (
    <div className="min-h-screen bg-black text-white">
      {/* Header */}
      <div className="sticky top-0 bg-black z-10 border-b border-gray-800">
        <div className="p-4">
          <h1 className="text-2xl font-bold mb-4">Sounds</h1>

          {/* Search */}
          <input
            type="text"
            value={searchQuery}
            onChange={(e) => setSearchQuery(e.target.value)}
            placeholder="Search sounds..."
            className="w-full bg-gray-900 text-white px-4 py-3 rounded-lg outline-none"
          />
        </div>

        {/* Tabs */}
        <div className="flex border-b border-gray-800">
          {['trending', 'favorites', 'discover'].map(tab => (
            <button
              key={tab}
              onClick={() => setActiveTab(tab as any)}
              className={`flex-1 py-3 text-sm font-semibold capitalize ${
                activeTab === tab
                  ? 'border-b-2 border-white'
                  : 'text-gray-500'
              }`}
            >
              {tab}
            </button>
          ))}
        </div>
      </div>

      {/* Sounds List */}
      <div className="p-4 space-y-3">
        {filteredSounds.map(sound => (
          <SoundCard
            key={sound.id}
            sound={sound}
            onUse={() => onUseSound(sound.id)}
            onFavorite={() => onFavorite(sound.id)}
          />
        ))}
      </div>
    </div>
  );
}

function SoundCard({
  sound,
  onUse,
  onFavorite
}: {
  sound: Sound;
  onUse: () => void;
  onFavorite: () => void;
}) {
  const [isPlaying, setIsPlaying] = useState(false);

  return (
    <div className="bg-gray-900 rounded-lg p-4 flex items-center gap-4">
      {/* Thumbnail */}
      <button
        onClick={() => setIsPlaying(!isPlaying)}
        className="relative w-16 h-16 rounded-lg overflow-hidden shrink-0"
      >
        <img
          src={sound.thumbnailUrl}
          alt={sound.name}
          className={`w-full h-full object-cover ${isPlaying ? 'animate-pulse' : ''}`}
        />
        <div className="absolute inset-0 flex items-center justify-center bg-black/30">
          <span className="text-white text-2xl">
            {isPlaying ? '⏸' : 'β–Ά'}
          </span>
        </div>
      </button>

      {/* Info */}
      <div className="flex-1 min-w-0">
        <h3 className="font-bold text-white truncate">{sound.name}</h3>
        <p className="text-sm text-gray-400 truncate">{sound.artist}</p>

        <div className="flex items-center gap-3 mt-2 text-xs text-gray-500">
          <span>{formatCount(sound.useCount)} videos</span>
          {sound.isTrending && (
            <span className="flex items-center gap-1 text-pink-500">
              Trending
            </span>
          )}
        </div>
      </div>

      {/* Actions */}
      <div className="flex flex-col gap-2">
        <button
          onClick={onUse}
          className="px-4 py-2 bg-white text-black rounded-full text-sm font-semibold hover:bg-gray-200"
        >
          Use Sound
        </button>
        <button
          onClick={onFavorite}
          className="p-2 hover:bg-gray-800 rounded-full"
        >
          Favorite
        </button>
      </div>
    </div>
  );
}

function formatCount(count: number): string {
  if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
  if (count >= 1000) return `${(count / 1000).toFixed(1)}K`;
  return count.toString();
}

Video Creation Flow

interface VideoCreationState {
  // Recording
  recordedChunks: Blob[];
  isRecording: boolean;
  recordingDuration: number;
  maxDuration: number; // 15, 60, or 180 seconds

  // Editing
  selectedSound?: Sound;
  soundVolume: number;
  selectedEffect?: Effect;
  filters: Filter[];
  trimStart: number;
  trimEnd: number;

  // Caption
  caption: string;
  hashtags: string[];
  mentions: string[];

  // Privacy
  privacy: 'public' | 'friends' | 'private';
  allowComments: boolean;
  allowDuet: boolean;
  allowStitch: boolean;
}

interface Effect {
  id: string;
  name: string;
  type: 'beauty' | 'ar' | 'green-screen' | 'time';
  thumbnailUrl: string;
  isPopular: boolean;
}

interface Filter {
  id: string;
  name: string;
  intensity: number; // 0-1
}

Video Recorder

'use client';

import { useState, useRef, useEffect } from 'react';

export default function VideoRecorder({
  maxDuration = 60,
  onComplete: (videoBlob: Blob, metadata: any) => void
}: {
  maxDuration?: number;
  onComplete: (videoBlob: Blob, metadata: any) => void;
}) {
  const [isRecording, setIsRecording] = useState(false);
  const [duration, setDuration] = useState(0);
  const [selectedSound, setSelectedSound] = useState<Sound | null>(null);
  const [selectedEffect, setSelectedEffect] = useState<Effect | null>(null);

  const videoRef = useRef<HTMLVideoElement>(null);
  const mediaRecorderRef = useRef<MediaRecorder | null>(null);
  const chunksRef = useRef<Blob[]>([]);

  useEffect(() => {
    // Initialize camera
    navigator.mediaDevices.getUserMedia({ video: true, audio: true })
      .then(stream => {
        if (videoRef.current) {
          videoRef.current.srcObject = stream;
        }
      });
  }, []);

  const startRecording = () => {
    if (!videoRef.current?.srcObject) return;

    const stream = videoRef.current.srcObject as MediaStream;
    const mediaRecorder = new MediaRecorder(stream);

    mediaRecorder.ondataavailable = (e) => {
      if (e.data.size > 0) {
        chunksRef.current.push(e.data);
      }
    };

    mediaRecorder.onstop = () => {
      const blob = new Blob(chunksRef.current, { type: 'video/webm' });
      onComplete(blob, {
        duration,
        sound: selectedSound,
        effect: selectedEffect
      });
    };

    chunksRef.current = [];
    mediaRecorder.start();
    mediaRecorderRef.current = mediaRecorder;
    setIsRecording(true);

    // Duration counter
    const interval = setInterval(() => {
      setDuration(d => {
        if (d >= maxDuration) {
          stopRecording();
          clearInterval(interval);
          return d;
        }
        return d + 0.1;
      });
    }, 100);
  };

  const stopRecording = () => {
    if (mediaRecorderRef.current && isRecording) {
      mediaRecorderRef.current.stop();
      setIsRecording(false);
      setDuration(0);
    }
  };

  return (
    <div className="relative h-screen bg-black flex flex-col">
      {/* Video Preview */}
      <div className="flex-1 relative">
        <video
          ref={videoRef}
          autoPlay
          playsInline
          muted
          className="w-full h-full object-cover"
        />

        {/* Effect Overlay */}
        {selectedEffect && (
          <div className="absolute top-4 left-4 bg-black/50 px-3 py-2 rounded-full text-white text-sm">
            {selectedEffect.name}
          </div>
        )}

        {/* Sound Indicator */}
        {selectedSound && (
          <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">
            {selectedSound.name}
          </div>
        )}

        {/* Recording Indicator */}
        {isRecording && (
          <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
            <div className="w-20 h-20 border-4 border-red-500 rounded-full animate-pulse"></div>
          </div>
        )}
      </div>

      {/* Controls */}
      <div className="bg-black border-t border-gray-800 p-6">
        {/* Duration Bar */}
        <div className="mb-6">
          <div className="h-1 bg-gray-700 rounded-full overflow-hidden">
            <div
              className={`h-full transition-all ${
                isRecording ? 'bg-red-500' : 'bg-gray-500'
              }`}
              style={{ width: `${(duration / maxDuration) * 100}%` }}
            />
          </div>
          <div className="flex justify-between mt-2 text-xs text-gray-400">
            <span>{duration.toFixed(1)}s</span>
            <span>{maxDuration}s</span>
          </div>
        </div>

        {/* Actions */}
        <div className="flex items-center justify-around">
          {/* Effects */}
          <button className="flex flex-col items-center gap-1">
            <div className="w-12 h-12 bg-gray-800 rounded-full flex items-center justify-center text-2xl">
              β—†
            </div>
            <span className="text-xs text-white">Effects</span>
          </button>

          {/* Record Button */}
          <button
            onClick={isRecording ? stopRecording : startRecording}
            className={`w-20 h-20 rounded-full border-4 flex items-center justify-center transition ${
              isRecording
                ? 'border-red-500 bg-red-500'
                : 'border-white bg-transparent'
            }`}
          >
            {isRecording && (
              <div className="w-8 h-8 bg-white rounded"></div>
            )}
          </button>

          {/* Sounds */}
          <button className="flex flex-col items-center gap-1">
            <div className="w-12 h-12 bg-gray-800 rounded-full flex items-center justify-center text-2xl">
              β—‰
            </div>
            <span className="text-xs text-white">Sounds</span>
          </button>
        </div>
      </div>
    </div>
  );
}

Summary

Sounds library - discover, search, trending sounds Sound card - preview, play, use sound Use count - how many videos use the sound Video recorder - camera access, recording, duration Effects - AR effects, filters, beauty mode Recording controls - start/stop, duration bar Sound selection - pick audio track for video Duration limits - 15s, 60s, 180s options

In the next exercise we'll add duet, stitch, and advanced editing!

See you soon!

Code for this lesson: App.jsx
// Video Info Overlay
const video = {
  author: { username: 'CaptainJack', avatar: '', isVerified: true },
  caption: 'Found the legendary treasure of Cortez! This was an epic journey across seven seas',
  hashtags: ['treasure', 'adventure', 'piratelife'],
  likes: 125300,
  comments: 3420,
  shares: 892,
  sound: { name: "Pirate's Anthem - Redbeard" },
  hasLiked: false,
  hasBookmarked: false,
  isFollowingAuthor: false
};

import { useState } from 'react';

export default function App() {
  const [videoState, setVideoState] = useState(video);

  const formatCount = (count) => {
    if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
    if (count >= 1000) return `${(count / 1000).toFixed(1)}K`;
    return count.toString();
  };

  return (
    <div className="h-screen bg-gradient-to-br from-purple-900 to-blue-900 relative flex items-center justify-center">
      <div className="text-9xl">β—†</div>

      {/* Right Actions */}
      <div className="absolute right-4 bottom-20 flex flex-col gap-6 items-center">
        {/* Avatar */}
        <div className="relative">
          <div className="text-4xl">{videoState.author.avatar}</div>
          {!videoState.isFollowingAuthor && (
            <button
              onClick={() => setVideoState({...videoState, isFollowingAuthor: true})}
              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"
            >
              +
            </button>
          )}
          {videoState.author.isVerified && (
            <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">
              √
            </div>
          )}
        </div>

        {/* Like */}
        <button
          onClick={() => setVideoState({
            ...videoState,
            hasLiked: !videoState.hasLiked,
            likes: videoState.hasLiked ? videoState.likes - 1 : videoState.likes + 1
          })}
          className="flex flex-col items-center gap-1"
        >
          <div className={`text-sm font-semibold ${videoState.hasLiked ? 'text-red-500' : 'text-white'}`}>
            {videoState.hasLiked ? 'Liked' : 'Like'}
          </div>
          <span className="text-white text-xs font-bold">
            {formatCount(videoState.likes)}
          </span>
        </button>

        {/* Comment */}
        <button className="flex flex-col items-center gap-1">
          <div className="text-sm font-semibold text-white">Comment</div>
          <span className="text-white text-xs font-bold">
            {formatCount(videoState.comments)}
          </span>
        </button>

        {/* Bookmark */}
        <button
          onClick={() => setVideoState({...videoState, hasBookmarked: !videoState.hasBookmarked})}
          className="flex flex-col items-center gap-1"
        >
          <div className={`text-sm font-semibold ${videoState.hasBookmarked ? 'text-yellow-400' : 'text-white'}`}>
            {videoState.hasBookmarked ? 'Saved' : 'Save'}
          </div>
        </button>

        {/* Share */}
        <button className="flex flex-col items-center gap-1">
          <div className="text-sm font-semibold text-white">Share</div>
          <span className="text-white text-xs font-bold">
            {formatCount(videoState.shares)}
          </span>
        </button>

        {/* Sound */}
        <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">
          β—‰
        </button>
      </div>

      {/* Bottom Info */}
      <div className="absolute bottom-4 left-4 right-20">
        <div className="flex items-center gap-2 mb-2">
          <span className="text-white font-bold">@{videoState.author.username}</span>
          {videoState.author.isVerified && <span className="text-blue-400">√</span>}
          {videoState.isFollowingAuthor && (
            <span className="text-xs bg-gray-700 px-2 py-0.5 rounded text-white">Following</span>
          )}
        </div>

        <p className="text-white text-sm mb-2 line-clamp-2">{videoState.caption}</p>

        <div className="flex flex-wrap gap-2 mb-3">
          {videoState.hashtags.map((tag, idx) => (
            <span key={idx} className="text-white text-sm font-semibold">#{tag}</span>
          ))}
        </div>

        <div className="flex items-center gap-2 bg-black/40 px-3 py-1.5 rounded-full max-w-xs">
          <span className="text-white text-xs">Music</span>
          <span className="text-white text-xs truncate">{videoState.sound.name}</span>
        </div>
      </div>
    </div>
  );
}

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What are sounds for in TikTok?

  2. 2. What does the useCount field in the Sound interface store?

These are 2 of 5 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Vertical ordering

    Order the Sounds page tabs as they appear in the code (activeTab):

  • Horizontal ordering

    Arrange the steps of creating a TikTok video:

  • Code editor

    Sounds discovery

  • Click in order

    Build the expression that checks the sound name in lowercase (click in order):

  • Click in order

    Build the read of a sound category in a condition (click in order):

  • Horizontal ordering

    Arrange the start of the call requesting camera and microphone access:

  • Vertical ordering

    Order the video recording steps in VideoRecorder (top to bottom):

  • Code editor

    Video recording interface

  • Click in order

    Build the creation of a Blob from the recorded chunks (click in order):

  • Horizontal ordering

    Arrange the expression computing the recording progress bar width:

Useful articles