JavaScript SDK Guide

The MediaConvert.io JavaScript SDK works in both Node.js and browser environments. This guide covers installation, configuration, and common usage patterns for modern JavaScript applications.

Installation

Node.js

bash
npm install @mediaconvert/sdk

# With TypeScript support
npm install @mediaconvert/sdk @types/mediaconvert

# With async/await support
npm install @mediaconvert/sdk axios

Browser (CDN)

html
<!-- Latest version -->
<script src="https://cdn.jsdelivr.net/npm/@mediaconvert/sdk@latest/dist/mediaconvert.min.js"></script>

<!-- Specific version -->
<script src="https://cdn.jsdelivr.net/npm/@mediaconvert/sdk@1.0.0/dist/mediaconvert.min.js"></script>

ES Modules

javascript
// ES6 imports
import MediaConvert from '@mediaconvert/sdk';

// CommonJS
const MediaConvert = require('@mediaconvert/sdk');

// Browser ES modules
import MediaConvert from 'https://cdn.skypack.dev/@mediaconvert/sdk';

Quick Start

Basic Configuration

javascript
import MediaConvert, { ConversionJob, Account } from '@mediaconvert/sdk';

// Configure the client
MediaConvert.configure({
  apiToken: process.env.MEDIACONVERT_API_TOKEN,
  apiBaseUrl: 'https://mediaconvert.io/api/v1',
  timeout: 30000,
  retries: 3
});

// Alternative: Create client instance
const client = new MediaConvert({
  apiToken: process.env.MEDIACONVERT_API_TOKEN
});

Your First Conversion

javascript
async function convertVideo() {
  try {
    // Create a conversion job
    const job = await ConversionJob.create({
      jobType: 'video',
      inputUrl: 'https://bucket.s3.amazonaws.com/input.mp4?...',
      outputUrl: 'https://bucket.s3.amazonaws.com/output.webm?...',
      inputFormat: 'mp4',
      outputFormat: 'webm',
      inputSizeBytes: 52428800,
      webhookUrl: 'https://your-app.com/webhooks/mediaconvert'
    });

    console.log(`Job created: ${job.id}`);
    console.log(`Status: ${job.status}`);
    // New micro-precision API (recommended)
    console.log(`Estimated cost: €${job.estimatedCostMicros / 1_000_000}`);
    // Or using backward-compatible cents field
    console.log(`Estimated cost: €${job.estimatedCostCents / 100}`);

    return job;
  } catch (error) {
    console.error('Conversion failed:', error.message);
    throw error;
  }
}

Core Classes

MediaConvert.ConversionJob

The main class for managing conversion jobs:

javascript
// Create a job
const job = await ConversionJob.create({
  jobType: 'video',
  inputUrl: inputPresignedUrl,
  outputUrl: outputPresignedUrl,
  inputFormat: 'mp4',
  outputFormat: 'webm',
  inputSizeBytes: fileSizeBytes
});

// Check job status
await job.reload();
console.log(`Status: ${job.status}`);
console.log(`Progress: ${job.progressPercentage}%`);

// Wait for completion with progress callback
await job.waitUntilComplete((currentJob) => {
  console.log(`Progress: ${currentJob.progressPercentage}%`);
});

// Access results
console.log(`Processing time: ${job.processingTimeSeconds}s`);
console.log(`Output size: ${job.outputSizeBytes} bytes`);
// New micro-precision API (recommended)
console.log(`Total cost: €${job.costMicros / 1_000_000}`);
// Or using backward-compatible cents field
console.log(`Total cost: €${job.costCents / 100}`);

MediaConvert.Account

Manage your account information:

javascript
// Get account details
const account = await Account.current();
console.log(`Email: ${account.email}`);
console.log(`Tier: ${account.tier}`);
console.log(`Credits: €${account.creditsBalanceCents / 100}`);

// Check usage
const usage = await account.usageSummary();
console.log(`Jobs this month: ${usage.jobsCount}`);
console.log(`Data processed: ${usage.totalMbProcessed} MB`);
console.log(`Total spent: €${usage.totalCostMicros / 1_000_000}`);

