VideoCascadeLogo
VideoCascade

Combining Videos

Complete examples for combining multiple videos with per-file segment removal and audio control

Learn how to combine multiple videos into a single seamless output with advanced per-file controls. This guide provides production-ready examples for creating video compilations, courses, and complex video compositions.

Overview

Video combining allows you to:

  • Merge multiple videos into a single output
  • Remove segments from individual videos before combining
  • Control audio per video (mute specific videos)
  • Apply processing to the combined output
  • Create compilations for courses, social media, and more

Basic Combination

The simplest example: combine 2-3 videos into one.

async function combineVideos(videoUrls) {
  try {
    const response = await fetch('https://api.videocascade.com/v1/videos', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer vca_your_api_key',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        combine: true,
        files: videoUrls.map(url => ({ url })),
      }),
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const data = await response.json();
    console.log('Combination started:', data.videoId);

    return data.videoId;

} catch (error) {
console.error('Error combining videos:', error);
throw error;
}
}

// Usage
const videoId = await combineVideos([
'https://example.com/intro.mp4',
'https://example.com/main-content.mp4',
'https://example.com/outro.mp4',
]);

// Wait for completion
const result = await waitForCompletion(videoId);
console.log('Combined video:', result.videoUrl);
import requests

def combine_videos(video_urls):
    """Combine multiple videos into one"""
    try:
        response = requests.post(
            'https://api.videocascade.com/v1/videos',
            headers={
                'Authorization': 'Bearer vca_your_api_key',
                'Content-Type': 'application/json',
            },
            json={
                'combine': True,
                'files': [{'url': url} for url in video_urls],
            }
        )

        response.raise_for_status()
        data = response.json()

        print(f"Combination started: {data['videoId']}")
        return data['videoId']

    except requests.exceptions.RequestException as error:
        print(f"Error combining videos: {error}")
        raise

# Usage
video_id = combine_videos([
    'https://example.com/intro.mp4',
    'https://example.com/main-content.mp4',
    'https://example.com/outro.mp4',
])

# Wait for completion
result = wait_for_completion(video_id)
print(f"Combined video: {result['videoUrl']}")
interface FileInput {
  url: string;
  removeSegments?: Array<{ startTime: number; endTime: number }>;
  disableAudio?: boolean;
}

interface CombineRequest {
combine: true;
files: FileInput[];
}

async function combineVideos(videoUrls: string[]): Promise<string> {
try {
const request: CombineRequest = {
combine: true,
files: videoUrls.map(url => ({ url })),
};

    const response = await fetch('https://api.videocascade.com/v1/videos', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer vca_your_api_key',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(request),
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const data = await response.json();
    console.log('Combination started:', data.videoId);

    return data.videoId;

} catch (error) {
console.error('Error combining videos:', error);
throw error;
}
}

// Usage
const videoId = await combineVideos([
'https://example.com/intro.mp4',
'https://example.com/main-content.mp4',
'https://example.com/outro.mp4',
]);

// Wait for completion
const result = await waitForCompletion(videoId);
console.log('Combined video:', result.videoUrl);

Per-File Segment Removal

Remove specific segments from individual videos before combining them.

async function combineWithSegmentRemoval() {
  const response = await fetch('https://api.videocascade.com/v1/videos', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer vca_your_api_key',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      combine: true,
      files: [
        {
          url: 'https://example.com/intro.mp4',
          // Remove first 3 seconds (fade in)
          removeSegments: [
            { startTime: 0, endTime: 3 }
          ]
        },
        {
          url: 'https://example.com/main-content.mp4',
          // Remove multiple awkward pauses
          removeSegments: [
            { startTime: 45, endTime: 52 },    // 7-second pause
            { startTime: 120, endTime: 135 },  // 15-second pause
            { startTime: 200, endTime: 205 }   // 5-second pause
          ]
        },
        {
          url: 'https://example.com/outro.mp4',
          // Remove last 5 seconds (fade out)
          removeSegments: [
            { startTime: 25, endTime: 30 }
          ]
        }
      ],
    }),
  });

  const data = await response.json();
  console.log('Processing started:', data.videoId);

  return data.videoId;
}

