DS DevShelfHub Projects · AI tools
Articles / Build a Next.js Video Player with ImageKit: Uploads, Auto-Thumbnails, Watermarks, and Adaptive Quality

AI Engineering

Build a Next.js Video Player with ImageKit: Upload and Stream

By DevShelfHub

A complete walkthrough for building a production-shape video upload-and-playback app in Next.js — secure server-issued upload tokens, auto-generated thumbnails via URL transformations, image-overlay watermarks, quality switching, format conversion, and the path to adaptive bitrate streaming. The whole media stack collapsed into URL parameters.

Build a Next.js Video Player with ImageKit: Upload and Stream

Introduction

Building a serious video player inside a Next.js app sounds straightforward until you start listing the work. Uploads, thumbnails (often auto-generated), watermarks, playback controls, quality switching, format conversion, secure server-side auth for uploads, a database to track metadata, and routes for watching individual videos. That’s a stack, not a feature.

The shortcut most production teams take is to push the heavy lifting — storage, transformation, optimisation, delivery — to an image and video API. This guide walks through building a complete video upload-and-playback app in Next.js using ImageKit as the media layer. It covers the upload pipeline, server-side auth handshake, URL-based transformations (thumbnails, watermarks, quality, format), and the player itself. Output is a clean base you can extend for any video-driven product.

📚 Table of contents

  • The architecture: client uploads, server-issued tokens
  • Project scaffold and environment variables
  • The upload-auth route — the security boundary
  • Storing video metadata (local JSON, swap for a real DB later)
  • The upload UI and end-to-end flow
  • The video library: auto-generated thumbnails from a URL
  • The simple player: transformations, posters, quality control
  • Watermarks via URL transformations
  • Adaptive bitrate streaming for production
  • Common mistakes & pro tips
  • Frequently asked questions

🧱 The architecture

The single design decision that drives the whole app: never put your ImageKit private key in the browser. The client asks the Next.js backend for a short-lived signed token, then uses that token to upload directly to ImageKit. The backend stays in control of who can upload, how often, and how large.

  1. Client calls /api/upload-auth.
  2. Server returns a token, signature, and expiration using the ImageKit private key.
  3. Client uploads directly to ImageKit using the token.
  4. Client posts the resulting file path and metadata to /api/videos.
  5. Server persists metadata; client redirects to /watch/[id].

🛠️ Project scaffold

Create a Next.js app with the App Router and TypeScript. Install the ImageKit Next.js SDK and the uuid package for unique video ids. Add three env vars:

  • NEXT_PUBLIC_IMAGEKIT_URL_ENDPOINT — safe to expose to the browser.
  • IMAGEKIT_PUBLIC_KEY — safe to expose to the browser.
  • IMAGEKIT_PRIVATE_KEY — server-side only. Used to sign upload tokens.

Folder layout: src/app/api/upload-auth/route.ts, src/app/api/videos/route.ts (list + create), src/app/api/videos/[id]/route.ts (single video), and src/app/watch/[id]/page.tsx for the watch page. Components live under src/components/video/ with a nested player/ folder.

🔐 The upload-auth route

This is the security boundary. The route reads both keys from the environment, calls getUploadAuthParameters from @imagekit/next/server, and returns a JSON object with a token, signature, expire timestamp, and the public key. The client never sees the private key.

TypeScript — src/app/api/upload-auth/route.ts
import { getUploadAuthParameters } from "@imagekit/next/server";
import { NextResponse } from "next/server";

export async function GET() {
  try {
    const privateKey = process.env.IMAGEKIT_PRIVATE_KEY!;
    const publicKey = process.env.IMAGEKIT_PUBLIC_KEY!;
    const auth = getUploadAuthParameters({ privateKey, publicKey });
    return NextResponse.json({ ...auth, publicKey });
  } catch (err) {
    return NextResponse.json(
      { error: "Failed to generate upload credentials" },
      { status: 500 }
    );
  }
}

Why short-lived tokens matter

Each token authorises one upload and expires quickly. Even if it leaks, the blast radius is a single file. Long-lived API keys are a much larger risk.

Wrap the handler in try/catch, return 500 with a generic error message if something fails, and don’t leak the underlying exception to the client.

📁 Storing video metadata

To keep the tutorial focused, store metadata in a local data/videos.json file. The TypeScript record:

TypeScript — src/types/video.ts
export interface WatermarkConfig {
  imagePath: string;
  position: "top_left" | "top_right" | "bottom_left" | "bottom_right";
  opacity?: number;
  width?: number;
}

export interface Video {
  id: string;
  title: string;
  description: string;
  filePath: string;
  fileName: string;
  thumbnailPath?: string;
  duration?: number;
  createdAt: string;
  watermark?: WatermarkConfig;
  quality?: number;
  format?: "auto" | "mp4" | "webm";
}

Expose getAllVideos, getVideoById, and saveVideo from src/lib/video-storage.ts. In production swap this for Postgres, Supabase, or any database of choice — the function signatures stay the same.

🎬 The upload UI

The VideoUpload component is a client component holding form state for title, description, video file, optional thumbnail, and optional watermark. On submit it:

  1. Calls /api/upload-auth for the token.
  2. Uses upload() from @imagekit/next to push the video, thumbnail, and watermark in parallel via Promise.all.
  3. Posts the resulting paths plus user-entered metadata to /api/videos.
  4. Redirects to /watch/[id] on success.

Free-tier ImageKit caps uploads at around 20–40 MB. Surface that error in the UI so users know why a large file failed.

📚 The video library — auto-generated thumbnails

The home page lists all videos with thumbnails. The clever ImageKit trick: if you don’t upload a thumbnail, append /ik-thumbnail.jpg to the video URL and ImageKit returns a thumbnail image generated from the first frame — cached after the first request.

👉 Pair this with the <Image> component from @imagekit/next and pass a transformation array ([{ width: 320, height: 180 }]) to deliver optimally-sized thumbnails for every breakpoint.

🎛️ The simple player — transformations, posters, quality

The SimplePlayer component takes a video record and the URL endpoint. It builds a transformation array that handles quality, optional format conversion, and an optional watermark overlay. The watermark uses the image-overlay transformation with type: 'image', input (the watermark path), position, and transformation: [{ width: 120 }].

Quality switching

  • Local quality state (1–100) drives a number input.
  • The <Video> component keys off quality so it remounts on change.
  • Anything below ~80 still looks great and dramatically reduces bandwidth.
  • buildSrc generates the poster URL with its own transformation (1920x1080).

Add suppressHydrationWarning on the quality input to silence the React hydration warning that pops up on browser-only state.

🖼️ Watermarks via URL transformations

ImageKit transformations are query-parameter overlays on the asset URL. To watermark a video, upload a watermark image once, then pass it as an image overlay in the player’s transformation array. The watermark appears in the chosen corner with the chosen opacity and width.

Because the transformation lives in the URL, the watermarked video plays anywhere — you could paste the URL into a browser tab and the watermark would still be baked in. That makes it trivial to test in isolation.

⚡ Adaptive bitrate streaming for production

Once you ship past a few users, the static-MP4-with-quality-slider approach hits a ceiling. ImageKit exposes adaptive bitrate streaming via the ik-master transformation, returning an HLS manifest that automatically scales quality to the viewer’s bandwidth.

The trade-off: not every URL transformation (overlays, format conversion) composes cleanly with HLS. For most apps the right pattern is two delivery modes — rich-transformation MP4 for marketing or hero pieces, adaptive HLS for the long tail.

💡 Common mistakes & pro tips

❌ Common mistakes

  • Leaking IMAGEKIT_PRIVATE_KEY to the browser by prefixing it NEXT_PUBLIC_.
  • Using the wrong env var name on the server (IMAGEKIT_URL_ENDPOINT vs NEXT_PUBLIC_IMAGEKIT_URL_ENDPOINT).
  • Hard-coding transformation widths instead of matching CSS breakpoints.
  • Skipping the try/catch on the upload-auth route and leaking stack traces.

✅ Pro tips

  • Generate thumbnails via /ik-thumbnail.jpg first; only upload custom ones for hero videos.
  • Cap upload size on the backend before issuing a token.
  • Cache poster URLs — transformation generation is fast but not free on first hit.
  • Use a real database from day one if you expect > 100 videos.

Conclusion

A real video feature is a stack of small problems — auth, uploads, thumbnails, watermarks, playback, optimisation, delivery. Pushing the media-handling pieces to a URL-driven API and keeping your Next.js app focused on the auth boundary, metadata, and player UI cuts the work to a single weekend. From here, the obvious next steps are a real database, signed playback URLs for private content, captions, and HLS for adaptive delivery once your audience grows.

Related reading: OnSpace AI mobile app builder tutorialClaude Code hands-on guideCursor AI reviewPostgres TimescaleDB time-series fixlearn Python in 2026: zero to specialization

Build a Next.js Video Player with ImageKit: Uploads, Auto-Thumbnails, Watermarks, and Adaptive Quality FAQ

Why not just upload to S3 directly?

You can, but you'd still need to build thumbnail generation, transcoding, watermarking, and adaptive streaming yourself. Media APIs like ImageKit, Mux, or Cloudinary collapse all of that into URL parameters.

Can I do server-side rendering of the watch page?

Yes — the watch route is a server component that reads the video by id, then renders the client-side SimplePlayer with the video and URL endpoint as props.

How big can uploaded videos be?

Free tier caps you at roughly 20–40 MB. Paid plans lift that significantly. For very large files, chunked uploads or pre-signed direct-to-storage flows are the production path.

What about subtitles and captions?

Store WebVTT files alongside the video and pass them as track elements inside the Video component. ImageKit also exposes a captions transformation if you prefer baking captions into the stream.