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
}