├── examples ├── live_view │ ├── test │ │ ├── test_helper.exs │ │ ├── webrtc_live_view_web │ │ │ └── controllers │ │ │ │ ├── page_controller_test.exs │ │ │ │ ├── error_json_test.exs │ │ │ │ └── error_html_test.exs │ │ └── support │ │ │ └── conn_case.ex │ ├── priv │ │ └── static │ │ │ ├── favicon.ico │ │ │ ├── robots.txt │ │ │ └── images │ │ │ └── logo.svg │ ├── .formatter.exs │ ├── lib │ │ ├── webrtc_live_view.ex │ │ ├── webrtc_live_view_web │ │ │ ├── controllers │ │ │ │ ├── page_controller.ex │ │ │ │ ├── page_html.ex │ │ │ │ ├── error_json.ex │ │ │ │ └── error_html.ex │ │ │ ├── components │ │ │ │ ├── layouts.ex │ │ │ │ └── layouts │ │ │ │ │ ├── root.html.heex │ │ │ │ │ └── app.html.heex │ │ │ ├── live │ │ │ │ └── home.ex │ │ │ ├── router.ex │ │ │ ├── endpoint.ex │ │ │ └── telemetry.ex │ │ ├── webrtc_live_view │ │ │ ├── pipeline.ex │ │ │ └── application.ex │ │ └── webrtc_live_view_web.ex │ ├── config │ │ ├── prod.exs │ │ ├── test.exs │ │ ├── config.exs │ │ ├── dev.exs │ │ └── runtime.exs │ ├── README.md │ ├── .gitignore │ ├── assets │ │ ├── js │ │ │ └── app.js │ │ └── vendor │ │ │ └── topbar.js │ └── mix.exs ├── phoenix_signaling │ ├── test │ │ ├── test_helper.exs │ │ ├── phoenix_signaling_web │ │ │ └── controllers │ │ │ │ ├── page_controller_test.exs │ │ │ │ ├── error_json_test.exs │ │ │ │ └── error_html_test.exs │ │ └── support │ │ │ └── conn_case.ex │ ├── assets │ │ ├── js │ │ │ ├── app.js │ │ │ └── signaling.js │ │ └── vendor │ │ │ └── topbar.js │ ├── priv │ │ └── static │ │ │ ├── favicon.ico │ │ │ ├── robots.txt │ │ │ └── images │ │ │ └── logo.svg │ ├── lib │ │ ├── phoenix_signaling_web │ │ │ ├── controllers │ │ │ │ ├── page_html │ │ │ │ │ └── home.html.heex │ │ │ │ ├── page_html.ex │ │ │ │ ├── page_controller.ex │ │ │ │ ├── error_json.ex │ │ │ │ └── error_html.ex │ │ │ ├── components │ │ │ │ ├── layouts.ex │ │ │ │ └── layouts │ │ │ │ │ ├── root.html.heex │ │ │ │ │ └── app.html.heex │ │ │ ├── router.ex │ │ │ ├── endpoint.ex │ │ │ └── telemetry.ex │ │ ├── phoenix_signaling.ex │ │ ├── phoenix_signaling │ │ │ └── application.ex │ │ └── phoenix_signaling_web.ex │ ├── .formatter.exs │ ├── README.md │ ├── config │ │ ├── prod.exs │ │ ├── test.exs │ │ ├── config.exs │ │ ├── dev.exs │ │ └── runtime.exs │ ├── .gitignore │ └── mix.exs └── websocket_signaling │ ├── assets │ ├── bbb_vp8.mkv │ ├── file_to_browser │ │ ├── index.html │ │ └── file_to_browser.js │ └── browser_to_file │ │ └── index.html │ ├── browser_to_file.exs │ └── file_to_browser.exs ├── test ├── fixtures │ ├── ref_audio │ ├── ref_video │ ├── input_bbb.mkv │ └── input_bbb_h264.mkv └── test_helper.exs ├── assets ├── index.js ├── player.js └── capture.js ├── .formatter.exs ├── .editorconfig ├── package.json ├── .github ├── ISSUE_TEMPLATE │ └── please--open-new-issues-in-membranefranework-membrane_core.md └── workflows │ ├── on_issue_opened.yaml │ ├── on_pr_opened.yaml │ └── fetch_changes.yml ├── lib └── membrane_webrtc │ ├── app.ex │ ├── phoenix_signaling │ ├── channel.ex │ ├── registry.ex │ └── socket.ex │ ├── utils.ex │ ├── signaling_channel.ex │ ├── phoenix_signaling.ex │ ├── ex_webrtc │ └── utils.ex │ ├── simple_websocket_server.ex │ ├── whip_client.ex │ ├── live │ ├── player.ex │ └── capture.ex │ ├── signaling.ex │ ├── sink.ex │ ├── whip_server.ex │ └── source.ex ├── .circleci └── config.yml ├── mix.exs ├── .gitignore ├── README.md ├── .credo.exs └── LICENSE /examples/live_view/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test/fixtures/ref_audio: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_webrtc_plugin/HEAD/test/fixtures/ref_audio -------------------------------------------------------------------------------- /test/fixtures/ref_video: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_webrtc_plugin/HEAD/test/fixtures/ref_video -------------------------------------------------------------------------------- /test/fixtures/input_bbb.mkv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_webrtc_plugin/HEAD/test/fixtures/input_bbb.mkv -------------------------------------------------------------------------------- /test/fixtures/input_bbb_h264.mkv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_webrtc_plugin/HEAD/test/fixtures/input_bbb_h264.mkv -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | Logger.configure(level: :info) 2 | ExUnit.start(capture_log: false) 3 | 4 | # Logger.configure(level: :debug) 5 | # ExUnit.start() 6 | -------------------------------------------------------------------------------- /examples/live_view/priv/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_webrtc_plugin/HEAD/examples/live_view/priv/static/favicon.ico -------------------------------------------------------------------------------- /examples/websocket_signaling/assets/bbb_vp8.mkv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_webrtc_plugin/HEAD/examples/websocket_signaling/assets/bbb_vp8.mkv -------------------------------------------------------------------------------- /assets/index.js: -------------------------------------------------------------------------------- 1 | import { createCaptureHook } from "./capture.js"; 2 | import { createPlayerHook } from "./player.js"; 3 | 4 | export { createCaptureHook, createPlayerHook }; 5 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/assets/js/app.js: -------------------------------------------------------------------------------- 1 | // If you want to use Phoenixdle method=PUT/DELETE in forms and buttons. 2 | import "phoenix_html" 3 | import "./signaling.js" 4 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/priv/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_webrtc_plugin/HEAD/examples/phoenix_signaling/priv/static/favicon.ico -------------------------------------------------------------------------------- /examples/phoenix_signaling/lib/phoenix_signaling_web/controllers/page_html/home.html.heex: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: [ 3 | "{lib,test,config,examples}/**/*.{ex,exs}", 4 | ".formatter.exs", 5 | "*.exs" 6 | ], 7 | import_deps: [:membrane_core, :phoenix] 8 | ] 9 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:phoenix], 3 | plugins: [Phoenix.LiveView.HTMLFormatter], 4 | inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"] 5 | ] 6 | -------------------------------------------------------------------------------- /examples/live_view/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:phoenix, :membrane_core], 3 | plugins: [Phoenix.LiveView.HTMLFormatter], 4 | inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"] 5 | ] 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | max_line_length = 100 10 | tab_width = 2 11 | trim_trailing_whitespace = true 12 | -------------------------------------------------------------------------------- /examples/live_view/priv/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/priv/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /examples/live_view/lib/webrtc_live_view.ex: -------------------------------------------------------------------------------- 1 | defmodule WebrtcLiveView do 2 | @moduledoc """ 3 | WebrtcLiveView keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /examples/live_view/test/webrtc_live_view_web/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule WebrtcLiveViewWeb.PageControllerTest do 2 | use WebrtcLiveViewWeb.ConnCase 3 | 4 | test "GET /", %{conn: conn} do 5 | conn = get(conn, ~p"/") 6 | assert html_response(conn, 200) =~ "Peace of mind from prototype to production" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /examples/live_view/lib/webrtc_live_view_web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule WebrtcLiveViewWeb.PageController do 2 | use WebrtcLiveViewWeb, :controller 3 | 4 | def home(conn, _params) do 5 | # The home page is often custom made, 6 | # so skip the default app layout. 7 | render(conn, :home, layout: false) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/lib/phoenix_signaling.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixSignaling do 2 | @moduledoc """ 3 | PhoenixSignaling keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/test/phoenix_signaling_web/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixSignalingWeb.PageControllerTest do 2 | use PhoenixSignalingWeb.ConnCase 3 | 4 | test "GET /", %{conn: conn} do 5 | conn = get(conn, ~p"/") 6 | assert html_response(conn, 200) =~ "Peace of mind from prototype to production" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /examples/live_view/lib/webrtc_live_view_web/controllers/page_html.ex: -------------------------------------------------------------------------------- 1 | defmodule WebrtcLiveViewWeb.PageHTML do 2 | @moduledoc """ 3 | This module contains pages rendered by PageController. 4 | 5 | See the `page_html` directory for all templates available. 6 | """ 7 | use WebrtcLiveViewWeb, :html 8 | 9 | embed_templates "page_html/*" 10 | end 11 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/lib/phoenix_signaling_web/controllers/page_html.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixSignalingWeb.PageHTML do 2 | @moduledoc """ 3 | This module contains pages rendered by PageController. 4 | 5 | See the `page_html` directory for all templates available. 6 | """ 7 | use PhoenixSignalingWeb, :html 8 | 9 | embed_templates("page_html/*") 10 | end 11 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/README.md: -------------------------------------------------------------------------------- 1 | # PhoenixSignaling 2 | 3 | To start your Phoenix server: 4 | 5 | * Run `mix setup` to install and setup dependencies 6 | * Start Phoenix with `mix phx.server` or inside IEx with `iex -S mix phx.server` 7 | 8 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 9 | You should be able to see a video player displaying video captured from your camera. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "membrane_webrtc_plugin", 3 | "version": "0.1.0", 4 | "description": "Phoenix Live Components for Membrane WebRTC Plugin", 5 | "main": "./assets/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/membraneframework/membrane_webrtc_plugin.git" 9 | }, 10 | "license": "Apache-2.0", 11 | "homepage": "https://github.com/membraneframework/membrane_webrtc_plugin" 12 | } 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/please--open-new-issues-in-membranefranework-membrane_core.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Please, open new issues in membranefranework/membrane_core 3 | about: New issues related to this repo should be opened there 4 | title: "[DO NOT OPEN]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please, do not open this issue here. Open it in the [membrane_core](https://github.com/membraneframework/membrane_core) repository instead. 11 | 12 | Thanks for helping us grow :) 13 | -------------------------------------------------------------------------------- /lib/membrane_webrtc/app.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.WebRTC.App do 2 | @moduledoc false 3 | use Application 4 | 5 | @spec start(term(), term()) :: Supervisor.on_start() 6 | def start(_opts, _args) do 7 | children = 8 | [{Registry, name: Membrane.WebRTC.WhipRegistry, keys: :unique}] ++ 9 | if Code.ensure_loaded?(Phoenix), do: [Membrane.WebRTC.PhoenixSignaling.Registry], else: [] 10 | 11 | Supervisor.start_link(children, strategy: :one_for_all, name: __MODULE__.Supervisor) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /examples/live_view/test/webrtc_live_view_web/controllers/error_json_test.exs: -------------------------------------------------------------------------------- 1 | defmodule WebrtcLiveViewWeb.ErrorJSONTest do 2 | use WebrtcLiveViewWeb.ConnCase, async: true 3 | 4 | test "renders 404" do 5 | assert WebrtcLiveViewWeb.ErrorJSON.render("404.json", %{}) == %{ 6 | errors: %{detail: "Not Found"} 7 | } 8 | end 9 | 10 | test "renders 500" do 11 | assert WebrtcLiveViewWeb.ErrorJSON.render("500.json", %{}) == 12 | %{errors: %{detail: "Internal Server Error"}} 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/test/phoenix_signaling_web/controllers/error_json_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixSignalingWeb.ErrorJSONTest do 2 | use PhoenixSignalingWeb.ConnCase, async: true 3 | 4 | test "renders 404" do 5 | assert PhoenixSignalingWeb.ErrorJSON.render("404.json", %{}) == %{ 6 | errors: %{detail: "Not Found"} 7 | } 8 | end 9 | 10 | test "renders 500" do 11 | assert PhoenixSignalingWeb.ErrorJSON.render("500.json", %{}) == 12 | %{errors: %{detail: "Internal Server Error"}} 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /examples/live_view/lib/webrtc_live_view_web/components/layouts.ex: -------------------------------------------------------------------------------- 1 | defmodule WebrtcLiveViewWeb.Layouts do 2 | @moduledoc """ 3 | This module holds different layouts used by your application. 4 | 5 | See the `layouts` directory for all templates available. 6 | The "root" layout is a skeleton rendered as part of the 7 | application router. The "app" layout is set as the default 8 | layout on both `use WebrtcLiveViewWeb, :controller` and 9 | `use WebrtcLiveViewWeb, :live_view`. 10 | """ 11 | use WebrtcLiveViewWeb, :html 12 | 13 | embed_templates "layouts/*" 14 | end 15 | -------------------------------------------------------------------------------- /examples/live_view/test/webrtc_live_view_web/controllers/error_html_test.exs: -------------------------------------------------------------------------------- 1 | defmodule WebrtcLiveViewWeb.ErrorHTMLTest do 2 | use WebrtcLiveViewWeb.ConnCase, async: true 3 | 4 | # Bring render_to_string/4 for testing custom views 5 | import Phoenix.Template 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(WebrtcLiveViewWeb.ErrorHTML, "404", "html", []) == "Not Found" 9 | end 10 | 11 | test "renders 500.html" do 12 | assert render_to_string(WebrtcLiveViewWeb.ErrorHTML, "500", "html", []) == 13 | "Internal Server Error" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/lib/phoenix_signaling_web/components/layouts.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixSignalingWeb.Layouts do 2 | @moduledoc """ 3 | This module holds different layouts used by your application. 4 | 5 | See the `layouts` directory for all templates available. 6 | The "root" layout is a skeleton rendered as part of the 7 | application router. The "app" layout is set as the default 8 | layout on both `use PhoenixSignalingWeb, :controller` and 9 | `use PhoenixSignalingWeb, :live_view`. 10 | """ 11 | use PhoenixSignalingWeb, :html 12 | 13 | embed_templates("layouts/*") 14 | end 15 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/test/phoenix_signaling_web/controllers/error_html_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixSignalingWeb.ErrorHTMLTest do 2 | use PhoenixSignalingWeb.ConnCase, async: true 3 | 4 | # Bring render_to_string/4 for testing custom views 5 | import Phoenix.Template 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(PhoenixSignalingWeb.ErrorHTML, "404", "html", []) == "Not Found" 9 | end 10 | 11 | test "renders 500.html" do 12 | assert render_to_string(PhoenixSignalingWeb.ErrorHTML, "500", "html", []) == 13 | "Internal Server Error" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /examples/live_view/lib/webrtc_live_view/pipeline.ex: -------------------------------------------------------------------------------- 1 | defmodule WebRTCLiveView.Pipeline do 2 | use Membrane.Pipeline 3 | 4 | @impl true 5 | def handle_init(_ctx, opts) do 6 | spec = 7 | child(:webrtc_source, %Membrane.WebRTC.Source{ 8 | allowed_video_codecs: :vp8, 9 | signaling: opts[:ingress_signaling] 10 | }) 11 | |> via_out(:output, options: [kind: :video]) 12 | |> via_in(:input, options: [kind: :video]) 13 | |> child(:webrtc_sink, %Membrane.WebRTC.Sink{ 14 | video_codec: :vp8, 15 | signaling: opts[:egress_signaling] 16 | }) 17 | 18 | {[spec: spec], %{}} 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /examples/live_view/config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # Note we also include the path to a cache manifest 4 | # containing the digested version of static files. This 5 | # manifest is generated by the `mix assets.deploy` task, 6 | # which you should run after static files are built and 7 | # before starting your production server. 8 | config :webrtc_live_view, WebrtcLiveViewWeb.Endpoint, 9 | cache_static_manifest: "priv/static/cache_manifest.json" 10 | 11 | # Do not print debug messages in production 12 | config :logger, level: :info 13 | 14 | # Runtime production configuration, including reading 15 | # of environment variables, is done on config/runtime.exs. 16 | -------------------------------------------------------------------------------- /.github/workflows/on_issue_opened.yaml: -------------------------------------------------------------------------------- 1 | name: 'Close issue when opened' 2 | on: 3 | issues: 4 | types: 5 | - opened 6 | jobs: 7 | close: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout membrane_core 11 | uses: actions/checkout@v3 12 | with: 13 | repository: membraneframework/membrane_core 14 | - name: Close issue 15 | uses: ./.github/actions/close_issue 16 | with: 17 | GITHUB_TOKEN: ${{ secrets.MEMBRANEFRAMEWORKADMIN_TOKEN }} 18 | ISSUE_URL: ${{ github.event.issue.html_url }} 19 | ISSUE_NUMBER: ${{ github.event.issue.number }} 20 | REPOSITORY: ${{ github.repository }} 21 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # Note we also include the path to a cache manifest 4 | # containing the digested version of static files. This 5 | # manifest is generated by the `mix assets.deploy` task, 6 | # which you should run after static files are built and 7 | # before starting your production server. 8 | config :phoenix_signaling, PhoenixSignalingWeb.Endpoint, 9 | cache_static_manifest: "priv/static/cache_manifest.json" 10 | 11 | # Do not print debug messages in production 12 | config :logger, level: :info 13 | 14 | # Runtime production configuration, including reading 15 | # of environment variables, is done on config/runtime.exs. 16 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/lib/phoenix_signaling_web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixSignalingWeb.PageController do 2 | use PhoenixSignalingWeb, :controller 3 | 4 | alias Membrane.WebRTC.PhoenixSignaling 5 | 6 | def home(conn, _params) do 7 | unique_id = UUID.uuid4() 8 | 9 | Task.start(fn -> 10 | input_sg = PhoenixSignaling.new("#{unique_id}_egress") 11 | output_sg = PhoenixSignaling.new("#{unique_id}_ingress") 12 | 13 | Boombox.run( 14 | input: {:webrtc, input_sg}, 15 | output: {:webrtc, output_sg} 16 | ) 17 | end) 18 | 19 | render(conn, :home, layout: false, signaling_id: unique_id) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /examples/live_view/config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :webrtc_live_view, WebrtcLiveViewWeb.Endpoint, 6 | http: [ip: {127, 0, 0, 1}, port: 4002], 7 | secret_key_base: "IMaoMRytJjFhXafOgZQyn2pDxQ6CR3WFaCf7srkc0hfwu8GkAPUEsLHyPmZQzks8", 8 | server: false 9 | 10 | # Print only warnings and errors during test 11 | config :logger, level: :warning 12 | 13 | # Initialize plugs at runtime for faster test compilation 14 | config :phoenix, :plug_init_mode, :runtime 15 | 16 | # Enable helpful, but potentially expensive runtime checks 17 | config :phoenix_live_view, 18 | enable_expensive_runtime_checks: true 19 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :phoenix_signaling, PhoenixSignalingWeb.Endpoint, 6 | http: [ip: {127, 0, 0, 1}, port: 4002], 7 | secret_key_base: "DKPPNlPKdLqat38NAqorUOGB7udlRpIw+r0jPUEJH7L3F+HgKBrdNd4/wxc6tlwf", 8 | server: false 9 | 10 | # Print only warnings and errors during test 11 | config :logger, level: :warning 12 | 13 | # Initialize plugs at runtime for faster test compilation 14 | config :phoenix, :plug_init_mode, :runtime 15 | 16 | # Enable helpful, but potentially expensive runtime checks 17 | config :phoenix_live_view, 18 | enable_expensive_runtime_checks: true 19 | -------------------------------------------------------------------------------- /examples/live_view/lib/webrtc_live_view_web/components/layouts/root.html.heex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <.live_title default="WebrtcLiveView" suffix=" · Phoenix Framework"> 8 | {assigns[:page_title]} 9 | 10 | 11 | 13 | 14 | 15 | {@inner_content} 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/websocket_signaling/assets/file_to_browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Membrane WebRTC file to browser example 9 | 10 | 11 | 13 |
14 |

Membrane WebRTC file to browser example

