├── .gitignore ├── rtp ├── test │ └── test_helper.exs ├── config │ └── config.exs ├── samples │ ├── audio.opus │ └── video.h264 ├── .formatter.exs ├── receive.exs ├── send.exs ├── mix.exs ├── lib │ └── membrane_demo_rtp │ │ ├── receive_pipeline.ex │ │ └── send_pipeline.ex ├── README.md └── .gitignore ├── rtmp_to_hls ├── test │ └── test_helper.exs ├── lib │ ├── rtmp_to_hls_web │ │ ├── views │ │ │ ├── page_view.ex │ │ │ ├── layout_view.ex │ │ │ ├── error_view.ex │ │ │ └── error_helpers.ex │ │ ├── controllers │ │ │ ├── page_controller.ex │ │ │ └── hls_controller.ex │ │ ├── templates │ │ │ ├── layout │ │ │ │ ├── app.html.heex │ │ │ │ ├── live.html.heex │ │ │ │ └── root.html.heex │ │ │ └── page │ │ │ │ └── index.html.heex │ │ ├── router.ex │ │ ├── gettext.ex │ │ ├── endpoint.ex │ │ └── telemetry.ex │ ├── rtmp_to_hls.ex │ ├── rtmp_to_hls │ │ ├── application.ex │ │ └── pipeline.ex │ └── rtmp_to_hls_web.ex ├── .formatter.exs ├── priv │ ├── static │ │ ├── favicon.ico │ │ ├── images │ │ │ ├── membrane.png │ │ │ └── phoenix.png │ │ └── robots.txt │ └── gettext │ │ ├── en │ │ └── LC_MESSAGES │ │ │ └── errors.po │ │ └── errors.pot ├── doc_assets │ ├── OBS_settings.webp │ └── RTMP_to_HLS_pipeline.png ├── config │ ├── test.exs │ ├── config.exs │ ├── runtime.exs │ ├── prod.exs │ └── dev.exs ├── .gitignore ├── assets │ ├── js │ │ └── app.js │ └── css │ │ ├── app.css │ │ └── phoenix.css ├── mix.exs └── README.md ├── simple_element ├── test │ └── test_helper.exs ├── config │ └── config.exs ├── sample.mp3 ├── run.exs ├── .formatter.exs ├── mix.exs ├── lib │ └── membrane_demo │ │ └── simple_element │ │ ├── pipeline.ex │ │ └── counter.ex ├── README.md └── .gitignore ├── simple_pipeline ├── test │ └── test_helper.exs ├── config │ └── config.exs ├── sample.mp3 ├── run.exs ├── .formatter.exs ├── mix.exs ├── lib │ └── membrane_demo │ │ └── simple_pipeline.ex ├── README.md └── .gitignore ├── webrtc_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 │ │ ├── router.ex │ │ ├── live │ │ │ └── home.ex │ │ ├── endpoint.ex │ │ └── telemetry.ex │ ├── webrtc_live_view │ │ ├── pipeline.ex │ │ ├── application.ex │ │ └── contours_drawer.ex │ └── webrtc_live_view_web.ex ├── config │ ├── prod.exs │ ├── test.exs │ ├── config.exs │ └── dev.exs ├── .gitignore ├── README.md ├── assets │ └── js │ │ └── app.js └── mix.exs ├── rtmp_to_adaptive_hls ├── test │ └── test_helper.exs ├── .formatter.exs ├── priv │ ├── static │ │ └── favicon.ico │ └── gettext │ │ ├── en │ │ └── LC_MESSAGES │ │ │ └── errors.po │ │ └── errors.pot ├── lib │ ├── rtmp_to_adaptive_hls_web │ │ ├── views │ │ │ ├── page_view.ex │ │ │ ├── layout_view.ex │ │ │ ├── error_view.ex │ │ │ └── error_helpers.ex │ │ ├── controllers │ │ │ ├── page_controller.ex │ │ │ └── hls_controller.ex │ │ ├── templates │ │ │ ├── layout │ │ │ │ ├── app.html.heex │ │ │ │ ├── live.html.heex │ │ │ │ └── root.html.heex │ │ │ └── page │ │ │ │ └── index.html.heex │ │ ├── router.ex │ │ ├── gettext.ex │ │ ├── endpoint.ex │ │ └── telemetry.ex │ ├── rtmp_to_adaptive_hls.ex │ ├── rtmp_to_adaptive_hls │ │ ├── client_handler.ex │ │ └── application.ex │ └── rtmp_to_adaptive_hls_web.ex ├── doc_assets │ ├── OBS_settings.webp │ └── RTMP_to_HLS_pipeline.png ├── config │ ├── test.exs │ ├── config.exs │ ├── runtime.exs │ ├── prod.exs │ └── dev.exs ├── .gitignore ├── assets │ ├── js │ │ └── app.js │ └── css │ │ ├── app.css │ │ └── phoenix.css └── mix.exs ├── mix_audio ├── .formatter.exs ├── sound_1000f.wav ├── sound_500f.wav ├── .gitignore ├── README.md └── mix_audio.exs ├── rtp_to_hls ├── .formatter.exs ├── stream.html ├── .gitignore ├── README.md ├── send.exs └── rtp_to_hls.exs ├── rtsp_to_hls ├── config │ ├── prod.exs │ ├── test.exs │ ├── dev.exs │ └── config.exs ├── assets │ └── video.mp4 ├── server.exs ├── .formatter.exs ├── stream.html ├── .gitignore ├── rtsp_to_hls.exs ├── mix.exs ├── lib │ └── pipeline.ex └── README.md ├── camera_to_hls ├── .formatter.exs ├── stream.html ├── .gitignore ├── README.md └── camera_to_hls.exs ├── camera_to_hls_nerves ├── rootfs_overlay │ ├── etc │ │ └── iex.exs │ └── stream.html ├── .formatter.exs ├── lib │ └── camera_to_hls_nerves │ │ ├── application.ex │ │ └── pipeline.ex ├── .gitignore ├── config │ ├── config.exs │ └── host.exs ├── rel │ └── vm.args.eex └── mix.exs ├── livebooks ├── audio_mixer │ ├── assets │ │ ├── beep.aac │ │ └── sample.mp3 │ ├── README.md │ └── audio_mixer.livemd ├── playing_mp3_file │ ├── assets │ │ └── sample.mp3 │ ├── README.md │ └── playing_mp3_file.livemd ├── README.md ├── messages_source_and_sink │ └── README.md ├── openai_realtime_with_membrane_webrtc │ ├── assets │ │ └── index.html │ └── README.md ├── soundwave │ └── README.md ├── rtmp │ └── README.md └── speech_to_text │ └── README.md ├── .github ├── ISSUE_TEMPLATE │ └── please--open-new-issues-in-membranefranework-membrane_core.md └── workflows │ ├── on_issue_opened.yaml │ ├── on_pr_opened.yaml │ ├── webrtc_videoroom_master_build.yml │ └── webrtc_videoroom_tag_build.yml └── .circleci └── config.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /rtp/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /rtmp_to_hls/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /simple_element/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /simple_pipeline/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /webrtc_live_view/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /rtp/config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :logger, level: :info 4 | -------------------------------------------------------------------------------- /mix_audio/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["*.exs"] 4 | ] 5 | -------------------------------------------------------------------------------- /rtp_to_hls/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["*.exs"] 4 | ] 5 | -------------------------------------------------------------------------------- /rtsp_to_hls/config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :logger, level: :debug 4 | -------------------------------------------------------------------------------- /rtsp_to_hls/config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :logger, level: :debug 4 | -------------------------------------------------------------------------------- /simple_element/config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :logger, level: :info 4 | -------------------------------------------------------------------------------- /camera_to_hls/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["*.exs"] 4 | ] 5 | -------------------------------------------------------------------------------- /simple_pipeline/config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :logger, level: :info 4 | -------------------------------------------------------------------------------- /rtp/samples/audio.opus: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_demo/HEAD/rtp/samples/audio.opus -------------------------------------------------------------------------------- /rtp/samples/video.h264: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_demo/HEAD/rtp/samples/video.h264 -------------------------------------------------------------------------------- /rtsp_to_hls/config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :logger, :console, format: "[$level] $message\n" 4 | -------------------------------------------------------------------------------- /mix_audio/sound_1000f.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_demo/HEAD/mix_audio/sound_1000f.wav -------------------------------------------------------------------------------- /mix_audio/sound_500f.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_demo/HEAD/mix_audio/sound_500f.wav -------------------------------------------------------------------------------- /simple_element/sample.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_demo/HEAD/simple_element/sample.mp3 -------------------------------------------------------------------------------- /rtsp_to_hls/assets/video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_demo/HEAD/rtsp_to_hls/assets/video.mp4 -------------------------------------------------------------------------------- /simple_pipeline/sample.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_demo/HEAD/simple_pipeline/sample.mp3 -------------------------------------------------------------------------------- /rtmp_to_hls/lib/rtmp_to_hls_web/views/page_view.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpToHlsWeb.PageView do 2 | use RtmpToHlsWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /rtmp_to_hls/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:phoenix], 3 | inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /rtsp_to_hls/server.exs: -------------------------------------------------------------------------------- 1 | Membrane.SimpleRTSPServer.start_link("assets/video.mp4", port: 30001) 2 | 3 | Process.sleep(:infinity) 4 | -------------------------------------------------------------------------------- /simple_pipeline/run.exs: -------------------------------------------------------------------------------- 1 | {:ok, _supervisor, _pid} = 2 | Membrane.Pipeline.start_link(Membrane.Demo.SimplePipeline, "sample.mp3") 3 | -------------------------------------------------------------------------------- /camera_to_hls_nerves/rootfs_overlay/etc/iex.exs: -------------------------------------------------------------------------------- 1 | NervesMOTD.print() 2 | 3 | # Add Toolshed helpers to the IEx session 4 | use Toolshed 5 | -------------------------------------------------------------------------------- /rtmp_to_hls/priv/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_demo/HEAD/rtmp_to_hls/priv/static/favicon.ico -------------------------------------------------------------------------------- /livebooks/audio_mixer/assets/beep.aac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_demo/HEAD/livebooks/audio_mixer/assets/beep.aac -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:phoenix], 3 | inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /livebooks/audio_mixer/assets/sample.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_demo/HEAD/livebooks/audio_mixer/assets/sample.mp3 -------------------------------------------------------------------------------- /rtmp_to_hls/doc_assets/OBS_settings.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_demo/HEAD/rtmp_to_hls/doc_assets/OBS_settings.webp -------------------------------------------------------------------------------- /rtsp_to_hls/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: [ 4 | "{config,lib,test}/**/*.{ex,exs}", 5 | "*.exs" 6 | ] 7 | ] 8 | -------------------------------------------------------------------------------- /webrtc_live_view/priv/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_demo/HEAD/webrtc_live_view/priv/static/favicon.ico -------------------------------------------------------------------------------- /rtmp_to_hls/priv/static/images/membrane.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_demo/HEAD/rtmp_to_hls/priv/static/images/membrane.png -------------------------------------------------------------------------------- /rtmp_to_hls/priv/static/images/phoenix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_demo/HEAD/rtmp_to_hls/priv/static/images/phoenix.png -------------------------------------------------------------------------------- /livebooks/playing_mp3_file/assets/sample.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_demo/HEAD/livebooks/playing_mp3_file/assets/sample.mp3 -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/priv/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_demo/HEAD/rtmp_to_adaptive_hls/priv/static/favicon.ico -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/lib/rtmp_to_adaptive_hls_web/views/page_view.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpToAdaptiveHlsWeb.PageView do 2 | use RtmpToAdaptiveHlsWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /rtmp_to_hls/doc_assets/RTMP_to_HLS_pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_demo/HEAD/rtmp_to_hls/doc_assets/RTMP_to_HLS_pipeline.png -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/doc_assets/OBS_settings.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_demo/HEAD/rtmp_to_adaptive_hls/doc_assets/OBS_settings.webp -------------------------------------------------------------------------------- /rtp/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | import_deps: [:membrane_core] 5 | ] 6 | -------------------------------------------------------------------------------- /simple_element/run.exs: -------------------------------------------------------------------------------- 1 | alias Membrane.Demo.SimpleElement.Pipeline 2 | 3 | {:ok, _supervisor, _pid} = 4 | Membrane.Pipeline.start_link(Pipeline, "sample.mp3") 5 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/doc_assets/RTMP_to_HLS_pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_demo/HEAD/rtmp_to_adaptive_hls/doc_assets/RTMP_to_HLS_pipeline.png -------------------------------------------------------------------------------- /simple_element/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: [ 3 | "{lib,test,config}/**/*.{ex,exs}", 4 | "*.exs", 5 | ".formatter.exs" 6 | ], 7 | import_deps: [:membrane_core] 8 | ] 9 | -------------------------------------------------------------------------------- /simple_pipeline/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: [ 3 | "{lib,test,config}/**/*.{ex,exs}", 4 | "*.exs", 5 | ".formatter.exs" 6 | ], 7 | import_deps: [:membrane_core] 8 | ] 9 | -------------------------------------------------------------------------------- /rtsp_to_hls/config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :logger, :console, 4 | format: "$date $time $metadata[$level] $message\n", 5 | metadata: [:request_id] 6 | 7 | import_config "#{Mix.env()}.exs" 8 | -------------------------------------------------------------------------------- /webrtc_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 | -------------------------------------------------------------------------------- /rtmp_to_hls/lib/rtmp_to_hls_web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpToHlsWeb.PageController do 2 | use RtmpToHlsWeb, :controller 3 | 4 | def index(conn, _params) do 5 | render(conn, "index.html") 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /camera_to_hls_nerves/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: [ 4 | "{mix,.formatter}.exs", 5 | "{config,lib,test}/**/*.{ex,exs}", 6 | "rootfs_overlay/etc/iex.exs" 7 | ], 8 | import_deps: [:membrane_core] 9 | ] 10 | -------------------------------------------------------------------------------- /rtmp_to_hls/priv/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://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 | -------------------------------------------------------------------------------- /webrtc_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 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/lib/rtmp_to_adaptive_hls_web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpToAdaptiveHlsWeb.PageController do 2 | use RtmpToAdaptiveHlsWeb, :controller 3 | 4 | def index(conn, _params) do 5 | render(conn, "index.html") 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /rtmp_to_hls/lib/rtmp_to_hls_web/templates/layout/app.html.heex: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | <%= @inner_content %> 5 |
6 | -------------------------------------------------------------------------------- /livebooks/README.md: -------------------------------------------------------------------------------- 1 | # Livebook examples 2 | 3 | This folder contains interactive Livebook examples. To launch them you need to install [Livebook](https://livebook.dev) first. For Linux, we recommend [installing it via EScript](https://github.com/livebook-dev/livebook?tab=readme-ov-file#escript). 4 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/lib/rtmp_to_adaptive_hls_web/templates/layout/app.html.heex: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | <%= @inner_content %> 5 |
6 | -------------------------------------------------------------------------------- /rtmp_to_hls/lib/rtmp_to_hls.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpToHls do 2 | @moduledoc """ 3 | RtmpToHls 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 | -------------------------------------------------------------------------------- /rtp/receive.exs: -------------------------------------------------------------------------------- 1 | alias Membrane.Demo.RTP.ReceivePipeline 2 | 3 | {:ok, _supervisor, _pid} = 4 | Membrane.Pipeline.start_link(ReceivePipeline, %{ 5 | video_port: 5000, 6 | audio_port: 5002, 7 | secure?: "--secure" in System.argv(), 8 | srtp_key: String.duplicate("a", 30) 9 | }) 10 | 11 | Process.sleep(:infinity) 12 | -------------------------------------------------------------------------------- /webrtc_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 | -------------------------------------------------------------------------------- /webrtc_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 | -------------------------------------------------------------------------------- /rtmp_to_hls/lib/rtmp_to_hls_web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpToHlsWeb.LayoutView do 2 | use RtmpToHlsWeb, :view 3 | 4 | # Phoenix LiveDashboard is available only in development by default, 5 | # so we instruct Elixir to not warn if the dashboard route is missing. 6 | @compile {:no_warn_undefined, {Routes, :live_dashboard_path, 2}} 7 | end 8 | -------------------------------------------------------------------------------- /webrtc_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 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/lib/rtmp_to_adaptive_hls.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpToAdaptiveHls do 2 | @moduledoc """ 3 | RtmpToAdaptiveHls 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 | -------------------------------------------------------------------------------- /webrtc_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 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/lib/rtmp_to_adaptive_hls_web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpToAdaptiveHlsWeb.LayoutView do 2 | use RtmpToAdaptiveHlsWeb, :view 3 | 4 | # Phoenix LiveDashboard is available only in development by default, 5 | # so we instruct Elixir to not warn if the dashboard route is missing. 6 | @compile {:no_warn_undefined, {Routes, :live_dashboard_path, 2}} 7 | end 8 | -------------------------------------------------------------------------------- /rtp/send.exs: -------------------------------------------------------------------------------- 1 | alias Membrane.Demo.RTP.SendPipeline 2 | 3 | {:ok, _supervisor, _pid} = 4 | Membrane.Pipeline.start_link(SendPipeline, %{ 5 | video_port: 5000, 6 | video_ssrc: 1234, 7 | audio_port: 5002, 8 | audio_ssrc: 1236, 9 | secure?: "--secure" in System.argv(), 10 | srtp_key: String.duplicate("a", 30) 11 | }) 12 | 13 | Process.sleep(:infinity) 14 | -------------------------------------------------------------------------------- /rtmp_to_hls/priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: en\n" 12 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: en\n" 12 | -------------------------------------------------------------------------------- /rtmp_to_hls/priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here has no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | 11 | -------------------------------------------------------------------------------- /rtmp_to_hls/lib/rtmp_to_hls_web/templates/layout/live.html.heex: -------------------------------------------------------------------------------- 1 |
2 | 5 | 6 | 9 | 10 | <%= @inner_content %> 11 |
12 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here has no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | 11 | -------------------------------------------------------------------------------- /rtmp_to_hls/lib/rtmp_to_hls_web/controllers/hls_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpToHlsWeb.HlsController do 2 | use RtmpToHlsWeb, :controller 3 | 4 | alias Plug 5 | 6 | def index(conn, %{"filename" => filename}) do 7 | path = "output/#{filename}" 8 | 9 | if File.exists?(path) do 10 | conn |> Plug.Conn.send_file(200, path) 11 | else 12 | conn |> Plug.Conn.send_resp(404, "File not found") 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/lib/rtmp_to_adaptive_hls_web/templates/layout/live.html.heex: -------------------------------------------------------------------------------- 1 |
2 | 5 | 6 | 9 | 10 | <%= @inner_content %> 11 |
12 | -------------------------------------------------------------------------------- /camera_to_hls_nerves/lib/camera_to_hls_nerves/application.ex: -------------------------------------------------------------------------------- 1 | defmodule CameraToHlsNerves.Application do 2 | use Application 3 | 4 | def start(_type, _args) do 5 | File.rm_rf!("/data/output") 6 | File.mkdir!("/data/output") 7 | File.cd!("/") 8 | 9 | children = [ 10 | CameraToHlsNerves.Pipeline 11 | ] 12 | 13 | Supervisor.start_link(children, strategy: :one_for_one, name: CameraToHlsNervesSupervisor) 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/lib/rtmp_to_adaptive_hls_web/controllers/hls_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpToAdaptiveHlsWeb.HlsController do 2 | use RtmpToAdaptiveHlsWeb, :controller 3 | 4 | alias Plug 5 | 6 | def index(conn, %{"filename" => filename}) do 7 | path = "output/#{filename}" 8 | 9 | if File.exists?(path) do 10 | conn |> Plug.Conn.send_file(200, path) 11 | else 12 | conn |> Plug.Conn.send_resp(404, "File not found") 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /webrtc_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 | -------------------------------------------------------------------------------- /camera_to_hls_nerves/.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 third-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 | -------------------------------------------------------------------------------- /rtmp_to_hls/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 :rtmp_to_hls, RtmpToHlsWeb.Endpoint, 6 | http: [ip: {127, 0, 0, 1}, port: 4002], 7 | secret_key_base: "7QKtyBTigBZGpLDW0nc1U44mpE1U9kQzDd60hynU4bcnAUfLXPJLQxRlde8fPDhb", 8 | server: false 9 | 10 | # Print only warnings and errors during test 11 | config :logger, level: :warn 12 | 13 | # Initialize plugs at runtime for faster test compilation 14 | config :phoenix, :plug_init_mode, :runtime 15 | -------------------------------------------------------------------------------- /webrtc_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 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/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 :rtmp_to_adaptive_hls, RtmpToAdaptiveHlsWeb.Endpoint, 6 | http: [ip: {127, 0, 0, 1}, port: 4002], 7 | secret_key_base: "7QKtyBTigBZGpLDW0nc1U44mpE1U9kQzDd60hynU4bcnAUfLXPJLQxRlde8fPDhb", 8 | server: false 9 | 10 | # Print only warnings and errors during test 11 | config :logger, level: :warn 12 | 13 | # Initialize plugs at runtime for faster test compilation 14 | config :phoenix, :plug_init_mode, :runtime 15 | -------------------------------------------------------------------------------- /webrtc_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 | -------------------------------------------------------------------------------- /rtmp_to_hls/lib/rtmp_to_hls_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpToHlsWeb.ErrorView do 2 | use RtmpToHlsWeb, :view 3 | 4 | # If you want to customize a particular status code 5 | # for a certain format, you may uncomment below. 6 | # def render("500.html", _assigns) do 7 | # "Internal Server Error" 8 | # end 9 | 10 | # By default, Phoenix returns the status message from 11 | # the template name. For example, "404.html" becomes 12 | # "Not Found". 13 | def template_not_found(template, _assigns) do 14 | Phoenix.Controller.status_message_from_template(template) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /rtmp_to_hls/lib/rtmp_to_hls_web/templates/page/index.html.heex: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/lib/rtmp_to_adaptive_hls_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpToAdaptiveHlsWeb.ErrorView do 2 | use RtmpToAdaptiveHlsWeb, :view 3 | 4 | # If you want to customize a particular status code 5 | # for a certain format, you may uncomment below. 6 | # def render("500.html", _assigns) do 7 | # "Internal Server Error" 8 | # end 9 | 10 | # By default, Phoenix returns the status message from 11 | # the template name. For example, "404.html" becomes 12 | # "Not Found". 13 | def template_not_found(template, _assigns) do 14 | Phoenix.Controller.status_message_from_template(template) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/lib/rtmp_to_adaptive_hls_web/templates/page/index.html.heex: -------------------------------------------------------------------------------- 1 | 19 | 20 |
21 |
23 | -------------------------------------------------------------------------------- /webrtc_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 | -------------------------------------------------------------------------------- /livebooks/audio_mixer/README.md: -------------------------------------------------------------------------------- 1 | # Membrane audio mixer 2 | 3 | This livebook shows how to mix a beep sound into background music over a period of time. 4 | 5 | To run the demo, [install Livebook](https://github.com/livebook-dev/livebook#escript) and open the `audio_mixer.livemd` file there. 6 | 7 | ## Copyright and License 8 | 9 | Copyright 2024, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 10 | 11 | [![Software Mansion](https://docs.membrane.stream/static/logo/swm_logo_readme.png)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 12 | 13 | Licensed under the [Apache License, Version 2.0](LICENSE) 14 | -------------------------------------------------------------------------------- /camera_to_hls/stream.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | The video should show up below. If it doesn't, make sure that the index.m3u8 file is generated in the output 8 | directory and refresh the page. 9 |
10 | 11 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /livebooks/messages_source_and_sink/README.md: -------------------------------------------------------------------------------- 1 | # Membrane messages source and sink 2 | 3 | This livebook shows how to setup a simple pipeline and send messages through it. 4 | 5 | To run the demo, [install Livebook](https://github.com/livebook-dev/livebook#escript) and open the `messages_source_and_sink.livemd` file there. 6 | 7 | ## Copyright and License 8 | 9 | Copyright 2024, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 10 | 11 | [![Software Mansion](https://docs.membrane.stream/static/logo/swm_logo_readme.png)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 12 | 13 | Licensed under the [Apache License, Version 2.0](LICENSE) 14 | -------------------------------------------------------------------------------- /livebooks/playing_mp3_file/README.md: -------------------------------------------------------------------------------- 1 | # Membrane playing mp3 file demo 2 | 3 | This livebook shows how to load `MP3` audio from the file, transcode it to the `AAC` codec, and play it. 4 | 5 | To run the demo, [install Livebook](https://github.com/livebook-dev/livebook#escript) and open the `playing_mp3_file.livemd` file there. 6 | 7 | ## Copyright and License 8 | 9 | Copyright 2024, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 10 | 11 | [![Software Mansion](https://docs.membrane.stream/static/logo/swm_logo_readme.png)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 12 | 13 | Licensed under the [Apache License, Version 2.0](LICENSE) 14 | -------------------------------------------------------------------------------- /rtp_to_hls/stream.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | The video should show up below. If it doesn't, make sure that the index.m3u8 file is generated in the output 8 | directory and refresh the page. 9 |
10 | 11 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /webrtc_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 | -------------------------------------------------------------------------------- /rtsp_to_hls/stream.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | The video should show up below. If it doesn't, make sure that the index.m3u8 file is generated in the output 8 | directory and refresh the page. 9 |
10 | 11 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /camera_to_hls_nerves/rootfs_overlay/stream.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | The video should show up below. If it doesn't, make sure that the index.m3u8 file is generated in the /data/output 8 | directory and refresh the page. 9 |
10 | 11 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /webrtc_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 | -------------------------------------------------------------------------------- /rtmp_to_hls/lib/rtmp_to_hls_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpToHlsWeb.Router do 2 | use RtmpToHlsWeb, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug :fetch_live_flash 8 | plug :put_root_layout, {RtmpToHlsWeb.LayoutView, :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 "/", RtmpToHlsWeb do 18 | pipe_through :browser 19 | 20 | get "/", PageController, :index 21 | get "/video/:filename", HlsController, :index 22 | end 23 | 24 | # Other scopes may use custom stacks. 25 | # scope "/api", RtmpToHlsWeb do 26 | # pipe_through :api 27 | # end 28 | end 29 | -------------------------------------------------------------------------------- /livebooks/openai_realtime_with_membrane_webrtc/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | OpenAI Realtime with Membrane WebRTC demo 9 | 10 | 11 | 13 |
14 |

OpenAI Realtime with Membrane WebRTC demo

15 |
Connecting
16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /webrtc_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 | -------------------------------------------------------------------------------- /rtmp_to_hls/lib/rtmp_to_hls_web/templates/layout/root.html.heex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= csrf_meta_tag() %> 8 | <%= live_title_tag assigns[:page_title] || "RtmpToHls", suffix: " · Membrane Framework" %> 9 | 10 | 11 | 12 | 13 |

RTMP to HLS demo

14 | <%= @inner_content %> 15 | 16 | 17 | -------------------------------------------------------------------------------- /livebooks/soundwave/README.md: -------------------------------------------------------------------------------- 1 | # Membrane Soundwave demo 2 | 3 | This livebook example shows how to perform real-time soundwave plotting with the use of the [Membrane Framework](https://github.com/membraneframework) and [Vega-Lite](https://vega.github.io/vega-lite/). 4 | 5 | To run the demo, [install Livebook](https://github.com/livebook-dev/livebook#escript) and open the `soundwave.livemd` file there. 6 | 7 | ## Copyright and License 8 | 9 | Copyright 2024, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 10 | 11 | [![Software Mansion](https://docs.membrane.stream/static/logo/swm_logo_readme.png)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 12 | 13 | Licensed under the [Apache License, Version 2.0](LICENSE) 14 | -------------------------------------------------------------------------------- /mix_audio/.gitignore: -------------------------------------------------------------------------------- 1 | # HLS output 2 | /output.aac 3 | 4 | .elixir_ls 5 | 6 | # The directory Mix will write compiled artifacts to. 7 | /_build/ 8 | 9 | # If you run "mix test --cover", coverage assets end up here. 10 | /cover/ 11 | 12 | # The directory Mix downloads your dependencies sources to. 13 | /deps/ 14 | 15 | # Where third-party dependencies like ExDoc output generated docs. 16 | /doc/ 17 | 18 | # Ignore .fetch files in case you like to edit your project deps locally. 19 | /.fetch 20 | 21 | # If the VM crashes, it generates a dump, let's ignore it too. 22 | erl_crash.dump 23 | 24 | # Also ignore archive artifacts (built via "mix archive.build"). 25 | *.ez 26 | 27 | # Ignore package tarball (built via "mix hex.build"). 28 | membrane_demo_rtp_to_hls-*.tar 29 | 30 | # macOS 31 | .DS_Store 32 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/lib/rtmp_to_adaptive_hls_web/templates/layout/root.html.heex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= csrf_meta_tag() %> 8 | <%= live_title_tag assigns[:page_title] || "RtmpToHls", suffix: " · Membrane Framework" %> 9 | 10 | 11 | 12 | 13 |

RTMP to HLS demo

14 | <%= @inner_content %> 15 | 16 | 17 | -------------------------------------------------------------------------------- /rtp_to_hls/.gitignore: -------------------------------------------------------------------------------- 1 | # HLS output 2 | /output/* 3 | 4 | .elixir_ls 5 | 6 | # The directory Mix will write compiled artifacts to. 7 | /_build/ 8 | 9 | # If you run "mix test --cover", coverage assets end up here. 10 | /cover/ 11 | 12 | # The directory Mix downloads your dependencies sources to. 13 | /deps/ 14 | 15 | # Where third-party dependencies like ExDoc output generated docs. 16 | /doc/ 17 | 18 | # Ignore .fetch files in case you like to edit your project deps locally. 19 | /.fetch 20 | 21 | # If the VM crashes, it generates a dump, let's ignore it too. 22 | erl_crash.dump 23 | 24 | # Also ignore archive artifacts (built via "mix archive.build"). 25 | *.ez 26 | 27 | # Ignore package tarball (built via "mix hex.build"). 28 | membrane_demo_rtp_to_hls-*.tar 29 | 30 | # macOS 31 | .DS_Store 32 | -------------------------------------------------------------------------------- /camera_to_hls/.gitignore: -------------------------------------------------------------------------------- 1 | # HLS output 2 | /output/* 3 | 4 | .elixir_ls 5 | 6 | # The directory Mix will write compiled artifacts to. 7 | /_build/ 8 | 9 | # If you run "mix test --cover", coverage assets end up here. 10 | /cover/ 11 | 12 | # The directory Mix downloads your dependencies sources to. 13 | /deps/ 14 | 15 | # Where third-party dependencies like ExDoc output generated docs. 16 | /doc/ 17 | 18 | # Ignore .fetch files in case you like to edit your project deps locally. 19 | /.fetch 20 | 21 | # If the VM crashes, it generates a dump, let's ignore it too. 22 | erl_crash.dump 23 | 24 | # Also ignore archive artifacts (built via "mix archive.build"). 25 | *.ez 26 | 27 | # Ignore package tarball (built via "mix hex.build"). 28 | membrane_demo_rtp_to_hls-*.tar 29 | 30 | # macOS 31 | .DS_Store 32 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/lib/rtmp_to_adaptive_hls_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpToAdaptiveHlsWeb.Router do 2 | use RtmpToAdaptiveHlsWeb, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug :fetch_live_flash 8 | plug :put_root_layout, {RtmpToAdaptiveHlsWeb.LayoutView, :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 "/", RtmpToAdaptiveHlsWeb do 18 | pipe_through :browser 19 | 20 | get "/", PageController, :index 21 | get "/video/:filename", HlsController, :index 22 | end 23 | 24 | # Other scopes may use custom stacks. 25 | # scope "/api", RtmpToAdaptiveHlsWeb do 26 | # pipe_through :api 27 | # end 28 | end 29 | -------------------------------------------------------------------------------- /livebooks/openai_realtime_with_membrane_webrtc/README.md: -------------------------------------------------------------------------------- 1 | # OpenAI Realtime API Integration with Membrane WebRTC 2 | 3 | This is a [Livebook](https://livebook.dev) example, that allows to conversate with AI via browser using [OpenAI Realtime API](https://openai.com/index/introducing-the-realtime-api/). 4 | 5 | To run the demo, [install Livebook](https://github.com/livebook-dev/livebook#escript) and open the `openai_realtime_with_membrane_webrtc.livemd` file there. 6 | 7 | ## Copyright and License 8 | 9 | Copyright 2024, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 10 | 11 | [![Software Mansion](https://docs.membrane.stream/static/logo/swm_logo_readme.png)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 12 | 13 | Licensed under the [Apache License, Version 2.0](LICENSE) 14 | -------------------------------------------------------------------------------- /rtmp_to_hls/lib/rtmp_to_hls_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpToHlsWeb.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import RtmpToHlsWeb.Gettext 9 | 10 | # Simple translation 11 | gettext("Here is the string to translate") 12 | 13 | # Plural translation 14 | ngettext("Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3) 17 | 18 | # Domain-based translation 19 | dgettext("errors", "Here is the error message to translate") 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :rtmp_to_hls 24 | end 25 | -------------------------------------------------------------------------------- /livebooks/rtmp/README.md: -------------------------------------------------------------------------------- 1 | # Membrane RMTP demo 2 | 3 | Sender livebook shows how to download video and audio from the web using the `Hackney` plugin, and stream it via `RTMP` to the other, receiver livebook. 4 | 5 | Receiver livebook shows how to receive `RTMP` stream mentioned above and play it in the livebook. 6 | 7 | To run the demo, [install Livebook](https://github.com/livebook-dev/livebook#escript) and open both `rtmp_sender.livemd` and `rtmp_receiver.livemd` files there. 8 | 9 | ## Copyright and License 10 | 11 | Copyright 2024, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 12 | 13 | [![Software Mansion](https://docs.membrane.stream/static/logo/swm_logo_readme.png)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 14 | 15 | Licensed under the [Apache License, Version 2.0](LICENSE) 16 | -------------------------------------------------------------------------------- /rtsp_to_hls/.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 third-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 | # Ignore package tarball (built via "mix hex.build"). 23 | hls_proxy_api-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | 28 | # HLS files 29 | *.m3u8 30 | *.m4s 31 | *.mp4 32 | !assets/video.mp4 33 | 34 | # macOS 35 | .DS_Store 36 | -------------------------------------------------------------------------------- /camera_to_hls_nerves/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application and its 2 | # dependencies. 3 | # 4 | # This configuration file is loaded before any dependency and is restricted to 5 | # this project. 6 | import Config 7 | 8 | # Enable the Nerves integration with Mix 9 | Application.start(:nerves_bootstrap) 10 | 11 | config :camera_to_hls_nerves, target: Mix.target() 12 | 13 | # https://hexdocs.pm/nerves/advanced-configuration.html for details. 14 | 15 | config :nerves, :firmware, rootfs_overlay: "rootfs_overlay" 16 | 17 | # Set the SOURCE_DATE_EPOCH date for reproducible builds. 18 | # See https://reproducible-builds.org/docs/source-date-epoch/ for more information 19 | 20 | config :nerves, source_date_epoch: "1698662793" 21 | 22 | if Mix.target() == :host do 23 | import_config "host.exs" 24 | else 25 | import_config "target.exs" 26 | end 27 | -------------------------------------------------------------------------------- /camera_to_hls_nerves/config/host.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # Add configuration that is only needed when running on the host here. 4 | 5 | config :nerves_runtime, 6 | kv_backend: 7 | {Nerves.Runtime.KVBackend.InMemory, 8 | contents: %{ 9 | # The KV store on Nerves systems is typically read from UBoot-env, but 10 | # this allows us to use a pre-populated InMemory store when running on 11 | # host for development and testing. 12 | # 13 | # https://hexdocs.pm/nerves_runtime/readme.html#using-nerves_runtime-in-tests 14 | # https://hexdocs.pm/nerves_runtime/readme.html#nerves-system-and-firmware-metadata 15 | 16 | "nerves_fw_active" => "a", 17 | "a.nerves_fw_architecture" => "generic", 18 | "a.nerves_fw_description" => "N/A", 19 | "a.nerves_fw_platform" => "host", 20 | "a.nerves_fw_version" => "0.0.0" 21 | }} 22 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/lib/rtmp_to_adaptive_hls_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpToAdaptiveHlsWeb.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import RtmpToAdaptiveHlsWeb.Gettext 9 | 10 | # Simple translation 11 | gettext("Here is the string to translate") 12 | 13 | # Plural translation 14 | ngettext("Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3) 17 | 18 | # Domain-based translation 19 | dgettext("errors", "Here is the error message to translate") 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :rtmp_to_adaptive_hls 24 | end 25 | -------------------------------------------------------------------------------- /webrtc_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 | -------------------------------------------------------------------------------- /mix_audio/README.md: -------------------------------------------------------------------------------- 1 | # Audio mixing 2 | 3 | This script demonstrates mixing two audio tracks 4 | 5 | To run the demo, you need [Elixir installed](https://elixir-lang.org/install.html) on your machine (it's best to use a version manager, like `asdf`). Then, run 6 | 7 | ```bash 8 | elixir mix_audio.exs 9 | ``` 10 | 11 | and it will generate `output.aac`, that you can play with VLC or other player. 12 | 13 | Should there be any errors when compiling the script's dependencies, you may need to install [FDK AAC](https://github.com/mstorsjo/fdk-aac), which we use to encode the output stream. 14 | 15 | 16 | ## Copyright and License 17 | 18 | Copyright 2022, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 19 | 20 | [![Software Mansion](https://docs.membrane.stream/static/logo/swm_logo_readme.png)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 21 | 22 | Licensed under the [Apache License, Version 2.0](LICENSE) 23 | -------------------------------------------------------------------------------- /rtmp_to_hls/.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 | # Ignore package tarball (built via "mix hex.build"). 23 | rtmp_to_hls-*.tar 24 | 25 | # Ignore assets that are produced by build tools. 26 | /priv/static/assets/ 27 | 28 | # Ignore digested assets cache. 29 | /priv/static/cache_manifest.json 30 | 31 | # In case you use Node.js/npm, you want to ignore these. 32 | npm-debug.log 33 | /assets/node_modules/ 34 | 35 | /output 36 | 37 | # macOS 38 | .DS_Store 39 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/.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 | # Ignore package tarball (built via "mix hex.build"). 23 | rtmp_to_adaptive_hls-*.tar 24 | 25 | # Ignore assets that are produced by build tools. 26 | /priv/static/assets/ 27 | 28 | # Ignore digested assets cache. 29 | /priv/static/cache_manifest.json 30 | 31 | # In case you use Node.js/npm, you want to ignore these. 32 | npm-debug.log 33 | /assets/node_modules/ 34 | 35 | /output 36 | 37 | # macOS 38 | .DS_Store 39 | -------------------------------------------------------------------------------- /rtsp_to_hls/rtsp_to_hls.exs: -------------------------------------------------------------------------------- 1 | require Logger 2 | alias Membrane.Demo.RTSPToHLS 3 | 4 | rtsp_stream_url = "rtsp://localhost:30001" 5 | output_path = "hls_output" 6 | rtp_port = 20000 7 | 8 | # Prepare a clean directory where output files will be put 9 | File.rm_rf!(output_path) 10 | File.mkdir_p(output_path) 11 | 12 | pipeline_options = %{ 13 | port: rtp_port, 14 | port_range_size: 5, 15 | output_path: output_path, 16 | stream_url: rtsp_stream_url, 17 | parent_pid: self() 18 | } 19 | 20 | {:ok, _sup, _pid} = Membrane.Pipeline.start_link(RTSPToHLS.Pipeline, pipeline_options) 21 | 22 | # Wait until first chunks of the stream become available 23 | receive do 24 | :track_playable -> :ok 25 | end 26 | 27 | {:ok, _server} = 28 | :inets.start(:httpd, 29 | bind_address: ~c"localhost", 30 | port: 8000, 31 | document_root: ~c".", 32 | server_name: ~c"rtsp_to_hls", 33 | server_root: "/tmp" 34 | ) 35 | 36 | Logger.info("Playback available at http://localhost:8000/stream.html") 37 | 38 | Process.sleep(:infinity) 39 | -------------------------------------------------------------------------------- /webrtc_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 | -------------------------------------------------------------------------------- /livebooks/speech_to_text/README.md: -------------------------------------------------------------------------------- 1 | # Membrane Speech To Text demo 2 | 3 | This is a [Livebook](https://livebook.dev) example, that shows how to perform a real-time speech-to-text conversion with the use of the [Membrane Framework](https://github.com/membraneframework) and the [Bumblebee](https://github.com/elixir-nx/bumblebee). 4 | 5 | To run the demo, [install Livebook](https://livebook.dev/#install) and open the `speech_to_text.livemd` file there. 6 | If you're using Livebook Desktop, you can also [click here](https://livebook.dev/run?url=https%3A%2F%2Fraw.githubusercontent.com%2Fmembraneframework%2Fmembrane_demo%2Fmaster%2Fspeech_to_text%2Fspeech_to_text.livemd) instead. 7 | 8 | ## Copyright and License 9 | 10 | Copyright 2022, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 11 | 12 | [![Software Mansion](https://docs.membrane.stream/static/logo/swm_logo_readme.png)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 13 | 14 | Licensed under the [Apache License, Version 2.0](LICENSE) 15 | -------------------------------------------------------------------------------- /webrtc_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: :h264, 9 | signaling: opts[:ingress_signaling] 10 | }) 11 | |> via_out(:output, options: [kind: :video]) 12 | |> child(%Membrane.Transcoder{output_stream_format: Membrane.RawVideo}) 13 | |> child(%Membrane.FFmpeg.SWScale.Converter{format: :RGB}) 14 | |> child(WebRTCLiveView.CountoursDrawer) 15 | |> child(%Membrane.FFmpeg.SWScale.Converter{format: :I420}) 16 | |> child(%Membrane.Transcoder{ 17 | output_stream_format: %Membrane.H264{alignment: :nalu, stream_structure: :annexb} 18 | }) 19 | |> via_in(:input, options: [kind: :video]) 20 | |> child(:webrtc_sink, %Membrane.WebRTC.Sink{ 21 | video_codec: :h264, 22 | signaling: opts[:egress_signaling] 23 | }) 24 | 25 | {[spec: spec], %{}} 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/lib/rtmp_to_adaptive_hls/client_handler.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Demo.RtmpToAdaptiveHls.ClientHandler do 2 | @moduledoc """ 3 | An implementation of `Membrane.RTMPServer.ClientHandlerBehaviour` compatible with the 4 | `Membrane.RTMP.Source` element, which also send information about RTMP stream metadata to the `pipeline` process 5 | """ 6 | 7 | @behaviour Membrane.RTMPServer.ClientHandler 8 | 9 | @handler Membrane.RTMP.Source.ClientHandlerImpl 10 | 11 | defstruct [] 12 | 13 | @impl true 14 | def handle_init(%{pipeline: pid} = opts) do 15 | state = @handler.handle_init(opts) 16 | Map.put(state, :pipeline, pid) 17 | end 18 | 19 | @impl true 20 | defdelegate handle_info(msg, state), to: @handler 21 | 22 | @impl true 23 | defdelegate handle_data_available(payload, state), to: @handler 24 | 25 | @impl true 26 | defdelegate handle_connection_closed(state), to: @handler 27 | 28 | @impl true 29 | defdelegate handle_delete_stream(state), to: @handler 30 | 31 | @impl true 32 | def handle_metadata(message, state) do 33 | send(state.pipeline, message) 34 | state 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /simple_element/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Demo.SimpleElement.MixProject do 2 | use Mix.Project 3 | 4 | @version "0.5.0" 5 | @github_url "https://github.com/membraneframework/membrane-demo" 6 | 7 | def project do 8 | [ 9 | app: :membrane_demo_simple_element, 10 | version: @version, 11 | elixir: "~> 1.12", 12 | name: "Membrane Demo Simple Element", 13 | description: "Membrane simple element demo", 14 | homepage_url: "https://membraneframework.org", 15 | source_url: @github_url, 16 | start_permanent: Mix.env() == :prod, 17 | deps: deps() 18 | ] 19 | end 20 | 21 | # Run "mix help compile.app" to learn about applications. 22 | def application do 23 | [ 24 | extra_applications: [:logger] 25 | ] 26 | end 27 | 28 | # Run "mix help deps" to learn about dependencies. 29 | defp deps do 30 | [ 31 | {:membrane_core, "~> 1.0"}, 32 | {:membrane_file_plugin, "~> 0.17.0"}, 33 | {:membrane_portaudio_plugin, "~> 0.19.2"}, 34 | {:membrane_ffmpeg_swresample_plugin, "~> 0.20.2"}, 35 | {:membrane_mp3_mad_plugin, "~> 0.18.3"}, 36 | {:dialyxir, "~> 1.2", only: [:dev], runtime: false} 37 | ] 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /simple_pipeline/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Demo.SimplePipeline.MixProject do 2 | use Mix.Project 3 | 4 | @version "0.5.0" 5 | @github_url "https://github.com/membraneframework/membrane-demo" 6 | 7 | def project do 8 | [ 9 | app: :membrane_demo_simple_pipeline, 10 | version: @version, 11 | elixir: "~> 1.12", 12 | name: "Membrane Demo Simple Pipeline", 13 | description: "Membrane simple pipeline demo", 14 | homepage_url: "https://membraneframework.org", 15 | source_url: @github_url, 16 | start_permanent: Mix.env() == :prod, 17 | deps: deps() 18 | ] 19 | end 20 | 21 | # Run "mix help compile.app" to learn about applications. 22 | def application do 23 | [ 24 | extra_applications: [:logger] 25 | ] 26 | end 27 | 28 | # Run "mix help deps" to learn about dependencies. 29 | defp deps do 30 | [ 31 | {:membrane_core, "~> 1.0"}, 32 | {:membrane_file_plugin, "~> 0.17.0"}, 33 | {:membrane_portaudio_plugin, "~> 0.19.2"}, 34 | {:membrane_ffmpeg_swresample_plugin, "~> 0.20.2"}, 35 | {:membrane_mp3_mad_plugin, "~> 0.18.3"}, 36 | {:dialyxir, "~> 1.2", only: [:dev], runtime: false} 37 | ] 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /rtp/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Demo.RTP.MixProject do 2 | use Mix.Project 3 | 4 | @version "0.1.0" 5 | 6 | def project do 7 | [ 8 | app: :membrane_demo_rtp, 9 | version: @version, 10 | elixir: "~> 1.12", 11 | start_permanent: Mix.env() == :prod, 12 | deps: deps() 13 | ] 14 | end 15 | 16 | # Run "mix help compile.app" to learn about applications. 17 | def application do 18 | [ 19 | extra_applications: [:logger] 20 | ] 21 | end 22 | 23 | # Run "mix help deps" to learn about dependencies. 24 | defp deps do 25 | [ 26 | {:membrane_core, "~> 1.0"}, 27 | {:membrane_rtp_plugin, "~> 0.31.0"}, 28 | {:membrane_udp_plugin, "~> 0.13.0"}, 29 | {:membrane_h264_ffmpeg_plugin, "~> 0.31.6"}, 30 | {:membrane_h26x_plugin, "~> 0.10.1"}, 31 | {:membrane_rtp_h264_plugin, "~> 0.20.1"}, 32 | {:membrane_opus_plugin, "~> 0.20.1"}, 33 | {:membrane_rtp_opus_plugin, "~> 0.10.0"}, 34 | {:membrane_sdl_plugin, "~> 0.18.2"}, 35 | {:membrane_portaudio_plugin, "~> 0.19.2"}, 36 | {:membrane_file_plugin, "~> 0.17.0"}, 37 | {:membrane_realtimer_plugin, "~> 0.9.0"}, 38 | {:ex_libsrtp, "~> 0.7.2"} 39 | ] 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /webrtc_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 | -------------------------------------------------------------------------------- /webrtc_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 | -------------------------------------------------------------------------------- /webrtc_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 | -------------------------------------------------------------------------------- /simple_element/lib/membrane_demo/simple_element/pipeline.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Demo.SimpleElement.Pipeline do 2 | @moduledoc """ 3 | Pipeline that contains `Membrane.Demo.SimpleElement.Counter` element. 4 | """ 5 | 6 | use Membrane.Pipeline 7 | 8 | alias Membrane.{File, FFmpeg, MP3.MAD, PortAudio, Time} 9 | 10 | @impl true 11 | def handle_init(_ctx, path_to_mp3) do 12 | spec = 13 | child(:file, %File.Source{location: path_to_mp3}) 14 | |> child(:decoder, MAD.Decoder) 15 | |> child(:converter, %FFmpeg.SWResample.Converter{ 16 | output_stream_format: %Membrane.RawAudio{ 17 | sample_format: :s16le, 18 | sample_rate: 48000, 19 | channels: 2 20 | } 21 | }) 22 | |> via_in(:input, options: [divisor: 10]) 23 | |> child(:counter, %Membrane.Demo.SimpleElement.Counter{interval: Time.seconds(5)}) 24 | |> child(:sink, PortAudio.Sink) 25 | 26 | {[spec: spec], %{}} 27 | end 28 | 29 | @impl true 30 | def handle_child_notification({:counter, counter_value}, _from, _ctx, state) do 31 | IO.inspect(counter_value, label: "Count of buffers processed:") 32 | {[], state} 33 | end 34 | 35 | @impl true 36 | def handle_child_notification(_notification, _from, _ctx, state) do 37 | {[], state} 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | commands: 3 | build_test: 4 | parameters: 5 | demo: 6 | type: string 7 | steps: 8 | - run: cd "<>" && mix deps.get 9 | - run: cd "<>" && mix deps.compile --all 10 | - run: cd "<>" && mix format --check-formatted 11 | - run: cd "<>" && mix test 12 | build_exs: 13 | parameters: 14 | demo: 15 | type: string 16 | steps: 17 | - run: cd "<>" && mix format --check-formatted 18 | - run: cd "<>" && elixir <>.exs 19 | 20 | jobs: 21 | build: 22 | docker: 23 | - image: membraneframeworklabs/docker_membrane:latest 24 | environment: 25 | MIX_ENV: test 26 | CI: true 27 | working_directory: '~/app' 28 | steps: 29 | - checkout 30 | - build_exs: 31 | demo: camera_to_hls 32 | - build_test: 33 | demo: rtmp_to_hls 34 | - build_test: 35 | demo: rtp 36 | - build_exs: 37 | demo: rtp_to_hls 38 | - build_test: 39 | demo: rtsp_to_hls 40 | - build_test: 41 | demo: simple_element 42 | - build_test: 43 | demo: simple_pipeline 44 | - build_exs: 45 | demo: mix_audio 46 | -------------------------------------------------------------------------------- /rtmp_to_hls/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 | # Configures the endpoint 11 | config :rtmp_to_hls, RtmpToHlsWeb.Endpoint, 12 | url: [host: "localhost"], 13 | render_errors: [view: RtmpToHlsWeb.ErrorView, accepts: ~w(html json), layout: false], 14 | pubsub_server: RtmpToHls.PubSub, 15 | live_view: [signing_salt: "wVzAotlb"] 16 | 17 | # Configure esbuild (the version is required) 18 | config :esbuild, 19 | version: "0.12.18", 20 | default: [ 21 | args: ~w(js/app.js --bundle --target=es2016 --outdir=../priv/static/assets), 22 | cd: Path.expand("../assets", __DIR__), 23 | env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} 24 | ] 25 | 26 | # Configures Elixir's Logger 27 | config :logger, :console, 28 | format: "$time $metadata[$level] $message\n", 29 | metadata: [:request_id] 30 | 31 | # Use Jason for JSON parsing in Phoenix 32 | config :phoenix, :json_library, Jason 33 | 34 | # Import environment specific config. This must remain at the bottom 35 | # of this file so it overrides the configuration defined above. 36 | import_config "#{config_env()}.exs" 37 | -------------------------------------------------------------------------------- /webrtc_live_view/README.md: -------------------------------------------------------------------------------- 1 | # WebRTC LiveView 2 | 3 | This project demonstrates how to use Membrane WebRTC with dedicated Phoenix LiveViews. 4 | 5 | This example uses [WebRTC plugin](https://github.com/membraneframework/membrane_webrtc_plugin) that is responsible for receiving and sending mutlimedia via WebRTC. 6 | 7 | Membrane modules defined in this project are placed in `lib/webrtc_live_view/pipeline.ex` and `lib/webrtc_live_view/contours_drawer.ex`. 8 | The Phoenix LiveViews dedicated to Membrane WebRTC are used in the file lib/webrtc_live_view_web/live/home.ex. 9 | 10 | ## Running the demo 11 | 12 | To run the demo, you'll need to have [Elixir installed](https://elixir-lang.org/install.html). Then, do the following: 13 | 14 | * Run `mix setup` to install and setup dependencies 15 | * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server` 16 | 17 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 18 | 19 | ## Copyright and License 20 | 21 | Copyright 2025, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 22 | 23 | [![Software Mansion](https://membraneframework.github.io/static/logo/swm_logo_readme.png)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 24 | 25 | Licensed under the [Apache License, Version 2.0](LICENSE) 26 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/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 | # Configures the endpoint 11 | config :rtmp_to_adaptive_hls, RtmpToAdaptiveHlsWeb.Endpoint, 12 | url: [host: "localhost"], 13 | render_errors: [view: RtmpToAdaptiveHlsWeb.ErrorView, accepts: ~w(html json), layout: false], 14 | pubsub_server: RtmpToAdaptiveHls.PubSub, 15 | live_view: [signing_salt: "wVzAotlb"] 16 | 17 | # Configure esbuild (the version is required) 18 | config :esbuild, 19 | version: "0.12.18", 20 | default: [ 21 | args: ~w(js/app.js --bundle --target=es2016 --outdir=../priv/static/assets), 22 | cd: Path.expand("../assets", __DIR__), 23 | env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} 24 | ] 25 | 26 | # Configures Elixir's Logger 27 | config :logger, :console, 28 | format: "$time $metadata[$level] $message\n", 29 | metadata: [:request_id] 30 | 31 | # Use Jason for JSON parsing in Phoenix 32 | config :phoenix, :json_library, Jason 33 | 34 | # Import environment specific config. This must remain at the bottom 35 | # of this file so it overrides the configuration defined above. 36 | import_config "#{config_env()}.exs" 37 | -------------------------------------------------------------------------------- /webrtc_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 | -------------------------------------------------------------------------------- /webrtc_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 | {:ok, _task_pid} = 13 | Task.start_link(fn -> 14 | Membrane.Pipeline.start_link(WebRTCLiveView.Pipeline, 15 | ingress_signaling: ingress_signaling, 16 | egress_signaling: egress_signaling 17 | ) 18 | end) 19 | 20 | socket 21 | |> Capture.attach( 22 | id: "mediaCapture", 23 | signaling: ingress_signaling, 24 | video?: true, 25 | audio?: false 26 | ) 27 | |> Player.attach( 28 | id: "videoPlayer", 29 | signaling: egress_signaling 30 | ) 31 | else 32 | socket 33 | end 34 | 35 | {:ok, socket} 36 | end 37 | 38 | def render(assigns) do 39 | ~H""" 40 |

Captured stream preview

41 | 42 | 43 |

Stream sent by the server

44 | 45 | """ 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /rtsp_to_hls/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Demo.RTSPToHLS.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :rtsp_to_hls_demo, 7 | version: "0.1.0", 8 | elixir: "~> 1.13", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | compilers: Mix.compilers(), 11 | start_permanent: Mix.env() == :prod, 12 | deps: deps(), 13 | dialyzer: dialyzer() 14 | ] 15 | end 16 | 17 | # Run "mix help compile.app" to learn about applications. 18 | def application do 19 | [ 20 | extra_applications: [:logger, :runtime_tools] 21 | ] 22 | end 23 | 24 | defp elixirc_paths(_env), do: ["lib"] 25 | 26 | # Run "mix help deps" to learn about dependencies. 27 | defp deps do 28 | [ 29 | {:membrane_core, "~> 1.0"}, 30 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, 31 | {:dialyxir, ">= 0.0.0", only: :dev, runtime: false}, 32 | {:credo, ">= 0.0.0", only: :dev, runtime: false}, 33 | {:connection, "~> 1.1"}, 34 | {:membrane_http_adaptive_stream_plugin, "~> 0.19.0"}, 35 | {:membrane_realtimer_plugin, "~> 0.9.0"}, 36 | {:membrane_rtsp_plugin, "~> 0.7.0"}, 37 | {:membrane_aac_plugin, "~> 0.19.0"}, 38 | {:membrane_rtp_aac_plugin, "~> 0.9.1"}, 39 | {:membrane_simple_rtsp_server, "~> 0.1.6"}, 40 | {:membrane_mp4_plugin, "~> 0.36.0"}, 41 | {:membrane_udp_plugin, "~> 0.14.0"} 42 | ] 43 | end 44 | 45 | defp dialyzer() do 46 | [ 47 | flags: [:error_handling] 48 | ] 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /rtmp_to_hls/lib/rtmp_to_hls_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpToHlsWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :rtmp_to_hls 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: "_rtmp_to_hls_key", 10 | signing_salt: "qyNPZi3z" 11 | ] 12 | 13 | # socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]] 14 | 15 | # Serve at "/" the static files from "priv/static" directory. 16 | # 17 | # You should set gzip to true if you are running phx.digest 18 | # when deploying your static files in production. 19 | plug Plug.Static, 20 | at: "/", 21 | from: :rtmp_to_hls, 22 | gzip: false, 23 | only: ~w(assets fonts images favicon.ico robots.txt) 24 | 25 | # Code reloading can be explicitly enabled under the 26 | # :code_reloader configuration of your endpoint. 27 | if code_reloading? do 28 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 29 | plug Phoenix.LiveReloader 30 | plug Phoenix.CodeReloader 31 | end 32 | 33 | plug Plug.RequestId 34 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] 35 | 36 | plug Plug.Parsers, 37 | parsers: [:urlencoded, :multipart, :json], 38 | pass: ["*/*"], 39 | json_decoder: Phoenix.json_library() 40 | 41 | plug Plug.MethodOverride 42 | plug Plug.Head 43 | plug Plug.Session, @session_options 44 | plug RtmpToHlsWeb.Router 45 | end 46 | -------------------------------------------------------------------------------- /rtmp_to_hls/lib/rtmp_to_hls_web/telemetry.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpToHlsWeb.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.stop.duration", 26 | unit: {:native, :millisecond} 27 | ), 28 | summary("phoenix.router_dispatch.stop.duration", 29 | tags: [:route], 30 | unit: {:native, :millisecond} 31 | ), 32 | 33 | # VM Metrics 34 | summary("vm.memory.total", unit: {:byte, :kilobyte}), 35 | summary("vm.total_run_queue_lengths.total"), 36 | summary("vm.total_run_queue_lengths.cpu"), 37 | summary("vm.total_run_queue_lengths.io") 38 | ] 39 | end 40 | 41 | defp periodic_measurements do 42 | [ 43 | # A module, function and arguments to be invoked periodically. 44 | # This function must call :telemetry.execute/3 and a metric must be added above. 45 | # {RtmpToHlsWeb, :count_users, []} 46 | ] 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /webrtc_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: %{"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 | level: :info 39 | 40 | # Use Jason for JSON parsing in Phoenix 41 | config :phoenix, :json_library, Jason 42 | 43 | # Import environment specific config. This must remain at the bottom 44 | # of this file so it overrides the configuration defined above. 45 | import_config "#{config_env()}.exs" 46 | -------------------------------------------------------------------------------- /.github/workflows/webrtc_videoroom_master_build.yml: -------------------------------------------------------------------------------- 1 | name: WebRTC Videoroom - Master Build 2 | on: 3 | push: 4 | branches: 5 | - "master" 6 | paths: 7 | - "webrtc/videoroom/**" 8 | - "!webrtc/videoroom/README.md" 9 | 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Declare variables 20 | id: vars 21 | shell: bash 22 | run: | 23 | echo "::set-output name=version::$(git describe --tags --always)" 24 | 25 | - name: Cache Docker layers 26 | uses: actions/cache@v2 27 | with: 28 | path: /tmp/.buildx-cache 29 | key: ${{ runner.os }}-buildx-${{ github.sha }} 30 | restore-keys: | 31 | ${{ runner.os }}-buildx 32 | 33 | - name: Login to Docker Hub 34 | uses: docker/login-action@v1 35 | with: 36 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 37 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 38 | 39 | - name: Set up Docker Buildx 40 | id: buildx 41 | uses: docker/setup-buildx-action@v1 42 | 43 | - name: Build and push latest version 44 | id: docker_build_latest 45 | uses: docker/build-push-action@v2 46 | with: 47 | context: webrtc/videoroom 48 | file: webrtc/videoroom/Dockerfile 49 | push: true 50 | build-args: VERSION= ${{ steps.vars.outputs.version }} 51 | tags: membraneframework/demo_webrtc_videoroom:latest 52 | -------------------------------------------------------------------------------- /camera_to_hls_nerves/lib/camera_to_hls_nerves/pipeline.ex: -------------------------------------------------------------------------------- 1 | defmodule CameraToHlsNerves.Pipeline do 2 | use Membrane.Pipeline 3 | 4 | def start_link(opts) do 5 | Membrane.Pipeline.start_link(__MODULE__, opts) 6 | end 7 | 8 | @impl true 9 | def handle_init(_ctx, _opts) do 10 | spec = [ 11 | child(:source, Membrane.Rpicam.Source) 12 | |> child(:parser, Membrane.H264.Parser) 13 | |> via_in(:input, 14 | options: [ 15 | encoding: :H264, 16 | track_name: "my_track", 17 | segment_duration: Membrane.Time.seconds(5) 18 | ] 19 | ) 20 | |> child(:hls_sink, %Membrane.HTTPAdaptiveStream.SinkBin{ 21 | manifest_module: Membrane.HTTPAdaptiveStream.HLS, 22 | storage: %Membrane.HTTPAdaptiveStream.Storages.FileStorage{directory: "/data/output"}, 23 | target_window_duration: Membrane.Time.seconds(10) 24 | }) 25 | ] 26 | 27 | {[spec: spec], %{}} 28 | end 29 | 30 | @impl true 31 | def handle_child_notification({:track_playable, _track_info}, :hls_sink, _context, state) do 32 | Supervisor.start_child(CameraToHlsNervesSupervisor, %{ 33 | id: :hls_server, 34 | start: {:inets, :start, [:httpd, httpd_options(), :stand_alone]} 35 | }) 36 | 37 | {[], state} 38 | end 39 | 40 | def handle_child_notification(_notification, _child, _context, state) do 41 | {[], state} 42 | end 43 | 44 | defp httpd_options() do 45 | [ 46 | bind_address: ~c"0.0.0.0", 47 | port: 8000, 48 | document_root: ~c".", 49 | server_name: ~c"camera_to_hls_nerves", 50 | server_root: ~c"/" 51 | ] 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/lib/rtmp_to_adaptive_hls_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpToAdaptiveHlsWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :rtmp_to_adaptive_hls 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: "_rtmp_to_adaptive_hls_key", 10 | signing_salt: "qyNPZi3z" 11 | ] 12 | 13 | # socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]] 14 | 15 | # Serve at "/" the static files from "priv/static" directory. 16 | # 17 | # You should set gzip to true if you are running phx.digest 18 | # when deploying your static files in production. 19 | plug Plug.Static, 20 | at: "/", 21 | from: :rtmp_to_adaptive_hls, 22 | gzip: false, 23 | only: ~w(assets fonts images favicon.ico robots.txt) 24 | 25 | # Code reloading can be explicitly enabled under the 26 | # :code_reloader configuration of your endpoint. 27 | if code_reloading? do 28 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 29 | plug Phoenix.LiveReloader 30 | plug Phoenix.CodeReloader 31 | end 32 | 33 | plug Plug.RequestId 34 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] 35 | 36 | plug Plug.Parsers, 37 | parsers: [:urlencoded, :multipart, :json], 38 | pass: ["*/*"], 39 | json_decoder: Phoenix.json_library() 40 | 41 | plug Plug.MethodOverride 42 | plug Plug.Head 43 | plug Plug.Session, @session_options 44 | plug RtmpToAdaptiveHlsWeb.Router 45 | end 46 | -------------------------------------------------------------------------------- /webrtc_live_view/lib/webrtc_live_view/contours_drawer.ex: -------------------------------------------------------------------------------- 1 | defmodule WebRTCLiveView.CountoursDrawer do 2 | use Membrane.Filter 3 | 4 | alias Evision.{ColorConversionCodes, Constant} 5 | 6 | def_input_pad :input, accepted_format: %Membrane.RawVideo{pixel_format: :RGB} 7 | def_output_pad :output, accepted_format: %Membrane.RawVideo{pixel_format: :RGB} 8 | 9 | @impl true 10 | def handle_buffer(:input, buffer, ctx, state) do 11 | %{height: height, width: width} = ctx.pads.input.stream_format 12 | 13 | {:ok, image} = 14 | Vix.Vips.Image.new_from_binary(buffer.payload, width, height, 3, :VIPS_FORMAT_UCHAR) 15 | 16 | {:ok, image} = Image.to_evision(image) 17 | 18 | grayscale = Evision.cvtColor(image, ColorConversionCodes.cv_COLOR_BGR2GRAY()) 19 | flags = Bitwise.bor(Constant.cv_THRESH_BINARY(), Constant.cv_THRESH_OTSU()) 20 | {_ok, bw} = Evision.threshold(grayscale, 50, 255, flags) 21 | 22 | {contours, _} = 23 | Evision.findContours( 24 | bw, 25 | Constant.cv_RETR_LIST(), 26 | Constant.cv_CHAIN_APPROX_NONE() 27 | ) 28 | 29 | contours = 30 | Enum.filter(contours, fn c -> 31 | trunc(Evision.contourArea(c)) in 100..200_000 32 | end) 33 | 34 | {:ok, image} = 35 | image 36 | |> Evision.drawContours(contours, _index = -1, _edge_color = {0, 0, 255}, thickness: 2) 37 | |> Image.from_evision() 38 | 39 | {:ok, payload} = 40 | image |> Image.flatten!() |> Image.to_colorspace!(:srgb) |> Vix.Vips.Image.write_to_binary() 41 | 42 | buffer = %Membrane.Buffer{buffer | payload: payload} 43 | {[buffer: {:output, buffer}], state} 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/lib/rtmp_to_adaptive_hls_web/telemetry.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpToAdaptiveHlsWeb.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.stop.duration", 26 | unit: {:native, :millisecond} 27 | ), 28 | summary("phoenix.router_dispatch.stop.duration", 29 | tags: [:route], 30 | unit: {:native, :millisecond} 31 | ), 32 | 33 | # VM Metrics 34 | summary("vm.memory.total", unit: {:byte, :kilobyte}), 35 | summary("vm.total_run_queue_lengths.total"), 36 | summary("vm.total_run_queue_lengths.cpu"), 37 | summary("vm.total_run_queue_lengths.io") 38 | ] 39 | end 40 | 41 | defp periodic_measurements do 42 | [ 43 | # A module, function and arguments to be invoked periodically. 44 | # This function must call :telemetry.execute/3 and a metric must be added above. 45 | # {RtmpToAdaptiveHlsWeb, :count_users, []} 46 | ] 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /rtp/lib/membrane_demo_rtp/receive_pipeline.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Demo.RTP.ReceivePipeline do 2 | use Membrane.Pipeline 3 | 4 | require Logger 5 | 6 | alias Membrane.{H264, Opus, RTP, UDP} 7 | 8 | @local_ip {127, 0, 0, 1} 9 | 10 | @impl true 11 | def handle_init(_ctx, opts) do 12 | %{audio_port: audio_port, video_port: video_port, secure?: secure?, srtp_key: srtp_key} = opts 13 | 14 | srtp = 15 | if secure? do 16 | [%ExLibSRTP.Policy{ssrc: :any_inbound, key: srtp_key}] 17 | else 18 | false 19 | end 20 | 21 | spec = 22 | {[ 23 | child(:video_src, %UDP.Source{ 24 | local_port_no: video_port, 25 | local_address: @local_ip 26 | }) 27 | |> child(:video_rtp_demuxer, %RTP.Demuxer{srtp: srtp}) 28 | |> via_out(:output, options: [stream_id: {:encoding_name, :H264}]) 29 | |> child(:video_depayloader, RTP.H264.Depayloader) 30 | |> child(:video_parser, %H264.Parser{ 31 | generate_best_effort_timestamps: %{framerate: {30, 1}} 32 | }) 33 | |> child(:video_decoder, H264.FFmpeg.Decoder) 34 | |> child(:video_player, Membrane.SDL.Player), 35 | child(:audio_src, %UDP.Source{ 36 | local_port_no: audio_port, 37 | local_address: @local_ip 38 | }) 39 | |> child(:audio_rtp_demuxer, %RTP.Demuxer{srtp: srtp}) 40 | |> via_out(:output, options: [stream_id: {:encoding_name, :opus}]) 41 | |> child(:audio_depayloader, RTP.Opus.Depayloader) 42 | |> child(:audio_decoder, Opus.Decoder) 43 | |> child(:audio_player, Membrane.PortAudio.Sink) 44 | ], stream_sync: :sinks} 45 | 46 | {[spec: spec], %{}} 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /rtmp_to_hls/lib/rtmp_to_hls_web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpToHlsWeb.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Generates tag for inlined form input errors. 10 | """ 11 | def error_tag(form, field) do 12 | Enum.map(Keyword.get_values(form.errors, field), fn error -> 13 | content_tag(:span, translate_error(error), 14 | class: "invalid-feedback", 15 | phx_feedback_for: input_name(form, field) 16 | ) 17 | end) 18 | end 19 | 20 | @doc """ 21 | Translates an error message using gettext. 22 | """ 23 | def translate_error({msg, opts}) do 24 | # When using gettext, we typically pass the strings we want 25 | # to translate as a static argument: 26 | # 27 | # # Translate "is invalid" in the "errors" domain 28 | # dgettext("errors", "is invalid") 29 | # 30 | # # Translate the number of files with plural rules 31 | # dngettext("errors", "1 file", "%{count} files", count) 32 | # 33 | # Because the error messages we show in our forms and APIs 34 | # are defined inside Ecto, we need to translate them dynamically. 35 | # This requires us to call the Gettext module passing our gettext 36 | # backend as first argument. 37 | # 38 | # Note we use the "errors" domain, which means translations 39 | # should be written to the errors.po file. The :count option is 40 | # set by Ecto and indicates we should also apply plural rules. 41 | if count = opts[:count] do 42 | Gettext.dngettext(RtmpToHlsWeb.Gettext, "errors", msg, msg, count, opts) 43 | else 44 | Gettext.dgettext(RtmpToHlsWeb.Gettext, "errors", msg, opts) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /rtp_to_hls/README.md: -------------------------------------------------------------------------------- 1 | # RTP to HLS 2 | 3 | This project demonstrates handling RTP streams and converting them to HLS streams. 4 | 5 | The whole idea has been described in [this blog post](https://blog.swmansion.com/live-video-streaming-in-elixir-made-simple-with-membrane-fc5b2083982d). 6 | 7 | ## Running the demo 8 | 9 | To run the demo, you'll need to have [Elixir installed](https://elixir-lang.org/install.html). Then, run 10 | 11 | ```shell 12 | elixir rtp_to_hls.exs 13 | ``` 14 | 15 | After a while, the server will start listening for UDP connections on port 5000. Then, start the RTP stream with 16 | 17 | ```shell 18 | elixir send.exs 19 | ``` 20 | 21 | When the server prints that playback is available, visit `http://localhost:8000/stream.html` and you should see the stream there. The stream can be also played with players other than the browser, like `vlc` or `ffplay`, for example 22 | 23 | ```bash 24 | ffplay http://localhost:8000/output/index.m3u8 25 | ``` 26 | 27 | The RTP stream can be sent with other tools as well, for example with [GStreamer](https://gstreamer.freedesktop.org/): 28 | 29 | ```shell 30 | gst-launch-1.0 -v audiotestsrc ! audio/x-raw,rate=44100 ! faac ! rtpmp4gpay pt=127 ! udpsink host=127.0.0.1 port=5000 \ 31 | videotestsrc ! video/x-raw,format=I420 ! x264enc key-int-max=10 tune=zerolatency ! rtph264pay pt=96 ! udpsink host=127.0.0.1 port=5000 32 | ``` 33 | 34 | 35 | ## Copyright and License 36 | 37 | Copyright 2020, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 38 | 39 | [![Software Mansion](https://membraneframework.github.io/static/logo/swm_logo_readme.png)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 40 | 41 | Licensed under the [Apache License, Version 2.0](LICENSE) 42 | -------------------------------------------------------------------------------- /rtmp_to_hls/lib/rtmp_to_hls/application.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpToHls.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 | alias Membrane.RTMP.Source.TcpServer 9 | 10 | @port 9006 11 | @local_ip {127, 0, 0, 1} 12 | 13 | @impl true 14 | def start(_type, _args) do 15 | File.mkdir_p("output") 16 | 17 | tcp_server_options = %TcpServer{ 18 | port: @port, 19 | listen_options: [ 20 | :binary, 21 | packet: :raw, 22 | active: false, 23 | ip: @local_ip 24 | ], 25 | socket_handler: fn socket -> 26 | {:ok, _sup, pid} = 27 | Membrane.Pipeline.start_link(Membrane.Demo.RtmpToHls, socket: socket) 28 | 29 | {:ok, pid} 30 | end 31 | } 32 | 33 | children = [ 34 | # Start the Tcp Server 35 | # Membrane.Demo.RtmpToHls, 36 | %{ 37 | id: TcpServer, 38 | start: {TcpServer, :start_link, [tcp_server_options]} 39 | }, 40 | # Start the Telemetry supervisor 41 | RtmpToHlsWeb.Telemetry, 42 | # Start the PubSub system 43 | {Phoenix.PubSub, name: RtmpToHls.PubSub}, 44 | # Start the Endpoint (http/https) 45 | RtmpToHlsWeb.Endpoint 46 | ] 47 | 48 | # See https://hexdocs.pm/elixir/Supervisor.html 49 | # for other strategies and supported options 50 | opts = [strategy: :one_for_one, name: RtmpToHls.Supervisor] 51 | Supervisor.start_link(children, opts) 52 | end 53 | 54 | # Tell Phoenix to update the endpoint configuration 55 | # whenever the application is updated. 56 | @impl true 57 | def config_change(changed, _new, removed) do 58 | RtmpToHlsWeb.Endpoint.config_change(changed, removed) 59 | :ok 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /rtmp_to_hls/lib/rtmp_to_hls/pipeline.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Demo.RtmpToHls do 2 | use Membrane.Pipeline 3 | 4 | alias Membrane.RTMP.SourceBin 5 | 6 | @impl true 7 | def handle_init(_context, socket: socket) do 8 | structure = [ 9 | child(:src, %SourceBin{socket: socket}) 10 | |> via_out(:audio) 11 | |> via_in(Pad.ref(:input, :audio), 12 | options: [encoding: :AAC, segment_duration: Membrane.Time.seconds(4)] 13 | ) 14 | |> child(:sink, %Membrane.HTTPAdaptiveStream.SinkBin{ 15 | manifest_module: Membrane.HTTPAdaptiveStream.HLS, 16 | target_window_duration: :infinity, 17 | persist?: false, 18 | storage: %Membrane.HTTPAdaptiveStream.Storages.FileStorage{directory: "output"} 19 | }), 20 | get_child(:src) 21 | |> via_out(:video) 22 | |> via_in(Pad.ref(:input, :video), 23 | options: [encoding: :H264, segment_duration: Membrane.Time.seconds(4)] 24 | ) 25 | |> get_child(:sink) 26 | ] 27 | 28 | {[spec: structure], %{socket: socket}} 29 | end 30 | 31 | @impl true 32 | def handle_child_notification( 33 | {:socket_control_needed, _socket, _source} = notification, 34 | :src, 35 | _ctx, 36 | state 37 | ) do 38 | send(self(), notification) 39 | {[], state} 40 | end 41 | 42 | @impl true 43 | def handle_child_notification(_notification, _child, _ctx, state) do 44 | {[], state} 45 | end 46 | 47 | @impl true 48 | def handle_info({:socket_control_needed, socket, source} = notification, _ctx, state) do 49 | case SourceBin.pass_control(socket, source) do 50 | :ok -> 51 | :ok 52 | 53 | {:error, :not_owner} -> 54 | Process.send_after(self(), notification, 200) 55 | end 56 | 57 | {[], state} 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /rtp/lib/membrane_demo_rtp/send_pipeline.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Demo.RTP.SendPipeline do 2 | use Membrane.Pipeline 3 | 4 | alias Membrane.{File, H264, Opus, RTP, UDP} 5 | 6 | @impl true 7 | def handle_init(_ctx, opts) do 8 | %{ 9 | audio_port: audio_port, 10 | video_port: video_port, 11 | secure?: secure?, 12 | srtp_key: srtp_key 13 | } = opts 14 | 15 | srtp = 16 | if secure? do 17 | [%ExLibSRTP.Policy{ssrc: :any_inbound, key: srtp_key}] 18 | else 19 | false 20 | end 21 | 22 | spec = [ 23 | child(:video_src, %File.Source{ 24 | location: "samples/video.h264" 25 | }) 26 | |> child(:video_parser, %H264.Parser{ 27 | generate_best_effort_timestamps: %{framerate: {30, 1}}, 28 | output_alignment: :nalu 29 | }) 30 | |> child(:video_payloader, RTP.H264.Payloader) 31 | |> child(:video_rtp_muxer, %RTP.Muxer{srtp: srtp}) 32 | |> child(:video_realtimer, Membrane.Realtimer) 33 | |> child(:video_sink, %UDP.Sink{ 34 | destination_port_no: video_port, 35 | destination_address: {127, 0, 0, 1} 36 | }), 37 | child(:audio_src, %File.Source{ 38 | location: "samples/audio.opus" 39 | }) 40 | |> child(:audio_parser, %Opus.Parser{ 41 | input_delimitted?: true, 42 | delimitation: :undelimit, 43 | generate_best_effort_timestamps?: true 44 | }) 45 | |> child(:audio_payloader, RTP.Opus.Payloader) 46 | |> child(:audio_rtp_muxer, %RTP.Muxer{srtp: srtp}) 47 | |> child(:audio_realtimer, Membrane.Realtimer) 48 | |> child(:audio_sink, %UDP.Sink{ 49 | destination_port_no: audio_port, 50 | destination_address: {127, 0, 0, 1} 51 | }) 52 | ] 53 | 54 | {[spec: spec], %{}} 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/lib/rtmp_to_adaptive_hls_web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpToAdaptiveHlsWeb.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Generates tag for inlined form input errors. 10 | """ 11 | def error_tag(form, field) do 12 | Enum.map(Keyword.get_values(form.errors, field), fn error -> 13 | content_tag(:span, translate_error(error), 14 | class: "invalid-feedback", 15 | phx_feedback_for: input_name(form, field) 16 | ) 17 | end) 18 | end 19 | 20 | @doc """ 21 | Translates an error message using gettext. 22 | """ 23 | def translate_error({msg, opts}) do 24 | # When using gettext, we typically pass the strings we want 25 | # to translate as a static argument: 26 | # 27 | # # Translate "is invalid" in the "errors" domain 28 | # dgettext("errors", "is invalid") 29 | # 30 | # # Translate the number of files with plural rules 31 | # dngettext("errors", "1 file", "%{count} files", count) 32 | # 33 | # Because the error messages we show in our forms and APIs 34 | # are defined inside Ecto, we need to translate them dynamically. 35 | # This requires us to call the Gettext module passing our gettext 36 | # backend as first argument. 37 | # 38 | # Note we use the "errors" domain, which means translations 39 | # should be written to the errors.po file. The :count option is 40 | # set by Ecto and indicates we should also apply plural rules. 41 | if count = opts[:count] do 42 | Gettext.dngettext(RtmpToAdaptiveHlsWeb.Gettext, "errors", msg, msg, count, opts) 43 | else 44 | Gettext.dgettext(RtmpToAdaptiveHlsWeb.Gettext, "errors", msg, opts) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /simple_pipeline/lib/membrane_demo/simple_pipeline.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Demo.SimplePipeline do 2 | @moduledoc """ 3 | Sample Membrane pipeline that will play an `.mp3` file. 4 | """ 5 | 6 | use Membrane.Pipeline 7 | 8 | @doc """ 9 | In order to play `.mp3` file we need to read it first. 10 | 11 | In membrane every entry point to data flow is called `Source`. Since we want 12 | to play a `file`, we will use `File.Source`. 13 | 14 | Next problem that arises is the fact that we are reading MPEG Layer 3 frames 15 | not raw audio. To deal with that we need to use `Filter` called decoder. It 16 | takes `.mp3` frames and yields RAW audio data. 17 | 18 | There is one tiny problem here though. Decoder returns `%Raw{format: :s24le}` 19 | data, but PortAudio (module that actually talks with the audio driver of your 20 | computer) wants `%Raw{format: :s16le, sample_rate: 48000, channels: 2}`. 21 | 22 | That's where `SWResample.Converter` comes into play. It will consume data that 23 | doesn't suite our needs and will yield data in format we want. 24 | """ 25 | @impl true 26 | def handle_init(_ctx, path_to_mp3) do 27 | # Setup the flow of the data 28 | # Stream from file 29 | spec = 30 | child(:file, %Membrane.File.Source{location: path_to_mp3}) 31 | # Decode frames 32 | |> child(:decoder, Membrane.MP3.MAD.Decoder) 33 | # Convert Raw :s24le to Raw :s16le 34 | |> child(:converter, %Membrane.FFmpeg.SWResample.Converter{ 35 | output_stream_format: %Membrane.RawAudio{ 36 | sample_format: :s16le, 37 | sample_rate: 48000, 38 | channels: 2 39 | } 40 | }) 41 | # Stream data into PortAudio to play it on speakers. 42 | |> child(:portaudio, Membrane.PortAudio.Sink) 43 | 44 | {[spec: spec], %{}} 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /webrtc_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 | -------------------------------------------------------------------------------- /rtmp_to_hls/assets/js/app.js: -------------------------------------------------------------------------------- 1 | // We import the CSS which is extracted to its own file by esbuild. 2 | // Remove this line if you add a your own CSS build pipeline (e.g postcss). 3 | import "../css/app.css" 4 | 5 | // If you want to use Phoenix channels, run `mix help phx.gen.channel` 6 | // to get started and then uncomment the line below. 7 | // import "./user_socket.js" 8 | 9 | // You can include dependencies in two ways. 10 | // 11 | // The simplest option is to put them in assets/vendor and 12 | // import them using relative paths: 13 | // 14 | // import "./vendor/some-package.js" 15 | // 16 | // Alternatively, you can `npm install some-package` and import 17 | // them using a path starting with the package name: 18 | // 19 | // import "some-package" 20 | // 21 | 22 | // Include phoenix_html to handle method=PUT/DELETE in forms and buttons. 23 | import "phoenix_html" 24 | // Establish Phoenix Socket and LiveView configuration. 25 | // import {Socket} from "phoenix" 26 | // import {LiveSocket} from "phoenix_live_view" 27 | // import topbar from "../vendor/topbar" 28 | 29 | // let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") 30 | // let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}}) 31 | 32 | // Show progress bar on live navigation and form submits 33 | // topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) 34 | // window.addEventListener("phx:page-loading-start", info => topbar.show()) 35 | // window.addEventListener("phx:page-loading-stop", info => topbar.hide()) 36 | 37 | // connect if there are any LiveViews on the page 38 | // liveSocket.connect() 39 | 40 | // expose liveSocket on window for web console debug logs and latency simulation: 41 | // >> liveSocket.enableDebug() 42 | // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session 43 | // >> liveSocket.disableLatencySim() 44 | // window.liveSocket = liveSocket 45 | -------------------------------------------------------------------------------- /rtmp_to_hls/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 | if config_env() == :prod do 10 | # The secret key base is used to sign/encrypt cookies and other secrets. 11 | # A default value is used in config/dev.exs and config/test.exs but you 12 | # want to use a different value for prod and you most likely don't want 13 | # to check this value into version control, so we use an environment 14 | # variable instead. 15 | secret_key_base = 16 | System.get_env("SECRET_KEY_BASE") || 17 | raise """ 18 | environment variable SECRET_KEY_BASE is missing. 19 | You can generate one by calling: mix phx.gen.secret 20 | """ 21 | 22 | config :rtmp_to_hls, RtmpToHlsWeb.Endpoint, 23 | http: [ 24 | # Enable IPv6 and bind on all interfaces. 25 | # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. 26 | # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html 27 | # for details about using IPv6 vs IPv4 and loopback vs public addresses. 28 | ip: {0, 0, 0, 0, 0, 0, 0, 0}, 29 | port: String.to_integer(System.get_env("PORT") || "4000") 30 | ], 31 | secret_key_base: secret_key_base 32 | 33 | # ## Using releases 34 | # 35 | # If you are doing OTP releases, you need to instruct Phoenix 36 | # to start each relevant endpoint: 37 | # 38 | # config :rtmp_to_hls, RtmpToHlsWeb.Endpoint, server: true 39 | # 40 | # Then you can assemble a release by calling `mix release`. 41 | # See `mix help release` for more information. 42 | end 43 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/assets/js/app.js: -------------------------------------------------------------------------------- 1 | // We import the CSS which is extracted to its own file by esbuild. 2 | // Remove this line if you add a your own CSS build pipeline (e.g postcss). 3 | import "../css/app.css" 4 | 5 | // If you want to use Phoenix channels, run `mix help phx.gen.channel` 6 | // to get started and then uncomment the line below. 7 | // import "./user_socket.js" 8 | 9 | // You can include dependencies in two ways. 10 | // 11 | // The simplest option is to put them in assets/vendor and 12 | // import them using relative paths: 13 | // 14 | // import "./vendor/some-package.js" 15 | // 16 | // Alternatively, you can `npm install some-package` and import 17 | // them using a path starting with the package name: 18 | // 19 | // import "some-package" 20 | // 21 | 22 | // Include phoenix_html to handle method=PUT/DELETE in forms and buttons. 23 | import "phoenix_html" 24 | // Establish Phoenix Socket and LiveView configuration. 25 | // import {Socket} from "phoenix" 26 | // import {LiveSocket} from "phoenix_live_view" 27 | // import topbar from "../vendor/topbar" 28 | 29 | // let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") 30 | // let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}}) 31 | 32 | // Show progress bar on live navigation and form submits 33 | // topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) 34 | // window.addEventListener("phx:page-loading-start", info => topbar.show()) 35 | // window.addEventListener("phx:page-loading-stop", info => topbar.hide()) 36 | 37 | // connect if there are any LiveViews on the page 38 | // liveSocket.connect() 39 | 40 | // expose liveSocket on window for web console debug logs and latency simulation: 41 | // >> liveSocket.enableDebug() 42 | // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session 43 | // >> liveSocket.disableLatencySim() 44 | // window.liveSocket = liveSocket 45 | -------------------------------------------------------------------------------- /.github/workflows/webrtc_videoroom_tag_build.yml: -------------------------------------------------------------------------------- 1 | name: WebRTC Videoroom - Tag Build 2 | on: 3 | push: 4 | tags: 5 | - "*.*.*" 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Declare variables 14 | id: vars 15 | shell: bash 16 | run: | 17 | echo "::set-output name=version::$(git describe --tags --always)" 18 | 19 | - name: Get tag 20 | id: tag 21 | uses: dawidd6/action-get-tag@v1 22 | 23 | - name: Cache Docker layers 24 | uses: actions/cache@v2 25 | with: 26 | path: /tmp/.buildx-cache 27 | key: ${{ runner.os }}-buildx-${{ github.sha }} 28 | restore-keys: | 29 | ${{ runner.os }}-buildx 30 | 31 | - name: Login to Docker Hub 32 | uses: docker/login-action@v1 33 | with: 34 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 35 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 36 | 37 | - name: Set up Docker Buildx 38 | id: buildx 39 | uses: docker/setup-buildx-action@v1 40 | 41 | - name: Build and push tagged version 42 | id: docker_build_tag 43 | uses: docker/build-push-action@v2 44 | with: 45 | context: webrtc/videoroom 46 | file: webrtc/videoroom/Dockerfile 47 | push: true 48 | build-args: VERSION= ${{ steps.vars.outputs.version }} 49 | tags: membraneframework/demo_webrtc_videoroom:${{ steps.tag.outputs.tag }} 50 | 51 | - name: Build and push latest version 52 | id: docker_build_latest 53 | uses: docker/build-push-action@v2 54 | with: 55 | context: webrtc/videoroom 56 | file: webrtc/videoroom/Dockerfile 57 | push: true 58 | build-args: VERSION= ${{ steps.vars.outputs.version }} 59 | tags: membraneframework/demo_webrtc_videoroom:latest 60 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/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 | if config_env() == :prod do 10 | # The secret key base is used to sign/encrypt cookies and other secrets. 11 | # A default value is used in config/dev.exs and config/test.exs but you 12 | # want to use a different value for prod and you most likely don't want 13 | # to check this value into version control, so we use an environment 14 | # variable instead. 15 | secret_key_base = 16 | System.get_env("SECRET_KEY_BASE") || 17 | raise """ 18 | environment variable SECRET_KEY_BASE is missing. 19 | You can generate one by calling: mix phx.gen.secret 20 | """ 21 | 22 | config :rtmp_to_adaptive_hls, RtmpToAdaptiveHlsWeb.Endpoint, 23 | http: [ 24 | # Enable IPv6 and bind on all interfaces. 25 | # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. 26 | # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html 27 | # for details about using IPv6 vs IPv4 and loopback vs public addresses. 28 | ip: {0, 0, 0, 0, 0, 0, 0, 0}, 29 | port: String.to_integer(System.get_env("PORT") || "4000") 30 | ], 31 | secret_key_base: secret_key_base 32 | 33 | # ## Using releases 34 | # 35 | # If you are doing OTP releases, you need to instruct Phoenix 36 | # to start each relevant endpoint: 37 | # 38 | # config :rtmp_to_adaptive_hls, RtmpToAdaptiveHlsWeb.Endpoint, server: true 39 | # 40 | # Then you can assemble a release by calling `mix release`. 41 | # See `mix help release` for more information. 42 | end 43 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/lib/rtmp_to_adaptive_hls/application.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpToAdaptiveHls.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 | @port 9006 9 | @local_ip {127, 0, 0, 1} 10 | 11 | @impl true 12 | def start(_type, _args) do 13 | File.mkdir_p("output") 14 | 15 | rtmp_server_options = %{ 16 | port: @port, 17 | listen_options: [ 18 | :binary, 19 | packet: :raw, 20 | active: false, 21 | ip: @local_ip 22 | ], 23 | handle_new_client: fn client_ref, app, stream_key -> 24 | {:ok, _sup, pid} = 25 | Membrane.Pipeline.start_link(Membrane.Demo.RtmpToAdaptiveHls, %{ 26 | client_ref: client_ref, 27 | app: app, 28 | stream_key: stream_key 29 | }) 30 | 31 | {Membrane.Demo.RtmpToAdaptiveHls.ClientHandler, %{pipeline: pid}} 32 | end 33 | } 34 | 35 | children = [ 36 | # Start the RTMP server 37 | %{ 38 | id: Membrane.RTMPServer, 39 | start: {Membrane.RTMPServer, :start_link, [rtmp_server_options]} 40 | }, 41 | # Start the Telemetry supervisor 42 | RtmpToAdaptiveHlsWeb.Telemetry, 43 | # Start the PubSub system 44 | {Phoenix.PubSub, name: RtmpToAdaptiveHls.PubSub}, 45 | # Start the Endpoint (http/https) 46 | RtmpToAdaptiveHlsWeb.Endpoint 47 | ] 48 | 49 | # See https://hexdocs.pm/elixir/Supervisor.html 50 | # for other strategies and supported options 51 | opts = [strategy: :one_for_one, name: RtmpToAdaptiveHls.Supervisor] 52 | Supervisor.start_link(children, opts) 53 | end 54 | 55 | # Tell Phoenix to update the endpoint configuration 56 | # whenever the application is updated. 57 | @impl true 58 | def config_change(changed, _new, removed) do 59 | RtmpToAdaptiveHlsWeb.Endpoint.config_change(changed, removed) 60 | :ok 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /rtmp_to_hls/assets/css/app.css: -------------------------------------------------------------------------------- 1 | /* This file is for your main application CSS */ 2 | @import "./phoenix.css"; 3 | 4 | /* Alerts and form errors used by phx.new */ 5 | .alert { 6 | padding: 15px; 7 | margin-bottom: 20px; 8 | border: 1px solid transparent; 9 | border-radius: 4px; 10 | } 11 | .alert-info { 12 | color: #31708f; 13 | background-color: #d9edf7; 14 | border-color: #bce8f1; 15 | } 16 | .alert-warning { 17 | color: #8a6d3b; 18 | background-color: #fcf8e3; 19 | border-color: #faebcc; 20 | } 21 | .alert-danger { 22 | color: #a94442; 23 | background-color: #f2dede; 24 | border-color: #ebccd1; 25 | } 26 | .alert p { 27 | margin-bottom: 0; 28 | } 29 | .alert:empty { 30 | display: none; 31 | } 32 | .invalid-feedback { 33 | color: #a94442; 34 | display: block; 35 | margin: -1rem 0 2rem; 36 | } 37 | 38 | /* LiveView specific classes for your customization */ 39 | .phx-no-feedback.invalid-feedback, 40 | .phx-no-feedback .invalid-feedback { 41 | display: none; 42 | } 43 | 44 | .phx-click-loading { 45 | opacity: 0.5; 46 | transition: opacity 1s ease-out; 47 | } 48 | 49 | .phx-disconnected{ 50 | cursor: wait; 51 | } 52 | .phx-disconnected *{ 53 | pointer-events: none; 54 | } 55 | 56 | .phx-modal { 57 | opacity: 1!important; 58 | position: fixed; 59 | z-index: 1; 60 | left: 0; 61 | top: 0; 62 | width: 100%; 63 | height: 100%; 64 | overflow: auto; 65 | background-color: rgb(0,0,0); 66 | background-color: rgba(0,0,0,0.4); 67 | } 68 | 69 | .phx-modal-content { 70 | background-color: #fefefe; 71 | margin: 15vh auto; 72 | padding: 20px; 73 | border: 1px solid #888; 74 | width: 80%; 75 | } 76 | 77 | .phx-modal-close { 78 | color: #aaa; 79 | float: right; 80 | font-size: 28px; 81 | font-weight: bold; 82 | } 83 | 84 | .phx-modal-close:hover, 85 | .phx-modal-close:focus { 86 | color: black; 87 | text-decoration: none; 88 | cursor: pointer; 89 | } 90 | 91 | #player { 92 | width: 100%; 93 | height: auto; 94 | margin: auto auto; 95 | } -------------------------------------------------------------------------------- /rtp/README.md: -------------------------------------------------------------------------------- 1 | # RTP 2 | 3 | This project demonstrates handling RTP in Membrane. 4 | 5 | This example uses [RTP plugin](https://github.com/membraneframework/membrane_rtp_plugin) that is responsible for receiving and sending RTP streams. 6 | 7 | ## Running the demo 8 | 9 | To run the demo, you'll need to have [Elixir installed](https://elixir-lang.org/install.html). Then, do the following: 10 | 11 | - Open a terminal in the project directory 12 | - Type `mix deps.get` to download dependencies 13 | - Type `mix run receive.exs` to run the receiving pipeline 14 | - Wait until the script runs 15 | - Open another terminal in the project directory 16 | - Type `mix run send.exs` to run the sending pipeline 17 | 18 | You should be able to see a player showing an example video. 19 | 20 | The sender pipeline (run with `send.exs`) takes sample audio and video files and sends them via RTP. 21 | The receiving pipeline (run with `receive.exs`) receives the audio and video streams and plays them. 22 | 23 | If you wish to stream using SRTP, add a `--secure` flag when running both `receive.exs` and `send.exs`. 24 | 25 | You can also use another tool, like [GStreamer](https://gstreamer.freedesktop.org/), to send the stream. In this case, you only need to start the receiving pipeline: 26 | 27 | ```shell 28 | mix run receive.exs 29 | ``` 30 | 31 | and launch GStreamer: 32 | 33 | ```shell 34 | gst-launch-1.0 -v audiotestsrc ! audio/x-raw,rate=48000,channels=2 ! opusenc ! rtpopuspay pt=120 ! udpsink host=127.0.0.1 port=5002\ 35 | videotestsrc ! video/x-raw,format=I420 ! x264enc key-int-max=10 tune=zerolatency ! rtph264pay pt=96 ! udpsink host=127.0.0.1 port=5000 36 | ``` 37 | 38 | ## Copyright and License 39 | 40 | Copyright 2018, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 41 | 42 | [![Software Mansion](https://membraneframework.github.io/static/logo/swm_logo_readme.png)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 43 | 44 | Licensed under the [Apache License, Version 2.0](LICENSE) 45 | -------------------------------------------------------------------------------- /rtmp_to_hls/config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # For production, don't forget to configure the url host 4 | # to something meaningful, Phoenix uses this information 5 | # when generating URLs. 6 | # 7 | # Note we also include the path to a cache manifest 8 | # containing the digested version of static files. This 9 | # manifest is generated by the `mix phx.digest` task, 10 | # which you should run after static files are built and 11 | # before starting your production server. 12 | config :rtmp_to_hls, RtmpToHlsWeb.Endpoint, 13 | url: [host: "example.com", port: 80], 14 | cache_static_manifest: "priv/static/cache_manifest.json" 15 | 16 | # Do not print debug messages in production 17 | config :logger, level: :info 18 | 19 | # ## SSL Support 20 | # 21 | # To get SSL working, you will need to add the `https` key 22 | # to the previous section and set your `:url` port to 443: 23 | # 24 | # config :rtmp_to_hls, RtmpToHlsWeb.Endpoint, 25 | # ..., 26 | # url: [host: "example.com", port: 443], 27 | # https: [ 28 | # ..., 29 | # port: 443, 30 | # cipher_suite: :strong, 31 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 32 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") 33 | # ] 34 | # 35 | # The `cipher_suite` is set to `:strong` to support only the 36 | # latest and more secure SSL ciphers. This means old browsers 37 | # and clients may not be supported. You can set it to 38 | # `:compatible` for wider support. 39 | # 40 | # `:keyfile` and `:certfile` expect an absolute path to the key 41 | # and cert in disk or a relative path inside priv, for example 42 | # "priv/ssl/server.key". For all supported SSL configuration 43 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 44 | # 45 | # We also recommend setting `force_ssl` in your endpoint, ensuring 46 | # no data is ever sent via http, always redirecting to https: 47 | # 48 | # config :rtmp_to_hls, RtmpToHlsWeb.Endpoint, 49 | # force_ssl: [hsts: true] 50 | # 51 | # Check `Plug.SSL` for all available options in `force_ssl`. 52 | -------------------------------------------------------------------------------- /camera_to_hls_nerves/rel/vm.args.eex: -------------------------------------------------------------------------------- 1 | ## Customize flags given to the VM: http://erlang.org/doc/man/erl.html 2 | 3 | ## Do not set -name or -sname here. Prefer configuring them at runtime 4 | ## Configure -setcookie in the mix.exs release section or at runtime 5 | 6 | ## Number of dirty schedulers doing IO work (file, sockets, and others) 7 | ##+SDio 5 8 | 9 | ## Increase number of concurrent ports/sockets 10 | ##+Q 65536 11 | 12 | ## Tweak GC to run more often 13 | ##-env ERL_FULLSWEEP_AFTER 10 14 | 15 | ## Use Ctrl-C to interrupt the current shell rather than invoking the emulator's 16 | ## break handler and possibly exiting the VM. 17 | +Bc 18 | 19 | # Allow time warps so that the Erlang system time can more closely match the 20 | # OS system time. 21 | +C multi_time_warp 22 | 23 | ## Load code at system startup 24 | ## See http://erlang.org/doc/system_principles/system_principles.html#code-loading-strategy 25 | -mode embedded 26 | 27 | # Load code as per the boot script since not using archives 28 | # See https://www.erlang.org/doc/man/init.html#command-line-flags 29 | -code_path_choice strict 30 | 31 | ## Disable scheduler busy wait to reduce idle CPU usage and avoid delaying 32 | ## other OS processes. See http://erlang.org/doc/man/erl.html#+sbwt 33 | +sbwt none 34 | +sbwtdcpu none 35 | +sbwtdio none 36 | 37 | ## Save the shell history between reboots 38 | ## See http://erlang.org/doc/man/kernel_app.html for additional options 39 | -kernel shell_history enabled 40 | 41 | ## Enable heartbeat monitoring of the Erlang runtime system 42 | -heart -env HEART_BEAT_TIMEOUT 30 43 | 44 | ## Start the Elixir shell 45 | 46 | -noshell 47 | -user elixir 48 | -run elixir start_iex 49 | 50 | 51 | ## Enable colors in the shell 52 | -elixir ansi_enabled true 53 | 54 | ## Options added after -extra are interpreted as plain arguments and can be 55 | ## retrieved using :init.get_plain_arguments(). Options before the "--" are 56 | ## interpreted by Elixir and anything afterwards is left around for other IEx 57 | ## and user applications. 58 | -extra --no-halt 59 | -- 60 | --dot-iex /etc/iex.exs 61 | -------------------------------------------------------------------------------- /simple_pipeline/README.md: -------------------------------------------------------------------------------- 1 | # Simple Membrane Pipeline 2 | 3 | This demo shows how to create a pipeline that plays an mp3 file. 4 | 5 | ## Prerequisites 6 | 7 | To run the demo, you need [Elixir installed](https://elixir-lang.org/install.html) on your machine (it's best to use a version manager, like `asdf`). 8 | 9 | If you are running the demo on Linux, make sure to have the following dependencies installed in your system: 10 | - portaudio19-dev, 11 | - pkg-config 12 | 13 | On Ubuntu, you can install them with the following command: 14 | ```shell 15 | apt install portaudio19-dev pkg-config 16 | ``` 17 | 18 | ## Running the demo 19 | 20 | To run the demo, clone the `membrane_demo` repository and checkout to the demo directory: 21 | 22 | ```shell 23 | git clone https://github.com/membraneframework/membrane_demo 24 | cd membrane_demo/simple_pipeline 25 | ``` 26 | 27 | Then you need to download the dependencies of the mix project: 28 | 29 | ```shell 30 | mix deps.get 31 | ``` 32 | 33 | You may be asked to install `Hex` and then `rebar3`. 34 | 35 | To start the demo pipeline run `mix run --no-halt run.exs` or type the following commands into an IEx shell (started by `iex -S mix`): 36 | 37 | ```elixir 38 | {:ok, _supervisor, _pid} = Membrane.Pipeline.start_link(Membrane.Demo.SimplePipeline, "sample.mp3") 39 | ``` 40 | 41 | Should there be any errors when compiling the script's dependencies, you may need to install the some dependencies manually on your system: 42 | * [PortAudio](https://www.portaudio.com/) - which we use to play the audio 43 | * [FFmpeg](https://ffmpeg.org/) - which we use to resample the audio 44 | * [MAD](https://www.underbit.com/products/mad/) - which is used to decode audio 45 | 46 | 47 | ## Copyright and License 48 | 49 | Copyright 2018, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 50 | 51 | [![Software Mansion](https://membraneframework.github.io/static/logo/swm_logo_readme.png)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 52 | 53 | Licensed under the [Apache License, Version 2.0](LICENSE) 54 | -------------------------------------------------------------------------------- /mix_audio/mix_audio.exs: -------------------------------------------------------------------------------- 1 | Logger.configure(level: :info) 2 | 3 | Mix.install([ 4 | {:membrane_core, "~> 1.0"}, 5 | {:membrane_file_plugin, "~> 0.17.0"}, 6 | {:membrane_wav_plugin, "~> 0.10.1"}, 7 | {:membrane_audio_mix_plugin, "~> 0.16.0"}, 8 | {:membrane_aac_fdk_plugin, "0.18.8"} 9 | ]) 10 | 11 | defmodule MixAudio do 12 | @moduledoc """ 13 | Mix several .wav files into single .aac file. 14 | """ 15 | use Membrane.Pipeline 16 | 17 | @impl true 18 | def handle_init(_ctx, {wav1, wav2}) do 19 | # Setup the flow of the data 20 | spec = [ 21 | # Read & parse first WAV 22 | child(%Membrane.File.Source{location: wav1}) 23 | |> child(Membrane.WAV.Parser) 24 | |> get_child(:mixer), 25 | # Read & parse second WAV 26 | child(%Membrane.File.Source{location: wav2}) 27 | |> child(Membrane.WAV.Parser) 28 | # Offset audio by 2 seconds 29 | |> via_in(:input, options: [offset: Membrane.Time.seconds(2)]) 30 | |> get_child(:mixer), 31 | # Spawn the mixer and setup the audio format that it should operate on 32 | child(:mixer, %Membrane.AudioMixer{ 33 | stream_format: %Membrane.RawAudio{ 34 | channels: 1, 35 | sample_rate: 16_000, 36 | sample_format: :s16le 37 | } 38 | }) 39 | # Encode output to AAC 40 | |> child(Membrane.AAC.FDK.Encoder) 41 | # Save the output to file 42 | |> child(:sink, %Membrane.File.Sink{location: "output.aac"}) 43 | ] 44 | 45 | {[spec: spec], %{}} 46 | end 47 | 48 | @impl true 49 | def handle_element_end_of_stream(:sink, :input, _ctx, state) do 50 | {[terminate: :normal], state} 51 | end 52 | 53 | @impl true 54 | def handle_element_end_of_stream(_element, _pad, _ctx, state) do 55 | {[], state} 56 | end 57 | end 58 | 59 | {:ok, _supervisor, pipeline} = 60 | Membrane.Pipeline.start_link(MixAudio, {"sound_500f.wav", "sound_1000f.wav"}) 61 | 62 | Process.monitor(pipeline) 63 | 64 | # Wait for the pipeline to terminate 65 | receive do 66 | {:DOWN, _monitor, :process, ^pipeline, _reason} -> :ok 67 | end 68 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/assets/css/app.css: -------------------------------------------------------------------------------- 1 | /* This file is for your main application CSS */ 2 | @import "./phoenix.css"; 3 | @import "https://cdn.vidstack.io/player/theme.css"; 4 | @import "https://cdn.vidstack.io/player/video.css"; 5 | 6 | /* Alerts and form errors used by phx.new */ 7 | .alert { 8 | padding: 15px; 9 | margin-bottom: 20px; 10 | border: 1px solid transparent; 11 | border-radius: 4px; 12 | } 13 | .alert-info { 14 | color: #31708f; 15 | background-color: #d9edf7; 16 | border-color: #bce8f1; 17 | } 18 | .alert-warning { 19 | color: #8a6d3b; 20 | background-color: #fcf8e3; 21 | border-color: #faebcc; 22 | } 23 | .alert-danger { 24 | color: #a94442; 25 | background-color: #f2dede; 26 | border-color: #ebccd1; 27 | } 28 | .alert p { 29 | margin-bottom: 0; 30 | } 31 | .alert:empty { 32 | display: none; 33 | } 34 | .invalid-feedback { 35 | color: #a94442; 36 | display: block; 37 | margin: -1rem 0 2rem; 38 | } 39 | 40 | /* LiveView specific classes for your customization */ 41 | .phx-no-feedback.invalid-feedback, 42 | .phx-no-feedback .invalid-feedback { 43 | display: none; 44 | } 45 | 46 | .phx-click-loading { 47 | opacity: 0.5; 48 | transition: opacity 1s ease-out; 49 | } 50 | 51 | .phx-disconnected{ 52 | cursor: wait; 53 | } 54 | .phx-disconnected *{ 55 | pointer-events: none; 56 | } 57 | 58 | .phx-modal { 59 | opacity: 1!important; 60 | position: fixed; 61 | z-index: 1; 62 | left: 0; 63 | top: 0; 64 | width: 100%; 65 | height: 100%; 66 | overflow: auto; 67 | background-color: rgb(0,0,0); 68 | background-color: rgba(0,0,0,0.4); 69 | } 70 | 71 | .phx-modal-content { 72 | background-color: #fefefe; 73 | margin: 15vh auto; 74 | padding: 20px; 75 | border: 1px solid #888; 76 | width: 80%; 77 | } 78 | 79 | .phx-modal-close { 80 | color: #aaa; 81 | float: right; 82 | font-size: 28px; 83 | font-weight: bold; 84 | } 85 | 86 | .phx-modal-close:hover, 87 | .phx-modal-close:focus { 88 | color: black; 89 | text-decoration: none; 90 | cursor: pointer; 91 | } 92 | 93 | #player { 94 | width: 100%; 95 | height: auto; 96 | margin: auto auto; 97 | } 98 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # For production, don't forget to configure the url host 4 | # to something meaningful, Phoenix uses this information 5 | # when generating URLs. 6 | # 7 | # Note we also include the path to a cache manifest 8 | # containing the digested version of static files. This 9 | # manifest is generated by the `mix phx.digest` task, 10 | # which you should run after static files are built and 11 | # before starting your production server. 12 | config :rtmp_to_adaptive_hls, RtmpToAdaptiveHlsWeb.Endpoint, 13 | url: [host: "example.com", port: 80], 14 | cache_static_manifest: "priv/static/cache_manifest.json" 15 | 16 | # Do not print debug messages in production 17 | config :logger, level: :info 18 | 19 | # ## SSL Support 20 | # 21 | # To get SSL working, you will need to add the `https` key 22 | # to the previous section and set your `:url` port to 443: 23 | # 24 | # config :rtmp_to_adaptive_hls, RtmpToAdaptiveHlsWeb.Endpoint, 25 | # ..., 26 | # url: [host: "example.com", port: 443], 27 | # https: [ 28 | # ..., 29 | # port: 443, 30 | # cipher_suite: :strong, 31 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 32 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") 33 | # ] 34 | # 35 | # The `cipher_suite` is set to `:strong` to support only the 36 | # latest and more secure SSL ciphers. This means old browsers 37 | # and clients may not be supported. You can set it to 38 | # `:compatible` for wider support. 39 | # 40 | # `:keyfile` and `:certfile` expect an absolute path to the key 41 | # and cert in disk or a relative path inside priv, for example 42 | # "priv/ssl/server.key". For all supported SSL configuration 43 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 44 | # 45 | # We also recommend setting `force_ssl` in your endpoint, ensuring 46 | # no data is ever sent via http, always redirecting to https: 47 | # 48 | # config :rtmp_to_adaptive_hls, RtmpToAdaptiveHlsWeb.Endpoint, 49 | # force_ssl: [hsts: true] 50 | # 51 | # Check `Plug.SSL` for all available options in `force_ssl`. 52 | -------------------------------------------------------------------------------- /rtsp_to_hls/lib/pipeline.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Demo.RTSPToHLS.Pipeline do 2 | @moduledoc """ 3 | The pipeline which converts the stream to HLS. 4 | """ 5 | use Membrane.Pipeline 6 | 7 | require Logger 8 | 9 | @impl true 10 | def handle_init(_context, options) do 11 | spec = [ 12 | child(:source, %Membrane.RTSP.Source{ 13 | transport: {:udp, options.port, options.port + options.port_range_size}, 14 | allowed_media_types: [:video, :audio], 15 | stream_uri: options.stream_url, 16 | on_connection_closed: :send_eos 17 | }), 18 | child(:hls, %Membrane.HTTPAdaptiveStream.SinkBin{ 19 | target_window_duration: Membrane.Time.seconds(120), 20 | manifest_module: Membrane.HTTPAdaptiveStream.HLS, 21 | storage: %Membrane.HTTPAdaptiveStream.Storages.FileStorage{ 22 | directory: options.output_path 23 | } 24 | }) 25 | ] 26 | 27 | {[spec: spec], %{parent_pid: options.parent_pid}} 28 | end 29 | 30 | @impl true 31 | def handle_child_notification({:set_up_tracks, tracks}, :source, _ctx, state) do 32 | track_specs = 33 | Enum.uniq_by(tracks, & &1.type) 34 | |> Enum.filter(&(&1.type in [:audio, :video])) 35 | |> Enum.map(fn track -> 36 | encoding = 37 | case track do 38 | %{type: :audio} -> :AAC 39 | %{type: :video} -> :H264 40 | end 41 | 42 | get_child(:source) 43 | |> via_out(Pad.ref(:output, track.control_path)) 44 | |> via_in(:input, 45 | options: [encoding: encoding, segment_duration: Membrane.Time.seconds(4)] 46 | ) 47 | |> get_child(:hls) 48 | end) 49 | 50 | {[spec: track_specs], state} 51 | end 52 | 53 | @impl true 54 | def handle_child_notification({:track_playable, _ref}, :hls, _ctx, state) do 55 | send(state.parent_pid, :track_playable) 56 | {[], state} 57 | end 58 | 59 | @impl true 60 | def handle_child_notification(notification, _element, _ctx, state) do 61 | Logger.warning("Ignoring notification #{inspect(notification)}") 62 | {[], state} 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /webrtc_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 33 | .querySelector("meta[name='csrf-token']") 34 | .getAttribute("content"); 35 | let liveSocket = new LiveSocket("/live", Socket, { 36 | longPollFallbackMs: 2500, 37 | params: { _csrf_token: csrfToken }, 38 | hooks: hooks, 39 | }); 40 | 41 | // Show progress bar on live navigation and form submits 42 | topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" }); 43 | window.addEventListener("phx:page-loading-start", (_info) => topbar.show(300)); 44 | window.addEventListener("phx:page-loading-stop", (_info) => topbar.hide()); 45 | 46 | // connect if there are any LiveViews on the page 47 | liveSocket.connect(); 48 | 49 | // expose liveSocket on window for web console debug logs and latency simulation: 50 | // >> liveSocket.enableDebug() 51 | // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session 52 | // >> liveSocket.disableLatencySim() 53 | window.liveSocket = liveSocket; 54 | -------------------------------------------------------------------------------- /rtmp_to_hls/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule RtmpToHls.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :rtmp_to_hls, 7 | version: "0.1.0", 8 | elixir: "~> 1.12", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | compilers: Mix.compilers(), 11 | start_permanent: Mix.env() == :prod, 12 | aliases: aliases(), 13 | deps: deps() 14 | ] 15 | end 16 | 17 | # Configuration for the OTP application. 18 | # 19 | # Type `mix help compile.app` for more information. 20 | def application do 21 | [ 22 | mod: {RtmpToHls.Application, []}, 23 | extra_applications: [:logger, :runtime_tools, :inets] 24 | ] 25 | end 26 | 27 | # Specifies which paths to compile per environment. 28 | defp elixirc_paths(:test), do: ["lib", "test/support"] 29 | defp elixirc_paths(_), do: ["lib"] 30 | 31 | # Specifies your project dependencies. 32 | # 33 | # Type `mix help deps` for examples and options. 34 | defp deps do 35 | [ 36 | {:phoenix, "~> 1.6.0"}, 37 | {:phoenix_html, "~> 3.0"}, 38 | {:phoenix_live_reload, "~> 1.2", only: :dev}, 39 | {:phoenix_live_view, "~> 0.16.0"}, 40 | {:floki, ">= 0.30.0", only: :test}, 41 | {:esbuild, "~> 0.2", runtime: Mix.env() == :dev}, 42 | {:telemetry_metrics, "~> 0.6"}, 43 | {:telemetry_poller, "~> 1.0"}, 44 | {:telemetry, "~> 1.0", override: true}, 45 | {:gettext, "~> 0.18"}, 46 | {:jason, "~> 1.2"}, 47 | {:plug_cowboy, "~> 2.5"}, 48 | 49 | ## Membrane deps 50 | {:membrane_core, "~> 1.0"}, 51 | {:membrane_rtmp_plugin, "~> 0.23.2"}, 52 | {:membrane_http_adaptive_stream_plugin, "~> 0.18.4"} 53 | ] 54 | end 55 | 56 | # Aliases are shortcuts or tasks specific to the current project. 57 | # For example, to install project dependencies and perform other setup tasks, run: 58 | # 59 | # $ mix setup 60 | # 61 | # See the documentation for `Mix` for more info on aliases. 62 | defp aliases do 63 | [ 64 | setup: ["deps.get"], 65 | "assets.deploy": ["esbuild default --minify", "phx.digest"] 66 | ] 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /livebooks/playing_mp3_file/playing_mp3_file.livemd: -------------------------------------------------------------------------------- 1 | # Playing MP3 File 2 | 3 | ```elixir 4 | File.cd(__DIR__) 5 | Logger.configure(level: :error) 6 | 7 | Mix.install([ 8 | {:membrane_core, "~> 1.0"}, 9 | {:membrane_file_plugin, "~> 0.17.0"}, 10 | {:membrane_mp3_mad_plugin, "~> 0.18.3"}, 11 | {:membrane_ffmpeg_swresample_plugin, "~> 0.20.2"}, 12 | {:membrane_aac_fdk_plugin, "~> 0.18.8"}, 13 | {:membrane_kino_plugin, github: "membraneframework-labs/membrane_kino_plugin", tag: "v0.3.2"} 14 | ]) 15 | ``` 16 | 17 | ## Description 18 | 19 | This is example of loading `MP3` audio from the file, transcoding it to the `AAC` codec, and playing it via `Membrane.Kino.Player`. 20 | 21 | ## Pipeline definition 22 | 23 | Defines simple linear pipeline of the given structure: 24 | 25 | 1. Load `MP3` file from the file. 26 | 2. Transcode `MP3` to `AAC` (it is required by `Kino.Player`): 27 | 1. Decode `MP3` format to `RawAudio`, 28 | 2. Change `sample_format` from `s24le` to `s16le` (it is required by `FDK.Encoder`), 29 | 3. Encode it to `AAC` format. 30 | 3. Fill in audio stream to the player via `:audio` input pad. 31 | 32 | ```elixir 33 | import Membrane.ChildrenSpec, 34 | only: [{:child, 2}, {:child, 3}, {:via_in, 2}] 35 | 36 | alias Membrane.{ 37 | File, 38 | MP3, 39 | FFmpeg, 40 | RawAudio, 41 | AAC, 42 | Kino 43 | } 44 | 45 | audio_path = "./assets/sample.mp3" 46 | kino = Membrane.Kino.Player.new(audio: true) 47 | 48 | spec = 49 | child(:file_source, %File.Source{location: audio_path}) 50 | |> child(:decoder_mp3, MP3.MAD.Decoder) 51 | |> child(:converter, %FFmpeg.SWResample.Converter{ 52 | input_stream_format: %RawAudio{channels: 2, sample_format: :s24le, sample_rate: 48_000}, 53 | output_stream_format: %RawAudio{channels: 2, sample_format: :s16le, sample_rate: 44_100} 54 | }) 55 | |> child(:encoder_aac, AAC.FDK.Encoder) 56 | |> via_in(:audio) 57 | |> child(:player, %Kino.Player.Sink{kino: kino}) 58 | 59 | :ok 60 | ``` 61 | 62 | ## Player 63 | 64 | Run pipeline: 65 | 66 | ```elixir 67 | alias Membrane.RCPipeline, as: RC 68 | 69 | pipeline = RC.start!() 70 | RC.exec_actions(pipeline, spec: spec) 71 | 72 | kino 73 | ``` 74 | -------------------------------------------------------------------------------- /camera_to_hls/README.md: -------------------------------------------------------------------------------- 1 | # Camera feed to HLS 2 | 3 | This script demonstrates capturing camera video and broadcasting it via HLS. 4 | 5 | To run the demo, you need [Elixir installed](https://elixir-lang.org/install.html) on your machine (it's best to use a version manager, like `asdf`). Then, run 6 | 7 | ```bash 8 | elixir camera_to_hls.exs 9 | ``` 10 | 11 | and when it prints that playback is available, visit `http://localhost:8000/stream.html`. You should see the stream from the camera there. The stream can be also played with players other than the browser, like `vlc` or `ffplay`, for example 12 | 13 | ```bash 14 | ffplay http://localhost:8000/output/index.m3u8 15 | ``` 16 | 17 | Should there be any errors when compiling the script's dependencies, you may need to install [FFmpeg](https://ffmpeg.org/), which we use to encode the stream from the camera. 18 | 19 | 20 | You might be asked to grant access to your camera, as some operating systems require that. In case of the absence of a physical camera, it is necessary to use a virtual camera (e.g. OBS, [see how to set up the virtual camera in OBS](https://obsproject.com/kb/virtual-camera-guide)). 21 | 22 | For an example of serving HLS within a Phoenix project, see the [rtmp_to_hls demo](../rtmp_to_hls/). 23 | 24 |
25 | 26 | Debugging 27 | 28 | 29 | ### Camera not found 30 | If you have a camera but you still receiving the error 31 | ``` 32 | [video4linux2,v4l2 @ 0x745cf00b0440] Cannot open video device default: No such file or directory 33 | ``` 34 | The [Membrane CameraCapture Plugin](https://github.com/membraneframework/membrane_camera_capture_plugin) isn't able to find the default camera. 35 | You can find it manually with `v4l2-ctl --list-devices` and then manually set it on the CameraCapture Plugin. 36 | ```diff 37 | - child(:source, Membrane.CameraCapture) 38 | + child(:source, %Membrane.CameraCapture{device: "/dev/video0"}) 39 | ``` 40 |
41 | 42 | ## Copyright and License 43 | 44 | Copyright 2022, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 45 | 46 | [![Software Mansion](https://docs.membrane.stream/static/logo/swm_logo_readme.png)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 47 | 48 | Licensed under the [Apache License, Version 2.0](LICENSE) 49 | -------------------------------------------------------------------------------- /webrtc_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, "~> 0.25.3"}, 36 | {:membrane_transcoder_plugin, "~> 0.2.1"}, 37 | {:membrane_ffmpeg_swscale_plugin, "~> 0.16.2"}, 38 | {:evision, "~> 0.2.11"}, 39 | {:vix, "~> 0.33.0"}, 40 | {:image, "~> 0.59.0"}, 41 | {:phoenix, "~> 1.7.21"}, 42 | {:phoenix_html, "~> 4.1"}, 43 | {:phoenix_live_reload, "~> 1.2", only: :dev}, 44 | {:phoenix_live_view, "~> 1.0"}, 45 | {:floki, ">= 0.30.0", only: :test}, 46 | {:phoenix_live_dashboard, "~> 0.8.3"}, 47 | {:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, 48 | {:telemetry_metrics, "~> 1.0"}, 49 | {:telemetry_poller, "~> 1.0"}, 50 | {:jason, "~> 1.2"}, 51 | {:dns_cluster, "~> 0.1.1"}, 52 | {:bandit, "~> 1.5"} 53 | ] 54 | end 55 | 56 | # Aliases are shortcuts or tasks specific to the current project. 57 | # For example, to install project dependencies and perform other setup tasks, run: 58 | # 59 | # $ mix setup 60 | # 61 | # See the documentation for `Mix` for more info on aliases. 62 | defp aliases do 63 | [ 64 | setup: ["deps.get", "assets.setup", "assets.build"], 65 | "assets.setup": ["esbuild.install --if-missing"], 66 | "assets.build": ["esbuild webrtc_live_view"], 67 | "assets.deploy": [ 68 | "esbuild webrtc_live_view --minify", 69 | "phx.digest" 70 | ] 71 | ] 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /camera_to_hls_nerves/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule CameraToHlsNerves.MixProject do 2 | use Mix.Project 3 | 4 | @app :camera_to_hls_nerves 5 | @version "0.1.0" 6 | @all_targets [ 7 | :rpi4 8 | ] 9 | 10 | def project do 11 | [ 12 | app: @app, 13 | version: @version, 14 | elixir: "~> 1.11", 15 | archives: [nerves_bootstrap: "~> 1.12"], 16 | start_permanent: Mix.env() == :prod, 17 | deps: deps(), 18 | releases: [{@app, release()}], 19 | preferred_cli_target: [run: :host, test: :host] 20 | ] 21 | end 22 | 23 | # Run "mix help compile.app" to learn about applications. 24 | def application do 25 | [ 26 | mod: {CameraToHlsNerves.Application, []}, 27 | extra_applications: [:logger, :runtime_tools, :inets] 28 | ] 29 | end 30 | 31 | # Run "mix help deps" to learn about dependencies. 32 | defp deps do 33 | [ 34 | # Dependencies for all targets 35 | {:nerves, "~> 1.10", runtime: false}, 36 | {:shoehorn, "~> 0.9.1"}, 37 | {:ring_logger, "~> 0.10.0"}, 38 | {:toolshed, "~> 0.3.0"}, 39 | {:membrane_rpicam_plugin, "~> 0.1.5"}, 40 | {:membrane_h26x_plugin, "~> 0.10.1"}, 41 | {:membrane_http_adaptive_stream_plugin, "~> 0.18.4"}, 42 | 43 | # Allow Nerves.Runtime on host to support development, testing and CI. 44 | # See config/host.exs for usage. 45 | {:nerves_runtime, "~> 0.13.0"}, 46 | 47 | # Dependencies for all targets except :host 48 | {:nerves_pack, "~> 0.7.0", targets: @all_targets}, 49 | 50 | # Dependencies for specific targets 51 | # NOTE: It's generally low risk and recommended to follow minor version 52 | # bumps to Nerves systems. Since these include Linux kernel and Erlang 53 | # version updates, please review their release notes in case 54 | # changes to your application are needed. 55 | {:nerves_system_rpi4, "~> 1.24", runtime: false, targets: :rpi4} 56 | ] 57 | end 58 | 59 | def release do 60 | [ 61 | overwrite: true, 62 | # Erlang distribution is not started automatically. 63 | # See https://hexdocs.pm/nerves_pack/readme.html#erlang-distribution 64 | cookie: "#{@app}_cookie", 65 | include_erts: &Nerves.Release.erts/0, 66 | steps: [&Nerves.Release.init/1, :assemble], 67 | strip_beams: Mix.env() == :prod or [keep: ["Docs"]] 68 | ] 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule RtmpToAdaptiveHls.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :rtmp_to_adaptive_hls, 7 | version: "0.1.0", 8 | elixir: "~> 1.12", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | compilers: Mix.compilers(), 11 | start_permanent: Mix.env() == :prod, 12 | aliases: aliases(), 13 | deps: deps() 14 | ] 15 | end 16 | 17 | # Configuration for the OTP application. 18 | # 19 | # Type `mix help compile.app` for more information. 20 | def application do 21 | [ 22 | mod: {RtmpToAdaptiveHls.Application, []}, 23 | extra_applications: [:logger, :runtime_tools, :inets] 24 | ] 25 | end 26 | 27 | # Specifies which paths to compile per environment. 28 | defp elixirc_paths(:test), do: ["lib", "test/support"] 29 | defp elixirc_paths(_), do: ["lib"] 30 | 31 | # Specifies your project dependencies. 32 | # 33 | # Type `mix help deps` for examples and options. 34 | defp deps do 35 | [ 36 | {:phoenix, "~> 1.6.0"}, 37 | {:phoenix_html, "~> 3.0"}, 38 | {:phoenix_live_reload, "~> 1.2", only: :dev}, 39 | {:phoenix_live_view, "~> 0.16.0"}, 40 | {:floki, ">= 0.30.0", only: :test}, 41 | {:esbuild, "~> 0.2", runtime: Mix.env() == :dev}, 42 | {:telemetry_metrics, "~> 0.6"}, 43 | {:telemetry_poller, "~> 1.0"}, 44 | {:telemetry, "~> 1.0", override: true}, 45 | {:gettext, "~> 0.18"}, 46 | {:jason, "~> 1.2"}, 47 | {:plug_cowboy, "~> 2.5"}, 48 | 49 | ## Membrane deps 50 | {:membrane_core, "~> 1.0"}, 51 | {:membrane_framerate_converter_plugin, "~> 0.8.2"}, 52 | {:membrane_ffmpeg_swscale_plugin, "~> 0.15.1"}, 53 | {:membrane_h264_ffmpeg_plugin, "~> 0.31.6"}, 54 | {:membrane_http_adaptive_stream_plugin, "~> 0.18.5"}, 55 | {:membrane_rtmp_plugin, "~> 0.27.2"}, 56 | {:membrane_tee_plugin, "~> 0.12.0"} 57 | ] 58 | end 59 | 60 | # Aliases are shortcuts or tasks specific to the current project. 61 | # For example, to install project dependencies and perform other setup tasks, run: 62 | # 63 | # $ mix setup 64 | # 65 | # See the documentation for `Mix` for more info on aliases. 66 | defp aliases do 67 | [ 68 | setup: ["deps.get"], 69 | "assets.deploy": ["esbuild default --minify", "phx.digest"] 70 | ] 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /rtmp_to_hls/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 use it 8 | # with esbuild to bundle .js and .css sources. 9 | config :rtmp_to_hls, RtmpToHlsWeb.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: "RJ+YNDINYsAkM4evupc/vq5vMR9PPuJihcWgMB1DNMZYH+jrC3GcwpsLdi8mB61m", 17 | watchers: [ 18 | # Start the esbuild watcher by calling Esbuild.install_and_run(:default, args) 19 | esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]} 20 | ] 21 | 22 | # ## SSL Support 23 | # 24 | # In order to use HTTPS in development, a self-signed 25 | # certificate can be generated by running the following 26 | # Mix task: 27 | # 28 | # mix phx.gen.cert 29 | # 30 | # Note that this task requires Erlang/OTP 20 or later. 31 | # Run `mix help phx.gen.cert` for more information. 32 | # 33 | # The `http:` config above can be replaced with: 34 | # 35 | # https: [ 36 | # port: 4001, 37 | # cipher_suite: :strong, 38 | # keyfile: "priv/cert/selfsigned_key.pem", 39 | # certfile: "priv/cert/selfsigned.pem" 40 | # ], 41 | # 42 | # If desired, both `http:` and `https:` keys can be 43 | # configured to run both http and https servers on 44 | # different ports. 45 | 46 | # Watch static and templates for browser reloading. 47 | config :rtmp_to_hls, RtmpToHlsWeb.Endpoint, 48 | live_reload: [ 49 | patterns: [ 50 | ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", 51 | ~r"priv/gettext/.*(po)$", 52 | ~r"lib/rtmp_to_hls_web/(live|views)/.*(ex)$", 53 | ~r"lib/rtmp_to_hls_web/templates/.*(eex)$" 54 | ] 55 | ] 56 | 57 | # Do not include metadata nor timestamps in development logs 58 | config :logger, :console, format: "[$level] $message\n" 59 | 60 | # Set a higher stacktrace during development. Avoid configuring such 61 | # in production as building large stacktraces may be expensive. 62 | config :phoenix, :stacktrace_depth, 20 63 | 64 | # Initialize plugs at runtime for faster development compilation 65 | config :phoenix, :plug_init_mode, :runtime 66 | -------------------------------------------------------------------------------- /webrtc_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 | -------------------------------------------------------------------------------- /simple_element/lib/membrane_demo/simple_element/counter.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Demo.SimpleElement.Counter do 2 | @moduledoc """ 3 | Membrane element counting incoming buffers. 4 | 5 | Count of buffers divided by `divisor` (passed via `:input` pad options) 6 | is sent as a `{:counter, number}` notification once every `interval` 7 | (passed via element options). 8 | """ 9 | use Membrane.Filter 10 | 11 | def_options interval: [ 12 | spec: Membrane.Time.non_neg_t(), 13 | default: 1000, 14 | description: """ 15 | Amount of the time in milliseconds, telling how often 16 | the count of buffers should be sent and zeroed. 17 | """ 18 | ] 19 | 20 | def_input_pad :input, 21 | availability: :always, 22 | flow_control: :manual, 23 | demand_unit: :bytes, 24 | accepted_format: _any, 25 | options: [ 26 | divisor: [ 27 | type: :integer, 28 | default: 1, 29 | description: "Number by which the counter will be divided before sending notification" 30 | ] 31 | ] 32 | 33 | def_output_pad :output, 34 | availability: :always, 35 | flow_control: :manual, 36 | accepted_format: _any 37 | 38 | @impl true 39 | def handle_init(_ctx, %__MODULE{interval: interval}) do 40 | state = %{ 41 | interval: interval, 42 | counter: 0 43 | } 44 | 45 | {[], state} 46 | end 47 | 48 | @impl true 49 | def handle_terminate_request(_ctx, state) do 50 | {[stop_timer: :timer, terminate: :normal], %{state | counter: 0}} 51 | end 52 | 53 | @impl true 54 | def handle_playing(_ctx, state) do 55 | {[start_timer: {:timer, state.interval}], state} 56 | end 57 | 58 | @impl true 59 | def handle_demand(:output, size, :bytes, _context, state) do 60 | {[demand: {:input, size}], state} 61 | end 62 | 63 | @impl true 64 | def handle_buffer(:input, %Membrane.Buffer{} = buffer, _context, state) do 65 | state = %{state | counter: state.counter + 1} 66 | {[buffer: {:output, buffer}], state} 67 | end 68 | 69 | @impl true 70 | def handle_tick(:timer, ctx, state) do 71 | # create the term to send 72 | notification = { 73 | :counter, 74 | div(state.counter, ctx.pads.input.options.divisor) 75 | } 76 | 77 | # reset the counter 78 | new_state = %{state | counter: 0} 79 | 80 | {[notify_parent: notification], new_state} 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/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 use it 8 | # with esbuild to bundle .js and .css sources. 9 | config :rtmp_to_adaptive_hls, RtmpToAdaptiveHlsWeb.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: "RJ+YNDINYsAkM4evupc/vq5vMR9PPuJihcWgMB1DNMZYH+jrC3GcwpsLdi8mB61m", 17 | watchers: [ 18 | # Start the esbuild watcher by calling Esbuild.install_and_run(:default, args) 19 | esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]} 20 | ] 21 | 22 | # ## SSL Support 23 | # 24 | # In order to use HTTPS in development, a self-signed 25 | # certificate can be generated by running the following 26 | # Mix task: 27 | # 28 | # mix phx.gen.cert 29 | # 30 | # Note that this task requires Erlang/OTP 20 or later. 31 | # Run `mix help phx.gen.cert` for more information. 32 | # 33 | # The `http:` config above can be replaced with: 34 | # 35 | # https: [ 36 | # port: 4001, 37 | # cipher_suite: :strong, 38 | # keyfile: "priv/cert/selfsigned_key.pem", 39 | # certfile: "priv/cert/selfsigned.pem" 40 | # ], 41 | # 42 | # If desired, both `http:` and `https:` keys can be 43 | # configured to run both http and https servers on 44 | # different ports. 45 | 46 | # Watch static and templates for browser reloading. 47 | config :rtmp_to_adaptive_hls, RtmpToAdaptiveHlsWeb.Endpoint, 48 | live_reload: [ 49 | patterns: [ 50 | ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", 51 | ~r"priv/gettext/.*(po)$", 52 | ~r"lib/rtmp_to_adaptive_hls_web/(live|views)/.*(ex)$", 53 | ~r"lib/rtmp_to_adaptive_hls_web/templates/.*(eex)$" 54 | ] 55 | ] 56 | 57 | # Do not include metadata nor timestamps in development logs 58 | config :logger, :console, format: "[$level] $message\n" 59 | 60 | # Set a higher stacktrace during development. Avoid configuring such 61 | # in production as building large stacktraces may be expensive. 62 | config :phoenix, :stacktrace_depth, 20 63 | 64 | # Initialize plugs at runtime for faster development compilation 65 | config :phoenix, :plug_init_mode, :runtime 66 | -------------------------------------------------------------------------------- /rtmp_to_hls/assets/css/phoenix.css: -------------------------------------------------------------------------------- 1 | /* Includes some default style for the starter application. 2 | * This can be safely deleted to start fresh. 3 | */ 4 | 5 | /* Milligram v1.4.1 https://milligram.github.io 6 | * Copyright (c) 2020 CJ Patoilo Licensed under the MIT license 7 | */ 8 | 9 | html { 10 | background-color: #0b0c22; 11 | color: white; 12 | font-family: "Poppins", sans-serif; 13 | } 14 | 15 | a { 16 | text-decoration: none; 17 | color: inherit; 18 | } 19 | 20 | /* General style */ 21 | h1{font-size: 3.6rem; line-height: 1.25} 22 | h2{font-size: 2.8rem; line-height: 1.3} 23 | h3{font-size: 2.2rem; letter-spacing: -.08rem; line-height: 1.35} 24 | h4{font-size: 1.8rem; letter-spacing: -.05rem; line-height: 1.5} 25 | h5{font-size: 1.6rem; letter-spacing: 0; line-height: 1.4} 26 | h6{font-size: 1.4rem; letter-spacing: 0; line-height: 1.2} 27 | pre{padding: 1em;} 28 | 29 | .container{ 30 | margin: 0 auto; 31 | max-width: 80.0rem; 32 | padding: 0 2.0rem; 33 | position: relative; 34 | width: 100% 35 | } 36 | select { 37 | width: auto; 38 | } 39 | 40 | /* Phoenix promo and logo */ 41 | .phx-hero { 42 | text-align: center; 43 | border-bottom: 1px solid #e3e3e3; 44 | border-radius: 6px; 45 | padding: 3em 3em 1em; 46 | margin-bottom: 3rem; 47 | font-weight: 200; 48 | font-size: 120%; 49 | } 50 | .phx-hero input { 51 | background: #ffffff; 52 | } 53 | .phx-logo { 54 | min-width: 300px; 55 | margin: 1rem; 56 | display: block; 57 | } 58 | .phx-logo img { 59 | width: auto; 60 | display: block; 61 | } 62 | 63 | /* Headers */ 64 | header { 65 | width: 100%; 66 | margin-bottom: 2rem; 67 | } 68 | header section { 69 | align-items: center; 70 | display: flex; 71 | flex-direction: column; 72 | justify-content: space-between; 73 | } 74 | header section :first-child { 75 | order: 2; 76 | } 77 | header section :last-child { 78 | order: 1; 79 | } 80 | header nav ul, 81 | header nav li { 82 | margin: 0; 83 | padding: 0; 84 | display: block; 85 | text-align: right; 86 | white-space: nowrap; 87 | } 88 | header nav ul { 89 | margin: 1rem; 90 | margin-top: 0; 91 | } 92 | header nav a { 93 | display: block; 94 | } 95 | 96 | @media (min-width: 40.0rem) { /* Small devices (landscape phones, 576px and up) */ 97 | header section { 98 | flex-direction: row; 99 | } 100 | header nav ul { 101 | margin: 1rem; 102 | } 103 | .phx-logo { 104 | flex-basis: 527px; 105 | margin: 2rem 1rem; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/assets/css/phoenix.css: -------------------------------------------------------------------------------- 1 | /* Includes some default style for the starter application. 2 | * This can be safely deleted to start fresh. 3 | */ 4 | 5 | /* Milligram v1.4.1 https://milligram.github.io 6 | * Copyright (c) 2020 CJ Patoilo Licensed under the MIT license 7 | */ 8 | 9 | html { 10 | background-color: #0b0c22; 11 | color: white; 12 | font-family: "Poppins", sans-serif; 13 | } 14 | 15 | a { 16 | text-decoration: none; 17 | color: inherit; 18 | } 19 | 20 | /* General style */ 21 | h1{font-size: 3.6rem; line-height: 1.25} 22 | h2{font-size: 2.8rem; line-height: 1.3} 23 | h3{font-size: 2.2rem; letter-spacing: -.08rem; line-height: 1.35} 24 | h4{font-size: 1.8rem; letter-spacing: -.05rem; line-height: 1.5} 25 | h5{font-size: 1.6rem; letter-spacing: 0; line-height: 1.4} 26 | h6{font-size: 1.4rem; letter-spacing: 0; line-height: 1.2} 27 | pre{padding: 1em;} 28 | 29 | .container{ 30 | margin: 0 auto; 31 | max-width: 80.0rem; 32 | padding: 0 2.0rem; 33 | position: relative; 34 | width: 100% 35 | } 36 | select { 37 | width: auto; 38 | } 39 | 40 | /* Phoenix promo and logo */ 41 | .phx-hero { 42 | text-align: center; 43 | border-bottom: 1px solid #e3e3e3; 44 | border-radius: 6px; 45 | padding: 3em 3em 1em; 46 | margin-bottom: 3rem; 47 | font-weight: 200; 48 | font-size: 120%; 49 | } 50 | .phx-hero input { 51 | background: #ffffff; 52 | } 53 | .phx-logo { 54 | min-width: 300px; 55 | margin: 1rem; 56 | display: block; 57 | } 58 | .phx-logo img { 59 | width: auto; 60 | display: block; 61 | } 62 | 63 | /* Headers */ 64 | header { 65 | width: 100%; 66 | margin-bottom: 2rem; 67 | } 68 | header section { 69 | align-items: center; 70 | display: flex; 71 | flex-direction: column; 72 | justify-content: space-between; 73 | } 74 | header section :first-child { 75 | order: 2; 76 | } 77 | header section :last-child { 78 | order: 1; 79 | } 80 | header nav ul, 81 | header nav li { 82 | margin: 0; 83 | padding: 0; 84 | display: block; 85 | text-align: right; 86 | white-space: nowrap; 87 | } 88 | header nav ul { 89 | margin: 1rem; 90 | margin-top: 0; 91 | } 92 | header nav a { 93 | display: block; 94 | } 95 | 96 | @media (min-width: 40.0rem) { /* Small devices (landscape phones, 576px and up) */ 97 | header section { 98 | flex-direction: row; 99 | } 100 | header nav ul { 101 | margin: 1rem; 102 | } 103 | .phx-logo { 104 | flex-basis: 527px; 105 | margin: 2rem 1rem; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /simple_element/README.md: -------------------------------------------------------------------------------- 1 | # Simple Membrane Element 2 | 3 | This demo shows how to create a simple Membrane element and plug it into a pipeline. 4 | 5 | ## Prerequisites 6 | 7 | To run the demo, you need [Elixir installed](https://elixir-lang.org/install.html) on your machine (it's best to use a version manager, like `asdf`). 8 | 9 | If you are running the demo on Linux, make sure to have the following dependencies installed in your system: 10 | - portaudio19-dev, 11 | - pkg-config 12 | 13 | On Ubuntu, you can install them with the following command: 14 | ```shell 15 | apt install portaudio19-dev pkg-config 16 | ``` 17 | 18 | ## Running the demo 19 | 20 | To run the demo, clone the `membrane_demo` repository and checkout to the demo directory: 21 | 22 | ```shell 23 | git clone https://github.com/membraneframework/membrane_demo 24 | cd membrane_demo/simple_element 25 | ``` 26 | 27 | Then you need to download the dependencies of the mix project: 28 | 29 | ```shell 30 | mix deps.get 31 | ``` 32 | 33 | You may be asked to install `Hex` and then `rebar3`. 34 | 35 | To start the demo pipeline run `mix run --no-halt run.exs` or type the following commands into an IEx shell (started by `iex -S mix`): 36 | 37 | ```elixir 38 | Membrane.Pipeline.start_link(Membrane.Demo.SimpleElement.Pipeline, "sample.mp3") 39 | ``` 40 | 41 | You should hear the audio sample playing and see the number of buffers processed being periodically printed to the console. 42 | 43 | Should there be any errors when compiling the script's dependencies, you may need to install the some dependencies manually on your system: 44 | * [PortAudio](https://www.portaudio.com/) - which we use to play the audio 45 | * [FFmpeg](https://ffmpeg.org/) - which we use to resample the audio 46 | * [MAD](https://www.underbit.com/products/mad/) - which is used to decode audio 47 | 48 | ## How it works 49 | 50 | The pipeline takes a sample mp3 file, decodes it, and plays the audio. 51 | The simple `counter` element is responsible for counting the number of buffers 52 | passing through it and periodically prints the number of buffers processed to the console. 53 | 54 | The element is plugged in just before the audio player element in the pipeline. 55 | 56 | ## Copyright and License 57 | 58 | Copyright 2018, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 59 | 60 | [![Software Mansion](https://membraneframework.github.io/static/logo/swm_logo_readme.png)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 61 | 62 | Licensed under the [Apache License, Version 2.0](LICENSE) 63 | -------------------------------------------------------------------------------- /webrtc_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 | -------------------------------------------------------------------------------- /rtsp_to_hls/README.md: -------------------------------------------------------------------------------- 1 | # RTSP to HLS 2 | 3 | This demo demonstrates receiving the RTSP stream and converting it to the HLS stream. 4 | 5 | ## Prerequisites and running the demo 6 | 7 | Below is the instruction for the installation of required dependencies and how to run this demo on various operating systems: 8 | 9 | ### Prerequisites 10 | Make sure you have Elixir installed on your machine. For installation details, see: https://elixir-lang.org/install.html 11 | 12 | ### Running the demo 13 | 14 | To run the demo, clone the `membrane_demo` repository and checkout to the demo directory: 15 | 16 | ```shell 17 | git clone https://github.com/membraneframework/membrane_demo 18 | cd membrane_demo/rtsp_to_hls 19 | ``` 20 | 21 | Then you need to download the dependencies of the mix project: 22 | 23 | ```shell 24 | mix deps.get 25 | ``` 26 | You may be asked to install `Hex` and then `rebar3`. 27 | 28 | 29 | Now you need to compile the project: 30 | 31 | ```shell 32 | mix compile 33 | ``` 34 | 35 | ### Running simple RTSP server 36 | We will use a custom implementation of a simple RTSP server that you will be able to run with the `server.exs` script. 37 | That script will setup a server listening for RTSP connection on `rtsp://localhost:8554`. 38 | 39 | You can run the server with the following command: 40 | ```shell 41 | mix run server.exs 42 | ``` 43 | 44 | ### Running RTSP to HLS conversion 45 | After you have launched the RTSP server, you can run the pipeline that performs RTSP to HLS conversion: 46 | 47 | ```shell 48 | mix run rtsp_to_hls.exs 49 | ``` 50 | 51 | By default the pipeline is configured to connect to the RTSP server launched in the previous step of this manual. 52 | You can change the default pipeline configuration in the `rtsp_to_hls.exs` script implementation by modification of the following 53 | attributes: 54 | 55 | ##### rtsp_to_hls.exs 56 | 57 | ```elixir 58 | rtsp_stream_url = "rtsp://localhost:8554/livestream" 59 | output_path = "hls_output" 60 | rtp_port = 20000 61 | ``` 62 | 63 | After a moment the pipeline will start generating HLS output files (by default in the `hls_output` directory). 64 | Once that happens, an HTTP server is started and now you can watch the stream by visiting the following page in your browser: 65 | ``` 66 | http://localhost:8000/stream.html 67 | ``` 68 | 69 | Copyright 2022, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 70 | 71 | [![Software Mansion](https://membraneframework.github.io/static/logo/swm_logo_readme.png)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 72 | 73 | Licensed under the [Apache License, Version 2.0](LICENSE) 74 | -------------------------------------------------------------------------------- /simple_element/.gitignore: -------------------------------------------------------------------------------- 1 | compile_commands.json 2 | .elixir_ls 3 | 4 | # Created by https://www.gitignore.io/api/c,vim,linux,macos,elixir,windows,visualstudiocode 5 | 6 | bundlex.sh 7 | bundlex.bat 8 | 9 | ### C ### 10 | # Prerequisites 11 | *.d 12 | 13 | # Object files 14 | *.o 15 | *.ko 16 | *.obj 17 | *.elf 18 | 19 | # Linker output 20 | *.ilk 21 | *.map 22 | *.exp 23 | 24 | # Precompiled Headers 25 | *.gch 26 | *.pch 27 | 28 | # Libraries 29 | *.lib 30 | *.a 31 | *.la 32 | *.lo 33 | 34 | # Shared objects (inc. Windows DLLs) 35 | *.dll 36 | *.so 37 | *.so.* 38 | *.dylib 39 | 40 | # Executables 41 | *.exe 42 | *.out 43 | *.app 44 | *.i*86 45 | *.x86_64 46 | *.hex 47 | 48 | # Debug files 49 | *.dSYM/ 50 | *.su 51 | *.idb 52 | *.pdb 53 | 54 | # Kernel Module Compile Results 55 | *.mod* 56 | *.cmd 57 | .tmp_versions/ 58 | modules.order 59 | Module.symvers 60 | Mkfile.old 61 | dkms.conf 62 | 63 | ### Elixir ### 64 | /_build 65 | /cover 66 | /deps 67 | /doc 68 | /.fetch 69 | erl_crash.dump 70 | *.ez 71 | *.beam 72 | 73 | ### Elixir Patch ### 74 | ### Linux ### 75 | *~ 76 | 77 | # temporary files which can be created if a process still has a handle open of a deleted file 78 | .fuse_hidden* 79 | 80 | # KDE directory preferences 81 | .directory 82 | 83 | # Linux trash folder which might appear on any partition or disk 84 | .Trash-* 85 | 86 | # .nfs files are created when an open file is removed but is still being accessed 87 | .nfs* 88 | 89 | ### macOS ### 90 | *.DS_Store 91 | .AppleDouble 92 | .LSOverride 93 | 94 | # Icon must end with two \r 95 | Icon 96 | 97 | # Thumbnails 98 | ._* 99 | 100 | # Files that might appear in the root of a volume 101 | .DocumentRevisions-V100 102 | .fseventsd 103 | .Spotlight-V100 104 | .TemporaryItems 105 | .Trashes 106 | .VolumeIcon.icns 107 | .com.apple.timemachine.donotpresent 108 | 109 | # Directories potentially created on remote AFP share 110 | .AppleDB 111 | .AppleDesktop 112 | Network Trash Folder 113 | Temporary Items 114 | .apdisk 115 | 116 | ### Vim ### 117 | # swap 118 | .sw[a-p] 119 | .*.sw[a-p] 120 | # session 121 | Session.vim 122 | # temporary 123 | .netrwhist 124 | # auto-generated tag files 125 | tags 126 | 127 | ### VisualStudioCode ### 128 | .vscode/* 129 | !.vscode/settings.json 130 | !.vscode/tasks.json 131 | !.vscode/launch.json 132 | !.vscode/extensions.json 133 | .history 134 | 135 | ### Windows ### 136 | # Windows thumbnail cache files 137 | Thumbs.db 138 | ehthumbs.db 139 | ehthumbs_vista.db 140 | 141 | # Folder config file 142 | Desktop.ini 143 | 144 | # Recycle Bin used on file shares 145 | $RECYCLE.BIN/ 146 | 147 | # Windows Installer files 148 | *.cab 149 | *.msi 150 | *.msm 151 | *.msp 152 | 153 | # Windows shortcuts 154 | *.lnk 155 | 156 | 157 | # End of https://www.gitignore.io/api/c,vim,linux,macos,elixir,windows,visualstudiocode 158 | -------------------------------------------------------------------------------- /simple_pipeline/.gitignore: -------------------------------------------------------------------------------- 1 | compile_commands.json 2 | .elixir_ls 3 | 4 | # Created by https://www.gitignore.io/api/c,vim,linux,macos,elixir,windows,visualstudiocode 5 | 6 | bundlex.sh 7 | bundlex.bat 8 | 9 | ### C ### 10 | # Prerequisites 11 | *.d 12 | 13 | # Object files 14 | *.o 15 | *.ko 16 | *.obj 17 | *.elf 18 | 19 | # Linker output 20 | *.ilk 21 | *.map 22 | *.exp 23 | 24 | # Precompiled Headers 25 | *.gch 26 | *.pch 27 | 28 | # Libraries 29 | *.lib 30 | *.a 31 | *.la 32 | *.lo 33 | 34 | # Shared objects (inc. Windows DLLs) 35 | *.dll 36 | *.so 37 | *.so.* 38 | *.dylib 39 | 40 | # Executables 41 | *.exe 42 | *.out 43 | *.app 44 | *.i*86 45 | *.x86_64 46 | *.hex 47 | 48 | # Debug files 49 | *.dSYM/ 50 | *.su 51 | *.idb 52 | *.pdb 53 | 54 | # Kernel Module Compile Results 55 | *.mod* 56 | *.cmd 57 | .tmp_versions/ 58 | modules.order 59 | Module.symvers 60 | Mkfile.old 61 | dkms.conf 62 | 63 | ### Elixir ### 64 | /_build 65 | /cover 66 | /deps 67 | /doc 68 | /.fetch 69 | erl_crash.dump 70 | *.ez 71 | *.beam 72 | 73 | ### Elixir Patch ### 74 | ### Linux ### 75 | *~ 76 | 77 | # temporary files which can be created if a process still has a handle open of a deleted file 78 | .fuse_hidden* 79 | 80 | # KDE directory preferences 81 | .directory 82 | 83 | # Linux trash folder which might appear on any partition or disk 84 | .Trash-* 85 | 86 | # .nfs files are created when an open file is removed but is still being accessed 87 | .nfs* 88 | 89 | ### macOS ### 90 | *.DS_Store 91 | .AppleDouble 92 | .LSOverride 93 | 94 | # Icon must end with two \r 95 | Icon 96 | 97 | # Thumbnails 98 | ._* 99 | 100 | # Files that might appear in the root of a volume 101 | .DocumentRevisions-V100 102 | .fseventsd 103 | .Spotlight-V100 104 | .TemporaryItems 105 | .Trashes 106 | .VolumeIcon.icns 107 | .com.apple.timemachine.donotpresent 108 | 109 | # Directories potentially created on remote AFP share 110 | .AppleDB 111 | .AppleDesktop 112 | Network Trash Folder 113 | Temporary Items 114 | .apdisk 115 | 116 | ### Vim ### 117 | # swap 118 | .sw[a-p] 119 | .*.sw[a-p] 120 | # session 121 | Session.vim 122 | # temporary 123 | .netrwhist 124 | # auto-generated tag files 125 | tags 126 | 127 | ### VisualStudioCode ### 128 | .vscode/* 129 | !.vscode/settings.json 130 | !.vscode/tasks.json 131 | !.vscode/launch.json 132 | !.vscode/extensions.json 133 | .history 134 | 135 | ### Windows ### 136 | # Windows thumbnail cache files 137 | Thumbs.db 138 | ehthumbs.db 139 | ehthumbs_vista.db 140 | 141 | # Folder config file 142 | Desktop.ini 143 | 144 | # Recycle Bin used on file shares 145 | $RECYCLE.BIN/ 146 | 147 | # Windows Installer files 148 | *.cab 149 | *.msi 150 | *.msm 151 | *.msp 152 | 153 | # Windows shortcuts 154 | *.lnk 155 | 156 | 157 | # End of https://www.gitignore.io/api/c,vim,linux,macos,elixir,windows,visualstudiocode 158 | -------------------------------------------------------------------------------- /camera_to_hls/camera_to_hls.exs: -------------------------------------------------------------------------------- 1 | require Logger 2 | Logger.configure(level: :info) 3 | 4 | Mix.install([ 5 | {:membrane_core, "~> 1.0"}, 6 | {:membrane_camera_capture_plugin, "~> 0.7.2"}, 7 | {:membrane_ffmpeg_swscale_plugin, "~> 0.15.1"}, 8 | {:membrane_h264_ffmpeg_plugin, "~> 0.31.6"}, 9 | {:membrane_http_adaptive_stream_plugin, "~> 0.18.4"} 10 | ]) 11 | 12 | defmodule CameraToHls do 13 | use Membrane.Pipeline 14 | 15 | @impl true 16 | def handle_init(_ctx, _opts) do 17 | # Captures video from the camera (raw video, depending on camera/os) 18 | spec = 19 | child(:source, Membrane.CameraCapture) 20 | # Converts pixel format to I420 (this is still a raw video) 21 | |> child(:converter, %Membrane.FFmpeg.SWScale.PixelFormatConverter{format: :I420}) 22 | # Takes raw video in I420 pixel format and encodes it into H264. 23 | # The baseline profile is usually most suitable for live streaming 24 | |> child(:encoder, %Membrane.H264.FFmpeg.Encoder{profile: :baseline}) 25 | # Muxes H264 into CMAF (short, mp4-like chunks) and generates 26 | # an HLS playlist. 27 | |> via_in(:input, 28 | options: [ 29 | encoding: :H264, 30 | track_name: "my_track", 31 | segment_duration: Membrane.Time.seconds(5) 32 | ] 33 | ) 34 | |> child(:hls_sink, %Membrane.HTTPAdaptiveStream.SinkBin{ 35 | manifest_module: Membrane.HTTPAdaptiveStream.HLS, 36 | storage: %Membrane.HTTPAdaptiveStream.Storages.FileStorage{directory: "output"}, 37 | target_window_duration: Membrane.Time.seconds(10) 38 | }) 39 | 40 | {[spec: spec], %{}} 41 | end 42 | 43 | @impl true 44 | def handle_child_notification({:track_playable, _track_info}, :hls_sink, _context, state) do 45 | send(:script, :playlist_ready) 46 | {[], state} 47 | end 48 | 49 | @impl true 50 | def handle_child_notification(_notification, _child, _context, state) do 51 | {[], state} 52 | end 53 | end 54 | 55 | # On CI we just check if the script compiles 56 | if System.get_env("CI") == "true" do 57 | Logger.info("CI=true, exiting") 58 | exit(:normal) 59 | end 60 | 61 | File.rm_rf!("output") 62 | File.mkdir!("output") 63 | 64 | Process.register(self(), :script) 65 | 66 | Logger.info("Starting the pipeline") 67 | {:ok, _supervisor, _pipeline} = Membrane.Pipeline.start_link(CameraToHls) 68 | 69 | Logger.info("Waiting for the playlist to be ready") 70 | 71 | receive do 72 | :playlist_ready -> :ok 73 | end 74 | 75 | Logger.info("Starting HTTP server") 76 | :ok = :inets.start() 77 | 78 | {:ok, _server} = 79 | :inets.start(:httpd, 80 | bind_address: ~c"localhost", 81 | port: 8000, 82 | document_root: ~c".", 83 | server_name: ~c"camera_to_hls", 84 | server_root: "/tmp" 85 | ) 86 | 87 | Logger.info("Playback available at http://localhost:8000/stream.html") 88 | 89 | Process.sleep(:infinity) 90 | -------------------------------------------------------------------------------- /rtmp_to_hls/lib/rtmp_to_hls_web.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpToHlsWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, views, channels and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use RtmpToHlsWeb, :controller 9 | use RtmpToHlsWeb, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, 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 any helper function in modules 17 | and import those modules here. 18 | """ 19 | 20 | def controller do 21 | quote do 22 | use Phoenix.Controller, namespace: RtmpToHlsWeb 23 | 24 | import Plug.Conn 25 | import RtmpToHlsWeb.Gettext 26 | alias RtmpToHlsWeb.Router.Helpers, as: Routes 27 | end 28 | end 29 | 30 | def view do 31 | quote do 32 | use Phoenix.View, 33 | root: "lib/rtmp_to_hls_web/templates", 34 | namespace: RtmpToHlsWeb 35 | 36 | # Import convenience functions from controllers 37 | import Phoenix.Controller, 38 | only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1] 39 | 40 | # Include shared imports and aliases for views 41 | unquote(view_helpers()) 42 | end 43 | end 44 | 45 | def live_view do 46 | quote do 47 | use Phoenix.LiveView, 48 | layout: {RtmpToHlsWeb.LayoutView, "live.html"} 49 | 50 | unquote(view_helpers()) 51 | end 52 | end 53 | 54 | def live_component do 55 | quote do 56 | use Phoenix.LiveComponent 57 | 58 | unquote(view_helpers()) 59 | end 60 | end 61 | 62 | def router do 63 | quote do 64 | use Phoenix.Router 65 | 66 | import Plug.Conn 67 | import Phoenix.Controller 68 | import Phoenix.LiveView.Router 69 | end 70 | end 71 | 72 | def channel do 73 | quote do 74 | use Phoenix.Channel 75 | import RtmpToHlsWeb.Gettext 76 | end 77 | end 78 | 79 | defp view_helpers do 80 | quote do 81 | # Use all HTML functionality (forms, tags, etc) 82 | use Phoenix.HTML 83 | 84 | # Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc) 85 | import Phoenix.LiveView.Helpers 86 | 87 | # Import basic rendering functionality (render, render_layout, etc) 88 | import Phoenix.View 89 | 90 | import RtmpToHlsWeb.ErrorHelpers 91 | import RtmpToHlsWeb.Gettext 92 | alias RtmpToHlsWeb.Router.Helpers, as: Routes 93 | end 94 | end 95 | 96 | @doc """ 97 | When used, dispatch to the appropriate controller/view/etc. 98 | """ 99 | defmacro __using__(which) when is_atom(which) do 100 | apply(__MODULE__, which, []) 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /rtmp_to_adaptive_hls/lib/rtmp_to_adaptive_hls_web.ex: -------------------------------------------------------------------------------- 1 | defmodule RtmpToAdaptiveHlsWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, views, channels and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use RtmpToAdaptiveHlsWeb, :controller 9 | use RtmpToAdaptiveHlsWeb, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, 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 any helper function in modules 17 | and import those modules here. 18 | """ 19 | 20 | def controller do 21 | quote do 22 | use Phoenix.Controller, namespace: RtmpToAdaptiveHlsWeb 23 | 24 | import Plug.Conn 25 | import RtmpToAdaptiveHlsWeb.Gettext 26 | alias RtmpToAdaptiveHlsWeb.Router.Helpers, as: Routes 27 | end 28 | end 29 | 30 | def view do 31 | quote do 32 | use Phoenix.View, 33 | root: "lib/rtmp_to_adaptive_hls_web/templates", 34 | namespace: RtmpToAdaptiveHlsWeb 35 | 36 | # Import convenience functions from controllers 37 | import Phoenix.Controller, 38 | only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1] 39 | 40 | # Include shared imports and aliases for views 41 | unquote(view_helpers()) 42 | end 43 | end 44 | 45 | def live_view do 46 | quote do 47 | use Phoenix.LiveView, 48 | layout: {RtmpToAdaptiveHlsWeb.LayoutView, "live.html"} 49 | 50 | unquote(view_helpers()) 51 | end 52 | end 53 | 54 | def live_component do 55 | quote do 56 | use Phoenix.LiveComponent 57 | 58 | unquote(view_helpers()) 59 | end 60 | end 61 | 62 | def router do 63 | quote do 64 | use Phoenix.Router 65 | 66 | import Plug.Conn 67 | import Phoenix.Controller 68 | import Phoenix.LiveView.Router 69 | end 70 | end 71 | 72 | def channel do 73 | quote do 74 | use Phoenix.Channel 75 | import RtmpToAdaptiveHlsWeb.Gettext 76 | end 77 | end 78 | 79 | defp view_helpers do 80 | quote do 81 | # Use all HTML functionality (forms, tags, etc) 82 | use Phoenix.HTML 83 | 84 | # Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc) 85 | import Phoenix.LiveView.Helpers 86 | 87 | # Import basic rendering functionality (render, render_layout, etc) 88 | import Phoenix.View 89 | 90 | import RtmpToAdaptiveHlsWeb.ErrorHelpers 91 | import RtmpToAdaptiveHlsWeb.Gettext 92 | alias RtmpToAdaptiveHlsWeb.Router.Helpers, as: Routes 93 | end 94 | end 95 | 96 | @doc """ 97 | When used, dispatch to the appropriate controller/view/etc. 98 | """ 99 | defmacro __using__(which) when is_atom(which) do 100 | apply(__MODULE__, which, []) 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /webrtc_live_view/priv/static/images/logo.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /rtp_to_hls/send.exs: -------------------------------------------------------------------------------- 1 | require Logger 2 | Logger.configure(level: :info) 3 | 4 | Mix.install([ 5 | {:membrane_core, "~> 1.0"}, 6 | {:membrane_hackney_plugin, "~> 0.11.0"}, 7 | {:membrane_realtimer_plugin, "~> 0.9.0"}, 8 | {:membrane_h26x_plugin, "~> 0.10.0"}, 9 | {:membrane_funnel_plugin, "~> 0.9.0"}, 10 | {:membrane_mp4_plugin, "~> 0.34.1"}, 11 | {:membrane_rtp_plugin, "~> 0.31.0"}, 12 | {:membrane_rtp_h264_plugin, "~> 0.20.0"}, 13 | {:membrane_rtp_aac_plugin, "~> 0.9.1"}, 14 | {:membrane_udp_plugin, "~> 0.14.0"}, 15 | {:membrane_aac_plugin, "~> 0.19.0"} 16 | ]) 17 | 18 | defmodule SendRTP do 19 | use Membrane.Pipeline 20 | 21 | @samples_url "https://raw.githubusercontent.com/membraneframework/static/gh-pages/samples/big-buck-bunny/" 22 | 23 | @mp4_url @samples_url <> "bun33s.mp4" 24 | 25 | @impl true 26 | def handle_init(_ctx, _opts) do 27 | spec = 28 | child(:video_src, %Membrane.Hackney.Source{ 29 | location: @mp4_url, 30 | hackney_opts: [follow_redirect: true] 31 | }) 32 | |> child(:mp4, Membrane.MP4.Demuxer.ISOM) 33 | 34 | {[spec: spec], %{}} 35 | end 36 | 37 | @impl true 38 | def handle_child_notification({:new_tracks, tracks}, :mp4, _ctx, state) do 39 | audio_ssrc = 1234 40 | video_ssrc = 1235 41 | {audio_id, _format} = Enum.find(tracks, fn {_id, %format{}} -> format == Membrane.AAC end) 42 | {video_id, _format} = Enum.find(tracks, fn {_id, %format{}} -> format == Membrane.H264 end) 43 | 44 | spec = [ 45 | get_child(:mp4) 46 | |> via_out(Pad.ref(:output, video_id)) 47 | |> child(:video_parser, %Membrane.H264.Parser{ 48 | output_stream_structure: :annexb, 49 | output_alignment: :nalu 50 | }) 51 | |> child(:video_realtimer, Membrane.Realtimer) 52 | |> child(:video_payloader, Membrane.RTP.H264.Payloader) 53 | |> child(:rtp_muxer, Membrane.RTP.Muxer) 54 | |> child(:udp, %Membrane.UDP.Sink{ 55 | destination_port_no: 5000, 56 | destination_address: {127, 0, 0, 1} 57 | }), 58 | get_child(:mp4) 59 | |> via_out(Pad.ref(:output, audio_id)) 60 | |> child(:audio_parser, %Membrane.AAC.Parser{out_encapsulation: :none}) 61 | |> child(:audio_realtimer, Membrane.Realtimer) 62 | |> child(:audio_payloader, %Membrane.RTP.AAC.Payloader{frames_per_packet: 1, mode: :hbr}) 63 | |> get_child(:rtp_muxer) 64 | ] 65 | 66 | {[spec: spec], state} 67 | end 68 | 69 | @impl true 70 | def handle_child_notification(_notification, _child, _ctx, state) do 71 | {[], state} 72 | end 73 | 74 | @impl true 75 | def handle_element_end_of_stream(:udp, :input, _ctx, state) do 76 | {[terminate: :normal], state} 77 | end 78 | 79 | @impl true 80 | def handle_element_end_of_stream(_element, _pad, _ctx, state) do 81 | {[], state} 82 | end 83 | end 84 | 85 | {:ok, supervisor, _pipeline} = Membrane.Pipeline.start_link(SendRTP) 86 | 87 | monitor = Process.monitor(supervisor) 88 | 89 | receive do 90 | {:DOWN, ^monitor, _kind, _pid, :normal} -> :ok 91 | end 92 | -------------------------------------------------------------------------------- /webrtc_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 | -------------------------------------------------------------------------------- /rtmp_to_hls/README.md: -------------------------------------------------------------------------------- 1 | # RTMP to HLS 2 | 3 | This demo is an application that is capable of receiving an RTMP stream, converting it to HLS, and publishing via an HTTP server so that it can be played with a web player. 4 | 5 | ## Possible use cases 6 | 7 | The application presented in this demo could be used in the following scenario. 8 | There is one person, the so-called "streamer", and multiple "viewers", who want to see the stream of multimedia sent by the streamer. 9 | The streamer sends multimedia using RTMP, a protocol supported by popular streaming software (i.e. OBS). Such an RTMP stream is then converted into HLS and published by the HTTP server, which is capable of handling many HTTP requests. The viewers can then play the multimedia delivered with HTTP. Such a solution scales well because the streamer doesn't have a direct connection with any of the viewers. 10 | 11 | ## Architecture of the solution 12 | 13 | The system is divided into two parts: 14 | 15 | - the server, which is responsible for receiving the RTMP stream, converting it into HLS, and then publishing the created files with an HTTP server, 16 | - the client, responsible for playing the incoming HLS stream. 17 | 18 | ### Server 19 | 20 | The internal architecture of the server is presented below: 21 | ![Server scheme](doc_assets/RTMP_to_HLS_pipeline.png) 22 | 23 | ### Client 24 | 25 | The client is just a simple JavaScript application using the [HLS.js](https://github.com/video-dev/hls.js/) web player. 26 | 27 | ## Running the demo 28 | 29 | To run the demo, you'll need to have [Elixir installed](https://elixir-lang.org/install.html). Then, download the dependencies and start the server: 30 | 31 | ```shell 32 | mix deps.get 33 | mix phx.server 34 | ``` 35 | 36 | The server will be waiting for an RTMP stream on `localhost:9006`, and the client of the application will be available on `localhost:4000`. 37 | 38 | ## Exemplary stream generation with OBS 39 | 40 | You can send RTMP stream onto `localhost:9006` with your favorite streaming tool. Below we present how to generate an RTMP stream with 41 | [OBS](https://obsproject.com). 42 | Once you have OBS installed, you can perform the following steps: 43 | 44 | 1. Open the OBS application 45 | 2. Open the `Settings` windows 46 | 3. Go to the `Stream` tab, set `Service` to `Custom...` and then set the value in the `Server` field to `rtmp://localhost:9006` (the address where the server is waiting for the stream) 47 | 4. Go to the `Output`, set output mode to `Advanced`, and set `Keyframe Interval` to 2 seconds. 48 | 5. Finally, you can go back to the main window and start streaming with the `Start Streaming` button. 49 | 50 | Below you can see how to set the appropriate settings (steps 2. and 3. from the list of steps above): 51 | 52 | ![OBS settings](doc_assets/OBS_settings.webp) 53 | 54 | ## Copyright and License 55 | 56 | Copyright 2018, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 57 | 58 | [![Software Mansion](https://membraneframework.github.io/static/logo/swm_logo_readme.png)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane) 59 | 60 | Licensed under the [Apache License, Version 2.0](LICENSE) 61 | -------------------------------------------------------------------------------- /livebooks/audio_mixer/audio_mixer.livemd: -------------------------------------------------------------------------------- 1 | # Mixing audio files 2 | 3 | ```elixir 4 | File.cd(__DIR__) 5 | Logger.configure(level: :error) 6 | 7 | Mix.install([ 8 | {:membrane_core, "~> 1.0"}, 9 | {:membrane_audio_mix_plugin, "~> 0.16.0"}, 10 | {:membrane_file_plugin, "~> 0.17.0"}, 11 | {:membrane_mp3_mad_plugin, "~> 0.18.2"}, 12 | {:membrane_ffmpeg_swresample_plugin, "~> 0.20.2"}, 13 | {:membrane_aac_fdk_plugin, "~> 0.18.8"}, 14 | {:membrane_kino_plugin, github: "membraneframework-labs/membrane_kino_plugin", tag: "v0.3.2"}, 15 | {:membrane_tee_plugin, "~> 0.12.0"} 16 | ]) 17 | ``` 18 | 19 | ## Description 20 | 21 | This is an example of mixing multiple short "beep" sound into background music, one by one, every second. 22 | 23 | ## Pipeline definition 24 | 25 | Define all constants. 26 | 27 | ```elixir 28 | n_beeps = 30 29 | beep_filepath = "./assets/beep.aac" 30 | background_filepath = "./assets/sample.mp3" 31 | :ok 32 | ``` 33 | 34 | The file's "beep" sound input is decoded from `AAC` and split into separate inputs using the `Tee` element. These inputs are then filled into the mixer with corresponding time offsets. 35 | 36 | ```elixir 37 | import Membrane.ChildrenSpec 38 | 39 | alias Membrane.{File, AAC, Tee, Time} 40 | 41 | beep_audio_input = 42 | child({:file_source, :beep}, %File.Source{location: beep_filepath}) 43 | |> child({:decoder_aac, :beep}, AAC.FDK.Decoder) 44 | |> child(:beeps, Tee.PushOutput) 45 | 46 | beeps_split = 47 | for i <- 1..n_beeps do 48 | get_child(:beeps) 49 | |> via_in(:input, options: [offset: Time.seconds(i)]) 50 | |> get_child(:mixer) 51 | end 52 | 53 | :ok 54 | ``` 55 | 56 | The background music is loaded from a file and then decoded from the `MP3` format to the appropriate `Raw Audio` format. 57 | 58 | All mixer inputs must be of the same format. 59 | 60 | ```elixir 61 | import Membrane.ChildrenSpec 62 | 63 | alias Membrane.{File, RawAudio, MP3} 64 | alias Membrane.FFmpeg.SWResample.Converter 65 | 66 | background_audio_input = 67 | child(:file_source, %File.Source{location: background_filepath}) 68 | |> child(:decoder_mp3, MP3.MAD.Decoder) 69 | |> child(:converter, %Converter{ 70 | input_stream_format: %RawAudio{channels: 2, sample_format: :s24le, sample_rate: 48_000}, 71 | output_stream_format: %RawAudio{channels: 1, sample_format: :s16le, sample_rate: 44_100} 72 | }) 73 | |> get_child(:mixer) 74 | 75 | :ok 76 | ``` 77 | 78 | Mixer is created and directly connected to audio input of the player. 79 | 80 | ```elixir 81 | import Membrane.ChildrenSpec 82 | 83 | alias Membrane.{AudioMixer, AAC, Kino} 84 | 85 | kino = Membrane.Kino.Player.new(audio: true) 86 | 87 | mixer_output = 88 | child(:mixer, Membrane.AudioMixer) 89 | |> child(:encoder_aac, AAC.FDK.Encoder) 90 | |> via_in(:audio) 91 | |> child(:player, %Kino.Player.Sink{kino: kino}) 92 | 93 | :ok 94 | ``` 95 | 96 | Whole pipeline structure. 97 | 98 | ```elixir 99 | spec = beeps_split ++ [beep_audio_input, background_audio_input, mixer_output] 100 | :ok 101 | ``` 102 | 103 | ## Playing audio 104 | 105 | ```elixir 106 | alias Membrane.RCPipeline, as: RC 107 | 108 | pipeline = RC.start!() 109 | RC.exec_actions(pipeline, spec: spec) 110 | 111 | kino 112 | ``` 113 | -------------------------------------------------------------------------------- /rtp/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/c,vim,linux,macos,elixir,windows,visualstudiocode 3 | # Edit at https://www.gitignore.io/?templates=c,vim,linux,macos,elixir,windows,visualstudiocode 4 | 5 | ### C ### 6 | # Prerequisites 7 | *.d 8 | 9 | # Object files 10 | *.o 11 | *.ko 12 | *.obj 13 | *.elf 14 | 15 | # Linker output 16 | *.ilk 17 | *.map 18 | *.exp 19 | 20 | # Precompiled Headers 21 | *.gch 22 | *.pch 23 | 24 | # Libraries 25 | *.lib 26 | *.a 27 | *.la 28 | *.lo 29 | 30 | # Shared objects (inc. Windows DLLs) 31 | *.dll 32 | *.so 33 | *.so.* 34 | *.dylib 35 | 36 | # Executables 37 | *.exe 38 | *.out 39 | *.app 40 | *.i*86 41 | *.x86_64 42 | *.hex 43 | 44 | # Debug files 45 | *.dSYM/ 46 | *.su 47 | *.idb 48 | *.pdb 49 | 50 | # Kernel Module Compile Results 51 | *.mod* 52 | *.cmd 53 | .tmp_versions/ 54 | modules.order 55 | Module.symvers 56 | Mkfile.old 57 | dkms.conf 58 | 59 | ### Elixir ### 60 | /_build 61 | /cover 62 | /deps 63 | /doc 64 | /.fetch 65 | erl_crash.dump 66 | *.ez 67 | *.beam 68 | /config/*.secret.exs 69 | .elixir_ls/ 70 | 71 | ### Elixir Patch ### 72 | 73 | ### Linux ### 74 | *~ 75 | 76 | # temporary files which can be created if a process still has a handle open of a deleted file 77 | .fuse_hidden* 78 | 79 | # KDE directory preferences 80 | .directory 81 | 82 | # Linux trash folder which might appear on any partition or disk 83 | .Trash-* 84 | 85 | # .nfs files are created when an open file is removed but is still being accessed 86 | .nfs* 87 | 88 | ### macOS ### 89 | # General 90 | .DS_Store 91 | .AppleDouble 92 | .LSOverride 93 | 94 | # Icon must end with two \r 95 | Icon 96 | 97 | # Thumbnails 98 | ._* 99 | 100 | # Files that might appear in the root of a volume 101 | .DocumentRevisions-V100 102 | .fseventsd 103 | .Spotlight-V100 104 | .TemporaryItems 105 | .Trashes 106 | .VolumeIcon.icns 107 | .com.apple.timemachine.donotpresent 108 | 109 | # Directories potentially created on remote AFP share 110 | .AppleDB 111 | .AppleDesktop 112 | Network Trash Folder 113 | Temporary Items 114 | .apdisk 115 | 116 | ### Vim ### 117 | # Swap 118 | [._]*.s[a-v][a-z] 119 | [._]*.sw[a-p] 120 | [._]s[a-rt-v][a-z] 121 | [._]ss[a-gi-z] 122 | [._]sw[a-p] 123 | 124 | # Session 125 | Session.vim 126 | Sessionx.vim 127 | 128 | # Temporary 129 | .netrwhist 130 | 131 | # Auto-generated tag files 132 | tags 133 | 134 | # Persistent undo 135 | [._]*.un~ 136 | 137 | # Coc configuration directory 138 | .vim 139 | 140 | ### VisualStudioCode ### 141 | .vscode/* 142 | !.vscode/settings.json 143 | !.vscode/tasks.json 144 | !.vscode/launch.json 145 | !.vscode/extensions.json 146 | 147 | ### VisualStudioCode Patch ### 148 | # Ignore all local history of files 149 | .history 150 | 151 | ### Windows ### 152 | # Windows thumbnail cache files 153 | Thumbs.db 154 | Thumbs.db:encryptable 155 | ehthumbs.db 156 | ehthumbs_vista.db 157 | 158 | # Dump file 159 | *.stackdump 160 | 161 | # Folder config file 162 | [Dd]esktop.ini 163 | 164 | # Recycle Bin used on file shares 165 | $RECYCLE.BIN/ 166 | 167 | # Windows Installer files 168 | *.cab 169 | *.msi 170 | *.msix 171 | *.msm 172 | *.msp 173 | 174 | # Windows shortcuts 175 | *.lnk 176 | 177 | # End of https://www.gitignore.io/api/c,vim,linux,macos,elixir,windows,visualstudiocode 178 | -------------------------------------------------------------------------------- /rtp_to_hls/rtp_to_hls.exs: -------------------------------------------------------------------------------- 1 | require Logger 2 | Logger.configure(level: :info) 3 | 4 | Mix.install([ 5 | {:membrane_core, "~> 1.0"}, 6 | {:membrane_udp_plugin, "~> 0.13.0"}, 7 | {:membrane_rtp_plugin, "~> 0.31.0"}, 8 | {:membrane_rtp_h264_plugin, "~> 0.20.0"}, 9 | {:membrane_rtp_aac_plugin, "~> 0.9.1"}, 10 | {:membrane_aac_plugin, "~> 0.19.1"}, 11 | {:membrane_http_adaptive_stream_plugin, "~> 0.18.4"}, 12 | {:membrane_fake_plugin, "~> 0.11.0"}, 13 | {:membrane_h26x_plugin, "~> 0.10.0"} 14 | ]) 15 | 16 | defmodule RTPToHLS do 17 | use Membrane.Pipeline 18 | 19 | require Logger 20 | 21 | @impl true 22 | def handle_init(_ctx, %{port: port}) do 23 | spec = [ 24 | child(:app_source, %Membrane.UDP.Source{local_port_no: port}) 25 | |> child(:rtp_demuxer, Membrane.RTP.Demuxer) 26 | |> via_out(:output, options: [stream_id: {:payload_type, 96}]) 27 | |> child(:rtp_h264_depayloader, Membrane.RTP.H264.Depayloader) 28 | |> via_in(Pad.ref(:input, :video), 29 | options: [encoding: :H264, segment_duration: Membrane.Time.seconds(4)] 30 | ) 31 | |> child(:hls, %Membrane.HTTPAdaptiveStream.SinkBin{ 32 | manifest_module: Membrane.HTTPAdaptiveStream.HLS, 33 | target_window_duration: Membrane.Time.seconds(15), 34 | storage: %Membrane.HTTPAdaptiveStream.Storages.FileStorage{directory: "output"} 35 | }), 36 | get_child(:rtp_demuxer) 37 | |> via_out(:output, options: [stream_id: {:payload_type, 127}]) 38 | |> child(:rtp_aac_depayloader, %Membrane.RTP.AAC.Depayloader{mode: :hbr}) 39 | |> child(:aac_parser, %Membrane.AAC.Parser{ 40 | audio_specific_config: Base.decode16!("1210"), 41 | out_encapsulation: :none 42 | }) 43 | |> via_in(Pad.ref(:input, :audio), 44 | options: [encoding: :AAC, segment_duration: Membrane.Time.seconds(4)] 45 | ) 46 | |> get_child(:hls) 47 | ] 48 | 49 | {[spec: spec], %{}} 50 | end 51 | 52 | @impl true 53 | def handle_child_notification({:track_playable, _track_info}, :hls, _context, state) do 54 | send(:script, :playlist_ready) 55 | {[], state} 56 | end 57 | 58 | @impl true 59 | def handle_child_notification(_notification, _element, _ctx, state) do 60 | {[], state} 61 | end 62 | end 63 | 64 | File.rm_rf!("output") 65 | File.mkdir!("output") 66 | 67 | Process.register(self(), :script) 68 | 69 | Logger.info("Starting the pipeline") 70 | input_port = 5000 71 | {:ok, _supervisor, _pipeline} = Membrane.Pipeline.start_link(RTPToHLS, %{port: input_port}) 72 | 73 | if System.get_env("CI") == "true" do 74 | # Wait to catch potential pipeline setup errors 75 | Process.sleep(1000) 76 | Logger.info("CI=true, exiting") 77 | exit(:normal) 78 | end 79 | 80 | Logger.info("Waiting for the RTP stream on port #{input_port}") 81 | 82 | receive do 83 | :playlist_ready -> :ok 84 | end 85 | 86 | Logger.info("Starting HTTP server") 87 | :ok = :inets.start() 88 | 89 | {:ok, _server} = 90 | :inets.start(:httpd, 91 | bind_address: ~c"localhost", 92 | port: 8000, 93 | document_root: ~c".", 94 | server_name: ~c"rtp_to_hls", 95 | server_root: "/tmp" 96 | ) 97 | 98 | Logger.info("Playback available at http://localhost:8000/stream.html") 99 | 100 | Process.sleep(:infinity) 101 | --------------------------------------------------------------------------------