// Usage
const videoId = await combineWithSegmentRemoval();
const result = await waitForCompletion(videoId);
console.log('Combined video (with segments removed):', result.videoUrl);
def combine_with_segment_removal():
    """Combine videos with per-file segment removal"""
    response = requests.post(
        'https://api.videocascade.com/v1/videos',
        headers={
            'Authorization': 'Bearer vca_your_api_key',
            'Content-Type': 'application/json',
        },
        json={
            'combine': True,
            'files': [
                {
                    'url': 'https://example.com/intro.mp4',
                    # Remove first 3 seconds (fade in)
                    'removeSegments': [
                        {'startTime': 0, 'endTime': 3}
                    ]
                },
                {
                    'url': 'https://example.com/main-content.mp4',
                    # Remove multiple awkward pauses
                    'removeSegments': [
                        {'startTime': 45, 'endTime': 52},
                        {'startTime': 120, 'endTime': 135},
                        {'startTime': 200, 'endTime': 205}
                    ]
                },
                {
                    'url': 'https://example.com/outro.mp4',
                    # Remove last 5 seconds (fade out)
                    'removeSegments': [
                        {'startTime': 25, 'endTime': 30}
                    ]
                }
            ],
        }
    )

    response.raise_for_status()
    data = response.json()

    print(f"Processing started: {data['videoId']}")
    return data['videoId']

# Usage

video_id = combine_with_segment_removal()
result = wait_for_completion(video_id)
print(f"Combined video (with segments removed): {result['videoUrl']}")
interface RemoveSegment {
  startTime: number;
  endTime: number;
}

interface FileInput {
  url: string;
  removeSegments?: RemoveSegment[];
  disableAudio?: boolean;
}

async function combineWithSegmentRemoval(): Promise<string> {
  const files: FileInput[] = [
    {
      url: 'https://example.com/intro.mp4',
      // Remove first 3 seconds (fade in)
      removeSegments: [
        { startTime: 0, endTime: 3 }
      ]
    },
    {
      url: 'https://example.com/main-content.mp4',
      // Remove multiple awkward pauses
      removeSegments: [
        { startTime: 45, endTime: 52 },
        { startTime: 120, endTime: 135 },
        { startTime: 200, endTime: 205 }
      ]
    },
    {
      url: 'https://example.com/outro.mp4',
      // Remove last 5 seconds (fade out)
      removeSegments: [
        { startTime: 25, endTime: 30 }
      ]
    }
  ];

  const response = await fetch('https://api.videocascade.com/v1/videos', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer vca_your_api_key',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      combine: true,
      files,
    }),
  });

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  const data = await response.json();
  console.log('Processing started:', data.videoId);

  return data.videoId;
}

// Usage
const videoId = await combineWithSegmentRemoval();
const result = await waitForCompletion(videoId);
console.log('Combined video (with segments removed):', result.videoUrl);

Per-File Audio Control

Control which videos contribute audio to the final output.

async function combineWithAudioControl() {
  const response = await fetch('https://api.videocascade.com/v1/videos', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer vca_your_api_key',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      combine: true,
      files: [
        {
          url: 'https://example.com/b-roll-intro.mp4',
          disableAudio: true  // Silent B-roll footage
        },
        {
          url: 'https://example.com/main-interview.mp4'
          // Audio enabled (default) - interview audio
        },
        {
          url: 'https://example.com/b-roll-middle.mp4',
          disableAudio: true  // Silent B-roll footage
        },
        {
          url: 'https://example.com/conclusion.mp4'
          // Audio enabled - conclusion audio
        },
        {
          url: 'https://example.com/b-roll-outro.mp4',
          disableAudio: true  // Silent B-roll footage
        }
      ],
    }),
  });

const data = await response.json();
return data.videoId;
}