15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/lib/phoenix_signaling_web/components/layouts/root.html.heex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <.live_title default="PhoenixSignaling" suffix=" · Phoenix Framework"> 8 | {assigns[:page_title]} 9 | 10 | 11 | 13 | 14 | 15 | {@inner_content} 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/live_view/lib/webrtc_live_view_web/controllers/error_json.ex: -------------------------------------------------------------------------------- 1 | defmodule WebrtcLiveViewWeb.ErrorJSON do 2 | @moduledoc """ 3 | This module is invoked by your endpoint in case of errors on JSON requests. 4 | 5 | See config/config.exs. 6 | """ 7 | 8 | # If you want to customize a particular status code, 9 | # you may add your own clauses, such as: 10 | # 11 | # def render("500.json", _assigns) do 12 | # %{errors: %{detail: "Internal Server Error"}} 13 | # end 14 | 15 | # By default, Phoenix returns the status message from 16 | # the template name. For example, "404.json" becomes 17 | # "Not Found". 18 | def render(template, _assigns) do 19 | %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}} 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/lib/phoenix_signaling_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixSignalingWeb.Router do 2 | use PhoenixSignalingWeb, :router 3 | 4 | pipeline :browser do 5 | plug(:accepts, ["html"]) 6 | plug(:fetch_session) 7 | plug(:fetch_live_flash) 8 | plug(:put_root_layout, html: {PhoenixSignalingWeb.Layouts, :root}) 9 | plug(:protect_from_forgery) 10 | plug(:put_secure_browser_headers) 11 | end 12 | 13 | pipeline :api do 14 | plug(:accepts, ["json"]) 15 | end 16 | 17 | scope "/", PhoenixSignalingWeb do 18 | pipe_through(:browser) 19 | 20 | get("/", PageController, :home) 21 | end 22 | 23 | # Other scopes may use custom stacks. 24 | # scope "/api", PhoenixSignalingWeb do 25 | # pipe_through :api 26 | # end 27 | end 28 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/lib/phoenix_signaling_web/controllers/error_json.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixSignalingWeb.ErrorJSON do 2 | @moduledoc """ 3 | This module is invoked by your endpoint in case of errors on JSON requests. 4 | 5 | See config/config.exs. 6 | """ 7 | 8 | # If you want to customize a particular status code, 9 | # you may add your own clauses, such as: 10 | # 11 | # def render("500.json", _assigns) do 12 | # %{errors: %{detail: "Internal Server Error"}} 13 | # end 14 | 15 | # By default, Phoenix returns the status message from 16 | # the template name. For example, "404.json" becomes 17 | # "Not Found". 18 | def render(template, _assigns) do 19 | %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}} 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /.github/workflows/on_pr_opened.yaml: -------------------------------------------------------------------------------- 1 | name: Add PR to Smackore project board, if the author is from outside Membrane Team 2 | on: 3 | pull_request_target: 4 | types: 5 | - opened 6 | jobs: 7 | maybe_add_to_project_board: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout membrane_core 11 | uses: actions/checkout@v3 12 | with: 13 | repository: membraneframework/membrane_core 14 | - name: Puts PR in "New PRs by community" column in the Smackore project, if the author is from outside Membrane Team 15 | uses: ./.github/actions/add_pr_to_smackore_board 16 | with: 17 | GITHUB_TOKEN: ${{ secrets.MEMBRANEFRAMEWORKADMIN_TOKEN }} 18 | AUTHOR_LOGIN: ${{ github.event.pull_request.user.login }} 19 | PR_URL: ${{ github.event.pull_request.html_url }} 20 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | elixir: membraneframework/elixir@1 4 | 5 | workflows: 6 | version: 2 7 | build: 8 | jobs: 9 | - elixir/build_test: 10 | filters: &filters 11 | tags: 12 | only: /v.*/ 13 | cache-version: 3 14 | - elixir/test: 15 | filters: 16 | <<: *filters 17 | cache-version: 3 18 | - elixir/lint: 19 | filters: 20 | <<: *filters 21 | cache-version: 3 22 | - elixir/hex_publish: 23 | requires: 24 | - elixir/build_test 25 | - elixir/test 26 | - elixir/lint 27 | context: 28 | - Deployment 29 | filters: 30 | branches: 31 | ignore: /.*/ 32 | tags: 33 | only: /v.*/ 34 | cache-version: 3 35 | 36 | -------------------------------------------------------------------------------- /lib/membrane_webrtc/phoenix_signaling/channel.ex: -------------------------------------------------------------------------------- 1 | if Code.ensure_loaded?(Phoenix) do 2 | defmodule Membrane.WebRTC.PhoenixSignaling.Channel do 3 | @moduledoc false 4 | use Phoenix.Channel 5 | alias Membrane.WebRTC.PhoenixSignaling 6 | 7 | @impl true 8 | def join(signaling_id, _payload, socket) do 9 | PhoenixSignaling.register_channel(signaling_id) 10 | socket = assign(socket, :signaling_id, signaling_id) 11 | {:ok, socket} 12 | end 13 | 14 | @impl true 15 | def handle_in(signaling_id, msg, socket) do 16 | msg = Jason.decode!(msg) 17 | PhoenixSignaling.signal(signaling_id, msg) 18 | {:noreply, socket} 19 | end 20 | 21 | @impl true 22 | def handle_info({:membrane_webrtc_signaling, _pid, msg, _metadata}, socket) do 23 | push(socket, socket.assigns.signaling_id, msg) 24 | {:noreply, socket} 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /examples/live_view/lib/webrtc_live_view_web/controllers/error_html.ex: -------------------------------------------------------------------------------- 1 | defmodule WebrtcLiveViewWeb.ErrorHTML do 2 | @moduledoc """ 3 | This module is invoked by your endpoint in case of errors on HTML requests. 4 | 5 | See config/config.exs. 6 | """ 7 | use WebrtcLiveViewWeb, :html 8 | 9 | # If you want to customize your error pages, 10 | # uncomment the embed_templates/1 call below 11 | # and add pages to the error directory: 12 | # 13 | # * lib/webrtc_live_view_web/controllers/error_html/404.html.heex 14 | # * lib/webrtc_live_view_web/controllers/error_html/500.html.heex 15 | # 16 | # embed_templates "error_html/*" 17 | 18 | # The default is to render a plain text page based on 19 | # the template name. For example, "404.html" becomes 20 | # "Not Found". 21 | def render(template, _assigns) do 22 | Phoenix.Controller.status_message_from_template(template) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/lib/phoenix_signaling_web/controllers/error_html.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixSignalingWeb.ErrorHTML do 2 | @moduledoc """ 3 | This module is invoked by your endpoint in case of errors on HTML requests. 4 | 5 | See config/config.exs. 6 | """ 7 | use PhoenixSignalingWeb, :html 8 | 9 | # If you want to customize your error pages, 10 | # uncomment the embed_templates/1 call below 11 | # and add pages to the error directory: 12 | # 13 | # * lib/phoenix_signaling_web/controllers/error_html/404.html.heex 14 | # * lib/phoenix_signaling_web/controllers/error_html/500.html.heex 15 | # 16 | # embed_templates "error_html/*" 17 | 18 | # The default is to render a plain text page based on 19 | # the template name. For example, "404.html" becomes 20 | # "Not Found". 21 | def render(template, _assigns) do 22 | Phoenix.Controller.status_message_from_template(template) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /examples/live_view/README.md: -------------------------------------------------------------------------------- 1 | # Example Project 2 | 3 | Example project showing how `Membrane.WebRTC.Live.Capture` and `Membrane.WebRTC.Live.Player` can be used. 4 | 5 | It contains a simple demo, where: 6 | - the video stream is get from the browser and sent via WebRTC to Elixir server using `Membrane.WebRTC.Live.Capture` 7 | - then, this same video stream is re-sent again to the browser and displayed using `Membrane.WebRTC.Live.Player`. 8 | 9 | Usage of Phoenix LiveViews dedicated for Membrane WebRTC takes place in `lib/webrtc_live_view_web/live/home.ex`. 10 | 11 | ## Running the demo 12 | 13 | To run the demo, you'll need to have [Elixir installed](https://elixir-lang.org/install.html). Then, do the following: 14 | 15 | * Run `mix setup` to install and setup dependencies 16 | * Start Phoenix with `mix phx.server` or inside IEx with `iex -S mix phx.server` 17 | 18 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 19 | -------------------------------------------------------------------------------- /examples/live_view/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Temporary files, for example, from tests. 23 | /tmp/ 24 | 25 | # Ignore package tarball (built via "mix hex.build"). 26 | webrtc_live_view-*.tar 27 | 28 | # Ignore assets that are produced by build tools. 29 | /priv/static/assets/ 30 | 31 | # Ignore digested assets cache. 32 | /priv/static/cache_manifest.json 33 | 34 | # In case you use Node.js/npm, you want to ignore these. 35 | npm-debug.log 36 | /assets/node_modules/ 37 | 38 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Temporary files, for example, from tests. 23 | /tmp/ 24 | 25 | # Ignore package tarball (built via "mix hex.build"). 26 | phoenix_signaling-*.tar 27 | 28 | # Ignore assets that are produced by build tools. 29 | /priv/static/assets/ 30 | 31 | # Ignore digested assets cache. 32 | /priv/static/cache_manifest.json 33 | 34 | # In case you use Node.js/npm, you want to ignore these. 35 | npm-debug.log 36 | /assets/node_modules/ 37 | 38 | -------------------------------------------------------------------------------- /lib/membrane_webrtc/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.WebRTC.Utils do 2 | @moduledoc false 3 | 4 | alias Membrane.WebRTC.{Signaling, SimpleWebSocketServer} 5 | 6 | @spec validate_signaling!( 7 | Signaling.t() 8 | | {:websocket, SimpleWebSocketServer.options()} 9 | | {:whip, [{atom(), term()}]} 10 | ) :: 11 | :ok | no_return() 12 | def validate_signaling!(%Signaling{}), do: :ok 13 | 14 | def validate_signaling!({:websocket, options}) do 15 | _options = SimpleWebSocketServer.validate_options!(options) 16 | :ok 17 | end 18 | 19 | def validate_signaling!({:whip, options} = signaling) when is_list(options) do 20 | options 21 | |> Enum.each(fn 22 | {atom, _term} when is_atom(atom) -> :ok 23 | _other -> do_raise(signaling) 24 | end) 25 | 26 | :ok 27 | end 28 | 29 | def validate_signaling!(signaling), do: do_raise(signaling) 30 | 31 | @spec do_raise(term()) :: no_return() 32 | defp do_raise(signaling) do 33 | raise "Invalid signaling: #{inspect(signaling)}" 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /examples/live_view/lib/webrtc_live_view/application.ex: -------------------------------------------------------------------------------- 1 | defmodule WebrtcLiveView.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | @impl true 9 | def start(_type, _args) do 10 | children = [ 11 | WebrtcLiveViewWeb.Telemetry, 12 | {DNSCluster, query: Application.get_env(:webrtc_live_view, :dns_cluster_query) || :ignore}, 13 | {Phoenix.PubSub, name: WebrtcLiveView.PubSub}, 14 | # Start a worker by calling: WebrtcLiveView.Worker.start_link(arg) 15 | # {WebrtcLiveView.Worker, arg}, 16 | # Start to serve requests, typically the last entry 17 | WebrtcLiveViewWeb.Endpoint 18 | ] 19 | 20 | # See https://hexdocs.pm/elixir/Supervisor.html 21 | # for other strategies and supported options 22 | opts = [strategy: :one_for_one, name: WebrtcLiveView.Supervisor] 23 | Supervisor.start_link(children, opts) 24 | end 25 | 26 | # Tell Phoenix to update the endpoint configuration 27 | # whenever the application is updated. 28 | @impl true 29 | def config_change(changed, _new, removed) do 30 | WebrtcLiveViewWeb.Endpoint.config_change(changed, removed) 31 | :ok 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/membrane_webrtc/signaling_channel.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.WebRTC.SignalingChannel do 2 | @moduledoc """ 3 | Deprecated since v0.24.0. Use #{inspect(Membrane.WebRTC.Signaling)} instead. 4 | """ 5 | require Logger 6 | 7 | alias Membrane.WebRTC.Signaling 8 | 9 | @type t :: Signaling.t() 10 | @type message :: Signaling.message() 11 | @type message_content :: Signaling.message_content() 12 | @type ex_webrtc_message :: Signaling.ex_webrtc_message() 13 | @type json_data_message :: Signaling.json_data_message() 14 | 15 | @spec new() :: t 16 | def new() do 17 | Logger.warning(""" 18 | Module #{inspect(__MODULE__)} is deprecated since v0.24.0. Use #{inspect(Signaling)} instead. 19 | """) 20 | 21 | Signaling.new() 22 | end 23 | 24 | @spec register_peer(t, message_format: :ex_webrtc | :json_data, pid: pid) :: :ok 25 | defdelegate register_peer(signaling, opts \\ []), to: Signaling 26 | 27 | @doc false 28 | @spec register_element(t) :: :ok 29 | defdelegate register_element(signaling), to: Signaling 30 | 31 | @spec signal(t, message_content, metadata :: map) :: :ok 32 | defdelegate signal(signaling, message, metadata \\ %{}), to: Signaling 33 | 34 | @spec close(t) :: :ok 35 | defdelegate close(signaling), to: Signaling 36 | end 37 | -------------------------------------------------------------------------------- /examples/live_view/test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule WebrtcLiveViewWeb.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | we enable the SQL sandbox, so changes done to the database 12 | are reverted at the end of every test. If you are using 13 | PostgreSQL, you can even run database tests asynchronously 14 | by setting `use WebrtcLiveViewWeb.ConnCase, async: true`, although 15 | this option is not recommended for other databases. 16 | """ 17 | 18 | use ExUnit.CaseTemplate 19 | 20 | using do 21 | quote do 22 | # The default endpoint for testing 23 | @endpoint WebrtcLiveViewWeb.Endpoint 24 | 25 | use WebrtcLiveViewWeb, :verified_routes 26 | 27 | # Import conveniences for testing with connections 28 | import Plug.Conn 29 | import Phoenix.ConnTest 30 | import WebrtcLiveViewWeb.ConnCase 31 | end 32 | end 33 | 34 | setup _tags do 35 | {:ok, conn: Phoenix.ConnTest.build_conn()} 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/lib/phoenix_signaling/application.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixSignaling.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | @impl true 9 | def start(_type, _args) do 10 | children = [ 11 | PhoenixSignalingWeb.Telemetry, 12 | {DNSCluster, query: Application.get_env(:phoenix_signaling, :dns_cluster_query) || :ignore}, 13 | {Phoenix.PubSub, name: PhoenixSignaling.PubSub}, 14 | # Start a worker by calling: PhoenixSignaling.Worker.start_link(arg) 15 | # {PhoenixSignaling.Worker, arg}, 16 | # Start to serve requests, typically the last entry 17 | PhoenixSignalingWeb.Endpoint 18 | ] 19 | 20 | # See https://hexdocs.pm/elixir/Supervisor.html 21 | # for other strategies and supported options 22 | opts = [strategy: :one_for_one, name: PhoenixSignaling.Supervisor] 23 | Supervisor.start_link(children, opts) 24 | end 25 | 26 | # Tell Phoenix to update the endpoint configuration 27 | # whenever the application is updated. 28 | @impl true 29 | def config_change(changed, _new, removed) do 30 | PhoenixSignalingWeb.Endpoint.config_change(changed, removed) 31 | :ok 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /examples/live_view/lib/webrtc_live_view_web/components/layouts/app.html.heex: -------------------------------------------------------------------------------- 1 |
2 | <%!--
3 |
4 | 5 | 6 | 7 |

8 | v{Application.spec(:phoenix, :vsn)} 9 |

10 |
11 | 25 |
--%> 26 |
27 |
28 |
29 | <.flash_group flash={@flash} /> 30 | {@inner_content} 31 |
32 |
33 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/lib/phoenix_signaling_web/components/layouts/app.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | 7 |

8 | v{Application.spec(:phoenix, :vsn)} 9 |

10 |
11 | 25 |
26 |
27 |
28 |
29 | <.flash_group flash={@flash} /> 30 | {@inner_content} 31 |
32 |
33 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixSignalingWeb.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | we enable the SQL sandbox, so changes done to the database 12 | are reverted at the end of every test. If you are using 13 | PostgreSQL, you can even run database tests asynchronously 14 | by setting `use PhoenixSignalingWeb.ConnCase, async: true`, although 15 | this option is not recommended for other databases. 16 | """ 17 | 18 | use ExUnit.CaseTemplate 19 | 20 | using do 21 | quote do 22 | # The default endpoint for testing 23 | @endpoint PhoenixSignalingWeb.Endpoint 24 | 25 | use PhoenixSignalingWeb, :verified_routes 26 | 27 | # Import conveniences for testing with connections 28 | import Plug.Conn 29 | import Phoenix.ConnTest 30 | import PhoenixSignalingWeb.ConnCase 31 | end 32 | end 33 | 34 | setup _tags do 35 | {:ok, conn: Phoenix.ConnTest.build_conn()} 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /examples/live_view/lib/webrtc_live_view_web/live/home.ex: -------------------------------------------------------------------------------- 1 | defmodule WebrtcLiveViewWeb.Live.EchoLive do 2 | use WebrtcLiveViewWeb, :live_view 3 | 4 | alias Membrane.WebRTC.Live.{Capture, Player} 5 | 6 | def mount(_params, _session, socket) do 7 | socket = 8 | if connected?(socket) do 9 | ingress_signaling = Membrane.WebRTC.Signaling.new() 10 | egress_signaling = Membrane.WebRTC.Signaling.new() 11 | 12 | Membrane.Pipeline.start_link(WebRTCLiveView.Pipeline, 13 | ingress_signaling: ingress_signaling, 14 | egress_signaling: egress_signaling 15 | ) 16 | 17 | socket 18 | |> Capture.attach( 19 | id: "mediaCapture", 20 | signaling: ingress_signaling, 21 | video?: true, 22 | audio?: false 23 | ) 24 | |> Player.attach( 25 | id: "videoPlayer", 26 | signaling: egress_signaling 27 | ) 28 | else 29 | socket 30 | end 31 | 32 | {:ok, socket} 33 | end 34 | 35 | def render(assigns) do 36 | ~H""" 37 |

Captured stream preview

38 | 39 |

Stream sent by the server

