The upload() function
upload() takes three arguments: segments, a manifest string, and options.
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}`)
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.
What happens internally
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):
ffmpeg -i input.mp4 -codec: copy -hls_time 6 -hls_list_size 0 -f hls segments/manifest.m3u8
Reading files from a browser <input>:
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,
})),
)
Pass the original, unmodified manifest to upload(). The uploader rewrites it internally — adding EXT-X-KEY tags that point to your key server.
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:
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:
- Key fetch: The uploader sends
Authorization: Bearer <token> when fetching the content key
- 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.