// Usage: Create a documentary-style video with B-roll
const videoId = await combineWithAudioControl();
console.log('Creating documentary-style video:', videoId);
def combine_with_audio_control():
    """Combine videos with per-file audio control"""
    response = requests.post(
        'https://api.videocascade.com/v1/videos',
        headers={
            'Authorization': 'Bearer vca_your_api_key',
            'Content-Type': 'application/json',
        },
        json={
            'combine': True,
            'files': [
                {
                    'url': 'https://example.com/b-roll-intro.mp4',
                    'disableAudio': True  # Silent B-roll footage
                },
                {
                    'url': 'https://example.com/main-interview.mp4'
                    # Audio enabled (default) - interview audio
                },
                {
                    'url': 'https://example.com/b-roll-middle.mp4',
                    'disableAudio': True  # Silent B-roll footage
                },
                {
                    'url': 'https://example.com/conclusion.mp4'
                    # Audio enabled - conclusion audio
                },
                {
                    'url': 'https://example.com/b-roll-outro.mp4',
                    'disableAudio': True  # Silent B-roll footage
                }
            ],
        }
    )

    response.raise_for_status()
    data = response.json()
    return data['videoId']

# Usage: Create a documentary-style video with B-roll
video_id = combine_with_audio_control()
print(f"Creating documentary-style video: {video_id}")
async function combineWithAudioControl(): Promise<string> {
  const files: FileInput[] = [
    {
      url: 'https://example.com/b-roll-intro.mp4',
      disableAudio: true  // Silent B-roll footage
    },
    {
      url: 'https://example.com/main-interview.mp4'
      // Audio enabled (default) - interview audio
    },
    {
      url: 'https://example.com/b-roll-middle.mp4',
      disableAudio: true  // Silent B-roll footage
    },
    {
      url: 'https://example.com/conclusion.mp4'
      // Audio enabled - conclusion audio
    },
    {
      url: 'https://example.com/b-roll-outro.mp4',
      disableAudio: true  // Silent B-roll footage
    }
  ];

const response = await fetch('https://api.videocascade.com/v1/videos', {
method: 'POST',
headers: {
'Authorization': 'Bearer vca_your_api_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
combine: true,
files,
}),
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
return data.videoId;
}

// Usage: Create a documentary-style video with B-roll
const videoId = await combineWithAudioControl();
console.log('Creating documentary-style video:', videoId);

Combining with Audio Processing

Add background music and normalize audio levels across combined videos.

async function combineWithBackgroundMusic() {
  const response = await fetch('https://api.videocascade.com/v1/videos', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer vca_your_api_key',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      combine: true,
      files: [
        { url: 'https://example.com/video1.mp4' },
        { url: 'https://example.com/video2.mp4' },
        { url: 'https://example.com/video3.mp4' }
      ],
      // Audio processing for combined output
      normalizeAudio: true,      // Consistent volume levels
      removeSilence: false,      // Keep silence (for music)
      // Add background music overlay
      elements: [
        {
          type: 'audio',
          url: 'https://example.com/background-music.mp3',
          timing: { entireVideo: true },
          effects: {
            volume: 0.2,  // 20% volume
            fadeIn: { duration: 2 },
            fadeOut: { duration: 3 }
          },
          loop: true
        }
      ]
    }),
  });

  const data = await response.json();
  return data.videoId;
}

// Usage
const videoId = await combineWithBackgroundMusic();
console.log('Combining videos with background music:', videoId);
def combine_with_background_music():
    """Combine videos and add background music"""
    response = requests.post(
        'https://api.videocascade.com/v1/videos',
        headers={
            'Authorization': 'Bearer vca_your_api_key',
            'Content-Type': 'application/json',
        },
        json={
            'combine': True,
            'files': [
                {'url': 'https://example.com/video1.mp4'},
                {'url': 'https://example.com/video2.mp4'},
                {'url': 'https://example.com/video3.mp4'}
            ],
            # Audio processing for combined output
            'normalizeAudio': True,      # Consistent volume levels
            'removeSilence': False,      # Keep silence (for music)
            # Add background music overlay
            'elements': [
                {
                    'type': 'audio',
                    'url': 'https://example.com/background-music.mp3',
                    'timing': {'entireVideo': True},
                    'effects': {
                        'volume': 0.2,  # 20% volume
                        'fadeIn': {'duration': 2},
                        'fadeOut': {'duration': 3}
                    },
                    'loop': True
                }
            ]
        }
    )

    response.raise_for_status()
    data = response.json()
    return data['videoId']

# Usage

video_id = combine_with_background_music()
print(f"Combining videos with background music: {video_id}")
interface AudioElement {
  type: 'audio';
  url: string;
  timing: { entireVideo: boolean };
  effects?: {
    volume?: number;
    fadeIn?: { duration: number };
    fadeOut?: { duration: number };
  };
  loop?: boolean;
}