Node.js Integration

Express.js Application

javascript
const express = require('express');
const MediaConvert = require('@mediaconvert/sdk');
const app = express();

// Configure MediaConvert
MediaConvert.configure({
  apiToken: process.env.MEDIACONVERT_API_TOKEN,
  webhookSecret: process.env.MEDIACONVERT_WEBHOOK_SECRET
});

app.use(express.json());

// Start conversion endpoint
app.post('/api/convert', async (req, res) => {
  try {
    const { inputUrl, outputUrl, jobType, inputFormat, outputFormat } = req.body;

    const job = await MediaConvert.ConversionJob.create({
      jobType,
      inputUrl,
      outputUrl,
      inputFormat,
      outputFormat,
      webhookUrl: `${req.protocol}://${req.get('host')}/webhooks/mediaconvert`
    });

    res.status(201).json({
      jobId: job.id,
      status: job.status,
      estimatedCostMicros: job.estimatedCostMicros
    });
  } catch (error) {
    console.error('Conversion error:', error);
    res.status(400).json({ error: error.message });
  }
});

// Webhook endpoint
app.post('/webhooks/mediaconvert', MediaConvert.webhookHandler({
  onCompleted: async (jobData) => {
    console.log(`Job ${jobData.jobId} completed successfully`);
    // Process completed job
    await handleCompletedJob(jobData);
  },
  onFailed: async (jobData) => {
    console.log(`Job ${jobData.jobId} failed: ${jobData.errorMessage}`);
    // Handle failed job
    await handleFailedJob(jobData);
  },
  onProgress: async (jobData) => {
    console.log(`Job ${jobData.jobId} progress: ${jobData.progressPercentage}%`);
    // Update progress in database
    await updateJobProgress(jobData.jobId, jobData.progressPercentage);
  }
}));

async function handleCompletedJob(jobData) {
  // Update database, notify user, etc.
  await updateJobStatus(jobData.jobId, 'completed');
  await notifyUser(jobData.userId, 'Conversion completed!');
}

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

Background Job Processing (Bull Queue)

javascript
const Bull = require('bull');
const MediaConvert = require('@mediaconvert/sdk');

// Create conversion queue
const conversionQueue = new Bull('media conversion', {
  redis: {
    port: 6379,
    host: '127.0.0.1'
  }
});

// Process conversion jobs
conversionQueue.process('convert', async (job) => {
  const { inputUrl, outputUrl, options, userId } = job.data;

  try {
    const conversionJob = await MediaConvert.ConversionJob.create({
      inputUrl,
      outputUrl,
      webhookUrl: `https://yourapp.com/webhooks/conversion/${userId}`,
      ...options
    });

    // Store job reference
    await storeJobReference(userId, conversionJob.id, {
      status: conversionJob.status,
      inputUrl,
      outputUrl
    });

    return { 
      jobId: conversionJob.id, 
      status: conversionJob.status 
    };

  } catch (error) {
    if (error instanceof MediaConvert.InsufficientCreditsError) {
      // Don't retry - notify user to add credits
      await notifyInsufficientCredits(userId, error.requiredCredits, error.availableCredits);
      throw new Error(`Insufficient credits: need €${error.requiredCredits / 1_000_000} but only have €${error.availableCredits / 1_000_000}`);
    }

    if (error instanceof MediaConvert.RateLimitError) {
      // Retry after delay
      throw new Error(`Rate limited. Retry after ${error.retryAfter}s`);
    }

    throw error;
  }
});

// Add retry logic for rate limits
conversionQueue.on('failed', (job, err) => {
  if (err.message.includes('Rate limited')) {
    const retryAfter = extractRetryAfter(err.message);
    setTimeout(() => {
      job.retry();
    }, retryAfter * 1000);
  }
});

