> ## Documentation Index
> Fetch the complete documentation index at: https://flex-fields.bjanczak.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Examples target Filament 5. Verify optional dependencies before configuring integrations.
> Upload transfer completion, permanent storage and record acceptance are separate milestones. Native and Spatie image conversions retain the original.

# Media OS browser integrations

> Use capture, temporary upload and resource-lifecycle helpers in your own FLEX components.

Use these optional helpers when building your own capture/upload interface. Standard FLEX fields already provide their interface; you do not need to call this API to configure them. Browser helpers do not replace server-side validation or permanent storage.

## Load the public module

Resolve the published module URL through Filament rather than hard-coding an asset filename:

```blade theme={"theme":{"light":"github-light","dark":"github-dark"}}
<div data-media-module="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('media-os', 'janczakb/filament-flex-fields') }}"></div>
```

For a custom FLEX component, declare the `media-os` dependency in its component definition's `uses` list and load the module when that component activates. Use one resource owner per rendered instance.

```js theme={"theme":{"light":"github-light","dark":"github-dark"}}
const {
    createMediaOwner, createLivewireUpload, registerMediaSubmit,
    createDeviceSession, capturePhoto, createPondSession, MediaClientFailure,
} = await import(element.dataset.mediaModule)
const owner = createMediaOwner({ onCleanupError: (error) => console.error(error) })
```

Here `element` is your mounted component element. Call `owner.dispose()` when your component is destroyed or replaced.

## Local preview URLs

```js theme={"theme":{"light":"github-light","dark":"github-dark"}}
const previewUrl = owner.objectUrl('photo', blob)
previewImage.src = previewUrl
// When the preview is no longer needed:
owner.releaseUrl('photo')
```

Replacing the same key releases its previous object URL. Owner disposal also releases remaining URLs.

## Temporary Livewire transfer

```js theme={"theme":{"light":"github-light","dark":"github-dark"}}
const upload = createLivewireUpload({
    owner,
    wire: this.$wire,
    path: this.statePath,
    maxBytes: 5 * 1024 * 1024,
    onProgress: (percent) => { this.progress = percent },
})
const temporaryIdentity = await upload.start(file)
```

Call this inside your own initialized Alpine controller; `file` is a non-empty Blob/File. The returned identity belongs to Livewire temporary storage. Only one active operation per helper/state path is supported.

```js theme={"theme":{"light":"github-light","dark":"github-dark"}}
upload.cancel()
```

Cancellation stops that request. Temporary cleanup follows the server's configured retention; the browser does not promise permanent storage deletion.

## Complete capture before form submit

```js theme={"theme":{"light":"github-light","dark":"github-dark"}}
registerMediaSubmit(element.closest('form'), {
    owner,
    pending: () => captureBlob !== null,
    persist: async () => {
        await upload.start(new File([captureBlob], 'photo.jpg', { type: 'image/jpeg' }))
        captureBlob = null
    },
    onError: (error) => { this.error = error.reason },
})
```

`captureBlob` is your pending capture variable. The barrier waits for all participating captures before resuming the native form submit. An error blocks submission; fix/resume the upload and submit again. Clear component-owned Blob references on destruction.

## Open a camera or microphone

Acquire permission only after the user's capture action:

```js theme={"theme":{"light":"github-light","dark":"github-dark"}}
const device = createDeviceSession({ owner })
const stream = await device.open({ video: { facingMode: 'environment' }, audio: false })
video.srcObject = stream
await video.play()
```

For microphone capture:

```js theme={"theme":{"light":"github-light","dark":"github-dark"}}
const microphone = createDeviceSession({ owner })
const stream = await microphone.open({ audio: true, video: false })
```

Close the active session before opening another on that helper. `device.close()` stops its stream; `device.dispose()` also prevents reopening it. Disposing the owner stops owned streams, including late permission results.

## Capture a decoded photo

```js theme={"theme":{"light":"github-light","dark":"github-dark"}}
const blob = await capturePhoto(video, {
    owner,
    type: 'image/jpeg', quality: 0.9,
    maxWidth: 1920, maxHeight: 1920,
    maxPixels: 16_000_000, maxBytes: 5 * 1024 * 1024,
})
```

Wait until the video has a decoded frame before calling. Supported browser output types are JPEG, PNG and WebP. The server must still validate dimensions, content and file size.

## Existing FilePond integration

```js theme={"theme":{"light":"github-light","dark":"github-dark"}}
const session = createPondSession({
    owner,
    instantUpload: true,
    beforeAdd: async (file) => file.size <= 5 * 1024 * 1024,
    onChange: (snapshot) => { this.files = snapshot.pond?.getFiles() ?? [] },
})
session.register(nativeRegistration)
```

`nativeRegistration` must come from your initialized native integration and contain `component`, `pond` and its actual `ready` flag. Do not announce readiness before native initialization finishes. Re-register when that native instance changes.

```js theme={"theme":{"light":"github-light","dark":"github-dark"}}
await session.whenReady()
await session.add(file, 'file')
await session.pauseAll()
await session.resumeAll()
```

Individual controls use `pause(fileId)`, `resume(fileId)` and `retry(fileId)`. Pause/resume restarts native whole-file requests; it is not byte-offset resume.

## Helper reference

| Helper | Available operations |
| - | - |
| `createMediaOwner()` | `assertActive`, `listen`, `defer`, `trackStream`, `objectUrl`, `releaseUrl`, `wait`, `dispose` / `destroy`; `signal`, `disposed` |
| `createLivewireUpload()` | `start`, `cancel`, `dispose`; `uploading` |
| `registerMediaSubmit()` | Register pending/persist/error callbacks with a form |
| `createDeviceSession()` | `open`, `close`, `dispose`; `stream`, `pending` |
| `capturePhoto()` | Decode/export a bounded raster Blob |
| `createPondSession()` | `register`, `snapshot`, `whenReady`, `whenProcessed`, `selectFiles`, `add`, `retry`, `pause`, `resume`, `pauseAll`, `resumeAll`, `refresh`, `pending`, `processPending` |
| `MediaClientFailure` | Error with a stable `reason` and optional cause |

`owner.wait(check, { subscribe, timeoutMs })` waits for your readiness condition; `subscribe` returns a cleanup function. Deadlines are 1–120,000 ms. FilePond readiness defaults to 15 seconds, processing to 120 seconds. `transportOnly` in `createPondSession()` disables native preview presentation for an integration that renders its own file UI.

## Cleanup and errors

```js theme={"theme":{"light":"github-light","dark":"github-dark"}}
try {
    await upload.start(file)
} catch (error) {
    this.error = error instanceof MediaClientFailure ? error.reason : 'upload_failed'
}
```

```js theme={"theme":{"light":"github-light","dark":"github-dark"}}
owner.dispose()
captureBlob = null
```

Listeners registered with `owner.listen()` and callbacks registered with `owner.defer()` are released with the instance. Never reuse a disposed owner for a newly mounted field. Keep server attachment authorization in [Media OS access policies](https://flex-fields.bjanczak.com/docs/media-os-extensions.md#custom-access-policy).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.