40 | 41 | """ 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /examples/live_view/lib/webrtc_live_view_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule WebrtcLiveViewWeb.Router do 2 | use WebrtcLiveViewWeb, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug :fetch_live_flash 8 | plug :put_root_layout, html: {WebrtcLiveViewWeb.Layouts, :root} 9 | plug :protect_from_forgery 10 | plug :put_secure_browser_headers 11 | end 12 | 13 | pipeline :api do 14 | plug :accepts, ["json"] 15 | end 16 | 17 | scope "/", WebrtcLiveViewWeb do 18 | pipe_through :browser 19 | 20 | live "/", Live.EchoLive, :index 21 | end 22 | 23 | # Other scopes may use custom stacks. 24 | # scope "/api", WebrtcLiveViewWeb do 25 | # pipe_through :api 26 | # end 27 | 28 | # Enable LiveDashboard in development 29 | if Application.compile_env(:webrtc_live_view, :dev_routes) do 30 | # If you want to use the LiveDashboard in production, you should put 31 | # it behind authentication and allow only admins to access it. 32 | # If your application does not have an admins-only section yet, 33 | # you can use Plug.BasicAuth to set up some basic authentication 34 | # as long as you are also using SSL (which you should anyway). 35 | import Phoenix.LiveDashboard.Router 36 | 37 | scope "/dev" do 38 | pipe_through :browser 39 | 40 | live_dashboard "/dashboard", metrics: WebrtcLiveViewWeb.Telemetry 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /assets/player.js: -------------------------------------------------------------------------------- 1 | export function createPlayerHook(iceServers = [{ urls: `stun:stun.l.google.com:19302` }]) { 2 | return { 3 | async mounted() { 4 | this.pc = new RTCPeerConnection({ iceServers: iceServers }); 5 | this.el.srcObject = new MediaStream(); 6 | 7 | this.pc.ontrack = (event) => { 8 | this.el.srcObject.addTrack(event.track); 9 | }; 10 | 11 | this.pc.onicecandidate = (ev) => { 12 | console.log(`[${this.el.id}] Sent ICE candidate:`, ev.candidate); 13 | message = { type: `ice_candidate`, data: ev.candidate }; 14 | this.pushEventTo(this.el, `webrtc_signaling`, message); 15 | }; 16 | 17 | const eventName = `webrtc_signaling-${this.el.id}`; 18 | this.handleEvent(eventName, async (event) => { 19 | const { type, data } = event; 20 | 21 | switch (type) { 22 | case `sdp_offer`: 23 | console.log(`[${this.el.id}] Received SDP offer:`, data); 24 | await this.pc.setRemoteDescription(data); 25 | 26 | const answer = await this.pc.createAnswer(); 27 | await this.pc.setLocalDescription(answer); 28 | 29 | message = { type: `sdp_answer`, data: answer }; 30 | this.pushEventTo(this.el, `webrtc_signaling`, message); 31 | console.log(`[${this.el.id}] Sent SDP answer:`, answer); 32 | 33 | break; 34 | case `ice_candidate`: 35 | console.log(`[${this.el.id}] Received ICE candidate:`, data); 36 | await this.pc.addIceCandidate(data); 37 | } 38 | }); 39 | }, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /examples/websocket_signaling/assets/file_to_browser/file_to_browser.js: -------------------------------------------------------------------------------- 1 | const videoPlayer = document.getElementById("videoPlayer"); 2 | const pcConfig = { iceServers: [{ urls: "stun:stun.l.google.com:19302" }] }; 3 | const proto = window.location.protocol === "https:" ? "wss:" : "ws:"; 4 | const ws = new WebSocket(`${proto}//${window.location.hostname}:8829`); 5 | ws.onopen = () => start_connection(ws); 6 | ws.onclose = (event) => console.log("WebSocket connection was terminated:", event); 7 | 8 | const start_connection = async (ws) => { 9 | videoPlayer.srcObject = new MediaStream(); 10 | 11 | const pc = new RTCPeerConnection(pcConfig); 12 | pc.ontrack = (event) => videoPlayer.srcObject.addTrack(event.track); 13 | pc.onicecandidate = (event) => { 14 | if (event.candidate === null) return; 15 | 16 | console.log("Sent ICE candidate:", event.candidate); 17 | ws.send(JSON.stringify({ type: "ice_candidate", data: event.candidate })); 18 | }; 19 | 20 | ws.onmessage = async (event) => { 21 | const { type, data } = JSON.parse(event.data); 22 | 23 | switch (type) { 24 | case "sdp_offer": 25 | console.log("Received SDP offer:", data); 26 | await pc.setRemoteDescription(data); 27 | const answer = await pc.createAnswer(); 28 | await pc.setLocalDescription(answer); 29 | ws.send(JSON.stringify({ type: "sdp_answer", data: answer })); 30 | console.log("Sent SDP answer:", answer); 31 | break; 32 | case "ice_candidate": 33 | console.log("Received ICE candidate:", data); 34 | await pc.addIceCandidate(data); 35 | } 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /examples/websocket_signaling/assets/browser_to_file/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Membrane WebRTC WHIP/WHEP Example 9 | 10 | 11 | 13 |

Membrane WebRTC WHIP/WHEP Example

14 |
Connecting...
15 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | 7 | # General application configuration 8 | import Config 9 | 10 | config :phoenix_signaling, 11 | generators: [timestamp_type: :utc_datetime] 12 | 13 | # Configures the endpoint 14 | config :phoenix_signaling, PhoenixSignalingWeb.Endpoint, 15 | url: [host: "localhost"], 16 | adapter: Bandit.PhoenixAdapter, 17 | render_errors: [ 18 | formats: [html: PhoenixSignalingWeb.ErrorHTML, json: PhoenixSignalingWeb.ErrorJSON], 19 | layout: false 20 | ], 21 | pubsub_server: PhoenixSignaling.PubSub, 22 | live_view: [signing_salt: "rWTn+ozf"] 23 | 24 | # Configure esbuild (the version is required) 25 | config :esbuild, 26 | version: "0.17.11", 27 | phoenix_signaling: [ 28 | args: 29 | ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*), 30 | cd: Path.expand("../assets", __DIR__), 31 | env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} 32 | ] 33 | 34 | # Configures Elixir's Logger 35 | config :logger, :console, 36 | format: "$time $metadata[$level] $message\n", 37 | metadata: [:request_id] 38 | 39 | # Use Jason for JSON parsing in Phoenix 40 | config :phoenix, :json_library, Jason 41 | 42 | # Import environment specific config. This must remain at the bottom 43 | # of this file so it overrides the configuration defined above. 44 | import_config "#{config_env()}.exs" 45 | -------------------------------------------------------------------------------- /examples/live_view/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | 7 | # General application configuration 8 | import Config 9 | 10 | config :webrtc_live_view, 11 | generators: [timestamp_type: :utc_datetime] 12 | 13 | # Configures the endpoint 14 | config :webrtc_live_view, WebrtcLiveViewWeb.Endpoint, 15 | url: [host: "localhost"], 16 | adapter: Bandit.PhoenixAdapter, 17 | render_errors: [ 18 | formats: [html: WebrtcLiveViewWeb.ErrorHTML, json: WebrtcLiveViewWeb.ErrorJSON], 19 | layout: false 20 | ], 21 | pubsub_server: WebrtcLiveView.PubSub, 22 | live_view: [signing_salt: "X97dFT34"] 23 | 24 | # Configure esbuild (the version is required) 25 | config :esbuild, 26 | version: "0.17.11", 27 | webrtc_live_view: [ 28 | args: 29 | ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*), 30 | cd: Path.expand("../assets", __DIR__), 31 | env: %{ 32 | "NODE_PATH" => Enum.map_join(["../deps", "../../../.."], ":", &Path.expand(&1, __DIR__)) 33 | } 34 | ] 35 | 36 | # Configures Elixir's Logger 37 | config :logger, :console, 38 | format: "$time $metadata[$level] $message\n", 39 | metadata: [:request_id], 40 | level: :info 41 | 42 | # Use Jason for JSON parsing in Phoenix 43 | config :phoenix, :json_library, Jason 44 | 45 | # Import environment specific config. This must remain at the bottom 46 | # of this file so it overrides the configuration defined above. 47 | import_config "#{config_env()}.exs" 48 | -------------------------------------------------------------------------------- /lib/membrane_webrtc/phoenix_signaling.ex: -------------------------------------------------------------------------------- 1 | if Code.ensure_loaded?(Phoenix) do 2 | defmodule Membrane.WebRTC.PhoenixSignaling do 3 | @moduledoc """ 4 | Provides signaling capabilities for WebRTC connections through Phoenix channels. 5 | """ 6 | alias Membrane.WebRTC.PhoenixSignaling.Registry, as: SignalingRegistry 7 | alias Membrane.WebRTC.Signaling 8 | 9 | @typedoc """ 10 | A type representing an unique identifier that is used to distinguish between different Phoenix Signaling 11 | instances. 12 | """ 13 | @type signaling_id :: String.t() 14 | 15 | @doc """ 16 | Returns an instance of a Phoenix Signaling associated with given signaling ID. 17 | """ 18 | @spec new(signaling_id()) :: Signaling.t() 19 | def new(signaling_id) do 20 | SignalingRegistry.get_or_create(signaling_id) 21 | end 22 | 23 | @doc """ 24 | Registers Phoenix.Channel process as WebRTC signaling peer 25 | so that it can send and receive signaling messages. 26 | """ 27 | @spec register_channel(signaling_id(), pid() | nil) :: :ok 28 | def register_channel(signaling_id, channel_pid \\ nil) do 29 | channel_pid = channel_pid || self() 30 | signaling = SignalingRegistry.get_or_create(signaling_id) 31 | Signaling.register_peer(signaling, message_format: :json_data, pid: channel_pid) 32 | end 33 | 34 | @doc """ 35 | Sends a signal message via the Phoenix Signaling instance associated with given signaling ID. 36 | """ 37 | @spec signal(signaling_id(), Signaling.message_content()) :: :ok | no_return() 38 | def signal(signaling_id, msg) do 39 | signaling = SignalingRegistry.get!(signaling_id) 40 | Signaling.signal(signaling, msg) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /examples/live_view/lib/webrtc_live_view_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule WebrtcLiveViewWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :webrtc_live_view 3 | 4 | # The session will be stored in the cookie and signed, 5 | # this means its contents can be read but not tampered with. 6 | # Set :encryption_salt if you would also like to encrypt it. 7 | @session_options [ 8 | store: :cookie, 9 | key: "_webrtc_live_view_key", 10 | signing_salt: "RxBv85K8", 11 | same_site: "Lax" 12 | ] 13 | 14 | socket "/live", Phoenix.LiveView.Socket, 15 | websocket: [connect_info: [session: @session_options]], 16 | longpoll: [connect_info: [session: @session_options]] 17 | 18 | # Serve at "/" the static files from "priv/static" directory. 19 | # 20 | # You should set gzip to true if you are running phx.digest 21 | # when deploying your static files in production. 22 | plug Plug.Static, 23 | at: "/", 24 | from: :webrtc_live_view, 25 | gzip: false, 26 | only: WebrtcLiveViewWeb.static_paths() 27 | 28 | # Code reloading can be explicitly enabled under the 29 | # :code_reloader configuration of your endpoint. 30 | if code_reloading? do 31 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 32 | plug Phoenix.LiveReloader 33 | plug Phoenix.CodeReloader 34 | end 35 | 36 | plug Phoenix.LiveDashboard.RequestLogger, 37 | param_key: "request_logger", 38 | cookie_key: "request_logger" 39 | 40 | plug Plug.RequestId 41 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] 42 | 43 | plug Plug.Parsers, 44 | parsers: [:urlencoded, :multipart, :json], 45 | pass: ["*/*"], 46 | json_decoder: Phoenix.json_library() 47 | 48 | plug Plug.MethodOverride 49 | plug Plug.Head 50 | plug Plug.Session, @session_options 51 | plug WebrtcLiveViewWeb.Router 52 | end 53 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/lib/phoenix_signaling_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixSignalingWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :phoenix_signaling 3 | 4 | # The session will be stored in the cookie and signed, 5 | # this means its contents can be read but not tampered with. 6 | # Set :encryption_salt if you would also like to encrypt it. 7 | @session_options [ 8 | store: :cookie, 9 | key: "_phoenix_signaling_key", 10 | signing_salt: "TCDEzcXo", 11 | same_site: "Lax" 12 | ] 13 | 14 | # socket "/live", Phoenix.LiveView.Socket, 15 | # websocket: [connect_info: [session: @session_options]], 16 | # longpoll: [connect_info: [session: @session_options]] 17 | 18 | # Serve at "/" the static files from "priv/static" directory. 19 | # 20 | # You should set gzip to true if you are running phx.digest 21 | # when deploying your static files in production. 22 | plug(Plug.Static, 23 | at: "/", 24 | from: :phoenix_signaling, 25 | gzip: false, 26 | only: PhoenixSignalingWeb.static_paths() 27 | ) 28 | 29 | # Code reloading can be explicitly enabled under the 30 | # :code_reloader configuration of your endpoint. 31 | if code_reloading? do 32 | socket("/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket) 33 | plug(Phoenix.LiveReloader) 34 | plug(Phoenix.CodeReloader) 35 | end 36 | 37 | plug(Plug.RequestId) 38 | plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint]) 39 | 40 | plug(Plug.Parsers, 41 | parsers: [:urlencoded, :multipart, :json], 42 | pass: ["*/*"], 43 | json_decoder: Phoenix.json_library() 44 | ) 45 | 46 | plug(Plug.MethodOverride) 47 | plug(Plug.Head) 48 | plug(Plug.Session, @session_options) 49 | plug(PhoenixSignalingWeb.Router) 50 | 51 | socket("/signaling", Membrane.WebRTC.PhoenixSignaling.Socket, 52 | websocket: true, 53 | longpoll: false 54 | ) 55 | end 56 | -------------------------------------------------------------------------------- /.github/workflows/fetch_changes.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Fetch changes 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Trigger thrice a day 8 | schedule: 9 | - cron: '0 4,8,12 * * *' 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | jobs: 15 | # This workflow contains a single job called "build" 16 | build: 17 | # The type of runner that the job will run on 18 | runs-on: ubuntu-latest 19 | 20 | # Steps represent a sequence of tasks that will be executed as part of the job 21 | steps: 22 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 23 | - uses: actions/checkout@v3 24 | with: 25 | fetch-depth: '0' 26 | 27 | - name: webfactory/ssh-agent 28 | uses: webfactory/ssh-agent@v0.5.4 29 | with: 30 | ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} 31 | 32 | # Runs a set of commands using the runners shell 33 | - name: Add remote 34 | run: | 35 | git remote add source git@github.com:membraneframework/membrane_template_plugin.git 36 | git remote update 37 | 38 | echo "CURRENT_BRANCH=$(git branch --show-current)" >> $GITHUB_ENV 39 | 40 | - name: Check changes 41 | run: | 42 | echo ${{env.CURRENT_BRANCH}} 43 | echo "LOG_SIZE=$(git log origin/${{ env.CURRENT_BRANCH }}..source/${{ env.CURRENT_BRANCH }} | wc -l)" 44 | 45 | echo "LOG_SIZE=$(git log origin/${{ env.CURRENT_BRANCH }}..source/${{ env.CURRENT_BRANCH }} | wc -l)" >> $GITHUB_ENV 46 | 47 | - if: ${{ env.LOG_SIZE != '0'}} 48 | name: Merge changes 49 | run: | 50 | git config --global user.email "admin@membraneframework.com" 51 | git config --global user.name "MembraneFramework" 52 | 53 | git merge source/master 54 | git push origin master 55 | -------------------------------------------------------------------------------- /lib/membrane_webrtc/phoenix_signaling/registry.ex: -------------------------------------------------------------------------------- 1 | if Code.ensure_loaded?(Phoenix) do 2 | defmodule Membrane.WebRTC.PhoenixSignaling.Registry do 3 | @moduledoc false 4 | use GenServer 5 | alias Membrane.WebRTC.PhoenixSignaling 6 | alias Membrane.WebRTC.Signaling 7 | 8 | @spec start(term()) :: GenServer.on_start() 9 | def start(args) do 10 | GenServer.start(__MODULE__, args, name: __MODULE__) 11 | end 12 | 13 | @spec start_link(term()) :: GenServer.on_start() 14 | def start_link(args) do 15 | GenServer.start_link(__MODULE__, args, name: __MODULE__) 16 | end 17 | 18 | @spec get_or_create(PhoenixSignaling.signaling_id()) :: Signaling.t() 19 | def get_or_create(signaling_id) do 20 | GenServer.call(__MODULE__, {:get_or_create, signaling_id}) 21 | end 22 | 23 | @spec get(PhoenixSignaling.signaling_id()) :: Signaling.t() | nil 24 | def get(signaling_id) do 25 | GenServer.call(__MODULE__, {:get, signaling_id}) 26 | end 27 | 28 | @spec get!(PhoenixSignaling.signaling_id()) :: Signaling.t() | no_return() 29 | def get!(signaling_id) do 30 | case get(signaling_id) do 31 | nil -> 32 | raise "Couldn't find signaling instance associated with signaling_id: #{inspect(signaling_id)}" 33 | 34 | signaling -> 35 | signaling 36 | end 37 | end 38 | 39 | @impl true 40 | def init(_args) do 41 | {:ok, %{signaling_map: %{}}} 42 | end 43 | 44 | @impl true 45 | def handle_call({:get_or_create, signaling_id}, _from, state) do 46 | case Map.get(state.signaling_map, signaling_id) do 47 | nil -> 48 | signaling = Signaling.new() 49 | state = put_in(state, [:signaling_map, signaling_id], signaling) 50 | {:reply, signaling, state} 51 | 52 | signaling -> 53 | {:reply, signaling, state} 54 | end 55 | end 56 | 57 | @impl true 58 | def handle_call({:get, signaling_id}, _from, state) do 59 | {:reply, Map.get(state.signaling_map, signaling_id), state} 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /examples/live_view/assets/js/app.js: -------------------------------------------------------------------------------- 1 | // If you want to use Phoenix channels, run `mix help phx.gen.channel` 2 | // to get started and then uncomment the line below. 3 | // import "./user_socket.js" 4 | 5 | // You can include dependencies in two ways. 6 | // 7 | // The simplest option is to put them in assets/vendor and 8 | // import them using relative paths: 9 | // 10 | // import "../vendor/some-package.js" 11 | // 12 | // Alternatively, you can `npm install some-package --prefix assets` and import 13 | // them using a path starting with the package name: 14 | // 15 | // import "some-package" 16 | // 17 | 18 | // Include phoenix_html to handle method=PUT/DELETE in forms and buttons. 19 | import "phoenix_html"; 20 | // Establish Phoenix Socket and LiveView configuration. 21 | import { Socket } from "phoenix"; 22 | import { LiveSocket } from "phoenix_live_view"; 23 | import topbar from "../vendor/topbar"; 24 | import { createCaptureHook, createPlayerHook } from "membrane_webrtc_plugin"; 25 | 26 | const iceServers = [{ urls: "stun:stun.l.google.com:19302" }]; 27 | 28 | let hooks = {}; 29 | hooks.Capture = createCaptureHook(iceServers); 30 | hooks.Player = createPlayerHook(iceServers); 31 | 32 | let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content"); 33 | let liveSocket = new LiveSocket("/live", Socket, { 34 | longPollFallbackMs: 2500, 35 | params: { _csrf_token: csrfToken }, 36 | hooks: hooks, 37 | }); 38 | 39 | // Show progress bar on live navigation and form submits 40 | topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" }); 41 | window.addEventListener("phx:page-loading-start", (_info) => topbar.show(300)); 42 | window.addEventListener("phx:page-loading-stop", (_info) => topbar.hide()); 43 | 44 | // connect if there are any LiveViews on the page 45 | liveSocket.connect(); 46 | 47 | // expose liveSocket on window for web console debug logs and latency simulation: 48 | // >> liveSocket.enableDebug() 49 | // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session 50 | // >> liveSocket.disableLatencySim() 51 | window.liveSocket = liveSocket; 52 | -------------------------------------------------------------------------------- /examples/live_view/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule WebrtcLiveView.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :webrtc_live_view, 7 | version: "0.1.0", 8 | elixir: "~> 1.14", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | start_permanent: Mix.env() == :prod, 11 | aliases: aliases(), 12 | deps: deps() 13 | ] 14 | end 15 | 16 | # Configuration for the OTP application. 17 | # 18 | # Type `mix help compile.app` for more information. 19 | def application do 20 | [ 21 | mod: {WebrtcLiveView.Application, []}, 22 | extra_applications: [:logger, :runtime_tools] 23 | ] 24 | end 25 | 26 | # Specifies which paths to compile per environment. 27 | defp elixirc_paths(:test), do: ["lib", "test/support"] 28 | defp elixirc_paths(_), do: ["lib"] 29 | 30 | # Specifies your project dependencies. 31 | # 32 | # Type `mix help deps` for examples and options. 33 | defp deps do 34 | [ 35 | {:membrane_webrtc_plugin, path: "../.."}, 36 | {:phoenix, "~> 1.7.21"}, 37 | {:phoenix_html, "~> 4.1"}, 38 | {:phoenix_live_reload, "~> 1.2", only: :dev}, 39 | {:phoenix_live_view, "~> 1.0"}, 40 | {:floki, ">= 0.30.0", only: :test}, 41 | {:phoenix_live_dashboard, "~> 0.8.3"}, 42 | {:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, 43 | {:telemetry_metrics, "~> 1.0"}, 44 | {:telemetry_poller, "~> 1.0"}, 45 | {:jason, "~> 1.2"}, 46 | {:dns_cluster, "~> 0.1.1"}, 47 | {:bandit, "~> 1.5"} 48 | ] 49 | end 50 | 51 | # Aliases are shortcuts or tasks specific to the current project. 52 | # For example, to install project dependencies and perform other setup tasks, run: 53 | # 54 | # $ mix setup 55 | # 56 | # See the documentation for `Mix` for more info on aliases. 57 | defp aliases do 58 | [ 59 | setup: ["deps.get", "assets.setup", "assets.build"], 60 | "assets.setup": ["esbuild.install --if-missing"], 61 | "assets.build": ["esbuild webrtc_live_view"], 62 | "assets.deploy": [ 63 | "esbuild webrtc_live_view --minify", 64 | "phx.digest" 65 | ] 66 | ] 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixSignaling.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :phoenix_signaling, 7 | version: "0.1.0", 8 | elixir: "~> 1.14", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | start_permanent: Mix.env() == :prod, 11 | aliases: aliases(), 12 | deps: deps() 13 | ] 14 | end 15 | 16 | # Configuration for the OTP application. 17 | # 18 | # Type `mix help compile.app` for more information. 19 | def application do 20 | [ 21 | mod: {PhoenixSignaling.Application, []}, 22 | extra_applications: [:logger, :runtime_tools] 23 | ] 24 | end 25 | 26 | # Specifies which paths to compile per environment. 27 | defp elixirc_paths(:test), do: ["lib", "test/support"] 28 | defp elixirc_paths(_), do: ["lib"] 29 | 30 | # Specifies your project dependencies. 31 | # 32 | # Type `mix help deps` for examples and options. 33 | defp deps do 34 | [ 35 | {:phoenix, "~> 1.7.20"}, 36 | {:phoenix_html, "~> 4.1"}, 37 | {:phoenix_live_reload, "~> 1.2", only: :dev}, 38 | {:phoenix_live_view, "~> 1.0.0"}, 39 | {:floki, ">= 0.30.0", only: :test}, 40 | {:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, 41 | {:telemetry_metrics, "~> 1.0"}, 42 | {:telemetry_poller, "~> 1.0"}, 43 | {:jason, "~> 1.2"}, 44 | {:dns_cluster, "~> 0.1.1"}, 45 | {:bandit, "~> 1.5"}, 46 | {:boombox, github: "membraneframework/boombox"}, 47 | {:membrane_webrtc_plugin, path: "#{__DIR__}/../..", override: true}, 48 | {:uuid, "~> 1.1"} 49 | ] 50 | end 51 | 52 | # Aliases are shortcuts or tasks specific to the current project. 53 | # For example, to install project dependencies and perform other setup tasks, run: 54 | # 55 | # $ mix setup 56 | # 57 | # See the documentation for `Mix` for more info on aliases. 58 | defp aliases do 59 | [ 60 | setup: ["deps.get", "assets.setup", "assets.build"], 61 | "assets.setup": ["esbuild.install --if-missing"], 62 | "assets.build": ["esbuild phoenix_signaling"], 63 | "assets.deploy": [ 64 | "esbuild phoenix_signaling --minify", 65 | "phx.digest" 66 | ] 67 | ] 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /assets/capture.js: -------------------------------------------------------------------------------- 1 | export function createCaptureHook(iceServers = [{ urls: `stun:stun.l.google.com:19302` }]) { 2 | return { 3 | async mounted() { 4 | this.handleEvent(`media_constraints-${this.el.id}`, async (mediaConstraints) => { 5 | console.log(`[${this.el.id}] Received media constraints:`, mediaConstraints); 6 | 7 | const localStream = await navigator.mediaDevices.getUserMedia(mediaConstraints); 8 | const pcConfig = { iceServers: iceServers }; 9 | this.pc = new RTCPeerConnection(pcConfig); 10 | 11 | this.pc.onicecandidate = (event) => { 12 | if (event.candidate === null) return; 13 | console.log(`[${this.el.id}] Sent ICE candidate:`, event.candidate); 14 | message = { type: `ice_candidate`, data: event.candidate }; 15 | this.pushEventTo(this.el, `webrtc_signaling`, message); 16 | }; 17 | 18 | this.pc.onconnectionstatechange = () => { 19 | console.log( 20 | `[${this.el.id}] RTCPeerConnection state changed to`, 21 | this.pc.connectionState 22 | ); 23 | }; 24 | 25 | this.el.srcObject = new MediaStream(); 26 | 27 | for (const track of localStream.getTracks()) { 28 | this.pc.addTrack(track, localStream); 29 | this.el.srcObject.addTrack(track); 30 | } 31 | 32 | this.el.play(); 33 | 34 | this.handleEvent(`webrtc_signaling-${this.el.id}`, async (event) => { 35 | const { type, data } = event; 36 | 37 | switch (type) { 38 | case `sdp_answer`: 39 | console.log(`[${this.el.id}] Received SDP answer:`, data); 40 | await this.pc.setRemoteDescription(data); 41 | break; 42 | case `ice_candidate`: 43 | console.log(`[${this.el.id}] Received ICE candidate:`, data); 44 | await this.pc.addIceCandidate(data); 45 | break; 46 | } 47 | }); 48 | 49 | const offer = await this.pc.createOffer(); 50 | await this.pc.setLocalDescription(offer); 51 | console.log(`[${this.el.id}] Sent SDP offer:`, offer); 52 | message = { type: `sdp_offer`, data: offer }; 53 | this.pushEventTo(this.el, `webrtc_signaling`, message); 54 | }); 55 | }, 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /examples/websocket_signaling/browser_to_file.exs: -------------------------------------------------------------------------------- 1 | # This example receives audio and video from a browser via WebRTC 2 | # and saves it to a `recording.mkv` file. 3 | # To run it, type `elixir browser_to_file.exs` and open 4 | # http://localhost:8000/index.html in your browser. To finish recording, 5 | # click the `disconnect` button or close the tab. 6 | 7 | require Logger 8 | Logger.configure(level: :info) 9 | 10 | Mix.install([ 11 | {:membrane_webrtc_plugin, path: "#{__DIR__}/../.."}, 12 | :membrane_file_plugin, 13 | :membrane_realtimer_plugin, 14 | :membrane_matroska_plugin, 15 | :membrane_opus_plugin, 16 | :membrane_h264_plugin 17 | ]) 18 | 19 | defmodule Example.Pipeline do 20 | use Membrane.Pipeline 21 | 22 | alias Membrane.WebRTC 23 | 24 | @impl true 25 | def handle_init(_ctx, opts) do 26 | spec = 27 | [ 28 | child(:webrtc, %WebRTC.Source{ 29 | signaling: { 30 | :whip, 31 | token: "whip_it!", 32 | port: opts[:port], 33 | ip: :any, 34 | serve_static: "#{__DIR__}/assets/browser_to_file" 35 | } 36 | }), 37 | child(:matroska, Membrane.Matroska.Muxer), 38 | get_child(:webrtc) 39 | |> via_out(:output, options: [kind: :audio]) 40 | |> child(Membrane.Opus.Parser) 41 | |> get_child(:matroska), 42 | get_child(:webrtc) 43 | |> via_out(:output, options: [kind: :video]) 44 | |> get_child(:matroska), 45 | get_child(:matroska) 46 | |> child(:sink, %Membrane.File.Sink{location: "recording.mkv"}) 47 | ] 48 | 49 | {[spec: spec], %{}} 50 | end 51 | 52 | @impl true 53 | def handle_element_end_of_stream(:sink, :input, _ctx, state) do 54 | {[terminate: :normal], state} 55 | end 56 | 57 | @impl true 58 | def handle_element_end_of_stream(_element, _pad, _ctx, state) do 59 | {[], state} 60 | end 61 | end 62 | 63 | port = 8829 64 | {:ok, supervisor, _pipeline} = Membrane.Pipeline.start_link(Example.Pipeline, port: port) 65 | Process.monitor(supervisor) 66 | 67 | Logger.info(""" 68 | Visit http://localhost:#{port}/static/index.html to start the stream. To finish the recording properly, 69 | don't terminate this script - instead click 'disconnect' in the website or close the browser tab. 70 | """) 71 | 72 | receive do 73 | {:DOWN, _ref, :process, ^supervisor, _reason} -> :ok 74 | end 75 | -------------------------------------------------------------------------------- /lib/membrane_webrtc/phoenix_signaling/socket.ex: -------------------------------------------------------------------------------- 1 | if Code.ensure_loaded?(Phoenix) do 2 | defmodule Membrane.WebRTC.PhoenixSignaling.Socket do 3 | @moduledoc """ 4 | Phoenix Socket implementation which redirects all topics to a 5 | Phoenix Channel capable of processing WebRTC signaling messages. 6 | 7 | *Note:* This module will be available in your code only if you add `:phoenix` 8 | to the dependencies of of your root project. 9 | 10 | To use PhoenixSignaling, you need to: 11 | 1. Create Socket in your application endpoint, for instance: 12 | ``` 13 | socket "/signaling", Membrane.WebRTC.PhoenixSignaling.Socket, 14 | websocket: true, 15 | longpoll: false 16 | ``` 17 | 2. Create a Phoenix signaling channel with desired signaling ID and use it as `Membrane.WebRTC.Signaling.t()` 18 | for `Membrane.WebRTC.Source`, `Membrane.WebRTC.Sink` or [`Boombox`](https://github.com/membraneframework/boombox): 19 | ``` 20 | signaling = Membrane.WebRTC.PhoenixSignaling.new("") 21 | 22 | # use it with Membrane.WebRTC.Source: 23 | child(:webrtc_source, %Membrane.WebRTC.Source{signaling: signaling}) 24 | |> ... 25 | 26 | # or with Membrane.WebRTC.Sink: 27 | ... 28 | |> child(:webrtc_sink, %Membrane.WebRTC.Sink{signaling: signaling}) 29 | 30 | # or with Boombox: 31 | Boombox.run( 32 | input: {:webrtc, signaling}, 33 | output: ... 34 | ) 35 | ``` 36 | 2. Create signaling channel with desired signaling ID: 37 | ``` 38 | signaling = PhoenixSignaling.new("signaling_id") 39 | ``` 40 | 3. Use the Phoenix Socket to exchange WebRTC signaling data: 41 | ``` 42 | let socket = new Socket("/singaling", {params: {token: window.userToken}}) 43 | socket.connect() 44 | let channel = socket.channel('signaling_id') 45 | channel.join() 46 | .receive("ok", resp => { console.log("Joined successfully", resp) 47 | // here you can exchange WebRTC data 48 | }) 49 | .receive("error", resp => { console.log("Unable to join", resp) }) 50 | 51 | ``` 52 | """ 53 | use Phoenix.Socket 54 | 55 | channel("*", Membrane.WebRTC.PhoenixSignaling.Channel) 56 | 57 | @impl true 58 | def connect(_params, socket, _connect_info) do 59 | {:ok, socket} 60 | end 61 | 62 | @impl true 63 | def id(_socket), do: nil 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/membrane_webrtc/ex_webrtc/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.WebRTC.ExWebRTCUtils do 2 | @moduledoc false 3 | 4 | alias ExWebRTC.RTPCodecParameters 5 | 6 | @type codec :: :opus | :h264 | :vp8 7 | @type codec_or_codecs :: codec() | [codec()] 8 | 9 | @spec codec_params(codec_or_codecs()) :: [RTPCodecParameters.t()] 10 | def codec_params(:opus), 11 | do: [ 12 | %RTPCodecParameters{ 13 | payload_type: 111, 14 | mime_type: "audio/opus", 15 | clock_rate: codec_clock_rate(:opus), 16 | channels: 2 17 | } 18 | ] 19 | 20 | def codec_params(:h264) do 21 | [ 22 | %RTPCodecParameters{ 23 | payload_type: 96, 24 | mime_type: "video/H264", 25 | clock_rate: codec_clock_rate(:h264), 26 | sdp_fmtp_line: %ExSDP.Attribute.FMTP{ 27 | pt: 96, 28 | level_asymmetry_allowed: true, 29 | packetization_mode: 1, 30 | profile_level_id: 0x42E01F 31 | } 32 | } 33 | ] 34 | end 35 | 36 | def codec_params(:vp8) do 37 | [ 38 | %RTPCodecParameters{ 39 | payload_type: 102, 40 | mime_type: "video/VP8", 41 | clock_rate: codec_clock_rate(:vp8) 42 | } 43 | ] 44 | end 45 | 46 | def codec_params(codecs) when is_list(codecs) do 47 | codecs |> Enum.flat_map(&codec_params/1) 48 | end 49 | 50 | @spec codec_clock_rate(codec_or_codecs()) :: pos_integer() 51 | def codec_clock_rate(:opus), do: 48_000 52 | def codec_clock_rate(:vp8), do: 90_000 53 | def codec_clock_rate(:h264), do: 90_000 54 | 55 | def codec_clock_rate(codecs) when is_list(codecs) do 56 | cond do 57 | codecs == [:opus] -> 58 | 48_000 59 | 60 | codecs != [] and Enum.all?(codecs, &(&1 in [:vp8, :h264])) -> 61 | 90_000 62 | end 63 | end 64 | 65 | @spec get_video_codecs_from_sdp(ExWebRTC.SessionDescription.t()) :: [:h264 | :vp8] 66 | def get_video_codecs_from_sdp(%ExWebRTC.SessionDescription{sdp: sdp}) do 67 | ex_sdp = ExSDP.parse!(sdp) 68 | 69 | ex_sdp.media 70 | |> Enum.flat_map(fn 71 | %{type: :video, attributes: attributes} -> attributes 72 | _media -> [] 73 | end) 74 | |> Enum.flat_map(fn 75 | %ExSDP.Attribute.RTPMapping{encoding: "H264"} -> [:h264] 76 | %ExSDP.Attribute.RTPMapping{encoding: "VP8"} -> [:vp8] 77 | _attribute -> [] 78 | end) 79 | |> Enum.uniq() 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /examples/live_view/lib/webrtc_live_view_web/telemetry.ex: -------------------------------------------------------------------------------- 1 | defmodule WebrtcLiveViewWeb.Telemetry do 2 | use Supervisor 3 | import Telemetry.Metrics 4 | 5 | def start_link(arg) do 6 | Supervisor.start_link(__MODULE__, arg, name: __MODULE__) 7 | end 8 | 9 | @impl true 10 | def init(_arg) do 11 | children = [ 12 | # Telemetry poller will execute the given period measurements 13 | # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics 14 | {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} 15 | # Add reporters as children of your supervision tree. 16 | # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} 17 | ] 18 | 19 | Supervisor.init(children, strategy: :one_for_one) 20 | end 21 | 22 | def metrics do 23 | [ 24 | # Phoenix Metrics 25 | summary("phoenix.endpoint.start.system_time", 26 | unit: {:native, :millisecond} 27 | ), 28 | summary("phoenix.endpoint.stop.duration", 29 | unit: {:native, :millisecond} 30 | ), 31 | summary("phoenix.router_dispatch.start.system_time", 32 | tags: [:route], 33 | unit: {:native, :millisecond} 34 | ), 35 | summary("phoenix.router_dispatch.exception.duration", 36 | tags: [:route], 37 | unit: {:native, :millisecond} 38 | ), 39 | summary("phoenix.router_dispatch.stop.duration", 40 | tags: [:route], 41 | unit: {:native, :millisecond} 42 | ), 43 | summary("phoenix.socket_connected.duration", 44 | unit: {:native, :millisecond} 45 | ), 46 | sum("phoenix.socket_drain.count"), 47 | summary("phoenix.channel_joined.duration", 48 | unit: {:native, :millisecond} 49 | ), 50 | summary("phoenix.channel_handled_in.duration", 51 | tags: [:event], 52 | unit: {:native, :millisecond} 53 | ), 54 | 55 | # VM Metrics 56 | summary("vm.memory.total", unit: {:byte, :kilobyte}), 57 | summary("vm.total_run_queue_lengths.total"), 58 | summary("vm.total_run_queue_lengths.cpu"), 59 | summary("vm.total_run_queue_lengths.io") 60 | ] 61 | end 62 | 63 | defp periodic_measurements do 64 | [ 65 | # A module, function and arguments to be invoked periodically. 66 | # This function must call :telemetry.execute/3 and a metric must be added above. 67 | # {WebrtcLiveViewWeb, :count_users, []} 68 | ] 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/lib/phoenix_signaling_web/telemetry.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixSignalingWeb.Telemetry do 2 | use Supervisor 3 | import Telemetry.Metrics 4 | 5 | def start_link(arg) do 6 | Supervisor.start_link(__MODULE__, arg, name: __MODULE__) 7 | end 8 | 9 | @impl true 10 | def init(_arg) do 11 | children = [ 12 | # Telemetry poller will execute the given period measurements 13 | # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics 14 | {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} 15 | # Add reporters as children of your supervision tree. 16 | # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} 17 | ] 18 | 19 | Supervisor.init(children, strategy: :one_for_one) 20 | end 21 | 22 | def metrics do 23 | [ 24 | # Phoenix Metrics 25 | summary("phoenix.endpoint.start.system_time", 26 | unit: {:native, :millisecond} 27 | ), 28 | summary("phoenix.endpoint.stop.duration", 29 | unit: {:native, :millisecond} 30 | ), 31 | summary("phoenix.router_dispatch.start.system_time", 32 | tags: [:route], 33 | unit: {:native, :millisecond} 34 | ), 35 | summary("phoenix.router_dispatch.exception.duration", 36 | tags: [:route], 37 | unit: {:native, :millisecond} 38 | ), 39 | summary("phoenix.router_dispatch.stop.duration", 40 | tags: [:route], 41 | unit: {:native, :millisecond} 42 | ), 43 | summary("phoenix.socket_connected.duration", 44 | unit: {:native, :millisecond} 45 | ), 46 | sum("phoenix.socket_drain.count"), 47 | summary("phoenix.channel_joined.duration", 48 | unit: {:native, :millisecond} 49 | ), 50 | summary("phoenix.channel_handled_in.duration", 51 | tags: [:event], 52 | unit: {:native, :millisecond} 53 | ), 54 | 55 | # VM Metrics 56 | summary("vm.memory.total", unit: {:byte, :kilobyte}), 57 | summary("vm.total_run_queue_lengths.total"), 58 | summary("vm.total_run_queue_lengths.cpu"), 59 | summary("vm.total_run_queue_lengths.io") 60 | ] 61 | end 62 | 63 | defp periodic_measurements do 64 | [ 65 | # A module, function and arguments to be invoked periodically. 66 | # This function must call :telemetry.execute/3 and a metric must be added above. 67 | # {PhoenixSignalingWeb, :count_users, []} 68 | ] 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /examples/live_view/config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we can use it 8 | # to bundle .js and .css sources. 9 | config :webrtc_live_view, WebrtcLiveViewWeb.Endpoint, 10 | # Binding to loopback ipv4 address prevents access from other machines. 11 | # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. 12 | http: [ip: {127, 0, 0, 1}, port: 4000], 13 | check_origin: false, 14 | code_reloader: true, 15 | debug_errors: true, 16 | secret_key_base: "hvkMsB0coySlkK38GdeYwpOMBEFJFmK/ogj8SD791OFVAxlk89y1fOGkumXlYgIH", 17 | watchers: [ 18 | esbuild: {Esbuild, :install_and_run, [:webrtc_live_view, ~w(--sourcemap=inline --watch)]} 19 | ] 20 | 21 | # ## SSL Support 22 | # 23 | # In order to use HTTPS in development, a self-signed 24 | # certificate can be generated by running the following 25 | # Mix task: 26 | # 27 | # mix phx.gen.cert 28 | # 29 | # Run `mix help phx.gen.cert` for more information. 30 | # 31 | # The `http:` config above can be replaced with: 32 | # 33 | # https: [ 34 | # port: 4001, 35 | # cipher_suite: :strong, 36 | # keyfile: "priv/cert/selfsigned_key.pem", 37 | # certfile: "priv/cert/selfsigned.pem" 38 | # ], 39 | # 40 | # If desired, both `http:` and `https:` keys can be 41 | # configured to run both http and https servers on 42 | # different ports. 43 | 44 | # Watch static and templates for browser reloading. 45 | config :webrtc_live_view, WebrtcLiveViewWeb.Endpoint, 46 | live_reload: [ 47 | patterns: [ 48 | ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$", 49 | ~r"lib/webrtc_live_view_web/(controllers|live|components)/.*(ex|heex)$" 50 | ] 51 | ] 52 | 53 | # Enable dev routes for dashboard and mailbox 54 | config :webrtc_live_view, dev_routes: true 55 | 56 | # Do not include metadata nor timestamps in development logs 57 | config :logger, :console, format: "[$level] $message\n" 58 | 59 | # Set a higher stacktrace during development. Avoid configuring such 60 | # in production as building large stacktraces may be expensive. 61 | config :phoenix, :stacktrace_depth, 20 62 | 63 | # Initialize plugs at runtime for faster development compilation 64 | config :phoenix, :plug_init_mode, :runtime 65 | 66 | config :phoenix_live_view, 67 | # Include HEEx debug annotations as HTML comments in rendered markup 68 | debug_heex_annotations: true, 69 | # Enable helpful, but potentially expensive runtime checks 70 | enable_expensive_runtime_checks: true 71 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we can use it 8 | # to bundle .js and .css sources. 9 | # Binding to loopback ipv4 address prevents access from other machines. 10 | config :phoenix_signaling, PhoenixSignalingWeb.Endpoint, 11 | # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. 12 | http: [ip: {127, 0, 0, 1}, port: 4000], 13 | check_origin: false, 14 | code_reloader: true, 15 | debug_errors: true, 16 | secret_key_base: "ww7+NbKIOykx/9RbfIWdgDlgjsPTX84lIGmqiwuxMeyTakzcYsCbb7XgPDKv1/jy", 17 | watchers: [ 18 | esbuild: {Esbuild, :install_and_run, [:phoenix_signaling, ~w(--sourcemap=inline --watch)]} 19 | ] 20 | 21 | # ## SSL Support 22 | # 23 | # In order to use HTTPS in development, a self-signed 24 | # certificate can be generated by running the following 25 | # Mix task: 26 | # 27 | # mix phx.gen.cert 28 | # 29 | # Run `mix help phx.gen.cert` for more information. 30 | # 31 | # The `http:` config above can be replaced with: 32 | # 33 | # https: [ 34 | # port: 4001, 35 | # cipher_suite: :strong, 36 | # keyfile: "priv/cert/selfsigned_key.pem", 37 | # certfile: "priv/cert/selfsigned.pem" 38 | # ], 39 | # 40 | # If desired, both `http:` and `https:` keys can be 41 | # configured to run both http and https servers on 42 | # different ports. 43 | 44 | # Watch static and templates for browser reloading. 45 | config :phoenix_signaling, PhoenixSignalingWeb.Endpoint, 46 | live_reload: [ 47 | patterns: [ 48 | ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$", 49 | ~r"lib/phoenix_signaling_web/(controllers|live|components)/.*(ex|heex)$" 50 | ] 51 | ] 52 | 53 | # Enable dev routes for dashboard and mailbox 54 | config :phoenix_signaling, dev_routes: true 55 | 56 | # Do not include metadata nor timestamps in development logs 57 | config :logger, :console, format: "[$level] $message\n" 58 | 59 | # Set a higher stacktrace during development. Avoid configuring such 60 | # in production as building large stacktraces may be expensive. 61 | config :phoenix, :stacktrace_depth, 20 62 | 63 | # Initialize plugs at runtime for faster development compilation 64 | config :phoenix, :plug_init_mode, :runtime 65 | 66 | config :phoenix_live_view, 67 | # Include HEEx debug annotations as HTML comments in rendered markup 68 | debug_heex_annotations: true, 69 | # Enable helpful, but potentially expensive runtime checks 70 | enable_expensive_runtime_checks: true 71 | -------------------------------------------------------------------------------- /examples/live_view/priv/static/images/logo.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/priv/static/images/logo.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /examples/live_view/lib/webrtc_live_view_web.ex: -------------------------------------------------------------------------------- 1 | defmodule WebrtcLiveViewWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, components, channels, and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use WebrtcLiveViewWeb, :controller 9 | use WebrtcLiveViewWeb, :html 10 | 11 | The definitions below will be executed for every controller, 12 | component, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define additional modules and import 17 | those modules here. 18 | """ 19 | 20 | def static_paths, do: ~w(assets fonts images favicon.ico robots.txt) 21 | 22 | def router do 23 | quote do 24 | use Phoenix.Router, helpers: false 25 | 26 | # Import common connection and controller functions to use in pipelines 27 | import Plug.Conn 28 | import Phoenix.Controller 29 | import Phoenix.LiveView.Router 30 | end 31 | end 32 | 33 | def channel do 34 | quote do 35 | use Phoenix.Channel 36 | end 37 | end 38 | 39 | def controller do 40 | quote do 41 | use Phoenix.Controller, 42 | formats: [:html, :json], 43 | layouts: [html: WebrtcLiveViewWeb.Layouts] 44 | 45 | import Plug.Conn 46 | 47 | unquote(verified_routes()) 48 | end 49 | end 50 | 51 | def live_view do 52 | quote do 53 | use Phoenix.LiveView, 54 | layout: {WebrtcLiveViewWeb.Layouts, :app} 55 | 56 | unquote(html_helpers()) 57 | end 58 | end 59 | 60 | def live_component do 61 | quote do 62 | use Phoenix.LiveComponent 63 | 64 | unquote(html_helpers()) 65 | end 66 | end 67 | 68 | def html do 69 | quote do 70 | use Phoenix.Component 71 | 72 | # Import convenience functions from controllers 73 | import Phoenix.Controller, 74 | only: [get_csrf_token: 0, view_module: 1, view_template: 1] 75 | 76 | # Include general helpers for rendering HTML 77 | unquote(html_helpers()) 78 | end 79 | end 80 | 81 | defp html_helpers do 82 | quote do 83 | # HTML escaping functionality 84 | import Phoenix.HTML 85 | # Core UI components 86 | import WebrtcLiveViewWeb.CoreComponents 87 | 88 | # Shortcut for generating JS commands 89 | alias Phoenix.LiveView.JS 90 | 91 | # Routes generation with the ~p sigil 92 | unquote(verified_routes()) 93 | end 94 | end 95 | 96 | def verified_routes do 97 | quote do 98 | use Phoenix.VerifiedRoutes, 99 | endpoint: WebrtcLiveViewWeb.Endpoint, 100 | router: WebrtcLiveViewWeb.Router, 101 | statics: WebrtcLiveViewWeb.static_paths() 102 | end 103 | end 104 | 105 | @doc """ 106 | When used, dispatch to the appropriate controller/live_view/etc. 107 | """ 108 | defmacro __using__(which) when is_atom(which) do 109 | apply(__MODULE__, which, []) 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/lib/phoenix_signaling_web.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixSignalingWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, components, channels, and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use PhoenixSignalingWeb, :controller 9 | use PhoenixSignalingWeb, :html 10 | 11 | The definitions below will be executed for every controller, 12 | component, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define additional modules and import 17 | those modules here. 18 | """ 19 | 20 | def static_paths, do: ~w(assets fonts images favicon.ico robots.txt) 21 | 22 | def router do 23 | quote do 24 | use Phoenix.Router, helpers: false 25 | 26 | # Import common connection and controller functions to use in pipelines 27 | import Plug.Conn 28 | import Phoenix.Controller 29 | import Phoenix.LiveView.Router 30 | end 31 | end 32 | 33 | def channel do 34 | quote do 35 | use Phoenix.Channel 36 | end 37 | end 38 | 39 | def controller do 40 | quote do 41 | use Phoenix.Controller, 42 | formats: [:html, :json], 43 | layouts: [html: PhoenixSignalingWeb.Layouts] 44 | 45 | import Plug.Conn 46 | 47 | unquote(verified_routes()) 48 | end 49 | end 50 | 51 | def live_view do 52 | quote do 53 | use Phoenix.LiveView, 54 | layout: {PhoenixSignalingWeb.Layouts, :app} 55 | 56 | unquote(html_helpers()) 57 | end 58 | end 59 | 60 | def live_component do 61 | quote do 62 | use Phoenix.LiveComponent 63 | 64 | unquote(html_helpers()) 65 | end 66 | end 67 | 68 | def html do 69 | quote do 70 | use Phoenix.Component 71 | 72 | # Import convenience functions from controllers 73 | import Phoenix.Controller, 74 | only: [get_csrf_token: 0, view_module: 1, view_template: 1] 75 | 76 | # Include general helpers for rendering HTML 77 | unquote(html_helpers()) 78 | end 79 | end 80 | 81 | defp html_helpers do 82 | quote do 83 | # HTML escaping functionality 84 | import Phoenix.HTML 85 | # Core UI components 86 | import PhoenixSignalingWeb.CoreComponents 87 | 88 | # Shortcut for generating JS commands 89 | alias Phoenix.LiveView.JS 90 | 91 | # Routes generation with the ~p sigil 92 | unquote(verified_routes()) 93 | end 94 | end 95 | 96 | def verified_routes do 97 | quote do 98 | use Phoenix.VerifiedRoutes, 99 | endpoint: PhoenixSignalingWeb.Endpoint, 100 | router: PhoenixSignalingWeb.Router, 101 | statics: PhoenixSignalingWeb.static_paths() 102 | end 103 | end 104 | 105 | @doc """ 106 | When used, dispatch to the appropriate controller/live_view/etc. 107 | """ 108 | defmacro __using__(which) when is_atom(which) do 109 | apply(__MODULE__, which, []) 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.WebRTC.Plugin.Mixfile do 2 | use Mix.Project 3 | 4 | @version "0.26.1" 5 | @github_url "https://github.com/membraneframework/membrane_webrtc_plugin" 6 | 7 | def project do 8 | [ 9 | app: :membrane_webrtc_plugin, 10 | version: @version, 11 | elixir: "~> 1.13", 12 | elixirc_paths: elixirc_paths(Mix.env()), 13 | start_permanent: Mix.env() == :prod, 14 | deps: deps(), 15 | dialyzer: dialyzer(), 16 | 17 | # hex 18 | description: "Membrane WebRTC plugin", 19 | package: package(), 20 | 21 | # docs 22 | name: "Membrane WebRTC plugin", 23 | source_url: @github_url, 24 | docs: docs() 25 | ] 26 | end 27 | 28 | def application do 29 | [ 30 | mod: {Membrane.WebRTC.App, []}, 31 | extra_applications: [] 32 | ] 33 | end 34 | 35 | defp elixirc_paths(:test), do: ["lib", "test/support"] 36 | defp elixirc_paths(_env), do: ["lib"] 37 | 38 | defp deps do 39 | [ 40 | # Phoenix 41 | {:phoenix, ">= 0.0.0", optional: true}, 42 | {:phoenix_live_view, "~> 1.0", optional: true}, 43 | 44 | # Membrane 45 | {:membrane_core, "~> 1.2 and >= 1.2.2"}, 46 | {:membrane_rtp_plugin, "~> 0.31.1"}, 47 | {:membrane_rtp_h264_plugin, "~> 0.20.1"}, 48 | {:membrane_rtp_vp8_plugin, "~> 0.9.4"}, 49 | {:membrane_rtp_opus_plugin, "~> 0.10.0"}, 50 | 51 | # Other dependencies 52 | {:ex_webrtc, "~> 0.15.0"}, 53 | {:corsica, "~> 2.0"}, 54 | {:bandit, "~> 1.2"}, 55 | {:websock_adapter, "~> 0.5.0"}, 56 | {:req, "~> 0.5"}, 57 | {:membrane_matroska_plugin, "~> 0.5.0", only: :test}, 58 | {:membrane_mp4_plugin, "~> 0.35.2", only: :test}, 59 | {:membrane_h26x_plugin, "~> 0.10.2", only: :test}, 60 | {:membrane_file_plugin, "~> 0.17.0", only: :test}, 61 | {:membrane_realtimer_plugin, "~> 0.10.0", only: :test}, 62 | {:membrane_opus_plugin, "~> 0.20.0", only: :test}, 63 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, 64 | {:dialyxir, ">= 0.0.0", only: :dev, runtime: false}, 65 | {:credo, ">= 0.0.0", only: :dev, runtime: false} 66 | ] 67 | end 68 | 69 | defp dialyzer() do 70 | opts = [ 71 | flags: [:error_handling] 72 | ] 73 | 74 | if System.get_env("CI") == "true" do 75 | # Store PLTs in cacheable directory for CI 76 | [plt_local_path: "priv/plts", plt_core_path: "priv/plts"] ++ opts 77 | else 78 | opts 79 | end 80 | end 81 | 82 | defp package do 83 | [ 84 | maintainers: ["Membrane Team"], 85 | files: ~w(mix.exs lib assets package.json README.md LICENSE), 86 | licenses: ["Apache-2.0"], 87 | links: %{ 88 | "GitHub" => @github_url, 89 | "Membrane Framework Homepage" => "https://membrane.stream" 90 | } 91 | ] 92 | end 93 | 94 | defp docs do 95 | [ 96 | main: "readme", 97 | extras: ["README.md", "LICENSE"], 98 | formatters: ["html"], 99 | source_ref: "v#{@version}", 100 | nest_modules_by_prefix: [Membrane.WebRTC] 101 | ] 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /examples/websocket_signaling/file_to_browser.exs: -------------------------------------------------------------------------------- 1 | # This example reads a short part of the Big Buck Bunny movie 2 | # from an `.mkv` file and streams it to a browser. 3 | # To run it, type `elixir file_to_browser.exs` and open 4 | # http://localhost:8000/index.html in your browser. 5 | # Note that due to browsers' policy, you need to manually unmute 6 | # audio in the player to hear the sound. 7 | 8 | require Logger 9 | Logger.configure(level: :info) 10 | 11 | Mix.install([ 12 | {:membrane_webrtc_plugin, path: "#{__DIR__}/../.."}, 13 | :membrane_file_plugin, 14 | :membrane_realtimer_plugin, 15 | :membrane_matroska_plugin, 16 | :membrane_opus_plugin 17 | ]) 18 | 19 | defmodule Example.Pipeline do 20 | use Membrane.Pipeline 21 | 22 | alias Membrane.WebRTC 23 | 24 | @impl true 25 | def handle_init(_ctx, opts) do 26 | spec = 27 | child(%Membrane.File.Source{location: "#{__DIR__}/assets/bbb_vp8.mkv"}) 28 | |> child(:demuxer, Membrane.Matroska.Demuxer) 29 | 30 | {[spec: spec], %{audio_track: nil, video_track: nil, port: opts[:port]}} 31 | end 32 | 33 | @impl true 34 | def handle_child_notification({:new_track, {id, info}}, :demuxer, _ctx, state) do 35 | state = 36 | case info.codec do 37 | :opus -> %{state | audio_track: id} 38 | :h264 -> %{state | video_track: id} 39 | :vp8 -> %{state | video_track: id} 40 | end 41 | 42 | if state.audio_track && state.video_track do 43 | spec = [ 44 | child(:webrtc, %WebRTC.Sink{signaling: {:whip, uri: "http://localhost:8888"}}), 45 | get_child(:demuxer) 46 | |> via_out(Pad.ref(:output, state.video_track)) 47 | |> child({:realtimer, :video_track}, Membrane.Realtimer) 48 | |> via_in(Pad.ref(:input, :video_track), options: [kind: :video]) 49 | |> get_child(:webrtc), 50 | get_child(:demuxer) 51 | |> via_out(Pad.ref(:output, state.audio_track)) 52 | |> child({:realtimer, :audio_track}, Membrane.Realtimer) 53 | |> via_in(Pad.ref(:input, :audio_track), options: [kind: :audio]) 54 | |> get_child(:webrtc) 55 | ] 56 | 57 | {[spec: spec], state} 58 | else 59 | {[], state} 60 | end 61 | end 62 | 63 | @impl true 64 | def handle_child_notification({:end_of_stream, track}, :webrtc, _ctx, state) do 65 | state = %{state | track => nil} 66 | 67 | if !state.audio_track && !state.video_track do 68 | {[terminate: :normal], state} 69 | else 70 | {[], state} 71 | end 72 | end 73 | 74 | @impl true 75 | def handle_child_notification(_notification, _child, _ctx, state) do 76 | {[], state} 77 | end 78 | end 79 | 80 | {:ok, supervisor, _pipeline} = Membrane.Pipeline.start_link(Example.Pipeline, port: 8829) 81 | Process.monitor(supervisor) 82 | :ok = :inets.start() 83 | 84 | {:ok, _server} = 85 | :inets.start(:httpd, 86 | bind_address: ~c"localhost", 87 | port: 8000, 88 | document_root: ~c"#{__DIR__}/assets/file_to_browser", 89 | server_name: ~c"webrtc", 90 | server_root: "/tmp" 91 | ) 92 | 93 | Logger.info(""" 94 | The stream is available at http://localhost:8000/index.html. 95 | """) 96 | 97 | receive do 98 | {:DOWN, _ref, :process, ^supervisor, _reason} -> :ok 99 | end 100 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | compile_commands.json 2 | .gdb_history 3 | bundlex.sh 4 | bundlex.bat 5 | 6 | # Dir generated by tmp_dir ExUnit tag 7 | /tmp/ 8 | 9 | # Created by https://www.gitignore.io/api/c,vim,linux,macos,elixir,windows,visualstudiocode 10 | # Edit at https://www.gitignore.io/?templates=c,vim,linux,macos,elixir,windows,visualstudiocode 11 | 12 | ### C ### 13 | # Prerequisites 14 | *.d 15 | 16 | # Object files 17 | *.o 18 | *.ko 19 | *.obj 20 | *.elf 21 | 22 | # Linker output 23 | *.ilk 24 | *.map 25 | *.exp 26 | 27 | # Precompiled Headers 28 | *.gch 29 | *.pch 30 | 31 | # Libraries 32 | *.lib 33 | *.a 34 | *.la 35 | *.lo 36 | 37 | # Shared objects (inc. Windows DLLs) 38 | *.dll 39 | *.so 40 | *.so.* 41 | *.dylib 42 | 43 | # Executables 44 | *.exe 45 | *.out 46 | *.app 47 | *.i*86 48 | *.x86_64 49 | *.hex 50 | 51 | # Debug files 52 | *.dSYM/ 53 | *.su 54 | *.idb 55 | *.pdb 56 | 57 | # Kernel Module Compile Results 58 | *.mod* 59 | *.cmd 60 | .tmp_versions/ 61 | modules.order 62 | Module.symvers 63 | Mkfile.old 64 | dkms.conf 65 | 66 | ### Elixir ### 67 | /_build 68 | /cover 69 | /deps 70 | /doc 71 | /.fetch 72 | erl_crash.dump 73 | *.ez 74 | *.beam 75 | /config/*.secret.exs 76 | .elixir_ls/ 77 | 78 | ### Elixir Patch ### 79 | 80 | ### Linux ### 81 | *~ 82 | 83 | # temporary files which can be created if a process still has a handle open of a deleted file 84 | .fuse_hidden* 85 | 86 | # KDE directory preferences 87 | .directory 88 | 89 | # Linux trash folder which might appear on any partition or disk 90 | .Trash-* 91 | 92 | # .nfs files are created when an open file is removed but is still being accessed 93 | .nfs* 94 | 95 | ### macOS ### 96 | # General 97 | .DS_Store 98 | .AppleDouble 99 | .LSOverride 100 | 101 | # Icon must end with two \r 102 | Icon 103 | 104 | # Thumbnails 105 | ._* 106 | 107 | # Files that might appear in the root of a volume 108 | .DocumentRevisions-V100 109 | .fseventsd 110 | .Spotlight-V100 111 | .TemporaryItems 112 | .Trashes 113 | .VolumeIcon.icns 114 | .com.apple.timemachine.donotpresent 115 | 116 | # Directories potentially created on remote AFP share 117 | .AppleDB 118 | .AppleDesktop 119 | Network Trash Folder 120 | Temporary Items 121 | .apdisk 122 | 123 | ### Vim ### 124 | # Swap 125 | [._]*.s[a-v][a-z] 126 | [._]*.sw[a-p] 127 | [._]s[a-rt-v][a-z] 128 | [._]ss[a-gi-z] 129 | [._]sw[a-p] 130 | 131 | # Session 132 | Session.vim 133 | Sessionx.vim 134 | 135 | # Temporary 136 | .netrwhist 137 | # Auto-generated tag files 138 | tags 139 | # Persistent undo 140 | [._]*.un~ 141 | 142 | ### VisualStudioCode ### 143 | .vscode/* 144 | !.vscode/settings.json 145 | !.vscode/tasks.json 146 | !.vscode/launch.json 147 | !.vscode/extensions.json 148 | 149 | ### VisualStudioCode Patch ### 150 | # Ignore all local history of files 151 | .history 152 | 153 | ### Windows ### 154 | # Windows thumbnail cache files 155 | Thumbs.db 156 | Thumbs.db:encryptable 157 | ehthumbs.db 158 | ehthumbs_vista.db 159 | 160 | # Dump file 161 | *.stackdump 162 | 163 | # Folder config file 164 | [Dd]esktop.ini 165 | 166 | # Recycle Bin used on file shares 167 | $RECYCLE.BIN/ 168 | 169 | # Windows Installer files 170 | *.cab 171 | *.msi 172 | *.msix 173 | *.msm 174 | *.msp 175 | 176 | # Windows shortcuts 177 | *.lnk 178 | 179 | # End of https://www.gitignore.io/api/c,vim,linux,macos,elixir,windows,visualstudiocode 180 | -------------------------------------------------------------------------------- /examples/live_view/config/runtime.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # config/runtime.exs is executed for all environments, including 4 | # during releases. It is executed after compilation and before the 5 | # system starts, so it is typically used to load production configuration 6 | # and secrets from environment variables or elsewhere. Do not define 7 | # any compile-time configuration in here, as it won't be applied. 8 | # The block below contains prod specific runtime configuration. 9 | 10 | # ## Using releases 11 | # 12 | # If you use `mix release`, you need to explicitly enable the server 13 | # by passing the PHX_SERVER=true when you start it: 14 | # 15 | # PHX_SERVER=true bin/webrtc_live_view start 16 | # 17 | # Alternatively, you can use `mix phx.gen.release` to generate a `bin/server` 18 | # script that automatically sets the env var above. 19 | if System.get_env("PHX_SERVER") do 20 | config :webrtc_live_view, WebrtcLiveViewWeb.Endpoint, server: true 21 | end 22 | 23 | if config_env() == :prod do 24 | # The secret key base is used to sign/encrypt cookies and other secrets. 25 | # A default value is used in config/dev.exs and config/test.exs but you 26 | # want to use a different value for prod and you most likely don't want 27 | # to check this value into version control, so we use an environment 28 | # variable instead. 29 | secret_key_base = 30 | System.get_env("SECRET_KEY_BASE") || 31 | raise """ 32 | environment variable SECRET_KEY_BASE is missing. 33 | You can generate one by calling: mix phx.gen.secret 34 | """ 35 | 36 | host = System.get_env("PHX_HOST") || "example.com" 37 | port = String.to_integer(System.get_env("PORT") || "4000") 38 | 39 | config :webrtc_live_view, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") 40 | 41 | config :webrtc_live_view, WebrtcLiveViewWeb.Endpoint, 42 | url: [host: host, port: 443, scheme: "https"], 43 | http: [ 44 | # Enable IPv6 and bind on all interfaces. 45 | # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. 46 | # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0 47 | # for details about using IPv6 vs IPv4 and loopback vs public addresses. 48 | ip: {0, 0, 0, 0, 0, 0, 0, 0}, 49 | port: port 50 | ], 51 | secret_key_base: secret_key_base 52 | 53 | # ## SSL Support 54 | # 55 | # To get SSL working, you will need to add the `https` key 56 | # to your endpoint configuration: 57 | # 58 | # config :webrtc_live_view, WebrtcLiveViewWeb.Endpoint, 59 | # https: [ 60 | # ..., 61 | # port: 443, 62 | # cipher_suite: :strong, 63 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 64 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") 65 | # ] 66 | # 67 | # The `cipher_suite` is set to `:strong` to support only the 68 | # latest and more secure SSL ciphers. This means old browsers 69 | # and clients may not be supported. You can set it to 70 | # `:compatible` for wider support. 71 | # 72 | # `:keyfile` and `:certfile` expect an absolute path to the key 73 | # and cert in disk or a relative path inside priv, for example 74 | # "priv/ssl/server.key". For all supported SSL configuration 75 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 76 | # 77 | # We also recommend setting `force_ssl` in your config/prod.exs, 78 | # ensuring no data is ever sent via http, always redirecting to https: 79 | # 80 | # config :webrtc_live_view, WebrtcLiveViewWeb.Endpoint, 81 | # force_ssl: [hsts: true] 82 | # 83 | # Check `Plug.SSL` for all available options in `force_ssl`. 84 | end 85 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/config/runtime.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # config/runtime.exs is executed for all environments, including 4 | # during releases. It is executed after compilation and before the 5 | # system starts, so it is typically used to load production configuration 6 | # and secrets from environment variables or elsewhere. Do not define 7 | # any compile-time configuration in here, as it won't be applied. 8 | # The block below contains prod specific runtime configuration. 9 | 10 | # ## Using releases 11 | # 12 | # If you use `mix release`, you need to explicitly enable the server 13 | # by passing the PHX_SERVER=true when you start it: 14 | # 15 | # PHX_SERVER=true bin/phoenix_signaling start 16 | # 17 | # Alternatively, you can use `mix phx.gen.release` to generate a `bin/server` 18 | # script that automatically sets the env var above. 19 | if System.get_env("PHX_SERVER") do 20 | config :phoenix_signaling, PhoenixSignalingWeb.Endpoint, server: true 21 | end 22 | 23 | if config_env() == :prod do 24 | # The secret key base is used to sign/encrypt cookies and other secrets. 25 | # A default value is used in config/dev.exs and config/test.exs but you 26 | # want to use a different value for prod and you most likely don't want 27 | # to check this value into version control, so we use an environment 28 | # variable instead. 29 | secret_key_base = 30 | System.get_env("SECRET_KEY_BASE") || 31 | raise """ 32 | environment variable SECRET_KEY_BASE is missing. 33 | You can generate one by calling: mix phx.gen.secret 34 | """ 35 | 36 | host = System.get_env("PHX_HOST") || "example.com" 37 | port = String.to_integer(System.get_env("PORT") || "4000") 38 | 39 | config :phoenix_signaling, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") 40 | 41 | config :phoenix_signaling, PhoenixSignalingWeb.Endpoint, 42 | url: [host: host, port: 443, scheme: "https"], 43 | http: [ 44 | # Enable IPv6 and bind on all interfaces. 45 | # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. 46 | # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0 47 | # for details about using IPv6 vs IPv4 and loopback vs public addresses. 48 | ip: {0, 0, 0, 0, 0, 0, 0, 0}, 49 | port: port 50 | ], 51 | secret_key_base: secret_key_base 52 | 53 | # ## SSL Support 54 | # 55 | # To get SSL working, you will need to add the `https` key 56 | # to your endpoint configuration: 57 | # 58 | # config :phoenix_signaling, PhoenixSignalingWeb.Endpoint, 59 | # https: [ 60 | # ..., 61 | # port: 443, 62 | # cipher_suite: :strong, 63 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 64 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") 65 | # ] 66 | # 67 | # The `cipher_suite` is set to `:strong` to support only the 68 | # latest and more secure SSL ciphers. This means old browsers 69 | # and clients may not be supported. You can set it to 70 | # `:compatible` for wider support. 71 | # 72 | # `:keyfile` and `:certfile` expect an absolute path to the key 73 | # and cert in disk or a relative path inside priv, for example 74 | # "priv/ssl/server.key". For all supported SSL configuration 75 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 76 | # 77 | # We also recommend setting `force_ssl` in your config/prod.exs, 78 | # ensuring no data is ever sent via http, always redirecting to https: 79 | # 80 | # config :phoenix_signaling, PhoenixSignalingWeb.Endpoint, 81 | # force_ssl: [hsts: true] 82 | # 83 | # Check `Plug.SSL` for all available options in `force_ssl`. 84 | end 85 | -------------------------------------------------------------------------------- /lib/membrane_webrtc/simple_websocket_server.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.WebRTC.SimpleWebSocketServer do 2 | @moduledoc """ 3 | A simple WebSocket server spawned by `Membrane.WebRTC.Source` 4 | and `Membrane.WebRTC.Sink`. It accepts a single connection 5 | and passes the messages between the client and a Membrane 6 | element. 7 | 8 | The messages sent and received by the server are JSON-encoded 9 | `t:Membrane.WebRTC.Signaling.json_data_message/0`. 10 | Additionally, the server sends a `{type: "keep_alive", data: ""}` 11 | messages to prevent the WebSocket from being closed. 12 | 13 | Examples of configuring and interacting with the server can 14 | be found in the `examples` directory. 15 | """ 16 | 17 | alias Membrane.WebRTC.Signaling 18 | 19 | @typedoc """ 20 | Options for the server. 21 | 22 | The port is required, while the IP address defaults to `{127, 0, 0, 1}`. 23 | """ 24 | @type options :: [ip: :inet.ip_address(), port: :inet.port_number()] 25 | 26 | @doc false 27 | @spec child_spec({options, Signaling.t()}) :: Supervisor.child_spec() 28 | def child_spec({opts, signaling}) do 29 | opts = opts |> validate_options!() |> Map.new() 30 | 31 | Supervisor.child_spec( 32 | {Bandit, 33 | plug: {__MODULE__.Router, %{conn_cnt: :atomics.new(1, []), signaling: signaling}}, 34 | ip: opts.ip, 35 | port: opts.port}, 36 | [] 37 | ) 38 | end 39 | 40 | @spec validate_options!(options()) :: options() | no_return() 41 | def validate_options!(options), do: Keyword.validate!(options, [:port, ip: {127, 0, 0, 1}]) 42 | 43 | @doc false 44 | @spec start_link_supervised(pid, options) :: Signaling.t() 45 | def start_link_supervised(utility_supervisor, opts) do 46 | signaling = Signaling.new() 47 | 48 | {:ok, _pid} = 49 | Membrane.UtilitySupervisor.start_link_child( 50 | utility_supervisor, 51 | {__MODULE__, {opts, signaling}} 52 | ) 53 | 54 | signaling 55 | end 56 | 57 | defmodule Router do 58 | @moduledoc false 59 | use Plug.Router 60 | 61 | plug(:match) 62 | plug(:dispatch) 63 | 64 | get "/" do 65 | conn_cnt = :atomics.add_get(conn.private.conn_cnt, 1, 1) 66 | 67 | if conn_cnt == 1 do 68 | WebSockAdapter.upgrade( 69 | conn, 70 | Membrane.WebRTC.SimpleWebSocketServer.PeerHandler, 71 | %{signaling: conn.private.signaling}, 72 | [] 73 | ) 74 | else 75 | send_resp(conn, 429, "already connected") 76 | end 77 | end 78 | 79 | match _ do 80 | send_resp(conn, 404, "not found") 81 | end 82 | 83 | @impl true 84 | def call(conn, opts) do 85 | conn 86 | |> put_private(:conn_cnt, opts.conn_cnt) 87 | |> put_private(:signaling, opts.signaling) 88 | |> super(opts) 89 | end 90 | end 91 | 92 | defmodule PeerHandler do 93 | @moduledoc false 94 | 95 | @behaviour WebSock 96 | 97 | require Logger 98 | 99 | alias Membrane.WebRTC.Signaling 100 | 101 | @impl true 102 | def init(opts) do 103 | Signaling.register_peer(opts.signaling, message_format: :json_data) 104 | Process.send_after(self(), :keep_alive, 30_000) 105 | {:ok, %{signaling: opts.signaling}} 106 | end 107 | 108 | @impl true 109 | def handle_in({message, opcode: :text}, state) do 110 | Signaling.signal(state.signaling, Jason.decode!(message)) 111 | {:ok, state} 112 | end 113 | 114 | @impl true 115 | def handle_info({:membrane_webrtc_signaling, _pid, message, _metadata}, state) do 116 | {:push, {:text, Jason.encode!(message)}, state} 117 | end 118 | 119 | @impl true 120 | def handle_info(:keep_alive, state) do 121 | Process.send_after(self(), :keep_alive, 30_000) 122 | {:push, {:text, Jason.encode!(%{type: "keep_alive", data: ""})}, state} 123 | end 124 | 125 | @impl true 126 | def handle_info(message, state) do 127 | Logger.debug( 128 | "#{inspect(__MODULE__)} process ignores unsupported message #{inspect(message)}" 129 | ) 130 | 131 | {:ok, state} 132 | end 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /lib/membrane_webrtc/whip_client.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.WebRTC.WhipClient do 2 | @moduledoc """ 3 | WebRTC WHIP Client. 4 | 5 | Accepts the following options: 6 | - `uri` - address of a WHIP server 7 | - `signaling` - the signaling channel - pass the same signaling channel to `Membrane.WebRTC.Sink` 8 | to connect it with the WHIP client 9 | - `token` - token to authenticate in the server, defaults to an empty string 10 | """ 11 | use GenServer 12 | 13 | require Logger 14 | 15 | alias ExWebRTC.{ICECandidate, SessionDescription} 16 | alias Membrane.WebRTC.Signaling 17 | 18 | @spec start_link([ 19 | {:signaling, Signaling.t()} | {:uri, String.t()} | {:token, String.t()} 20 | ]) :: 21 | {:ok, pid()} 22 | def start_link(opts) do 23 | enforce_keys = [:signaling, :uri] 24 | opts = Keyword.validate!(opts, enforce_keys ++ [token: ""]) |> Map.new() 25 | missing_keys = Enum.reject(enforce_keys, &is_map_key(opts, &1)) 26 | 27 | unless missing_keys == [], 28 | do: raise(ArgumentError, "Missing option #{Enum.join(missing_keys, ", ")}") 29 | 30 | GenServer.start_link(__MODULE__, opts) 31 | end 32 | 33 | @impl true 34 | def init(opts) do 35 | Signaling.register_peer(opts.signaling) 36 | Process.monitor(opts.signaling.pid) 37 | {:ok, Map.merge(opts, %{resource_uri: nil})} 38 | end 39 | 40 | @impl true 41 | def handle_info( 42 | {:membrane_webrtc_signaling, pid, %SessionDescription{type: :offer, sdp: offer_sdp}, 43 | _metadata}, 44 | %{signaling: signaling} = state 45 | ) 46 | when signaling.pid == pid do 47 | resp = 48 | Req.post!(state.uri, 49 | headers: [ 50 | Accept: "application/sdp", 51 | "Content-Type": "application/sdp", 52 | authorization: "Bearer #{state.token}" 53 | ], 54 | body: offer_sdp 55 | ) 56 | 57 | %Req.Response{status: status, body: answer_sdp} = resp 58 | unless status in 200..299, do: raise("Invalid WHIP answer response status: #{status}") 59 | 60 | resource_id = 61 | case Req.Response.get_header(resp, "location") do 62 | [resource_id] -> resource_id 63 | _other -> raise "Invalid WHEP answer location header" 64 | end 65 | 66 | %URI{} = uri = URI.parse(state.uri) 67 | resource_uri = %URI{uri | path: resource_id} |> URI.to_string() 68 | 69 | pid = self() 70 | 71 | Task.start(fn -> 72 | monitor = Process.monitor(pid) 73 | 74 | receive do 75 | {:DOWN, ^monitor, _pid, _type, _reason} -> :ok 76 | end 77 | 78 | %Req.Response{status: status} = Req.delete!(resource_uri) 79 | 80 | unless status in 200..299, 81 | do: Logger.warning("Failed to send delete request to #{resource_uri}") 82 | end) 83 | 84 | Signaling.signal(signaling, %SessionDescription{type: :answer, sdp: answer_sdp}) 85 | {:noreply, %{state | resource_uri: resource_uri}} 86 | end 87 | 88 | @impl true 89 | def handle_info( 90 | {:membrane_webrtc_signaling, pid, %ICECandidate{} = candidate, _metadata}, 91 | %{signaling: signaling} = state 92 | ) 93 | when signaling.pid == pid do 94 | # It's not necessarily the mline that was in the SDP 95 | # but it shouldn't matter 96 | media = 97 | ExSDP.Media.new(:audio, 9, "UDP/TLS/RTP/SAVPF", 0) 98 | |> ExSDP.add_attribute({"candidate", candidate.candidate}) 99 | |> ExSDP.add_attribute({:mid, candidate.sdp_mid}) 100 | 101 | sdp = 102 | ExSDP.new() 103 | |> ExSDP.add_media(media) 104 | |> ExSDP.add_attribute({:ice_ufrag, candidate.username_fragment}) 105 | 106 | %Req.Response{status: status} = 107 | Req.patch!(state.resource_uri, 108 | headers: ["Content-Type": "application/trickle-ice-sdpfrag"], 109 | body: to_string(sdp) 110 | ) 111 | 112 | unless status in 200..299, 113 | do: Logger.error("Failed to send candindate to #{state.resource_uri}") 114 | 115 | {:noreply, state} 116 | end 117 | 118 | @impl true 119 | def handle_info( 120 | {:DOWN, _monitor, _type, pid, _reason}, 121 | %{signaling: %Signaling{pid: pid}} = state 122 | ) do 123 | {:stop, :normal, state} 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/assets/js/signaling.js: -------------------------------------------------------------------------------- 1 | import { Socket } from "phoenix"; 2 | 3 | async function startEgressConnection(channel, topic) { 4 | const pcConfig = { iceServers: [{ urls: "stun:stun.l.google.com:19302" }] }; 5 | const mediaConstraints = { video: true, audio: true }; 6 | 7 | const connStatus = document.getElementById("status"); 8 | const localStream = await navigator.mediaDevices.getUserMedia(mediaConstraints); 9 | const pc = new RTCPeerConnection(pcConfig); 10 | 11 | pc.onicecandidate = (event) => { 12 | if (event.candidate === null) return; 13 | console.log("Sent ICE candidate:", event.candidate); 14 | channel.push(topic, JSON.stringify({ type: "ice_candidate", data: event.candidate })); 15 | }; 16 | 17 | pc.onconnectionstatechange = () => { 18 | if (pc.connectionState == "connected") { 19 | const button = document.createElement("button"); 20 | button.innerHTML = "Disconnect"; 21 | button.onclick = () => { 22 | localStream.getTracks().forEach((track) => track.stop()); 23 | }; 24 | connStatus.innerHTML = "Connected "; 25 | connStatus.appendChild(button); 26 | } 27 | }; 28 | 29 | for (const track of localStream.getTracks()) { 30 | pc.addTrack(track, localStream); 31 | } 32 | 33 | channel.on(topic, async (payload) => { 34 | type = payload.type; 35 | data = payload.data; 36 | 37 | switch (type) { 38 | case "sdp_answer": 39 | console.log("Received SDP answer:", data); 40 | await pc.setRemoteDescription(data); 41 | break; 42 | case "ice_candidate": 43 | console.log("Received ICE candidate:", data); 44 | await pc.addIceCandidate(data); 45 | break; 46 | } 47 | }); 48 | 49 | const offer = await pc.createOffer(); 50 | await pc.setLocalDescription(offer); 51 | console.log("Sent SDP offer:", offer); 52 | channel.push(topic, JSON.stringify({ type: "sdp_offer", data: offer })); 53 | } 54 | 55 | async function startIngressConnection(channel, topic) { 56 | videoPlayer.srcObject = new MediaStream(); 57 | 58 | const pc = new RTCPeerConnection(pcConfig); 59 | pc.ontrack = (event) => videoPlayer.srcObject.addTrack(event.track); 60 | pc.onicecandidate = (event) => { 61 | if (event.candidate === null) return; 62 | 63 | console.log("Sent ICE candidate:", event.candidate); 64 | channel.push(topic, JSON.stringify({ type: "ice_candidate", data: event.candidate })); 65 | }; 66 | 67 | channel.on(topic, async (payload) => { 68 | type = payload.type; 69 | data = payload.data; 70 | 71 | switch (type) { 72 | case "sdp_offer": 73 | console.log("Received SDP offer:", data); 74 | await pc.setRemoteDescription(data); 75 | const answer = await pc.createAnswer(); 76 | await pc.setLocalDescription(answer); 77 | channel.push(topic, JSON.stringify({ type: "sdp_answer", data: answer })); 78 | console.log("Sent SDP answer:", answer); 79 | break; 80 | case "ice_candidate": 81 | console.log("Received ICE candidate:", data); 82 | await pc.addIceCandidate(data); 83 | } 84 | }); 85 | } 86 | const videoPlayer = document.getElementById("videoPlayer"); 87 | const signalingId = videoPlayer.getAttribute("signaling_id"); 88 | 89 | let socket = new Socket("/signaling", { params: { token: window.userToken } }); 90 | socket.connect(); 91 | let egressChannel = socket.channel(`${signalingId}_egress`); 92 | egressChannel 93 | .join() 94 | .receive("ok", (resp) => { 95 | console.log("Joined successfully to egress signaling socket", resp); 96 | startEgressConnection(egressChannel, `${signalingId}_egress`); 97 | }) 98 | .receive("error", (resp) => { 99 | console.log("Unable to join egress signaling socket", resp); 100 | }); 101 | 102 | const pcConfig = { iceServers: [{ urls: "stun:stun.l.google.com:19302" }] }; 103 | 104 | let ingressChannel = socket.channel(`${signalingId}_ingress`); 105 | ingressChannel 106 | .join() 107 | .receive("ok", (resp) => { 108 | console.log("Joined successfully to ingress signaling socket", resp); 109 | startIngressConnection(ingressChannel, `${signalingId}_ingress`); 110 | }) 111 | .receive("error", (resp) => { 112 | console.log("Unable to join ingress signaling socket", resp); 113 | }); 114 | -------------------------------------------------------------------------------- /examples/live_view/assets/vendor/topbar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license MIT 3 | * topbar 2.0.0, 2023-02-04 4 | * https://buunguyen.github.io/topbar 5 | * Copyright (c) 2021 Buu Nguyen 6 | */ 7 | (function (window, document) { 8 | "use strict"; 9 | 10 | // https://gist.github.com/paulirish/1579671 11 | (function () { 12 | var lastTime = 0; 13 | var vendors = ["ms", "moz", "webkit", "o"]; 14 | for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 15 | window.requestAnimationFrame = 16 | window[vendors[x] + "RequestAnimationFrame"]; 17 | window.cancelAnimationFrame = 18 | window[vendors[x] + "CancelAnimationFrame"] || 19 | window[vendors[x] + "CancelRequestAnimationFrame"]; 20 | } 21 | if (!window.requestAnimationFrame) 22 | window.requestAnimationFrame = function (callback, element) { 23 | var currTime = new Date().getTime(); 24 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 25 | var id = window.setTimeout(function () { 26 | callback(currTime + timeToCall); 27 | }, timeToCall); 28 | lastTime = currTime + timeToCall; 29 | return id; 30 | }; 31 | if (!window.cancelAnimationFrame) 32 | window.cancelAnimationFrame = function (id) { 33 | clearTimeout(id); 34 | }; 35 | })(); 36 | 37 | var canvas, 38 | currentProgress, 39 | showing, 40 | progressTimerId = null, 41 | fadeTimerId = null, 42 | delayTimerId = null, 43 | addEvent = function (elem, type, handler) { 44 | if (elem.addEventListener) elem.addEventListener(type, handler, false); 45 | else if (elem.attachEvent) elem.attachEvent("on" + type, handler); 46 | else elem["on" + type] = handler; 47 | }, 48 | options = { 49 | autoRun: true, 50 | barThickness: 3, 51 | barColors: { 52 | 0: "rgba(26, 188, 156, .9)", 53 | ".25": "rgba(52, 152, 219, .9)", 54 | ".50": "rgba(241, 196, 15, .9)", 55 | ".75": "rgba(230, 126, 34, .9)", 56 | "1.0": "rgba(211, 84, 0, .9)", 57 | }, 58 | shadowBlur: 10, 59 | shadowColor: "rgba(0, 0, 0, .6)", 60 | className: null, 61 | }, 62 | repaint = function () { 63 | canvas.width = window.innerWidth; 64 | canvas.height = options.barThickness * 5; // need space for shadow 65 | 66 | var ctx = canvas.getContext("2d"); 67 | ctx.shadowBlur = options.shadowBlur; 68 | ctx.shadowColor = options.shadowColor; 69 | 70 | var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0); 71 | for (var stop in options.barColors) 72 | lineGradient.addColorStop(stop, options.barColors[stop]); 73 | ctx.lineWidth = options.barThickness; 74 | ctx.beginPath(); 75 | ctx.moveTo(0, options.barThickness / 2); 76 | ctx.lineTo( 77 | Math.ceil(currentProgress * canvas.width), 78 | options.barThickness / 2 79 | ); 80 | ctx.strokeStyle = lineGradient; 81 | ctx.stroke(); 82 | }, 83 | createCanvas = function () { 84 | canvas = document.createElement("canvas"); 85 | var style = canvas.style; 86 | style.position = "fixed"; 87 | style.top = style.left = style.right = style.margin = style.padding = 0; 88 | style.zIndex = 100001; 89 | style.display = "none"; 90 | if (options.className) canvas.classList.add(options.className); 91 | document.body.appendChild(canvas); 92 | addEvent(window, "resize", repaint); 93 | }, 94 | topbar = { 95 | config: function (opts) { 96 | for (var key in opts) 97 | if (options.hasOwnProperty(key)) options[key] = opts[key]; 98 | }, 99 | show: function (delay) { 100 | if (showing) return; 101 | if (delay) { 102 | if (delayTimerId) return; 103 | delayTimerId = setTimeout(() => topbar.show(), delay); 104 | } else { 105 | showing = true; 106 | if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId); 107 | if (!canvas) createCanvas(); 108 | canvas.style.opacity = 1; 109 | canvas.style.display = "block"; 110 | topbar.progress(0); 111 | if (options.autoRun) { 112 | (function loop() { 113 | progressTimerId = window.requestAnimationFrame(loop); 114 | topbar.progress( 115 | "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2) 116 | ); 117 | })(); 118 | } 119 | } 120 | }, 121 | progress: function (to) { 122 | if (typeof to === "undefined") return currentProgress; 123 | if (typeof to === "string") { 124 | to = 125 | (to.indexOf("+") >= 0 || to.indexOf("-") >= 0 126 | ? currentProgress 127 | : 0) + parseFloat(to); 128 | } 129 | currentProgress = to > 1 ? 1 : to; 130 | repaint(); 131 | return currentProgress; 132 | }, 133 | hide: function () { 134 | clearTimeout(delayTimerId); 135 | delayTimerId = null; 136 | if (!showing) return; 137 | showing = false; 138 | if (progressTimerId != null) { 139 | window.cancelAnimationFrame(progressTimerId); 140 | progressTimerId = null; 141 | } 142 | (function loop() { 143 | if (topbar.progress("+.1") >= 1) { 144 | canvas.style.opacity -= 0.05; 145 | if (canvas.style.opacity <= 0.05) { 146 | canvas.style.display = "none"; 147 | fadeTimerId = null; 148 | return; 149 | } 150 | } 151 | fadeTimerId = window.requestAnimationFrame(loop); 152 | })(); 153 | }, 154 | }; 155 | 156 | if (typeof module === "object" && typeof module.exports === "object") { 157 | module.exports = topbar; 158 | } else if (typeof define === "function" && define.amd) { 159 | define(function () { 160 | return topbar; 161 | }); 162 | } else { 163 | this.topbar = topbar; 164 | } 165 | }.call(this, window, document)); 166 | -------------------------------------------------------------------------------- /examples/phoenix_signaling/assets/vendor/topbar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license MIT 3 | * topbar 2.0.0, 2023-02-04 4 | * https://buunguyen.github.io/topbar 5 | * Copyright (c) 2021 Buu Nguyen 6 | */ 7 | (function (window, document) { 8 | "use strict"; 9 | 10 | // https://gist.github.com/paulirish/1579671 11 | (function () { 12 | var lastTime = 0; 13 | var vendors = ["ms", "moz", "webkit", "o"]; 14 | for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 15 | window.requestAnimationFrame = 16 | window[vendors[x] + "RequestAnimationFrame"]; 17 | window.cancelAnimationFrame = 18 | window[vendors[x] + "CancelAnimationFrame"] || 19 | window[vendors[x] + "CancelRequestAnimationFrame"]; 20 | } 21 | if (!window.requestAnimationFrame) 22 | window.requestAnimationFrame = function (callback, element) { 23 | var currTime = new Date().getTime(); 24 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 25 | var id = window.setTimeout(function () { 26 | callback(currTime + timeToCall); 27 | }, timeToCall); 28 | lastTime = currTime + timeToCall; 29 | return id; 30 | }; 31 | if (!window.cancelAnimationFrame) 32 | window.cancelAnimationFrame = function (id) { 33 | clearTimeout(id); 34 | }; 35 | })(); 36 | 37 | var canvas, 38 | currentProgress, 39 | showing, 40 | progressTimerId = null, 41 | fadeTimerId = null, 42 | delayTimerId = null, 43 | addEvent = function (elem, type, handler) { 44 | if (elem.addEventListener) elem.addEventListener(type, handler, false); 45 | else if (elem.attachEvent) elem.attachEvent("on" + type, handler); 46 | else elem["on" + type] = handler; 47 | }, 48 | options = { 49 | autoRun: true, 50 | barThickness: 3, 51 | barColors: { 52 | 0: "rgba(26, 188, 156, .9)", 53 | ".25": "rgba(52, 152, 219, .9)", 54 | ".50": "rgba(241, 196, 15, .9)", 55 | ".75": "rgba(230, 126, 34, .9)", 56 | "1.0": "rgba(211, 84, 0, .9)", 57 | }, 58 | shadowBlur: 10, 59 | shadowColor: "rgba(0, 0, 0, .6)", 60 | className: null, 61 | }, 62 | repaint = function () { 63 | canvas.width = window.innerWidth; 64 | canvas.height = options.barThickness * 5; // need space for shadow 65 | 66 | var ctx = canvas.getContext("2d"); 67 | ctx.shadowBlur = options.shadowBlur; 68 | ctx.shadowColor = options.shadowColor; 69 | 70 | var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0); 71 | for (var stop in options.barColors) 72 | lineGradient.addColorStop(stop, options.barColors[stop]); 73 | ctx.lineWidth = options.barThickness; 74 | ctx.beginPath(); 75 | ctx.moveTo(0, options.barThickness / 2); 76 | ctx.lineTo( 77 | Math.ceil(currentProgress * canvas.width), 78 | options.barThickness / 2 79 | ); 80 | ctx.strokeStyle = lineGradient; 81 | ctx.stroke(); 82 | }, 83 | createCanvas = function () { 84 | canvas = document.createElement("canvas"); 85 | var style = canvas.style; 86 | style.position = "fixed"; 87 | style.top = style.left = style.right = style.margin = style.padding = 0; 88 | style.zIndex = 100001; 89 | style.display = "none"; 90 | if (options.className) canvas.classList.add(options.className); 91 | document.body.appendChild(canvas); 92 | addEvent(window, "resize", repaint); 93 | }, 94 | topbar = { 95 | config: function (opts) { 96 | for (var key in opts) 97 | if (options.hasOwnProperty(key)) options[key] = opts[key]; 98 | }, 99 | show: function (delay) { 100 | if (showing) return; 101 | if (delay) { 102 | if (delayTimerId) return; 103 | delayTimerId = setTimeout(() => topbar.show(), delay); 104 | } else { 105 | showing = true; 106 | if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId); 107 | if (!canvas) createCanvas(); 108 | canvas.style.opacity = 1; 109 | canvas.style.display = "block"; 110 | topbar.progress(0); 111 | if (options.autoRun) { 112 | (function loop() { 113 | progressTimerId = window.requestAnimationFrame(loop); 114 | topbar.progress( 115 | "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2) 116 | ); 117 | })(); 118 | } 119 | } 120 | }, 121 | progress: function (to) { 122 | if (typeof to === "undefined") return currentProgress; 123 | if (typeof to === "string") { 124 | to = 125 | (to.indexOf("+") >= 0 || to.indexOf("-") >= 0 126 | ? currentProgress 127 | : 0) + parseFloat(to); 128 | } 129 | currentProgress = to > 1 ? 1 : to; 130 | repaint(); 131 | return currentProgress; 132 | }, 133 | hide: function () { 134 | clearTimeout(delayTimerId); 135 | delayTimerId = null; 136 | if (!showing) return; 137 | showing = false; 138 | if (progressTimerId != null) { 139 | window.cancelAnimationFrame(progressTimerId); 140 | progressTimerId = null; 141 | } 142 | (function loop() { 143 | if (topbar.progress("+.1") >= 1) { 144 | canvas.style.opacity -= 0.05; 145 | if (canvas.style.opacity <= 0.05) { 146 | canvas.style.display = "none"; 147 | fadeTimerId = null; 148 | return; 149 | } 150 | } 151 | fadeTimerId = window.requestAnimationFrame(loop); 152 | })(); 153 | }, 154 | }; 155 | 156 | if (typeof module === "object" && typeof module.exports === "object") { 157 | module.exports = topbar; 158 | } else if (typeof define === "function" && define.amd) { 159 | define(function () { 160 | return topbar; 161 | }); 162 | } else { 163 | this.topbar = topbar; 164 | } 165 | }.call(this, window, document)); 166 | -------------------------------------------------------------------------------- /lib/membrane_webrtc/live/player.ex: -------------------------------------------------------------------------------- 1 | if Code.ensure_loaded?(Phoenix) and Code.ensure_loaded?(Phoenix.LiveView) do 2 | defmodule Membrane.WebRTC.Live.Player do 3 | @moduledoc ~S''' 4 | LiveView for playing audio and video received via WebRTC from `Membrane.WebRTC.Sink`. 5 | 6 | *Note:* This module will be available in your code only if you add `{:phoenix, "~> 1.7"}` 7 | and `{:phoenix_live_view, "~> 1.0"}` to the dependencies of of your root project. 8 | 9 | It: 10 | * renders a single HTMLVideoElement. 11 | * creates WebRTC PeerConnection on the browser side. 12 | * forwards signaling messages between the browser and `Membrane.WebRTC.Sink` via `Membrane.WebRTC.Signaling`. 13 | * attaches audio and video from the Elixir to the HTMLVideoElement. 14 | 15 | ## JavaScript Hook 16 | 17 | Player live view requires JavaScript hook to be registered under `Player` name. 18 | The hook can be created using `createPlayerHook` function. 19 | For example: 20 | 21 | ```javascript 22 | import { createPlayerHook } from "membrane_webrtc_plugin"; 23 | let Hooks = {}; 24 | const iceServers = [{ urls: "stun:stun.l.google.com:19302" }]; 25 | Hooks.Player = createPlayerHook(iceServers); 26 | let liveSocket = new LiveSocket("/live", Socket, { 27 | // ... 28 | hooks: Hooks 29 | }); 30 | ``` 31 | 32 | ## Examples 33 | 34 | ```elixir 35 | defmodule StreamerWeb.StreamViewerLive do 36 | use StreamerWeb, :live_view 37 | 38 | alias Membrane.WebRTC.Live.Player 39 | 40 | @impl true 41 | def render(assigns) do 42 | ~H""" 43 | 44 | """ 45 | end 46 | 47 | @impl true 48 | def mount(_params, _session, socket) do 49 | signaling = Membrane.WebRTC.Signaling.new() 50 | {:ok, _supervisor, _pipelne} = Membrane.Pipeline.start_link(MyPipeline, signaling: signaling) 51 | 52 | socket = socket |> Player.attach(id: "player", signaling: signaling) 53 | {:ok, socket} 54 | end 55 | end 56 | ``` 57 | ''' 58 | use Phoenix.LiveView 59 | require Logger 60 | 61 | alias Membrane.WebRTC.Signaling 62 | 63 | @type t() :: %__MODULE__{ 64 | id: String.t(), 65 | signaling: Signaling.t() 66 | } 67 | 68 | defstruct id: nil, signaling: nil 69 | 70 | attr(:socket, Phoenix.LiveView.Socket, required: true, doc: "Parent live view socket") 71 | 72 | attr(:player_id, :string, 73 | required: true, 74 | doc: """ 75 | ID of a `player` previously attached to the socket. It has to be the same as the value passed to `:id` 76 | field `#{inspect(__MODULE__)}.attach/2`. 77 | """ 78 | ) 79 | 80 | attr(:class, :string, default: nil, doc: "CSS/Tailwind classes for styling") 81 | 82 | @doc """ 83 | Helper function for rendering Player live view. 84 | """ 85 | def live_render(assigns) do 86 | ~H""" 87 | <%= live_render(@socket, __MODULE__, id: "#{@player_id}-lv", session: %{"class" => @class, "id" => @player_id}) %> 88 | """ 89 | end 90 | 91 | @doc """ 92 | Attaches required hooks and creates `#{inspect(__MODULE__)}` struct. 93 | 94 | Created struct is saved in socket's assigns (in `socket.assigns[#{inspect(__MODULE__)}][id]`) and then 95 | it is sent by an attached hook to a child live view process. 96 | 97 | Options: 98 | * `id` - player id. It is used to identify live view and generated HTML video player. It must be unique 99 | withing single page. 100 | * `signaling` - `Membrane.WebRTC.Signaling.t()`, that has been passed to `Membrane.WebRTC.Sink` as well. 101 | """ 102 | @spec attach(Phoenix.LiveView.Socket.t(), Keyword.t()) :: Phoenix.LiveView.Socket.t() 103 | def attach(socket, opts) do 104 | opts = opts |> Keyword.validate!([:id, :signaling]) 105 | player = struct!(__MODULE__, opts) 106 | 107 | all_players = 108 | socket.assigns 109 | |> Map.get(__MODULE__, %{}) 110 | |> Map.put(player.id, player) 111 | 112 | socket 113 | |> assign(__MODULE__, all_players) 114 | |> detach_hook(:player_handshake, :handle_info) 115 | |> attach_hook(:player_handshake, :handle_info, &parent_handshake/2) 116 | end 117 | 118 | @spec get_attached(Phoenix.LiveView.Socket.t(), String.t()) :: t() 119 | def get_attached(socket, id), do: socket.assigns[__MODULE__][id] 120 | 121 | ## CALLBACKS 122 | 123 | @impl true 124 | def render(%{player: nil} = assigns) do 125 | ~H""" 126 | """ 127 | end 128 | 129 | @impl true 130 | def render(assigns) do 131 | ~H""" 132 | 133 | """ 134 | end 135 | 136 | @impl true 137 | def mount(_params, %{"class" => class, "id" => id}, socket) do 138 | socket = socket |> assign(class: class, player: nil) 139 | 140 | socket = 141 | if connected?(socket), 142 | do: client_handshake(socket, id), 143 | else: socket 144 | 145 | {:ok, socket} 146 | end 147 | 148 | defp parent_handshake({__MODULE__, {:connected, id, player_pid}}, socket) do 149 | player_struct = 150 | socket.assigns 151 | |> Map.fetch!(__MODULE__) 152 | |> Map.fetch!(id) 153 | 154 | send(player_pid, player_struct) 155 | 156 | {:halt, socket} 157 | end 158 | 159 | defp parent_handshake(_msg, socket) do 160 | {:cont, socket} 161 | end 162 | 163 | defp client_handshake(socket, id) do 164 | send(socket.parent_pid, {__MODULE__, {:connected, id, self()}}) 165 | 166 | receive do 167 | %__MODULE__{} = player -> 168 | player.signaling 169 | |> Signaling.register_peer(message_format: :json_data) 170 | 171 | socket |> assign(player: player) 172 | after 173 | 5000 -> exit(:timeout) 174 | end 175 | end 176 | 177 | @impl true 178 | def handle_info({:membrane_webrtc_signaling, _pid, message, _metadata}, socket) do 179 | Logger.debug(""" 180 | #{log_prefix(socket.assigns.player.id)} Sent WebRTC signaling message: #{inspect(message, pretty: true)} 181 | """) 182 | 183 | {:noreply, 184 | socket 185 | |> push_event("webrtc_signaling-#{socket.assigns.player.id}", message)} 186 | end 187 | 188 | @impl true 189 | def handle_event("webrtc_signaling", message, socket) do 190 | Logger.debug(""" 191 | #{log_prefix(socket.assigns.player.id)} Received WebRTC signaling message: #{inspect(message, pretty: true)} 192 | """) 193 | 194 | if message["data"] do 195 | socket.assigns.player.signaling 196 | |> Signaling.signal(message) 197 | end 198 | 199 | {:noreply, socket} 200 | end 201 | 202 | defp log_prefix(id), do: [module: __MODULE__, id: id] |> inspect() 203 | end 204 | end 205 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Membrane WebRTC Plugin 2 | 3 | [![Hex.pm](https://img.shields.io/hexpm/v/membrane_webrtc_plugin.svg)](https://hex.pm/packages/membrane_webrtc_plugin) 4 | [![API Docs](https://img.shields.io/badge/api-docs-yellow.svg?style=flat)](https://hexdocs.pm/membrane_webrtc_plugin) 5 | [![CircleCI](https://circleci.com/gh/membraneframework/membrane_webrtc_plugin.svg?style=svg)](https://circleci.com/gh/membraneframework/membrane_webrtc_plugin) 6 | 7 | Membrane Plugin for sending and receiving streams via WebRTC. It's based on [ex_webrtc](https://github.com/elixir-webrtc/ex_webrtc). 8 | 9 | It's a part of the [Membrane Framework](https://membrane.stream). 10 | 11 | ## Installation 12 | 13 | The package can be installed by adding `membrane_webrtc_plugin` to your list of dependencies in `mix.exs`: 14 | 15 | ```elixir 16 | def deps do 17 | [ 18 | {:membrane_webrtc_plugin, "~> 0.26.1"} 19 | ] 20 | end 21 | ``` 22 | 23 | ## Demos 24 | 25 | The `examples` directory shows how to send and receive streams from a web browser. 26 | There are the following three demos: 27 | * `live_view` - a simple Phoenix LiveView project using `Membrane.WebRTC.Live.Player` and `Membrane.WebRTC.Live.Capture` to echo video stream 28 | captured from the user's browser. 29 | * `phoenix_signaling` - showcasing simple Phoenix application that uses `Membrane.WebRTC.PhoenixSignaling` to echo stream captured 30 | from the user's browser and sent via WebRTC. See `assets/phoenix_signaling/README.md` for details on how to run the demo. 31 | * `webrtc_signaling` - it consists of two scripts: `file_to_browser.exs` and `browser_to_file.exs`. The first one displays the stream from 32 | the fixture file in the user's browser. The latter captures the user's camera input from the browser and saves it in the file. 33 | To run one of these demos, type: `elixir ` and visit `http://localhost:4000`. 34 | 35 | ## Exchanging Signaling Messages 36 | 37 | To establish a WebRTC connection you have to exchange WebRTC signaling messages between peers. 38 | In `membrane_webrtc_plugin` it can be done by the user, with `Membrane.WebRTC.Signaling` or by passing WebSocket address to 39 | `Membrane.WebRTC.Source` or `Membrane.WebRTC.Sink`, but there are two additional ways of doing it, dedicated to be used within 40 | `Phoenix` projects: 41 | - The first one is to use `Membrane.WebRTC.PhoenixSignaling` along with `Membrane.WebRTC.PhoenixSignaling.Socket` 42 | - The second one is to use `Phoenix.LiveView` `Membrane.WebRTC.Live.Player` or `Membrane.WebRTC.Live.Capture`. These modules expect 43 | `t:Membrane.WebRTC.Signaling.t/0` as an argument and take advantage of WebSocket used by `Phoenix.LiveView` to exchange WebRTC 44 | signaling messages, so there is no need to add any code to handle signaling messages. 45 | 46 | ### How to use Membrane.WebRTC.PhoenixSignaling in your own Phoenix project? 47 | 48 | The see the full example, visit `example/phoenix_signaling`. 49 | 50 | 1. Create a new socket in your application endpoint, using the `Membrane.WebRTC.PhoenixSignaling.Socket`, for instance at `/signaling` path: 51 | ``` 52 | socket "/signaling", Membrane.WebRTC.PhoenixSignaling.Socket, 53 | websocket: true, 54 | longpoll: false 55 | ``` 56 | 2. Create a Phoenix signaling channel with the desired signaling ID and use it as `Membrane.WebRTC.Signaling.t()` 57 | for `Membrane.WebRTC.Source`, `Membrane.WebRTC.Sink` or [`Boombox`](https://github.com/membraneframework/boombox): 58 | ``` 59 | signaling = Membrane.WebRTC.PhoenixSignaling.new("") 60 | 61 | # use it with Membrane.WebRTC.Source: 62 | child(:webrtc_source, %Membrane.WebRTC.Source{signaling: signaling}) 63 | |> ... 64 | 65 | # or with Membrane.WebRTC.Sink: 66 | ... 67 | |> child(:webrtc_sink, %Membrane.WebRTC.Sink{signaling: signaling}) 68 | 69 | # or with Boombox: 70 | Boombox.run( 71 | input: {:webrtc, signaling}, 72 | output: ... 73 | ) 74 | ``` 75 | 76 | >Please note that `signaling_id` is expected to be globally unique for each WebRTC connection about to be 77 | >estabilished. You can, for instance: 78 | >1. Generate a unique id with `:uuid` package and assign it to the connection in the page controller: 79 | >``` 80 | >unique_id = UUID.uuid4() 81 | >render(conn, :home, layout: false, signaling_id: unique_id) 82 | >``` 83 | > 84 | >2. Generate HTML based on HEEx template, using the previously set assign: 85 | >``` 86 | > 87 | >``` 88 | > 89 | >3. Access it in your client code: 90 | >``` 91 | >const videoPlayer = document.getElementById('videoPlayer'); 92 | >const signalingId = videoPlayer.getAttribute('signaling_id'); 93 | >``` 94 | 95 | 96 | 3. Use the Phoenix Socket to exchange WebRTC signaling data. 97 | ``` 98 | let socket = new Socket("/signaling", {params: {token: window.userToken}}) 99 | socket.connect() 100 | let channel = socket.channel('') 101 | channel.join() 102 | .receive("ok", resp => { console.log("Signaling socket joined successfully", resp) 103 | // here you can exchange WebRTC data 104 | }) 105 | .receive("error", resp => { console.log("Unable to join signaling socket", resp) }) 106 | ``` 107 | 108 | Visit `examples/phoenix_signaling/assets/js/signaling.js` to see how WebRTC signaling messages exchange might look like. 109 | 110 | ## Integrating Phoenix.LiveView with Membrane WebRTC Plugin 111 | 112 | `membrane_webrtc_plugin` comes with two `Phoenix.LiveView`s: 113 | - `Membrane.WebRTC.Live.Capture` - exchanges WebRTC signaling messages between `Membrane.WebRTC.Source` and the browser. It 114 | expects the same `Membrane.WebRTC.Signaling` that has been passed to the related `Membrane.WebRTC.Source`. As a result, 115 | `Membrane.Webrtc.Source` will return the media stream captured from the browser, where `Membrane.WebRTC.Live.Capture` has been 116 | rendered. 117 | - `Membrane.WebRTC.Live.Player` - exchanges WebRTC signaling messages between `Membrane.WebRTC.Sink` and the browser. It 118 | expects the same `Membrane.WebRTC.Signaling` that has been passed to the related `Membrane.WebRTC.Sink`. As a result, 119 | `Membrane.WebRTC.Live.Player` will play media streams passed to the related `Membrane.WebRTC.Sink`. Currently supports up 120 | to one video stream and up to one audio stream. 121 | 122 | ### Usage 123 | 124 | To use `Phoenix.LiveView`s from this repository, you have to use related JS hooks. To do so, add the following code snippet to `assets/js/app.js` 125 | 126 | ```js 127 | import { createCaptureHook, createPlayerHook } from "membrane_webrtc_plugin"; 128 | 129 | let Hooks = {}; 130 | const iceServers = [{ urls: "stun:stun.l.google.com:19302" }]; 131 | Hooks.Capture = createCaptureHook(iceServers); 132 | Hooks.Player = createPlayerHook(iceServers); 133 | ``` 134 | 135 | and add `Hooks` to the WebSocket constructor. It can be done in the following way: 136 | 137 | ```js 138 | new LiveSocket("/live", Socket, { 139 | params: SomeParams, 140 | hooks: Hooks, 141 | }); 142 | ``` 143 | 144 | To see the full usage example, you can go to `examples/live_view/` directory in this repository (take a look especially at `examples/live_view/assets/js/app.js` and `examples/live_view/lib/example_project_web/live_views/echo.ex`). 145 | 146 | ## Copyright and License 147 | 148 | Copyright 2020, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane_webrtc_plugin) 149 | 150 | [![Software Mansion](https://logo.swmansion.com/logo?color=white&variant=desktop&width=200&tag=membrane-github)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane_webrtc_plugin) 151 | 152 | Licensed under the [Apache License, Version 2.0](LICENSE) 153 | -------------------------------------------------------------------------------- /lib/membrane_webrtc/signaling.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.WebRTC.Signaling do 2 | @moduledoc """ 3 | Signaling channel for sending WebRTC signaling messages between Membrane elements 4 | and other WebRTC peers. 5 | 6 | The flow of using the signaling channel is the following: 7 | - Create it with `new/0`. 8 | - Register the peer process (the one to send and receive signaling messages) 9 | with `register_peer/2`. 10 | - Pass the signaling to `Membrane.WebRTC.Source` or `Membrane.WebRTC.Sink` (this 11 | can also be done before the call to `register_peer/2`). 12 | - Send and receive signaling messages. Messages can be sent by calling `signal/2`. 13 | The signaling channel sends `t:message/0` to the peer. 14 | """ 15 | use GenServer 16 | 17 | require Logger 18 | 19 | alias ExWebRTC.{ICECandidate, SessionDescription} 20 | 21 | @enforce_keys [:pid] 22 | defstruct @enforce_keys 23 | 24 | @type t :: %__MODULE__{pid: pid()} 25 | 26 | @typedoc """ 27 | Messages sent by the signaling channel to the peer. 28 | """ 29 | @type message :: {:membrane_webrtc_signaling, pid(), message_content, metadata :: map} 30 | 31 | @typedoc """ 32 | Messages that the peer sends with `signal/2` and receives in `t:message/0`. 33 | 34 | If the `message_format` of the peer is `ex_webrtc` (default), they should be 35 | `t:ex_webrtc_message/0`. 36 | If the `message_format` is `json_data`, they should be `t:json_data_message/0`. 37 | 38 | The `message_format` of the peer can be set in `register_peer/2`. 39 | """ 40 | @type message_content :: ex_webrtc_message | json_data_message 41 | 42 | @typedoc """ 43 | Messages sent and received if `message_format` is `ex_webrtc`. 44 | """ 45 | @type ex_webrtc_message :: ICECandidate.t() | SessionDescription.t() 46 | 47 | @typedoc """ 48 | Messages sent and received if `message_format` is `json_data`. 49 | 50 | The keys and values are the following 51 | - `%{"type" => "sdp_offer", "data" => data}`, where data is the return value of 52 | `ExWebRTC.SessionDescription.to_json/1` or `RTCPeerConnection.create_offer` in the JavaScript API 53 | - `%{"type" => "sdp_answer", "data" => data}`, where data is the return value of 54 | `ExWebRTC.SessionDescription.to_json/1` or `RTCPeerConnection.create_answer` in the JavaScript API 55 | - `%{"type" => "ice_candidate", "data" => data}`, where data is the return value of 56 | `ExWebRTC.ICECandidate.to_json/1` or `event.candidate` from the `RTCPeerConnection.onicecandidate` 57 | callback in the JavaScript API. 58 | """ 59 | @type json_data_message :: %{String.t() => term} 60 | 61 | @spec new() :: t 62 | def new() do 63 | {:ok, pid} = GenServer.start_link(__MODULE__, []) 64 | %__MODULE__{pid: pid} 65 | end 66 | 67 | @doc """ 68 | Registers a process as a peer, so that it can send and receive signaling messages. 69 | 70 | Options: 71 | - `pid` - pid of the peer, `self()` by default 72 | - `message_format` - `:ex_webrtc` by default, see `t:message_content/0` 73 | 74 | See the moduledoc for details. 75 | """ 76 | @spec register_peer(t, message_format: :ex_webrtc | :json_data, pid: pid) :: :ok 77 | def register_peer(%__MODULE__{pid: pid}, opts \\ []) do 78 | opts = 79 | opts 80 | |> Keyword.validate!(message_format: :ex_webrtc, pid: self()) 81 | |> Map.new() 82 | |> Map.put(:is_element, false) 83 | 84 | GenServer.call(pid, {:register_peer, opts}) 85 | end 86 | 87 | @doc false 88 | @spec register_element(t) :: :ok 89 | def register_element(%__MODULE__{pid: pid}) do 90 | GenServer.call( 91 | pid, 92 | {:register_peer, %{pid: self(), message_format: :ex_webrtc, is_element: true}} 93 | ) 94 | end 95 | 96 | @doc """ 97 | Sends a signaling message to the signaling channel. 98 | 99 | The calling process must be previously registered with `register_peer/2`. 100 | See the moduledoc for details. 101 | """ 102 | @spec signal(t, message_content, metadata :: map) :: :ok 103 | def signal(%__MODULE__{pid: pid}, message, metadata \\ %{}) do 104 | send(pid, {:signal, self(), message, metadata}) 105 | :ok 106 | end 107 | 108 | @spec close(t) :: :ok 109 | def close(%__MODULE__{pid: pid}) do 110 | GenServer.stop(pid) 111 | end 112 | 113 | @impl true 114 | def init(_opts) do 115 | state = %{ 116 | peer_a: nil, 117 | peer_b: nil, 118 | message_queue: [] 119 | } 120 | 121 | {:ok, state} 122 | end 123 | 124 | @impl true 125 | def handle_call({:register_peer, peer}, _from, state) do 126 | Process.monitor(peer.pid) 127 | 128 | case state do 129 | %{peer_a: nil} -> 130 | {:reply, :ok, %{state | peer_a: peer}} 131 | 132 | %{peer_b: nil, message_queue: queue} -> 133 | state = %{state | peer_b: peer} 134 | 135 | queue 136 | |> Enum.reverse() 137 | |> Enum.each(fn {message, metadata} -> 138 | send_peer(state.peer_a, state.peer_b, message, metadata) 139 | end) 140 | 141 | {:reply, :ok, %{state | message_queue: []}} 142 | 143 | state -> 144 | raise """ 145 | Cannot register a peer, both peers already registered: \ 146 | #{inspect(state.peer_a.pid)}, #{inspect(state.peer_b.pid)} 147 | """ 148 | end 149 | end 150 | 151 | @impl true 152 | def handle_info({:signal, _from_pid, message, metadata}, %{peer_b: nil} = state) do 153 | {:noreply, %{state | message_queue: [{message, metadata} | state.message_queue]}} 154 | end 155 | 156 | @impl true 157 | def handle_info({:signal, from_pid, message, metadata}, state) do 158 | {from_peer, to_peer} = get_peers(from_pid, state) 159 | send_peer(from_peer, to_peer, message, metadata) 160 | {:noreply, state} 161 | end 162 | 163 | @impl true 164 | def handle_info({:DOWN, _monitor, :process, pid, reason}, state) do 165 | {peer, _other_peer} = get_peers(pid, state) 166 | reason = if peer.is_element, do: reason, else: :normal 167 | {:stop, reason, state} 168 | end 169 | 170 | defp get_peers(pid, state) do 171 | case state do 172 | %{peer_a: %{pid: ^pid} = peer_a, peer_b: peer_b} -> {peer_a, peer_b} 173 | %{peer_a: peer_a, peer_b: %{pid: ^pid} = peer_b} -> {peer_b, peer_a} 174 | end 175 | end 176 | 177 | defp send_peer( 178 | %{message_format: format}, 179 | %{message_format: format, pid: pid}, 180 | message, 181 | metadata 182 | ) do 183 | send(pid, {:membrane_webrtc_signaling, self(), message, metadata}) 184 | end 185 | 186 | defp send_peer( 187 | %{message_format: :ex_webrtc}, 188 | %{message_format: :json_data, pid: pid}, 189 | message, 190 | metadata 191 | ) do 192 | json_data = 193 | case message do 194 | %ICECandidate{} -> 195 | %{"type" => "ice_candidate", "data" => ICECandidate.to_json(message)} 196 | 197 | %SessionDescription{type: type} -> 198 | %{"type" => "sdp_#{type}", "data" => SessionDescription.to_json(message)} 199 | end 200 | 201 | send(pid, {:membrane_webrtc_signaling, self(), json_data, metadata}) 202 | end 203 | 204 | defp send_peer( 205 | %{message_format: :json_data}, 206 | %{message_format: :ex_webrtc, pid: pid}, 207 | message, 208 | metadata 209 | ) do 210 | message = 211 | case message do 212 | %{"type" => "ice_candidate", "data" => candidate} -> ICECandidate.from_json(candidate) 213 | %{"type" => "sdp_offer", "data" => offer} -> SessionDescription.from_json(offer) 214 | %{"type" => "sdp_answer", "data" => answer} -> SessionDescription.from_json(answer) 215 | end 216 | 217 | send(pid, {:membrane_webrtc_signaling, self(), message, metadata}) 218 | end 219 | end 220 | -------------------------------------------------------------------------------- /.credo.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Credo and you are probably reading 2 | # this after creating it with `mix credo.gen.config`. 3 | # 4 | # If you find anything wrong or unclear in this file, please report an 5 | # issue on GitHub: https://github.com/rrrene/credo/issues 6 | # 7 | %{ 8 | # 9 | # You can have as many configs as you like in the `configs:` field. 10 | configs: [ 11 | %{ 12 | # 13 | # Run any config using `mix credo -C `. If no config name is given 14 | # "default" is used. 15 | # 16 | name: "default", 17 | # 18 | # These are the files included in the analysis: 19 | files: %{ 20 | # 21 | # You can give explicit globs or simply directories. 22 | # In the latter case `**/*.{ex,exs}` will be used. 23 | # 24 | included: [ 25 | "lib/", 26 | "src/", 27 | "test/", 28 | "web/", 29 | "apps/*/lib/", 30 | "apps/*/src/", 31 | "apps/*/test/", 32 | "apps/*/web/" 33 | ], 34 | excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] 35 | }, 36 | # 37 | # Load and configure plugins here: 38 | # 39 | plugins: [], 40 | # 41 | # If you create your own checks, you must specify the source files for 42 | # them here, so they can be loaded by Credo before running the analysis. 43 | # 44 | requires: [], 45 | # 46 | # If you want to enforce a style guide and need a more traditional linting 47 | # experience, you can change `strict` to `true` below: 48 | # 49 | strict: false, 50 | # 51 | # To modify the timeout for parsing files, change this value: 52 | # 53 | parse_timeout: 5000, 54 | # 55 | # If you want to use uncolored output by default, you can change `color` 56 | # to `false` below: 57 | # 58 | color: true, 59 | # 60 | # You can customize the parameters of any check by adding a second element 61 | # to the tuple. 62 | # 63 | # To disable a check put `false` as second element: 64 | # 65 | # {Credo.Check.Design.DuplicatedCode, false} 66 | # 67 | checks: [ 68 | # 69 | ## Consistency Checks 70 | # 71 | {Credo.Check.Consistency.ExceptionNames, []}, 72 | {Credo.Check.Consistency.LineEndings, []}, 73 | {Credo.Check.Consistency.ParameterPatternMatching, []}, 74 | {Credo.Check.Consistency.SpaceAroundOperators, []}, 75 | {Credo.Check.Consistency.SpaceInParentheses, []}, 76 | {Credo.Check.Consistency.TabsOrSpaces, []}, 77 | 78 | # 79 | ## Design Checks 80 | # 81 | # You can customize the priority of any check 82 | # Priority values are: `low, normal, high, higher` 83 | # 84 | {Credo.Check.Design.AliasUsage, 85 | [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, 86 | # You can also customize the exit_status of each check. 87 | # If you don't want TODO comments to cause `mix credo` to fail, just 88 | # set this value to 0 (zero). 89 | # 90 | {Credo.Check.Design.TagTODO, [exit_status: 0]}, 91 | {Credo.Check.Design.TagFIXME, []}, 92 | 93 | # 94 | ## Readability Checks 95 | # 96 | {Credo.Check.Readability.AliasOrder, [priority: :normal]}, 97 | {Credo.Check.Readability.FunctionNames, []}, 98 | {Credo.Check.Readability.LargeNumbers, []}, 99 | {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, 100 | {Credo.Check.Readability.ModuleAttributeNames, []}, 101 | {Credo.Check.Readability.ModuleDoc, []}, 102 | {Credo.Check.Readability.ModuleNames, []}, 103 | {Credo.Check.Readability.ParenthesesInCondition, []}, 104 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, parens: true}, 105 | {Credo.Check.Readability.PredicateFunctionNames, []}, 106 | {Credo.Check.Readability.PreferImplicitTry, []}, 107 | {Credo.Check.Readability.RedundantBlankLines, []}, 108 | {Credo.Check.Readability.Semicolons, []}, 109 | {Credo.Check.Readability.SpaceAfterCommas, []}, 110 | {Credo.Check.Readability.StringSigils, []}, 111 | {Credo.Check.Readability.TrailingBlankLine, []}, 112 | {Credo.Check.Readability.TrailingWhiteSpace, []}, 113 | {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, 114 | {Credo.Check.Readability.VariableNames, []}, 115 | {Credo.Check.Readability.WithSingleClause, false}, 116 | 117 | # 118 | ## Refactoring Opportunities 119 | # 120 | {Credo.Check.Refactor.CondStatements, []}, 121 | {Credo.Check.Refactor.CyclomaticComplexity, []}, 122 | {Credo.Check.Refactor.FunctionArity, []}, 123 | {Credo.Check.Refactor.LongQuoteBlocks, []}, 124 | {Credo.Check.Refactor.MapInto, false}, 125 | {Credo.Check.Refactor.MatchInCondition, []}, 126 | {Credo.Check.Refactor.NegatedConditionsInUnless, []}, 127 | {Credo.Check.Refactor.NegatedConditionsWithElse, []}, 128 | {Credo.Check.Refactor.Nesting, []}, 129 | {Credo.Check.Refactor.UnlessWithElse, []}, 130 | {Credo.Check.Refactor.WithClauses, []}, 131 | 132 | # 133 | ## Warnings 134 | # 135 | {Credo.Check.Warning.BoolOperationOnSameValues, []}, 136 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, 137 | {Credo.Check.Warning.IExPry, []}, 138 | {Credo.Check.Warning.IoInspect, []}, 139 | {Credo.Check.Warning.LazyLogging, false}, 140 | {Credo.Check.Warning.MixEnv, []}, 141 | {Credo.Check.Warning.OperationOnSameValues, []}, 142 | {Credo.Check.Warning.OperationWithConstantResult, []}, 143 | {Credo.Check.Warning.RaiseInsideRescue, []}, 144 | {Credo.Check.Warning.UnusedEnumOperation, []}, 145 | {Credo.Check.Warning.UnusedFileOperation, []}, 146 | {Credo.Check.Warning.UnusedKeywordOperation, []}, 147 | {Credo.Check.Warning.UnusedListOperation, []}, 148 | {Credo.Check.Warning.UnusedPathOperation, []}, 149 | {Credo.Check.Warning.UnusedRegexOperation, []}, 150 | {Credo.Check.Warning.UnusedStringOperation, []}, 151 | {Credo.Check.Warning.UnusedTupleOperation, []}, 152 | {Credo.Check.Warning.UnsafeExec, []}, 153 | 154 | # 155 | # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) 156 | 157 | # 158 | # Controversial and experimental checks (opt-in, just replace `false` with `[]`) 159 | # 160 | {Credo.Check.Readability.StrictModuleLayout, 161 | priority: :normal, order: ~w/shortdoc moduledoc behaviour use import require alias/a}, 162 | {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, 163 | {Credo.Check.Consistency.UnusedVariableNames, force: :meaningful}, 164 | {Credo.Check.Design.DuplicatedCode, false}, 165 | {Credo.Check.Readability.AliasAs, false}, 166 | {Credo.Check.Readability.MultiAlias, false}, 167 | {Credo.Check.Readability.SinglePipe, false}, 168 | {Credo.Check.Readability.WithCustomTaggedTuple, false}, 169 | {Credo.Check.Refactor.ABCSize, false}, 170 | {Credo.Check.Refactor.AppendSingleItem, false}, 171 | {Credo.Check.Refactor.DoubleBooleanNegation, false}, 172 | {Credo.Check.Refactor.ModuleDependencies, false}, 173 | {Credo.Check.Refactor.NegatedIsNil, false}, 174 | {Credo.Check.Refactor.PipeChainStart, false}, 175 | {Credo.Check.Refactor.VariableRebinding, false}, 176 | {Credo.Check.Warning.LeakyEnvironment, false}, 177 | {Credo.Check.Warning.MapGetUnsafePass, false}, 178 | {Credo.Check.Warning.UnsafeToAtom, false} 179 | 180 | # 181 | # Custom checks can be created using `mix credo.gen.check`. 182 | # 183 | ] 184 | } 185 | ] 186 | } 187 | -------------------------------------------------------------------------------- /lib/membrane_webrtc/live/capture.ex: -------------------------------------------------------------------------------- 1 | if Code.ensure_loaded?(Phoenix) and Code.ensure_loaded?(Phoenix.LiveView) do 2 | defmodule Membrane.WebRTC.Live.Capture do 3 | @moduledoc ~S''' 4 | LiveView for capturing audio and video from a browser and sending it via WebRTC to `Membrane.WebRTC.Source`. 5 | 6 | *Note:* This module will be available in your code only if you add `{:phoenix, "~> 1.7"}` 7 | and `{:phoenix_live_view, "~> 1.0"}` to the dependencies of of your root project. 8 | 9 | It: 10 | * creates WebRTC PeerConnection on the browser side. 11 | * forwards signaling messages between the browser and `Membrane.WebRTC.Source` via `Membrane.WebRTC.Signaling`. 12 | * sends audio and video streams to the related `Membrane.WebRTC.Source`. 13 | 14 | ## JavaScript Hook 15 | 16 | Capture LiveView requires JavaScript hook to be registered under `Capture` name. 17 | The hook can be created using `createCaptureHook` function. 18 | For example: 19 | 20 | ```javascript 21 | import { createCaptureHook } from "membrane_webrtc_plugin"; 22 | let Hooks = {}; 23 | const iceServers = [{ urls: "stun:stun.l.google.com:19302" }]; 24 | Hooks.Capture = createCaptureHook(iceServers); 25 | let liveSocket = new LiveSocket("/live", Socket, { 26 | // ... 27 | hooks: Hooks 28 | }); 29 | ``` 30 | 31 | ## Examples 32 | 33 | ```elixir 34 | defmodule StreamerWeb.StreamSenderLive do 35 | use StreamerWeb, :live_view 36 | 37 | alias Membrane.WebRTC.Live.Capture 38 | 39 | @impl true 40 | def render(assigns) do 41 | ~H""" 42 | 43 | """ 44 | end 45 | 46 | @impl true 47 | def mount(_params, _session, socket) do 48 | signaling = Membrane.WebRTC.Signaling.new() 49 | {:ok, _supervisor, _pipelne} = Membrane.Pipeline.start_link(MyPipeline, signaling: signaling) 50 | 51 | socket = socket |> Capture.attach(id: "capture", signaling: signaling) 52 | {:ok, socket} 53 | end 54 | end 55 | ``` 56 | ''' 57 | use Phoenix.LiveView 58 | require Logger 59 | 60 | alias Membrane.WebRTC.Signaling 61 | 62 | @type t() :: %__MODULE__{ 63 | id: String.t(), 64 | signaling: Signaling.t(), 65 | preview?: boolean(), 66 | audio?: boolean(), 67 | video?: boolean() 68 | } 69 | 70 | defstruct id: nil, signaling: nil, video?: true, audio?: true, preview?: true 71 | 72 | attr(:socket, Phoenix.LiveView.Socket, required: true, doc: "Parent live view socket") 73 | 74 | attr(:capture_id, :string, 75 | required: true, 76 | doc: """ 77 | ID of a `caputre` previously attached to the socket. It has to be the same as the value passed to `:id` 78 | field `#{inspect(__MODULE__)}.attach/2`. 79 | """ 80 | ) 81 | 82 | attr(:class, :string, default: "", doc: "CSS/Tailwind classes for styling") 83 | 84 | @doc """ 85 | Helper function for rendering Capture LiveView. 86 | """ 87 | def live_render(assigns) do 88 | ~H""" 89 | <%= live_render(@socket, __MODULE__, id: "#{@capture_id}-lv", session: %{"class" => @class, "id" => @capture_id}) %> 90 | """ 91 | end 92 | 93 | @doc """ 94 | Attaches required hooks and creates `#{inspect(__MODULE__)}` struct. 95 | 96 | Created struct is saved in socket's assigns and then 97 | it is sent by an attached hook to a child LiveView process. 98 | 99 | Options: 100 | * `id` - capture id. It is used to identify live view and generated HTML video player. It must be unique 101 | within single page. 102 | * `signaling` - `Membrane.WebRTC.Signaling.t()`, that has been passed to `Membrane.WebRTC.Source` as well. 103 | * `video?` - if `true`, the video stream from the computer camera will be captured. Defaults to `true`. 104 | * `audio?` - if `true`, the audio stream from the computer microphone will be captured. Defaults to `true`. 105 | * `preview?` - if `true`, the function `#{inspect(__MODULE__)}.live_render/1` will return a video HTML tag 106 | with attached captured video stream. Defaults to `true`. 107 | """ 108 | @spec attach(Phoenix.LiveView.Socket.t(), Keyword.t()) :: Phoenix.LiveView.Socket.t() 109 | def attach(socket, opts) do 110 | opts = 111 | opts 112 | |> Keyword.validate!([ 113 | :id, 114 | :signaling, 115 | video?: true, 116 | audio?: true, 117 | preview?: true 118 | ]) 119 | 120 | capture = struct!(__MODULE__, opts) 121 | 122 | all_captures = 123 | socket.assigns 124 | |> Map.get(__MODULE__, %{}) 125 | |> Map.put(capture.id, capture) 126 | 127 | socket 128 | |> assign(__MODULE__, all_captures) 129 | |> detach_hook(:capture_handshake, :handle_info) 130 | |> attach_hook(:capture_handshake, :handle_info, &parent_handshake/2) 131 | end 132 | 133 | @spec get_attached(Phoenix.LiveView.Socket.t(), String.t()) :: t() 134 | def get_attached(socket, id), do: socket.assigns[__MODULE__][id] 135 | 136 | ## CALLBACKS 137 | 138 | @impl true 139 | def render(%{capture: nil} = assigns) do 140 | ~H""" 141 | """ 142 | end 143 | 144 | @impl true 145 | def render(%{capture: %__MODULE__{preview?: true}} = assigns) do 146 | ~H""" 147 | 154 | """ 155 | end 156 | 157 | @impl true 158 | def render(%{capture: %__MODULE__{preview?: false}} = assigns) do 159 | ~H""" 160 | 161 | """ 162 | end 163 | 164 | @impl true 165 | def mount(_params, %{"class" => class, "id" => id}, socket) do 166 | socket = socket |> assign(class: class, capture: nil) 167 | 168 | socket = 169 | if connected?(socket), 170 | do: client_handshake(socket, id), 171 | else: socket 172 | 173 | {:ok, socket} 174 | end 175 | 176 | defp parent_handshake({__MODULE__, {:connected, id, capture_pid}}, socket) do 177 | capture_struct = 178 | socket.assigns 179 | |> Map.fetch!(__MODULE__) 180 | |> Map.fetch!(id) 181 | 182 | send(capture_pid, capture_struct) 183 | 184 | {:halt, socket} 185 | end 186 | 187 | defp parent_handshake(_msg, socket) do 188 | {:cont, socket} 189 | end 190 | 191 | defp client_handshake(socket, id) do 192 | send(socket.parent_pid, {__MODULE__, {:connected, id, self()}}) 193 | 194 | receive do 195 | %__MODULE__{} = capture -> 196 | capture.signaling 197 | |> Signaling.register_peer(message_format: :json_data) 198 | 199 | media_constraints = %{ 200 | "audio" => capture.audio?, 201 | "video" => capture.video? 202 | } 203 | 204 | socket 205 | |> assign(capture: capture) 206 | |> push_event("media_constraints-#{capture.id}", media_constraints) 207 | after 208 | 5000 -> exit(:timeout) 209 | end 210 | end 211 | 212 | @impl true 213 | def handle_info({:membrane_webrtc_signaling, _pid, message, _metadata}, socket) do 214 | Logger.debug(""" 215 | #{log_prefix(socket.assigns.capture.id)} Sent WebRTC signaling message: #{inspect(message, pretty: true)} 216 | """) 217 | 218 | {:noreply, 219 | socket 220 | |> push_event("webrtc_signaling-#{socket.assigns.capture.id}", message)} 221 | end 222 | 223 | @impl true 224 | def handle_event("webrtc_signaling", message, socket) do 225 | Logger.debug(""" 226 | #{log_prefix(socket.assigns.capture.id)} Received WebRTC signaling message: #{inspect(message, pretty: true)} 227 | """) 228 | 229 | if message["data"] do 230 | socket.assigns.capture.signaling 231 | |> Signaling.signal(message) 232 | end 233 | 234 | {:noreply, socket} 235 | end 236 | 237 | defp log_prefix(id), do: [module: __MODULE__, id: id] |> inspect() 238 | end 239 | end 240 | -------------------------------------------------------------------------------- /lib/membrane_webrtc/sink.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.WebRTC.Sink do 2 | @moduledoc """ 3 | Membrane Bin that allows sending audio and video tracks via WebRTC. 4 | 5 | It sends an SDP offer and expects an answer during initialization and 6 | each time when new tracks are added. For more information about signaling, 7 | see the `signaling` option. 8 | 9 | Before connecting pads, each audio and video track has to be negotiated. 10 | Tracks passed via `tracks` option are negotiated during initialization. 11 | You can negotiate more tracks by sending `t:add_tracks/0` notification 12 | and waiting for `t:new_tracks/0` notification reply. 13 | 14 | When the tracks are negotiated, pads can be linked. The pad either 15 | has to have a `kind` option set or its id has to match the id of 16 | the track received in `t:new_tracks/0` notification. 17 | """ 18 | use Membrane.Bin 19 | 20 | alias Membrane.H264 21 | alias Membrane.RemoteStream 22 | alias Membrane.VP8 23 | alias Membrane.WebRTC.{ExWebRTCSink, Signaling, SimpleWebSocketServer} 24 | 25 | @typedoc """ 26 | Notification that should be sent to the bin to negotiate new tracks. 27 | 28 | See the moduledoc for details. 29 | """ 30 | @type add_tracks :: {:add_tracks, [:audio | :video]} 31 | 32 | @typedoc """ 33 | Notification sent when new tracks are negotiated. 34 | 35 | See the moduledoc for details. 36 | """ 37 | @type new_tracks :: {:new_tracks, [%{id: term, kind: :audio | :video}]} 38 | 39 | @typedoc """ 40 | WHIP client options 41 | 42 | - `uri` - Address of the WHIP server (HTTP/HTTPS) 43 | - `token` - WHIP token, defaults to an empty string 44 | """ 45 | @type whip_options :: [{:uri, String.t()} | {:token, String.t()}] 46 | 47 | def_options signaling: [ 48 | spec: 49 | Signaling.t() 50 | | {:whip, whip_options} 51 | | {:websocket, SimpleWebSocketServer.options()}, 52 | description: """ 53 | Signaling channel for passing WebRTC signaling messages (SDP and ICE). 54 | Either: 55 | - `#{inspect(Signaling)}` - See its docs for details. 56 | - `{:whip, options}` - Acts as a WHIP client, see `t:whip_options/0` for details. 57 | - `{:websocket, options}` - Spawns #{inspect(SimpleWebSocketServer)}, 58 | see there for details. 59 | """ 60 | ], 61 | tracks: [ 62 | spec: [:audio | :video], 63 | default: [:audio, :video], 64 | description: """ 65 | Tracks to be negotiated. By default one audio and one video track 66 | is negotiated, meaning that at most one audio and one video can be 67 | sent. 68 | """ 69 | ], 70 | video_codec: [ 71 | spec: :vp8 | :h264 | [:vp8 | :h264], 72 | default: [:vp8, :h264], 73 | description: """ 74 | Video codecs, that #{inspect(__MODULE__)} will try to negotiatie in SDP 75 | message exchange. Even if `[:vp8, :h264]` is passed to this option, there 76 | is a chance, that one of these codecs won't be approved by the second 77 | WebRTC peer. 78 | 79 | After SDP messages exchange, #{inspect(__MODULE__)} will send a parent 80 | notification `{:negotiated_video_codecs, codecs}` where `codecs` is 81 | a list of supported codecs. 82 | """ 83 | ], 84 | ice_servers: [ 85 | spec: [ExWebRTC.PeerConnection.Configuration.ice_server()], 86 | default: [%{urls: "stun:stun.l.google.com:19302"}] 87 | ], 88 | ice_port_range: [ 89 | spec: Enumerable.t(non_neg_integer()), 90 | default: [0] 91 | ], 92 | ice_ip_filter: [ 93 | spec: (:inet.ip_address() -> boolean()), 94 | default: &__MODULE__.default_ice_ip_filter/1 95 | ], 96 | payload_rtp: [ 97 | spec: boolean(), 98 | default: true 99 | ] 100 | 101 | def_input_pad :input, 102 | accepted_format: 103 | any_of( 104 | %Membrane.H264{alignment: :nalu}, 105 | %Membrane.RemoteStream{content_format: Membrane.VP8}, 106 | Membrane.VP8, 107 | Membrane.Opus, 108 | Membrane.RTP 109 | ), 110 | availability: :on_request, 111 | options: [ 112 | kind: [ 113 | spec: :audio | :video | nil, 114 | default: nil, 115 | description: """ 116 | When set, the pad is associated with the first negotiated track 117 | of the given kind. See the moduledoc for details. 118 | """ 119 | ] 120 | ] 121 | 122 | @impl true 123 | def handle_init(_ctx, opts) do 124 | :ok = Membrane.WebRTC.Utils.validate_signaling!(opts.signaling) 125 | 126 | spec = 127 | child(:webrtc, %ExWebRTCSink{ 128 | signaling: opts.signaling, 129 | tracks: opts.tracks, 130 | video_codec: opts.video_codec, 131 | ice_servers: opts.ice_servers, 132 | ice_port_range: opts.ice_port_range, 133 | ice_ip_filter: opts.ice_ip_filter 134 | }) 135 | 136 | {[spec: spec], %{payload_rtp: opts.payload_rtp, video_codec: opts.video_codec}} 137 | end 138 | 139 | @impl true 140 | def handle_setup(_ctx, state) do 141 | {[setup: :incomplete], state} 142 | end 143 | 144 | @impl true 145 | def handle_pad_added(Pad.ref(:input, pid) = pad_ref, %{pad_options: %{kind: kind}}, state) do 146 | spec = 147 | cond do 148 | not state.payload_rtp -> 149 | bin_input(pad_ref) 150 | |> via_in(pad_ref, options: [kind: kind]) 151 | |> get_child(:webrtc) 152 | 153 | kind == :audio -> 154 | bin_input(pad_ref) 155 | |> child({:rtp_opus_payloader, pid}, Membrane.RTP.Opus.Payloader) 156 | |> via_in(pad_ref, options: [kind: :audio, codec: :opus]) 157 | |> get_child(:webrtc) 158 | 159 | kind == :video -> 160 | bin_input(pad_ref) 161 | |> child({:connector, pad_ref}, %Membrane.Connector{notify_on_stream_format?: true}) 162 | end 163 | 164 | {[spec: spec], state} 165 | end 166 | 167 | @impl true 168 | def handle_child_notification( 169 | {:stream_format, _connector_pad, _stream_format}, 170 | {:connector, pad_ref}, 171 | ctx, 172 | state 173 | ) 174 | when is_map_key(ctx.children, {:rtp_payloader, pad_ref}) do 175 | {[], state} 176 | end 177 | 178 | @impl true 179 | def handle_child_notification( 180 | {:stream_format, _connector_pad, stream_format}, 181 | {:connector, pad_ref}, 182 | _ctx, 183 | state 184 | ) do 185 | codec = 186 | case stream_format do 187 | %H264{} -> :h264 188 | %VP8{} -> :vp8 189 | %RemoteStream{content_format: VP8} -> :vp8 190 | end 191 | 192 | payloader = 193 | case codec do 194 | :h264 -> %Membrane.RTP.H264.Payloader{max_payload_size: 1000} 195 | :vp8 -> Membrane.RTP.VP8.Payloader 196 | end 197 | 198 | spec = 199 | get_child({:connector, pad_ref}) 200 | |> child({:rtp_payloader, pad_ref}, payloader) 201 | |> via_in(pad_ref, options: [kind: :video, codec: codec]) 202 | |> get_child(:webrtc) 203 | 204 | {[spec: spec], state} 205 | end 206 | 207 | @impl true 208 | def handle_child_notification(:connected, :webrtc, _ctx, state) do 209 | {[setup: :complete], state} 210 | end 211 | 212 | @impl true 213 | def handle_child_notification({type, _content} = notification, :webrtc, _ctx, state) 214 | when type in [:new_tracks, :negotiated_video_codecs] do 215 | {[notify_parent: notification], state} 216 | end 217 | 218 | @impl true 219 | def handle_parent_notification({:add_tracks, tracks}, _ctx, state) do 220 | {[notify_child: {:webrtc, {:add_tracks, tracks}}], state} 221 | end 222 | 223 | @impl true 224 | def handle_element_end_of_stream(:webrtc, Pad.ref(:input, id), _ctx, state) do 225 | {[notify_parent: {:end_of_stream, id}], state} 226 | end 227 | 228 | @impl true 229 | def handle_element_end_of_stream(_name, _pad, _ctx, state) do 230 | {[], state} 231 | end 232 | 233 | def default_ice_ip_filter(_ip), do: true 234 | end 235 | -------------------------------------------------------------------------------- /lib/membrane_webrtc/whip_server.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.WebRTC.WhipServer do 2 | @moduledoc """ 3 | Server accepting WHIP connections. 4 | 5 | Accepts the following options: 6 | 7 | - `handle_new_client` - function that accepts the client token and returns either 8 | the signaling channel to negotiate the connection or error to reject it. The signaling 9 | channel can be passed to `Membrane.WebRTC.Source`. 10 | - `serve_static` - path to static assets that should be served along with WHIP, 11 | under a `/static` endpoint. Useful to serve HTML assets. If set to `false` (default), 12 | no static assets are served 13 | - Any of `t:Bandit.options/0` - Bandit configuration 14 | """ 15 | 16 | alias Membrane.WebRTC.Signaling 17 | 18 | @type option :: 19 | {:handle_new_client, 20 | (token :: String.t() -> {:ok, Signaling.t()} | {:error, reason :: term()})} 21 | | {:serve_static, String.t() | false} 22 | | {atom, term()} 23 | @spec child_spec([option()]) :: Supervisor.child_spec() 24 | def child_spec(opts) do 25 | Bandit.child_spec(bandit_opts(opts)) 26 | end 27 | 28 | @spec start_link([option()]) :: Supervisor.on_start() 29 | def start_link(opts) do 30 | Bandit.start_link(bandit_opts(opts)) 31 | end 32 | 33 | defp bandit_opts(opts) do 34 | {whip_opts, bandit_opts} = Keyword.split(opts, [:handle_new_client, :serve_static]) 35 | plug = {__MODULE__.Router, whip_opts} 36 | [plug: plug] ++ bandit_opts 37 | end 38 | 39 | defmodule Router do 40 | @moduledoc """ 41 | WHIP router pluggable to a plug pipeline. 42 | 43 | Accepts the same options as `Membrane.WebRTC.WhipServer`. 44 | 45 | ## Example 46 | 47 | ``` 48 | defmodule Router do 49 | use Plug.Router 50 | 51 | plug(Plug.Logger) 52 | plug(Plug.Static, at: "/static", from: "assets") 53 | plug(:match) 54 | plug(:dispatch) 55 | 56 | forward( 57 | "/whip", 58 | to: Membrane.WebRTC.WhipServer.Router, 59 | handle_new_client: &__MODULE__.handle_new_client/1 60 | ) 61 | 62 | def handle_new_client(token) do 63 | validate_token!(token) 64 | signaling = Membrane.WebRTC.Signaling.new() 65 | # pass the signaling to a pipeline 66 | {:ok, signaling} 67 | end 68 | end 69 | 70 | Bandit.start_link(plug: Router, ip: any) 71 | ``` 72 | """ 73 | use Plug.Router 74 | 75 | plug(Plug.Logger, log: :debug) 76 | plug(Corsica, origins: "*", allow_methods: :all, allow_headers: :all) 77 | plug(:match) 78 | plug(:dispatch) 79 | 80 | # TODO: the HTTP response codes are not completely compliant with the RFCs 81 | 82 | defmodule ClientHandler do 83 | @moduledoc false 84 | use GenServer 85 | 86 | @spec start_link(GenServer.options()) :: {:ok, pid()} 87 | def start_link(opts), do: GenServer.start_link(__MODULE__, [], opts) 88 | 89 | @spec exec(GenServer.server(), (state -> {resp, state})) :: resp 90 | when state: term(), resp: term() 91 | def exec(client_handler, fun), do: GenServer.call(client_handler, {:exec, fun}) 92 | @spec stop(GenServer.server()) :: :ok 93 | def stop(client_handler), do: GenServer.stop(client_handler) 94 | 95 | @impl true 96 | def init(_opts), do: {:ok, nil} 97 | 98 | @impl true 99 | def handle_call({:exec, fun}, _from, state) do 100 | {reply, state} = fun.(state) 101 | {:reply, reply, state} 102 | end 103 | 104 | @impl true 105 | def handle_info(_message, state), do: {:noreply, state} 106 | end 107 | 108 | post "/" do 109 | with {:ok, token} <- get_token(conn), 110 | {:ok, offer_sdp, conn} <- get_body(conn, "application/sdp"), 111 | resource_id = generate_resource_id(), 112 | {:ok, client_handler} = ClientHandler.start_link(name: handler_name(resource_id)), 113 | {:ok, answer_sdp} <- 114 | get_answer(client_handler, offer_sdp, token, conn.private.whip.handle_new_client) do 115 | Process.unlink(client_handler) 116 | 117 | conn 118 | |> put_resp_header("location", Path.join(conn.request_path, "resource/#{resource_id}")) 119 | |> put_resp_header("access-control-expose-headers", "location") 120 | |> put_resp_content_type("application/sdp") 121 | |> resp(201, answer_sdp) 122 | else 123 | {:error, _other} -> resp(conn, 400, "Bad request") 124 | end 125 | |> send_resp() 126 | end 127 | 128 | patch "resource/:resource_id" do 129 | with {:ok, sdp, conn} <- get_body(conn, "application/trickle-ice-sdpfrag"), 130 | sdp = ExSDP.parse!(sdp), 131 | media = List.first(sdp.media), 132 | {"candidate", candidate} <- ExSDP.get_attribute(media, "candidate") || :no_candidate do 133 | {:ice_ufrag, ufrag} = ExSDP.get_attribute(sdp, :ice_ufrag) 134 | {:mid, mid} = ExSDP.get_attribute(media, :mid) 135 | 136 | candidate = %ExWebRTC.ICECandidate{ 137 | candidate: candidate, 138 | sdp_mid: mid, 139 | username_fragment: ufrag, 140 | sdp_m_line_index: 0 141 | } 142 | 143 | ClientHandler.exec(handler_name(resource_id), fn signaling -> 144 | Signaling.signal(signaling, candidate) 145 | {:ok, signaling} 146 | end) 147 | 148 | resp(conn, 204, "") 149 | else 150 | :no_candidate -> resp(conn, 204, "") 151 | {:error, _res} -> resp(conn, 400, "Bad request") 152 | end 153 | |> send_resp() 154 | end 155 | 156 | delete "resource/:resource_id" do 157 | ClientHandler.stop(handler_name(resource_id)) 158 | send_resp(conn, 204, "") 159 | end 160 | 161 | get "static/*_" do 162 | case conn.private.whip.plug_static do 163 | nil -> send_resp(conn, 404, "Not found") 164 | plug_static -> Plug.Static.call(conn, plug_static) 165 | end 166 | end 167 | 168 | match _ do 169 | send_resp(conn, 404, "Not found") 170 | end 171 | 172 | @impl true 173 | def init(opts) do 174 | {handle_new_client, opts} = Keyword.pop(opts, :handle_new_client) 175 | unless handle_new_client, do: raise("Missing option 'handle_new_client'") 176 | {serve_static, opts} = Keyword.pop(opts, :serve_static, false) 177 | if opts != [], do: raise("Unknown options: #{Enum.join(opts, ", ")}") 178 | 179 | plug_static = 180 | if serve_static, do: Plug.Static.init(at: "static", from: serve_static) 181 | 182 | super(%{handle_new_client: handle_new_client, plug_static: plug_static}) 183 | end 184 | 185 | @impl true 186 | def call(conn, opts) do 187 | conn 188 | |> put_private(:whip, opts) 189 | |> super(opts) 190 | end 191 | 192 | defp get_token(conn) do 193 | with ["Bearer " <> token] <- get_req_header(conn, "authorization") do 194 | {:ok, token} 195 | else 196 | _other -> {:error, :unauthorized} 197 | end 198 | end 199 | 200 | defp get_body(conn, content_type) do 201 | with [^content_type] <- get_req_header(conn, "content-type"), 202 | {:ok, body, conn} <- read_body(conn) do 203 | {:ok, body, conn} 204 | else 205 | headers when is_list(headers) -> {:error, :unsupported_media} 206 | _other -> {:error, :bad_request} 207 | end 208 | end 209 | 210 | defp get_answer(client_handler, offer_sdp, token, handle_new_client) do 211 | ClientHandler.exec(client_handler, fn _state -> 212 | with {:ok, signaling} <- handle_new_client.(token) do 213 | Signaling.register_peer(signaling) 214 | 215 | Signaling.signal( 216 | signaling, 217 | %ExWebRTC.SessionDescription{type: :offer, sdp: offer_sdp}, 218 | %{candidates_in_sdp: true} 219 | ) 220 | 221 | receive do 222 | {:membrane_webrtc_signaling, _pid, answer, _metadata} -> 223 | %ExWebRTC.SessionDescription{type: :answer, sdp: answer_sdp} = answer 224 | {{:ok, answer_sdp}, signaling} 225 | after 226 | 5000 -> raise "Timeout waiting for SDP answer" 227 | end 228 | else 229 | {:error, reason} -> {{:error, reason}, nil} 230 | end 231 | end) 232 | end 233 | 234 | defp generate_resource_id() do 235 | for _i <- 1..10, into: "", do: <> 236 | end 237 | 238 | defp handler_name(resource_id) do 239 | {:via, Registry, {Membrane.WebRTC.WhipRegistry, resource_id}} 240 | end 241 | end 242 | end 243 | -------------------------------------------------------------------------------- /lib/membrane_webrtc/source.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.WebRTC.Source do 2 | @moduledoc """ 3 | Membrane Bin that allows receiving audio and video tracks via WebRTC. 4 | 5 | It expects an SDP offer to be sent by the other peer at the beginning 6 | of playback and each time new tracks are added. For more information 7 | about signaling, see the `signaling` option. 8 | 9 | Pads connected immediately when the bin is created 10 | (in the same spec `t:Membrane.ChildrenSpec.t/0`) need to have the `kind` 11 | option set to `:audio` or `:video`. Each of those pads will be associated 12 | with the first WebRTC track of the given kind that arrives. 13 | 14 | When a WebRTC tracks arrive and there's no pad to link them to, 15 | the `t:new_tracks/0` notification is sent. Then, the corresponding pads 16 | should be linked - the id of each pad should match one of the track ids. 17 | """ 18 | use Membrane.Bin 19 | require Membrane.Logger 20 | 21 | alias Membrane.WebRTC.{ 22 | ExWebRTCSource, 23 | ExWebRTCUtils, 24 | Signaling, 25 | SimpleWebSocketServer 26 | } 27 | 28 | @typedoc """ 29 | Notification sent when new tracks arrive. 30 | 31 | See moduledoc for details. 32 | """ 33 | @type new_tracks :: {:new_tracks, [%{id: term, kind: :audio | :video}]} 34 | 35 | @typedoc """ 36 | Options for WHIP server input. 37 | 38 | The server accepts a single connection and the stream is received by this source. The options are: 39 | 40 | - `token` - either expected WHIP token or a function returning true if the token is valid, otherwise false 41 | - `serve_static` - make WHIP server also serve static content, such as an HTML page under `/static` endpoint 42 | - Any of `t:Bandit.options/0` - in particular `ip` and `port` 43 | 44 | To handle multiple connections and have more control over the server, see `Membrane.WebRTC.WhipServer`. 45 | """ 46 | @type whip_options :: [ 47 | {:token, String.t() | (String.t() -> boolean())} 48 | | {:serve_static, String.t()} 49 | | {atom, term()} 50 | ] 51 | 52 | def_options signaling: [ 53 | spec: 54 | Signaling.t() 55 | | {:whip, whip_options()} 56 | | {:websocket, SimpleWebSocketServer.options()}, 57 | description: """ 58 | Signaling channel for passing WebRTC signaling messages (SDP and ICE). 59 | Either: 60 | - `#{inspect(Signaling)}` - See its docs for details. 61 | - `{:whip, options}` - Starts a WHIP server, see `t:whip_options/0` for details. 62 | - `{:websocket, options}` - Spawns #{inspect(SimpleWebSocketServer)}, 63 | see there for details. 64 | """ 65 | ], 66 | allowed_video_codecs: [ 67 | spec: :vp8 | :h264 | [:vp8 | :h264], 68 | default: :vp8, 69 | description: """ 70 | Specifies, which video codecs can be accepted by the source during the SDP 71 | negotiaion. 72 | 73 | Either `:vp8`, `:h264` or a list containing both options. 74 | 75 | Event if it is set to `[:h264, :vp8]`, the source will negotiate at most 76 | one video codec. Negotiated codec can be deduced from 77 | `{:negotiated_video_codecs, codecs}` notification sent to the parent. 78 | 79 | If prefer to receive one video codec over another, but you are still able 80 | to handle both of them, use `:preferred_video_codec` option. 81 | 82 | By default only `:vp8`. 83 | """ 84 | ], 85 | preferred_video_codec: [ 86 | spec: :vp8 | :h264, 87 | default: :vp8, 88 | description: """ 89 | Specyfies, which video codec will be preferred by the source, if both of 90 | them will be available. 91 | 92 | Usage of this option makes sense only if there are at least 2 codecs 93 | specified in the `:allowed_video_codecs` option. 94 | 95 | Defaults to `:vp8`. 96 | """ 97 | ], 98 | keyframe_interval: [ 99 | spec: Membrane.Time.t() | nil, 100 | default: nil, 101 | description: """ 102 | If set, a keyframe will be requested as often as specified on each video 103 | track. 104 | """ 105 | ], 106 | ice_servers: [ 107 | spec: [ExWebRTC.PeerConnection.Configuration.ice_server()], 108 | default: [%{urls: "stun:stun.l.google.com:19302"}] 109 | ], 110 | ice_port_range: [ 111 | spec: Enumerable.t(non_neg_integer()), 112 | default: [0] 113 | ], 114 | ice_ip_filter: [ 115 | spec: (:inet.ip_address() -> boolean()), 116 | default: &__MODULE__.default_ice_ip_filter/1 117 | ], 118 | depayload_rtp: [ 119 | spec: boolean(), 120 | default: true 121 | ], 122 | sdp_candidates_timeout: [ 123 | spec: Membrane.Time.t(), 124 | default: Membrane.Time.seconds(1), 125 | default_inspector: &Membrane.Time.pretty_duration/1 126 | ] 127 | 128 | def_output_pad :output, 129 | accepted_format: 130 | any_of( 131 | Membrane.H264, 132 | %Membrane.RemoteStream{content_format: Membrane.VP8}, 133 | %Membrane.RemoteStream{content_format: Membrane.Opus}, 134 | Membrane.RTP 135 | ), 136 | availability: :on_request, 137 | options: [kind: [default: nil]] 138 | 139 | @impl true 140 | def handle_init(_ctx, opts) do 141 | opts = opts |> Map.from_struct() |> Map.update!(:allowed_video_codecs, &Bunch.listify/1) 142 | spec = child(:webrtc, struct(ExWebRTCSource, opts)) 143 | 144 | :ok = Membrane.WebRTC.Utils.validate_signaling!(opts.signaling) 145 | 146 | state = 147 | %{tracks: %{}, negotiated_video_codecs: nil, awaiting_pads: MapSet.new()} 148 | |> Map.merge(opts) 149 | 150 | {[spec: spec], state} 151 | end 152 | 153 | @impl true 154 | def handle_pad_added(Pad.ref(:output, pad_id) = pad_ref, ctx, state) do 155 | %{kind: kind} = ctx.pad_options 156 | track = state.tracks[pad_id] 157 | 158 | if ctx.playback == :stopped and kind == nil do 159 | raise "Option `kind` not specified for pad #{inspect(pad_ref)}" 160 | end 161 | 162 | if ctx.playback == :playing and track == nil do 163 | raise "Unknown track id #{inspect(pad_id)}, cannot link pad #{inspect(pad_ref)}" 164 | end 165 | 166 | link_webrtc(pad_ref, kind || track.kind, state) 167 | end 168 | 169 | defp link_webrtc(pad_ref, kind, state) do 170 | spec = 171 | get_child(:webrtc) 172 | |> via_out(pad_ref, options: [kind: kind]) 173 | 174 | {spec, state} = 175 | cond do 176 | not state.depayload_rtp -> 177 | {spec |> bin_output(pad_ref), state} 178 | 179 | kind == :audio -> 180 | {spec |> get_depayloader(:audio, state) |> bin_output(pad_ref), state} 181 | 182 | kind == :video and state.negotiated_video_codecs == nil -> 183 | spec = 184 | [ 185 | spec 186 | |> child({:input_connector, pad_ref}, Membrane.Connector), 187 | child({:output_connector, pad_ref}, Membrane.Connector) 188 | |> bin_output(pad_ref) 189 | ] 190 | 191 | state = state |> Map.update!(:awaiting_pads, &MapSet.put(&1, pad_ref)) 192 | {spec, state} 193 | 194 | kind == :video -> 195 | {spec |> get_depayloader(:video, state) |> bin_output(pad_ref), state} 196 | end 197 | 198 | {[spec: spec], state} 199 | end 200 | 201 | @impl true 202 | def handle_child_notification({:new_tracks, tracks}, :webrtc, _ctx, state) do 203 | tracks_map = Map.new(tracks, &{&1.id, &1}) 204 | state = %{state | tracks: Map.merge(state.tracks, tracks_map)} 205 | {[notify_parent: {:new_tracks, tracks}], state} 206 | end 207 | 208 | @impl true 209 | def handle_child_notification({:negotiated_video_codecs, codecs}, :webrtc, _ctx, state) do 210 | state = %{state | negotiated_video_codecs: codecs} 211 | 212 | spec = 213 | state.awaiting_pads 214 | |> Enum.map(fn pad_ref -> 215 | get_child({:input_connector, pad_ref}) 216 | |> get_depayloader(:video, state) 217 | |> get_child({:output_connector, pad_ref}) 218 | end) 219 | 220 | state = %{state | awaiting_pads: MapSet.new()} 221 | 222 | {[notify_parent: {:negotiated_video_codecs, codecs}, spec: spec], state} 223 | end 224 | 225 | @impl true 226 | def handle_child_notification(notification, child, _ctx, state) do 227 | Membrane.Logger.debug( 228 | "Received notification from child #{inspect(child)}: #{inspect(notification)}" 229 | ) 230 | 231 | {[], state} 232 | end 233 | 234 | @spec get_depayloader( 235 | Membrane.ChildrenSpec.builder(), 236 | :audio | :video, 237 | map() 238 | ) :: Membrane.ChildrenSpec.builder() | no_return() 239 | 240 | defp get_depayloader(builder, :audio, _state) do 241 | child(builder, {:depayloader_bin, make_ref()}, %Membrane.RTP.DepayloaderBin{ 242 | depayloader: Membrane.RTP.Opus.Depayloader, 243 | clock_rate: ExWebRTCUtils.codec_clock_rate(:opus) 244 | }) 245 | end 246 | 247 | defp get_depayloader(builder, :video, state) do 248 | cond do 249 | state.allowed_video_codecs == [:vp8] -> 250 | get_vp8_depayloader(builder) 251 | 252 | state.allowed_video_codecs == [:h264] -> 253 | get_h264_depayloader(builder) 254 | 255 | state.negotiated_video_codecs == [:vp8] -> 256 | get_vp8_depayloader(builder) 257 | 258 | state.negotiated_video_codecs == [:h264] -> 259 | get_h264_depayloader(builder) 260 | 261 | state.negotiated_video_codecs == nil -> 262 | raise "Cannot select depayloader before end of SDP messages exchange" 263 | end 264 | end 265 | 266 | defp get_vp8_depayloader(builder) do 267 | child(builder, {:depayloader_bin, make_ref()}, %Membrane.RTP.DepayloaderBin{ 268 | depayloader: Membrane.RTP.VP8.Depayloader, 269 | clock_rate: ExWebRTCUtils.codec_clock_rate(:vp8) 270 | }) 271 | end 272 | 273 | defp get_h264_depayloader(builder) do 274 | child(builder, {:depayloader_bin, make_ref()}, %Membrane.RTP.DepayloaderBin{ 275 | depayloader: Membrane.RTP.H264.Depayloader, 276 | clock_rate: ExWebRTCUtils.codec_clock_rate(:h264) 277 | }) 278 | end 279 | 280 | def default_ice_ip_filter(_ip), do: true 281 | end 282 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Software Mansion 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------