// Usage: Add job to queue
async function enqueueConversion(userId, inputUrl, outputUrl, options = {}) {
  await conversionQueue.add('convert', {
    userId,
    inputUrl,
    outputUrl,
    options
  }, {
    attempts: 3,
    backoff: {
      type: 'exponential',
      delay: 2000
    }
  });
}

TypeScript Integration

typescript
import MediaConvert, { 
  ConversionJob, 
  Account, 
  MediaConvertError,
  InsufficientCreditsError,
  ValidationError,
  ConversionError,
  RateLimitError 
} from '@mediaconvert/sdk';

// Type definitions
interface ConversionOptions {
  jobType: 'video' | 'image' | 'audio' | 'document';
  inputUrl: string;
  outputUrl: string;
  inputFormat: string;
  outputFormat: string;
  inputSizeBytes?: number;
  webhookUrl?: string;
}

interface ConversionResult {
  success: boolean;
  jobId?: string;
  costMicros?: number;
  processingTime?: number;
  error?: string;
}

class MediaConversionService {
  constructor(private readonly apiToken: string) {
    MediaConvert.configure({
      apiToken: this.apiToken
    });
  }

  async convertWithRetry(
    options: ConversionOptions, 
    maxRetries: number = 3
  ): Promise<ConversionResult> {
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        const job = await ConversionJob.create(options);

        await job.waitUntilComplete((currentJob) => {
          console.log(`Attempt ${attempt + 1}: Progress ${currentJob.progressPercentage}%`);
        });

        return {
          success: true,
          jobId: job.id,
          costMicros: job.costMicros,
          processingTime: job.processingTimeSeconds
        };

      } catch (error) {
        if (error instanceof InsufficientCreditsError) {
          return {
            success: false,
            error: `Need €${error.requiredCredits / 1_000_000} but only have €${error.availableCredits / 1_000_000}`
          };
        }

        if (error instanceof ValidationError) {
          return {
            success: false,
            error: `Validation failed: ${error.errors.join(', ')}`
          };
        }

        if (error instanceof RateLimitError && attempt < maxRetries) {
          console.log(`Rate limited. Waiting ${error.retryAfter}s...`);
          await new Promise(resolve => setTimeout(resolve, error.retryAfter * 1000));
          continue;
        }

        if (attempt === maxRetries) {
          return {
            success: false,
            error: error.message
          };
        }

        // Exponential backoff for other errors
        const delay = Math.pow(2, attempt) * 1000;
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }

    return {
      success: false,
      error: 'Max retries exceeded'
    };
  }
}

// Usage
const conversionService = new MediaConversionService(process.env.MEDIACONVERT_API_TOKEN!);

const result = await conversionService.convertWithRetry({
  jobType: 'video',
  inputUrl: 'https://example.com/input.mp4',
  outputUrl: 'https://example.com/output.webm',
  inputFormat: 'mp4',
  outputFormat: 'webm'
});

if (result.success) {
  console.log(`Conversion completed! Cost: €${result.costMicros! / 1_000_000}`);
} else {
  console.error(`Conversion failed: ${result.error}`);
}

Browser Integration

Modern Frontend Framework (React)

javascript
import React, { useState, useEffect } from 'react';
import MediaConvert from '@mediaconvert/sdk';

// Configure for browser environment
MediaConvert.configure({
  apiToken: process.env.REACT_APP_MEDIACONVERT_API_TOKEN,
  // Note: In browser, consider using a backend proxy for API calls
  // to avoid exposing API tokens
});

