refactor: use chat capability surfaces

Move Sender modules under TribeOne.TribesPlugin.Sender and replace the Aether-specific chat integration with the public chat@1 surface contract.
This commit is contained in:
2026-05-26 01:13:38 +02:00
parent e771ab3c35
commit f8e2bfaada
63 changed files with 383 additions and 308 deletions
+6 -4
View File
@@ -23,7 +23,7 @@ test surfaces.
## Plugin Contract
- `manifest.json` `entry_module` must point at `Tribes.Plugins.Sender.Plugin`.
- `manifest.json` `entry_module` must point at `TribeOne.TribesPlugin.Sender.Plugin`.
- Keep streaming supervision children restartable under normal OTP supervision.
- Keep runtime spec fields aligned with `manifest.json`.
@@ -547,8 +547,10 @@ source directory, the same document is at `../docs/plugins.md`.
- Implement the runtime contract with `Tribes.Plugin` or `Tribes.Plugin.Base`.
- Prefer `use Tribes.Plugin.Base, otp_app: :your_plugin` for normal plugins; it
reads `manifest.json` and fills the manifest-backed spec fields.
- Keep the host-facing entry module under `Tribes.Plugins.*.Plugin`. It may be a
thin delegate to the plugin application's own module.
- Keep plugin modules under an owner-controlled namespace, such as
`TribeOne.TribesPlugin.<Plugin>.Plugin` for first-party plugins or
`AcmeCorp.TribesPlugins.Foo.Plugin` for third-party plugins. The entry module
must end in `.Plugin` and should be the module named by `manifest.json`.
- `register/1` must return a `Tribes.Plugin.Spec` struct or a map/struct that
validates into that spec.
@@ -617,7 +619,7 @@ checkout during development. For the full contract, see
- `manifest.json` is the static build/runtime contract. Keep `name`,
`version`, `entry_module`, `host_api`, `otp_app`, `provides`, `requires`, and
`enhances_with` aligned with the runtime spec returned by `register/1`.
- `entry_module` must be under `Tribes.Plugins.*.Plugin` and end in `.Plugin`.
- `entry_module` must be a valid module ending in `.Plugin` under an owner-controlled namespace.
- Capability versions are discrete breaking-change markers such as `ui@1` or
`social@1`, not semver. Framework APIs are part of the `host_api` foundation, not separate manifest capabilities.
- Use `requires` for hard dependencies and `enhances_with` for optional
+2 -2
View File
@@ -108,7 +108,7 @@ your_plugin/
```json
{
"name": "your_plugin",
"entry_module": "Tribes.Plugins.YourPlugin.Plugin",
"entry_module": "TribeOne.TribesPlugin.YourPlugin.Plugin",
"host_api": "1",
"otp_app": "your_plugin",
"provides": ["some_capability@1"],
@@ -117,7 +117,7 @@ your_plugin/
}
```
- **entry_module** — must be `Tribes.Plugins.*.Plugin`
- **entry_module** — must be `<Owner>.<Plugin>.Plugin`
- **otp_app** — required and must match `name`
- **provides** — capabilities this plugin makes available
- **requires** — hard dependencies (build fails without them)
+2 -2
View File
@@ -1,8 +1,8 @@
import Config
config :sender, ash_domains: [Sender.Streaming]
config :sender, ash_domains: [TribeOne.TribesPlugin.Sender.Streaming]
config :sender, Sender.HLS,
config :sender, TribeOne.TribesPlugin.Sender.HLS,
hls_time: 2,
hls_list_size: 6,
hls_delete_threshold: 3,
+12 -12
View File
@@ -3,17 +3,17 @@
Sender exposes two public control surfaces:
1. **Plugin management methods** for remote/admin orchestration, including Legion.
2. **`Sender.LocalStreams`** for IEx and local no-Legion operation on the node.
2. **`TribeOne.TribesPlugin.Sender.LocalStreams`** for IEx and local no-Legion operation on the node.
## Local stream control from IEx
Use `Sender.LocalStreams` when you are on a node and want to operate Sender
Use `TribeOne.TribesPlugin.Sender.LocalStreams` when you are on a node and want to operate Sender
without Legion:
```elixir
{:ok, live} = Sender.LocalStreams.start_origin()
Sender.LocalStreams.status()
Sender.LocalStreams.stop(live)
{:ok, live} = TribeOne.TribesPlugin.Sender.LocalStreams.start_origin()
TribeOne.TribesPlugin.Sender.LocalStreams.status()
TribeOne.TribesPlugin.Sender.LocalStreams.stop(live)
```
`start_origin/1` requires the local Tribes node identity to exist. It reads the
@@ -25,20 +25,20 @@ To generate a test stream with local `ffmpeg` and push it to the node's RTMP
listener:
```elixir
{:ok, live} = Sender.LocalStreams.start_origin(push_test?: true)
{:ok, live} = TribeOne.TribesPlugin.Sender.LocalStreams.start_origin(push_test?: true)
```
The generated publisher uses MuonTrap and runs under Sender supervision. Stop it
without stopping the stream:
```elixir
Sender.LocalStreams.stop_test_video(live)
TribeOne.TribesPlugin.Sender.LocalStreams.stop_test_video(live)
```
Use a finite test-video duration if desired:
```elixir
{:ok, live} = Sender.LocalStreams.start_origin(push_test?: true, duration: 30)
{:ok, live} = TribeOne.TribesPlugin.Sender.LocalStreams.start_origin(push_test?: true, duration: 30)
```
Useful defaults:
@@ -51,7 +51,7 @@ Useful defaults:
Common overrides:
```elixir
Sender.LocalStreams.start_origin(
TribeOne.TribesPlugin.Sender.LocalStreams.start_origin(
input_url: "rtmp://0.0.0.0:1936/live/source",
push_url: "rtmp://127.0.0.1:1936/live/source",
hls_base_url: "https://example.test/sender/hls",
@@ -62,8 +62,8 @@ Sender.LocalStreams.start_origin(
You can also start only the generated publisher after opening a stream:
```elixir
{:ok, test_video} = Sender.LocalStreams.push_test_video(duration: 10)
Sender.LocalStreams.stop_test_video(test_video["pid"])
{:ok, test_video} = TribeOne.TribesPlugin.Sender.LocalStreams.push_test_video(duration: 10)
TribeOne.TribesPlugin.Sender.LocalStreams.stop_test_video(test_video["pid"])
```
## Remote management methods
@@ -83,5 +83,5 @@ through Tribes' admin management API. The primary methods are:
- `stream.status`
- `endpoint_snapshots.report`
`Sender.LocalStreams` composes these same management handlers locally. It is not
`TribeOne.TribesPlugin.Sender.LocalStreams` composes these same management handlers locally. It is not
a separate remote command API for running arbitrary ffmpeg commands.
+1 -1
View File
@@ -431,7 +431,7 @@ Vinyl can sit in front of plugin HLS routes or external HLS origins. The plugin
Start with a backend abstraction:
```elixir
defmodule Sender.MediaBackend do
defmodule TribeOne.TribesPlugin.Sender.MediaBackend do
@callback start_ingest(StreamGeneration.t(), keyword()) :: {:ok, term()} | {:error, term()}
@callback stop_ingest(StreamGeneration.t(), keyword()) :: :ok | {:error, term()}
@callback health(term()) :: map()
+6 -6
View File
@@ -11,12 +11,12 @@ This file tracks the planned implementation from plugin template to clustered st
- [x] Enable the plugin supervision tree in `mix.exs`.
- [x] Decide initial media backend: supervised `ffmpeg` first, Membrane later behind a behaviour.
- [x] Decide MVP control plane: Legion drives admin/orchestration through Tribes plugin management methods.
- [x] Decide local HLS spool root config key: `config :sender, Sender.HLS, spool_root: ...`.
- [x] Decide local HLS spool root config key: `config :sender, TribeOne.TribesPlugin.Sender.HLS, spool_root: ...`.
- [x] Document required runtime binaries in deployment packaging: at least `ffmpeg`.
## Phase 1: Ash Domain and Synced Schema
- [x] Add `Sender.Streaming` Ash domain.
- [x] Add `TribeOne.TribesPlugin.Sender.Streaming` Ash domain.
- [x] Register the domain through the plugin spec `ash_domains`.
- [x] Add migrations for plugin tables.
- [x] Add `Stream` resource with AshNostrSync.
@@ -32,9 +32,9 @@ This file tracks the planned implementation from plugin template to clustered st
## Phase 2: Single-Node RTMP to HLS MVP
- [x] Add `Sender.MediaBackend` behaviour.
- [x] Implement `Sender.MediaBackend.FFmpeg`.
- [x] Add role-aware `Sender.MediaSession` runtime owner for `origin_ingest`.
- [x] Add `TribeOne.TribesPlugin.Sender.MediaBackend` behaviour.
- [x] Implement `TribeOne.TribesPlugin.Sender.MediaBackend.FFmpeg`.
- [x] Add role-aware `TribeOne.TribesPlugin.Sender.MediaSession` runtime owner for `origin_ingest`.
- [x] Run FFmpeg through `MuonTrap` instead of bare Erlang ports.
- [x] Supervise ingest pipeline processes.
- [x] Generate a stream key and validate RTMP ingest attempts.
@@ -135,7 +135,7 @@ This file tracks the planned implementation from plugin template to clustered st
## Phase 9: Membrane Evaluation
- [ ] Identify Membrane packages needed for RTMP ingest, HLS output, and optional transcoding.
- [ ] Build a backend prototype behind `Sender.MediaBackend`.
- [ ] Build a backend prototype behind `TribeOne.TribesPlugin.Sender.MediaBackend`.
- [ ] Compare operational behavior against ffmpeg: startup, failure handling, CPU, memory, latency.
- [ ] Decide whether Membrane becomes primary, optional, or deferred.
- [ ] Keep ffmpeg backend available unless Membrane fully covers production needs.
+16 -13
View File
@@ -1,4 +1,4 @@
defmodule Sender.Application do
defmodule TribeOne.TribesPlugin.Sender.Application do
@moduledoc """
OTP Application for this plugin.
@@ -11,29 +11,32 @@ defmodule Sender.Application do
@doc false
def children do
[
Sender.Stats,
Sender.SnapshotWorker,
Sender.HLSCleanup,
{Registry, keys: :unique, name: Sender.MediaSessionRegistry},
TribeOne.TribesPlugin.Sender.Stats,
TribeOne.TribesPlugin.Sender.SnapshotWorker,
TribeOne.TribesPlugin.Sender.HLSCleanup,
{Registry, keys: :unique, name: TribeOne.TribesPlugin.Sender.MediaSessionRegistry},
Supervisor.child_spec(
{DynamicSupervisor, strategy: :one_for_one, name: Sender.MediaSessionSupervisor},
id: Sender.MediaSessionSupervisor
{DynamicSupervisor,
strategy: :one_for_one, name: TribeOne.TribesPlugin.Sender.MediaSessionSupervisor},
id: TribeOne.TribesPlugin.Sender.MediaSessionSupervisor
),
Supervisor.child_spec(
{DynamicSupervisor, strategy: :one_for_one, name: Sender.MediaBackendSupervisor},
id: Sender.MediaBackendSupervisor
{DynamicSupervisor,
strategy: :one_for_one, name: TribeOne.TribesPlugin.Sender.MediaBackendSupervisor},
id: TribeOne.TribesPlugin.Sender.MediaBackendSupervisor
),
Supervisor.child_spec(
{DynamicSupervisor, strategy: :one_for_one, name: Sender.HLSReadinessSupervisor},
id: Sender.HLSReadinessSupervisor
{DynamicSupervisor,
strategy: :one_for_one, name: TribeOne.TribesPlugin.Sender.HLSReadinessSupervisor},
id: TribeOne.TribesPlugin.Sender.HLSReadinessSupervisor
),
Sender.EdgeSessionReconciler
TribeOne.TribesPlugin.Sender.EdgeSessionReconciler
]
end
@impl true
def start(_type, _args) do
opts = [strategy: :one_for_one, name: Sender.Supervisor]
opts = [strategy: :one_for_one, name: TribeOne.TribesPlugin.Sender.Supervisor]
Supervisor.start_link(children(), opts)
end
end
+16 -8
View File
@@ -1,14 +1,15 @@
defmodule Sender.ChatIntegration do
defmodule TribeOne.TribesPlugin.Sender.ChatIntegration do
@moduledoc false
@chat_capability "chat@1"
@aether_component_module Module.concat(["AetherWeb", "ChatPanelComponent"])
def component_module do
if chat_available?() && Code.ensure_loaded?(@aether_component_module) do
{:ok, @aether_component_module}
def embedded_panel(slug, opts \\ []) when is_binary(slug) do
with {:ok, provider} <- provider_module(),
true <- function_exported?(provider, :embedded_panel, 2),
{:ok, surface} <- provider.embedded_panel(slug, opts) do
{:ok, surface}
else
:unavailable
_other -> :unavailable
end
rescue
_error -> :unavailable
@@ -19,12 +20,19 @@ defmodule Sender.ChatIntegration do
end
def chat_available? do
Code.ensure_loaded?(Tribes.PluginRegistry) &&
Tribes.PluginRegistry.provides?(@chat_capability)
match?({:ok, _provider}, provider_module())
rescue
_error -> false
end
defp provider_module do
if Code.ensure_loaded?(Tribes.Plugin.Registry) do
Tribes.Plugin.Registry.provider_module(@chat_capability)
else
:error
end
end
defp slugify(value) do
value
|> String.downcase()
+2 -2
View File
@@ -1,11 +1,11 @@
defmodule Sender.EdgeSessionReconciler do
defmodule TribeOne.TribesPlugin.Sender.EdgeSessionReconciler do
@moduledoc false
use GenServer
require Logger
alias Sender.{HLS, Management, MediaSession, Streaming}
alias TribeOne.TribesPlugin.Sender.{HLS, Management, MediaSession, Streaming}
@active_statuses [:pending, :listening, :live, :degraded, :ending]
@default_initial_delay_ms 1_000
+2 -2
View File
@@ -1,9 +1,9 @@
defmodule Sender.HLS do
defmodule TribeOne.TribesPlugin.Sender.HLS do
@moduledoc """
Local HLS file serving helpers.
"""
alias Sender.Streaming.StreamGeneration
alias TribeOne.TribesPlugin.Sender.Streaming.StreamGeneration
@playlist_mime "application/vnd.apple.mpegurl"
@mpegts_mime "video/mp2t"
+3 -3
View File
@@ -1,12 +1,12 @@
defmodule Sender.HLSCleanup do
defmodule TribeOne.TribesPlugin.Sender.HLSCleanup do
@moduledoc false
use GenServer
require Logger
alias Sender.HLS
alias Sender.Streaming.StreamGeneration
alias TribeOne.TribesPlugin.Sender.HLS
alias TribeOne.TribesPlugin.Sender.Streaming.StreamGeneration
def child_spec(opts) do
%{
+2 -2
View File
@@ -1,11 +1,11 @@
defmodule Sender.HLSReadinessWatcher do
defmodule TribeOne.TribesPlugin.Sender.HLSReadinessWatcher do
@moduledoc false
use GenServer
require Logger
alias Sender.{HLS, Management, Streaming}
alias TribeOne.TribesPlugin.Sender.{HLS, Management, Streaming}
@default_poll_interval_ms 1_000
+6 -5
View File
@@ -1,14 +1,14 @@
defmodule Sender.LocalStreams do
defmodule TribeOne.TribesPlugin.Sender.LocalStreams do
@moduledoc """
Local Sender stream control helpers.
This module is intended for IEx and local operation when an external
orchestrator such as Legion is not managing Sender. It composes the same
orchestrator such as Legion is not managing TribeOne.TribesPlugin.Sender. It composes the same
management handlers that remote orchestration uses, and can optionally launch
a local ffmpeg test-video publisher with MuonTrap.
"""
alias Sender.Management.{StreamLifecycle, Topology}
alias TribeOne.TribesPlugin.Sender.Management.{StreamLifecycle, Topology}
alias Tribes.Plugin.ManagementContext
@default_input_url "rtmp://0.0.0.0:1935/live/source"
@@ -86,7 +86,7 @@ defmodule Sender.LocalStreams do
@doc """
Start a local generated ffmpeg test-video publisher.
The publisher is started under `Sender.MediaBackendSupervisor` by default, so
The publisher is started under `TribeOne.TribesPlugin.Sender.MediaBackendSupervisor` by default, so
it is not linked to the IEx shell process. Pass `duration: seconds` to make it
exit on its own.
"""
@@ -291,7 +291,8 @@ defmodule Sender.LocalStreams do
end
defp start_muontrap(executable, args, opts) do
supervisor = Keyword.get(opts, :supervisor, Sender.MediaBackendSupervisor)
supervisor =
Keyword.get(opts, :supervisor, TribeOne.TribesPlugin.Sender.MediaBackendSupervisor)
child_spec = %{
id: {__MODULE__, :test_video, System.unique_integer([:positive, :monotonic])},
+3 -3
View File
@@ -1,10 +1,10 @@
defmodule Sender.Management do
defmodule TribeOne.TribesPlugin.Sender.Management do
@moduledoc false
require Ash.Query
alias Sender.Streaming
alias Sender.Streaming.{Stream, StreamGeneration}
alias TribeOne.TribesPlugin.Sender.Streaming
alias TribeOne.TribesPlugin.Sender.Streaming.{Stream, StreamGeneration}
@default_stream_slug "default"
+2 -2
View File
@@ -1,4 +1,4 @@
defmodule Sender.Management.Capabilities do
defmodule TribeOne.TribesPlugin.Sender.Management.Capabilities do
@moduledoc false
def call(_context, _params) do
@@ -7,7 +7,7 @@ defmodule Sender.Management.Capabilities do
"ok" => true,
"schema_version" => "1",
"single_active_stream" => true,
"default_stream_slug" => Sender.Management.default_stream_slug(),
"default_stream_slug" => TribeOne.TribesPlugin.Sender.Management.default_stream_slug(),
"stream_modes" => ["origin_ingest", "hls_edge"],
"endpoint_types" => ["tribes_origin", "tribes_edge", "external_origin", "external_edge"],
"management_methods" => [
+4 -4
View File
@@ -1,9 +1,9 @@
defmodule Sender.Management.EndpointSnapshots do
defmodule TribeOne.TribesPlugin.Sender.Management.EndpointSnapshots do
@moduledoc false
alias Sender.Management
alias Sender.SnapshotPublisher
alias Sender.Streaming
alias TribeOne.TribesPlugin.Sender.Management
alias TribeOne.TribesPlugin.Sender.SnapshotPublisher
alias TribeOne.TribesPlugin.Sender.Streaming
def report(context, params) do
ash_opts = Management.ash_opts(context)
+3 -3
View File
@@ -1,8 +1,8 @@
defmodule Sender.Management.Stream do
defmodule TribeOne.TribesPlugin.Sender.Management.Stream do
@moduledoc false
alias Sender.Management
alias Sender.Streaming
alias TribeOne.TribesPlugin.Sender.Management
alias TribeOne.TribesPlugin.Sender.Streaming
def get_default(context, params) do
ash_opts = Management.ash_opts(context)
+4 -4
View File
@@ -1,9 +1,9 @@
defmodule Sender.Management.StreamKey do
defmodule TribeOne.TribesPlugin.Sender.Management.StreamKey do
@moduledoc false
alias Sender.Management
alias Sender.Streaming
alias Sender.Streaming.StreamKeys
alias TribeOne.TribesPlugin.Sender.Management
alias TribeOne.TribesPlugin.Sender.Streaming
alias TribeOne.TribesPlugin.Sender.Streaming.StreamKeys
def create(context, params) do
ash_opts = Management.ash_opts(context)
+10 -6
View File
@@ -1,9 +1,9 @@
defmodule Sender.Management.StreamLifecycle do
defmodule TribeOne.TribesPlugin.Sender.Management.StreamLifecycle do
@moduledoc false
alias Sender.{HLS, HLSCleanup, Management, MediaSession}
alias Sender.Management.Topology
alias Sender.Streaming
alias TribeOne.TribesPlugin.Sender.{HLS, HLSCleanup, Management, MediaSession}
alias TribeOne.TribesPlugin.Sender.Management.Topology
alias TribeOne.TribesPlugin.Sender.Streaming
def start(context, params) do
ash_opts = Management.ash_opts(context)
@@ -66,7 +66,8 @@ defmodule Sender.Management.StreamLifecycle do
end
defp start_hls_readiness_watcher(generation, rendition, params) do
supervisor = Map.get(params, "readiness_supervisor", Sender.HLSReadinessSupervisor)
supervisor =
Map.get(params, "readiness_supervisor", TribeOne.TribesPlugin.Sender.HLSReadinessSupervisor)
opts =
[
@@ -76,7 +77,10 @@ defmodule Sender.Management.StreamLifecycle do
]
|> maybe_put_opt(:poll_interval_ms, Map.get(params, "readiness_poll_interval_ms"))
case DynamicSupervisor.start_child(supervisor, {Sender.HLSReadinessWatcher, opts}) do
case DynamicSupervisor.start_child(
supervisor,
{TribeOne.TribesPlugin.Sender.HLSReadinessWatcher, opts}
) do
{:ok, _pid} -> :ok
{:error, {:already_started, _pid}} -> :ok
{:error, _reason} = error -> error
+3 -3
View File
@@ -1,8 +1,8 @@
defmodule Sender.Management.Topology do
defmodule TribeOne.TribesPlugin.Sender.Management.Topology do
@moduledoc false
alias Sender.Management
alias Sender.Streaming
alias TribeOne.TribesPlugin.Sender.Management
alias TribeOne.TribesPlugin.Sender.Streaming
def upsert_endpoint(context, params) do
with {:ok, attrs} <- endpoint_attrs(params),
+2 -2
View File
@@ -1,9 +1,9 @@
defmodule Sender.MediaBackend do
defmodule TribeOne.TribesPlugin.Sender.MediaBackend do
@moduledoc """
Behaviour for local media ingest/transmux backends.
"""
alias Sender.Streaming.StreamGeneration
alias TribeOne.TribesPlugin.Sender.Streaming.StreamGeneration
@callback start_ingest(StreamGeneration.t(), keyword()) ::
{:ok, pid()} | {:error, term()}
+7 -6
View File
@@ -1,4 +1,4 @@
defmodule Sender.MediaBackend.FFmpeg do
defmodule TribeOne.TribesPlugin.Sender.MediaBackend.FFmpeg do
@moduledoc """
ffmpeg-backed RTMP-to-HLS media backend.
@@ -7,18 +7,19 @@ defmodule Sender.MediaBackend.FFmpeg do
flags without launching a long-running media process.
"""
@behaviour Sender.MediaBackend
@behaviour TribeOne.TribesPlugin.Sender.MediaBackend
alias Sender.HLS
alias Sender.MediaBackend.FFmpeg.Worker
alias Sender.Streaming.StreamGeneration
alias TribeOne.TribesPlugin.Sender.HLS
alias TribeOne.TribesPlugin.Sender.MediaBackend.FFmpeg.Worker
alias TribeOne.TribesPlugin.Sender.Streaming.StreamGeneration
@default_rtmp_bind_url "rtmp://0.0.0.0:1935/live/source"
@executable_env "SENDER_FFMPEG_EXECUTABLE"
@impl true
def start_ingest(%StreamGeneration{} = generation, opts \\ []) do
supervisor = Keyword.get(opts, :supervisor, Sender.MediaBackendSupervisor)
supervisor =
Keyword.get(opts, :supervisor, TribeOne.TribesPlugin.Sender.MediaBackendSupervisor)
DynamicSupervisor.start_child(
supervisor,
+2 -2
View File
@@ -1,11 +1,11 @@
defmodule Sender.MediaBackend.FFmpeg.Worker do
defmodule TribeOne.TribesPlugin.Sender.MediaBackend.FFmpeg.Worker do
@moduledoc false
use GenServer
require Logger
alias Sender.MediaBackend.FFmpeg
alias TribeOne.TribesPlugin.Sender.MediaBackend.FFmpeg
def child_spec(opts) do
generation = Keyword.fetch!(opts, :generation)
+6 -5
View File
@@ -1,4 +1,4 @@
defmodule Sender.MediaBackend.HLSPullThrough do
defmodule TribeOne.TribesPlugin.Sender.MediaBackend.HLSPullThrough do
@moduledoc """
HLS pull-through backend for `tribes_edge` endpoints.
@@ -7,14 +7,15 @@ defmodule Sender.MediaBackend.HLSPullThrough do
Vinyl can then serve the local files from `/sender/hls/...`.
"""
@behaviour Sender.MediaBackend
@behaviour TribeOne.TribesPlugin.Sender.MediaBackend
alias Sender.MediaBackend.HLSPullThrough.Worker
alias Sender.Streaming.StreamGeneration
alias TribeOne.TribesPlugin.Sender.MediaBackend.HLSPullThrough.Worker
alias TribeOne.TribesPlugin.Sender.Streaming.StreamGeneration
@impl true
def start_ingest(%StreamGeneration{} = generation, opts \\ []) do
supervisor = Keyword.get(opts, :supervisor, Sender.MediaBackendSupervisor)
supervisor =
Keyword.get(opts, :supervisor, TribeOne.TribesPlugin.Sender.MediaBackendSupervisor)
DynamicSupervisor.start_child(
supervisor,
@@ -1,11 +1,11 @@
defmodule Sender.MediaBackend.HLSPullThrough.Worker do
defmodule TribeOne.TribesPlugin.Sender.MediaBackend.HLSPullThrough.Worker do
@moduledoc false
use GenServer
require Logger
alias Sender.HLS
alias TribeOne.TribesPlugin.Sender.HLS
@default_poll_interval_ms 1_000
+11 -7
View File
@@ -1,4 +1,4 @@
defmodule Sender.MediaSession do
defmodule TribeOne.TribesPlugin.Sender.MediaSession do
@moduledoc """
Owns one local node's runtime participation in a stream generation.
@@ -10,8 +10,8 @@ defmodule Sender.MediaSession do
use GenServer
alias Sender.MediaBackend.{FFmpeg, HLSPullThrough}
alias Sender.Streaming.StreamGeneration
alias TribeOne.TribesPlugin.Sender.MediaBackend.{FFmpeg, HLSPullThrough}
alias TribeOne.TribesPlugin.Sender.Streaming.StreamGeneration
@roles [:origin_ingest, :hls_edge]
@session_keys [
@@ -58,7 +58,8 @@ defmodule Sender.MediaSession do
DynamicSupervisor.on_start_child()
def start_role(role, %StreamGeneration{} = generation, opts)
when role in @roles and is_list(opts) do
supervisor = Keyword.get(opts, :session_supervisor, Sender.MediaSessionSupervisor)
supervisor =
Keyword.get(opts, :session_supervisor, TribeOne.TribesPlugin.Sender.MediaSessionSupervisor)
DynamicSupervisor.start_child(
supervisor,
@@ -88,7 +89,7 @@ defmodule Sender.MediaSession do
@spec whereis(role(), String.t()) :: pid() | nil
def whereis(role, generation_id) when role in @roles and is_binary(generation_id) do
case Registry.lookup(Sender.MediaSessionRegistry, {role, generation_id}) do
case Registry.lookup(TribeOne.TribesPlugin.Sender.MediaSessionRegistry, {role, generation_id}) do
[{pid, _value}] -> pid
[] -> nil
end
@@ -106,7 +107,9 @@ defmodule Sender.MediaSession do
role = Keyword.fetch!(opts, :role)
GenServer.start_link(__MODULE__, opts,
name: {:via, Registry, {Sender.MediaSessionRegistry, {role, generation.id}}}
name:
{:via, Registry,
{TribeOne.TribesPlugin.Sender.MediaSessionRegistry, {role, generation.id}}}
)
end
@@ -199,7 +202,8 @@ defmodule Sender.MediaSession do
end
defp backend_opts(opts) do
backend_supervisor = Keyword.get(opts, :backend_supervisor, Sender.MediaBackendSupervisor)
backend_supervisor =
Keyword.get(opts, :backend_supervisor, TribeOne.TribesPlugin.Sender.MediaBackendSupervisor)
opts
|> Keyword.drop(@session_keys)
+3 -3
View File
@@ -1,12 +1,12 @@
defmodule Sender.Playback do
defmodule TribeOne.TribesPlugin.Sender.Playback do
@moduledoc """
Builds browser playback metadata from synced stream state.
"""
require Ash.Query
alias Sender.Streaming
alias Sender.Streaming.{Rendition, Stream, StreamGeneration}
alias TribeOne.TribesPlugin.Sender.Streaming
alias TribeOne.TribesPlugin.Sender.Streaming.{Rendition, Stream, StreamGeneration}
@hls_mime "application/vnd.apple.mpegurl"
+18 -18
View File
@@ -1,4 +1,4 @@
defmodule Sender.Plugin do
defmodule TribeOne.TribesPlugin.Sender.Plugin do
@moduledoc """
Tribes plugin entry point.
"""
@@ -21,20 +21,20 @@ defmodule Sender.Plugin do
pages: [
%{
path: "/sender",
live_view: SenderWeb.HomeLive,
live_view: TribeOne.TribesPlugin.SenderWeb.HomeLive,
layout: nil
}
],
children: Sender.Application.children(),
children: TribeOne.TribesPlugin.Sender.Application.children(),
api_routes: [
{"streams", SenderWeb.StreamingAPIPlug},
{"player-events", SenderWeb.StreamingAPIPlug}
{"streams", TribeOne.TribesPlugin.SenderWeb.StreamingAPIPlug},
{"player-events", TribeOne.TribesPlugin.SenderWeb.StreamingAPIPlug}
],
management_methods: [
%{
name: "capabilities",
version: "1",
module: Sender.Management.Capabilities,
module: TribeOne.TribesPlugin.Sender.Management.Capabilities,
action: :call,
auth: :admin,
description: "Describe Sender management capabilities."
@@ -42,7 +42,7 @@ defmodule Sender.Plugin do
%{
name: "stream.get_default",
version: "1",
module: Sender.Management.Stream,
module: TribeOne.TribesPlugin.Sender.Management.Stream,
action: :get_default,
auth: :admin,
description: "Fetch the default Sender stream."
@@ -50,7 +50,7 @@ defmodule Sender.Plugin do
%{
name: "stream.ensure_default",
version: "1",
module: Sender.Management.Stream,
module: TribeOne.TribesPlugin.Sender.Management.Stream,
action: :ensure_default,
auth: :admin,
description: "Create the default Sender stream if it does not exist."
@@ -58,7 +58,7 @@ defmodule Sender.Plugin do
%{
name: "stream.update_default",
version: "1",
module: Sender.Management.Stream,
module: TribeOne.TribesPlugin.Sender.Management.Stream,
action: :update_default,
auth: :admin,
description: "Update the default Sender stream."
@@ -66,7 +66,7 @@ defmodule Sender.Plugin do
%{
name: "stream_key.create",
version: "1",
module: Sender.Management.StreamKey,
module: TribeOne.TribesPlugin.Sender.Management.StreamKey,
action: :create,
auth: :admin,
description: "Create an OBS stream key and return it once.",
@@ -75,7 +75,7 @@ defmodule Sender.Plugin do
%{
name: "media_endpoints.upsert",
version: "1",
module: Sender.Management.Topology,
module: TribeOne.TribesPlugin.Sender.Management.Topology,
action: :upsert_endpoint,
auth: :admin,
description: "Create or update a Sender media endpoint."
@@ -83,7 +83,7 @@ defmodule Sender.Plugin do
%{
name: "renditions.upsert",
version: "1",
module: Sender.Management.Topology,
module: TribeOne.TribesPlugin.Sender.Management.Topology,
action: :upsert_rendition,
auth: :admin,
description: "Create or update Sender rendition metadata."
@@ -91,7 +91,7 @@ defmodule Sender.Plugin do
%{
name: "stream.start",
version: "1",
module: Sender.Management.StreamLifecycle,
module: TribeOne.TribesPlugin.Sender.Management.StreamLifecycle,
action: :start,
auth: :admin,
description: "Start the default stream in origin_ingest or hls_edge mode."
@@ -99,7 +99,7 @@ defmodule Sender.Plugin do
%{
name: "stream.stop",
version: "1",
module: Sender.Management.StreamLifecycle,
module: TribeOne.TribesPlugin.Sender.Management.StreamLifecycle,
action: :stop,
auth: :admin,
description: "Stop the active Sender stream generation."
@@ -107,7 +107,7 @@ defmodule Sender.Plugin do
%{
name: "stream.status",
version: "1",
module: Sender.Management.StreamLifecycle,
module: TribeOne.TribesPlugin.Sender.Management.StreamLifecycle,
action: :status,
auth: :admin,
description: "Return current Sender stream and session status."
@@ -115,7 +115,7 @@ defmodule Sender.Plugin do
%{
name: "endpoint_snapshots.report",
version: "1",
module: Sender.Management.EndpointSnapshots,
module: TribeOne.TribesPlugin.Sender.Management.EndpointSnapshots,
action: :report,
auth: :admin,
description: "Report current endpoint stats for playback routing."
@@ -150,10 +150,10 @@ defmodule Sender.Plugin do
}
],
plugs: [
{"/sender/hls", SenderWeb.HLSPlug, []}
{"/sender/hls", TribeOne.TribesPlugin.SenderWeb.HLSPlug, []}
],
hooks: %{},
ash_domains: [Sender.Streaming],
ash_domains: [TribeOne.TribesPlugin.Sender.Streaming],
config_schema: %{
title: "Sender",
description: "RTMP ingest and HLS playback defaults for this Tribes cluster.",
+3 -3
View File
@@ -1,9 +1,9 @@
defmodule Sender.SnapshotPublisher do
defmodule TribeOne.TribesPlugin.Sender.SnapshotPublisher do
@moduledoc """
Publishes coarse node-local runtime stats into synced endpoint snapshots.
"""
alias Sender.Streaming
alias TribeOne.TribesPlugin.Sender.Streaming
@stale_after_ms 15_000
@@ -17,7 +17,7 @@ defmodule Sender.SnapshotPublisher do
local =
Keyword.get_lazy(opts, :local_stats, fn ->
Sender.Stats.snapshot(endpoint_id, stream_id, generation_id)
TribeOne.TribesPlugin.Sender.Stats.snapshot(endpoint_id, stream_id, generation_id)
end)
health = derive_health(local, attrs, opts)
+2 -2
View File
@@ -1,9 +1,9 @@
defmodule Sender.SnapshotWorker do
defmodule TribeOne.TribesPlugin.Sender.SnapshotWorker do
@moduledoc false
use GenServer
alias Sender.SnapshotPublisher
alias TribeOne.TribesPlugin.Sender.SnapshotPublisher
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
+1 -1
View File
@@ -1,4 +1,4 @@
defmodule Sender.Stats do
defmodule TribeOne.TribesPlugin.Sender.Stats do
@moduledoc """
Node-local live streaming counters.
+8 -8
View File
@@ -1,4 +1,4 @@
defmodule Sender.Streaming do
defmodule TribeOne.TribesPlugin.Sender.Streaming do
@moduledoc """
Ash domain for Sender's stream topology and operational metadata.
"""
@@ -7,44 +7,44 @@ defmodule Sender.Streaming do
otp_app: :sender
resources do
resource Sender.Streaming.Stream do
resource TribeOne.TribesPlugin.Sender.Streaming.Stream do
define(:create_stream, action: :create)
define(:update_stream, action: :update)
define(:list_streams, action: :read)
define(:get_stream, action: :by_id, args: [:id])
end
resource Sender.Streaming.StreamKey do
resource TribeOne.TribesPlugin.Sender.Streaming.StreamKey do
define(:create_stream_key, action: :create)
define(:list_stream_keys, action: :read)
end
resource Sender.Streaming.StreamGeneration do
resource TribeOne.TribesPlugin.Sender.Streaming.StreamGeneration do
define(:create_stream_generation, action: :create)
define(:update_stream_generation, action: :update)
define(:get_stream_generation, action: :by_id, args: [:id])
define(:list_stream_generations, action: :read)
end
resource Sender.Streaming.MediaEndpoint do
resource TribeOne.TribesPlugin.Sender.Streaming.MediaEndpoint do
define(:upsert_media_endpoint, action: :upsert)
define(:list_media_endpoints, action: :read)
define(:get_media_endpoint, action: :by_id, args: [:id])
end
resource Sender.Streaming.Rendition do
resource TribeOne.TribesPlugin.Sender.Streaming.Rendition do
define(:upsert_rendition, action: :upsert)
define(:update_rendition, action: :update)
define(:get_rendition, action: :by_id, args: [:id])
define(:list_renditions, action: :read)
end
resource Sender.Streaming.EndpointSnapshot do
resource TribeOne.TribesPlugin.Sender.Streaming.EndpointSnapshot do
define(:upsert_endpoint_snapshot, action: :upsert)
define(:list_endpoint_snapshots, action: :read)
end
resource Sender.Streaming.EndpointHistory do
resource TribeOne.TribesPlugin.Sender.Streaming.EndpointHistory do
define(:upsert_endpoint_history, action: :upsert)
define(:list_endpoint_histories, action: :read)
end
+2 -2
View File
@@ -1,9 +1,9 @@
defmodule Sender.Streaming.EndpointHistory do
defmodule TribeOne.TribesPlugin.Sender.Streaming.EndpointHistory do
@moduledoc false
use Ash.Resource,
otp_app: :sender,
domain: Sender.Streaming,
domain: TribeOne.TribesPlugin.Sender.Streaming,
data_layer: AshPostgres.DataLayer,
authorizers: [Ash.Policy.Authorizer],
extensions: [AshNostrSync]
+2 -2
View File
@@ -1,9 +1,9 @@
defmodule Sender.Streaming.EndpointSnapshot do
defmodule TribeOne.TribesPlugin.Sender.Streaming.EndpointSnapshot do
@moduledoc false
use Ash.Resource,
otp_app: :sender,
domain: Sender.Streaming,
domain: TribeOne.TribesPlugin.Sender.Streaming,
data_layer: AshPostgres.DataLayer,
authorizers: [Ash.Policy.Authorizer],
extensions: [AshNostrSync]
+2 -2
View File
@@ -1,9 +1,9 @@
defmodule Sender.Streaming.MediaEndpoint do
defmodule TribeOne.TribesPlugin.Sender.Streaming.MediaEndpoint do
@moduledoc false
use Ash.Resource,
otp_app: :sender,
domain: Sender.Streaming,
domain: TribeOne.TribesPlugin.Sender.Streaming,
data_layer: AshPostgres.DataLayer,
authorizers: [Ash.Policy.Authorizer],
extensions: [AshNostrSync]
+2 -2
View File
@@ -1,9 +1,9 @@
defmodule Sender.Streaming.Rendition do
defmodule TribeOne.TribesPlugin.Sender.Streaming.Rendition do
@moduledoc false
use Ash.Resource,
otp_app: :sender,
domain: Sender.Streaming,
domain: TribeOne.TribesPlugin.Sender.Streaming,
data_layer: AshPostgres.DataLayer,
authorizers: [Ash.Policy.Authorizer],
extensions: [AshNostrSync]
+1 -1
View File
@@ -1,4 +1,4 @@
defmodule Sender.Streaming.ResourceHelpers do
defmodule TribeOne.TribesPlugin.Sender.Streaming.ResourceHelpers do
@moduledoc false
def system_private_context(purpose \\ :sender_streaming) do
+2 -2
View File
@@ -1,9 +1,9 @@
defmodule Sender.Streaming.Stream do
defmodule TribeOne.TribesPlugin.Sender.Streaming.Stream do
@moduledoc false
use Ash.Resource,
otp_app: :sender,
domain: Sender.Streaming,
domain: TribeOne.TribesPlugin.Sender.Streaming,
data_layer: AshPostgres.DataLayer,
authorizers: [Ash.Policy.Authorizer],
extensions: [AshNostrSync]
+2 -2
View File
@@ -1,9 +1,9 @@
defmodule Sender.Streaming.StreamGeneration do
defmodule TribeOne.TribesPlugin.Sender.Streaming.StreamGeneration do
@moduledoc false
use Ash.Resource,
otp_app: :sender,
domain: Sender.Streaming,
domain: TribeOne.TribesPlugin.Sender.Streaming,
data_layer: AshPostgres.DataLayer,
authorizers: [Ash.Policy.Authorizer],
extensions: [AshNostrSync]
+2 -2
View File
@@ -1,9 +1,9 @@
defmodule Sender.Streaming.StreamKey do
defmodule TribeOne.TribesPlugin.Sender.Streaming.StreamKey do
@moduledoc false
use Ash.Resource,
otp_app: :sender,
domain: Sender.Streaming,
domain: TribeOne.TribesPlugin.Sender.Streaming,
data_layer: AshPostgres.DataLayer,
authorizers: [Ash.Policy.Authorizer],
extensions: [AshNostrSync]
+1 -1
View File
@@ -1,4 +1,4 @@
defmodule Sender.Streaming.StreamKeys do
defmodule TribeOne.TribesPlugin.Sender.Streaming.StreamKeys do
@moduledoc """
Helpers for generating and verifying RTMP stream keys.
"""
+2 -2
View File
@@ -1,9 +1,9 @@
defmodule SenderWeb.HLSPlug do
defmodule TribeOne.TribesPlugin.SenderWeb.HLSPlug do
@moduledoc false
import Plug.Conn
alias Sender.{HLS, Stats}
alias TribeOne.TribesPlugin.Sender.{HLS, Stats}
alias Tribes.RequestInfo
def init(opts), do: opts
+20 -28
View File
@@ -1,4 +1,4 @@
defmodule SenderWeb.HomeLive do
defmodule TribeOne.TribesPlugin.SenderWeb.HomeLive do
@moduledoc """
Example LiveView page for the plugin.
@@ -14,37 +14,36 @@ defmodule SenderWeb.HomeLive do
on_mount({Tribes.Plugin.LiveUserAuth, :live_user_optional})
alias Sender.ChatIntegration
alias TribeOne.TribesPlugin.Sender.ChatIntegration
alias Tribes.Plugin.Layouts
@chat_component_id "sender-stream-chat-panel"
def mount(_params, _session, socket) do
chat_component =
case ChatIntegration.component_module() do
{:ok, component} -> component
chat_slug = ChatIntegration.channel_slug()
chat_surface =
case ChatIntegration.embedded_panel(chat_slug,
id: @chat_component_id,
backend: :public_sync,
current_user: socket.assigns[:current_user],
session_privkey: socket.assigns[:session_privkey]
) do
{:ok, surface} -> surface
:unavailable -> nil
end
{:ok,
socket
|> assign(:page_title, "Streaming")
|> assign(:chat_component, chat_component)
|> assign(:chat_component_id, @chat_component_id)
|> assign(:chat_slug, ChatIntegration.channel_slug())}
|> assign(:chat_surface, chat_surface)
|> assign(:chat_slug, chat_slug)}
end
def handle_info(
{:aether_chat, :message, message},
%{assigns: %{chat_component: component}} = socket
)
when is_atom(component) do
send_update(component, id: socket.assigns.chat_component_id, incoming_message: message)
{:noreply, socket}
def handle_info(message, socket) do
Tribes.Plugin.Surface.handle_info(socket, socket.assigns[:chat_surface], message)
end
def handle_info({:aether_chat, :message, _message}, socket), do: {:noreply, socket}
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}>
@@ -61,8 +60,8 @@ defmodule SenderWeb.HomeLive do
<div class={[
"grid gap-4",
@chat_component && "xl:grid-cols-[minmax(0,1fr)_24rem]",
!@chat_component && "grid-cols-1"
@chat_surface && "xl:grid-cols-[minmax(0,1fr)_24rem]",
!@chat_surface && "grid-cols-1"
]}>
<section
id="sender-player"
@@ -73,18 +72,11 @@ defmodule SenderWeb.HomeLive do
</section>
<section
:if={@chat_component}
:if={@chat_surface}
id="sender-stream-chat"
class="min-h-[32rem] overflow-hidden rounded-box border border-base-300 bg-base-100"
>
<.live_component
module={@chat_component}
id={@chat_component_id}
slug={@chat_slug}
backend={:public_sync}
embedded?={true}
current_user={@current_user}
/>
<Tribes.Plugin.Surface.render surface={@chat_surface} />
</section>
</div>
</div>
+2 -2
View File
@@ -1,9 +1,9 @@
defmodule SenderWeb.StreamingAPIPlug do
defmodule TribeOne.TribesPlugin.SenderWeb.StreamingAPIPlug do
@moduledoc false
import Plug.Conn
alias Sender.{Playback, Stats}
alias TribeOne.TribesPlugin.Sender.{Playback, Stats}
alias Tribes.RequestInfo
def init(opts), do: opts
-5
View File
@@ -1,5 +0,0 @@
defmodule Tribes.Plugins.Sender.Plugin do
@moduledoc false
defdelegate register(context), to: Sender.Plugin
end
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "sender",
"version": "0.1.0",
"description": "RTMP ingest and HLS/LL-HLS streaming for Tribes",
"entry_module": "Tribes.Plugins.Sender.Plugin",
"entry_module": "TribeOne.TribesPlugin.Sender.Plugin",
"host_api": "1",
"otp_app": "sender",
"provides": ["sender@1"],
+2 -2
View File
@@ -1,4 +1,4 @@
defmodule Sender.MixProject do
defmodule TribeOne.TribesPlugin.Sender.MixProject do
use Mix.Project
def project do
@@ -23,7 +23,7 @@ defmodule Sender.MixProject do
def application do
[
extra_applications: [:logger],
mod: {Sender.Application, []}
mod: {TribeOne.TribesPlugin.Sender.Application, []}
]
end
+1 -1
View File
@@ -61,7 +61,7 @@
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"owl": {:hex, :owl, "0.13.0", "26010e066d5992774268f3163506972ddac0a7e77bfe57fa42a250f24d6b876e", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "59bf9d11ce37a4db98f57cb68fbfd61593bf419ec4ed302852b6683d3d2f7475"},
"parrhesia": {:git, "https://git.teralink.net/tribes/parrhesia.git", "0bd51b338ad3445830c00ede644270cf9c9d5abc", [ref: "0bd51b338ad3445830c00ede644270cf9c9d5abc"]},
"parrhesia": {:hex, :parrhesia, "0.14.0", "7ff96a6d645740a19b66567c3346162735ba821a90377616e74493637c35195e", [:mix], [{:bandit, "~> 1.5", [hex: :bandit, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:lib_secp256k1, "~> 0.7", [hex: :lib_secp256k1, repo: "hexpm", optional: false]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus, "~> 1.1", [hex: :telemetry_metrics_prometheus, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5", [hex: :websock_adapter, repo: "hexpm", optional: false]}, {:websockex, "~> 0.4", [hex: :websockex, repo: "hexpm", optional: false]}], "hexpm", "2bda91e6f385b6990465f2ccc9e8e0266105a31f72e2b466ae247f846ec3ef28"},
"phoenix": {:hex, :phoenix, "1.8.5", "919db335247e6d4891764dc3063415b0d2457641c5f9b3751b5df03d8e20bbcf", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "83b2bb125127e02e9f475c8e3e92736325b5b01b0b9b05407bcb4083b7a32485"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"},
"phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
@@ -1,4 +1,4 @@
defmodule Sender.Repo.Migrations.CreateSenderStreamingTables do
defmodule TribeOne.TribesPlugin.Sender.Repo.Migrations.CreateSenderStreamingTables do
use Ecto.Migration
def change do
+2 -2
View File
@@ -1,4 +1,4 @@
defmodule Sender.E2ERunner do
defmodule TribeOne.TribesPlugin.Sender.E2ERunner do
alias Parrhesia.API.Auth
@admin_pubkey "985826ac4ec99f6304785ebfaa303ec42973653a682cfa0a06caf9fec662eaa7"
@@ -637,4 +637,4 @@ defmodule Sender.E2ERunner do
end
end
Sender.E2ERunner.main(System.argv())
TribeOne.TribesPlugin.Sender.E2ERunner.main(System.argv())
+3 -3
View File
@@ -1,8 +1,8 @@
defmodule Sender.FFmpegBackendTest do
defmodule TribeOne.TribesPlugin.Sender.FFmpegBackendTest do
use ExUnit.Case, async: false
alias Sender.MediaBackend.FFmpeg
alias Sender.Streaming.StreamGeneration
alias TribeOne.TribesPlugin.Sender.MediaBackend.FFmpeg
alias TribeOne.TribesPlugin.Sender.Streaming.StreamGeneration
setup_all do
assert System.find_executable("ffmpeg"), "ffmpeg must be available on PATH"
+3 -3
View File
@@ -1,8 +1,8 @@
defmodule Sender.HLSCleanupTest do
defmodule TribeOne.TribesPlugin.Sender.HLSCleanupTest do
use ExUnit.Case, async: true
alias Sender.HLSCleanup
alias Sender.Streaming.StreamGeneration
alias TribeOne.TribesPlugin.Sender.HLSCleanup
alias TribeOne.TribesPlugin.Sender.Streaming.StreamGeneration
test "removes a stopped generation spool after the retention window" do
stream_id = Ash.UUID.generate()
+3 -3
View File
@@ -1,8 +1,8 @@
defmodule Sender.HLSPullThroughTest do
defmodule TribeOne.TribesPlugin.Sender.HLSPullThroughTest do
use ExUnit.Case, async: true
alias Sender.MediaBackend.HLSPullThrough.Worker
alias Sender.Streaming.StreamGeneration
alias TribeOne.TribesPlugin.Sender.MediaBackend.HLSPullThrough.Worker
alias TribeOne.TribesPlugin.Sender.Streaming.StreamGeneration
test "mirrors a live playlist and segments into the local spool" do
stream_id = Ash.UUID.generate()
+38 -20
View File
@@ -1,21 +1,5 @@
unless Code.ensure_loaded?(AetherWeb.ChatPanelComponent) do
defmodule AetherWeb.ChatPanelComponent do
use Phoenix.LiveComponent
@impl true
def update(assigns, socket), do: {:ok, assign(socket, assigns)}
@impl true
def render(assigns) do
~H"""
<div id="dummy-aether-chat-panel" data-slug={@slug}>Chat component</div>
"""
end
end
end
defmodule Sender.HomePageTest do
use Tribes.PluginTest.PageCase, plugin: Tribes.Plugins.Sender.Plugin
defmodule TribeOne.TribesPlugin.Sender.HomePageTest do
use Tribes.PluginTest.PageCase, plugin: TribeOne.TribesPlugin.Sender.Plugin
test "renders the plugin home page for signed-out visitors", %{conn: conn} do
{:ok, view, html} = live(conn, "/sender")
@@ -31,7 +15,7 @@ defmodule Sender.HomePageTest do
{:ok, view, _html} = live(conn, "/sender")
assert has_element?(view, "#sender-stream-chat")
assert has_element?(view, ~s|#dummy-aether-chat-panel[data-slug="sender-stream-default"]|)
assert has_element?(view, ~s|#dummy-chat-panel[data-slug="sender-stream-default"]|)
refute has_element?(view, "#sender-chat-frame")
end
@@ -41,6 +25,39 @@ defmodule Sender.HomePageTest do
assert html =~ "Streaming"
end
defmodule ChatProvider do
@behaviour Tribes.Capabilities.Chat.V1
@impl true
def embedded_panel(slug, _opts) do
{:ok,
Tribes.Plugin.Surface.live_component(
TribeOne.TribesPlugin.Sender.HomePageTest.ChatComponent,
"sender-chat-test-panel",
%{slug: slug},
capability: "chat@1",
provider: __MODULE__
)}
end
@impl true
def standalone_path(slug), do: "/chat/" <> slug
end
defmodule ChatComponent do
use Phoenix.LiveComponent
@impl true
def update(assigns, socket), do: {:ok, assign(socket, assigns)}
@impl true
def render(assigns) do
~H"""
<div id="dummy-chat-panel" data-slug={@slug}>Chat component</div>
"""
end
end
defp register_chat_provider! do
name = "sender_chat_test"
_ = Tribes.PluginRegistry.unregister_plugin(name)
@@ -52,7 +69,8 @@ defmodule Sender.HomePageTest do
name: name,
version: "0.1.0",
provides: ["chat@1"],
provider_priority: -100
provider_priority: -100,
entry_module: __MODULE__.ChatProvider
},
File.cwd!()
)
+4 -4
View File
@@ -1,8 +1,8 @@
defmodule Sender.LocalStreamsTest do
use Sender.DataCase, async: false
defmodule TribeOne.TribesPlugin.Sender.LocalStreamsTest do
use TribeOne.TribesPlugin.Sender.DataCase, async: false
alias Sender.LocalStreams
alias Sender.Management
alias TribeOne.TribesPlugin.Sender.LocalStreams
alias TribeOne.TribesPlugin.Sender.Management
alias Tribes.Cluster
setup_all do
+41 -23
View File
@@ -1,13 +1,21 @@
defmodule Sender.ManagementTest do
use Sender.DataCase, async: false
defmodule TribeOne.TribesPlugin.Sender.ManagementTest do
use TribeOne.TribesPlugin.Sender.DataCase, async: false
import ExUnit.CaptureLog
alias Sender.{EdgeSessionReconciler, MediaSession}
alias Sender.Management
alias Sender.Management.{EndpointSnapshots, Stream, StreamKey, StreamLifecycle, Topology}
alias Sender.Streaming
alias Sender.Streaming.StreamKeys
alias TribeOne.TribesPlugin.Sender.{EdgeSessionReconciler, MediaSession}
alias TribeOne.TribesPlugin.Sender.Management
alias TribeOne.TribesPlugin.Sender.Management.{
EndpointSnapshots,
Stream,
StreamKey,
StreamLifecycle,
Topology
}
alias TribeOne.TribesPlugin.Sender.Streaming
alias TribeOne.TribesPlugin.Sender.Streaming.StreamKeys
alias Tribes.Cluster
alias Tribes.Plugin.ManagementContext
@@ -20,7 +28,7 @@ defmodule Sender.ManagementTest do
}
defmodule EdgeBackend do
@behaviour Sender.MediaBackend
@behaviour TribeOne.TribesPlugin.Sender.MediaBackend
def start_ingest(_generation, opts) do
:persistent_term.put({__MODULE__, :last_opts}, opts)
@@ -41,7 +49,7 @@ defmodule Sender.ManagementTest do
end
test "plugin spec declares the Legion-facing management methods" do
spec = Sender.Plugin.register(%{})
spec = TribeOne.TribesPlugin.Sender.Plugin.register(%{})
methods = Map.new(spec.management_methods, &{&1.name, &1})
assert methods |> Map.keys() |> Enum.sort() == [
@@ -58,31 +66,33 @@ defmodule Sender.ManagementTest do
"stream_key.create"
]
assert methods["stream.start"].module == Sender.Management.StreamLifecycle
assert methods["stream.start"].module ==
TribeOne.TribesPlugin.Sender.Management.StreamLifecycle
assert methods["stream.start"].action == :start
assert methods["stream_key.create"].sensitive_response?
end
test "plugin spec exposes the Sender runtime supervision children" do
spec = Sender.Plugin.register(%{})
spec = TribeOne.TribesPlugin.Sender.Plugin.register(%{})
child_ids =
spec.children
|> Enum.map(&Supervisor.child_spec(&1, []))
|> Enum.map(& &1.id)
assert Sender.Stats in child_ids
assert Sender.SnapshotWorker in child_ids
assert Sender.HLSCleanup in child_ids
assert Sender.MediaSessionRegistry in child_ids
assert Sender.MediaSessionSupervisor in child_ids
assert Sender.MediaBackendSupervisor in child_ids
assert Sender.HLSReadinessSupervisor in child_ids
assert Sender.EdgeSessionReconciler in child_ids
assert TribeOne.TribesPlugin.Sender.Stats in child_ids
assert TribeOne.TribesPlugin.Sender.SnapshotWorker in child_ids
assert TribeOne.TribesPlugin.Sender.HLSCleanup in child_ids
assert TribeOne.TribesPlugin.Sender.MediaSessionRegistry in child_ids
assert TribeOne.TribesPlugin.Sender.MediaSessionSupervisor in child_ids
assert TribeOne.TribesPlugin.Sender.MediaBackendSupervisor in child_ids
assert TribeOne.TribesPlugin.Sender.HLSReadinessSupervisor in child_ids
assert TribeOne.TribesPlugin.Sender.EdgeSessionReconciler in child_ids
end
test "plugin spec declares sender-owned stream rollup metrics" do
spec = Sender.Plugin.register(%{})
spec = TribeOne.TribesPlugin.Sender.Plugin.register(%{})
metrics = Map.new(spec.metrics, &{&1.name, &1})
assert metrics["viewer_count"].query =~ "vinyl_hls_active_viewers"
@@ -211,9 +221,17 @@ defmodule Sender.ManagementTest do
spool_root =
Path.join(System.tmp_dir!(), "sender-management-test-#{System.unique_integer([:positive])}")
previous_hls = Application.get_env(:sender, Sender.HLS, [])
Application.put_env(:sender, Sender.HLS, Keyword.put(previous_hls, :spool_root, spool_root))
on_exit(fn -> Application.put_env(:sender, Sender.HLS, previous_hls) end)
previous_hls = Application.get_env(:sender, TribeOne.TribesPlugin.Sender.HLS, [])
Application.put_env(
:sender,
TribeOne.TribesPlugin.Sender.HLS,
Keyword.put(previous_hls, :spool_root, spool_root)
)
on_exit(fn ->
Application.put_env(:sender, TribeOne.TribesPlugin.Sender.HLS, previous_hls)
end)
assert {:ok, %{"endpoint" => endpoint}} =
Topology.upsert_endpoint(@context, %{
+7 -7
View File
@@ -1,5 +1,5 @@
defmodule Sender.MediaSessionTest.DisconnectingBackend do
@behaviour Sender.MediaBackend
defmodule TribeOne.TribesPlugin.Sender.MediaSessionTest.DisconnectingBackend do
@behaviour TribeOne.TribesPlugin.Sender.MediaBackend
@impl true
def start_ingest(generation, opts) do
@@ -32,11 +32,11 @@ defmodule Sender.MediaSessionTest.DisconnectingBackend do
end
end
defmodule Sender.MediaSessionTest do
defmodule TribeOne.TribesPlugin.Sender.MediaSessionTest do
use ExUnit.Case, async: true
alias Sender.MediaSession
alias Sender.Streaming.StreamGeneration
alias TribeOne.TribesPlugin.Sender.MediaSession
alias TribeOne.TribesPlugin.Sender.Streaming.StreamGeneration
test "origin_ingest keeps the session alive across backend disconnects" do
generation = generation()
@@ -44,7 +44,7 @@ defmodule Sender.MediaSessionTest do
assert {:ok, pid} =
MediaSession.start_origin_ingest(generation,
backend: Sender.MediaSessionTest.DisconnectingBackend,
backend: TribeOne.TribesPlugin.Sender.MediaSessionTest.DisconnectingBackend,
owner: self(),
exit_after_ms: 10,
backend_restart_delay_ms: 10,
@@ -64,7 +64,7 @@ defmodule Sender.MediaSessionTest do
assert {:ok, pid} =
MediaSession.start_origin_ingest(generation,
backend: Sender.MediaSessionTest.DisconnectingBackend,
backend: TribeOne.TribesPlugin.Sender.MediaSessionTest.DisconnectingBackend,
owner: self(),
exit_after_ms: 10,
backend_restart_delay_ms: 1_000
+37 -17
View File
@@ -1,7 +1,7 @@
defmodule Sender.PlaybackAPITest do
use Tribes.PluginTest.PageCase, plugin: Tribes.Plugins.Sender.Plugin
defmodule TribeOne.TribesPlugin.Sender.PlaybackAPITest do
use Tribes.PluginTest.PageCase, plugin: TribeOne.TribesPlugin.Sender.Plugin
alias Sender.{Stats, Streaming}
alias TribeOne.TribesPlugin.Sender.{Stats, Streaming}
@ash_opts [authorize?: false, context: %{ash_nostr_sync_skip_publish: true}]
@@ -60,10 +60,15 @@ defmodule Sender.PlaybackAPITest do
File.mkdir_p!(path)
File.write!(Path.join(path, "media.m3u8"), "#EXTM3U\n")
previous = Application.get_env(:sender, Sender.HLS, [])
Application.put_env(:sender, Sender.HLS, Keyword.put(previous, :spool_root, root))
previous = Application.get_env(:sender, TribeOne.TribesPlugin.Sender.HLS, [])
on_exit(fn -> Application.put_env(:sender, Sender.HLS, previous) end)
Application.put_env(
:sender,
TribeOne.TribesPlugin.Sender.HLS,
Keyword.put(previous, :spool_root, root)
)
on_exit(fn -> Application.put_env(:sender, TribeOne.TribesPlugin.Sender.HLS, previous) end)
conn =
conn
@@ -85,10 +90,15 @@ defmodule Sender.PlaybackAPITest do
File.mkdir_p!(path)
File.write!(Path.join(path, "media.m3u8"), "#EXTM3U\n")
previous = Application.get_env(:sender, Sender.HLS, [])
Application.put_env(:sender, Sender.HLS, Keyword.put(previous, :spool_root, root))
previous = Application.get_env(:sender, TribeOne.TribesPlugin.Sender.HLS, [])
on_exit(fn -> Application.put_env(:sender, Sender.HLS, previous) end)
Application.put_env(
:sender,
TribeOne.TribesPlugin.Sender.HLS,
Keyword.put(previous, :spool_root, root)
)
on_exit(fn -> Application.put_env(:sender, TribeOne.TribesPlugin.Sender.HLS, previous) end)
conn
|> Map.put(:remote_ip, {203, 0, 113, 20})
@@ -109,10 +119,15 @@ defmodule Sender.PlaybackAPITest do
File.mkdir_p!(path)
File.write!(Path.join(path, "media.m3u8"), "#EXTM3U\n")
previous = Application.get_env(:sender, Sender.HLS, [])
Application.put_env(:sender, Sender.HLS, Keyword.put(previous, :spool_root, root))
previous = Application.get_env(:sender, TribeOne.TribesPlugin.Sender.HLS, [])
on_exit(fn -> Application.put_env(:sender, Sender.HLS, previous) end)
Application.put_env(
:sender,
TribeOne.TribesPlugin.Sender.HLS,
Keyword.put(previous, :spool_root, root)
)
on_exit(fn -> Application.put_env(:sender, TribeOne.TribesPlugin.Sender.HLS, previous) end)
conn =
conn
@@ -141,7 +156,7 @@ defmodule Sender.PlaybackAPITest do
)
|> Map.put(:remote_ip, {203, 0, 113, 30})
|> put_req_header("content-type", "application/json")
|> SenderWeb.StreamingAPIPlug.call([])
|> TribeOne.TribesPlugin.SenderWeb.StreamingAPIPlug.call([])
assert decoded_response(conn, 202) == %{"ok" => true, "viewer_count" => 1}
assert Stats.viewer_count(stream_id, generation_id) == 1
@@ -165,7 +180,7 @@ defmodule Sender.PlaybackAPITest do
|> Map.put(:remote_ip, {127, 0, 0, 1})
|> put_req_header("content-type", "application/json")
|> put_req_header("x-forwarded-for", "203.0.113.30")
|> SenderWeb.StreamingAPIPlug.call([])
|> TribeOne.TribesPlugin.SenderWeb.StreamingAPIPlug.call([])
assert decoded_response(conn, 202) == %{"ok" => true}
assert Stats.viewer_count(stream_id, generation_id) == 0
@@ -179,10 +194,15 @@ defmodule Sender.PlaybackAPITest do
File.mkdir_p!(path)
File.write!(Path.join(path, "media.m3u8"), "#EXTM3U\n")
previous = Application.get_env(:sender, Sender.HLS, [])
Application.put_env(:sender, Sender.HLS, Keyword.put(previous, :spool_root, root))
previous = Application.get_env(:sender, TribeOne.TribesPlugin.Sender.HLS, [])
on_exit(fn -> Application.put_env(:sender, Sender.HLS, previous) end)
Application.put_env(
:sender,
TribeOne.TribesPlugin.Sender.HLS,
Keyword.put(previous, :spool_root, root)
)
on_exit(fn -> Application.put_env(:sender, TribeOne.TribesPlugin.Sender.HLS, previous) end)
conn = get(conn, "/sender/hls/stream/generation/source/media.m3u8")
+2 -2
View File
@@ -1,3 +1,3 @@
defmodule Sender.PluginContractTest do
use Tribes.PluginTest.ContractTest, plugin: Tribes.Plugins.Sender.Plugin
defmodule TribeOne.TribesPlugin.Sender.PluginContractTest do
use Tribes.PluginTest.ContractTest, plugin: TribeOne.TribesPlugin.Sender.Plugin
end
+4 -4
View File
@@ -1,8 +1,8 @@
defmodule Sender.StatsSnapshotTest do
use Sender.DataCase, async: false
defmodule TribeOne.TribesPlugin.Sender.StatsSnapshotTest do
use TribeOne.TribesPlugin.Sender.DataCase, async: false
alias Sender.{SnapshotPublisher, Stats}
alias Sender.Streaming
alias TribeOne.TribesPlugin.Sender.{SnapshotPublisher, Stats}
alias TribeOne.TribesPlugin.Sender.Streaming
@ash_opts [authorize?: false, context: %{ash_nostr_sync_skip_publish: true}]
+2 -2
View File
@@ -1,7 +1,7 @@
defmodule Sender.StreamKeysTest do
defmodule TribeOne.TribesPlugin.Sender.StreamKeysTest do
use ExUnit.Case, async: true
alias Sender.Streaming.StreamKeys
alias TribeOne.TribesPlugin.Sender.Streaming.StreamKeys
test "generates verifiable stream keys without storing raw secrets" do
key = StreamKeys.generate_key()
+20 -12
View File
@@ -1,19 +1,27 @@
defmodule Sender.StreamingSchemaTest do
use Sender.DataCase, async: false
defmodule TribeOne.TribesPlugin.Sender.StreamingSchemaTest do
use TribeOne.TribesPlugin.Sender.DataCase, async: false
alias Sender.Streaming
alias TribeOne.TribesPlugin.Sender.Streaming
alias Tribes.Cluster
@ash_opts [authorize?: false, context: %{ash_nostr_sync_skip_publish: true}]
test "sender sync resources declare control and bulk lanes" do
assert AshNostrSync.Info.lane(Sender.Streaming.Stream) == :control
assert AshNostrSync.Info.lane(Sender.Streaming.StreamGeneration) == :control
assert AshNostrSync.Info.lane(Sender.Streaming.MediaEndpoint) == :control
assert AshNostrSync.Info.lane(Sender.Streaming.Rendition) == :control
assert AshNostrSync.Info.lane(Sender.Streaming.StreamKey) == :control
assert AshNostrSync.Info.lane(Sender.Streaming.EndpointSnapshot) == :bulk
assert AshNostrSync.Info.lane(Sender.Streaming.EndpointHistory) == :bulk
assert AshNostrSync.Info.lane(TribeOne.TribesPlugin.Sender.Streaming.Stream) == :control
assert AshNostrSync.Info.lane(TribeOne.TribesPlugin.Sender.Streaming.StreamGeneration) ==
:control
assert AshNostrSync.Info.lane(TribeOne.TribesPlugin.Sender.Streaming.MediaEndpoint) ==
:control
assert AshNostrSync.Info.lane(TribeOne.TribesPlugin.Sender.Streaming.Rendition) == :control
assert AshNostrSync.Info.lane(TribeOne.TribesPlugin.Sender.Streaming.StreamKey) == :control
assert AshNostrSync.Info.lane(TribeOne.TribesPlugin.Sender.Streaming.EndpointSnapshot) ==
:bulk
assert AshNostrSync.Info.lane(TribeOne.TribesPlugin.Sender.Streaming.EndpointHistory) == :bulk
end
test "creates the core stream topology records" do
@@ -139,9 +147,9 @@ defmodule Sender.StreamingSchemaTest do
end
test "plugin spec exposes the streaming Ash domain" do
spec = Sender.Plugin.register(%{})
spec = TribeOne.TribesPlugin.Sender.Plugin.register(%{})
assert Sender.Streaming in spec.ash_domains
assert TribeOne.TribesPlugin.Sender.Streaming in spec.ash_domains
end
defp cluster_node(pubkey) do
+2 -2
View File
@@ -1,11 +1,11 @@
defmodule Sender.DataCase do
defmodule TribeOne.TribesPlugin.Sender.DataCase do
@moduledoc false
use ExUnit.CaseTemplate
using do
quote do
import Sender.DataCase
import TribeOne.TribesPlugin.Sender.DataCase
end
end