async function combineWithBackgroundMusic(): Promise<string> {
  const backgroundMusic: AudioElement = {
    type: 'audio',
    url: 'https://example.com/background-music.mp3',
    timing: { entireVideo: true },
    effects: {
      volume: 0.2,
      fadeIn: { duration: 2 },
      fadeOut: { duration: 3 }
    },
    loop: true
  };

  const response = await fetch('https://api.videocascade.com/v1/videos', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer vca_your_api_key',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      combine: true,
      files: [
        { url: 'https://example.com/video1.mp4' },
        { url: 'https://example.com/video2.mp4' },
        { url: 'https://example.com/video3.mp4' }
      ],
      normalizeAudio: true,
      removeSilence: false,
      elements: [backgroundMusic]
    }),
  });

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  const data = await response.json();
  return data.videoId;
}

// Usage
const videoId = await combineWithBackgroundMusic();
console.log('Combining videos with background music:', videoId);

Advanced: Combining with Overlays

Create complex compositions with videos, overlays, and audio.

async function createBrandedCompilation() {
  const response = await fetch('https://api.videocascade.com/v1/videos', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer vca_your_api_key',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      combine: true,
      files: [
        {
          url: 'https://example.com/clip1.mp4',
          removeSegments: [{ startTime: 0, endTime: 2 }]
        },
        {
          url: 'https://example.com/clip2.mp4',
          removeSegments: [{ startTime: 8, endTime: 10 }]
        },
        {
          url: 'https://example.com/clip3.mp4'
        }
      ],
      // Post-combination processing
      aspectRatio: '16:9',
      normalizeAudio: true,
      compressionQuality: 95,
      // Add overlays
      elements: [
        // Watermark logo (entire video)
        {
          type: 'image',
          url: 'https://example.com/watermark.png',
          timing: { entireVideo: true },
          position: { anchor: 'bottom-right' },
          size: { width: '15%' },
          effects: { opacity: 0.7 },
          zIndex: 100
        },
        // Opening title (first 5 seconds)
        {
          type: 'image',
          url: 'https://example.com/title-card.png',
          timing: { startTime: 0, endTime: 5 },
          position: { anchor: 'center' },
          size: { width: '60%' },
          effects: {
            fadeIn: { duration: 0.5 },
            fadeOut: { duration: 0.5 }
          },
          zIndex: 50
        },
        // Background music
        {
          type: 'audio',
          url: 'https://example.com/upbeat-music.mp3',
          timing: { entireVideo: true },
          effects: {
            volume: 0.25,
            fadeIn: { duration: 2 },
            fadeOut: { duration: 3 }
          },
          loop: true
        }
      ]
    }),
  });

const data = await response.json();
return data.videoId;
}

// Usage
const videoId = await createBrandedCompilation();
console.log('Creating branded compilation:', videoId);

const result = await waitForCompletion(videoId);
console.log('Compilation ready:', result.videoUrl);
def create_branded_compilation():
    """Create a branded video compilation with overlays"""
    response = requests.post(
        'https://api.videocascade.com/v1/videos',
        headers={
            'Authorization': 'Bearer vca_your_api_key',
            'Content-Type': 'application/json',
        },
        json={
            'combine': True,
            'files': [
                {
                    'url': 'https://example.com/clip1.mp4',
                    'removeSegments': [{'startTime': 0, 'endTime': 2}]
                },
                {
                    'url': 'https://example.com/clip2.mp4',
                    'removeSegments': [{'startTime': 8, 'endTime': 10}]
                },
                {
                    'url': 'https://example.com/clip3.mp4'
                }
            ],
            # Post-combination processing
            'aspectRatio': '16:9',
            'normalizeAudio': True,
            'compressionQuality': 95,
            # Add overlays
            'elements': [
                # Watermark logo (entire video)
                {
                    'type': 'image',
                    'url': 'https://example.com/watermark.png',
                    'timing': {'entireVideo': True},
                    'position': {'anchor': 'bottom-right'},
                    'size': {'width': '15%'},
                    'effects': {'opacity': 0.7},
                    'zIndex': 100
                },
                # Opening title (first 5 seconds)
                {
                    'type': 'image',
                    'url': 'https://example.com/title-card.png',
                    'timing': {'startTime': 0, 'endTime': 5},
                    'position': {'anchor': 'center'},
                    'size': {'width': '60%'},
                    'effects': {
                        'fadeIn': {'duration': 0.5},
                        'fadeOut': {'duration': 0.5}
                    },
                    'zIndex': 50
                },
                # Background music
                {
                    'type': 'audio',
                    'url': 'https://example.com/upbeat-music.mp3',
                    'timing': {'entireVideo': True},
                    'effects': {
                        'volume': 0.25,
                        'fadeIn': {'duration': 2},
                        'fadeOut': {'duration': 3}
                    },
                    'loop': True
                }
            ]
        }
    )

    response.raise_for_status()
    data = response.json()
    return data['videoId']