function VideoConverter() {
  const [file, setFile] = useState(null);
  const [job, setJob] = useState(null);
  const [progress, setProgress] = useState(0);
  const [status, setStatus] = useState('idle');

  const handleFileSelect = (event) => {
    setFile(event.target.files[0]);
  };

  const generatePresignedUrls = async (filename) => {
    // Call your backend to generate presigned URLs
    const response = await fetch('/api/presigned-urls', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ filename })
    });
    return response.json();
  };

  const uploadToS3 = async (file, presignedUrl) => {
    const formData = new FormData();
    formData.append('file', file);

    await fetch(presignedUrl, {
      method: 'PUT',
      body: file
    });
  };

  const startConversion = async () => {
    if (!file) return;

    try {
      setStatus('uploading');

      // Generate presigned URLs via backend
      const { inputUrl, outputUrl } = await generatePresignedUrls(file.name);

      // Upload file to S3
      await uploadToS3(file, inputUrl);

      setStatus('converting');

      // Start conversion
      const conversionJob = await MediaConvert.ConversionJob.create({
        jobType: 'video',
        inputUrl,
        outputUrl,
        inputFormat: file.name.split('.').pop(),
        outputFormat: 'webm',
        inputSizeBytes: file.size
      });

      setJob(conversionJob);

      // Monitor progress
      const progressInterval = setInterval(async () => {
        await conversionJob.reload();
        setProgress(conversionJob.progressPercentage || 0);

        if (conversionJob.status === 'completed') {
          setStatus('completed');
          clearInterval(progressInterval);
        } else if (conversionJob.status === 'failed') {
          setStatus('failed');
          clearInterval(progressInterval);
        }
      }, 2000);

    } catch (error) {
      console.error('Conversion error:', error);
      setStatus('error');
    }
  };

  return (
    <div className="video-converter">
      <h2>Video Converter</h2>

      <div className="upload-section">
        <input 
          type="file" 
          accept="video/*" 
          onChange={handleFileSelect}
          disabled={status !== 'idle'}
        />
        <button 
          onClick={startConversion} 
          disabled={!file || status !== 'idle'}
        >
          Convert to WebM
        </button>
      </div>

      {status !== 'idle' && (
        <div className="status-section">
          <div className="status">Status: {status}</div>

          {status === 'converting' && (
            <div className="progress">
              <div className="progress-bar">
                <div 
                  className="progress-fill" 
                  style={{ width: `${progress}%` }}
                />
              </div>
              <span>{progress}%</span>
            </div>
          )}

          {status === 'completed' && job && (
            <div className="results">
              <p>Conversion completed!</p>
              <p>Cost: {job.costMicros / 1_000_000}</p>
              <p>Processing time: {job.processingTimeSeconds}s</p>
            </div>
          )}

          {status === 'failed' && (
            <div className="error">
              Conversion failed. Please try again.
            </div>
          )}
        </div>
      )}
    </div>
  );
}

export default VideoConverter;

Vue.js Integration

javascript
<template>
  <div class="media-converter">
    <h2>Media Converter</h2>

    <div class="file-input">
      <input 
        type="file" 
        @change="handleFileSelect"
        :disabled="isProcessing"
      />
      <button 
        @click="startConversion" 
        :disabled="!selectedFile || isProcessing"
      >
        {{ isProcessing ? 'Converting...' : 'Convert' }}
      </button>
    </div>

    <div v-if="job" class="job-status">
      <p>Status: {{ job.status }}</p>
      <div v-if="job.progressPercentage" class="progress">
        Progress: {{ job.progressPercentage }}%
      </div>
      <div v-if="job.status === 'completed'" class="results">
        <p> Conversion completed!</p>
        <p>Cost: {{ (job.costMicros / 1_000_000).toFixed(6) }}</p>
      </div>
    </div>

    <div v-if="error" class="error">
      {{ error }}
    </div>
  </div>
</template>

<script>
import MediaConvert from '@mediaconvert/sdk';

