Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Membrane WebRTC plugin based on ex_webrtc #1

Merged
merged 17 commits into from
Apr 4, 2024
2 changes: 1 addition & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[
inputs: [
"{lib,test,config}/**/*.{ex,exs}",
"{lib,test,config,examples}/**/*.{ex,exs}",
".formatter.exs",
"*.exs"
],
Expand Down
22 changes: 10 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,33 @@
# Membrane Template Plugin
# Membrane WebRTC Plugin

[![Hex.pm](https://img.shields.io/hexpm/v/membrane_template_plugin.svg)](https://hex.pm/packages/membrane_template_plugin)
[![API Docs](https://img.shields.io/badge/api-docs-yellow.svg?style=flat)](https://hexdocs.pm/membrane_template_plugin)
[![CircleCI](https://circleci.com/gh/membraneframework/membrane_template_plugin.svg?style=svg)](https://circleci.com/gh/membraneframework/membrane_template_plugin)
[![Hex.pm](https://img.shields.io/hexpm/v/membrane_webrtc_plugin.svg)](https://hex.pm/packages/membrane_webrtc_plugin)
[![API Docs](https://img.shields.io/badge/api-docs-yellow.svg?style=flat)](https://hexdocs.pm/membrane_webrtc_plugin)
[![CircleCI](https://circleci.com/gh/membraneframework/membrane_webrtc_plugin.svg?style=svg)](https://circleci.com/gh/membraneframework/membrane_webrtc_plugin)

This repository contains a template for new plugins.

Check out different branches for other flavors of this template.
Membrane Plugin for sending and receiving streams via WebRTC. It's based on [ex_webrtc](https://github.com/elixir-webrtc/ex_webrtc).

It's a part of the [Membrane Framework](https://membrane.stream).

## Installation

The package can be installed by adding `membrane_template_plugin` to your list of dependencies in `mix.exs`:
The package can be installed by adding `membrane_webrtc_plugin` to your list of dependencies in `mix.exs`:

```elixir
def deps do
[
{:membrane_template_plugin, "~> 0.1.0"}
{:membrane_webrtc_plugin, "~> 0.1.0"}
]
end
```

## Usage

TODO
The `examples` directory shows how to send and receive streams from a web browser.

## Copyright and License

Copyright 2020, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane_template_plugin)
Copyright 2020, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane_webrtc_plugin)

[![Software Mansion](https://logo.swmansion.com/logo?color=white&variant=desktop&width=200&tag=membrane-github)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane_template_plugin)
[![Software Mansion](https://logo.swmansion.com/logo?color=white&variant=desktop&width=200&tag=membrane-github)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane_webrtc_plugin)

Licensed under the [Apache License, Version 2.0](LICENSE)
Binary file added examples/assets/bbb_vp8.mkv
Binary file not shown.
59 changes: 59 additions & 0 deletions examples/assets/browser_to_file/browser_to_file.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const pcConfig = { 'iceServers': [{ 'urls': 'stun:stun.l.google.com:19302' },] };
const mediaConstraints = { video: true, audio: true }

const proto = window.location.protocol === "https:" ? "wss:" : "ws:"
const ws = new WebSocket(`${proto}//${window.location.hostname}:8829`);
const connStatus = document.getElementById("status");
ws.onopen = _ => start_connection(ws);
ws.onclose = event => {
connStatus.innerHTML = "Disconnected"
console.log("WebSocket connection was terminated:", event);
}

const start_connection = async (ws) => {
const localStream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
const pc = new RTCPeerConnection(pcConfig);

pc.onicecandidate = event => {
if (event.candidate === null) return;
console.log("Sent ICE candidate:", event.candidate);
ws.send(JSON.stringify({ type: "ice_candidate", data: event.candidate }));
};

pc.onconnectionstatechange = () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be even more correct, we should check against pc.connectionState, like here

if (pc.connectionState == "connected") {
const button = document.createElement('button');
button.innerHTML = "Disconnect";
button.onclick = () => {
ws.close();
localStream.getTracks().forEach(track => track.stop())
}
connStatus.innerHTML = "Connected ";
connStatus.appendChild(button);
}
}

for (const track of localStream.getTracks()) {
pc.addTrack(track, localStream);
}

ws.onmessage = async event => {
const { type, data } = JSON.parse(event.data);

switch (type) {
case "sdp_answer":
console.log("Received SDP answer:", data);
await pc.setRemoteDescription(data);
break;
case "ice_candidate":
console.log("Recieved ICE candidate:", data);
await pc.addIceCandidate(data);
break;
}
};

const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
console.log("Sent SDP offer:", offer)
ws.send(JSON.stringify({ type: "sdp_offer", data: offer }));
};
20 changes: 20 additions & 0 deletions examples/assets/browser_to_file/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Membrane WebRTC browser to file example</title>
</head>

<body
style="background-color: black; color: white; font-family: Arial, Helvetica, sans-serif; min-height: 100vh; margin: 0px; padding: 5px 0px 5px 0px">
<main>
<h1>Membrane WebRTC browser to file example</h1>
<div id="status">Connecting</div>
</main>
<script src="browser_to_file.js"></script>
</body>

</html>
37 changes: 37 additions & 0 deletions examples/assets/file_to_browser/file_to_browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const videoPlayer = document.getElementById("videoPlayer");
const pcConfig = { 'iceServers': [{ 'urls': 'stun:stun.l.google.com:19302' },] };
const proto = window.location.protocol === "https:" ? "wss:" : "ws:"
const ws = new WebSocket(`${proto}//${window.location.hostname}:8829`);
ws.onopen = () => start_connection(ws);
ws.onclose = event => console.log("WebSocket connection was terminated:", event);

const start_connection = async (ws) => {
videoPlayer.srcObject = new MediaStream();

const pc = new RTCPeerConnection(pcConfig);
pc.ontrack = event => videoPlayer.srcObject.addTrack(event.track);
pc.onicecandidate = event => {
if (event.candidate === null) return;

console.log("Sent ICE candidate:", event.candidate);
ws.send(JSON.stringify({ type: "ice_candidate", data: event.candidate }));
};

ws.onmessage = async event => {
const { type, data } = JSON.parse(event.data);

switch (type) {
case "sdp_offer":
console.log("Received SDP offer:", data);
await pc.setRemoteDescription(data);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
ws.send(JSON.stringify({ type: "sdp_answer", data: answer }));
console.log("Sent SDP answer:", answer)
break;
case "ice_candidate":
console.log("Recieved ICE candidate:", data);
await pc.addIceCandidate(data);
}
};
};
20 changes: 20 additions & 0 deletions examples/assets/file_to_browser/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Membrane WebRTC file to browser example</title>
</head>

<body
style="background-color: black; color: white; font-family: Arial, Helvetica, sans-serif; min-height: 100vh; margin: 0px; padding: 5px 0px 5px 0px">
<main>
<h1>Membrane WebRTC file to browser example</h1>
<video id="videoPlayer" controls muted autoplay></video>
</main>
<script src="file_to_browser.js"></script>
</body>

</html>
72 changes: 72 additions & 0 deletions examples/browser_to_file.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# This example receives audio and video from a browser via WebRTC
# and saves it to a `recording.mkv` file.
# To run it, type `elixir browser_to_file.exs` and open
# http://localhost:8000/index.html in your browser. To finish recording,
# click the `disconnect` button or close the tab.

Logger.configure(level: :info)

Mix.install([
{:membrane_webrtc_plugin, path: "#{__DIR__}/.."},
:membrane_file_plugin,
:membrane_realtimer_plugin,
:membrane_matroska_plugin,
:membrane_opus_plugin,
:membrane_h264_plugin
])

defmodule Example.Pipeline do
use Membrane.Pipeline

alias Membrane.WebRTC

@impl true
def handle_init(_ctx, opts) do
spec =
[
child(:webrtc, %WebRTC.Source{
signaling: {:websocket, port: opts[:port]}
}),
child(:matroska, Membrane.Matroska.Muxer),
get_child(:webrtc)
|> via_out(:output, options: [kind: :audio])
|> child(Membrane.Opus.Parser)
|> get_child(:matroska),
get_child(:webrtc)
|> via_out(:output, options: [kind: :video])
|> get_child(:matroska),
get_child(:matroska)
|> child(:sink, %Membrane.File.Sink{location: "recording.mkv"})
]

{[spec: spec], %{}}
end

@impl true
def handle_element_end_of_stream(:sink, :input, _ctx, state) do
{[terminate: :normal], state}
end

@impl true
def handle_element_end_of_stream(_element, _pad, _ctx, state) do
{[], state}
end
end

{:ok, supervisor, _pipeline} = Membrane.Pipeline.start_link(Example.Pipeline, port: 8829)
Process.monitor(supervisor)

:ok = :inets.start()

{:ok, _server} =
:inets.start(:httpd,
bind_address: ~c"localhost",
port: 8000,
document_root: ~c"#{__DIR__}/assets/browser_to_file",
server_name: ~c"webrtc",
server_root: "/tmp"
)

receive do
{:DOWN, _ref, :process, ^supervisor, _reason} -> :ok
end
94 changes: 94 additions & 0 deletions examples/file_to_browser.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# This example reads a short part of the Big Buck Bunny movie
# from an `.mkv` file and streams it to a browser.
# To run it, type `elixir file_to_browser.exs` and open
# http://localhost:8000/index.html in your browser.
# Note that due to browsers' policy, you need to manually unmute
# audio in the player to hear the sound.

Logger.configure(level: :info)

Mix.install([
{:membrane_webrtc_plugin, path: "#{__DIR__}/.."},
:membrane_file_plugin,
:membrane_realtimer_plugin,
:membrane_matroska_plugin,
:membrane_opus_plugin
])

defmodule Example.Pipeline do
use Membrane.Pipeline

alias Membrane.WebRTC

@impl true
def handle_init(_ctx, opts) do
spec =
child(%Membrane.File.Source{location: "#{__DIR__}/assets/bbb_vp8.mkv"})
|> child(:demuxer, Membrane.Matroska.Demuxer)

{[spec: spec], %{audio_track: nil, video_track: nil, port: opts[:port]}}
end

@impl true
def handle_child_notification({:new_track, {id, info}}, :demuxer, _ctx, state) do
state =
case info.codec do
:opus -> %{state | audio_track: id}
:h264 -> %{state | video_track: id}
:vp8 -> %{state | video_track: id}
end

if state.audio_track && state.video_track do
spec = [
child(:webrtc, %WebRTC.Sink{signaling: {:websocket, port: state.port}}),
get_child(:demuxer)
|> via_out(Pad.ref(:output, state.video_track))
|> child({:realtimer, :video_track}, Membrane.Realtimer)
|> via_in(Pad.ref(:input, :video_track), options: [kind: :video])
|> get_child(:webrtc),
get_child(:demuxer)
|> via_out(Pad.ref(:output, state.audio_track))
|> child({:realtimer, :audio_track}, Membrane.Realtimer)
|> via_in(Pad.ref(:input, :audio_track), options: [kind: :audio])
|> get_child(:webrtc)
]

{[spec: spec], state}
else
{[], state}
end
end

@impl true
def handle_child_notification({:end_of_stream, track}, :webrtc, _ctx, state) do
state = %{state | track => nil}

if !state.audio_track && !state.video_track do
{[terminate: :normal], state}
else
{[], state}
end
end

@impl true
def handle_child_notification(_notification, _child, _ctx, state) do
{[], state}
end
end

{:ok, supervisor, _pipeline} = Membrane.Pipeline.start_link(Example.Pipeline, port: 8829)
Process.monitor(supervisor)
:ok = :inets.start()

{:ok, _server} =
:inets.start(:httpd,
bind_address: ~c"localhost",
port: 8000,
document_root: ~c"#{__DIR__}/assets/file_to_browser",
server_name: ~c"webrtc",
server_root: "/tmp"
)

receive do
{:DOWN, _ref, :process, ^supervisor, _reason} -> :ok
end
2 changes: 0 additions & 2 deletions lib/membrane_template.ex

This file was deleted.

Loading