> ## Documentation Index
> Fetch the complete documentation index at: https://developer.thehaystack.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Working with Media

> Upload and manage video and audio content

## Overview

This guide covers the complete workflow for uploading media, monitoring processing, adding chapters, and managing media assets.

<Warning>
  **Important for YouTube/Vimeo Users:** Even if you're hosting videos on YouTube or Vimeo for playback, you must still upload the original video file to Haystack. This allows Haystack to provide AI-powered features like transcription, semantic search, and chapter generation. Your embedded player will continue using YouTube/Vimeo, while Haystack provides the search and discovery layer.
</Warning>

## Prerequisites

* An item created (see [Managing Content](/guides/managing-content))
* Media file or URL ready to upload
* API token for authentication

## Upload Process

Uploading media to Haystack is a two-step process:

### Step 1: Create Media Asset

First, create a media asset record and receive an upload URL:

```javascript theme={null}
async function createMediaAsset(itemId, externalConfig = null) {
  const payload = { itemId };

  // Optional: If using YouTube/Vimeo for playback
  if (externalConfig) {
    payload.externalUrl = externalConfig.url;
    payload.externalPlatform = externalConfig.platform; // 'youtube' or 'vimeo'
  }

  const response = await fetch(`${API_URL}/media/create`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(payload)
  });

  const { mediaAsset, uploadUrl } = await response.json();
  return { mediaAsset, uploadUrl };
}

// Example 1: Using Haystack player
const { mediaAsset, uploadUrl } = await createMediaAsset(42);

// Example 2: Using YouTube for playback
const { mediaAsset, uploadUrl } = await createMediaAsset(42, {
  url: 'https://www.youtube.com/watch?v=abc123',
  platform: 'youtube'
});

// Example 3: Using Vimeo for playback
const { mediaAsset, uploadUrl } = await createMediaAsset(42, {
  url: 'https://vimeo.com/123456789',
  platform: 'vimeo'
});
```

<Note>
  **External URLs for playback only:** If you provide a YouTube or Vimeo URL, it will be used for embedded playback in your application. However, you **must still upload the original video file** in Step 2 for AI processing.
</Note>

### Step 2: Upload File

Use the returned `uploadUrl` to upload your video or audio file:

```javascript theme={null}
async function uploadFile(uploadUrl, file) {
  const response = await fetch(uploadUrl, {
    method: 'PUT',
    headers: {
      'Content-Type': file.type || 'video/mp4'
    },
    body: file
  });

  if (!response.ok) {
    throw new Error('Upload failed');
  }

  return response;
}

// Usage
const videoFile = document.getElementById('fileInput').files[0];
await uploadFile(uploadUrl, videoFile);
console.log('Upload complete! Processing will begin automatically.');
```

<Warning>
  **File size limit:** Maximum file size is 5GB. There is no way to upload files larger than 5GB.
</Warning>

**Supported formats:**

* Video: MP4, MOV, AVI, MKV, WebM
* Audio: MP3, WAV, AAC, M4A, FLAC

### Complete Upload Function

Here's a complete helper function that handles both steps:

```javascript theme={null}
async function uploadMediaToHaystack(itemId, file, externalConfig = null) {
  try {
    // Step 1: Create media asset and get upload URL
    console.log('Creating media asset...');
    const { mediaAsset, uploadUrl } = await createMediaAsset(itemId, externalConfig);

    // Step 2: Upload the file
    console.log('Uploading file...');
    await uploadFile(uploadUrl, file);

    console.log('Upload complete! Media asset ID:', mediaAsset.id);
    return mediaAsset;
  } catch (error) {
    console.error('Upload failed:', error);
    throw error;
  }
}

// Usage examples:

// With Haystack player
const videoFile = document.getElementById('fileInput').files[0];
await uploadMediaToHaystack(42, videoFile);

// With YouTube player (must still upload the file!)
await uploadMediaToHaystack(42, videoFile, {
  url: 'https://www.youtube.com/watch?v=abc123',
  platform: 'youtube'
});
```

## Monitoring Processing Status

### Polling Method

Check processing status periodically:

```javascript theme={null}
async function waitForProcessing(mediaAssetId, maxAttempts = 60) {
  for (let i = 0; i < maxAttempts; i++) {
    const response = await fetch(`${API_URL}/media/${mediaAssetId}`, {
      headers: {
        'Authorization': `Bearer ${API_TOKEN}`
      }
    });

    const { mediaAsset } = await response.json();

    console.log(`Status: ${mediaAsset.status}`);

    if (mediaAsset.status === 'ready') {
      console.log('Processing complete!');
      return mediaAsset;
    }

    if (mediaAsset.status === 'error') {
      throw new Error('Processing failed: ' + mediaAsset.errors);
    }

    // Wait 30 seconds before checking again
    await new Promise(resolve => setTimeout(resolve, 30000));
  }

  throw new Error('Processing timeout');
}
```

