-
Notifications
You must be signed in to change notification settings - Fork 76
Description
Feature Request: Optional Stream Recording
Overview
Implement the ability for streamers to choose whether their live streams should be automatically recorded by Mux and made available as VOD (Video on Demand) content after the stream ends.
User Story
As a streamer, I want to be able to toggle whether my streams are recorded so that I have control over what content is saved and made available for replay.
Why This Feature?
- User Control: Some streamers want VODs for later viewing, others prefer ephemeral content
- Privacy: Gives streamers control over their content persistence
- Cost Optimization: Reduces Mux storage costs for users who don't need recordings
- Flexibility: Streamers can change this preference at any time
Implementation Details
1. Database Schema Changes
Add recording preference column:
ALTER TABLE users ADD COLUMN enable_recording BOOLEAN DEFAULT false;Create recordings table:
CREATE TABLE stream_recordings (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
stream_session_id INTEGER REFERENCES stream_sessions(id) ON DELETE SET NULL,
mux_asset_id VARCHAR(255) NOT NULL,
playback_id VARCHAR(255) NOT NULL,
title VARCHAR(255),
duration INTEGER, -- Duration in seconds
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(50) DEFAULT 'processing', -- processing, ready, error
UNIQUE(mux_asset_id)
);
CREATE INDEX idx_recordings_user_id ON stream_recordings(user_id);
CREATE INDEX idx_recordings_playback_id ON stream_recordings(playback_id);2. Frontend Changes
A. Add toggle in Stream Settings UI
Location: components/settings/stream-channel-preferences/stream-preference.tsx
Add a new section:
<div className="flex items-center justify-between p-4 border border-gray-700 rounded-lg">
<div className="flex-1">
<h3 className="text-lg font-semibold text-white">Record Live Streams</h3>
<p className="text-sm text-gray-400 mt-1">
Automatically save recordings of your live streams. Recordings will be available
for replay after your stream ends. You can download or delete recordings anytime.
</p>
</div>
<Switch
checked={enableRecording}
onCheckedChange={handleRecordingToggle}
className="ml-4"
/>
</div>B. Update User Type Definition
Location: types/user.ts
Add to User interface:
export interface User {
// ... existing fields
enable_recording?: boolean;
}
export interface UserUpdateInput {
// ... existing fields
enable_recording?: boolean;
}3. Backend API Changes
A. Update Stream Creation API
Location: app/api/streams/create/route.ts
Modify to conditionally enable recording:
// Fetch user's recording preference
const userResult = await sql`
SELECT enable_recording FROM users WHERE wallet = ${wallet}
`;
const user = userResult.rows[0];
// Configure stream with optional recording
const streamConfig: any = {
playback_policy: ["public"],
};
// Only add recording settings if user has enabled it
if (user?.enable_recording) {
streamConfig.new_asset_settings = {
playback_policy: ["public"],
};
}
const stream = await mux.video.liveStreams.create(streamConfig);B. Update User Settings API
Location: app/api/users/updates/[wallet]/route.ts
Add handling for recording preference:
// Handle enable_recording field
const enableRecording = formData.get("enable_recording");
if (enableRecording !== null) {
await sql`
UPDATE users
SET enable_recording = ${enableRecording === 'true'}
WHERE wallet = ${wallet}
`;
}C. Add Webhook Handler for Recordings
Location: app/api/webhooks/mux/route.ts
Add new webhook event handler:
case "video.asset.ready":
// Recording is ready for playback
const assetId = event.data.id;
const assetPlaybackId = event.data.playback_ids?.[0]?.id;
const duration = event.data.duration;
if (assetId && assetPlaybackId) {
// Find the stream session this recording belongs to
const streamResult = await sql`
SELECT ss.id, ss.user_id, ss.title
FROM stream_sessions ss
JOIN users u ON u.id = ss.user_id
WHERE u.mux_stream_id = ${event.data.live_stream_id}
ORDER BY ss.created_at DESC
LIMIT 1
`;
if (streamResult.rows.length > 0) {
const session = streamResult.rows[0];
await sql`
INSERT INTO stream_recordings (
user_id,
stream_session_id,
mux_asset_id,
playback_id,
title,
duration,
status
)
VALUES (
${session.user_id},
${session.id},
${assetId},
${assetPlaybackId},
${session.title || 'Stream Recording'},
${duration || 0},
'ready'
)
ON CONFLICT (mux_asset_id) DO UPDATE
SET status = 'ready', duration = ${duration || 0}
`;
console.log(`✅ Stream recording saved: ${assetId}`);
}
}
break;
case "video.asset.errored":
// Handle recording errors
const erroredAssetId = event.data.id;
await sql`
UPDATE stream_recordings
SET status = 'error'
WHERE mux_asset_id = ${erroredAssetId}
`;
console.error(`❌ Stream recording failed: ${erroredAssetId}`);
break;D. Create Recordings API Endpoints
Create: app/api/streams/recordings/[wallet]/route.ts
import { NextRequest, NextResponse } from "next/server";
import { sql } from "@vercel/postgres";
export async function GET(
req: NextRequest,
{ params }: { params: { wallet: string } }
) {
try {
const { wallet } = params;
// Get user recordings
const result = await sql`
SELECT
r.id,
r.mux_asset_id,
r.playback_id,
r.title,
r.duration,
r.created_at,
r.status,
ss.started_at as stream_date
FROM stream_recordings r
JOIN users u ON u.id = r.user_id
LEFT JOIN stream_sessions ss ON ss.id = r.stream_session_id
WHERE u.wallet = ${wallet}
ORDER BY r.created_at DESC
`;
return NextResponse.json({
success: true,
recordings: result.rows,
});
} catch (error) {
console.error("Error fetching recordings:", error);
return NextResponse.json(
{ success: false, error: "Failed to fetch recordings" },
{ status: 500 }
);
}
}4. UI for Viewing Recordings (Optional)
Create Recordings Page
Location: app/dashboard/recordings/page.tsx
Display user's recorded streams with:
- Thumbnail
- Title
- Duration
- Date recorded
- Playback button
- Download option
- Delete option
Acceptance Criteria
- Database migration adds
enable_recordingcolumn to users table - Database migration creates
stream_recordingstable - Stream settings page has a toggle for "Record Live Streams"
- Toggle state is saved and persists across sessions
- When toggle is ON, Mux creates recordings automatically
- When toggle is OFF, no recordings are created
- Webhook handler saves recording metadata when
video.asset.readyevent fires - User can view their recordings list
- Recordings can be played back using MuxPlayer with
streamType="on-demand" - User types include
enable_recordingfield
Technical Notes
Mux Configuration
- When
new_asset_settingsis included in stream creation, Mux automatically records the stream - Recording starts when stream goes live and stops when stream ends
- Processing typically takes a few minutes after stream ends
video.asset.readywebhook fires when recording is ready for playback
Cost Considerations
- Mux charges for storage of recorded assets
- Default setting should be
falseto avoid unexpected costs - Consider adding a warning about storage costs in the UI
Testing Checklist
- Toggle recording ON → Start stream → End stream → Verify recording appears
- Toggle recording OFF → Start stream → End stream → Verify no recording created
- Toggle setting while stream is live → Should not affect current stream
- Webhook receives
video.asset.ready→ Recording saved to database - Playback recorded video → Works correctly with MuxPlayer
Related Files
components/settings/stream-channel-preferences/stream-preference.tsxapp/api/streams/create/route.tsapp/api/users/updates/[wallet]/route.tsapp/api/webhooks/mux/route.tstypes/user.tsdb/schema.sql