Skip to content

Commit b895d50

Browse files
committed
adding rtcp processing example
1 parent f96e563 commit b895d50

File tree

10 files changed

+392
-0
lines changed

10 files changed

+392
-0
lines changed
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Used by "mix format"
2+
[
3+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4+
]

examples/rtcp-processing/.gitignore

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
echo-*.tar
24+
25+
# Temporary files, for example, from tests.
26+
/tmp/

examples/rtcp-processing/README.md

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# RTCP Processing
2+
3+
Receive video and audio from a browser and display the RTCP packets in the browser
4+
5+
rtcp-processing demonstrates the Public API for processing RTCP packets in ExWebRTC.
6+
7+
RTCP is used for statistics and control information for media in WebRTC. Using these messages you can get information about the quality of the media, round trip time and packet loss. You can also craft messages to influence the media quality.
8+
9+
While in `examples/rtcp-processing` directory
10+
11+
1. Run `mix deps.get`
12+
2. Run `mix run --no-halt`
13+
3. Visit `http://127.0.0.1:8829/index.html` in your browser and press the `play` button.
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<meta http-equiv="X-UA-Compatible" content="ie=edge">
8+
<title>Elixir WebRTC RTCP Example</title>
9+
</head>
10+
11+
<body>
12+
<main>
13+
<h1>Elixir WebRTC RTCP Example</h1>
14+
</main>
15+
<video id="videoPlayer" autoplay controls></video>
16+
<h2>RTCP Packets from the server:</h2>
17+
<div id="rtcpPackets"></div>
18+
<script src="script.js"></script>
19+
</body>
20+
21+
</html>
+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
const pcConfig = { 'iceServers': [{ 'urls': 'stun:stun.l.google.com:19302' },] };
2+
const mediaConstraints = { video: true, audio: true }
3+
4+
const proto = window.location.protocol === "https:" ? "wss:" : "ws:"
5+
const ws = new WebSocket(`${proto}//${window.location.host}/ws`);
6+
ws.onopen = _ => start_connection(ws);
7+
ws.onclose = event => console.log("WebSocket connection was terminated:", event);
8+
const start_connection = async (ws) => {
9+
const videoPlayer = document.getElementById("videoPlayer");
10+
const rtcpPacketsEl = document.getElementById("rtcpPackets");
11+
videoPlayer.srcObject = new MediaStream();
12+
13+
const pc = new RTCPeerConnection(pcConfig);
14+
pc.ontrack = event => videoPlayer.srcObject.addTrack(event.track);
15+
pc.onicecandidate = event => {
16+
if (event.candidate === null) return;
17+
18+
console.log("Sent ICE candidate:", event.candidate);
19+
ws.send(JSON.stringify({ type: "ice", data: event.candidate }));
20+
};
21+
22+
const localStream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
23+
for (const track of localStream.getTracks()) {
24+
pc.addTrack(track, localStream);
25+
}
26+
pc.addEventListener(
27+
"connectionstatechange",
28+
(_event) => {
29+
if (pc.connectionState == "connected") {
30+
setInterval(function() {
31+
ws.send(JSON.stringify({ type: "getRTCP" }))
32+
}, 1000)
33+
}
34+
},
35+
false,
36+
);
37+
38+
ws.onmessage = async event => {
39+
console.debug('[message] Data received from server: ', event.data);
40+
41+
try {
42+
const { type, data } = JSON.parse(event.data);
43+
44+
switch (type) {
45+
case "answer":
46+
console.log("Received SDP answer:", data);
47+
await pc.setRemoteDescription(data)
48+
break;
49+
case "ice":
50+
console.log("Recieved ICE candidate:", data);
51+
await pc.addIceCandidate(data);
52+
case "rtcpPackets":
53+
console.log("Recieved rtcp packets:", data);
54+
var p = document.createElement('p');
55+
p.innerHTML = JSON.stringify(data)
56+
rtcpPacketsEl.appendChild(p)
57+
58+
}
59+
} catch (error) {
60+
console.error(error);
61+
}
62+
};
63+
64+
const offer = await pc.createOffer();
65+
await pc.setLocalDescription(offer);
66+
console.log("Sent SDP offer:", offer)
67+
ws.send(JSON.stringify({ type: "offer", data: offer }));
68+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
defmodule RtcpProcessing.Application do
2+
use Application
3+
4+
require Logger
5+
6+
@ip {127, 0, 0, 1}
7+
@port 8829
8+
9+
@impl true
10+
def start(_type, _args) do
11+
Logger.configure(level: :info)
12+
13+
children = [
14+
{Bandit, plug: RtcpProcessing.Router, ip: @ip, port: @port}
15+
]
16+
17+
Supervisor.start_link(children, strategy: :one_for_one)
18+
end
19+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
defmodule RtcpProcessing.PeerHandler do
2+
require Logger
3+
4+
alias ExWebRTC.{
5+
ICECandidate,
6+
MediaStreamTrack,
7+
PeerConnection,
8+
RTPCodecParameters,
9+
SessionDescription
10+
}
11+
12+
@behaviour WebSock
13+
14+
@ice_servers [
15+
%{urls: "stun:stun.l.google.com:19302"}
16+
]
17+
18+
@video_codecs [
19+
%RTPCodecParameters{
20+
payload_type: 96,
21+
mime_type: "video/VP8",
22+
clock_rate: 90_000
23+
}
24+
]
25+
26+
@audio_codecs [
27+
%RTPCodecParameters{
28+
payload_type: 111,
29+
mime_type: "audio/opus",
30+
clock_rate: 48_000,
31+
channels: 2
32+
}
33+
]
34+
35+
@impl true
36+
def init(_) do
37+
{:ok, pc} =
38+
PeerConnection.start_link(
39+
ice_servers: @ice_servers,
40+
video_codecs: @video_codecs,
41+
audio_codecs: @audio_codecs
42+
)
43+
44+
video_track = MediaStreamTrack.new(:video)
45+
audio_track = MediaStreamTrack.new(:audio)
46+
47+
{:ok, _sender} = PeerConnection.add_track(pc, video_track)
48+
{:ok, _sender} = PeerConnection.add_track(pc, audio_track)
49+
50+
state = %{
51+
peer_connection: pc,
52+
out_video_track_id: video_track.id,
53+
out_audio_track_id: audio_track.id,
54+
in_video_track_id: nil,
55+
in_audio_track_id: nil,
56+
rtcp_packets: []
57+
}
58+
59+
{:ok, state}
60+
end
61+
62+
@impl true
63+
def handle_in({msg, [opcode: :text]}, state) do
64+
msg
65+
|> Jason.decode!()
66+
|> handle_ws_msg(state)
67+
end
68+
69+
@impl true
70+
def handle_info({:ex_webrtc, _from, msg}, state) do
71+
handle_webrtc_msg(msg, state)
72+
end
73+
74+
@impl true
75+
def terminate(reason, _state) do
76+
Logger.warning("WebSocket connection was terminated, reason: #{inspect(reason)}")
77+
end
78+
79+
defp handle_ws_msg(%{"type" => "offer", "data" => data}, state) do
80+
Logger.info("Received SDP offer: #{inspect(data)}")
81+
82+
offer = SessionDescription.from_json(data)
83+
:ok = PeerConnection.set_remote_description(state.peer_connection, offer)
84+
85+
{:ok, answer} = PeerConnection.create_answer(state.peer_connection)
86+
:ok = PeerConnection.set_local_description(state.peer_connection, answer)
87+
88+
answer_json = SessionDescription.to_json(answer)
89+
90+
msg =
91+
%{"type" => "answer", "data" => answer_json}
92+
|> Jason.encode!()
93+
94+
Logger.info("Sent SDP answer: #{inspect(answer_json)}")
95+
96+
{:push, {:text, msg}, state}
97+
end
98+
99+
defp handle_ws_msg(%{"type" => "ice", "data" => data}, state) do
100+
Logger.info("Received ICE candidate: #{inspect(data)}")
101+
102+
candidate = ICECandidate.from_json(data)
103+
:ok = PeerConnection.add_ice_candidate(state.peer_connection, candidate)
104+
{:ok, state}
105+
end
106+
107+
defp handle_ws_msg(%{"type" => "getRTCP"}, state) do
108+
Logger.info("Received request for RTCP packets")
109+
110+
packets =
111+
Enum.map(state.rtcp_packets, fn packet ->
112+
"packet: #{inspect(packet)}"
113+
end)
114+
115+
msg = Jason.encode!(%{type: "rtcpPackets", data: packets})
116+
117+
{:push, {:text, msg}, %{state | rtcp_packets: []}}
118+
end
119+
120+
defp handle_webrtc_msg({:ice_candidate, candidate}, state) do
121+
candidate_json = ICECandidate.to_json(candidate)
122+
123+
msg =
124+
%{"type" => "ice", "data" => candidate_json}
125+
|> Jason.encode!()
126+
127+
Logger.info("Sent ICE candidate: #{inspect(candidate_json)}")
128+
129+
{:push, {:text, msg}, state}
130+
end
131+
132+
defp handle_webrtc_msg({:track, track}, state) do
133+
%MediaStreamTrack{kind: kind, id: id} = track
134+
135+
state =
136+
case kind do
137+
:video -> %{state | in_video_track_id: id}
138+
:audio -> %{state | in_audio_track_id: id}
139+
end
140+
141+
{:ok, state}
142+
end
143+
144+
defp handle_webrtc_msg({:rtp, id, packet}, %{in_audio_track_id: id} = state) do
145+
PeerConnection.send_rtp(state.peer_connection, state.out_audio_track_id, packet)
146+
{:ok, state}
147+
end
148+
149+
defp handle_webrtc_msg({:rtp, id, packet}, %{in_video_track_id: id} = state) do
150+
PeerConnection.send_rtp(state.peer_connection, state.out_video_track_id, packet)
151+
{:ok, state}
152+
end
153+
154+
defp handle_webrtc_msg({:rtcp, packets}, state) do
155+
{:ok, %{state | rtcp_packets: state.rtcp_packets ++ packets}}
156+
end
157+
158+
defp handle_webrtc_msg(_msg, state), do: {:ok, state}
159+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
defmodule RtcpProcessing.Router do
2+
use Plug.Router
3+
4+
plug(Plug.Static, at: "/", from: "assets")
5+
plug(:match)
6+
plug(:dispatch)
7+
8+
get "/ws" do
9+
WebSockAdapter.upgrade(conn, RtcpProcessing.PeerHandler, %{}, [])
10+
end
11+
12+
match _ do
13+
send_resp(conn, 404, "not found")
14+
end
15+
end

examples/rtcp-processing/mix.exs

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
defmodule RtcpProcessing.MixProject do
2+
use Mix.Project
3+
4+
def project do
5+
[
6+
app: :rtcp_processing,
7+
version: "0.1.0",
8+
elixir: "~> 1.15",
9+
start_permanent: Mix.env() == :prod,
10+
deps: deps()
11+
]
12+
end
13+
14+
def application do
15+
[
16+
extra_applications: [:logger],
17+
mod: {RtcpProcessing.Application, []}
18+
]
19+
end
20+
21+
defp deps do
22+
[
23+
{:plug, "~> 1.15.0"},
24+
{:bandit, "~> 1.2.0"},
25+
{:websock_adapter, "~> 0.5.0"},
26+
{:jason, "~> 1.4.0"},
27+
{:ex_webrtc, path: "../../."}
28+
]
29+
end
30+
end

0 commit comments

Comments
 (0)