<Warning>
  Avoid polling too frequently. Check every 30-60 seconds to avoid rate limits.
</Warning>

<Note>
  For the best user experience with bulk uploads, implement a background job that polls the API every 30-60 seconds to check status updates.
</Note>

## Adding Chapters

### Automatic Chapter Detection

Request automatic chapter generation during processing:

```javascript theme={null}
async function processWithChapters(itemId) {
  const response = await fetch(`${API_URL}/items/${itemId}/process`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      generateChapters: true
    })
  });

  const { item } = await response.json();
  return item;
}
```

### Manual Chapters

Create custom chapters with precise control:

```javascript theme={null}
async function createChapter(itemId, mediaAssetId, title, startMs) {
  const response = await fetch(`${API_URL}/media/chapters`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      itemId,
      mediaAssetId,
      title,
      startMs,
      suggested: false,
      accepted: true
    })
  });

  const { chapter } = await response.json();
  return chapter;
}

// Example: Create multiple chapters
const chapters = [
  { title: 'Welcome & Announcements', startMs: 0 },
  { title: 'Worship', startMs: 180000 },
  { title: 'Message: The Power of Prayer', startMs: 1200000 },
  { title: 'Altar Call', startMs: 2400000 },
  { title: 'Closing Prayer', startMs: 2700000 }
];

for (const chapterData of chapters) {
  await createChapter(42, 100, chapterData.title, chapterData.startMs);
}
```

<Tip>
  **Chapter best practices:**

  * Use 5-15 minute segments
  * Start each chapter at a natural transition
  * Use descriptive, clear titles
  * Order chapters chronologically (handled automatically by `sortOrder`)
</Tip>

## Managing Thumbnails

Thumbnail availability depends on which video player you're using.

### For Haystack Custom Player

When using the Haystack custom player, thumbnails are automatically generated via Mux. Access them via the Mux image API:

```javascript theme={null}
function getThumbnailUrl(muxPlaybackId, options = {}) {
  const {
    time = 0,        // Seconds into video
    width = 640,     // Width in pixels
    height = null,   // Auto if not specified
    fitMode = 'smartcrop'
  } = options;

  const params = new URLSearchParams({
    time: time.toString(),
    width: width.toString(),
    fit_mode: fitMode
  });

  if (height) params.set('height', height.toString());

  return `https://image.mux.com/${muxPlaybackId}/thumbnail.jpg?${params}`;
}

// Usage
const url = getThumbnailUrl('abc123def456', {
  time: 120,  // 2 minutes in
  width: 1280
});
```

**Extract thumbnails from key moments:**

```javascript theme={null}
// Get thumbnails for each chapter
chapters.forEach(chapter => {
  const thumbnailUrl = getThumbnailUrl(muxPlaybackId, {
    time: chapter.startTimeSeconds,
    width: 640
  });

  console.log(`${chapter.title}: ${thumbnailUrl}`);
});
```

### For YouTube/Vimeo Players

<Warning>
  Mux-generated thumbnails are **not available** when using YouTube or Vimeo for playback. Use the artwork files you uploaded to your items instead.
</Warning>

When using YouTube or Vimeo players, retrieve thumbnails from your item's artwork:

```javascript theme={null}
async function getItemArtwork(itemId) {
  const response = await fetch(`${API_URL}/items/${itemId}`, {
    headers: {
      'Authorization': `Bearer ${API_TOKEN}`
    }
  });

  const { item } = await response.json();

  // Artwork URLs are directly on the item
  return {
    square: item.squareImgUrl,
    wide: item.wideImgUrl,
    ultraWide: item.ultraWideImgUrl,
    vertical: item.verticalImgUrl
  };
}

// Usage
const artwork = await getItemArtwork(42);
console.log('Square thumbnail:', artwork.square);
console.log('Wide thumbnail:', artwork.wide);
console.log('Ultra-wide thumbnail:', artwork.ultraWide);
console.log('Vertical thumbnail:', artwork.vertical);
```

## Playback Integration

### HLS Streaming

Use the HLS playback URL with any HLS-compatible player:

```javascript theme={null}
async function getPlaybackUrl(mediaAssetId) {
  const response = await fetch(`${API_URL}/media/${mediaAssetId}`, {
    headers: {
      'Authorization': `Bearer ${API_TOKEN}`
    }
  });

  const { mediaAsset } = await response.json();
  return `https://stream.mux.com/${mediaAsset.muxPlaybackId}.m3u8`;
}

// Usage with Video.js
const playbackUrl = await getPlaybackUrl(100);