export default {
  name: 'MediaConverter',
  data() {
    return {
      selectedFile: null,
      job: null,
      error: null,
      isProcessing: false,
      progressInterval: null
    };
  },
  methods: {
    handleFileSelect(event) {
      this.selectedFile = event.target.files[0];
      this.error = null;
    },

    async startConversion() {
      if (!this.selectedFile) return;

      this.isProcessing = true;
      this.error = null;

      try {
        // Generate presigned URLs via your backend API
        const urls = await this.$http.post('/api/presigned-urls', {
          filename: this.selectedFile.name
        });

        // Upload file to S3
        await fetch(urls.data.inputUrl, {
          method: 'PUT',
          body: this.selectedFile
        });

        // Create conversion job
        this.job = await MediaConvert.ConversionJob.create({
          jobType: 'video',
          inputUrl: urls.data.inputUrl,
          outputUrl: urls.data.outputUrl,
          inputFormat: this.selectedFile.name.split('.').pop(),
          outputFormat: 'webm',
          inputSizeBytes: this.selectedFile.size
        });

        // Monitor progress
        this.monitorProgress();

      } catch (error) {
        this.error = error.message;
        this.isProcessing = false;
      }
    },

    async monitorProgress() {
      this.progressInterval = setInterval(async () => {
        try {
          await this.job.reload();

          if (this.job.status === 'completed' || this.job.status === 'failed') {
            clearInterval(this.progressInterval);
            this.isProcessing = false;
          }
        } catch (error) {
          console.error('Progress monitoring error:', error);
        }
      }, 2000);
    }
  },

  beforeDestroy() {
    if (this.progressInterval) {
      clearInterval(this.progressInterval);
    }
  }
};
</script>

<style scoped>
.progress {
  margin: 10px 0;
}

.error {
  color: red;
  margin-top: 10px;
}

.results {
  color: green;
  margin-top: 10px;
}
</style>

Error Handling Patterns

Comprehensive Error Handling

javascript
import { 
  AuthenticationError,
  InsufficientCreditsError,
  ValidationError,
  ConversionError,
  RateLimitError,
  APIError,
  NetworkError,
  TimeoutError
} from '@mediaconvert/sdk';

async function robustConversion(inputUrl, outputUrl, options = {}) {
  const maxRetries = 3;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const job = await MediaConvert.ConversionJob.create({
        inputUrl,
        outputUrl,
        ...options
      });

      await job.waitUntilComplete({
        timeout: 3600000, // 1 hour
        progressCallback: (job) => {
          console.log(`Progress: ${job.progressPercentage}%`);
        }
      });

      return {
        success: true,
        jobId: job.id,
        costMicros: job.costMicros,
        processingTime: job.processingTimeSeconds
      };

    } catch (error) {
      console.error(`Attempt ${attempt + 1} failed:`, error.message);

      // Handle different error types
      if (error instanceof AuthenticationError) {
        return {
          success: false,
          error: 'Authentication failed - check your API token',
          retryable: false
        };
      }

      if (error instanceof InsufficientCreditsError) {
        return {
          success: false,
          error: `Need €${error.requiredCredits / 1_000_000} but only have €${error.availableCredits / 1_000_000}`,
          retryable: false,
          requiredCreditsMicros: error.requiredCredits,
          availableCreditsMicros: error.availableCredits
        };
      }

      if (error instanceof ValidationError) {
        return {
          success: false,
          error: `Validation failed: ${error.errors.join(', ')}`,
          retryable: false
        };
      }

      if (error instanceof ConversionError) {
        return {
          success: false,
          error: `Conversion failed: ${error.message}`,
          retryable: false
        };
      }

      if (error instanceof RateLimitError) {
        if (attempt < maxRetries) {
          console.log(`Rate limited. Waiting ${error.retryAfter} seconds...`);
          await new Promise(resolve => setTimeout(resolve, error.retryAfter * 1000));
          continue;
        } else {
          return {
            success: false,
            error: `Rate limited after ${maxRetries} attempts`,
            retryAfter: error.retryAfter
          };
        }
      }

      if (error instanceof NetworkError || error instanceof TimeoutError) {
        if (attempt < maxRetries) {
          const waitTime = Math.pow(2, attempt) * 1000;
          console.log(`Network error. Retrying in ${waitTime}ms...`);
          await new Promise(resolve => setTimeout(resolve, waitTime));
          continue;
        } else {
          return {
            success: false,
            error: `Network error after ${maxRetries} attempts: ${error.message}`,
            retryable: true
          };
        }
      }

      if (error instanceof APIError) {
        if (attempt < maxRetries && error.statusCode >= 500) {
          const waitTime = Math.pow(2, attempt) * 1000;
          console.log(`Server error. Retrying in ${waitTime}ms...`);
          await new Promise(resolve => setTimeout(resolve, waitTime));
          continue;
        } else {
          return {
            success: false,
            error: `API error: ${error.message} (Status: ${error.statusCode})`,
            retryable: error.statusCode >= 500
          };
        }
      }

      // Unknown error
      if (attempt === maxRetries) {
        return {
          success: false,
          error: `Unknown error: ${error.message}`,
          retryable: true
        };
      }

      await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
    }
  }
}

