Skip to content

Feature: Add optional stream recording with user preference toggle #258

@davedumto

Description

@davedumto

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_recording column to users table
  • Database migration creates stream_recordings table
  • 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.ready event fires
  • User can view their recordings list
  • Recordings can be played back using MuxPlayer with streamType="on-demand"
  • User types include enable_recording field

Technical Notes

Mux Configuration

  • When new_asset_settings is 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.ready webhook fires when recording is ready for playback

Cost Considerations

  • Mux charges for storage of recorded assets
  • Default setting should be false to avoid unexpected costs
  • Consider adding a warning about storage costs in the UI

Testing Checklist

  1. Toggle recording ON → Start stream → End stream → Verify recording appears
  2. Toggle recording OFF → Start stream → End stream → Verify no recording created
  3. Toggle setting while stream is live → Should not affect current stream
  4. Webhook receives video.asset.ready → Recording saved to database
  5. Playback recorded video → Works correctly with MuxPlayer

Related Files

  • components/settings/stream-channel-preferences/stream-preference.tsx
  • app/api/streams/create/route.ts
  • app/api/users/updates/[wallet]/route.ts
  • app/api/webhooks/mux/route.ts
  • types/user.ts
  • db/schema.sql

References

Metadata

Metadata

Assignees

Labels

Stellar WaveIssues in the Stellar wave program

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions