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:

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:
{
  "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.