// Usage with comprehensive error handling
const result = await robustConversion(inputUrl, outputUrl, {
  jobType: 'video',
  inputFormat: 'mp4',
  outputFormat: 'webm'
});

if (result.success) {
  console.log(`✅ Conversion completed! Cost: €${result.costMicros / 1_000_000}`);
} else {
  console.error(`❌ Conversion failed: ${result.error}`);

  if (result.retryable) {
    console.log('💡 This error might be temporary. You can try again.');
  }

  if (result.requiredCreditsMicros) {
    console.log(`💳 Add €${(result.requiredCreditsMicros - result.availableCreditsMicros) / 1_000_000} to your account`);
  }
}

Batch Processing

Concurrent Processing with Promise Control

javascript
class BatchProcessor {
  constructor(maxConcurrency = 5) {
    this.maxConcurrency = maxConcurrency;
    this.jobs = [];
  }

  addJob(inputUrl, outputUrl, options = {}) {
    const jobPromise = MediaConvert.ConversionJob.create({
      inputUrl,
      outputUrl,
      ...options
    });

    this.jobs.push(jobPromise);
    return jobPromise;
  }

  async processAll() {
    const results = {
      completed: [],
      failed: []
    };

    // Process in batches to control concurrency
    for (let i = 0; i < this.jobs.length; i += this.maxConcurrency) {
      const batch = this.jobs.slice(i, i + this.maxConcurrency);

      const batchResults = await Promise.allSettled(
        batch.map(async (jobPromise) => {
          const job = await jobPromise;
          await job.waitUntilComplete();
          return job;
        })
      );

      batchResults.forEach((result) => {
        if (result.status === 'fulfilled') {
          results.completed.push(result.value);
        } else {
          results.failed.push({
            error: result.reason,
            job: result.reason.job || null
          });
        }
      });

      console.log(`Processed batch ${Math.ceil((i + this.maxConcurrency) / this.maxConcurrency)}`);
    }

    return results;
  }

  async processAllWithProgress(progressCallback) {
    const totalJobs = this.jobs.length;
    let completedJobs = 0;

    const jobsWithProgress = await Promise.all(this.jobs);

    const results = await Promise.allSettled(
      jobsWithProgress.map(async (job) => {
        try {
          await job.waitUntilComplete((currentJob) => {
            // Individual job progress
            progressCallback({
              jobId: currentJob.id,
              progress: currentJob.progressPercentage,
              overallProgress: (completedJobs / totalJobs) * 100
            });
          });

          completedJobs++;
          return job;
        } catch (error) {
          completedJobs++;
          throw error;
        }
      })
    );

    return {
      completed: results.filter(r => r.status === 'fulfilled').map(r => r.value),
      failed: results.filter(r => r.status === 'rejected').map(r => ({
        error: r.reason,
        job: r.reason.job
      }))
    };
  }
}

// Usage
const processor = new BatchProcessor(3); // Max 3 concurrent jobs

// Add multiple jobs
processor.addJob('s3://bucket/input1.mp4', 's3://bucket/output1.webm', { outputFormat: 'webm' });
processor.addJob('s3://bucket/input2.mp4', 's3://bucket/output2.mp4', { outputFormat: 'mp4' });
processor.addJob('s3://bucket/input3.jpg', 's3://bucket/output3.webp', { outputFormat: 'webp' });