const player = videojs('my-video', {
  sources: [{
    src: playbackUrl,
    type: 'application/x-mpegURL'
  }]
});
```

### HLS.js Example

```html theme={null}
<video id="video" controls></video>

<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<script>
  const video = document.getElementById('video');
  const playbackUrl = 'https://stream.mux.com/abc123.m3u8';

  if (Hls.isSupported()) {
    const hls = new Hls();
    hls.loadSource(playbackUrl);
    hls.attachMedia(video);
  } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
    // Native HLS support (Safari)
    video.src = playbackUrl;
  }
</script>
```

## Multiple Media Assets

Items can have multiple media assets (e.g., audio and video versions):

```javascript theme={null}
// Upload video version
const videoAsset = await uploadMedia(itemId, videoFile);

// Upload audio-only version
const audioAsset = await uploadMedia(itemId, audioFile);

// List all media for an item
const response = await fetch(
  `${API_URL}/media?itemId=${itemId}`,
  {
    headers: {
      'Authorization': `Bearer ${API_TOKEN}`
    }
  }
);

const { mediaAssets } = await response.json();
console.log(`Item has ${mediaAssets.length} media assets`);
```

## Updating Media

### Replace Media

To replace media, delete the old asset and upload a new one:

```javascript theme={null}
// Delete old media
await fetch(`${API_URL}/media/${oldMediaAssetId}`, {
  method: 'DELETE',
  headers: {
    'Authorization': `Bearer ${API_TOKEN}`
  }
});

// Upload new media
const newAsset = await uploadMedia(itemId, newFile);
```

<Warning>
  Deleting media is permanent and cannot be undone. Ensure you have backups if needed.
</Warning>

## Complete Upload Workflow

Here's a complete example combining all steps:

```javascript theme={null}
async function completeMediaWorkflow(itemId, videoFile, externalConfig = null) {
  try {
    // 1. Upload media
    console.log('Uploading media...');
    const mediaAsset = await uploadMediaToHaystack(itemId, videoFile, externalConfig);

    // 2. Wait for processing
    console.log('Processing media...');
    const processedAsset = await waitForProcessing(mediaAsset.id);

    // 3. Add chapters
    console.log('Adding chapters...');
    const chapters = [
      { title: 'Introduction', startTimeSeconds: 0 },
      { title: 'Main Content', startTimeSeconds: 300 },
      { title: 'Conclusion', startTimeSeconds: 2400 }
    ];
    await createChapters(processedAsset.id, chapters);

    // 4. Publish the item
    console.log('Publishing item...');
    await fetch(`${API_URL}/items/${itemId}/publish`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${API_TOKEN}`
      }
    });

    console.log('✓ Complete! Item is now live.');
    return processedAsset;

  } catch (error) {
    console.error('Workflow failed:', error.message);
    throw error;
  }
}
```

## Troubleshooting

### Common Issues

<AccordionGroup>
  <Accordion title="Upload Fails or Times Out">
    **Causes:**

    * File too large (>5GB for direct upload)
    * Poor internet connection
    * Unsupported codec

    **Solutions:**

    * Use external URL method for large files
    * Compress video before upload
    * Ensure video uses H.264 codec
  </Accordion>

  <Accordion title="Processing Fails">
    **Causes:**

    * Corrupt video file
    * Invalid URL
    * Unsupported format

    **Solutions:**

    * Test playback locally before upload
    * Ensure URL is publicly accessible
    * Convert to MP4 with H.264/AAC
  </Accordion>

  <Accordion title="Transcription Quality Poor">
    **Causes:**

    * Low audio quality
    * Background noise
    * Multiple overlapping speakers

    **Solutions:**

    * Use high-quality audio recording
    * Remove background noise in post-production
    * Ensure clear, single speaker audio
  </Accordion>

  <Accordion title="Slow Processing">
    **Causes:**

    * Very long videos (>2 hours)
    * 4K resolution
    * High platform load

    **Solutions:**

    * Expect 1-2x realtime processing
    * Poll every 30-60 seconds to check status
    * Consider splitting very large files into segments
  </Accordion>
</AccordionGroup>

## Best Practices

<CardGroup cols={2}>
  <Card title="Optimize Before Upload" icon="gauge-high">
    Compress videos to reasonable bitrates (5-10 Mbps for 1080p) to save storage and bandwidth
  </Card>

  <Card title="Poll Responsibly" icon="clock">
    Check processing status every 30-60 seconds to avoid rate limits
  </Card>

  <Card title="Add Chapters" icon="list">
    Break long content into chapters for better user experience and engagement
  </Card>

  <Card title="Test Playback" icon="circle-play">
    Verify video plays correctly before uploading to catch issues early
  </Card>
</CardGroup>

## Next Steps

<Card title="Media Management Concepts" icon="book" href="/concepts/media-management">
  Deep dive into media processing pipeline
</Card>
