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

# Basic Usage

> Upload encrypted video with a single upload() call — handles key fetch, encryption, presign, and manifest rewrite. Includes playback verification.

## The `upload()` function

`upload()` takes three arguments: segments, a manifest string, and options.

```typescript theme={null}
import { upload } from "@blindcast/uploader"

// 1. Read segments (e.g., from a previous pipeline step or file input)
const segments = [
  { index: 0, data: new Uint8Array(seg0Bytes), key: "seg-0.ts" },
  { index: 1, data: new Uint8Array(seg1Bytes), key: "seg-1.ts" },
  { index: 2, data: new Uint8Array(seg2Bytes), key: "seg-2.ts" },
]

const manifest = await fetch("/api/manifest/my-video").then((r) => r.text())

// 2. Upload — encrypts, presigns, uploads, and rewrites manifest
const result = await upload(segments, manifest, {
  contentId: "my-video-001",
  keyServerUrl: "https://keys.example.com/keys",
  presignUrl: "https://api.example.com/presign",
  auth: async () => getAccessToken(),
})

if (!result.ok) {
  console.error(`Upload failed: [${result.error.code}] ${result.error.message}`)
  return
}

// 3. Use the result

const { manifestUrl, segmentUrls } = result.value
console.log(`Uploaded ${segmentUrls.length} segments`)
console.log(`Manifest: ${manifestUrl}`)
```

<Note>
  `keyServerUrl` must include the `/keys` path — e.g., `https://your-worker.workers.dev/keys`, not just the base URL. The uploader appends `/:contentId` when fetching the content key.
</Note>

## What happens internally

```mermaid theme={null}
sequenceDiagram
  participant Browser as Browser (uploader)
  participant KS as Key Server
  participant PS as Presign Server
  participant S3 as S3

  Browser->>KS: GET /keys/my-video-001 (with Bearer token)
  KS-->>Browser: content key (16 bytes)
  Browser->>Browser: Encrypt each segment with content key

  loop For each segment
    Browser->>PS: POST /presign { key, contentType }
    PS-->>Browser: presigned PUT URL
    Browser->>S3: PUT encrypted bytes to presigned URL
  end

  Browser->>Browser: Rewrite manifest with EXT-X-KEY tags
  Browser->>PS: POST /presign { key: manifest }
  PS-->>Browser: presigned PUT URL
  Browser->>S3: PUT rewritten manifest
```

## Preparing segments

The uploader expects **pre-segmented HLS content** — `.ts` segment files and a `.m3u8` manifest. It does not convert raw video to HLS.

**Server-side (ffmpeg):**

```bash theme={null}
ffmpeg -i input.mp4 -codec: copy -hls_time 6 -hls_list_size 0 -f hls segments/manifest.m3u8
```

**Reading files from a browser `<input>`:**

```typescript theme={null}
const fileInput = document.querySelector<HTMLInputElement>("#file-input")!
const files = Array.from(fileInput.files!)

// Separate manifest from segments
const manifestFile = files.find((f) => f.name.endsWith(".m3u8"))!
const segmentFiles = files
  .filter((f) => f.name.endsWith(".ts"))
  .sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }))

// Read manifest as text
const manifest = await manifestFile.text()

// Read segments as SegmentInput[]
const segments = await Promise.all(
  segmentFiles.map(async (file, index) => ({
    index,
    data: new Uint8Array(await file.arrayBuffer()),
    key: file.name,
  })),
)
```

<Note>
  Pass the **original, unmodified** manifest to `upload()`. The uploader rewrites it internally — adding `EXT-X-KEY` tags that point to your key server.
</Note>

## SegmentInput format

```typescript theme={null}
interface SegmentInput {
  index: number      // Segment number (used for IV derivation)
  data: Uint8Array   // Raw .ts segment bytes (plaintext — will be encrypted)
  key: string        // Object key/path in storage (e.g., "seg-0.ts")
}
```

## Verify playback

After uploading, verify the content plays back correctly using the [Player](/player/overview):

```typescript theme={null}
import { createPlayer } from "@blindcast/player"

// Use the manifest URL returned by upload()
const playerResult = createPlayer(videoEl, {
  keyServerUrl: "https://keys.example.com/keys",
  keyServerAuth: async () => getAccessToken(),
})

if (playerResult.ok) {
  playerResult.value.load(result.value.manifestUrl)
}
```

The player fetches the same content key from the same key server and decrypts the segments you just uploaded. This closes the loop: upload → store → play.

## Authentication

The `auth` callback is called twice:

1. **Key fetch:** The uploader sends `Authorization: Bearer <token>` when fetching the content key
2. **Presign requests:** The uploader sends the same header when requesting presigned URLs

Make sure your auth token is valid for both the key server and presign endpoints. If they use different auth, you'll need to configure auth at the transport level.
