# External AV and voting integration

RIS owns meeting information, access control, publication, formal ballots and audit records. Recording, streaming, microphones and physical voting equipment belong to external systems. The archived terminal code is in `archive/voting-terminals/`; it is excluded from the active solution, CI, Aspire and Compose.

The presenter/hall display and browser capture/ingest routes have been removed. The livestream page follows externally supplied media. Completed meeting recordings and approved transcripts remain available through meeting watch. External AV controls remain at `/administratie/av`, governed by `StreamControl` and the selected provider's actual capabilities. RTMP and NDI adapters are remote control clients; RIS no longer implements its own stream ingest or recording server.

## Media provider destinations

Configure server-approved HTTPS origins for each media adapter in **both API and Workers**:

```text
MediaProviders__Notubiz__AllowedOrigins__0=https://api.notubiz.nl
MediaProviders__CompanyWebcast__AllowedOrigins__0=https://your-contracted-provider-host.example
```

Replace examples with the exact contracted origins. An approved origin contains only scheme, hostname and optional port. A provider record may include a path under that origin. Unapproved endpoints, private addresses and redirects fail closed before credentials are sent. Editing a provider record never grants approval to its destination. A rejected update retains the original endpoint and credentials.

External AV control endpoints use the existing `AV` configuration and adapter interfaces. They are separate from the media-provider allowlist above. Configuring a stream does not grant publication access to a confidential meeting.

## Voting API

1. An administrator creates a dedicated OAuth client at `POST /api/oauth-clients` with the exact scope `voting.write`. Store its generated secret in the external system's secret store.
2. Staff with `MeetingManagement` maintain voter mappings using the existing person create/update API's optional `externalId` field, e.g. `vendor:member-7`. Existing migration identities use this same field; coordinate changes with import configuration. An empty string explicitly clears a mapping, and an omitted field preserves it on update. Duplicate mappings are rejected on staff writes; any ambiguous legacy mapping also fails closed during ingestion.
3. Staff create the formal vote session and open it with `Provider` enabled using `POST /api/vote-sessions/{id}/open-configured`. The provider client cannot create or establish sessions through staff-only endpoints.
4. The external system obtains a token using form-encoded `POST /api/oauth/token` with `grant_type=client_credentials`, `client_id`, `client_secret`, and `scope=voting.write`.
5. It records a ballot with `POST /api/vote-sessions/{id}/provider-votes`:

```json
{
  "voterExternalId": "vendor:member-7",
  "choice": "For"
}
```

Allowed choices are `For`, `Against`, and `Abstain`. RIS resolves the person from the stored mapping and requires an active council member with a party and no formal absence for that meeting. A request cannot select a trusted RIS person ID directly. Current client status, expiry and scope are checked for every ballot, so client revocation takes effect immediately. The first accepted ballot returns 201; an identical retry returns 200, and an attempted overwrite returns 409. Session transitions, ballots and audit changes participate in the same PostgreSQL concurrency boundary.

Staff close and establish the formal result through the existing voting workflow. Named results and tallies become public only after establishment and remain subject to the meeting and document access rules. Manual recording remains an audited fallback with the same membership and absence checks.

Any future first-party AV or terminal implementation should be extracted into its own repository/deployment and use these authenticated interfaces. The archive is historical source, with unmaintained packages and no supported deployment path.
