Mux REST adapter implementing Rindle.Streaming.Provider.
Configuration
Credentials and tunables are resolved from the application environment on
every call (D-30 — no caching at module load time; adopters using
config/runtime.exs are unaffected):
config :rindle, Rindle.Streaming.Provider.Mux,
token_id: System.get_env("RINDLE_MUX_TOKEN_ID"),
token_secret: System.get_env("RINDLE_MUX_TOKEN_SECRET"),
signing_key_id: System.get_env("RINDLE_MUX_SIGNING_KEY_ID"),
signing_private_key: System.get_env("RINDLE_MUX_SIGNING_PRIVATE_KEY"),
webhook_secrets:
System.get_env("RINDLE_MUX_WEBHOOK_SECRETS", "")
|> String.split(",", trim: true),
webhook_tolerance_seconds: 300,
provider_polling_floor_seconds: 30,
provider_stuck_threshold_seconds: 7200DSL ↔ Mux REST translation (D-04 memo correction)
Phase 33 ships the DSL atom :playback_policy (singular) and the schema
column playback_policy (singular). The Phase 33 schema also defines
field :playback_ids, {:array, :string} (PLURAL ARRAY).
At the SDK boundary this adapter translates to the current Mux REST API
keys: inputs (PLURAL list of objects) and playback_policies (PLURAL
string list). The Mux singular keys are deprecated as of 2026-05 — always
use plural here. Param construction lives in a single private helper
(build_create_params/2) so workers never duplicate this logic.
The Phase 33 callback contract returns playback_ids: [playback_id()] (a
list); the row schema persists playback_ids as {:array, :string}. The
adapter writes the array verbatim; reads use List.first/1 only when a
single id is needed (e.g., for URL minting).
Test wiring
Tests configure the adapter to route HTTP calls through
Rindle.Streaming.Provider.Mux.ClientMock by overriding the
:http_client key on the same :rindle, __MODULE__ config keyspace
(see test/rindle/streaming/provider/mux/mux_test.exs for the canonical
setup). The default :http_client is Rindle.Streaming.Provider.Mux.HTTP.
Security invariants
provider_asset_idnever crosses into adopter-facing URLs, log lines, or telemetry metadata. Only the public-sideplayback_idis embedded in URLs. Telemetry emit sites useRindle.Domain.MediaProviderAsset.redact_id/1(security invariant 14).signed_playback_url/3ALWAYS passes:expirationexplicitly toMux.Token.sign_playback_id/2. The SDK default is 7 days (Pitfall 1) — relying on it would silently mint over-long tokens.
Telemetry Contract
The adapter and its companion workers emit the following events. All
events with metadata.asset_id redact the value to its last-4-char tag
("...abcd") via Rindle.Domain.MediaProviderAsset.redact_id/1
(security invariant 14). Adopters writing telemetry handlers MUST treat
asset_id as a redacted identifier — it is not a stable correlation key.
[:rindle, :provider, :ingest, :start | :stop | :exception]— emitted byRindle.Workers.MuxIngestVariant(Phase 34).- measurements:
%{system_time, duration?}(durationonly on:stop/:exception) - metadata:
%{profile, provider, asset_id, variant_name, kind?, reason?} kind: :error | :cancelledis added on:exceptionto distinguish genuine errors ({:error, _}) from atomic-promote cancellations ({:cancel, {:stale_source, _}}).
- measurements:
[:rindle, :provider, :sync, :resolved | :stuck]— emitted byRindle.Workers.MuxSyncProviderAsset(Phase 34).- measurements:
%{system_time} - metadata:
%{profile, provider, asset_id, provider_state, age_ms} :stuckfires when a row in:processing/:uploadingexceeds:provider_stuck_threshold_seconds(default 7200).
- measurements:
[:rindle, :delivery, :streaming, :resolved]— already shipped by Phase 33'sdispatch_streaming/4. No new event from Phase 34 on this path; the metadata extension is the documented v1.4-contract addition.
Phase 35 will add [:rindle, :provider, :webhook, _] events. Phase 34
does not emit them.
Summary
Functions
Server-push ingest entry point. Translates DSL :playback_policy (singular,
via opts) to the Mux REST PLURAL playback_policies key. Returns the
Phase 33 contract shape {:ok, %{provider_asset_id: _, playback_ids: [_]}}
(PLURAL array, even with a single element).
Worker-facing variant of create_asset/3 that exposes the 429 Retry-After
seconds value so the Plan 02 worker can snooze cleanly. Param construction
(PLURAL keys) lives ONLY here in the adapter — never duplicated in workers.
Functions
Server-push ingest entry point. Translates DSL :playback_policy (singular,
via opts) to the Mux REST PLURAL playback_policies key. Returns the
Phase 33 contract shape {:ok, %{provider_asset_id: _, playback_ids: [_]}}
(PLURAL array, even with a single element).
Errors are normalized to the Phase 33 atom set:
:provider_quota_exceeded(HTTP 429) — caller can extractRetry-Afterfrom%Tesla.Env{}.headersviacreate_asset_with_retry_hint/3if it needs the snooze duration (Plan 02 worker uses that variant).:provider_sync_failed(HTTP 4xx/5xx other than 429).
@spec create_asset_with_retry_hint(module(), String.t(), keyword()) :: {:ok, %{provider_asset_id: String.t(), playback_ids: [String.t()]}} | {:error, :provider_quota_exceeded, non_neg_integer()} | {:error, atom()} | {:error, term()}
Worker-facing variant of create_asset/3 that exposes the 429 Retry-After
seconds value so the Plan 02 worker can snooze cleanly. Param construction
(PLURAL keys) lives ONLY here in the adapter — never duplicated in workers.
Returns:
{:ok, %{provider_asset_id: _, playback_ids: [_]}}— happy path.{:error, :provider_quota_exceeded, retry_after_seconds}— HTTP 429 with parsedRetry-After(60-second floor when header is missing or unparseable).{:error, :provider_sync_failed}— other 4xx/5xx.{:error, term()}— transport/lower-level error.