You've already forked tribes-plugin-sender
forked from tribes/tribes-plugin-template
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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
%{
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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])},
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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" => [
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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.",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -1,4 +1,4 @@
|
||||
defmodule Sender.Stats do
|
||||
defmodule TribeOne.TribesPlugin.Sender.Stats do
|
||||
@moduledoc """
|
||||
Node-local live streaming counters.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
defmodule Sender.Streaming.StreamKeys do
|
||||
defmodule TribeOne.TribesPlugin.Sender.Streaming.StreamKeys do
|
||||
@moduledoc """
|
||||
Helpers for generating and verifying RTMP stream keys.
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
defmodule Tribes.Plugins.Sender.Plugin do
|
||||
@moduledoc false
|
||||
|
||||
defdelegate register(context), to: Sender.Plugin
|
||||
end
|
||||
+1
-1
@@ -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"],
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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!()
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, %{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}]
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user