# Usage
video_id = create_branded_compilation()
print(f"Creating branded compilation: {video_id}")

result = wait_for_completion(video_id)
print(f"Compilation ready: {result['videoUrl']}")
type Element = ImageElement | AudioElement;

interface ImageElement {
type: 'image';
url: string;
timing: { entireVideo?: boolean; startTime?: number; endTime?: number };
position?: { anchor: string };
size?: { width: string };
effects?: {
opacity?: number;
fadeIn?: { duration: number };
fadeOut?: { duration: number };
};
zIndex?: number;
}

async function createBrandedCompilation(): Promise<string> {
const elements: Element[] = [
// Watermark logo (entire video)
{
type: 'image',
url: 'https://example.com/watermark.png',
timing: { entireVideo: true },
position: { anchor: 'bottom-right' },
size: { width: '15%' },
effects: { opacity: 0.7 },
zIndex: 100
},
// Opening title (first 5 seconds)
{
type: 'image',
url: 'https://example.com/title-card.png',
timing: { startTime: 0, endTime: 5 },
position: { anchor: 'center' },
size: { width: '60%' },
effects: {
fadeIn: { duration: 0.5 },
fadeOut: { duration: 0.5 }
},
zIndex: 50
},
// Background music
{
type: 'audio',
url: 'https://example.com/upbeat-music.mp3',
timing: { entireVideo: true },
effects: {
volume: 0.25,
fadeIn: { duration: 2 },
fadeOut: { duration: 3 }
},
loop: true
}
];

const response = await fetch('https://api.videocascade.com/v1/videos', {
method: 'POST',
headers: {
'Authorization': 'Bearer vca_your_api_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
combine: true,
files: [
{
url: 'https://example.com/clip1.mp4',
removeSegments: [{ startTime: 0, endTime: 2 }]
},
{
url: 'https://example.com/clip2.mp4',
removeSegments: [{ startTime: 8, endTime: 10 }]
},
{
url: 'https://example.com/clip3.mp4'
}
],
aspectRatio: '16:9',
normalizeAudio: true,
compressionQuality: 95,
elements
}),
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
return data.videoId;
}

// Usage
const videoId = await createBrandedCompilation();
console.log('Creating branded compilation:', videoId);

const result = await waitForCompletion(videoId);
console.log('Compilation ready:', result.videoUrl);

Real-World Use Case: Course Creation Platform

A complete example for creating online course videos from lecture segments.

const express = require('express');
const app = express();

app.use(express.json());

// Database models (example with Prisma)
// Course lectures stored in database with video URLs

async function createCourseVideo(courseId, userId) {
  try {
    // Fetch course lectures from database
    const lectures = await db.lecture.findMany({
      where: { courseId },
      orderBy: { order: 'asc' },
      include: {
        segments: true  // Include segment removal data
      }
    });

    if (lectures.length === 0) {
      throw new Error('No lectures found for course');
    }

    // Build files array with per-lecture segment removal
    const files = lectures.map(lecture => ({
      url: lecture.videoUrl,
      removeSegments: lecture.segments
        .filter(seg => seg.shouldRemove)
        .map(seg => ({
          startTime: seg.startTime,
          endTime: seg.endTime
        })),
      disableAudio: lecture.muteAudio || false
    }));

    // Get course branding
    const course = await db.course.findUnique({
      where: { id: courseId },
      include: { branding: true }
    });

    // Combine lectures with branding
    const response = await fetch('https://api.videocascade.com/v1/videos', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.VIDEO_CASCADE_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        combine: true,
        files,
        aspectRatio: '16:9',
        normalizeAudio: true,
        removeSilence: false,  // Keep pauses for teaching
        compressionQuality: 95,
        elements: [
          // Course watermark
          {
            type: 'image',
            url: course.branding.watermarkUrl,
            timing: { entireVideo: true },
            position: { anchor: 'top-right' },
            size: { width: '10%' },
            effects: { opacity: 0.6 },
            zIndex: 100
          },
          // Intro card
          {
            type: 'image',
            url: course.branding.introCardUrl,
            timing: { startTime: 0, endTime: 5 },
            position: { anchor: 'center' },
            size: { width: '80%' },
            effects: {
              fadeIn: { duration: 0.5 },
              fadeOut: { duration: 0.5 }
            },
            zIndex: 50
          },
          // Background music (low volume)
          course.branding.backgroundMusicUrl && {
            type: 'audio',
            url: course.branding.backgroundMusicUrl,
            timing: { entireVideo: true },
            effects: {
              volume: 0.15,
              fadeIn: { duration: 3 },
              fadeOut: { duration: 5 }
            },
            loop: true
          }
        ].filter(Boolean),  // Remove null elements
        webhookUrl: `${process.env.APP_URL}/api/webhooks/course-video-complete`
      }),
    });

    if (!response.ok) {
      throw new Error(`VideoCascade API error: ${response.status}`);
    }

    const data = await response.json();

    // Save to database
    await db.courseVideo.create({
      data: {
        courseId,
        userId,
        videoId: data.videoId,
        status: 'processing',
        lectureCount: lectures.length,
        startedAt: new Date()
      }
    });

    return {
      success: true,
      videoId: data.videoId,
      lectureCount: lectures.length
    };

  } catch (error) {
    console.error('Error creating course video:', error);
    throw error;
  }
}

