progressive reveal implementation
TT# Progressive Video Reveal System with Mux ## Implementation Guide for NextJS + Supabase + Vercel This document outlines the implementation of our progressive video reveal system using Mux for video processing and delivery. ## System Overview Our platform needs to progressively
- Uploaded
- Uploaded May 15, 2026
- File type
- TXT
- Queried
- 33
Full document
Showing the full document.
# Progressive Video Reveal System with Mux ## Implementation Guide for NextJS + Supabase + Vercel This document outlines the implementation of our progressive video reveal system using Mux for video processing and delivery. ## System Overview Our platform needs to progressively reveal videos as users contribute payments. Instead of generating 1000 different blur levels, we'll use a tiered approach with 10 quality levels and client-side fine-tuning. ### Technology Stack - **Frontend**: js hosted on Vercel - **Backend**: js API routes / Serverless Functions - **Database**: Supabase - **Video Processing & Delivery**: Mux - **Fine-tuning**: WebGL shaders
## Implementation Steps ### 1. Mux Account Setup 1. **Create a Mux account** at com) 2. **Generate API credentials** in the Mux dashboard 3. **Store credentials securely** in Vercel environment variables: ``` MUX_TOKEN_ID=your_token_id MUX_TOKEN_SECRET=your_token_secret ``` ### 2. Database Structure in Supabase Create these tables in Supabase: ```sql -- Store video metadata CREATE TABLE videos (id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), creator_id UUID REFERENCES users(id), title TEXT NOT NULL, description TEXT, total_price DECIMAL NOT NULL, current_amount DECIMAL DEFAULT 0, percent_revealed DECIMAL DEFAULT 0, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW());
-- Store Mux assets for each blur tier CREATE TABLE video_tiers (id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), video_id UUID REFERENCES videos(id), tier_level INTEGER NOT NULL, -- 0-9 (0 = clearest, 9 = most blurred) mux_asset_id TEXT NOT NULL, mux_playback_id TEXT NOT NULL, is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()); ``` ### 3. Video Upload & Processing Workflow #### A. Server-side API Route (`/api/videos/upload`) ```javascript // js import Mux from '@mux/mux-node'; import {createClient} from '@supabase/supabase-js'; // Initialize Mux const {Video} = new Mux({tokenId: MUX_TOKEN_ID, tokenSecret: MUX_TOKEN_SECRET,}); // Initialize Supabase const supabase =
NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY); export default async function handler(req, res) {if == 'POST') {return json({error: 'Method not allowed'});} try {const {title, description, totalPrice, userId} = body; // 1. Create video record in database const {data: video, error} = await insert({title, description, total_price: totalPrice, creator_id: userId, percent_revealed: single(); if (error) throw error; // 2. Create direct upload URL from Mux const upload = await create({cors_origin: NEXT_PUBLIC_APP_URL, new_asset_settings: {playback_policy: 'signed',},}); return json({uploadUrl: url, videoId: id, uploadId: id});} catch (error) error('Error creating upload:', error); return json({error: message});}} ``` #### B. Video Upload Completion Handler (`/api/videos/uploaded`) ```javascript // js import Mux from '@mux/mux-node'; import {createClient} from '@supabase/supabase-js'; import {spawn} from 'child_process'; import fs from 'fs'; import path from 'path';
// Initialize Mux const {Video} = new Mux({tokenId: MUX_TOKEN_ID, tokenSecret: MUX_TOKEN_SECRET,}); // Initialize Supabase const supabase = NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY); export default async function handler(req, res) {if == 'POST') {return json({error: 'Method not allowed'});} try {const {uploadId, videoId} = body; // 1. Wait for upload to complete and get asset details const upload = await get(uploadId); const asset = await asset_id); // 2. Download the video for processing const downloadUrl = await id); // (Download video code - see separate function below) const videoPath = await url, videoId); // 3. Generate blur tiers using FFmpeg const blurLevels = 10; for (let i = 0; i < blurLevels; i++) {const blurAmount = i * (9/9); // 0 to 9 blur strength const outputPath =
join('/tmp', mp4`); // Generate blurred version await blurVideo(videoPath, outputPath, blurAmount); // Upload to Mux const tierAsset = await create({input: outputPath, playback_policy: 'signed',}); // Store tier in database await insert({video_id: videoId, tier_level: i, mux_asset_id: id, mux_playback_id: id}); // Clean up temporary file unlinkSync(outputPath);} // Clean up original downloaded file unlinkSync(videoPath); return json({success: true});} catch (error) error('Error processing uploaded video:', error); return json({error: message});}} // Helper function to download video async function downloadVideo(url, videoId) {const outputPath = join('/tmp', mp4`); // Download implementation... return outputPath;} // Helper function to blur video with FFmpeg async function blurVideo(inputPath, outputPath, blurAmount) {return new Promise((resolve, reject) => {const ffmpeg = spawn('ffmpeg', ['-i', inputPath, '-vf', `boxblur=${blurAmount}:1`, '-c:a', 'copy', outputPath]);
on('close', (code) => {if (code === 0) resolve(); else reject(new Error(`FFmpeg process exited with code ${code}`));});});} ``` ### 4. Video Playback with Progressive Reveal #### A. API Route to Get Video Playback URL (`/api/videos/[id]/playback`) ```javascript // js import Mux from '@mux/mux-node'; import jwt from 'jsonwebtoken'; import {createClient} from '@supabase/supabase-js'; // Initialize Mux const {Video} = new Mux({tokenId: MUX_TOKEN_ID, tokenSecret: MUX_TOKEN_SECRET,}); // Initialize Supabase const supabase = NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY); export default async function handler(req, res) {const {id} = query; try {// 1. Get video reveal percentage const {data: video} = await eq('id', single(); if video) {return
json({error: 'Video not found'});} // 2. Determine which tier to serve const revealPercentage = percent_revealed; const tierLevel = floor(revealPercentage / 10); // 0-9 based on percentage const fineAdjustment = revealPercentage % 10; // Amount for WebGL to adjust // 3. Get the appropriate tier video const {data: tier} = await eq('video_id', eq('tier_level', single(); if tier) {return json({error: 'Video tier not found'});} // 4. Generate signed playback URL const token = sign({sub: mux_playback_id, aud: 'v', exp: now() / 1000) + 3600, // 1 hour expiry}, MUX_PRIVATE_KEY); const playbackUrl = token=${token}`; return json({playbackUrl, fineAdjustment, revealPercentage});} catch (error)
error('Error generating playback URL:', error); return json({error: message});}} ``` #### B. Frontend Video Player Component ```jsx // js import {useState, useEffect, useRef} from 'react'; import Hls from js'; const VideoPlayer = ({videoId}) => {const [playbackData, setPlaybackData] = useState(null); const [loading, setLoading] = useState(true); const videoRef = useRef(null); const canvasRef = useRef(null); const requestRef = useRef(null); // Fetch playback URL useEffect(() => {const getPlaybackUrl = async () => {try {const response = await fetch(`/api/videos/${videoId}/playback`); const data = await json(); setPlaybackData(data); setLoading(false);} catch (error) error('Error fetching playback URL:', error);}}; getPlaybackUrl();}, [videoId]); // Set up video playback useEffect(() => {if
playbackData) return; const videoElement = current; const hls = new Hls(); playbackUrl); attachMedia(videoElement); return () => destroy();};}, [playbackData]); // Set up WebGL for fine-tuning blur useEffect(() => {if playbackData current) return; const video = current; const canvas = current; const gl = getContext('webgl'); // Set up WebGL shader for blur adjustment // (WebGL shader setup code) // Animation loop to render video with adjusted blur const renderVideo = () => {if readyState >= 2) {// Render video to canvas with adjusted blur using WebGL // (WebGL rendering code using fineAdjustment)} current = requestAnimationFrame(renderVideo);}; addEventListener('loadeddata', () =>
width = videoWidth; height = videoHeight; current = requestAnimationFrame(renderVideo);}); return () => current);};}, [playbackData]); if (loading) {return <div>Loading </div>;} return (<div className="relative"> {/* Hidden video element for HLS playback */} <video ref={videoRef} muted={false} controls={false} style={{display: 'none'}} /> {/* Canvas display with blur adjustment */} <canvas ref={canvasRef} className="w-full h-auto" /> {/* Video controls overlay */} <div className="absolute bottom-0 left-0 right-0 p-4 bg-black bg-opacity-50"> <div className="text-white"> revealPercentage)}% Revealed </div> {/* Custom video controls */} </div> </div>);}; export default VideoPlayer; ``` ### 5. Processing Purchase Transactions ```javascript // js import {createClient} from '@supabase/supabase-js'; // Initialize Supabase const supabase =
NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY); export default async function handler(req, res) {if == 'POST') {return json({error: 'Method not allowed'});} const {id} = query; const {amount, userId} = body; try {// 1. Start a Supabase transaction const {data: video, error: videoError} = await eq('id', single(); if (videoError) throw videoError; // 2. Calculate new reveal percentage const newAmount = current_amount + amount; const newPercentage = min(100, (newAmount / total_price) * 100); // 3. Update video record const {error: updateError} = await update({current_amount: newAmount, percent_revealed: newPercentage, updated_at: new eq('id', id); if (updateError) throw updateError; // 4. Cleanup obsolete tiers if threshold passed const oldTierLevel =
percent_revealed / 10); const newTierLevel = floor(newPercentage / 10); if (newTierLevel > oldTierLevel) {// Deactivate obsolete tiers await update({is_active: eq('video_id', lt('tier_level', newTierLevel - 1); // Keep one tier back for safety // Optional: Schedule actual deletion via a background job} return json({success: true, newPercentage, newTierLevel});} catch (error) error('Error processing purchase:', error); return json({error: message});}} ``` ## WebGL Shader for Fine-Tuning Here's a basic implementation of the WebGL shader for blur adjustment: ```javascript // Setup WebGL for blur adjustment function setupWebGL(gl, video, fineAdjustment) {// Vertex shader - positions and texture coordinates const vertexShaderSource = ` attribute vec2 a_position; attribute vec2 a_texCoord; varying vec2 v_texCoord; void main() {gl_Position = vec4(a_position, 0, 1); v_texCoord = a_texCoord;} `; // Fragment shader - applies adjustable gaussian blur const fragmentShaderSource = ` precision mediump float; varying vec2 v_texCoord; uniform sampler2D u_image; uniform vec2 u_textureSize; uniform float u_blurAmount; void main() {vec4 color = vec4(0.0); float blurRadius = u_blurAmount * 0.01; // Scale down the blur amount // Simple Gaussian blur implementation for (int i = -2; i <= 2; i++) {for (int j = -2; j <= 2; j++) {vec2 offset = vec2(float(i), float(j)) * blurRadius; color += texture2D(u_image, v_texCoord + offset / u_textureSize);}} gl_FragColor = color / 25.0;} `; // Compile and link shaders // (Shader compilation code) // Set up buffers for a simple quad covering the canvas // (Buffer setup code) // Set uniforms
getUniformLocation(program, "u_textureSize"), videoWidth, videoHeight); getUniformLocation(program, "u_blurAmount"), fineAdjustment);} ``` ## Cleanup Process For automatic tier cleanup, you can implement a scheduled function, using Vercel Cron Jobs): ```javascript // js import Mux from '@mux/mux-node'; import {createClient} from '@supabase/supabase-js'; export default async function handler(req, res) {// Only allow this to be run as a scheduled job if == `Bearer CRON_SECRET}`) {return json({error: 'Unauthorized'});} const supabase = NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY); const {Video} = new Mux({tokenId: MUX_TOKEN_ID, tokenSecret: MUX_TOKEN_SECRET,}); try {// Get inactive tiers to clean up const {data: inactiveTiers} = await eq('is_active', false); // Process in batches to avoid rate limits for (const tier of inactiveTiers) {// Delete from Mux await
mux_asset_id); // Delete from database await eq('id', id);} return json({success: true, count: length});} catch (error) error('Error cleaning up tiers:', error); return json({error: message});}} ``` ## Conclusion This implementation gives you a secure, efficient progressive video reveal system using Mux. The approach: 1. Minimizes storage by using only 10 quality tiers 2. Provides smooth transitions with WebGL fine-tuning 3. Handles security through Mux's signed URLs 4. Automatically cleans up obsolete tiers to optimize costs For further optimizations: - Consider implementing a CDN in front of Mux for even faster delivery - Add video analytics using Mux Data - Implement WebGL shader variations for different reveal effects
Want to learn more?
Ask about this document