// Process with progress tracking
const results = await processor.processAllWithProgress((progress) => {
  console.log(`Job ${progress.jobId}: ${progress.progress}% (Overall: ${progress.overallProgress}%)`);
});

console.log(`${results.completed.length} completed, ${results.failed.length} failed`);

// Calculate total cost
const totalCostMicros = results.completed.reduce((sum, job) => sum + (job.costMicros || 0), 0);
console.log(`Total cost: €${totalCostMicros / 1_000_000}`);

AWS S3 Integration

S3 Helper Class

javascript
import AWS from 'aws-sdk';

class S3Helper {
  constructor(bucketName, region = 'us-east-1') {
    this.bucketName = bucketName;
    this.s3 = new AWS.S3({ region });
  }

  async generatePresignedUrl(key, method = 'getObject', expiresIn = 3600) {
    const params = {
      Bucket: this.bucketName,
      Key: key,
      Expires: expiresIn
    };

    try {
      if (method === 'putObject') {
        return await this.s3.getSignedUrlPromise('putObject', params);
      } else {
        return await this.s3.getSignedUrlPromise('getObject', params);
      }
    } catch (error) {
      throw new Error(`Failed to generate presigned URL: ${error.message}`);
    }
  }

  async getObjectSize(key) {
    try {
      const response = await this.s3.headObject({
        Bucket: this.bucketName,
        Key: key
      }).promise();

      return response.ContentLength;
    } catch (error) {
      throw new Error(`Failed to get object size: ${error.message}`);
    }
  }

  async uploadFile(file, key) {
    try {
      await this.s3.upload({
        Bucket: this.bucketName,
        Key: key,
        Body: file
      }).promise();

      return await this.generatePresignedUrl(key);
    } catch (error) {
      throw new Error(`Failed to upload file: ${error.message}`);
    }
  }
}

// Complete S3-to-S3 conversion workflow
async function convertFromS3(bucketName, inputKey, outputKey, options = {}) {
  const s3Helper = new S3Helper(bucketName);

  // Get input file size
  const inputSizeBytes = await s3Helper.getObjectSize(inputKey);

  // Generate presigned URLs
  const inputUrl = await s3Helper.generatePresignedUrl(inputKey, 'getObject');
  const outputUrl = await s3Helper.generatePresignedUrl(outputKey, 'putObject');

  // Create conversion job
  const job = await MediaConvert.ConversionJob.create({
    inputUrl,
    outputUrl,
    inputSizeBytes,
    ...options
  });

  return job;
}

// Usage
const job = await convertFromS3(
  'my-media-bucket',
  'videos/input.mp4',
  'videos/output.webm',
  {
    jobType: 'video',
    inputFormat: 'mp4',
    outputFormat: 'webm'
  }
);

console.log(`Conversion started: ${job.id}`);
await job.waitUntilComplete();
console.log(`Conversion completed! Cost: €${job.costMicros / 1_000_000}`);

Testing

Jest Testing Framework

javascript
// __tests__/mediaconvert.test.js
import MediaConvert, { ConversionJob } from '@mediaconvert/sdk';

// Mock the HTTP client
jest.mock('@mediaconvert/sdk');