// API endpoint
app.post('/api/courses/:courseId/create-video', async (req, res) => {
  try {
    const { courseId } = req.params;
    const userId = req.user.id;

    // Check permissions
    const course = await db.course.findUnique({
      where: { id: courseId }
    });

    if (!course || course.instructorId !== userId) {
      return res.status(403).json({ error: 'Access denied' });
    }

    const result = await createCourseVideo(courseId, userId);

    return res.status(200).json({
      message: 'Course video creation started',
      ...result
    });

  } catch (error) {
    console.error('API error:', error);
    return res.status(500).json({
      error: 'Failed to create course video'
    });
  }
});

// Webhook endpoint
app.post('/api/webhooks/course-video-complete', async (req, res) => {
  try {
    const payload = req.body;

    // Verify signature
    if (!verifyWebhookSignature(payload)) {
      return res.status(401).send('Invalid signature');
    }

    if (payload.event === 'video.completed') {
      // Update database
      const courseVideo = await db.courseVideo.update({
        where: { videoId: payload.videoId },
        data: {
          status: 'completed',
          videoUrl: payload.finalVideoUrl,
          duration: payload.durationMinutes,
          completedAt: new Date()
        },
        include: {
          course: {
            include: {
              instructor: true
            }
          }
        }
      });

      // Notify instructor
      await sendEmail({
        to: courseVideo.course.instructor.email,
        subject: `Your course video is ready: ${courseVideo.course.title}`,
        template: 'course-video-complete',
        data: {
          courseName: courseVideo.course.title,
          videoUrl: courseVideo.videoUrl,
          lectureCount: courseVideo.lectureCount,
          duration: courseVideo.duration
        }
      });

      // Publish course if auto-publish enabled
      if (courseVideo.course.autoPublish) {
        await db.course.update({
          where: { id: courseVideo.courseId },
          data: {
            published: true,
            publishedAt: new Date()
          }
        });
      }
    } else if (payload.event === 'video.failed') {
      await db.courseVideo.update({
        where: { videoId: payload.videoId },
        data: {
          status: 'failed',
          errorMessage: payload.errorMessage
        }
      });

      // Notify instructor of failure
      const courseVideo = await db.courseVideo.findUnique({
        where: { videoId: payload.videoId },
        include: {
          course: { include: { instructor: true } }
        }
      });

      await sendEmail({
        to: courseVideo.course.instructor.email,
        subject: 'Course video creation failed',
        body: `We encountered an error: ${payload.errorMessage}`
      });
    }

    return res.status(200).send('OK');

  } catch (error) {
    console.error('Webhook error:', error);
    return res.status(500).send('Failed');
  }
});

