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

WebRTC Components #1

Open
wants to merge 46 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
be9250d
wip
FelonEkonom Feb 6, 2025
b8b612e
wip
FelonEkonom Feb 7, 2025
4996e6d
Try fix demo wip
FelonEkonom Feb 11, 2025
9ad6fa4
Maybe it works now
FelonEkonom Feb 11, 2025
626a508
Now player works more or less
FelonEkonom Feb 12, 2025
cef29f0
Rename demo
FelonEkonom Feb 12, 2025
8922025
Refactor js
FelonEkonom Feb 12, 2025
7d2f4cc
Suffix event names with player id
FelonEkonom Feb 12, 2025
874c175
Write Capture, write capture demo
FelonEkonom Feb 13, 2025
8beb69e
Logs refactor
FelonEkonom Feb 13, 2025
f6f434d
Refactor logs
FelonEkonom Feb 13, 2025
16f4533
Celanup demo
FelonEkonom Feb 13, 2025
83eaf49
Refactor live views, add example project
FelonEkonom Feb 17, 2025
43aaeef
Fix bugs in components and demo
FelonEkonom Feb 18, 2025
a512cc5
Add path to .. to esbuild in example project
FelonEkonom Feb 18, 2025
c5b853b
Add JS hooks
FelonEkonom Feb 18, 2025
82340a5
Add live view module content
FelonEkonom Feb 18, 2025
aa449df
Make example project work wip
FelonEkonom Feb 18, 2025
9040ff0
Create new example project
FelonEkonom Feb 18, 2025
791000b
Finally fix example project
FelonEkonom Feb 18, 2025
edf3322
Comment out phoenix header
FelonEkonom Feb 18, 2025
8ffc2f0
Remove old example project
FelonEkonom Feb 18, 2025
123d123
Delete single-file demos
FelonEkonom Feb 19, 2025
6c6972c
Rename boombox_live to membrane_webrtc_live
FelonEkonom Feb 19, 2025
d075767
Merge remote-tracking branch 'origin/master' into implement-webrtc-co…
FelonEkonom Feb 19, 2025
e5e823b
Refactor README.md
FelonEkonom Feb 19, 2025
7ef6aee
Refactor README.md
FelonEkonom Feb 19, 2025
745bdf8
Refactor liveview modules attrs
FelonEkonom Feb 19, 2025
63a8f3b
Refactor moduledocs of player and capture
FelonEkonom Feb 19, 2025
cbdc0a7
Remove leftovers
FelonEkonom Feb 19, 2025
d672533
Remove unnecesary modules
FelonEkonom Feb 19, 2025
c164850
Bump webrtc plugin 0.24.0
FelonEkonom Feb 21, 2025
30d3ca5
simplify liveviews APIs
mat-hek Feb 24, 2025
10e4194
Partially implement CR
FelonEkonom Feb 25, 2025
14f9209
Add captured video preview
FelonEkonom Feb 25, 2025
88dd337
Refactor player and capture
FelonEkonom Feb 26, 2025
aad34f2
Fix README.md
FelonEkonom Feb 26, 2025
5cf38ed
Fix dialyzer
FelonEkonom Feb 26, 2025
e6d833c
Fix lint
FelonEkonom Feb 27, 2025
2abbd7c
Fix lint
FelonEkonom Feb 27, 2025
e6ce995
Refactor example project filenames
FelonEkonom Feb 27, 2025
0d257ee
Send Objects via websocket instead of strings
FelonEkonom Mar 3, 2025
d71adac
Refactor element style
FelonEkonom Mar 3, 2025
4b3be44
Remove old debug code
FelonEkonom Mar 3, 2025
bc136cd
Refactor live render attrs
FelonEkonom Mar 3, 2025
af61686
Update attributes docs
FelonEkonom Mar 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 37 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,59 @@
# Membrane Template Plugin
# Membrane WebRTC Live