describe('MediaConvert Integration', () => {
  beforeEach(() => {
    // Reset mocks
    jest.clearAllMocks();

    // Configure MediaConvert
    MediaConvert.configure({
      apiToken: 'test_token'
    });
  });

  test('should create conversion job successfully', async () => {
    const mockJob = {
      id: 'job_test123',
      status: 'pending',
      estimatedCostMicros: 150000 // €0.15
    };

    ConversionJob.create.mockResolvedValue(mockJob);

    const job = await ConversionJob.create({
      jobType: 'video',
      inputUrl: 'https://example.com/input.mp4',
      outputUrl: 'https://example.com/output.webm'
    });

    expect(job.id).toBe('job_test123');
    expect(job.status).toBe('pending');
    expect(job.estimatedCostMicros).toBe(150000);
    expect(ConversionJob.create).toHaveBeenCalledWith({
      jobType: 'video',
      inputUrl: 'https://example.com/input.mp4',
      outputUrl: 'https://example.com/output.webm'
    });
  });

  test('should handle insufficient credits error', async () => {
    const error = new MediaConvert.InsufficientCreditsError(
      'Insufficient credits',
      200000, // required
      50000   // available
    );

    ConversionJob.create.mockRejectedValue(error);

    await expect(ConversionJob.create({
      jobType: 'video',
      inputUrl: 'https://example.com/input.mp4',
      outputUrl: 'https://example.com/output.webv'
    })).rejects.toThrow(MediaConvert.InsufficientCreditsError);
  });

  test('should retry on rate limit error', async () => {
    const rateLimitError = new MediaConvert.RateLimitError('Rate limited', 30);
    const successJob = { id: 'job_success', status: 'pending' };

    ConversionJob.create
      .mockRejectedValueOnce(rateLimitError)
      .mockResolvedValue(successJob);

    // Test retry logic would go here
    // This depends on your specific retry implementation
  });
});

// Integration test helpers
export class MediaConvertTestHelper {
  static mockSuccessfulJob() {
    return {
      id: 'job_test123',
      status: 'completed',
      progressPercentage: 100,
      costMicros: 150000,
      processingTimeSeconds: 45,
      reload: jest.fn().mockResolvedValue(undefined),
      waitUntilComplete: jest.fn().mockResolvedValue(undefined)
    };
  }

  static mockFailedJob() {
    return {
      id: 'job_test456',
      status: 'failed',
      errorMessage: 'Unsupported input format',
      reload: jest.fn().mockResolvedValue(undefined)
    };
  }

  static setupSuccessfulConversion() {
    ConversionJob.create.mockResolvedValue(this.mockSuccessfulJob());
  }

  static setupFailedConversion() {
    ConversionJob.create.mockResolvedValue(this.mockFailedJob());
  }
}

Performance Optimization

Connection Pooling and Caching

javascript
import MediaConvert from '@mediaconvert/sdk';
import NodeCache from 'node-cache';

// Create cache for job status with 30 second TTL
const jobCache = new NodeCache({ stdTTL: 30 });

// Configure with connection pooling
MediaConvert.configure({
  apiToken: process.env.MEDIACONVERT_API_TOKEN,
  // Enable connection pooling
  keepAlive: true,
  maxSockets: 10,
  // Custom HTTP agent for connection reuse
  httpAgent: new require('http').Agent({
    keepAlive: true,
    maxSockets: 10
  }),
  httpsAgent: new require('https').Agent({
    keepAlive: true,
    maxSockets: 10
  })
});

// Cached job status checking
async function getCachedJobStatus(jobId) {
  const cacheKey = `job_status_${jobId}`;
  let status = jobCache.get(cacheKey);

  if (!status) {
    const job = await ConversionJob.get(jobId);
    status = {
      id: job.id,
      status: job.status,
      progressPercentage: job.progressPercentage
    };
    jobCache.set(cacheKey, status);
  }

  return status;
}

// Efficient batch status checking
async function getBatchJobStatuses(jobIds) {
  const uncachedJobIds = [];
  const results = {};

  // Check cache first
  jobIds.forEach(jobId => {
    const cached = jobCache.get(`job_status_${jobId}`);
    if (cached) {
      results[jobId] = cached;
    } else {
      uncachedJobIds.push(jobId);
    }
  });

  // Fetch uncached jobs in parallel
  if (uncachedJobIds.length > 0) {
    const jobs = await Promise.all(
      uncachedJobIds.map(jobId => ConversionJob.get(jobId))
    );

    jobs.forEach(job => {
      const status = {
        id: job.id,
        status: job.status,
        progressPercentage: job.progressPercentage
      };
      results[job.id] = status;
      jobCache.set(`job_status_${job.id}`, status);
    });
  }

  return results;
}

Next Steps

Support