app.listen(3000);
from flask import Flask, request, jsonify
from datetime import datetime
import requests

app = Flask(**name**)

def create_course_video(course_id, user_id):
"""Create combined video from course lectures"""
try: # Fetch course lectures from database
lectures = db.session.query(Lecture).filter_by(
course_id=course_id
).order_by(Lecture.order).all()

        if not lectures:
            raise ValueError('No lectures found for course')

        # Build files array
        files = []
        for lecture in lectures:
            file_config = {
                'url': lecture.video_url,
            }

            # Add segment removal if configured
            segments_to_remove = [
                {'startTime': seg.start_time, 'endTime': seg.end_time}
                for seg in lecture.segments
                if seg.should_remove
            ]
            if segments_to_remove:
                file_config['removeSegments'] = segments_to_remove

            if lecture.mute_audio:
                file_config['disableAudio'] = True

            files.append(file_config)

        # Get course branding
        course = db.session.query(Course).get(course_id)
        branding = course.branding

        # Build elements array
        elements = [
            # Course watermark
            {
                'type': 'image',
                'url': branding.watermark_url,
                'timing': {'entireVideo': True},
                'position': {'anchor': 'top-right'},
                'size': {'width': '10%'},
                'effects': {'opacity': 0.6},
                'zIndex': 100
            },
            # Intro card
            {
                'type': 'image',
                'url': branding.intro_card_url,
                'timing': {'startTime': 0, 'endTime': 5},
                'position': {'anchor': 'center'},
                'size': {'width': '80%'},
                'effects': {
                    'fadeIn': {'duration': 0.5},
                    'fadeOut': {'duration': 0.5}
                },
                'zIndex': 50
            }
        ]

        # Add background music if configured
        if branding.background_music_url:
            elements.append({
                'type': 'audio',
                'url': branding.background_music_url,
                'timing': {'entireVideo': True},
                'effects': {
                    'volume': 0.15,
                    'fadeIn': {'duration': 3},
                    'fadeOut': {'duration': 5}
                },
                'loop': True
            })

        # Submit to VideoCascade
        response = requests.post(
            'https://api.videocascade.com/v1/videos',
            headers={
                'Authorization': f"Bearer {os.getenv('VIDEO_CASCADE_API_KEY')}",
                'Content-Type': 'application/json',
            },
            json={
                'combine': True,
                'files': files,
                'aspectRatio': '16:9',
                'normalizeAudio': True,
                'removeSilence': False,
                'compressionQuality': 95,
                'elements': elements,
                'webhookUrl': f"{os.getenv('APP_URL')}/api/webhooks/course-video-complete"
            }
        )

        response.raise_for_status()
        data = response.json()

        # Save to database
        course_video = CourseVideo(
            course_id=course_id,
            user_id=user_id,
            video_id=data['videoId'],
            status='processing',
            lecture_count=len(lectures),
            started_at=datetime.utcnow()
        )
        db.session.add(course_video)
        db.session.commit()

        return {
            'success': True,
            'videoId': data['videoId'],
            'lectureCount': len(lectures)
        }

    except Exception as error:
        print(f"Error creating course video: {error}")
        raise

@app.route('/api/courses/<course_id>/create-video', methods=['POST'])
def create_video_endpoint(course_id):
try:
user_id = request.user.id

        # Check permissions
        course = db.session.query(Course).get(course_id)
        if not course or course.instructor_id != user_id:
            return jsonify({'error': 'Access denied'}), 403

        result = create_course_video(course_id, user_id)

        return jsonify({
            'message': 'Course video creation started',
            **result
        }), 200

    except Exception as error:
        print(f"API error: {error}")
        return jsonify({'error': 'Failed to create course video'}), 500

