Behaviour contract for streaming providers (Phase 33 — promoted from v1.4 reserved shim).
A streaming provider implements asset-CRUD + signed-playback-URL + webhook-verify
against an external streaming service (e.g. Mux). The dispatch surface that decides
whether to call into a provider lives on Rindle.Delivery.streaming_url/3, NOT on
this behaviour.
Security invariant 14
Implementations MUST NOT expose provider_asset_id (or any provider-internal
identifier) in adopter-facing URLs, telemetry metadata, log lines, or inspect/2
output. Only the public-side playback_id (or its provider equivalent) crosses
into URLs. Custom Inspect impls on persistence rows enforce redaction at the
schema layer (see Rindle.Domain.MediaProviderAsset).
Callback discipline
Every callback returns an :ok-tuple or :error-tuple. No raises on the happy
path. verify_webhook/3 returns a normalized provider_event map — provider
structs (e.g. Mux structs) MUST NOT cross this boundary.
Summary
Types
Capability atom advertised by capabilities/0. Closed vocabulary lives in Rindle.Streaming.Capabilities.
Public-side playback identifier. Safe for URL embedding.
Provider-internal asset identifier. Treated as a secret; never exposed in adopter-facing paths.
Normalized webhook event surface. Provider-specific structs MUST be normalized
into this shape by verify_webhook/3 before crossing into core.
Locked finite-state-machine vocabulary for media_provider_assets.state.
Callbacks
Capabilities advertised by this provider. Filtered against Rindle.Streaming.Capabilities.known/0 by safe/1.
Create a provider-side asset for source_url under profile. Returns the
provider-internal identifier and (when known) initial playback ids.
OPTIONAL: Mint a direct-creator upload URL the browser can PUT to. Reserved for Phase 37 / v1.7; no v1.6 adapter implements this callback.
Delete the provider-side asset. Idempotent on :not_found.
Fetch the current provider-side state for provider_asset_id.
Mint a signed playback URL for playback_id under profile. Implementations
MUST respect the profile's signed_url_ttl_seconds policy (no hidden defaults).
Returns the v1.4-stable shape %{url, kind, mime}.
Verify a raw webhook payload against secrets. Returns a normalized
provider_event map on success — provider-specific structs MUST be normalized
before returning.
Types
@type capability() ::
:signed_playback
| :public_playback
| :webhook_ingest
| :server_push_ingest
| :direct_creator_upload
Capability atom advertised by capabilities/0. Closed vocabulary lives in Rindle.Streaming.Capabilities.
@type playback_id() :: String.t()
Public-side playback identifier. Safe for URL embedding.
@type provider_asset_id() :: String.t()
Provider-internal asset identifier. Treated as a secret; never exposed in adopter-facing paths.
@type provider_event() :: %{ :type => atom(), :provider_asset_id => provider_asset_id() | nil, :playback_ids => [playback_id()], :state => provider_state() | nil, :occurred_at => DateTime.t() | nil, :raw => map(), optional(:upload_id) => String.t() | nil }
Normalized webhook event surface. Provider-specific structs MUST be normalized
into this shape by verify_webhook/3 before crossing into core.
state is nil when the webhook payload carries no recognized status
(e.g. lifecycle events like video.asset.created that pre-date transcoding).
:upload_id is OPTIONAL and populated only by adapter typed branches that
carry both an upload id and a provider asset id (e.g. Mux's
video.upload.asset_created — see D-29 / D-30, added in Phase 35 as
forward-compat for Phase 37 / direct-creator-upload).
@type provider_state() :: String.t()
Locked finite-state-machine vocabulary for media_provider_assets.state.
BL-04 alignment: the schema column is :string (see
Rindle.Domain.MediaProviderAsset.@states), the FSM keys are strings
(Rindle.Domain.ProviderAssetFSM.@allowed_transitions), and adapter
implementations return strings (e.g. Rindle.Streaming.Provider.Mux.normalize_state/1).
This typespec mirrors that surface — the closed set lives at the schema
layer, not in the type system. Adopters MUST treat values as one of:
"pending" | "uploading" | "processing" | "ready" | "errored" | "deleted"
Callbacks
@callback capabilities() :: [capability()]
Capabilities advertised by this provider. Filtered against Rindle.Streaming.Capabilities.known/0 by safe/1.
@callback create_asset(profile :: module(), source_url :: String.t(), opts :: keyword()) :: {:ok, %{provider_asset_id: provider_asset_id(), playback_ids: [playback_id()]}} | {:error, term()}
Create a provider-side asset for source_url under profile. Returns the
provider-internal identifier and (when known) initial playback ids.
@callback create_direct_upload(profile :: module(), opts :: keyword()) :: {:ok, %{ upload_url: String.t(), upload_id: String.t(), provider_asset_id: provider_asset_id() | nil }} | {:error, term()}
OPTIONAL: Mint a direct-creator upload URL the browser can PUT to. Reserved for Phase 37 / v1.7; no v1.6 adapter implements this callback.
@callback delete_asset(provider_asset_id()) :: :ok | {:error, term()}
Delete the provider-side asset. Idempotent on :not_found.
@callback get_asset(provider_asset_id()) :: {:ok, %{state: provider_state(), playback_ids: [playback_id()], raw: map()}} | {:error, term()}
Fetch the current provider-side state for provider_asset_id.
@callback signed_playback_url(profile :: module(), playback_id(), opts :: keyword()) :: {:ok, %{url: String.t(), kind: :hls, mime: String.t()}} | {:error, term()}
Mint a signed playback URL for playback_id under profile. Implementations
MUST respect the profile's signed_url_ttl_seconds policy (no hidden defaults).
Returns the v1.4-stable shape %{url, kind, mime}.
@callback verify_webhook(raw_body :: binary(), headers :: map(), secrets :: [String.t()]) :: {:ok, provider_event()} | {:error, term()}
Verify a raw webhook payload against secrets. Returns a normalized
provider_event map on success — provider-specific structs MUST be normalized
before returning.