[![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_live.svg)](https://hex.pm/packages/membrane_webrtc_live)
[![API Docs](https://img.shields.io/badge/api-docs-yellow.svg?style=flat)](https://hexdocs.pm/membrane_webrtc_live)
[![CircleCI](https://circleci.com/gh/membraneframework/membrane_webrtc_live.svg?style=svg)](https://circleci.com/gh/membraneframework/membrane_webrtc_live)

This repository contains a template for new plugins.

Check out different branches for other flavors of this template.
Phoenix LiveViews that can be used with Membrane Components from [membrane_webrtc_plugin](https://github.com/membraneframework/membrane_webrtc_plugin).

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_live` to your list of dependencies in `mix.exs`:

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

## Usage
## Modules

`Membrane.WebRTC.Live` comes with two `Phoenix.LiveView`s:
- `Membrane.WebRTC.Live.Capture` - exchanges WebRTC signaling messages between `Membrane.WebRTC.Source` and the browser. It expects the same `Membrane.WebRTC.Signaling` that has been passed to the related `Membrane.WebRTC.Source`. As a result, `Membrane.Webrtc.Source` will return the media stream captured from the browser, where `Membrane.WebRTC.Live.Capture` has been rendered.
- `Membrane.WebRTC.Live.Player` - exchanges WebRTC signaling messages between `Membrane.WebRTC.Sink` and the browser. It expects the same `Membrane.WebRTC.Signaling` that has been passed to the related `Membrane.WebRTC.Sink`. As a result, `Membrane.WebRTC.Live.Player` will play media streams passed to the related `Membrane.WebRTC.Sink`. Currently supports up to one video stream and up to one audio stream.

## Usage

To use `Phoenix.LiveView`s from this repository, you have to use related JS hooks. To do so, add the following code snippet to `assets/js/app.js`

```js
import { createCaptureHook, createPlayerHook } from "membrane_webrtc_live";

let Hooks = {};
const iceServers = [{ urls: "stun:stun.l.google.com:19302" }];
Hooks.Capture = createCaptureHook(iceServers);
Hooks.Player = createPlayerHook(iceServers);
```

and add `Hooks` to the WebSocket constructor. It can be done in a following way:

```js
new LiveSocket("/live", Socket, {
params: SomeParams,
hooks: Hooks,
});
```

TODO
To see the full usage example, you can go to `example_project/` directory in this repository (take a look especially at `example_project/assets/js/app.js` and `example_project/lib/example_project_web/live_views/echo.ex`).

## Copyright and License

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

[![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_live)

Licensed under the [Apache License, Version 2.0](LICENSE)
57 changes: 57 additions & 0 deletions assets/capture.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
export function createCaptureHook(iceServers = [{ urls: "stun:stun.l.google.com:19302" }]) {
return {
async mounted() {
this.handleEvent("media_constraints-" + this.el.id, async (mediaConstraints) => {
console.log("[" + this.el.id + "] Received media constraints:", mediaConstraints);

const localStream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
const pcConfig = { iceServers: iceServers };
this.pc = new RTCPeerConnection(pcConfig);

this.pc.onicecandidate = (event) => {
if (event.candidate === null) return;
console.log("[" + this.el.id + "] Sent ICE candidate:", event.candidate);
message = JSON.stringify({ type: "ice_candidate", data: event.candidate });
this.pushEventTo(this.el, "webrtc_signaling", message);
};

this.pc.onconnectionstatechange = () => {
console.log(
"[" + this.el.id + "] RTCPeerConnection state changed to",
this.pc.connectionState
);
};

this.el.srcObject = new MediaStream();

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

this.el.play();

this.handleEvent("webrtc_signaling-" + this.el.id, async (event) => {
const { type, data } = event;

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

const offer = await this.pc.createOffer();
await this.pc.setLocalDescription(offer);
console.log("[" + this.el.id + "] Sent SDP offer:", offer);
message = JSON.stringify({ type: "sdp_offer", data: offer });
this.pushEventTo(this.el, "webrtc_signaling", message);
});
},
};
}
4 changes: 4 additions & 0 deletions assets/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createCaptureHook } from "./capture.js";
import { createPlayerHook } from "./player.js";

export { createCaptureHook, createPlayerHook };
41 changes: 41 additions & 0 deletions assets/player.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export function createPlayerHook(iceServers = [{ urls: "stun:stun.l.google.com:19302" }]) {
return {
async mounted() {
this.pc = new RTCPeerConnection({ iceServers: iceServers });
this.el.srcObject = new MediaStream();

this.pc.ontrack = (event) => {
this.el.srcObject.addTrack(event.track);
};

this.pc.onicecandidate = (ev) => {
console.log("[" + this.el.id + "] Sent ICE candidate:", ev.candidate);
message = JSON.stringify({ type: "ice_candidate", data: ev.candidate });
this.pushEventTo(this.el, "webrtc_signaling", message);
};

const eventName = "webrtc_signaling-" + this.el.id;
this.handleEvent(eventName, async (event) => {
const { type, data } = event;

switch (type) {
case "sdp_offer":
console.log("[" + this.el.id + "] Received SDP offer:", data);
await this.pc.setRemoteDescription(data);

const answer = await this.pc.createAnswer();
await this.pc.setLocalDescription(answer);

message = JSON.stringify({ type: "sdp_answer", data: answer });
this.pushEventTo(this.el, "webrtc_signaling", message);
console.log("[" + this.el.id + "] Sent SDP answer:", answer);

break;
case "ice_candidate":
console.log("[" + this.el.id + "] Recieved ICE candidate:", data);
await this.pc.addIceCandidate(data);
}
});
},
};
}
5 changes: 5 additions & 0 deletions example_project/.formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
import_deps: [:phoenix],
plugins: [Phoenix.LiveView.HTMLFormatter],
inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"]
]
37 changes: 37 additions & 0 deletions example_project/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Temporary files, for example, from tests.
/tmp/

# Ignore package tarball (built via "mix hex.build").
example_project-*.tar

# Ignore assets that are produced by build tools.
/priv/static/assets/

# Ignore digested assets cache.
/priv/static/cache_manifest.json

# In case you use Node.js/npm, you want to ignore these.
npm-debug.log
/assets/node_modules/

23 changes: 23 additions & 0 deletions example_project/README.md
Copy link
Member

Choose a reason for hiding this comment

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

let's create a proper readme

  • write what this example does
  • mention which files are important
  • remove the Phoenix links
  • mention that this uses boombox

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Example Project

Example project showing how `Membrane.WebRTC.Live.Capture` and `Membrane.WebRTC.Live.Player` can be used.

It contains a simple demo, where:
- the video stream is get from the browser and sent via WebRTC to Elixir server using `Membrane.WebRTC.Live.Capture`
- then, this same video stream is re-sent again to the browser and displayed using `Membrane.WebRTC.Live.Player`.

This demo uses also [Boombox](https://hex.pm/packages/boombox).

The most important file in the project is `example_project/lib/example_project_web/live_views/echo.ex`, that
contains the usage of `Boombox` and LiveViews defined in `membrane_webrtc_live`.

You can also take a look at `example_project/assets/js/app.js` to see how you can use `membrane_webrtc_live` JS hooks.

## Run server

To start Phoenix server:

* Run `mix setup` to install and setup dependencies
* Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`

Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
51 changes: 51 additions & 0 deletions example_project/assets/js/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
// to get started and then uncomment the line below.
// import "./user_socket.js"

// You can include dependencies in two ways.
//
// The simplest option is to put them in assets/vendor and
// import them using relative paths:
//
// import "../vendor/some-package.js"
//
// Alternatively, you can `npm install some-package --prefix assets` and import
// them using a path starting with the package name:
//
// import "some-package"
//

// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html";
// Establish Phoenix Socket and LiveView configuration.
import { Socket } from "phoenix";
import { LiveSocket } from "phoenix_live_view";
import topbar from "../vendor/topbar";

import { createCaptureHook, createPlayerHook } from "membrane_webrtc_live";

let Hooks = {};
const iceServers = [{ urls: "stun:stun.l.google.com:19302" }];
Hooks.Capture = createCaptureHook(iceServers);
Hooks.Player = createPlayerHook(iceServers);

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
let liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: { _csrf_token: csrfToken },
hooks: Hooks,
});

// Show progress bar on live navigation and form submits
topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" });
window.addEventListener("phx:page-loading-start", (_info) => topbar.show(300));
window.addEventListener("phx:page-loading-stop", (_info) => topbar.hide());

// connect if there are any LiveViews on the page
liveSocket.connect();

// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket;
Loading