@app.route('/api/webhooks/course-video-complete', methods=['POST'])
def webhook_endpoint():
try:
payload = request.get_json()

        # Verify signature
        if not verify_webhook_signature(payload):
            return 'Invalid signature', 401

        if payload['event'] == 'video.completed':
            # Update database
            course_video = db.session.query(CourseVideo).filter_by(
                video_id=payload['videoId']
            ).first()

            course_video.status = 'completed'
            course_video.video_url = payload['finalVideoUrl']
            course_video.duration = payload['durationMinutes']
            course_video.completed_at = datetime.utcnow()
            db.session.commit()

            # Notify instructor
            send_email(
                to=course_video.course.instructor.email,
                subject=f"Your course video is ready: {course_video.course.title}",
                template='course-video-complete',
                data={
                    'courseName': course_video.course.title,
                    'videoUrl': course_video.video_url,
                    'lectureCount': course_video.lecture_count,
                    'duration': course_video.duration
                }
            )

            # Auto-publish if enabled
            if course_video.course.auto_publish:
                course_video.course.published = True
                course_video.course.published_at = datetime.utcnow()
                db.session.commit()

        elif payload['event'] == 'video.failed':
            course_video = db.session.query(CourseVideo).filter_by(
                video_id=payload['videoId']
            ).first()

            course_video.status = 'failed'
            course_video.error_message = payload['errorMessage']
            db.session.commit()

            # Notify instructor
            send_email(
                to=course_video.course.instructor.email,
                subject='Course video creation failed',
                body=f"Error: {payload['errorMessage']}"
            )

        return 'OK', 200

    except Exception as error:
        print(f"Webhook error: {error}")
        return 'Failed', 500

if **name** == '**main**':
app.run(port=3000)

Best Practices

1. Use Matching Codecs

For fastest processing with no quality loss:

// All videos same codec = stream copy (fast, no re-encoding)
files: [
  { url: 'https://example.com/video1.mp4' },  // H.264/AAC
  { url: 'https://example.com/video2.mp4' },  // H.264/AAC
  { url: 'https://example.com/video3.mp4' }   // H.264/AAC
]

2. Remove Segments Per-File

More precise than global segment removal:

// Per-file (recommended for combining)
files: [
  {
    url: 'video1.mp4',
    removeSegments: [{ startTime: 0, endTime: 3 }]
  }
]

// vs Global (applies to final output)
{
  files: [{ url: 'video1.mp4' }],
  removeSegments: [{ startTime: 0, endTime: 3 }]
}

3. Normalize Audio Levels

Essential when combining videos from different sources:

{
  combine: true,
  files: [...],
  normalizeAudio: true  // Consistent volume across all videos
}

4. Set High Compression Quality

Avoid quality degradation from re-encoding:

{
  combine: true,
  files: [...],
  compressionQuality: 100  // Maximum quality (larger file)
}

5. Use Webhooks for Long Processing

Combining multiple videos can take time:

{
  combine: true,
  files: [...],
  webhookUrl: 'https://yourapp.com/webhooks/combine-complete'
}

Common Variations

Social Media Compilation

Create highlight reels for TikTok/Instagram:

{
  combine: true,
  files: [
    { url: 'clip1.mp4', removeSegments: [{ startTime: 5, endTime: 8 }] },
    { url: 'clip2.mp4', removeSegments: [{ startTime: 10, endTime: 15 }] },
    { url: 'clip3.mp4' }
  ],
  aspectRatio: '9:16',  // Vertical format
  resizeMode: 'cover',
  compressionQuality: 90
}

Video Podcast Creation

Combine interview segments with B-roll:

{
  combine: true,
  files: [
    { url: 'intro-broll.mp4', disableAudio: true },
    { url: 'interview-part1.mp4' },
    { url: 'transition-broll.mp4', disableAudio: true },
    { url: 'interview-part2.mp4' },
    { url: 'outro-broll.mp4', disableAudio: true }
  ],
  normalizeAudio: true,
  removeNoise: true
}

Documentary Style

Mix narration with silent footage:

{
  combine: true,
  files: [
    { url: 'narration-intro.mp4' },
    { url: 'footage-segment1.mp4', disableAudio: true },
    { url: 'narration-middle.mp4' },
    { url: 'footage-segment2.mp4', disableAudio: true },
    { url: 'narration-conclusion.mp4' }
  ],
  normalizeAudio: true
}

Next Steps