├── .gitignore
├── Makefile
├── README.md
├── doc
├── api.md
├── concepts.md
├── events.md
├── fs.md
├── index.md
├── intro.md
├── janus.md
├── kms.md
└── roadmap.md
├── include
├── nkmedia.hrl
├── nkmedia_call.hrl
└── nkmedia_room.hrl
├── priv
├── certs
│ └── wss.pem
├── freeswitch.xml
└── www
├── rebar
├── rebar.config
├── src
├── fs_backend
│ ├── nkmedia_fs.erl
│ ├── nkmedia_fs_api_syntax.erl
│ ├── nkmedia_fs_build.erl
│ ├── nkmedia_fs_callbacks.erl
│ ├── nkmedia_fs_cmd.erl
│ ├── nkmedia_fs_conference.erl.1
│ ├── nkmedia_fs_docker.erl
│ ├── nkmedia_fs_engine.erl
│ ├── nkmedia_fs_event_protocol.erl
│ ├── nkmedia_fs_session.erl
│ ├── nkmedia_fs_sip.erl
│ ├── nkmedia_fs_util.erl
│ ├── nkmedia_fs_verto.erl
│ ├── nkmedia_fs_verto_proxy.erl
│ ├── nkmedia_fs_verto_proxy_client.erl
│ └── nkmedia_fs_verto_proxy_server.erl
├── janus_backend
│ ├── nkmedia_janus.erl
│ ├── nkmedia_janus_admin.erl
│ ├── nkmedia_janus_api_syntax.erl
│ ├── nkmedia_janus_build.erl
│ ├── nkmedia_janus_callbacks.erl
│ ├── nkmedia_janus_client.erl
│ ├── nkmedia_janus_docker.erl
│ ├── nkmedia_janus_engine.erl
│ ├── nkmedia_janus_op.erl
│ ├── nkmedia_janus_op.erl.2
│ ├── nkmedia_janus_proxy.erl
│ ├── nkmedia_janus_proxy_client.erl
│ ├── nkmedia_janus_proxy_server.erl
│ ├── nkmedia_janus_room.erl
│ └── nkmedia_janus_session.erl
├── kms_backend
│ ├── nkmedia_kms.erl
│ ├── nkmedia_kms_api.erl
│ ├── nkmedia_kms_api_syntax.erl
│ ├── nkmedia_kms_build.erl
│ ├── nkmedia_kms_callbacks.erl
│ ├── nkmedia_kms_client.erl
│ ├── nkmedia_kms_docker.erl
│ ├── nkmedia_kms_engine.erl
│ ├── nkmedia_kms_proxy.erl
│ ├── nkmedia_kms_proxy_client.erl
│ ├── nkmedia_kms_proxy_server.erl
│ ├── nkmedia_kms_room.erl
│ ├── nkmedia_kms_session.erl
│ └── nkmedia_kms_session_lib.erl
├── nkmedia.app.src
├── nkmedia.erl
├── nkmedia_api.erl
├── nkmedia_api_events.erl
├── nkmedia_api_syntax.erl
├── nkmedia_app.erl
├── nkmedia_callbacks.erl
├── nkmedia_core.erl
├── nkmedia_session.erl
├── nkmedia_sup.erl
├── nkmedia_util.erl
└── plugins
│ ├── nkmedia_room.erl
│ ├── nkmedia_room_api.erl
│ ├── nkmedia_room_api_events.erl
│ ├── nkmedia_room_api_syntax.erl
│ ├── nkmedia_room_callbacks.erl
│ └── nkmedia_room_msglog.erl
├── test
├── app.config
├── basic_test.erl
└── vm.args
└── util
├── shell_app.config
└── shell_vm.args
/.gitignore:
--------------------------------------------------------------------------------
1 | log
2 | ebin
3 | deps
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | REPO ?= nkmedia
2 | RELOADER ?= -s nklib_reloader
3 |
4 |
5 | .PHONY: deps release
6 |
7 | all: deps compile
8 |
9 | compile:
10 | ./rebar compile
11 |
12 | cnodeps:
13 | ./rebar compile skip_deps=true
14 |
15 | deps:
16 | ./rebar get-deps
17 |
18 | clean:
19 | ./rebar clean
20 |
21 | distclean: clean
22 | ./rebar delete-deps
23 |
24 | tests: compile eunit
25 |
26 | eunit:
27 | export ERL_FLAGS="-config test/app.config -args_file test/vm.args"; \
28 | ./rebar eunit skip_deps=true
29 |
30 | shell:
31 | erl -config util/shell_app.config -args_file util/shell_vm.args -s nkmedia_app $(RELOADER)
32 |
33 | sample:
34 | erl -config util/shell_app.config -args_file util/shell_vm.args -s nkmedia_app -s nkmedia_sample $(RELOADER)
35 |
36 | shell2:
37 | erl -config util/shell_app.config -args_file util/shell_vm.args $(RELOADER)
38 |
39 |
40 | docs:
41 | ./rebar skip_deps=true doc
42 |
43 |
44 | APPS = kernel stdlib sasl erts ssl tools os_mon runtime_tools crypto inets \
45 | xmerl webtool snmp public_key mnesia eunit syntax_tools compiler
46 | COMBO_PLT = $(HOME)/.$(REPO)_combo_dialyzer_plt
47 |
48 | check_plt:
49 | dialyzer --check_plt --plt $(COMBO_PLT) --apps $(APPS) deps/*/ebin
50 |
51 | build_plt:
52 | dialyzer --build_plt --output_plt $(COMBO_PLT) --apps $(APPS) deps/*/ebin
53 |
54 | dialyzer:
55 | dialyzer -Wno_return --plt $(COMBO_PLT) ebin/nkmedia*.beam #| \
56 | # fgrep -v -f ./dialyzer.ignore-warnings
57 |
58 | cleanplt:
59 | @echo
60 | @echo "Are you sure? It takes about 1/2 hour to re-build."
61 | @echo Deleting $(COMBO_PLT) in 5 seconds.
62 | @echo
63 | sleep 5
64 | rm $(COMBO_PLT)
65 |
66 |
67 | build_tests:
68 | erlc -pa ebin -pa deps/lager/ebin -o ebin -I include \
69 | +export_all +debug_info +"{parse_transform, lager_transform}" \
70 | test/*.erl
71 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # NkMEDIA
3 |
4 | **IMPORTANT** NkMEDIA is still under development, and not yet ready for general use.
5 |
6 | NkMEDIA is an scalable and flexible media server for WebRTC and SIP. Using NkMEDIA, it is easy to build powerful gateways, recorders, MCUs, SFUs, PBXs or any other media-based application. It is written in [Erlang](http://www.erlang.org).
7 |
8 | NkMEDIA is made of a very simple and efficient core, and a set of plugins and backends that extend its capabilities. At its core, it is only capable of controlling _peer to peer_ calls. However, activating plugins like _nkmedia_janus_ (based on [Janus](https://janus.conf.meetecho.com/index.html)), _nkmedia_fs_ (based on [Freeswitch](https://freeswitch.org)) and _nkmedia_kms_ (based on [Kurento](https://www.kurento.org)), it can perform complex media operations in an very simple way. Since each backend has very different characteristics, you can use the very best tool for each situation. For example, Janus is very lightweight and a great choice to write proxies and SFUs. Freeswitch has full PBX capabilities (allowing you to _park_ and _transfer_ calls to multiple destinations without starting new WebRTC sessions, detect _dtmf_ tones, etc.) and has a very powerful video MCU. Kurento is the most flexible tool to design any media processing system.
9 |
10 | NkMEDIA can be managed using [NetComposer API](https://github.com/NetComposer/nkservice/blob/luerl/doc/api_intro.md), using the same API for all backends. This means that the session creation functions, starting publishers, recordings, etc., will be nearly identical for all backends, while NkMEDIA will adapt it to the specific backend.
11 |
12 | NkMEDIA has full support for Trickle ICE and non Trickle ICE clients and servers. You can connect clients that does not support trickle at all to all-trickle backends like Kurento, and trickle clients to non-trickle servers like Freeswitch, automatically.
13 |
14 | You can control NkMEDIA through the management interface, creating any number of _sessions_. It offers a clean, very easy to use API, independent of any supported backend. You don't need to know how to install or manage Janus, Freeswitch or Kurento instances. When you order an operation to be performed on the session (like starting a proxy, recording, starting an SFU, etc.), NkMEDIA selects the right backend that supports that operation automatically and in a complete transparent way. For operations supported by several active backends (like `echo`) you can also force the selection.
15 |
16 | In real-life deployments, you will typically connect a server-side application to the management interface. However, being a websocket connection, you can also use a browser to manage sessions (its own or any other's session, if it is authorized).
17 |
18 | See the [User Guide](doc/index.md#user-guide) for a more detailed explanation of the architecture.
19 |
20 | ## Features
21 | * Full support for WebRTC and SIP-class SDPs
22 | * WebRTC P2P calls.
23 | * Proxied (server-through) calls (including SIP/WebRTC gateways, with or without transcoding).
24 | * Full Trickle ICE support. Connect trickle and non-trickle clients and backends automatically.
25 | * [MCU](https://webrtcglossary.com/mcu/) based multi audio/video conferences
26 | * [SFU](https://webrtcglossary.com/sfu/) (or mixed SFU+MCU) WebRTC distribution.
27 | * Recording (with or without transcoding).
28 | * Abstract API, independant of every specific backend.
29 | * Downloads, installs and monitors automatically instances of Janus, Freeswitch and Kurento, using [Docker](https://www.docker.com) containers.
30 | * Supports thousands of simultaneous connections, with WebRTC and SIP.
31 | * Robust and highly scalable, using all available processor cores automatically.
32 | * Sophisticated plugin mechanism, that adds very low overhead to the core.
33 | * Hot, on-the-fly core and application configuration and code upgrades.
34 | * Security-sensitive architecture. The backends do not expose any management port, only RTP traffic.
35 |
36 | ## Current Backends
37 | * [**nkmedia_janus**](doc/janus.md): Janus backend with support for webrtc echo, calls through the server, SFUs and SIP (in and out) gateways. Also dyamic muting of audio and video, bandwith control and recording.
38 | * [**nkmedia_fs**](doc/fs.md): Freeswitch backend with support for echo, calls through the server, MCUs and and SIP (in and out) gateways. SIP calls can participate in MCU sessions or be connected to webrtc endpoints.
39 | * [**nkmedia_kms**](doc/kms.md): Kurento backend with support for echo, calls through the server, SFUs and and SIP (in and out) gateways. SIP calls can participate in SFU sessions or be connected to webrtc endpoints. State of the art recording support, inmediately playable and seekable.
40 |
41 | In the [future](doc/roadmap.md), NkMEDIA will add support for:
42 | * Multi-node configurations based on [NetComposer](http://www.slideshare.net/carlosjgf/net-composer-v2).
43 | * Support for multiple Janus, Freeswitch and Kurento boxes simultaneously.
44 |
45 |
46 | # Documentation
47 |
48 | [ 1. User Guide](doc/index.md#user-guide)
49 | [ 2. API Guide](doc/index.md#management-interface)
50 | [ 3. Cookbook](doc/index.md#cookbook)
51 | [ 4. Advanced Concepts](doc/index.md#advanced)
52 | [ 5. Roadmap](doc/roadmap.md)
53 |
54 |
55 | ## Installation
56 |
57 | Currently, NkMEDIA is only available in source form. To build it, you only need Erlang (> r17).
58 | To run NkMEDIA, you also need also Docker (>1.6). The docker daemon must be configured to use TCP/TLS connections.
59 |
60 | ```
61 | git clone https://github.com/NetComposer/nkmedia
62 | cd nkmedia
63 | make
64 | ```
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/doc/concepts.md:
--------------------------------------------------------------------------------
1 | # Concepts
2 |
3 | * [Services](#services)
4 | * [Sessions](#sessions)
5 | * [Offers and Answers](#offers-and-answers)
6 | * [Calls](#calls)
7 |
8 |
9 |
10 | ## Services
11 |
12 | TBD
13 |
14 |
15 | ## Sessions
16 |
17 | Sessions are the key element in NkMEDIA. A session is multimedia communication between two parties. Each party can be an endpoint (like a browser or SIP phone) or a media processing facility (like a Proxy, SFU, MCU, etc.), that could itself be connected to other sessions.
18 |
19 | All sessions have an _offer_ and an _answer_. The session starts without offer or answer, and enters in _ready_ state when both (offer and answer) are available and have a corresponding SDP.
20 |
21 | To set the offer, you have several options:
22 | * Set a _raw_, direct SDP
23 | * Start a media processing that takes an SDP from you but generates another one for the session (like a proxy)
24 | * Start a media processing that generates the offer (like a file player)
25 |
26 | Once the offer is set, you must set the answer. Again, there are several options:
27 | * Set a _raw_, direct SDP
28 | * Start a media processing that generates the answer, based on the offer (like a MCU)
29 | * Start a _invite_ to get the answer from other party.
30 |
31 |
32 |
33 |
34 |
35 | ## Offers and Answers
36 |
37 | In NkMEDIA, _offer_ and _answer_ objects are described as a json object, with the following fields:
38 |
39 | Field|Sample|Description
40 | ---|---|---
41 | sdp|"v=0..."|SDP
42 | sdp_type|"webrtc"|Can be "webrtc" (the default) or "rtp". Informs to NkMEDIA if it is an SDP generated at a WebRTC endpoint or not.
43 | trickle_ice|false|If the SDP has no candidates and Trickle ICE must be used, it must be indicated as `trickle_ice=true`
44 |
45 |
--------------------------------------------------------------------------------
/doc/events.md:
--------------------------------------------------------------------------------
1 | # NkMEDIA API External Interface - Events
2 |
3 | This documente describes the currently supported External API events for core NkMEDIA.
4 | See the [API Introduction](intro.md) for an introduction to the interface and [API Commands](api.md) for a detailed description of available commands.
5 |
6 | Many NkMEDIA operations launch events of different types. All API connections subscribed to these events will receive them. See NkSERVICE documentation for a description of how to subscribe and receive events.
7 |
8 | See also each backend and plugin documentation:
9 |
10 | * [nkmedia_janus](janus.md)
11 | * [nkmedia_fs](fs.md)
12 | * [nkmedia_kms](kms.md)
13 | * [nkmedia_room](room.md)
14 | * [nkcollab_call](call.md)
15 | * [nkmedia_sip](sip.md)
16 | * [nkmedia_verto](verto.md)
17 |
18 | Also, for Erlang developers, you can have a look at the [event dispatcher](../src/nkmedia_api_events.erl).
19 |
20 | All events have the following structure:
21 |
22 | ```js
23 | {
24 | class: "core",
25 | cmd: "event",
26 | data: {
27 | class: "media",
28 | subclass: "session",
29 | type: "...",
30 | obj_id: "...",
31 | body: {
32 | ...
33 | }
34 | },
35 | tid: 1
36 | }
37 | ```
38 | Then `obj_id` will be the _session id_ of the session generating the event. The following _types_ are supported:
39 |
40 |
41 | Type|Body|Description
42 | ---|---|---
43 | answer|`{answer: ...}`|Fired when a session has an answer available
44 | type|{`type: ..., ...}`|The session type has been updated
45 | candidate|`{sdpMid: .., sdpMLineIndex: ..., candidate: ...}`|A new _trickle ICE_ candidate is available
46 | candidate_end|`{}`|No more _trickle ICE_ candidates arte available
47 | status|`{...}`|Some session-specific status is fired
48 | info|`{...}`|Some user-specific event is fired
49 | destroyed|`{code: Code, reason: Reason}`|The session has been stopped
50 |
51 |
52 | **Sample**
53 |
54 | ```js
55 | {
56 | class: "core",
57 | cmd: "event",
58 | data: {
59 | class: "media",
60 | subclass: "session",
61 | type: "stop",
62 | obj_id: "39ce4076-391a-1260-75db-38c9862f00d9",
63 | body: {
64 | code: 0,
65 | reason: "User stop"
66 | }
67 | },
68 | tid: 1
69 | }
70 | ```
71 |
--------------------------------------------------------------------------------
/doc/fs.md:
--------------------------------------------------------------------------------
1 | # Freeswitch Backend
2 |
3 | * [**Session Types**](#session-types)
4 | * [park](#park)
5 | * [echo](#echo)
6 | * [bridge](#bridge)
7 | * [mcu](#mcu)
8 | * [**Trickle ICE**](#trickle-ice)
9 | * [**SIP**](#sip)
10 | * [**Media update**](#media-update)
11 | * [**Type update**](#type-update)
12 | * [**Recording**](#recording)
13 | * [**Room Management**](#room-management)
14 | * [**Calls**](#calls)
15 |
16 |
17 | ## Session Types
18 |
19 | When the `nkmedia_fs` backend is selected (either manually, using the `backend: "nkmedia_fs"` option when creating the session or automatically, depending on the type), the session types described bellow can be created. Freeswitch allows two modes for all types: as an _offerer_ or as an _offeree_.
20 |
21 | As an **offerer**, you create the session without an _offer_, and instruct Freeswitch to make one either calling [get_offer](api.md#get_offer) or using `wait_reply: true` in the [session creation](api.md#create) request. Once you have the _answer_, you must call [set_answer](api.md#set_answer) to complete the session.
22 |
23 | As an **offeree**, you create the session with an offer, and you get the answer from Freeswitch either calling [get_answer](api.md#get_offer) or using `wait_reply: true` in the session creation request.
24 |
25 |
26 |
27 | ## park
28 |
29 | You can use this session type to _place_ the session at the Freeswitch mediaserver, without yet sending audio or video, and before updating it to any other type.
30 |
31 | **Sample**
32 |
33 | ```js
34 | {
35 | class: "media",
36 | subclass: "session",
37 | cmd: "start",
38 | data: {
39 | type: "park",
40 | offer: {
41 | sdp: "v=0.."
42 | },
43 | wait_reply: true
44 | }
45 | tid: 1
46 | }
47 | ```
48 | -->
49 | ```js
50 | {
51 | result: "ok",
52 | data: {
53 | session_id: "54c1b637-36fb-70c2-8080-28f07603cda8",
54 | answer: {
55 | sdp: "v=0..."
56 | }
57 | },
58 | tid: 1
59 | }
60 | ```
61 |
62 |
63 | ## echo
64 |
65 | Allows to create an _echo_ session, sending the audio and video back to the caller.
66 |
67 | **Sample**
68 |
69 | ```js
70 | {
71 | class: "media",
72 | subclass: "session",
73 | cmd: "create",
74 | data: {
75 | type: "echo",
76 | offer: {
77 | sdp: "v=0.."
78 | },
79 | wait_reply: true
80 | }
81 | tid: 1
82 | }
83 | ```
84 |
85 |
86 | ## bridge
87 |
88 | Allows to connect two different Freeswitch sessions together.
89 |
90 | Once you have a session of any type, you can start (or switch) any other session and bridge it to the first one through the server. You need to use type _bridge_ and include the field `peer_id` pointing to the first one.
91 |
92 | It is recommended to use the field `master_id` in the new second session, so that it becomes a _slave_ of the first, _master_ session. This way, if either sessions stops, the other will also stop automatically.
93 |
94 | **Sample**
95 |
96 | ```js
97 | {
98 | class: "media",
99 | subclass: "session",
100 | cmd: "create",
101 | data: {
102 | type: "bridge",
103 | peer_id: "54c1b637-36fb-70c2-8080-28f07603cda8",
104 | master_id: "54c1b637-36fb-70c2-8080-28f07603cda8",
105 | offer: {
106 | sdp: "v=0.."
107 | },
108 | wait_reply: true
109 | }
110 | tid: 1
111 | }
112 | ```
113 |
114 |
115 | ## mcu
116 |
117 | This session type connects the session to a new or existing MCU conference at a Freeswitch instance.
118 | The optional field `room_id` can be used to connect to an existing room, or create a new one with this name.
119 |
120 | **Sample**
121 |
122 | ```js
123 | {
124 | class: "media",
125 | subclass: "session",
126 | cmd: "start",
127 | data: {
128 | type: "mcu",
129 | room_id: "41605362-3955-8f28-e371-38c9862f00d9",
130 | offer: {
131 | sdp: "v=0.."
132 | },
133 | wait_reply: true
134 |
135 | }
136 | tid: 1
137 | }
138 | ```
139 | -->
140 | ```js
141 | {
142 | result: "ok",
143 | data: {
144 | session_id: "54c1b637-36fb-70c2-8080-28f07603cda8",
145 | room_id: "41605362-3955-8f28-e371-38c9862f00d9",
146 | answer: {
147 | sdp: "v=0..."
148 | }
149 | },
150 | tid: 1
151 | }
152 | ```
153 |
154 | See [Room Management](#room-management) to learn about operations that can be performed on the room.
155 |
156 |
157 | ## Trickle ICE
158 |
159 | Freeswitch has currenly no support for _trickle ICE_, however NkMEDIA is able to _emulate_ it.
160 |
161 | If you want to _trickle ICE_ when sending offfers or answers to the backend, you must use the field `trickle_ice: true`. You can now use the commands [set_candidate](api.md#set_candidate) and [set_candidate_end](api.md#set_candidate_end) to send candidates to the backend. NkMEDIA will buffer the candidates and, when either you call `set_candidate_end` or the `trickle_ice_timeout` is fired, all of them will be incorporated in the SDP and sent to Freeswitch.
162 |
163 | When Freesewitch generates an offer or answer, it will never use _trickle ICE_.
164 |
165 |
166 |
167 | ## SIP
168 |
169 | The Freeswitch backend has full support for SIP.
170 |
171 | If the offer you send in has a SIP-like SDP, you must also include the option `sdp_type: "rtp"` on it. The generated answer will also be SIP compatible. If you want Freeswitch to generate a SIP offer, use the `sdp_type: "rtp"` parameter in the session creation request. Your answer must also be then SIP compatible.
172 |
173 |
174 |
175 | ## Media update
176 |
177 | TBD
178 |
179 |
180 | ## Type udpdate
181 |
182 | Freeswitch allows you to change the session to type to any other type at any moment, calling [set_type](api.md#set_type).
183 | You can for example first `park` a call, then include it on a `bridge` or an `mcu`.
184 |
185 |
186 | ## Recording
187 |
188 | TBD
189 |
190 |
191 |
192 | ## Room management
193 |
194 | In the near future, you will be able to perform several updates over any MCU, calling [room_action](api.md#room_action). Currenly the only supported option is to change the layout of the mcu in real time:
195 |
196 | **Sample**
197 |
198 | ```js
199 | {
200 | class: "media",
201 | subclass: "session",
202 | cmd: "room_action",
203 | data: {
204 | action: "layout"
205 | room_id: "41605362-3955-8f28-e371-38c9862f00d9",
206 | layout: "2x2"
207 | }
208 | tid: 1
209 | }
210 | ```
211 |
212 |
213 | ## Calls
214 |
215 | When using the [call plugin](call.md) with this backend, the _caller_ session will be of type `park`, and the _callee_ session will have type `bridge`, connected to the first. You will get the answer for the callee inmediately.
216 |
217 | You can start several parallel destinations, and each of them is a fully independent session.
218 |
219 | You can receive and send calls to SIP endpoints.
220 |
--------------------------------------------------------------------------------
/doc/index.md:
--------------------------------------------------------------------------------
1 | # Welcome to NkMEDIA documentation
2 |
3 | ## User Guide
4 | * Introduction
5 | * Features
6 | * [Concepts](concepts.md)
7 | * Tutorial
8 | * Starting NkMEDIA
9 | * Starting a Service
10 |
11 | ## Management Interface
12 | * [Introduction](intro.md)
13 | * [Core API](api.md)
14 | * [Core API Events](events.md)
15 | * Plugins API
16 | * [nkmedia_janus](janus.md)
17 | * [nkmedia_fs](fs.md)
18 | * [nkmedia_kms](kms.md)
19 |
20 | ## Cookbook
21 | * Peer to Peer calls
22 | * Call through server
23 | * SIP gateways
24 | * Recording
25 | * SFU (Selective Forwarding Unit)
26 | * MCU (Multipoint Control Unit)
27 |
28 | ## Advanced
29 | * Plugin architecture
30 | * Docker Management
31 |
32 | ## [Roadmap](roadmap.md)
33 |
--------------------------------------------------------------------------------
/doc/intro.md:
--------------------------------------------------------------------------------
1 | # NkMEDIA External Interface Introduction
2 |
3 | TBD
4 |
5 | See [NkSERVICE API Introduction](https://github.com/NetComposer/nkservice/blob/luerl/doc/api_intro.md).
6 |
7 |
--------------------------------------------------------------------------------
/doc/janus.md:
--------------------------------------------------------------------------------
1 | # Janus Backend
2 |
3 | This document describes the characteristics of the Janus backend
4 |
5 | * [**Session Types**](#session-types)
6 | * [echo](#echo)
7 | * [bridge](#bridge) and [sip](#sip) gateways
8 | * [publish](#publish)
9 | * [listen](#listen)
10 | * [**Trickle ICE**](#trickle-ice)
11 | * [**Media update**](#media-update)
12 | * [**Type update**](#type-update)
13 | * [**Recording**](#recording)
14 | * [**Calls*](#calls)
15 |
16 |
17 | ## Session Types
18 |
19 | When the `nkmedia_janus` backend is selected (either manually, using the `backend: "nkmedia_janus"` option when creating the session or automatically, depending on the type), the following session types can be created:
20 |
21 | ## echo
22 |
23 | Allows to create an _echo_ session, sending the audio and video back to the caller. The caller must include the _offer_ in the [session creation request](api.md#create), and you must use [get_answer](api.md#get_answer) to get the answer (or use `wait_reply: true` in the request).
24 |
25 | The available [media updates](#media-update) can also be included in the creation request.
26 |
27 | **Sample**
28 |
29 | ```js
30 | {
31 | class: "media",
32 | subclass: "session",
33 | cmd: "create",
34 | data: {
35 | type: "echo",
36 | offer: {
37 | sdp: "v=0.."
38 | },
39 | wait_reply: true,
40 | mute_audio: true
41 | }
42 | tid: 1
43 | }
44 | ```
45 | -->
46 | ```js
47 | {
48 | result: "ok",
49 | data: {
50 | session_id: "54c1b637-36fb-70c2-8080-28f07603cda8",
51 | answer: {
52 | sdp: "v=0..."
53 | }
54 | },
55 | tid: 1
56 | }
57 | ```
58 |
59 | ## **bridge**
60 |
61 | Allows you to connect a pair of sessions throgh the server, managing medias, bandwidth and recording.
62 |
63 | Because of the way Janus works, you must first creata a session with type _proxy_ with the _offer_ from the _caller_. You must then start a second session for the callee, now with type `bridge`, without any offer, but including the field `peer_id` pointing to the first session.
64 |
65 | You must then get the offer for the second session (calling [get_offer](api.md#get_offer)), and send it to the _callee_. When you have the callee answer (or hangup) you must send it to the slave session (calling [set_answer](api.md#set_answer)). The first session (type _proxy_) will then generate the answer for the caller, and will chnage itself to type _bridge_ also. You can now either wait for the answer event for the first session or call [get_answer](api.md#get_answer) on it.
66 |
67 | The available [media updates](#media-update) can also be included in the creation request.
68 |
69 | It is recommended to use the field `master_id` in the second (bridge) session, so that it becomes a _slave_ of the first, _master_ session. This way, if either sessions stops, the other will also stop automatically.
70 |
71 |
72 | Samples TBD
73 |
74 |
75 | ### SIP
76 |
77 | The proxy-bridge combo is also capable of connect SIP and WebRTC sessions together.
78 |
79 | You can make a SIP-to-WebRTC gateway using an offer with a _SIP-type_ SDP, and using `sdp_type: "rtp"` in it. The generated _master_ answer will also be SIP-like, ready to be sent to the calling SIP device.
80 |
81 | To make a WebRTC-to-SIP gateway, you must use the option `sdp_type: "rtp"` in the session creation request. The _proxy offer_ you get will then be SIP-like. When the remote SIP party answers, you must call [set_answer](api.md#set_answer) as usual.
82 |
83 | You cannot however use any media upates on a SIP proxy session.
84 |
85 | See the [call plugin](call.md) to be able to use NkMEDIA's SIP signalling.
86 |
87 |
88 | ## publish
89 |
90 | Allows you to _publish_ a session with audio/video/data to a _room_, working as an _SFU_ (_selective forwarding unit_). Any number of listeners can then be connected to this session.
91 |
92 | If you don't include a room, a new one will be created automatically (using options `room_audiocodec`, `room_videocodec` and `room_bitrate`). If you include a room, it must already exist.
93 |
94 | The available [media updates](#media-update) can also be included in the creation request.
95 |
96 |
97 | Field|Default|Description
98 | ---|---|---
99 | room_id|(automatic)|Room to use
100 | room_audio_codec|`"opus"`|Forces audio codec (`opus`, `isac32`, `isac16`, `pcmu`, `pcma`)
101 | room_video_codec|`"vp8"`|Forces video codec (`vp8`, `vp9`, `h264`)
102 | room_bitrate|`0`|Bitrate for the room (kbps, 0:unlimited)
103 |
104 | **Sample**
105 |
106 | ```js
107 | {
108 | class: "media",
109 | subclass: "session",
110 | cmd: "create",
111 | data: {
112 | type: "publish",
113 | offer: {
114 | sdp: "v=0.."
115 | },
116 | wait_reply: true,
117 | room_video_codec: "vp9"
118 | }
119 | tid: 1
120 | }
121 | ```
122 | -->
123 | ```js
124 | {
125 | result: "ok",
126 | data: {
127 | session_id: "54c1b637-36fb-70c2-8080-28f07603cda8",
128 | room: "bbd48487-3783-f511-ee41-28f07603cda8",
129 | answer: {
130 | sdp: "v=0..."
131 | }
132 | },
133 | tid: 1
134 | }
135 | ```
136 |
137 |
138 | ## listen
139 |
140 | Allows you to _listen_ to a previously started publisher, working as an _SFU_ (selective forwarding unit). You must tell the `publisher id` and the room will be found automatically.
141 |
142 | You must not include any offer in the [session creation request](api.md#create), because Janus will make one for you. You must then supply an _answer_ calling the [set answer](api.md#set_answer) command.
143 |
144 |
145 | **Sample**
146 |
147 | ```js
148 | {
149 | class: "media",
150 | subclass: "session",
151 | cmd: "create",
152 | data: {
153 | type: "listen",
154 | publisher_id: "54c1b637-36fb-70c2-8080-28f07603cda8",
155 | wait_reply: true
156 | }
157 | tid: 1
158 | }
159 | ```
160 | -->
161 | ```js
162 | {
163 | result: "ok",
164 | data: {
165 | session_id: "2052a043-3785-de87-581b-28f07603cda8",
166 | offer: {
167 | sdp: "v=0..."
168 | }
169 | },
170 | tid: 1
171 | }
172 | ```
173 |
174 | You must now set the answer:
175 |
176 | ```js
177 | {
178 | class: "media",
179 | subclass: "session",
180 | cmd: "set_answer",
181 | data: {
182 | session_id: "2052a043-3785-de87-581b-28f07603cda8",
183 | answer: {
184 | sdp: "v=0..."
185 | }
186 | }
187 | tid: 2
188 | }
189 | ```
190 | -->
191 | ```js
192 | {
193 | result: "ok",
194 | tid: 2
195 | }
196 | ```
197 |
198 | ## Trickle ICE
199 |
200 | When sending an offer or answer to the backend, it can include all candidates in the SDP or you can use _trickle ICE_. In this case, you must not include the candidates in the SDP, but use the field `trickle_ice: true`. You can now use the commands [set_candidate](api.md#set_candidate) and [set_candidate_end](api.md#set_candidate_end) to send candidates to the backend.
201 |
202 | When Janus generates an offer or answer, it will never use _trickle ICE_.
203 |
204 |
205 |
206 | ## Media update
207 |
208 | This backend allows to you perform, at any moment and in all session types (except SIP `proxy`) the following [media updates](api.md#update_media):
209 |
210 | * `mute_audio`: Mute the outgoing audio.
211 | * `mute_video`: Mute the outgoing video.
212 | * `mute_data`: Mute the outgoing data channel.
213 | * `bitrate`: Limit the incoming bandwidth
214 |
215 | **Sample**
216 |
217 | ```js
218 | {
219 | class: "media",
220 | subclass: "session",
221 | cmd: "update_media",
222 | data: {
223 | mute_audio: true,
224 | bitrate: 100000
225 | }
226 | tid: 1
227 | }
228 | ```
229 |
230 |
231 | ## Type udpdate
232 |
233 | Then only [type update](api.md#set_type) that the Janus backend supports is changing a `listen` session type to another `listen` type, but pointing to a publisher on the same room.
234 |
235 |
236 | **Sample**
237 |
238 | ```js
239 | {
240 | class: "media",
241 | class: "session",
242 | cmd: "set_type",
243 | data: {
244 | session_id: "2052a043-3785-de87-581b-28f07603cda8",
245 | type: "listen"
246 | publisher: "29f394bf-3785-e5b1-bb56-28f07603cda8"
247 | }
248 | tid: 1
249 | }
250 | ```
251 |
252 |
253 | ## Recording
254 |
255 | At any moment, and in all session types (except SIP `proxy`) you can order to start or stop recording of the session, using the [recorder_action](api.md#recorder_action) command.
256 |
257 | To start recording, use the `start` action.
258 |
259 |
260 | **Sample**
261 |
262 | ```js
263 | {
264 | class: "media",
265 | class: "session",
266 | cmd: "recorder_action",
267 | data: {
268 | session_id: "2052a043-3785-de87-581b-28f07603cda8",
269 | action: start
270 | }
271 | tid: 1
272 | }
273 | ```
274 |
275 | To stop recording, use the `stop` action.
276 |
277 | TBD: how to access the file
278 |
279 |
280 | ## Calls
281 |
282 | When using the [call plugin](call.md) with this backend, the _caller_ session will be of type `proxy`, and the _callee_ session will have type `bridge`, connected to the first.
283 |
284 | You can start several parallel destinations but, since all of them will share the same _offer_, you should only use the offer once your _acccept_ has been accepted.
285 |
286 | You can receive and send calls to SIP endpoints.
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
--------------------------------------------------------------------------------
/doc/roadmap.md:
--------------------------------------------------------------------------------
1 | # Roadmap
2 |
3 | ## v0.1
4 |
5 | * ~~Freeswitch integration~~.
6 | * ~~Janus Integration~~.
7 | * Kurento Integration.
8 | * Demostration Client.
9 |
10 |
11 | ## No date
12 |
13 | * Multi-node cluster support.
14 | * Multiple backend boxes support.
15 | * More application examples.
16 | * Better statistics support.
17 | * Admin web console.
18 | * [Matrix](http://matrix.org) support
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/include/nkmedia.hrl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2015 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | -ifndef(NKMEDIA_HRL_).
22 | -define(NKMEDIA_HRL_, 1).
23 |
24 | %% ===================================================================
25 | %% Defines
26 | %% ===================================================================
27 |
28 |
29 | % -define(WS_TIMEOUT, 60*60*1000).
30 |
31 | -define(SUPPORTED_FS, [<<"v1.6.5-r01">>]).
32 | -define(SUPPORTED_JANUS, [<<"master-r01">>]).
33 |
34 | -define(DEF_WAIT_TIMEOUT, 60).
35 | -define(DEF_READY_TIMEOUT, 24*60*60).
36 | -define(DEF_RING_TIMEOUT, 30).
37 | -define(MAX_RING_TIMEOUT, 180).
38 |
39 |
40 |
41 | -define(DEF_SYNC_TIMEOUT, 30000).
42 |
43 | -define(FS_DEF_BASE, 50000).
44 | -define(JANUS_DEF_BASE, 50010).
45 |
46 | -define(SESSION(Map, Session), maps:merge(Session, Map)).
47 | -define(SESSION_RM(Key, Session), maps:remove(Key, Session)).
48 |
49 |
50 | %% ===================================================================
51 | %% Records
52 | %% ===================================================================
53 |
54 |
55 |
56 |
57 | -endif.
58 |
59 |
--------------------------------------------------------------------------------
/include/nkmedia_call.hrl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2015 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | -ifndef(NKMEDIA_CALL_HRL_).
22 | -define(NKMEDIA_CALL_HRL_, 1).
23 |
24 | %% ===================================================================
25 | %% Defines
26 | %% ===================================================================
27 |
28 |
29 | -define(CALL(Map, Call), maps:merge(Call, Map)).
30 | -define(CALL_RM(Key, Call), maps:remove(Key, Call)).
31 | -define(CALL_MERGE(Map, Call), maps:merge(Map, Call)).
32 |
33 |
34 |
35 | %% ===================================================================
36 | %% Records
37 | %% ===================================================================
38 |
39 |
40 |
41 |
42 | -endif.
43 |
44 |
--------------------------------------------------------------------------------
/include/nkmedia_room.hrl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2015 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | -ifndef(NKMEDIA_ROOM_HRL_).
22 | -define(NKMEDIA_ROOM_HRL_, 1).
23 |
24 | %% ===================================================================
25 | %% Defines
26 | %% ===================================================================
27 |
28 |
29 | -define(ROOM(Map, Room), maps:merge(Room, Map)).
30 | -define(ROOM_RM(Key, Room), maps:remove(Key, Room)).
31 | -define(ROOM_MERGE(Map, Room), maps:merge(Map, Room)).
32 |
33 |
34 |
35 | %% ===================================================================
36 | %% Records
37 | %% ===================================================================
38 |
39 |
40 |
41 |
42 | -endif.
43 |
44 |
--------------------------------------------------------------------------------
/priv/www:
--------------------------------------------------------------------------------
1 | ../../../NkSystem/util/nkmedia/www/
--------------------------------------------------------------------------------
/rebar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NetComposer/nkmedia/24480866a523bfd6490abfe90ea46c6130ffe51f/rebar
--------------------------------------------------------------------------------
/rebar.config:
--------------------------------------------------------------------------------
1 | % {lib_dirs, ["deps"]}.
2 |
3 | {erl_opts, [
4 | % native,
5 | debug_info,
6 | fail_on_warning,
7 | {parse_transform, lager_transform}
8 | ]}.
9 |
10 | {cover_enabled, true}.
11 | {cover_export_enabled, true}.
12 |
13 |
14 | {deps, [
15 | {nkservice, ".*", {git, "https://github.com/netcomposer/nkservice.git", {branch, "luerl"}}},
16 | {nksip, ".*", {git, "https://github.com/netcomposer/nksip.git", {branch, "develop"}}}
17 | ]}.
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/fs_backend/nkmedia_fs.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc NkMEDIA application
22 |
23 | -module(nkmedia_fs).
24 | -author('Carlos Gonzalez ').
25 | -export_type([id/0, stats/0]).
26 |
27 |
28 | %% ===================================================================
29 | %% Types
30 | %% ===================================================================
31 |
32 | -type id() :: nkmedia_fs_engine:id().
33 |
34 |
35 | -type stats() :: #{
36 | idle => integer(),
37 | max_sessions => integer(),
38 | session_count => integer(),
39 | session_peak_five_mins => integer(),
40 | session_peak_max => integer(),
41 | sessions_sec => integer(),
42 | sessions_sec_five_mins => integer(),
43 | sessions_sec_max => integer(),
44 | all_sessions => integer(),
45 | uptime => integer()
46 | }.
47 |
48 |
49 |
50 | %% ===================================================================
51 | %% Public functions
52 | %% ===================================================================
53 |
54 |
--------------------------------------------------------------------------------
/src/fs_backend/nkmedia_fs_api_syntax.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc
22 | -module(nkmedia_fs_api_syntax).
23 | -author('Carlos Gonzalez ').
24 |
25 | -export([syntax/5]).
26 |
27 | % -include_lib("nkservice/include/nkservice.hrl").
28 |
29 |
30 |
31 | %% ===================================================================
32 | %% Syntax
33 | %% ===================================================================
34 |
35 |
36 | %% @private
37 | syntax(_Sub, _Cmd, Syntax, Defaults, Mandatory) ->
38 | {Syntax, Defaults, Mandatory}.
39 |
40 |
--------------------------------------------------------------------------------
/src/fs_backend/nkmedia_fs_callbacks.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc Plugin implementig the Freeswitch backend
22 | -module(nkmedia_fs_callbacks).
23 | -author('Carlos Gonzalez ').
24 |
25 | -export([plugin_deps/0, plugin_group/0, plugin_syntax/0, plugin_config/2,
26 | plugin_start/2, plugin_stop/2]).
27 | -export([error_code/1]).
28 | -export([nkmedia_fs_get_mediaserver/1]).
29 | -export([nkmedia_session_start/3, nkmedia_session_stop/2,
30 | nkmedia_session_offer/4, nkmedia_session_answer/4, nkmedia_session_cmd/3,
31 | nkmedia_session_handle_call/3, nkmedia_session_handle_cast/2]).
32 | -export([api_syntax/4]).
33 | -export([nkdocker_notify/2]).
34 |
35 | -include_lib("nkservice/include/nkservice.hrl").
36 |
37 |
38 |
39 | %% ===================================================================
40 | %% Types
41 | %% ===================================================================
42 |
43 | % -type continue() :: continue | {continue, list()}.
44 |
45 |
46 |
47 | %% ===================================================================
48 | %% Plugin callbacks
49 | %% ===================================================================
50 |
51 |
52 | plugin_deps() ->
53 | [nkmedia, nkmedia_room].
54 |
55 |
56 | plugin_group() ->
57 | nkmedia_backends.
58 |
59 |
60 | plugin_syntax() ->
61 | #{
62 | fs_docker_image => fun parse_image/3
63 | }.
64 |
65 |
66 | plugin_config(Config, _Service) ->
67 | Cache = case Config of
68 | #{fs_docker_image:=FsConfig} -> FsConfig;
69 | _ -> nkmedia_fs_build:defaults(#{})
70 | end,
71 | {ok, Config, Cache}.
72 |
73 |
74 | plugin_start(Config, #{name:=Name}) ->
75 | lager:info("Plugin NkMEDIA Freeswitch (~s) starting", [Name]),
76 | case nkdocker_monitor:register(?MODULE) of
77 | {ok, DockerMonId} ->
78 | nkmedia_app:put(docker_fs_mon_id, DockerMonId),
79 | lager:info("Installed images: ~s",
80 | [nklib_util:bjoin(find_images(DockerMonId))]);
81 | {error, Error} ->
82 | lager:error("Could not start Docker Monitor: ~p", [Error]),
83 | erlang:error(docker_monitor)
84 | end,
85 | {ok, Config}.
86 |
87 |
88 | plugin_stop(Config, #{name:=Name}) ->
89 | lager:info("Plugin NkMEDIA Freeswitch (~p) stopping", [Name]),
90 | nkdocker_monitor:unregister(?MODULE),
91 | {ok, Config}.
92 |
93 |
94 |
95 | %% ===================================================================
96 | %% Offering Callbacks
97 | %% ===================================================================
98 |
99 |
100 | %% @private
101 | -spec nkmedia_fs_get_mediaserver(nkservice:id()) ->
102 | {ok, nkmedia_fs_engine:id()} | {error, term()}.
103 |
104 | nkmedia_fs_get_mediaserver(SrvId) ->
105 | case nkmedia_fs_engine:get_all(SrvId) of
106 | [{FsId, _}|_] ->
107 | {ok, FsId};
108 | [] ->
109 | {error, no_mediaserver}
110 | end.
111 |
112 |
113 |
114 |
115 | %% ===================================================================
116 | %% Implemented Callbacks - error
117 | %% ===================================================================
118 |
119 | %% @private See nkservice_callbacks
120 | error_code({fs_error, Error}) -> {302001, "Freeswitch error: ~s", [Error]};
121 | error_code(fs_invite_error) -> {302002, "Freeswitch invite error"};
122 | error_code(fs_get_answer_error) -> {302003, "Freeswitch get answer error"};
123 | error_code(fs_get_offer_error) -> {302004, "Freeswitch get offer error"};
124 | error_code(fs_channel_parked) -> {302005, "Freeswitch channel parked"};
125 | error_code(fs_channel_stop) -> {302006, "Freeswitch channel stop"};
126 | error_code(fs_transfer_error) -> {302007, "Freeswitch transfer error"};
127 | error_code(fs_bridge_error) -> {302008, "Freeswitch bridge error"};
128 | error_code(_) -> continue.
129 |
130 |
131 | %% ===================================================================
132 | %% Implemented Callbacks - nkmedia_session
133 | %% ===================================================================
134 |
135 |
136 | %% @private
137 | nkmedia_session_start(Type, Role, Session) ->
138 | case maps:get(backend, Session, nkmedia_fs) of
139 | nkmedia_fs ->
140 | nkmedia_fs_session:start(Type, Role, Session);
141 | _ ->
142 | continue
143 | end.
144 |
145 |
146 | %% @private
147 | nkmedia_session_offer(Type, Role, Offer, #{nkmedia_fs_id:=_}=Session) ->
148 | nkmedia_fs_session:offer(Type, Role, Offer, Session);
149 |
150 | nkmedia_session_offer(_Type, _Role, _Offer, _Session) ->
151 | continue.
152 |
153 |
154 | %% @private
155 | nkmedia_session_answer(Type, Role, Answer, #{nkmedia_fs_id:=_}=Session) ->
156 | nkmedia_fs_session:answer(Type, Role, Answer, Session);
157 |
158 | nkmedia_session_answer(_Type, _Role, _Answer, _Session) ->
159 | continue.
160 |
161 |
162 | %% @private
163 | nkmedia_session_cmd(Update, Opts, #{nkmedia_fs_id:=_}=Session) ->
164 | nkmedia_fs_session:cmd(Update, Opts, Session);
165 |
166 | nkmedia_session_cmd(_Update, _Opts, _Session) ->
167 | continue.
168 |
169 |
170 | %% @private
171 | nkmedia_session_stop(Reason, #{nkmedia_fs_id:=_}=Session) ->
172 | nkmedia_fs_session:stop(Reason, Session);
173 |
174 | nkmedia_session_stop(_Reason, _Session) ->
175 | continue.
176 |
177 |
178 | %% @private
179 | nkmedia_session_handle_call({nkmedia_fs, Msg}, From, Session) ->
180 | nkmedia_fs_session:handle_call(Msg, From, Session);
181 |
182 | nkmedia_session_handle_call(_Msg, _From, _Session) ->
183 | continue.
184 |
185 |
186 | %% @private
187 | nkmedia_session_handle_cast({nkmedia_fs, Msg}, Session) ->
188 | nkmedia_fs_session:handle_cast(Msg, Session);
189 |
190 | nkmedia_session_handle_cast(_Msg, _Session) ->
191 | continue.
192 |
193 |
194 | %% ===================================================================
195 | %% API
196 | %% ===================================================================
197 |
198 | %% @private
199 | api_syntax(#api_req{class = <<"media">>}=Req, Syntax, Defaults, Mandatory) ->
200 | #api_req{subclass=Sub, cmd=Cmd} = Req,
201 | {S2, D2, M2} = nkmedia_fs_api_syntax:syntax(Sub, Cmd, Syntax, Defaults, Mandatory),
202 | {continue, [Req, S2, D2, M2]};
203 |
204 | api_syntax(_Req, _Syntax, _Defaults, _Mandatory) ->
205 | continue.
206 |
207 |
208 | %% ===================================================================
209 | %% Docker Monitor Callbacks
210 | %% ===================================================================
211 |
212 | nkdocker_notify(MonId, {Op, {<<"nk_fs_", _/binary>>=Name, Data}}) ->
213 | nkmedia_fs_docker:notify(MonId, Op, Name, Data);
214 |
215 | nkdocker_notify(_MonId, _Op) ->
216 | ok.
217 |
218 |
219 |
220 | %% ===================================================================
221 | %% Internal
222 | %% ===================================================================
223 |
224 |
225 | %% @private
226 | parse_image(_Key, Map, _Ctx) when is_map(Map) ->
227 | {ok, Map};
228 |
229 | parse_image(_, Image, _Ctx) ->
230 | case binary:split(Image, <<"/">>) of
231 | [Comp, <<"nk_freeswitch:", Tag/binary>>] ->
232 | [Vsn, Rel] = binary:split(Tag, <<"-">>),
233 | Def = #{comp=>Comp, vsn=>Vsn, rel=>Rel},
234 | {ok, nkmedia_fs_build:defaults(Def)};
235 | _ ->
236 | error
237 | end.
238 |
239 |
240 | %% @private
241 | find_images(MonId) ->
242 | {ok, Docker} = nkdocker_monitor:get_docker(MonId),
243 | {ok, Images} = nkdocker:images(Docker),
244 | Tags = lists:flatten([T || #{<<"RepoTags">>:=T} <- Images]),
245 | lists:filter(
246 | fun(Img) -> length(binary:split(Img, <<"/nk_freeswitch_">>))==2 end, Tags).
247 |
--------------------------------------------------------------------------------
/src/fs_backend/nkmedia_fs_docker.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc NkMEDIA Docker management application
22 | -module(nkmedia_fs_docker).
23 | -author('Carlos Gonzalez ').
24 |
25 | -export([start/1, stop/1, stop_all/0]).
26 | -export([notify/4]).
27 |
28 | -include("../../include/nkmedia.hrl").
29 |
30 | %% ===================================================================
31 | %% Types
32 | %% ===================================================================
33 |
34 | %% ===================================================================
35 | %% Freeswitch Instance
36 | %% ===================================================================
37 |
38 |
39 | %% @doc Starts a FS instance
40 | %% BASE+0: Event port
41 | %% BASE+1: WS verto port
42 | %% BASE+2: SIP Port
43 | -spec start(nkservice:name()) ->
44 | {ok, Name::binary()} | {error, term()}.
45 |
46 | start(Service) ->
47 | try
48 | SrvId = case nkservice_srv:get_srv_id(Service) of
49 | {ok, SrvId0} -> SrvId0;
50 | not_found -> throw(unknown_service)
51 | end,
52 | Config = nkservice_srv:get_item(SrvId, config_nkmedia_fs),
53 | BasePort = 35000, %crypto:rand_uniform(32768, 65535),
54 | Pass = nklib_util:luid(),
55 | Image = nkmedia_fs_build:run_name(Config),
56 | ErlangIp = nklib_util:to_host(nkmedia_app:get(erlang_ip)),
57 | FsIp = nklib_util:to_host(nkmedia_app:get(docker_ip)),
58 | Name = list_to_binary([
59 | "nk_fs_",
60 | nklib_util:to_binary(SrvId), "_",
61 | nklib_util:to_binary(BasePort)
62 | ]),
63 | LogDir = <<(nkmedia_app:get(log_dir))/binary, $/, Name/binary>>,
64 | ExtIp = nklib_util:to_host(nkpacket_app:get(ext_ip)),
65 | Env = [
66 | {"NK_FS_IP", FsIp},
67 | {"NK_ERLANG_IP", ErlangIp},
68 | {"NK_RTP_IP", "$${local_ip_v4}"},
69 | {"NK_EXT_IP", ExtIp},
70 | {"NK_BASE", nklib_util:to_binary(BasePort)},
71 | {"NK_PASS", nklib_util:to_binary(Pass)},
72 | {"NK_SRV_ID", nklib_util:to_binary(SrvId)}
73 | ],
74 | Labels = [
75 | {"nkmedia", "freeswitch"}
76 | ],
77 | % Cmds = ["bash"],
78 | Cmds = ["bash", "/usr/local/freeswitch/start.sh"],
79 | DockerOpts1 = #{
80 | name => Name,
81 | env => Env,
82 | cmds => Cmds,
83 | net => host,
84 | interactive => true,
85 | labels => Labels,
86 | volumes => [{LogDir, "/usr/local/freeswitch/log"}]
87 | },
88 | DockerOpts2 = case nkmedia_app:get(docker_log) of
89 | undefined -> DockerOpts1;
90 | DockerLog -> DockerOpts1#{docker_log=>DockerLog}
91 | end,
92 | DockerPid = case get_docker_pid() of
93 | {ok, DockerPid0} -> DockerPid0;
94 | {error, Error1} -> throw(Error1)
95 | end,
96 | nkdocker:rm(DockerPid, Name),
97 | case nkdocker:create(DockerPid, Image, DockerOpts2) of
98 | {ok, _} -> ok;
99 | {error, Error2} -> throw(Error2)
100 | end,
101 | lager:info("NkMEDIA FS Docker: starting instance ~s", [Name]),
102 | case nkdocker:start(DockerPid, Name) of
103 | ok ->
104 | {ok, Name};
105 | {error, Error3} ->
106 | {error, Error3}
107 | end
108 | catch
109 | throw:Throw -> {error, Throw}
110 | end.
111 |
112 |
113 |
114 | %% @doc Stops a FS instance
115 | -spec stop(binary()) ->
116 | ok | {error, term()}.
117 |
118 | stop(Name) ->
119 | case get_docker_pid() of
120 | {ok, DockerPid} ->
121 | case nkdocker:kill(DockerPid, Name) of
122 | ok -> ok;
123 | {error, {not_found, _}} -> ok;
124 | E1 -> lager:warning("NkMEDIA could not kill ~s: ~p", [Name, E1])
125 | end,
126 | case nkdocker:rm(DockerPid, Name) of
127 | ok -> ok;
128 | {error, {not_found, _}} -> ok;
129 | E2 -> lager:warning("NkMEDIA could not remove ~s: ~p", [Name, E2])
130 | end,
131 | ok;
132 | {error, Error} ->
133 | {error, Error}
134 | end.
135 |
136 |
137 | %% @doc
138 | stop_all() ->
139 | case get_docker_pid() of
140 | {ok, DockerPid} ->
141 | {ok, List} = nkdocker:ps(DockerPid),
142 | lists:foreach(
143 | fun(#{<<"Names">>:=[<<"/", Name/binary>>]}) ->
144 | case Name of
145 | <<"nk_fs_", _/binary>> ->
146 | lager:info("Stopping ~s", [Name]),
147 | stop(Name);
148 | _ ->
149 | ok
150 | end
151 | end,
152 | List);
153 | {error, Error} ->
154 | {error, Error}
155 | end.
156 |
157 |
158 | %% @private
159 | -spec get_docker_pid() ->
160 | {ok, pid()} | {error, term()}.
161 |
162 | get_docker_pid() ->
163 | DockerMonId = nkmedia_app:get(docker_fs_mon_id),
164 | nkdocker_monitor:get_docker(DockerMonId).
165 |
166 |
167 |
168 | %% @private
169 | notify(MonId, ping, Name, Data) ->
170 | notify(MonId, start, Name, Data);
171 |
172 | notify(MonId, start, Name, Data) ->
173 | case Data of
174 | #{
175 | name := Name,
176 | labels := #{<<"nkmedia">> := <<"freeswitch">>},
177 | env := #{
178 | <<"NK_FS_IP">> := Host,
179 | <<"NK_BASE">> := Base,
180 | <<"NK_PASS">> := Pass,
181 | <<"NK_SRV_ID">> := SrvId
182 | },
183 | image := Image
184 | } ->
185 | case binary:split(Image, <<"/">>) of
186 | [Comp, <<"nk_freeswitch:", Tag/binary>>] ->
187 | [Vsn, Rel] = binary:split(Tag, <<"-">>),
188 | Config = #{
189 | srv_id => nklib_util:to_atom(SrvId),
190 | name => Name,
191 | comp => Comp,
192 | vsn => Vsn,
193 | rel => Rel,
194 | host => Host,
195 | base => nklib_util:to_integer(Base),
196 | pass => Pass
197 | },
198 | connect_fs(MonId, Config);
199 | _ ->
200 | lager:warning("Started unrecognized freeswitch")
201 | end;
202 | _ ->
203 | lager:warning("Started unrecognized freeswitch")
204 | end;
205 |
206 | notify(MonId, stop, Name, Data) ->
207 | case Data of
208 | #{
209 | name := Name,
210 | labels := #{<<"nkmedia">> := <<"freeswitch">>}
211 | } ->
212 | remove_fs(MonId, Name);
213 | _ ->
214 | ok
215 | end;
216 |
217 | notify(_MonId, stats, Name, Stats) ->
218 | nkmedia_fs_engine:stats(Name, Stats).
219 |
220 |
221 |
222 | %% ===================================================================
223 | %% Internal
224 | %% ===================================================================
225 |
226 | %% @private
227 | connect_fs(MonId, #{name:=Name}=Config) ->
228 | spawn(
229 | fun() ->
230 | timer:sleep(2000),
231 | case nkmedia_fs_engine:connect(Config) of
232 | {ok, _Pid} ->
233 | ok = nkdocker_monitor:start_stats(MonId, Name);
234 | {error, {already_started, _Pid}} ->
235 | ok;
236 | {error, Error} ->
237 | lager:warning("Could not connect to Freeswitch ~s: ~p",
238 | [Name, Error])
239 | end
240 | end),
241 | ok.
242 |
243 |
244 | %% @private
245 | remove_fs(MonId, Name) ->
246 | spawn(
247 | fun() ->
248 | nkmedia_fs_engine:stop(Name),
249 | case nkdocker_monitor:get_docker(MonId) of
250 | {ok, Pid} ->
251 | nkdocker:rm(Pid, Name);
252 | _ ->
253 | ok
254 | end
255 | end),
256 | ok.
257 |
258 |
--------------------------------------------------------------------------------
/src/fs_backend/nkmedia_fs_util.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc
22 | -module(nkmedia_fs_util).
23 | -author('Carlos Gonzalez ').
24 |
25 | -export([verto_class/1, verto_req/3, verto_resp/2, verto_error/3, verto_error/4]).
26 |
27 |
28 | %% ===================================================================
29 | %% Public
30 | %% ===================================================================
31 |
32 |
33 | %% @doc Get message class
34 | -spec verto_class(map()) ->
35 | event |
36 | {{req, binary()}, integer()} |
37 | {{resp, {ok, binary()}}, integer()} |
38 | {{resp, {error, integer(), binary()}}, integer()} |
39 | unknown.
40 |
41 | verto_class(#{<<"method">>:=<<"verto.event">>, <<"jsonrpc">>:=<<"2.0">>}) ->
42 | event;
43 |
44 | verto_class(#{<<"method">>:=Method, <<"id">>:=Id, <<"jsonrpc">>:=<<"2.0">>}) ->
45 | {{req, Method}, nklib_util:to_integer(Id)};
46 |
47 | verto_class(#{<<"error">>:=Error, <<"id">>:=Id, <<"jsonrpc">>:=<<"2.0">>}) ->
48 | Code = maps:get(<<"code">>, Error, 0),
49 | Msg = maps:get(<<"message">>, Error, <<>>),
50 | {{resp, {error, Code, Msg}}, nklib_util:to_integer(Id)};
51 |
52 | verto_class(#{<<"result">>:=Result, <<"id">>:=Id, <<"jsonrpc">>:=<<"2.0">>}) ->
53 | Msg1 = maps:get(<<"message">>, Result, <<>>),
54 | Msg2 = case Msg1 of
55 | <<>> -> maps:get(<<"method">>, Result, <<"none">>);
56 | _ -> Msg1
57 | end,
58 | {{resp, {ok, Msg2}}, nklib_util:to_integer(Id)};
59 |
60 | verto_class(Msg) ->
61 | lager:warning("Unknown verto message: ~p", [Msg]),
62 | unknown.
63 |
64 |
65 | %% @doc
66 | verto_req(Id, Method, Params) ->
67 | #{
68 | <<"id">> => Id,
69 | <<"jsonrpc">> => <<"2.0">>,
70 | <<"method">> => Method,
71 | <<"params">> => Params
72 | }.
73 |
74 |
75 | %% @private
76 | verto_resp(Method, Msg) when is_binary(Method) ->
77 | verto_resp(#{<<"method">> => Method}, Msg);
78 |
79 | verto_resp(Result, #{<<"id">>:=Id}) when is_map(Result) ->
80 | #{
81 | <<"id">> => Id,
82 | <<"jsonrpc">> => <<"2.0">>,
83 | <<"result">> => Result
84 | }.
85 |
86 |
87 | %% @private
88 | verto_error(Code, Txt, Msg) ->
89 | verto_error(Code, Txt, <<>>, Msg).
90 |
91 |
92 | %% @private
93 | verto_error(Code, Txt, Method, #{<<"id">>:=Id}) ->
94 | Error1 = #{
95 | <<"code">> => Code,
96 | <<"message">> => nklib_util:to_binary(Txt)
97 | },
98 | Error2 = case Method of
99 | <<>> -> Error1;
100 | _ -> Error1#{<<"method">> => nklib_util:to_binary(Method)}
101 | end,
102 | #{
103 | <<"id">> => Id,
104 | <<"jsonrpc">> => <<"2.0">>,
105 | <<"error">> => Error2
106 | }.
107 |
108 |
109 |
110 |
111 |
112 |
--------------------------------------------------------------------------------
/src/fs_backend/nkmedia_fs_verto_proxy.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc Plugin implementing a Kurento proxy server for testing
22 | -module(nkmedia_fs_verto_proxy).
23 | -author('Carlos Gonzalez ').
24 |
25 | -export([plugin_deps/0, plugin_syntax/0, plugin_listen/2,
26 | plugin_start/2, plugin_stop/2]).
27 | -export([nkmedia_fs_verto_proxy_init/2, nkmedia_fs_verto_proxy_find_fs/2,
28 | nkmedia_fs_verto_proxy_in/2, nkmedia_fs_verto_proxy_out/2,
29 | nkmedia_fs_verto_proxy_terminate/2, nkmedia_fs_verto_proxy_handle_call/3,
30 | nkmedia_fs_verto_proxy_handle_cast/2, nkmedia_fs_verto_proxy_handle_info/2]).
31 |
32 |
33 | -define(WS_TIMEOUT, 60*60*1000).
34 | -include_lib("nkservice/include/nkservice.hrl").
35 |
36 |
37 |
38 | %% ===================================================================
39 | %% Types
40 | %% ===================================================================
41 |
42 | -type state() :: term().
43 | -type continue() :: continue | {continue, list()}.
44 |
45 | %% ===================================================================
46 | %% Plugin callbacks
47 | %% ===================================================================
48 |
49 |
50 | plugin_deps() ->
51 | [nkmedia_fs].
52 |
53 |
54 | plugin_syntax() ->
55 | nkpacket:register_protocol(verto_proxy, nkmedia_fs_verto_proxy_server),
56 | #{
57 | verto_proxy => fun parse_listen/3
58 | }.
59 |
60 |
61 | plugin_listen(Config, #{id:=SrvId}) ->
62 | % verto_proxy will be already parsed
63 | Listen = maps:get(verto_proxy, Config, []),
64 | % With the 'user' parameter we tell nkmedia_kurento protocol
65 | % to use the service callback module, so it will find
66 | % nkmedia_kurento_* funs there.
67 | Opts = #{
68 | class => {nkmedia_fs_verto_proxy, SrvId},
69 | idle_timeout => ?WS_TIMEOUT
70 | },
71 | [{Conns, maps:merge(ConnOpts, Opts)} || {Conns, ConnOpts} <- Listen].
72 |
73 |
74 |
75 | plugin_start(Config, #{name:=Name}) ->
76 | lager:info("Plugin NkMEDIA FS VERTO Proxy (~s) starting", [Name]),
77 | {ok, Config}.
78 |
79 |
80 | plugin_stop(Config, #{name:=Name}) ->
81 | lager:info("Plugin NkMEDIA FS VERTO Proxy (~p) stopping", [Name]),
82 | {ok, Config}.
83 |
84 |
85 |
86 | %% ===================================================================
87 | %% Offering callbacks
88 | %% ===================================================================
89 |
90 |
91 |
92 | %% @doc Called when a new FS proxy connection arrives
93 | -spec nkmedia_fs_verto_proxy_init(nkpacket:nkport(), state()) ->
94 | {ok, state()}.
95 |
96 | nkmedia_fs_verto_proxy_init(_NkPort, State) ->
97 | {ok, State}.
98 |
99 |
100 | %% @doc Called to select a FS server
101 | -spec nkmedia_fs_verto_proxy_find_fs(nkmedia_service:id(), state()) ->
102 | {ok, [nkmedia_fs_verto_engine:id()], state()}.
103 |
104 | nkmedia_fs_verto_proxy_find_fs(SrvId, State) ->
105 | List = [Name || {Name, _} <- nkmedia_fs_engine:get_all(SrvId)],
106 | {ok, List, State}.
107 |
108 |
109 | %% @doc Called when a new msg arrives
110 | -spec nkmedia_fs_verto_proxy_in(map(), state()) ->
111 | {ok, map(), state()} | {stop, term(), state()} | continue().
112 |
113 | nkmedia_fs_verto_proxy_in(Msg, State) ->
114 | {ok, Msg, State}.
115 |
116 |
117 | %% @doc Called when a new msg is to be answered
118 | -spec nkmedia_fs_verto_proxy_out(map(), state()) ->
119 | {ok, map(), state()} | {stop, term(), state()} | continue().
120 |
121 | nkmedia_fs_verto_proxy_out(Msg, State) ->
122 | {ok, Msg, State}.
123 |
124 |
125 | %% @doc Called when the connection is stopped
126 | -spec nkmedia_fs_verto_proxy_terminate(Reason::term(), state()) ->
127 | {ok, state()}.
128 |
129 | nkmedia_fs_verto_proxy_terminate(_Reason, State) ->
130 | {ok, State}.
131 |
132 |
133 | %% @doc
134 | -spec nkmedia_fs_verto_proxy_handle_call(Msg::term(), {pid(), term()}, state()) ->
135 | {ok, state()} | continue().
136 |
137 | nkmedia_fs_verto_proxy_handle_call(Msg, _From, State) ->
138 | lager:error("Module ~p received unexpected call: ~p", [?MODULE, Msg]),
139 | {ok, State}.
140 |
141 |
142 | %% @doc
143 | -spec nkmedia_fs_verto_proxy_handle_cast(Msg::term(), state()) ->
144 | {ok, state()}.
145 |
146 | nkmedia_fs_verto_proxy_handle_cast(Msg, State) ->
147 | lager:error("Module ~p received unexpected cast: ~p", [?MODULE, Msg]),
148 | {ok, State}.
149 |
150 |
151 | %% @doc
152 | -spec nkmedia_fs_verto_proxy_handle_info(Msg::term(), state()) ->
153 | {ok, State::map()}.
154 |
155 | nkmedia_fs_verto_proxy_handle_info(Msg, State) ->
156 | lager:error("Module ~p received unexpected info: ~p", [?MODULE, Msg]),
157 | {ok, State}.
158 |
159 |
160 |
161 |
162 |
163 | %% ===================================================================
164 | %% Internal
165 | %% ===================================================================
166 |
167 |
168 | parse_listen(_Key, [{[{_, _, _, _}|_], Opts}|_]=Multi, _Ctx) when is_map(Opts) ->
169 | {ok, Multi};
170 |
171 | parse_listen(verto_proxy, Url, _Ctx) ->
172 | Opts = #{valid_schemes=>[verto_proxy], resolve_type=>listen},
173 | case nkpacket:multi_resolve(Url, Opts) of
174 | {ok, List} -> {ok, List};
175 | _ -> error
176 | end.
177 |
178 |
179 |
180 |
181 |
182 |
--------------------------------------------------------------------------------
/src/fs_backend/nkmedia_fs_verto_proxy_server.erl:
--------------------------------------------------------------------------------
1 |
2 | %% -------------------------------------------------------------------
3 | %%
4 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
5 | %%
6 | %% This file is provided to you under the Apache License,
7 | %% Version 2.0 (the "License"); you may not use this file
8 | %% except in compliance with the License. You may obtain
9 | %% a copy of the License at
10 | %%
11 | %% http://www.apache.org/licenses/LICENSE-2.0
12 | %%
13 | %% Unless required by applicable law or agreed to in writing,
14 | %% software distributed under the License is distributed on an
15 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | %% KIND, either express or implied. See the License for the
17 | %% specific language governing permissions and limitations
18 | %% under the License.
19 | %%
20 | %% -------------------------------------------------------------------
21 |
22 | %% @doc
23 | -module(nkmedia_fs_verto_proxy_server).
24 | -author('Carlos Gonzalez ').
25 |
26 | -export([get_all/0, send_reply/2]).
27 | -export([transports/1, default_port/1]).
28 | -export([conn_init/1, conn_encode/2, conn_parse/3, conn_stop/3,
29 | conn_handle_call/4, conn_handle_cast/3, conn_handle_info/3]).
30 |
31 |
32 | -define(LLOG(Type, Txt, Args, State),
33 | lager:Type("NkMEDIA VERTO Proxy Server (~s) "++Txt, [State#state.remote | Args])).
34 |
35 |
36 |
37 | %% ===================================================================
38 | %% Types
39 | %% ===================================================================
40 |
41 | -type user_state() :: nkmedia_fs_verto_proxy:state().
42 |
43 |
44 | %% ===================================================================
45 | %% Public
46 | %% ===================================================================
47 |
48 | get_all() ->
49 | [{Local, Remote} || {Remote, Local} <- nklib_proc:values(?MODULE)].
50 |
51 |
52 | send_reply(Pid, Event) ->
53 | gen_server:call(Pid, {send_reply, Event}).
54 |
55 |
56 |
57 | %% ===================================================================
58 | %% Protocol callbacks
59 | %% ===================================================================
60 |
61 |
62 | -record(state, {
63 | srv_id :: nkservice:id(),
64 | remote :: binary(),
65 | proxy :: pid(),
66 | user_state :: user_state()
67 | }).
68 |
69 |
70 | %% @private
71 | -spec transports(nklib:scheme()) ->
72 | [nkpacket:transport()].
73 |
74 | transports(_) -> [wss, ws].
75 |
76 | -spec default_port(nkpacket:transport()) ->
77 | inet:port_number() | invalid.
78 |
79 | default_port(ws) -> 8081;
80 | default_port(wss) -> 8082.
81 |
82 |
83 | -spec conn_init(nkpacket:nkport()) ->
84 | {ok, #state{}}.
85 |
86 | conn_init(NkPort) ->
87 | {ok, {_, SrvId}, _} = nkpacket:get_user(NkPort),
88 | {ok, Remote} = nkpacket:get_remote_bin(NkPort),
89 | State = #state{srv_id=SrvId, remote=Remote},
90 | ?LLOG(notice, "new connection (~p)", [self()], State),
91 | {ok, State2} = handle(nkmedia_fs_verto_proxy_init, [NkPort], State),
92 | {ok, List, State3} = handle(nkmedia_fs_verto_proxy_find_fs, [SrvId], State2),
93 | connect(List, State3).
94 |
95 |
96 | %% @private
97 | -spec conn_parse(term()|close, nkpacket:nkport(), #state{}) ->
98 | {ok, #state{}} | {stop, term(), #state{}}.
99 |
100 | conn_parse(close, _NkPort, State) ->
101 | {ok, State};
102 |
103 | conn_parse({text, <<"#S", _/binary>>=Msg}, _NkPort, #state{proxy=Pid}=State) ->
104 | nkmedia_fs_verto_proxy_client:send(Pid, Msg),
105 | {ok, State};
106 |
107 | conn_parse({text, Data}, _NkPort, #state{proxy=Pid}=State) ->
108 | Msg = case nklib_json:decode(Data) of
109 | error ->
110 | ?LLOG(warning, "JSON decode error: ~p", [Data], State),
111 | error(json_decode);
112 | Json ->
113 | Json
114 | end,
115 | % ?LLOG(info, "received\n~s", [nklib_json:encode_pretty(Msg)], State),
116 | case handle(nkmedia_fs_verto_proxy_in, [Msg], State) of
117 | {ok, Msg2, State2} ->
118 | ok = nkmedia_fs_verto_proxy_client:send(Pid, Msg2),
119 | {ok, State2};
120 | {stop, Reason, State2} ->
121 | {stop, Reason, State2}
122 | end.
123 |
124 | %% @private
125 | -spec conn_encode(term(), nkpacket:nkport()) ->
126 | {ok, nkpacket:outcoming()} | continue | {error, term()}.
127 |
128 | conn_encode(Msg, _NkPort) when is_map(Msg) ->
129 | Json = nklib_json:encode(Msg),
130 | {ok, {text, Json}};
131 |
132 | conn_encode(Msg, _NkPort) when is_binary(Msg) ->
133 | {ok, {text, Msg}}.
134 |
135 |
136 | %% @doc Called when the connection received an erlang message
137 | -spec conn_handle_call(term(), term(), nkpacket:nkport(), #state{}) ->
138 | {ok, #state{}} | {stop, Reason::term(), #state{}}.
139 |
140 | conn_handle_call({send_reply, Event}, From, NkPort, State) ->
141 | % ?LLOG(info, "sending\n~s", [nklib_json:encode_pretty(Event)], State),
142 | case handle(nkmedia_fs_verto_proxy_out, [Event], State) of
143 | {ok, Event2, State2} ->
144 | case nkpacket_connection:send(NkPort, Event2) of
145 | ok ->
146 | gen_server:reply(From, ok),
147 | {ok, State2};
148 | {error, Error} ->
149 | gen_server:reply(From, error),
150 | ?LLOG(notice, "error sending event: ~p", [Error], State),
151 | {stop, normal, State2}
152 | end;
153 | {stop, Reason, State2} ->
154 | {stop, Reason, State2}
155 | end;
156 |
157 | conn_handle_call(Msg, From, _NkPort, State) ->
158 | handle(nkmedia_fs_verto_proxy_handle_call, [Msg, From], State).
159 |
160 |
161 | -spec conn_handle_cast(term(), nkpacket:nkport(), #state{}) ->
162 | {ok, #state{}} | {stop, Reason::term(), #state{}}.
163 |
164 | conn_handle_cast(Msg, _NkPort, State) ->
165 | handle(nkmedia_fs_verto_proxy_handle_cast, [Msg], State).
166 |
167 |
168 | %% @doc Called when the connection received an erlang message
169 | -spec conn_handle_info(term(), nkpacket:nkport(), #state{}) ->
170 | {ok, #state{}} | {stop, Reason::term(), #state{}}.
171 |
172 | conn_handle_info({'DOWN', _Ref, process, Pid, Reason}, _NkPort,
173 | #state{proxy=Pid}=State) ->
174 | ?LLOG(notice, "stopped because server stopped (~p)", [Reason], State),
175 | {stop, normal, State};
176 |
177 | conn_handle_info(Msg, _NkPort, State) ->
178 | handle(nkmedia_fs_verto_proxy_handle_info, [Msg], State).
179 |
180 |
181 | %% @doc Called when the connection stops
182 | -spec conn_stop(Reason::term(), nkpacket:nkport(), #state{}) ->
183 | ok.
184 |
185 | conn_stop(Reason, _NkPort, State) ->
186 | catch handle(nkmedia_fs_verto_proxy_terminate, [Reason], State).
187 |
188 |
189 |
190 |
191 | %% ===================================================================
192 | %% Internal
193 | %% ===================================================================
194 |
195 | %% @private
196 | handle(Fun, Args, State) ->
197 | nklib_gen_server:handle_any(Fun, Args, State, #state.srv_id, #state.user_state).
198 |
199 |
200 | %% @private
201 | connect([], _State) ->
202 | {stop, no_fs_available};
203 |
204 | connect([Name|Rest], State) ->
205 | case nkmedia_fs_verto_proxy_client:start(Name) of
206 | {ok, ProxyPid} ->
207 | ?LLOG(info, "connected to Freeswitch server ~s", [Name], State),
208 | monitor(process, ProxyPid),
209 | nklib_proc:put(?MODULE, {proxy_client, ProxyPid}),
210 | {ok, State#state{proxy=ProxyPid}};
211 | {error, Error} ->
212 | ?LLOG(warning, "could not start proxy to ~s: ~p",
213 | [Name, Error], State),
214 | connect(Rest, State)
215 | end.
216 |
217 |
218 |
219 |
--------------------------------------------------------------------------------
/src/janus_backend/nkmedia_janus.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc NkMEDIA application
22 |
23 | -module(nkmedia_janus).
24 | -author('Carlos Gonzalez ').
25 |
26 | -export_type([id/0]).
27 |
28 |
29 | %% ===================================================================
30 | %% Types
31 | %% ===================================================================
32 |
33 | -type id() :: nkmedia_janus_engine:id().
34 |
35 |
36 |
37 |
38 |
39 |
40 | %% ===================================================================
41 | %% Public functions
42 | %% ===================================================================
43 |
44 |
--------------------------------------------------------------------------------
/src/janus_backend/nkmedia_janus_admin.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc
22 | -module(nkmedia_janus_admin).
23 | -author('Carlos Gonzalez ').
24 |
25 | -export([list_sessions/1, list_handles/2, handle_info/3, set_log_level/2]).
26 | -export([print_handle_info/3]).
27 | -export([print_all/1]).
28 |
29 |
30 | %% ===================================================================
31 | %% Types
32 | %% ===================================================================
33 |
34 |
35 |
36 | %% ===================================================================
37 | %% Public
38 | %% ===================================================================
39 |
40 |
41 | %% @doc Gets active sessions
42 | -spec list_sessions(nkmedia_janus:id()|nkmedia_janus:config()) ->
43 | {ok, [integer()]} | error.
44 |
45 | list_sessions(Id) ->
46 | case admin_req(Id, list_sessions, <<>>, #{}) of
47 | {ok, #{<<"sessions">>:=Sessions}} ->
48 | {ok, Sessions};
49 | {error, Error} ->
50 | {error, Error}
51 | end.
52 |
53 |
54 | %% @doc Gets handles for a session
55 | -spec list_handles(nkmedia_janus:id()|nkmedia_janus:config(), integer()) ->
56 | {ok, [integer()]} | error.
57 |
58 | list_handles(Id, Session) ->
59 | Url = <<"/", (nklib_util:to_binary(Session))/binary>>,
60 | case admin_req(Id, list_handles, Url, #{}) of
61 | {ok, #{<<"handles">>:=Handles}} ->
62 | {ok, Handles};
63 | {error, Error} ->
64 | {error, Error}
65 | end.
66 |
67 |
68 | %% @doc Gets info on a handle for a session
69 | -spec handle_info(nkmedia_janus:id()|nkmedia_janus:config(), integer(), integer()) ->
70 | {ok, map()} | error.
71 |
72 | handle_info(Id, Session, Handle) ->
73 | Url = <<"/", (nklib_util:to_binary(Session))/binary,
74 | "/", (nklib_util:to_binary(Handle))/binary>>,
75 | case admin_req(Id, handle_info, Url, #{}) of
76 | {ok, #{<<"info">>:=Info}} ->
77 | {ok, Info};
78 | {error, Error} ->
79 | {error, Error}
80 | end.
81 |
82 |
83 | %% @doc Gets info on a handle for a session
84 | -spec print_handle_info(nkmedia_janus:id()|nkmedia_janus:config(),
85 | integer(), integer()) ->
86 | ok.
87 |
88 | print_handle_info(Id, Session, Handle) ->
89 | {ok, Info} = handle_info(Id, Session, Handle),
90 | io:format("~s", [nklib_json:encode_pretty(Info)]).
91 |
92 |
93 |
94 | %% @doc Gets info on a handle for a session
95 | -spec set_log_level(nkmedia_janus:id()|nkmedia_janus:config(), integer()) ->
96 | {ok, [integer()]} | error.
97 |
98 | set_log_level(Id, Level) when Level>=0, Level=<7 ->
99 | case admin_req(Id, set_log_level, <<>>, #{level=>Level}) of
100 | {ok, #{<<"level">>:=Level}} ->
101 | ok;
102 | {error, Error} ->
103 | {error, Error}
104 | end.
105 |
106 |
107 | %% @private
108 | print_all(Id) ->
109 | {ok, Sessions} = list_sessions(Id),
110 | print_all(Id, Sessions).
111 |
112 |
113 | print_all(_Id, []) ->
114 | ok;
115 | print_all(Id, [Session|Rest]) ->
116 | {ok, Handles} = list_handles(Id, Session),
117 | print_all(Id, Session, Handles),
118 | print_all(Id, Rest).
119 |
120 | print_all(_Id, _Session, []) ->
121 | ok;
122 | print_all(Id, Session, [Handle|Rest]) ->
123 | {ok, Info} = handle_info(Id, Session, Handle),
124 | io:format("\n\nSession ~p Handle ~p:\n", [Session, Handle]),
125 | io:format("~s\n", [nklib_json:encode_pretty(Info)]),
126 | print_all(Id, Session, Rest).
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 | %% ===================================================================
135 | %% Private
136 | %% ===================================================================
137 |
138 | %% @private Launches an admin command
139 | -spec admin_req(nkmedia_janus:config()|nkmedia_janus:id(),
140 | atom()|binary(), binary(), map()) ->
141 | {ok, atom()|binary(), map()} | {error, term()}.
142 |
143 | admin_req(#{base:=Base, host:=Host, pass:=Pass}, Cmd, Url, Data) ->
144 | Msg = Data#{
145 | janus => Cmd,
146 | transaction => nklib_util:uid(),
147 | admin_secret => Pass
148 | },
149 | Url1 = <<"http://", Host/binary,
150 | ":", (nklib_util:to_binary(Base+1))/binary,
151 | "/admin", Url/binary>>,
152 | Url2 = binary_to_list(Url1),
153 | Body = nklib_json:encode(Msg),
154 | Opts = [{full_result, false}, {body_format, binary}],
155 | case httpc:request(post, {Url2, [], "", Body}, [], Opts) of
156 | {ok, {200, Body2}} ->
157 | case catch nklib_json:decode(Body2) of
158 | #{<<"janus">> := <<"success">>} = Msg2 ->
159 | {ok, Msg2};
160 | #{
161 | <<"janus">> := <<"error">>,
162 | <<"error">> := #{<<"reason">>:=ErrReason}
163 | } ->
164 | {error, ErrReason};
165 | _ ->
166 | {error, json_error}
167 | end;
168 | _ ->
169 | {error, no_response}
170 | end;
171 |
172 | admin_req(Id, Cmd, Url, Data) ->
173 | case nkmedia_janus_engine:get_config(Id) of
174 | {ok, Config} ->
175 | admin_req(Config, Cmd, Url, Data);
176 | {error, Error} ->
177 | {error, Error}
178 | end.
179 |
180 |
181 |
182 |
--------------------------------------------------------------------------------
/src/janus_backend/nkmedia_janus_api_syntax.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc NkMEDIA external API Syntax
22 |
23 | -module(nkmedia_janus_api_syntax).
24 | -author('Carlos Gonzalez ').
25 | -export([syntax/5]).
26 |
27 |
28 | -include_lib("nkservice/include/nkservice.hrl").
29 | -include("../../include/nkmedia.hrl").
30 |
31 |
32 | %% ===================================================================
33 | %% Syntax
34 | %% ===================================================================
35 |
36 |
37 |
38 | %% @private
39 | syntax(<<"session">>, <<"create">>, Syntax, Defaults, Mandatory) ->
40 | {
41 | Syntax#{
42 | room_bitrate => {integer, 0, none},
43 | room_audio_codec => {enum, [opus, isac32, isac16, pcmu, pcma]},
44 | room_video_codec => {enum , [vp8, vp9, h264]}
45 | },
46 | Defaults,
47 | Mandatory
48 | };
49 |
50 | syntax(<<"room">>, <<"create">>, Syntax, Defaults, Mandatory) ->
51 | {
52 | Syntax#{
53 | bitrate => {integer, 0, none},
54 | audio_codec => {enum, [opus, isac32, isac16, pcmu, pcma]},
55 | video_codec => {enum , [vp8, vp9, h264]}
56 | },
57 | Defaults,
58 | Mandatory
59 | };
60 |
61 | syntax(_Sub, _Cmd, Syntax, Defaults, Mandatory) ->
62 | {Syntax, Defaults, Mandatory}.
63 |
--------------------------------------------------------------------------------
/src/janus_backend/nkmedia_janus_docker.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc NkMEDIA Docker management application
22 | -module(nkmedia_janus_docker).
23 | -author('Carlos Gonzalez ').
24 |
25 | -export([start/1, stop/1, stop_all/0]).
26 | -export([notify/4]).
27 |
28 | -include("../../include/nkmedia.hrl").
29 |
30 |
31 | %% ===================================================================
32 | %% Types
33 |
34 | %% ===================================================================
35 |
36 | %% ===================================================================
37 | %% Freeswitch Instance
38 | %% ===================================================================
39 |
40 |
41 | %% @doc Starts a JANUS instance
42 | %% BASE+0: WS port
43 | %% BASE+1: WS admin port
44 | -spec start(nkservice:name()) ->
45 | {ok, Name::binary()} | {error, term()}.
46 |
47 | start(Service) ->
48 | try
49 | SrvId = case nkservice_srv:get_srv_id(Service) of
50 | {ok, SrvId0} -> SrvId0;
51 | not_found -> throw(unknown_service)
52 | end,
53 | Config = nkservice_srv:get_item(SrvId, config_nkmedia_janus),
54 | BasePort = crypto:rand_uniform(32768, 65535),
55 | Pass = nklib_util:luid(),
56 | Image = nkmedia_janus_build:run_name(Config),
57 | JanusIp = nklib_util:to_host(nkmedia_app:get(docker_ip)),
58 | Name = list_to_binary([
59 | "nk_janus_",
60 | nklib_util:to_binary(SrvId), "_",
61 | nklib_util:to_binary(BasePort)
62 | ]),
63 | LogDir = <<(nkmedia_app:get(log_dir))/binary, $/, Name/binary>>,
64 | RecDir = filename:join(nkmedia_app:get(record_dir), <<"tmp">>),
65 | ExtIp = nklib_util:to_host(nkpacket_app:get(ext_ip)),
66 | Env = [
67 | {"NK_JANUS_IP", JanusIp},
68 | {"NK_EXT_IP", ExtIp},
69 | {"NK_PASS", nklib_util:to_binary(Pass)},
70 | {"NK_BASE", nklib_util:to_binary(BasePort)},
71 | {"NK_SRV_ID", nklib_util:to_binary(SrvId)},
72 | {"NK_RECORDS_DIR", "/tmp/record"}
73 | ],
74 | Labels = [
75 | {"nkmedia", "janus"}
76 | ],
77 | % Cmds = ["bash"],
78 | DockerOpts1 = #{
79 | name => Name,
80 | env => Env,
81 | net => host,
82 | interactive => true,
83 | labels => Labels,
84 | volumes => [{LogDir, "/var/log/janus"}, {RecDir, "/tmp/record"}]
85 | },
86 | DockerOpts2 = case nkmedia_app:get(docker_log) of
87 | undefined -> DockerOpts1;
88 | DockerLog -> DockerOpts1#{docker_log=>DockerLog}
89 | end,
90 | DockerPid = case get_docker_pid() of
91 | {ok, DockerPid0} -> DockerPid0;
92 | {error, Error1} -> throw(Error1)
93 | end,
94 | nkdocker:rm(DockerPid, Name),
95 | case nkdocker:create(DockerPid, Image, DockerOpts2) of
96 | {ok, _} -> ok;
97 | {error, Error2} -> throw(Error2)
98 | end,
99 | lager:info("NkMEDIA JANUS Docker: starting instance ~s", [Name]),
100 | lager:info("Log dir: ~s\nRecord dir: ~s", [LogDir, RecDir]),
101 | case nkdocker:start(DockerPid, Name) of
102 | ok ->
103 | {ok, Name};
104 | {error, Error3} ->
105 | {error, Error3}
106 | end
107 | catch
108 | throw:Throw -> {error, Throw}
109 | end.
110 |
111 |
112 | %% @doc Stops a JANUS instance
113 | -spec stop(binary()) ->
114 | ok | {error, term()}.
115 |
116 | stop(Name) ->
117 | case get_docker_pid() of
118 | {ok, DockerPid} ->
119 | case nkdocker:kill(DockerPid, Name) of
120 | ok -> ok;
121 | {error, {not_found, _}} -> ok;
122 | E1 -> lager:warning("NkMEDIA could not kill ~s: ~p", [Name, E1])
123 | end,
124 | case nkdocker:rm(DockerPid, Name) of
125 | ok -> ok;
126 | {error, {not_found, _}} -> ok;
127 | E2 -> lager:warning("NkMEDIA could not remove ~s: ~p", [Name, E2])
128 | end,
129 | ok;
130 | {error, Error} ->
131 | {error, Error}
132 | end.
133 |
134 |
135 | %% @doc
136 | stop_all() ->
137 | case get_docker_pid() of
138 | {ok, DockerPid} ->
139 | {ok, List} = nkdocker:ps(DockerPid),
140 | lists:foreach(
141 | fun(#{<<"Names">>:=[<<"/", Name/binary>>]}) ->
142 | case Name of
143 | <<"nk_janus_", _/binary>> ->
144 | lager:info("Stopping ~s", [Name]),
145 | stop(Name);
146 | _ ->
147 | ok
148 | end
149 | end,
150 | List);
151 | {error, Error} ->
152 | {error, Error}
153 | end.
154 |
155 |
156 | %% @private
157 | -spec get_docker_pid() ->
158 | {ok, pid()} | {error, term()}.
159 |
160 | get_docker_pid() ->
161 | DockerMonId = nkmedia_app:get(docker_janus_mon_id),
162 | nkdocker_monitor:get_docker(DockerMonId).
163 |
164 |
165 | %% @private
166 | notify(MonId, ping, Name, Data) ->
167 | notify(MonId, start, Name, Data);
168 |
169 | notify(MonId, start, Name, Data) ->
170 | case Data of
171 | #{
172 | name := Name,
173 | labels := #{<<"nkmedia">> := <<"janus">>},
174 | env := #{
175 | <<"NK_JANUS_IP">> := Host,
176 | <<"NK_BASE">> := Base,
177 | <<"NK_PASS">> := Pass,
178 | <<"NK_SRV_ID">> := SrvId
179 | },
180 | image := Image
181 | } ->
182 | case binary:split(Image, <<"/">>) of
183 | [Comp, <<"nk_janus:", Tag/binary>>] ->
184 | [Vsn, Rel] = binary:split(Tag, <<"-">>),
185 | Config = #{
186 | srv_id => nklib_util:to_atom(SrvId),
187 | name => Name,
188 | comp => Comp,
189 | vsn => Vsn,
190 | rel => Rel,
191 | host => Host,
192 | base => nklib_util:to_integer(Base),
193 | pass => Pass
194 | },
195 | connect_janus(MonId, Config);
196 | _ ->
197 | lager:warning("Started unrecognized janus")
198 | end;
199 | _ ->
200 | lager:warning("Started unrecognized janus")
201 | end;
202 |
203 | notify(MonId, stop, Name, Data) ->
204 | case Data of
205 | #{
206 | name := Name,
207 | labels := #{<<"nkmedia">> := <<"janus">>}
208 | } ->
209 | remove_janus(MonId, Name);
210 | _ ->
211 | ok
212 | end;
213 |
214 | notify(_MonId, stats, Name, Stats) ->
215 | nkmedia_janus_engine:stats(Name, Stats).
216 |
217 |
218 |
219 |
220 | %% ===================================================================
221 | %% Internal
222 | %% ===================================================================
223 |
224 | %% @private
225 | connect_janus(MonId, #{name:=Name}=Config) ->
226 | spawn(
227 | fun() ->
228 | case nkmedia_janus_engine:connect(Config) of
229 | {ok, _Pid} ->
230 | ok = nkdocker_monitor:start_stats(MonId, Name);
231 | {error, {already_started, _Pid}} ->
232 | ok;
233 | {error, Error} ->
234 | lager:warning("Could not connect to Janus ~s: ~p",
235 | [Name, Error])
236 | end
237 | end),
238 | ok.
239 |
240 |
241 | %% @private
242 | remove_janus(MonId, Name) ->
243 | spawn(
244 | fun() ->
245 | nkmedia_janus_engine:stop(Name),
246 | case nkdocker_monitor:get_docker(MonId) of
247 | {ok, Pid} ->
248 | nkdocker:rm(Pid, Name);
249 | _ ->
250 | ok
251 | end
252 | end),
253 | ok.
254 |
--------------------------------------------------------------------------------
/src/janus_backend/nkmedia_janus_proxy.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc Plugin implementing a Kurento proxy server for testing
22 | -module(nkmedia_janus_proxy).
23 | -author('Carlos Gonzalez ').
24 |
25 | -export([plugin_deps/0, plugin_syntax/0, plugin_listen/2,
26 | plugin_start/2, plugin_stop/2]).
27 | -export([nkmedia_janus_proxy_init/2, nkmedia_janus_proxy_find_janus/2,
28 | nkmedia_janus_proxy_in/2, nkmedia_janus_proxy_out/2,
29 | nkmedia_janus_proxy_terminate/2, nkmedia_janus_proxy_handle_call/3,
30 | nkmedia_janus_proxy_handle_cast/2, nkmedia_janus_proxy_handle_info/2]).
31 |
32 |
33 | -define(WS_TIMEOUT, 60*60*1000).
34 | -include_lib("nkservice/include/nkservice.hrl").
35 |
36 |
37 |
38 | %% ===================================================================
39 | %% Types
40 | %% ===================================================================
41 |
42 | -type state() :: term().
43 | -type continue() :: continue | {continue, list()}.
44 |
45 |
46 | %% ===================================================================
47 | %% Plugin callbacks
48 | %% ===================================================================
49 |
50 |
51 | plugin_deps() ->
52 | [nkmedia_janus].
53 |
54 |
55 | plugin_syntax() ->
56 | nkpacket:register_protocol(janus_proxy, nkmedia_janus_proxy_server),
57 | #{
58 | janus_proxy => fun parse_listen/3
59 | }.
60 |
61 |
62 | plugin_listen(Config, #{id:=SrvId}) ->
63 | % janus_proxy will be already parsed
64 | Listen = maps:get(janus_proxy, Config, []),
65 | % With the 'user' parameter we tell nkmedia_kurento protocol
66 | % to use the service callback module, so it will find
67 | % nkmedia_kurento_* funs there.
68 | Opts = #{
69 | class => {nkmedia_janus_proxy, SrvId},
70 | idle_timeout => ?WS_TIMEOUT,
71 | ws_proto => <<"janus-protocol">>
72 | },
73 | [{Conns, maps:merge(ConnOpts, Opts)} || {Conns, ConnOpts} <- Listen].
74 |
75 |
76 |
77 | plugin_start(Config, #{name:=Name}) ->
78 | lager:info("Plugin NkMEDIA JANUS Proxy (~s) starting", [Name]),
79 | {ok, Config}.
80 |
81 |
82 | plugin_stop(Config, #{name:=Name}) ->
83 | lager:info("Plugin NkMEDIA JANUS Proxy (~p) stopping", [Name]),
84 | {ok, Config}.
85 |
86 |
87 |
88 | %% ===================================================================
89 | %% Offering callbacks
90 | %% ===================================================================
91 |
92 |
93 |
94 | %% @doc Called when a new KMS proxy connection arrives
95 | -spec nkmedia_janus_proxy_init(nkpacket:nkport(), state()) ->
96 | {ok, state()}.
97 |
98 | nkmedia_janus_proxy_init(_NkPort, State) ->
99 | {ok, State}.
100 |
101 |
102 | %% @doc Called to select a KMS server
103 | -spec nkmedia_janus_proxy_find_janus(nkmedia_service:id(), state()) ->
104 | {ok, [nkmedia_janus_engine:id()], state()}.
105 |
106 | nkmedia_janus_proxy_find_janus(SrvId, State) ->
107 | List = [Name || {Name, _} <- nkmedia_janus_engine:get_all(SrvId)],
108 | {ok, List, State}.
109 |
110 |
111 | %% @doc Called when a new msg arrives
112 | -spec nkmedia_janus_proxy_in(map(), state()) ->
113 | {ok, map(), state()} | {stop, term(), state()} | continue().
114 |
115 | nkmedia_janus_proxy_in(Msg, State) ->
116 | {ok, Msg, State}.
117 |
118 |
119 | %% @doc Called when a new msg is to be answered
120 | -spec nkmedia_janus_proxy_out(map(), state()) ->
121 | {ok, map(), state()} | {stop, term(), state()} | continue().
122 |
123 | nkmedia_janus_proxy_out(Msg, State) ->
124 | {ok, Msg, State}.
125 |
126 |
127 | %% @doc Called when the connection is stopped
128 | -spec nkmedia_janus_proxy_terminate(Reason::term(), state()) ->
129 | {ok, state()}.
130 |
131 | nkmedia_janus_proxy_terminate(_Reason, State) ->
132 | {ok, State}.
133 |
134 |
135 | %% @doc
136 | -spec nkmedia_janus_proxy_handle_call(Msg::term(), {pid(), term()}, state()) ->
137 | {ok, state()} | continue().
138 |
139 | nkmedia_janus_proxy_handle_call(Msg, _From, State) ->
140 | lager:error("Module ~p received unexpected call: ~p", [?MODULE, Msg]),
141 | {ok, State}.
142 |
143 |
144 | %% @doc
145 | -spec nkmedia_janus_proxy_handle_cast(Msg::term(), state()) ->
146 | {ok, state()}.
147 |
148 | nkmedia_janus_proxy_handle_cast(Msg, State) ->
149 | lager:error("Module ~p received unexpected cast: ~p", [?MODULE, Msg]),
150 | {ok, State}.
151 |
152 |
153 | %% @doc
154 | -spec nkmedia_janus_proxy_handle_info(Msg::term(), state()) ->
155 | {ok, State::map()}.
156 |
157 | nkmedia_janus_proxy_handle_info(Msg, State) ->
158 | lager:error("Module ~p received unexpected info: ~p", [?MODULE, Msg]),
159 | {ok, State}.
160 |
161 |
162 |
163 |
164 |
165 | %% ===================================================================
166 | %% Internal
167 | %% ===================================================================
168 |
169 |
170 | parse_listen(_Key, [{[{_, _, _, _}|_], Opts}|_]=Multi, _Ctx) when is_map(Opts) ->
171 | {ok, Multi};
172 |
173 | parse_listen(janus_proxy, Url, _Ctx) ->
174 | Opts = #{valid_schemes=>[janus_proxy], resolve_type=>listen},
175 | case nkpacket:multi_resolve(Url, Opts) of
176 | {ok, List} -> {ok, List};
177 | _ -> error
178 | end.
179 |
180 |
181 |
182 |
183 |
184 |
--------------------------------------------------------------------------------
/src/janus_backend/nkmedia_janus_proxy_server.erl:
--------------------------------------------------------------------------------
1 |
2 | %% -------------------------------------------------------------------
3 | %%
4 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
5 | %%
6 | %% This file is provided to you under the Apache License,
7 | %% Version 2.0 (the "License"); you may not use this file
8 | %% except in compliance with the License. You may obtain
9 | %% a copy of the License at
10 | %%
11 | %% http://www.apache.org/licenses/LICENSE-2.0
12 | %%
13 | %% Unless required by applicable law or agreed to in writing,
14 | %% software distributed under the License is distributed on an
15 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | %% KIND, either express or implied. See the License for the
17 | %% specific language governing permissions and limitations
18 | %% under the License.
19 | %%
20 | %% -------------------------------------------------------------------
21 |
22 | %% @doc
23 | -module(nkmedia_janus_proxy_server).
24 | -author('Carlos Gonzalez ').
25 |
26 | -export([get_all/0, send_reply/2]).
27 | -export([transports/1, default_port/1]).
28 | -export([conn_init/1, conn_encode/2, conn_parse/3, conn_stop/3,
29 | conn_handle_call/4, conn_handle_cast/3, conn_handle_info/3]).
30 |
31 |
32 | -define(LLOG(Type, Txt, Args, State),
33 | lager:Type("NkMEDIA JANUS Proxy Server (~s) "++Txt, [State#state.remote | Args])).
34 |
35 |
36 | %% ===================================================================
37 | %% Types
38 | %% ===================================================================
39 |
40 | -type user_state() :: nkmedia_janus_proxy:state().
41 |
42 |
43 | %% ===================================================================
44 | %% Public
45 | %% ===================================================================
46 |
47 | get_all() ->
48 | [{Local, Remote} || {Remote, Local} <- nklib_proc:values(?MODULE)].
49 |
50 |
51 | send_reply(Pid, Event) ->
52 | gen_server:call(Pid, {send_reply, self(), Event}).
53 |
54 |
55 |
56 | %% ===================================================================
57 | %% Protocol callbacks
58 | %% ===================================================================
59 |
60 |
61 | -record(state, {
62 | srv_id :: nkservice:id(),
63 | remote :: binary(),
64 | proxy :: pid(),
65 | user_state :: user_state()
66 | }).
67 |
68 |
69 | %% @private
70 | -spec transports(nklib:scheme()) ->
71 | [nkpacket:transport()].
72 |
73 | transports(_) -> [wss, ws].
74 |
75 | -spec default_port(nkpacket:transport()) ->
76 | inet:port_number() | invalid.
77 |
78 | default_port(ws) -> 8081;
79 | default_port(wss) -> 8082.
80 |
81 |
82 | -spec conn_init(nkpacket:nkport()) ->
83 | {ok, #state{}}.
84 |
85 | conn_init(NkPort) ->
86 | {ok, {_, SrvId}, _} = nkpacket:get_user(NkPort),
87 | {ok, Remote} = nkpacket:get_remote_bin(NkPort),
88 | State = #state{srv_id=SrvId, remote=Remote},
89 | ?LLOG(notice, "new connection (~p)", [self()], State),
90 | {ok, State2} = handle(nkmedia_janus_proxy_init, [NkPort], State),
91 | {ok, List, State3} = handle(nkmedia_janus_proxy_find_janus, [SrvId], State2),
92 | connect(List, State3).
93 |
94 |
95 | %% @private
96 | -spec conn_parse(term()|close, nkpacket:nkport(), #state{}) ->
97 | {ok, #state{}} | {stop, term(), #state{}}.
98 |
99 | conn_parse(close, _NkPort, State) ->
100 | {ok, State};
101 |
102 | conn_parse({text, Data}, _NkPort, #state{proxy=Pid}=State) ->
103 | Msg = case nklib_json:decode(Data) of
104 | error ->
105 | ?LLOG(warning, "JSON decode error: ~p", [Data], State),
106 | error(json_decode);
107 | Json ->
108 | Json
109 | end,
110 | % ?LLOG(info, "received\n~s", [nklib_json:encode_pretty(Msg)], State),
111 | case handle(nkmedia_janus_proxy_in, [Msg], State) of
112 | {ok, Msg2, State2} ->
113 | ok = nkmedia_janus_proxy_client:send(Pid, self(), Msg2),
114 | {ok, State2};
115 | {stop, Reason, State2} ->
116 | {stop, Reason, State2}
117 | end.
118 |
119 |
120 | %% @private
121 | -spec conn_encode(term(), nkpacket:nkport()) ->
122 | {ok, nkpacket:outcoming()} | continue | {error, term()}.
123 |
124 | conn_encode(Msg, _NkPort) when is_map(Msg) ->
125 | Json = nklib_json:encode(Msg),
126 | {ok, {text, Json}};
127 |
128 | conn_encode(Msg, _NkPort) when is_binary(Msg) ->
129 | {ok, {text, Msg}}.
130 |
131 |
132 | %% @doc Called when the connection received an erlang message
133 | -spec conn_handle_call(term(), term(), nkpacket:nkport(), #state{}) ->
134 | {ok, #state{}} | {stop, Reason::term(), #state{}}.
135 |
136 | conn_handle_call({send_reply, _Pid, Event}, From, NkPort, State) ->
137 | % ?LLOG(info, "sending\n~s", [nklib_json:encode_pretty(Event)], State),
138 | case handle(nkmedia_janus_proxy_out, [Event], State) of
139 | {ok, Event2, State2} ->
140 | case nkpacket_connection:send(NkPort, Event2) of
141 | ok ->
142 | gen_server:reply(From, ok),
143 | {ok, State2};
144 | {error, Error} ->
145 | gen_server:reply(From, error),
146 | ?LLOG(notice, "error sending event: ~p", [Error], State),
147 | {stop, normal, State2}
148 | end;
149 | {stop, Reason, State2} ->
150 | {stop, Reason, State2}
151 | end;
152 |
153 | conn_handle_call(Msg, From, _NkPort, State) ->
154 | handle(nkmedia_janus_proxy_handle_call, [Msg, From], State).
155 |
156 |
157 | -spec conn_handle_cast(term(), nkpacket:nkport(), #state{}) ->
158 | {ok, #state{}} | {stop, Reason::term(), #state{}}.
159 |
160 | conn_handle_cast(Msg, _NkPort, State) ->
161 | handle(nkmedia_janus_proxy_handle_cast, [Msg], State).
162 |
163 |
164 | %% @doc Called when the connection received an erlang message
165 | -spec conn_handle_info(term(), nkpacket:nkport(), #state{}) ->
166 | {ok, #state{}} | {stop, Reason::term(), #state{}}.
167 |
168 | conn_handle_info({send_reply, _Pid, Event}, NkPort, State) ->
169 | case nkpacket_connection:send(NkPort, Event) of
170 | ok ->
171 | {ok, State};
172 | {error, Error} ->
173 | ?LLOG(notice, "error sending event: ~p", [Error], State),
174 | {stop, normal, State}
175 | end;
176 |
177 | conn_handle_info({'DOWN', _Ref, process, Pid, Reason}, _NkPort,
178 | #state{proxy=Pid}=State) ->
179 | ?LLOG(notice, "stopped because server stopped (~p)", [Reason], State),
180 | {stop, normal, State};
181 |
182 | conn_handle_info(Msg, _NkPort, State) ->
183 | handle(nkmedia_janus_proxy_handle_info, [Msg], State).
184 |
185 | %% @doc Called when the connection stops
186 | -spec conn_stop(Reason::term(), nkpacket:nkport(), #state{}) ->
187 | ok.
188 |
189 | conn_stop(Reason, _NkPort, State) ->
190 | catch handle(nkmedia_fs_verto_proxy_terminate, [Reason], State).
191 |
192 |
193 |
194 | %% ===================================================================
195 | %% Internal
196 | %% ===================================================================
197 |
198 | %% @private
199 | handle(Fun, Args, State) ->
200 | nklib_gen_server:handle_any(Fun, Args, State, #state.srv_id, #state.user_state).
201 |
202 |
203 | %% @private
204 | connect([], _State) ->
205 | {stop, no_janus_available};
206 |
207 | connect([Name|Rest], State) ->
208 | case nkmedia_janus_proxy_client:start(Name) of
209 | {ok, ProxyPid} ->
210 | ?LLOG(info, "connected to Janus server ~s", [Name], State),
211 | monitor(process, ProxyPid),
212 | nklib_proc:put(?MODULE, {proxy_client, ProxyPid}),
213 | {ok, State#state{proxy=ProxyPid}};
214 | {error, Error} ->
215 | ?LLOG(warning, "could not start proxy to ~s: ~p",
216 | [Name, Error], State),
217 | connect(Rest, State)
218 | end.
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
--------------------------------------------------------------------------------
/src/janus_backend/nkmedia_janus_room.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc Janus room (SFU) management
22 | -module(nkmedia_janus_room).
23 | -author('Carlos Gonzalez ').
24 |
25 | -export([init/2, terminate/2, timeout/2, handle_cast/2]).
26 | -export([janus_check/3]).
27 |
28 | -define(LLOG(Type, Txt, Args, Room),
29 | lager:Type("NkMEDIA Janus Room ~s "++Txt, [maps:get(room_id, Room) | Args])).
30 |
31 | -include("../../include/nkmedia_room.hrl").
32 |
33 |
34 | %% ===================================================================
35 | %% Types
36 | %% ===================================================================
37 |
38 |
39 | -type room_id() :: nkmedia_room:id().
40 |
41 | -type room() ::
42 | nkmedia_room:room() |
43 | #{
44 | nkmedia_janus_id => nkmedia_janus:id()
45 | }.
46 |
47 |
48 |
49 | %% ===================================================================
50 | %% External
51 | %% ===================================================================
52 |
53 | %% @private Called periodically from nkmedia_janus_engine
54 | janus_check(JanusId, RoomId, Data) ->
55 | case nkmedia_room:find(RoomId) of
56 | {ok, Pid} ->
57 | #{<<"num_participants">>:=Num} = Data,
58 | gen_server:cast(Pid, {nkmedia_janus, {participants, Num}});
59 | not_found ->
60 | spawn(
61 | fun() ->
62 | lager:warning("Destroying orphan Janus room ~s", [RoomId]),
63 | destroy_room(#{nkmedia_janus_id=>JanusId, room_id=>RoomId})
64 | end)
65 | end.
66 |
67 |
68 |
69 | %% ===================================================================
70 | %% Callbacks
71 | %% ===================================================================
72 |
73 | %% @doc Creates a new room
74 | %% Use nkmedia_janus_op:list_rooms/1 to check rooms directly on Janus
75 | -spec init(room_id(), room()) ->
76 | {ok, room()} | {error, term()}.
77 |
78 | init(_RoomId, Room) ->
79 | case get_janus(Room) of
80 | {ok, Room2} ->
81 | case create_room(Room2) of
82 | {ok, Room3} ->
83 | {ok, ?ROOM(#{class=>sfu, backend=>nkmedia_janus}, Room3)};
84 | {error, Error} ->
85 | {error, Error}
86 | end;
87 | error ->
88 | {error, no_mediaserver}
89 | end.
90 |
91 |
92 | %% @doc
93 | -spec terminate(term(), room()) ->
94 | {ok, room()} | {error, term()}.
95 |
96 | terminate(_Reason, Room) ->
97 | case destroy_room(Room) of
98 | ok ->
99 | ?LLOG(info, "stopping, destroying room", [], Room);
100 | {error, Error} ->
101 | ?LLOG(warning, "could not destroy room: ~p", [Error], Room)
102 | end,
103 | {ok, Room}.
104 |
105 |
106 |
107 | %% @private
108 | -spec timeout(room_id(), room()) ->
109 | {ok, room()} | {stop, nkservice:error(), room()}.
110 |
111 | timeout(RoomId, #{nkmedia_janus_id:=JanusId}=Room) ->
112 | case length(nkmedia_room:get_all_with_role(publisher, Room)) of
113 | 0 ->
114 | {stop, timeout, Room};
115 | _ ->
116 | case nkmedia_janus_engine:check_room(JanusId, RoomId) of
117 | {ok, _} ->
118 | {ok, Room};
119 | _ ->
120 | ?LLOG(warning, "room is not on engine ~p ~p",
121 | [JanusId, RoomId], Room),
122 | {stop, timeout, Room}
123 | end
124 | end.
125 |
126 |
127 | %% @private
128 | -spec handle_cast(term(), room()) ->
129 | {noreply, room()}.
130 |
131 | handle_cast({participants, Num}, Room) ->
132 | case length(nkmedia_room:get_all_with_role(publisher, Room)) of
133 | Num ->
134 | ok;
135 | Other ->
136 | ?LLOG(notice, "Janus says ~p participants, we have ~p!",
137 | [Num, Other], Room),
138 | case Num of
139 | 0 ->
140 | nkmedia_room:stop(self(), no_room_members);
141 | _ ->
142 | ok
143 | end
144 | end,
145 | {noreply, Room}.
146 |
147 |
148 |
149 |
150 | % ===================================================================
151 | %% Internal
152 | %% ===================================================================
153 |
154 |
155 | %% @private
156 | -spec get_janus(room()) ->
157 | {ok, room()} | error.
158 |
159 | get_janus(#{nkmedia_janus_id:=_}=Room) ->
160 | {ok, Room};
161 |
162 | get_janus(#{srv_id:=SrvId}=Room) ->
163 | case SrvId:nkmedia_janus_get_mediaserver(SrvId) of
164 | {ok, JanusId} ->
165 | {ok, ?ROOM(#{nkmedia_janus_id=>JanusId}, Room)};
166 | {error, _Error} ->
167 | error
168 | end.
169 |
170 |
171 | %% @private
172 | -spec create_room(room()) ->
173 | {ok, room()} | {error, term()}.
174 |
175 | create_room(#{nkmedia_janus_id:=JanusId, room_id:=RoomId}=Room) ->
176 | Merge = #{audio_codec=>opus, video_codec=>vp8, bitrate=>500000},
177 | Room2 = ?ROOM_MERGE(Merge, Room),
178 | Opts = #{
179 | audiocodec => maps:get(audio_codec, Room2),
180 | videocodec => maps:get(video_codec, Room2),
181 | bitrate => maps:get(bitrate, Room2)
182 | },
183 | case nkmedia_janus_op:start(JanusId, RoomId) of
184 | {ok, Pid} ->
185 | case nkmedia_janus_op:create_room(Pid, RoomId, Opts) of
186 | ok ->
187 | {ok, Room2};
188 | {error, Error} ->
189 | {error, Error}
190 | end;
191 | {error, Error} ->
192 | {error, Error}
193 | end.
194 |
195 |
196 | -spec destroy_room(room()) ->
197 | ok | {error, term()}.
198 |
199 | destroy_room(#{nkmedia_janus_id:=JanusId, room_id:=RoomId}) ->
200 | case nkmedia_janus_op:start(JanusId, RoomId) of
201 | {ok, Pid} ->
202 | nkmedia_janus_op:destroy_room(Pid, RoomId);
203 | {error, Error} ->
204 | {error, Error}
205 | end.
206 |
207 |
208 |
--------------------------------------------------------------------------------
/src/kms_backend/nkmedia_kms.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc NkMEDIA application
22 |
23 | -module(nkmedia_kms).
24 | -author('Carlos Gonzalez ').
25 |
26 | -export_type([id/0]).
27 |
28 |
29 | %% ===================================================================
30 | %% Types
31 | %% ===================================================================
32 |
33 | -type id() :: nkmedia_kms_engine:id().
34 |
35 |
36 |
37 |
38 | %% ===================================================================
39 | %% Public functions
40 | %% ===================================================================
41 |
42 |
--------------------------------------------------------------------------------
/src/kms_backend/nkmedia_kms_api.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc NkMEDIA external API
22 |
23 | -module(nkmedia_kms_api).
24 | -author('Carlos Gonzalez ').
25 | -export([cmd/4]).
26 |
27 | -include_lib("nkservice/include/nkservice.hrl").
28 | % -include_lib("nksip/include/nksip.hrl").
29 |
30 |
31 | %% ===================================================================
32 | %% Types
33 | %% ===================================================================
34 |
35 |
36 | %% ===================================================================
37 | %% Commands
38 | %% ===================================================================
39 |
40 | %% @doc
41 | -spec cmd(nkservice:id(), atom(), Data::map(), map()) ->
42 | {ok, map(), State::map()} | {error, nkservice:error(), State::map()}.
43 |
44 | cmd(<<"session">>, Cmd, #api_req{data=Data}, State)
45 | when Cmd == <<"pause_record">>; Cmd == <<"resume_record">> ->
46 | #{session_id:=SessId} = Data,
47 | Cmd2 = binary_to_atom(Cmd, latin1),
48 | case nkmedia_session:cmd(SessId, Cmd2, Data) of
49 | {ok, Reply} ->
50 | {ok, Reply, State};
51 | {error, Error} ->
52 | {error, Error, State}
53 | end;
54 |
55 |
56 | cmd(_SrvId, _Other, _Data, _State) ->
57 | continue.
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/src/kms_backend/nkmedia_kms_api_syntax.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc NkMEDIA external API
22 | -module(nkmedia_kms_api_syntax).
23 | -author('Carlos Gonzalez ').
24 | -export([syntax/5]).
25 |
26 |
27 |
28 | %% ===================================================================
29 | %% Syntax
30 | %% ===================================================================
31 |
32 |
33 |
34 | %% @private
35 | syntax(_Sub, _Cmd, Syntax, Defaults, Mandatory) ->
36 | {Syntax, Defaults, Mandatory}.
37 |
--------------------------------------------------------------------------------
/src/kms_backend/nkmedia_kms_build.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc NkMEDIA Utilities to build KMS images
22 | -module(nkmedia_kms_build).
23 | -author('Carlos Gonzalez ').
24 |
25 | -export([build_base/0, build_base/1, remove_base/0, remove_base/1]).
26 | -export([build_run/0, build_run/1, remove_run/0, remove_run/1]).
27 | -export([run_name/1, defaults/1]).
28 |
29 | -include("../../include/nkmedia.hrl").
30 |
31 |
32 | %% Last is 6.5.0.20160530172436.trusty
33 | %% r02 uses last 14.04 with correct libssl1.0.2
34 |
35 | -define(KMS_COMP, <<"netcomposer">>).
36 | -define(KMS_VSN, <<"6.6.1">>).
37 | -define(KMS_REL, <<"r01">>).
38 |
39 |
40 |
41 | %% ===================================================================
42 | %% Public
43 | %% ===================================================================
44 |
45 |
46 | %% @doc Builds base image (netcomposer/nk_kurento_base:v1.6.5-r01)
47 | build_base() ->
48 | build_base(#{}).
49 |
50 |
51 | %% @doc
52 | build_base(Config) ->
53 | Name = base_name(Config),
54 | #{vsn:=Vsn} = defaults(Config),
55 | Tar = nkdocker_util:make_tar([{"Dockerfile", base_dockerfile(Vsn)}]),
56 | nkdocker_util:build(Name, Tar).
57 |
58 |
59 | %% @doc
60 | remove_base() ->
61 | remove_base(#{}).
62 |
63 |
64 | %% @doc
65 | remove_base(Config) ->
66 | Name = base_name(Config),
67 | case nkdocker:start_link() of
68 | {ok, Pid} ->
69 | Res = case nkdocker:rmi(Pid, Name, #{force=>true}) of
70 | {ok, _} -> ok;
71 | {error, {not_found, _}} -> ok;
72 | E3 -> lager:warning("NkMEDIA could not remove ~s: ~p", [Name, E3])
73 | end,
74 | nkdocker:stop(Pid),
75 | Res;
76 | {error, Error} ->
77 | {error, Error}
78 | end.
79 |
80 |
81 | %% @doc
82 | build_run() ->
83 | build_run(#{}).
84 |
85 |
86 | %% @doc Builds run image (netcomposer/nk_kurento:...)
87 | build_run(Config) ->
88 | Name = run_name(Config),
89 | Tar = nkdocker_util:make_tar([
90 | {"Dockerfile", run_dockerfile(Config)},
91 | {"start.sh", run_start()}
92 | ]),
93 | nkdocker_util:build(Name, Tar).
94 |
95 |
96 | %% @doc
97 | remove_run() ->
98 | remove_run(#{}).
99 |
100 |
101 | %% @doc
102 | remove_run(Config) ->
103 | Config2 = defaults(Config),
104 | Name = run_name(Config2),
105 | case nkdocker:start_link() of
106 | {ok, Pid} ->
107 | Res = case nkdocker:rmi(Pid, Name, #{force=>true}) of
108 | {ok, _} -> ok;
109 | {error, {not_found, _}} -> ok;
110 | E3 -> lager:warning("NkMEDIA could not remove ~s: ~p", [Name, E3])
111 | end,
112 | nkdocker:stop(Pid),
113 | Res;
114 | {error, Error} ->
115 | {error, Error}
116 | end.
117 |
118 |
119 | %% @private
120 | defaults(Config) ->
121 | Defs = #{
122 | comp => ?KMS_COMP,
123 | vsn => ?KMS_VSN,
124 | rel => ?KMS_REL
125 | },
126 | maps:merge(Defs, Config).
127 |
128 |
129 |
130 | %% ===================================================================
131 | %% Base image (Comp/nk_kurento_base:vXXX-rXXX)
132 | %% ===================================================================
133 |
134 |
135 | %% @private
136 | base_name(Config) ->
137 | Config2 = defaults(Config),
138 | #{comp:=Comp, vsn:=Vsn, rel:=Rel} = Config2,
139 | list_to_binary([Comp, "/nk_kurento_base:", Vsn, "-", Rel]).
140 |
141 |
142 | % %% @private 6.5.0 r02
143 | % base_dockerfile(_Vsn) ->
144 | % <<"
145 | % FROM ubuntu:14.04
146 | % RUN apt-get update && apt-get install -y wget vim nano telnet && \\
147 | % echo \"deb http://ubuntu.kurento.org trusty kms6\" | tee /etc/apt/sources.list.d/kurento.list && \\
148 | % wget -O - http://ubuntu.kurento.org/kurento.gpg.key | apt-key add - && \\
149 | % apt-get update && \\
150 | % apt-get -y install kurento-media-server-6.0 && \\
151 | % apt-get -y upgrade && \\
152 | % apt-get clean && rm -rf /var/lib/apt/lists/*
153 | % ">>.
154 |
155 |
156 | %% @private
157 | base_dockerfile(_Vsn) ->
158 | <<"
159 | FROM ubuntu:14.04
160 | RUN apt-get update && apt-get install -y wget vim nano telnet && \\
161 | echo \"deb http://ubuntu.kurento.org trusty-dev kms6\" | tee /etc/apt/sources.list.d/kurento.list && \\
162 | wget -O - http://ubuntu.kurento.org/kurento.gpg.key | apt-key add - && \\
163 | apt-get update && \\
164 | apt-get -y install kurento-media-server-6.0 && \\
165 | apt-get -y install kms-crowddetector-6.0 kms-platedetector-6.0 kms-pointerdetector-6.0 && \\
166 | apt-get -y dist-upgrade && \\
167 | apt-get clean && rm -rf /var/lib/apt/lists/*
168 | ">>.
169 |
170 |
171 | %% ===================================================================
172 | %% Instance build files (Comp/nk_kurento:vXXX-rXXX)
173 | %% ===================================================================
174 |
175 |
176 | %% @private
177 | run_name(Config) ->
178 | Config2 = defaults(Config),
179 | #{comp:=Comp, vsn:=Vsn, rel:=Rel} = Config2,
180 | list_to_binary([Comp, "/nk_kurento:", Vsn, "-", Rel]).
181 |
182 |
183 | run_dockerfile(Config) ->
184 | list_to_binary([
185 | "FROM ", base_name(Config), "\n"
186 | "WORKDIR /root\n"
187 | "ADD start.sh /usr/local/bin\n"
188 | "ENTRYPOINT [\"sh\", \"/usr/local/bin/start.sh\"]\n"
189 | ]).
190 |
191 |
192 | run_start() ->
193 | WebRTC = config_webrtc(),
194 | <<"
195 | #!/bin/bash
196 | set -e
197 | BASE=${NK_BASE-50020}
198 | perl -i -pe s/8888/$BASE/g /etc/kurento/kurento.conf.json
199 |
200 | STUN_IP=${NK_STUN_IP-\"83.211.9.232\"}
201 | STUN_PORT=${NK_STUN_PORT-3478}
202 |
203 | export CONF=\"/etc/kurento/modules/kurento\"
204 |
205 | cp $CONF/WebRtcEndpoint.conf.ini $CONF/WebRtcEndpoint.conf.ini.0
206 | cat > $CONF/WebRtcEndpoint.conf.ini <&1
212 | ">>.
213 |
214 |
215 | config_webrtc() ->
216 | <<"
217 | ; Only IP address are supported, not domain names for addresses
218 | ; You have to find a valid stun server. You can check if it works
219 | ; usin this tool:
220 | ; http://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/
221 | stunServerAddress=$STUN_IP
222 | stunServerPort=$STUN_PORT
223 |
224 | ; turnURL gives the necessary info to configure TURN for WebRTC.
225 | ; 'address' must be an IP (not a domain).
226 | ; 'transport' is optional (UDP by default).
227 | ; turnURL=user:password@address:port(?transport=[udp|tcp|tls])
228 | ">>.
229 |
230 |
231 |
232 | %% ===================================================================
233 | %% Utilities
234 | %% ===================================================================
235 |
236 |
237 |
--------------------------------------------------------------------------------
/src/kms_backend/nkmedia_kms_docker.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc NkMEDIA Docker management application
22 | -module(nkmedia_kms_docker).
23 | -author('Carlos Gonzalez ').
24 |
25 | -export([start/1, stop/1, stop_all/0]).
26 | -export([notify/4]).
27 |
28 | -include("../../include/nkmedia.hrl").
29 |
30 | %% ===================================================================
31 | %% Types
32 | %% ===================================================================
33 |
34 | %% ===================================================================
35 | %% Kurento Instance
36 | %% ===================================================================
37 |
38 |
39 | %% @doc Starts a KMS instance
40 | -spec start(nkservice:name()) ->
41 | {ok, Name::binary()} | {error, term()}.
42 |
43 | start(Service) ->
44 | try
45 | SrvId = case nkservice_srv:get_srv_id(Service) of
46 | {ok, SrvId0} -> SrvId0;
47 | not_found -> throw(unknown_service)
48 | end,
49 | Config = nkservice_srv:get_item(SrvId, config_nkmedia_kms),
50 | BasePort = 36000, %crypto:rand_uniform(32768, 65535),
51 | Image = nkmedia_kms_build:run_name(Config),
52 | KmsIp = nklib_util:to_host(nkmedia_app:get(docker_ip)),
53 | Name = list_to_binary([
54 | "nk_kms_",
55 | nklib_util:to_binary(SrvId), "_",
56 | nklib_util:to_binary(BasePort)
57 | ]),
58 | {_, [{StunIp, StunPort}|_]} = nkpacket_stun:get_stun_servers(),
59 | _LogDir = <<(nkmedia_app:get(log_dir))/binary, $/, Name/binary>>,
60 | _RecDir = filename:join(nkmedia_app:get(record_dir), <<"tmp">>),
61 | Env = [
62 | {"NK_KMS_IP", KmsIp},
63 | {"NK_BASE", nklib_util:to_binary(BasePort)},
64 | {"NK_SRV_ID", nklib_util:to_binary(SrvId)},
65 | {"NK_STUN_IP", nklib_util:to_host(StunIp)},
66 | {"NK_STUN_PORT", StunPort},
67 | {"GST_DEBUG", "Kurento*:5"}
68 |
69 | ],
70 | Labels = [
71 | {"nkmedia", "kurento"}
72 | ],
73 | DockerOpts = #{
74 | name => Name,
75 | env => Env,
76 | net => host,
77 | interactive => true,
78 | labels => Labels,
79 | ulimits => [{nproc, 65536, 65536}]
80 | % volumes => [{LogDir, "/usr/local/kurento/log"}]
81 | },
82 | DockerPid = case get_docker_pid() of
83 | {ok, DockerPid0} -> DockerPid0;
84 | {error, Error1} -> throw(Error1)
85 | end,
86 | nkdocker:rm(DockerPid, Name),
87 | case nkdocker:create(DockerPid, Image, DockerOpts) of
88 | {ok, _} -> ok;
89 | {error, Error2} -> throw(Error2)
90 | end,
91 | lager:info("NkMEDIA KMS Docker: starting instance ~s", [Name]),
92 | case nkdocker:start(DockerPid, Name) of
93 | ok ->
94 | {ok, Name};
95 | {error, Error3} ->
96 | {error, Error3}
97 | end
98 | catch
99 | throw:Throw -> {error, Throw}
100 | end.
101 |
102 |
103 |
104 | %% @doc Stops a KMS instance
105 | -spec stop(binary()) ->
106 | ok | {error, term()}.
107 |
108 | stop(Name) ->
109 | case get_docker_pid() of
110 | {ok, DockerPid} ->
111 | case nkdocker:kill(DockerPid, Name) of
112 | ok -> ok;
113 | {error, {not_found, _}} -> ok;
114 | E1 -> lager:warning("NkMEDIA could not kill ~s: ~p", [Name, E1])
115 | end,
116 | case nkdocker:rm(DockerPid, Name) of
117 | ok -> ok;
118 | {error, {not_found, _}} -> ok;
119 | E2 -> lager:warning("NkMEDIA could not remove ~s: ~p", [Name, E2])
120 | end,
121 | ok;
122 | {error, Error} ->
123 | {error, Error}
124 | end.
125 |
126 |
127 | %% @doc
128 | stop_all() ->
129 | case get_docker_pid() of
130 | {ok, DockerPid} ->
131 | {ok, List} = nkdocker:ps(DockerPid),
132 | lists:foreach(
133 | fun(#{<<"Names">>:=[<<"/", Name/binary>>|_]}) ->
134 | case Name of
135 | <<"nk_kms_", _/binary>> ->
136 | lager:info("Stopping ~s", [Name]),
137 | stop(Name);
138 | _ ->
139 | ok
140 | end
141 | end,
142 | List);
143 | {error, Error} ->
144 | {error, Error}
145 | end.
146 |
147 |
148 | %% @private
149 | -spec get_docker_pid() ->
150 | {ok, pid()} | {error, term()}.
151 |
152 | get_docker_pid() ->
153 | DockerMonId = nkmedia_app:get(docker_kms_mon_id),
154 | nkdocker_monitor:get_docker(DockerMonId).
155 |
156 |
157 |
158 | %% @private
159 | notify(MonId, ping, Name, Data) ->
160 | notify(MonId, start, Name, Data);
161 |
162 | notify(MonId, start, Name, Data) ->
163 | case Data of
164 | #{
165 | name := Name,
166 | labels := #{<<"nkmedia">> := <<"kurento">>},
167 | env := #{
168 | <<"NK_KMS_IP">> := Host,
169 | <<"NK_BASE">> := Base,
170 | <<"NK_SRV_ID">> := SrvId
171 | },
172 | image := Image
173 | } ->
174 | case binary:split(Image, <<"/">>) of
175 | [Comp, <<"nk_kurento:", Tag/binary>>] ->
176 | [Vsn, Rel] = binary:split(Tag, <<"-">>),
177 | Config = #{
178 | srv_id => nklib_util:to_atom(SrvId),
179 | name => Name,
180 | comp => Comp,
181 | vsn => Vsn,
182 | rel => Rel,
183 | host => Host,
184 | base => nklib_util:to_integer(Base)
185 | },
186 | connect_kms(MonId, Config);
187 | _ ->
188 | lager:warning("Started unrecognized kurento")
189 | end;
190 | _ ->
191 | lager:warning("Started unrecognized kurento")
192 | end;
193 |
194 | notify(MonId, stop, Name, Data) ->
195 | case Data of
196 | #{
197 | name := Name,
198 | labels := #{<<"nkmedia">> := <<"kurento">>}
199 | } ->
200 | remove_kms(MonId, Name);
201 | _ ->
202 | ok
203 | end;
204 |
205 | notify(_MonId, stats, Name, Stats) ->
206 | nkmedia_kms_engine:stats(Name, Stats).
207 |
208 |
209 |
210 | %% ===================================================================
211 | %% Internal
212 | %% ===================================================================
213 |
214 | %% @private
215 | connect_kms(_MonId, #{name:=Name}=Config) ->
216 | spawn(
217 | fun() ->
218 | % timer:sleep(2000),
219 | case nkmedia_kms_engine:connect(Config) of
220 | {ok, _Pid} ->
221 | % ok = nkdocker_monitor:start_stats(MonId, Name);
222 | ok;
223 | {error, {already_started, _Pid}} ->
224 | ok;
225 | {error, Error} ->
226 | lager:warning("Could not connect to Kurento ~s: ~p",
227 | [Name, Error])
228 | end
229 | end),
230 | ok.
231 |
232 |
233 | %% @private
234 | remove_kms(MonId, Name) ->
235 | spawn(
236 | fun() ->
237 | nkmedia_kms_engine:stop(Name),
238 | case nkdocker_monitor:get_docker(MonId) of
239 | {ok, Pid} ->
240 | nkdocker:rm(Pid, Name);
241 | _ ->
242 | ok
243 | end
244 | end),
245 | ok.
246 |
247 |
--------------------------------------------------------------------------------
/src/kms_backend/nkmedia_kms_proxy.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc Plugin implementing a Kurento proxy server for testing
22 | -module(nkmedia_kms_proxy).
23 | -author('Carlos Gonzalez ').
24 |
25 | -export([plugin_deps/0, plugin_syntax/0, plugin_listen/2,
26 | plugin_start/2, plugin_stop/2]).
27 | -export([nkmedia_kms_proxy_init/2, nkmedia_kms_proxy_find_kms/2,
28 | nkmedia_kms_proxy_in/2, nkmedia_kms_proxy_out/2,
29 | nkmedia_kms_proxy_terminate/2, nkmedia_kms_proxy_handle_call/3,
30 | nkmedia_kms_proxy_handle_cast/2, nkmedia_kms_proxy_handle_info/2]).
31 |
32 |
33 | -define(WS_TIMEOUT, 60*60*1000).
34 | -include_lib("nkservice/include/nkservice.hrl").
35 |
36 |
37 |
38 | %% ===================================================================
39 | %% Types
40 | %% ===================================================================
41 |
42 | -type state() :: term().
43 | -type continue() :: continue | {continue, list()}.
44 |
45 | %% ===================================================================
46 | %% Plugin callbacks
47 | %% ===================================================================
48 |
49 |
50 | plugin_deps() ->
51 | [nkmedia_kms].
52 |
53 |
54 | plugin_syntax() ->
55 | nkpacket:register_protocol(kms, nkmedia_kms_proxy_server),
56 | nkpacket:register_protocol(kmss, nkmedia_kms_proxy_server),
57 | #{
58 | kurento_proxy => fun parse_listen/3
59 | }.
60 |
61 |
62 | plugin_listen(Config, #{id:=SrvId}) ->
63 | % kurento_proxy will be already parsed
64 | Listen = maps:get(kurento_proxy, Config, []),
65 | % With the 'user' parameter we tell nkmedia_kurento protocol
66 | % to use the service callback module, so it will find
67 | % nkmedia_kurento_* funs there.
68 | Opts = #{
69 | class => {nkmedia_kms_proxy, SrvId},
70 | idle_timeout => ?WS_TIMEOUT
71 | },
72 | [{Conns, maps:merge(ConnOpts, Opts)} || {Conns, ConnOpts} <- Listen].
73 |
74 |
75 |
76 | plugin_start(Config, #{name:=Name}) ->
77 | lager:info("Plugin NkMEDIA KMS Proxy (~s) starting", [Name]),
78 | {ok, Config}.
79 |
80 |
81 | plugin_stop(Config, #{name:=Name}) ->
82 | lager:info("Plugin NkMEDIA KMS Proxy (~p) stopping", [Name]),
83 | {ok, Config}.
84 |
85 |
86 |
87 | %% ===================================================================
88 | %% Offering callbacks
89 | %% ===================================================================
90 |
91 |
92 |
93 | %% @doc Called when a new KMS proxy connection arrives
94 | -spec nkmedia_kms_proxy_init(nkpacket:nkport(), state()) ->
95 | {ok, state()}.
96 |
97 | nkmedia_kms_proxy_init(_NkPort, State) ->
98 | {ok, State}.
99 |
100 |
101 | %% @doc Called to select a KMS server
102 | -spec nkmedia_kms_proxy_find_kms(nkmedia_service:id(), state()) ->
103 | {ok, [nkmedia_kms_engine:id()], state()}.
104 |
105 | nkmedia_kms_proxy_find_kms(SrvId, State) ->
106 | List = [Name || {Name, _} <- nkmedia_kms_engine:get_all(SrvId)],
107 | {ok, List, State}.
108 |
109 |
110 | %% @doc Called when a new msg arrives
111 | -spec nkmedia_kms_proxy_in(map(), state()) ->
112 | {ok, map(), state()} | {stop, term(), state()} | continue().
113 |
114 | nkmedia_kms_proxy_in(Msg, State) ->
115 | {ok, Msg, State}.
116 |
117 |
118 | %% @doc Called when a new msg is to be answered
119 | -spec nkmedia_kms_proxy_out(map(), state()) ->
120 | {ok, map(), state()} | {stop, term(), state()} | continue().
121 |
122 | nkmedia_kms_proxy_out(Msg, State) ->
123 | {ok, Msg, State}.
124 |
125 |
126 | %% @doc Called when the connection is stopped
127 | -spec nkmedia_kms_proxy_terminate(Reason::term(), state()) ->
128 | {ok, state()}.
129 |
130 | nkmedia_kms_proxy_terminate(_Reason, State) ->
131 | {ok, State}.
132 |
133 |
134 | %% @doc
135 | -spec nkmedia_kms_proxy_handle_call(Msg::term(), {pid(), term()}, state()) ->
136 | {ok, state()} | continue().
137 |
138 | nkmedia_kms_proxy_handle_call(Msg, _From, State) ->
139 | lager:error("Module ~p received unexpected call: ~p", [?MODULE, Msg]),
140 | {ok, State}.
141 |
142 |
143 | %% @doc
144 | -spec nkmedia_kms_proxy_handle_cast(Msg::term(), state()) ->
145 | {ok, state()}.
146 |
147 | nkmedia_kms_proxy_handle_cast(Msg, State) ->
148 | lager:error("Module ~p received unexpected cast: ~p", [?MODULE, Msg]),
149 | {ok, State}.
150 |
151 |
152 | %% @doc
153 | -spec nkmedia_kms_proxy_handle_info(Msg::term(), state()) ->
154 | {ok, State::map()}.
155 |
156 | nkmedia_kms_proxy_handle_info(Msg, State) ->
157 | lager:error("Module ~p received unexpected info: ~p", [?MODULE, Msg]),
158 | {ok, State}.
159 |
160 |
161 |
162 |
163 |
164 | %% ===================================================================
165 | %% Internal
166 | %% ===================================================================
167 |
168 |
169 | parse_listen(_Key, [{[{_, _, _, _}|_], Opts}|_]=Multi, _Ctx) when is_map(Opts) ->
170 | {ok, Multi};
171 |
172 | parse_listen(kurento_proxy, Url, _Ctx) ->
173 | Opts = #{valid_schemes=>[kms, kmss], resolve_type=>listen},
174 | case nkpacket:multi_resolve(Url, Opts) of
175 | {ok, List} -> {ok, List};
176 | _ -> error
177 | end.
178 |
179 |
180 |
181 |
182 |
183 |
--------------------------------------------------------------------------------
/src/kms_backend/nkmedia_kms_proxy_server.erl:
--------------------------------------------------------------------------------
1 |
2 | %% -------------------------------------------------------------------
3 | %%
4 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
5 | %%
6 | %% This file is provided to you under the Apache License,
7 | %% Version 2.0 (the "License"); you may not use this file
8 | %% except in compliance with the License. You may obtain
9 | %% a copy of the License at
10 | %%
11 | %% http://www.apache.org/licenses/LICENSE-2.0
12 | %%
13 | %% Unless required by applicable law or agreed to in writing,
14 | %% software distributed under the License is distributed on an
15 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | %% KIND, either express or implied. See the License for the
17 | %% specific language governing permissions and limitations
18 | %% under the License.
19 | %%
20 | %% -------------------------------------------------------------------
21 |
22 | %% @doc
23 | -module(nkmedia_kms_proxy_server).
24 | -author('Carlos Gonzalez ').
25 |
26 | -export([get_all/0, send_reply/2]).
27 | -export([transports/1, default_port/1]).
28 | -export([conn_init/1, conn_encode/2, conn_parse/3, conn_stop/3,
29 | conn_handle_call/4, conn_handle_cast/3, conn_handle_info/3]).
30 |
31 |
32 | -define(LLOG(Type, Txt, Args, State),
33 | lager:Type("NkMEDIA KMS proxy server (~s) "++Txt, [State#state.remote | Args])).
34 |
35 |
36 | %% ===================================================================
37 | %% Types
38 | %% ===================================================================
39 |
40 | -type user_state() :: nkmedia_kms_proxy:state().
41 |
42 |
43 | %% ===================================================================
44 | %% Public
45 | %% ===================================================================
46 |
47 | get_all() ->
48 | [{Local, Remote} || {Remote, Local} <- nklib_proc:values(?MODULE)].
49 |
50 |
51 | send_reply(Pid, Event) ->
52 | gen_server:call(Pid, {send_reply, self(), Event}).
53 |
54 |
55 |
56 | %% ===================================================================
57 | %% Protocol callbacks
58 | %% ===================================================================
59 |
60 |
61 | -record(state, {
62 | srv_id :: nkservice:id(),
63 | remote :: binary(),
64 | proxy :: pid(),
65 | user_state :: user_state()
66 | }).
67 |
68 |
69 | %% @private
70 | -spec transports(nklib:scheme()) ->
71 | [nkpacket:transport()].
72 |
73 | transports(kms) -> [ws];
74 | transports(kmss) -> [wss].
75 |
76 | -spec default_port(nkpacket:transport()) ->
77 | inet:port_number() | invalid.
78 |
79 | default_port(ws) -> 8001;
80 | default_port(wss) -> 8002.
81 |
82 |
83 | -spec conn_init(nkpacket:nkport()) ->
84 | {ok, #state{}}.
85 |
86 | conn_init(NkPort) ->
87 | {ok, {_, SrvId}, _} = nkpacket:get_user(NkPort),
88 | {ok, Remote} = nkpacket:get_remote_bin(NkPort),
89 | State = #state{srv_id=SrvId, remote=Remote},
90 | ?LLOG(notice, "new connection (~s, ~p)", [Remote, self()], State),
91 | {ok, State2} = handle(nkmedia_kms_proxy_init, [NkPort], State),
92 | {ok, List, State3} = handle(nkmedia_kms_proxy_find_kms, [SrvId], State2),
93 | connect(List, State3).
94 |
95 |
96 |
97 | %% @private
98 | -spec conn_parse(term()|close, nkpacket:nkport(), #state{}) ->
99 | {ok, #state{}} | {stop, term(), #state{}}.
100 |
101 | conn_parse(close, _NkPort, State) ->
102 | {ok, State};
103 |
104 | conn_parse({text, Data}, _NkPort, #state{proxy=Pid}=State) ->
105 | Msg = case nklib_json:decode(Data) of
106 | error ->
107 | ?LLOG(warning, "JSON decode error: ~p", [Data], State),
108 | error(json_decode);
109 | Json ->
110 | Json
111 | end,
112 | % ?LLOG(info, "received\n~s", [nklib_json:encode_pretty(Msg)], State),
113 | case handle(nkmedia_kms_proxy_in, [Msg], State) of
114 | {ok, Msg2, State2} ->
115 | ok = nkmedia_kms_proxy_client:send(Pid, self(), Msg2),
116 | {ok, State2};
117 | {stop, Reason, State2} ->
118 | {stop, Reason, State2}
119 | end.
120 |
121 |
122 | %% @private
123 | -spec conn_encode(term(), nkpacket:nkport()) ->
124 | {ok, nkpacket:outcoming()} | continue | {error, term()}.
125 |
126 | conn_encode(Msg, _NkPort) when is_map(Msg) ->
127 | Json = nklib_json:encode(Msg),
128 | {ok, {text, Json}};
129 |
130 | conn_encode(Msg, _NkPort) when is_binary(Msg) ->
131 | {ok, {text, Msg}}.
132 |
133 |
134 | %% @doc Called when the connection received an erlang message
135 | -spec conn_handle_call(term(), term(), nkpacket:nkport(), #state{}) ->
136 | {ok, #state{}} | {stop, Reason::term(), #state{}}.
137 |
138 | conn_handle_call({send_reply, _Pid, Event}, From, NkPort, State) ->
139 | % ?LLOG(info, "sending\n~s", [nklib_json:encode_pretty(Event)], State),
140 | case handle(nkmedia_kms_proxy_out, [Event], State) of
141 | {ok, Event2, State2} ->
142 | case nkpacket_connection:send(NkPort, Event2) of
143 | ok ->
144 | gen_server:reply(From, ok),
145 | {ok, State2};
146 | {error, Error} ->
147 | gen_server:reply(From, error),
148 | ?LLOG(notice, "error sending event: ~p", [Error], State),
149 | {stop, normal, State2}
150 | end;
151 | {stop, Reason, State2} ->
152 | {stop, Reason, State2}
153 | end;
154 |
155 | conn_handle_call(Msg, From, _NkPort, State) ->
156 | handle(nkmedia_kms_proxy_handle_call, [Msg, From], State).
157 |
158 |
159 | -spec conn_handle_cast(term(), nkpacket:nkport(), #state{}) ->
160 | {ok, #state{}} | {stop, Reason::term(), #state{}}.
161 |
162 | conn_handle_cast(Msg, _NkPort, State) ->
163 | handle(nkmedia_kms_proxy_handle_cast, [Msg], State).
164 |
165 |
166 | %% @doc Called when the connection received an erlang message
167 | -spec conn_handle_info(term(), nkpacket:nkport(), #state{}) ->
168 | {ok, #state{}} | {stop, Reason::term(), #state{}}.
169 |
170 | conn_handle_info({'DOWN', _Ref, process, Pid, Reason}, _NkPort,
171 | #state{proxy=Pid}=State) ->
172 | ?LLOG(notice, "stopped because server stopped (~p)", [Reason], State),
173 | {stop, normal, State};
174 |
175 | conn_handle_info(Msg, _NkPort, State) ->
176 | handle(nkmedia_kms_proxy_handle_info, [Msg], State).
177 |
178 |
179 | %% @doc Called when the connection stops
180 | -spec conn_stop(Reason::term(), nkpacket:nkport(), #state{}) ->
181 | ok.
182 |
183 | conn_stop(Reason, _NkPort, State) ->
184 | catch handle(nkmedia_kms_proxy_terminate, [Reason], State).
185 |
186 |
187 | %% ===================================================================
188 | %% Internal
189 | %% ===================================================================
190 |
191 | %% @private
192 | handle(Fun, Args, State) ->
193 | nklib_gen_server:handle_any(Fun, Args, State, #state.srv_id, #state.user_state).
194 |
195 |
196 | %% @private
197 | connect([], _State) ->
198 | {stop, no_kms_available};
199 |
200 | connect([Name|Rest], State) ->
201 | case nkmedia_kms_proxy_client:start(Name) of
202 | {ok, ProxyPid} ->
203 | ?LLOG(info, "connected to Kurento server ~s", [Name], State),
204 | monitor(process, ProxyPid),
205 | nklib_proc:put(?MODULE, {proxy_client, ProxyPid}),
206 | {ok, State#state{proxy=ProxyPid}};
207 | {error, Error} ->
208 | ?LLOG(warning, "could not start proxy to ~s: ~p",
209 | [Name, Error], State),
210 | connect(Rest, State)
211 | end.
212 |
213 |
214 |
215 |
--------------------------------------------------------------------------------
/src/kms_backend/nkmedia_kms_room.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc Kurento room management
22 | -module(nkmedia_kms_room).
23 | -author('Carlos Gonzalez ').
24 |
25 | -export([init/2, terminate/2, timeout/2]).
26 |
27 | -define(LLOG(Type, Txt, Args, Room),
28 | lager:Type("NkMEDIA Kms Room ~s "++Txt, [maps:get(room_id, Room) | Args])).
29 |
30 | -include("../../include/nkmedia_room.hrl").
31 |
32 |
33 | %% ===================================================================
34 | %% Types
35 | %% ===================================================================
36 |
37 |
38 | -type room_id() :: nkmedia_room:id().
39 |
40 | -type room() ::
41 | nkmedia_room:room() |
42 | #{
43 | nkmedia_kms_id => nkmedia_kms:id()
44 | }.
45 |
46 |
47 |
48 | %% ===================================================================
49 | %% External
50 | %% ===================================================================
51 |
52 |
53 |
54 |
55 | %% ===================================================================
56 | %% Callbacks
57 | %% ===================================================================
58 |
59 |
60 |
61 | %% @doc Creates a new room
62 | -spec init(room_id(), room()) ->
63 | {ok, room()} | {error, term()}.
64 |
65 | init(_RoomId, Room) ->
66 | case get_kms(Room) of
67 | {ok, Room2} ->
68 | {ok, ?ROOM(#{class=>sfu, backend=>nkmedia_kms}, Room2)};
69 | error ->
70 | {error, no_mediaserver}
71 | end.
72 |
73 |
74 | %% @doc
75 | -spec terminate(term(), room()) ->
76 | {ok, room()} | {error, term()}.
77 |
78 | terminate(_Reason, Room) ->
79 | ?LLOG(info, "stopping, destroying room", [], Room),
80 | {ok, Room}.
81 |
82 |
83 |
84 | %% @private
85 | -spec timeout(room_id(), room()) ->
86 | {ok, room()} | {stop, nkservice:error(), room()}.
87 |
88 | timeout(_RoomId, Room) ->
89 | case length(nkmedia_room:get_all_with_role(publisher, Room)) of
90 | 0 ->
91 | {stop, timeout, Room};
92 | _ ->
93 | {ok, Room}
94 | end.
95 |
96 |
97 |
98 | % ===================================================================
99 | %% Internal
100 | %% ===================================================================
101 |
102 |
103 | %% @private
104 | get_kms(#{nkmedia_kms_id:=_}=Room) ->
105 | {ok, Room};
106 |
107 | get_kms(#{srv_id:=SrvId}=Room) ->
108 | case SrvId:nkmedia_kms_get_mediaserver(SrvId) of
109 | {ok, KmsId} ->
110 | {ok, ?ROOM(#{nkmedia_kms_id=>KmsId}, Room)};
111 | {error, _Error} ->
112 | error
113 | end.
114 |
--------------------------------------------------------------------------------
/src/nkmedia.app.src:
--------------------------------------------------------------------------------
1 | {application, nkmedia, [
2 | {description, "NkMEDIA Framework"},
3 | {vsn, "develop"},
4 | {modules, []},
5 | {registered, []},
6 | {mod, {nkmedia_app, []}},
7 | {applications, [nkdocker, nksip, inets]},
8 | {env, []}
9 | ]}.
10 |
--------------------------------------------------------------------------------
/src/nkmedia.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc NkMEDIA application
22 |
23 | -module(nkmedia).
24 | -author('Carlos Gonzalez ').
25 | -export_type([offer/0, answer/0, role/0, candidate/0]).
26 | -export_type([engine_id/0, engine_config/0]).
27 |
28 | -include("nkmedia.hrl").
29 | -include_lib("nksip/include/nksip.hrl").
30 |
31 |
32 | %% ===================================================================
33 | %% Types
34 | %% ===================================================================
35 |
36 |
37 | -type offer() ::
38 | #{
39 | sdp => binary(),
40 | sdp_type => rtp | webrtc,
41 | trickle_ice => boolean(), % Default false, all candidates must be in SDP
42 | backend => atom()
43 | }.
44 |
45 |
46 | -type answer() ::
47 | #{
48 | sdp => binary(),
49 | sdp_type => rtp | webrtc,
50 | trickle_ice => boolean(), % Default false, all candidates must be in SDP
51 | backend => atom()
52 | }.
53 |
54 | -type role() ::
55 | offerer | offeree.
56 |
57 |
58 | -type engine_id() :: binary().
59 |
60 |
61 | -type engine_config() ::
62 | #{
63 | srv_id => nkservice:id(), % Service Id
64 | name => binary(), % Engine Id (docker name)
65 | comp => binary(), % Docker Company
66 | vsn => binary(), % Version
67 | rel => binary(), % Release
68 | host => binary(), % Host
69 | pass => binary(), % Pass
70 | base => integer() % Base Port
71 | }.
72 |
73 |
74 | -type candidate() :: #candidate{}.
75 |
76 |
77 |
78 | %% ===================================================================
79 | %% Public functions
80 | %% ===================================================================
81 |
--------------------------------------------------------------------------------
/src/nkmedia_api.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc NkMEDIA external API
22 |
23 | -module(nkmedia_api).
24 | -author('Carlos Gonzalez ').
25 | -export([cmd/3]).
26 | -export([session_stopped/3, api_session_down/3]).
27 |
28 | -include_lib("nkservice/include/nkservice.hrl").
29 | -include_lib("nksip/include/nksip.hrl").
30 |
31 |
32 | %% ===================================================================
33 | %% Types
34 | %% ===================================================================
35 |
36 |
37 | %% ===================================================================
38 | %% Commands
39 | %% ===================================================================
40 |
41 | %% @doc
42 | -spec cmd(binary(), Data::map(), map()) ->
43 | {ok, map(), State::map()} | {error, nkservice:error(), State::map()}.
44 |
45 | %% Create a session from the API
46 | %% We create the session linked with the API server process
47 | %% - we capture the destroy event (nkmedia_session_reg_event() -> session_stopped() here)
48 | %% - if the session is killed, it is detected in
49 | %% api_server_reg_down() -> api_session_down() here
50 | %% It also subscribes the API session to events
51 | cmd(<<"create">>, Req, State) ->
52 | #api_req{srv_id=SrvId, data=Data, user=User, session=UserSession} = Req,
53 | #{type:=Type} = Data,
54 | Config = Data#{
55 | register => {nkmedia_api, self()},
56 | user_id => User,
57 | user_session => UserSession
58 | },
59 | {ok, SessId, Pid} = nkmedia_session:start(SrvId, Type, Config),
60 | nkservice_api_server:register(self(), {nkmedia_session, SessId, Pid}),
61 | case maps:get(subscribe, Data, true) of
62 | true ->
63 | Body = maps:get(events_body, Data, #{}),
64 | Event = get_session_event(SrvId, SessId, Body),
65 | nkservice_api_server:register_event(self(), Event);
66 | false ->
67 | ok
68 | end,
69 | case get_create_reply(SessId, Config) of
70 | {ok, Reply} ->
71 | {ok, Reply, State};
72 | {error, Error} ->
73 | nkmedia_session:stop(SessId, Error),
74 | {error, Error, State}
75 | end;
76 |
77 | cmd(<<"destroy">>, #api_req{data=Data}, State) ->
78 | #{session_id:=SessId} = Data,
79 | nkmedia_session:stop(SessId),
80 | {ok, #{}, State};
81 |
82 | cmd(<<"set_answer">>, #api_req{data=Data}, State) ->
83 | #{answer:=Answer, session_id:=SessId} = Data,
84 | case nkmedia_session:cmd(SessId, set_answer, #{answer=>Answer}) of
85 | {ok, Reply} ->
86 | {ok, Reply, State};
87 | {error, Error} ->
88 | {error, Error, State}
89 | end;
90 |
91 | cmd(<<"get_offer">>, #api_req{data=#{session_id:=SessId}}, State) ->
92 | case nkmedia_session:get_offer(SessId) of
93 | {ok, Offer} ->
94 | {ok, Offer, State};
95 | {error, Error} ->
96 | {error, Error, State}
97 | end;
98 |
99 | cmd(<<"get_answer">>, #api_req{data=#{session_id:=SessId}}, State) ->
100 | case nkmedia_session:get_answer(SessId) of
101 | {ok, Answer} ->
102 | {ok, Answer, State};
103 | {error, Error} ->
104 | {error, Error, State}
105 | end;
106 |
107 | cmd(Cmd, #api_req{data=Data}, State)
108 | when Cmd == <<"update_media">>;
109 | Cmd == <<"set_type">>;
110 | Cmd == <<"recorder_action">>;
111 | Cmd == <<"player_action">>;
112 | Cmd == <<"room_action">> ->
113 | #{session_id:=SessId} = Data,
114 | Cmd2 = binary_to_atom(Cmd, latin1),
115 | case nkmedia_session:cmd(SessId, Cmd2, Data) of
116 | {ok, Reply} ->
117 | {ok, Reply, State};
118 | {error, Error} ->
119 | {error, Error, State}
120 | end;
121 |
122 | cmd(<<"set_candidate">>, #api_req{data=Data}, State) ->
123 | #{
124 | session_id := SessId,
125 | sdpMid := Id,
126 | sdpMLineIndex := Index,
127 | candidate := ALine
128 | } = Data,
129 | Candidate = #candidate{m_id=Id, m_index=Index, a_line=ALine},
130 | case nkmedia_session:candidate(SessId, Candidate) of
131 | ok ->
132 | {ok, #{}, State};
133 | {error, Error} ->
134 | {error, Error, State}
135 | end;
136 |
137 | cmd(<<"set_candidate_end">>, #api_req{data=Data}, State) ->
138 | #{session_id := SessId} = Data,
139 | Candidate = #candidate{last=true},
140 | case nkmedia_session:candidate(SessId, Candidate) of
141 | ok ->
142 | {ok, #{}, State};
143 | {error, Error} ->
144 | {error, Error, State}
145 | end;
146 |
147 | cmd(<<"get_info">>, #api_req{data=Data}, State) ->
148 | #{session_id:=SessId} = Data,
149 | case nkmedia_session:get_session(SessId) of
150 | {ok, Session} ->
151 | Data2 = nkmedia_api_syntax:get_info(Session),
152 | {ok, Data2, State};
153 | {error, Error} ->
154 | {error, Error, State}
155 | end;
156 |
157 | cmd(<<"get_list">>, _Req, State) ->
158 | Res = [#{session_id=>Id} || {Id, _Pid} <- nkmedia_session:get_all()],
159 | {ok, Res, State};
160 |
161 |
162 | cmd(Other, _Data, State) ->
163 | {error, {unknown_command, Other}, State}.
164 |
165 |
166 |
167 |
168 | %% ===================================================================
169 | %% Session callbacks
170 | %% ===================================================================
171 |
172 | %% @private Sent by the session when it is stopping
173 | %% We sent a message to the API session to remove the session before
174 | %% it receives the DOWN.
175 | session_stopped(SessId, ApiPid, Session) ->
176 | #{srv_id:=SrvId} = Session,
177 | Event = get_session_event(SrvId, SessId, undefined),
178 | nkservice_api_server:unregister_event(ApiPid, Event),
179 | nkservice_api_server:unregister(ApiPid, {nkmedia_session, SessId, self()}),
180 | {ok, Session}.
181 |
182 |
183 |
184 | %% ===================================================================
185 | %% API server callbacks
186 | %% ===================================================================
187 |
188 |
189 | %% @private Called when API server detects a registered session is down
190 | %% Normally it should have been unregistered first
191 | %% (detected above and sent in the cast after)
192 | api_session_down(SessId, Reason, State) ->
193 | #{srv_id:=SrvId} = State,
194 | lager:warning("API Server: Session ~s is down: ~p", [SessId, Reason]),
195 | Event = get_session_event(SrvId, SessId, undefined),
196 | nkservice_api_server:unregister_event(self(), Event),
197 | nkmedia_api_events:session_down(SrvId, SessId).
198 |
199 |
200 |
201 | %% ===================================================================
202 | %% Internal
203 | %% ===================================================================
204 |
205 | %% @private
206 | get_create_reply(SessId, Config) ->
207 | case maps:get(wait_reply, Config, false) of
208 | false ->
209 | {ok, #{session_id=>SessId}};
210 | true ->
211 | case Config of
212 | #{offer:=_, answer:=_} ->
213 | {ok, #{session_id=>SessId}};
214 | #{offer:=_} ->
215 | case nkmedia_session:get_answer(SessId) of
216 | {ok, Answer} ->
217 | {ok, #{session_id=>SessId, answer=>Answer}};
218 | {error, Error} ->
219 | {error, Error}
220 | end;
221 | _ ->
222 | case nkmedia_session:get_offer(SessId) of
223 | {ok, Offer} ->
224 | {ok, #{session_id=>SessId, offer=>Offer}};
225 | {error, Error} ->
226 | {error, Error}
227 | end
228 | end
229 | end.
230 |
231 |
232 | %% @private
233 | get_session_event(SrvId, SessId, Body) ->
234 | #event{
235 | srv_id = SrvId,
236 | class = <<"media">>,
237 | subclass = <<"session">>,
238 | type = <<"*">>,
239 | obj_id = SessId,
240 | body = Body
241 | }.
242 |
243 |
244 |
245 |
--------------------------------------------------------------------------------
/src/nkmedia_api_events.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc NkMEDIA external events processing
22 |
23 | -module(nkmedia_api_events).
24 | -author('Carlos Gonzalez ').
25 |
26 | -export([event/3, session_down/2]).
27 | -export([send_event/5, send_event/6]).
28 |
29 | -include_lib("nkservice/include/nkservice.hrl").
30 | -include_lib("nksip/include/nksip.hrl").
31 |
32 |
33 |
34 | %% ===================================================================
35 | %% Callbacks
36 | %% ===================================================================
37 |
38 | %% @private
39 | -spec event(nkmedia_session:id(), nkmedia_session:event(),
40 | nkmedia_session:session()) ->
41 | {ok, nkmedia_session:session()}.
42 |
43 | event(SessId, created, Session) ->
44 | Data = nkmedia_api_syntax:get_info(Session),
45 | do_send_event(SessId, created, Data, Session);
46 |
47 | event(SessId, {answer, Answer}, Session) ->
48 | do_send_event(SessId, answer, #{answer=>Answer}, Session);
49 |
50 | event(SessId, {type, Type, Ext}, Session) ->
51 | do_send_event(SessId, type, Ext#{type=>Type}, Session);
52 |
53 | event(SessId, {candidate, #candidate{last=true}}, Session) ->
54 | do_send_event(SessId, candidate_end, #{}, Session);
55 |
56 | event(SessId, {candidate, #candidate{a_line=Line, m_id=Id, m_index=Index}}, Session) ->
57 | Data = #{sdpMid=>Id, sdpMLineIndex=>Index, candidate=>Line},
58 | do_send_event(SessId, candidate, Data, Session);
59 |
60 | event(SessId, {status, Class, Data}, Session) ->
61 | do_send_event(SessId, status, Data#{class=>Class}, Session);
62 |
63 | event(SessId, {info, Info, Meta}, Session) ->
64 | do_send_event(SessId, info, Meta#{info=>Info}, Session);
65 |
66 | event(SessId, {destroyed, Reason}, #{srv_id:=SrvId}=Session) ->
67 | {Code, Txt} = nkservice_util:error_code(SrvId, Reason),
68 | do_send_event(SessId, destroyed, #{code=>Code, reason=>Txt}, Session);
69 |
70 | event(_SessId, _Event, Session) ->
71 | {ok, Session}.
72 |
73 |
74 |
75 | %% @private
76 | -spec session_down(nkservice:id(), nkmedia_session:id()) ->
77 | ok.
78 |
79 | session_down(SrvId, SessId) ->
80 | {Code, Txt} = nkservice_util:error_code(SrvId, process_down),
81 | send_event(SrvId, session, SessId, destroyed, #{code=>Code, reason=>Txt}).
82 |
83 |
84 |
85 | %% ===================================================================
86 | %% Internal
87 | %% ===================================================================
88 |
89 | %% @doc Sends an event
90 | -spec send_event(nkservice:id(), atom(), binary(), atom(), map()) ->
91 | ok.
92 |
93 | send_event(SrvId, Class, Id, Type, Body) ->
94 | send_event(SrvId, Class, Id, Type, Body, undefined).
95 |
96 |
97 | %% @doc Sends an event
98 | -spec send_event(nkservice:id(), atom(), binary(), atom(), map(), pid()) ->
99 | ok.
100 |
101 | send_event(SrvId, Class, Id, Type, Body, Pid) ->
102 | lager:info("MEDIA EVENT (~s:~s:~s): ~p", [Class, Type, Id, Body]),
103 | Event = #event{
104 | srv_id = SrvId,
105 | class = <<"media">>,
106 | subclass = nklib_util:to_binary(Class),
107 | type = nklib_util:to_binary(Type),
108 | obj_id = Id,
109 | body = Body,
110 | pid = Pid
111 | },
112 | nkservice_events:send(Event).
113 |
114 |
115 | %% @private
116 | do_send_event(SessId, Type, Body, #{srv_id:=SrvId}=Session) ->
117 | send_event(SrvId, session, SessId, Type, Body),
118 | {ok, Session}.
119 |
120 |
121 |
122 |
--------------------------------------------------------------------------------
/src/nkmedia_api_syntax.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc NkMEDIA external API
22 |
23 | -module(nkmedia_api_syntax).
24 | -author('Carlos Gonzalez ').
25 | -export([syntax/4, offer/0, answer/0, get_info/1]).
26 |
27 |
28 |
29 | %% ===================================================================
30 | %% Syntax
31 | %% ===================================================================
32 |
33 |
34 | %% @private
35 | syntax(<<"create">>, Syntax, Defaults, Mandatory) ->
36 | {
37 | Syntax#{
38 | type => atom, %% p2p, proxy...
39 | wait_reply => boolean,
40 |
41 | session_id => binary,
42 | offer => offer(),
43 | no_offer_trickle_ice => atom,
44 | no_answer_trickle_ice => atom,
45 | trickle_ice_timeout => {integer, 100, 30000},
46 | sdp_type => {enum, [webrtc, rtp]}, %% For generated SDP only
47 | backend => atom, %% nkmedia_janus, etc.
48 | master_id => binary,
49 | set_master_answer => boolean,
50 | stop_after_peer => boolean,
51 | subscribe => boolean,
52 | events_body => any,
53 | wait_timeout => {integer, 1, none},
54 | ready_timeout => {integer, 1, none},
55 |
56 | % Type-specific
57 | peer_id => binary,
58 | room_id => binary,
59 | create_room => boolean,
60 | publisher_id => binary,
61 | layout => binary,
62 | loops => {integer, 0, none},
63 | uri => binary,
64 | mute_audio => boolean,
65 | mute_video => boolean,
66 | mute_data => boolean,
67 | bitrate => integer
68 | },
69 | Defaults,
70 | [type|Mandatory]
71 | };
72 |
73 | syntax(<<"destroy">>, Syntax, Defaults, Mandatory) ->
74 | {
75 | Syntax#{session_id => binary},
76 | Defaults,
77 | [session_id|Mandatory]
78 | };
79 |
80 | syntax(<<"set_answer">>, Syntax, Defaults, Mandatory) ->
81 | {
82 | Syntax#{
83 | session_id => binary,
84 | answer => answer()
85 | },
86 | Defaults,
87 | [session_id, answer|Mandatory]
88 | };
89 |
90 | syntax(<<"get_offer">>, Syntax, Defaults, Mandatory) ->
91 | {
92 | Syntax#{session_id => binary},
93 | Defaults,
94 | [session_id|Mandatory]
95 | };
96 |
97 | syntax(<<"get_answer">>, Syntax, Defaults, Mandatory) ->
98 | {
99 | Syntax#{session_id => binary},
100 | Defaults,
101 | [session_id|Mandatory]
102 | };
103 |
104 | syntax(<<"update_media">>, Syntax, Defaults, Mandatory) ->
105 | {
106 | Syntax#{
107 | session_id => binary,
108 | mute_audio => boolean,
109 | mute_video => boolean,
110 | mute_data => boolean,
111 | bitrate => integer
112 | },
113 | Defaults,
114 | [session_id|Mandatory]
115 | };
116 |
117 | syntax(<<"set_type">>, Syntax, Defaults, Mandatory) ->
118 | {
119 | Syntax#{
120 | session_id => binary,
121 | type => atom,
122 |
123 | % Type specific
124 | room_id => binary,
125 | create_room => boolean,
126 | publisher_id => binary,
127 | uri => binary,
128 | layout => binary
129 | },
130 | Defaults,
131 | [session_id, type|Mandatory]
132 | };
133 |
134 | syntax(<<"recorder_action">>, Syntax, Defaults, Mandatory) ->
135 | {
136 | Syntax#{
137 | session_id => binary,
138 | action => atom,
139 | uri => binary
140 | },
141 | Defaults,
142 | [session_id|Mandatory]
143 | };
144 |
145 | syntax(<<"player_action">>, Syntax, Defaults, Mandatory) ->
146 | {
147 | Syntax#{
148 | session_id => binary,
149 | action => atom,
150 | uri => binary,
151 | loops => {integer, 0, none},
152 | position => integer
153 | },
154 | Defaults,
155 | [session_id|Mandatory]
156 | };
157 |
158 | syntax(<<"room_action">>, Syntax, Defaults, Mandatory) ->
159 | {
160 | Syntax#{
161 | session_id => binary,
162 | action => atom,
163 | layout => binary
164 | },
165 | Defaults,
166 | [session_id|Mandatory]
167 | };
168 |
169 | syntax(<<"set_candidate">>, Syntax, Defaults, Mandatory) ->
170 | {
171 | Syntax#{
172 | session_id => binary,
173 | sdpMid => binary,
174 | sdpMLineIndex => integer,
175 | candidate => binary
176 | },
177 | Defaults#{sdpMid=><<>>},
178 | [session_id, sdpMLineIndex, candidate|Mandatory]
179 | };
180 |
181 | syntax(<<"set_candidate_end">>, Syntax, Defaults, Mandatory) ->
182 | {
183 | Syntax#{
184 | session_id => binary
185 | },
186 | Defaults,
187 | [session_id|Mandatory]
188 | };
189 |
190 | syntax(<<"get_info">>, Syntax, Defaults, Mandatory) ->
191 | {
192 | Syntax#{session_id => binary},
193 | Defaults,
194 | [session_id|Mandatory]
195 | };
196 |
197 | syntax(<<"get_list">>, Syntax, Defaults, Mandatory) ->
198 | {
199 | Syntax,
200 | Defaults,
201 | Mandatory
202 | };
203 |
204 |
205 | syntax(_Cmd, Syntax, Defaults, Mandatory) ->
206 | {Syntax, Defaults, Mandatory}.
207 |
208 |
209 | %% @private
210 | offer() ->
211 | #{
212 | sdp => binary,
213 | sdp_type => {enum, [rtp, webrtc]},
214 | trickle_ice => boolean
215 | }.
216 |
217 |
218 | %% @private
219 | answer() ->
220 | #{
221 | sdp => binary,
222 | sdp_type => {enum, [rtp, webrtc]},
223 | trickle_ice => boolean
224 | }.
225 |
226 |
227 |
228 | %% ===================================================================
229 | %% Get info
230 | %% ===================================================================
231 |
232 |
233 | %% @private
234 | get_info(Session) ->
235 | Keys = [
236 | session_id,
237 | offer,
238 | answer,
239 | no_offer_trickle_ice,
240 | no_answer_trickle_ice,
241 | trickle_ice_timeout,
242 | sdp_type,
243 | backend,
244 | master_id,
245 | slave_id,
246 | set_master_answer,
247 | stop_after_peer,
248 | wait_timeout,
249 | ready_timeout,
250 | user_id,
251 | user_session,
252 | backend_role,
253 | type,
254 | type_ext,
255 | status,
256 |
257 | peer_id,
258 | room_id,
259 | create_room,
260 | publisher_id,
261 | layout,
262 | loops,
263 | uri,
264 | mute_audio,
265 | mute_video,
266 | mute_data,
267 | bitrate
268 | ],
269 | maps:with(Keys, Session).
270 |
271 |
272 |
273 |
--------------------------------------------------------------------------------
/src/nkmedia_app.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc NkMEDIA OTP Application Module
22 | -module(nkmedia_app).
23 | -author('Carlos Gonzalez ').
24 | -behaviour(application).
25 |
26 | -export([start/0, start/2, stop/1]).
27 | -export([get/1, get/2, put/2, del/1]).
28 | -export([get_env/1, get_env/2, set_env/2]).
29 |
30 | -include("nkmedia.hrl").
31 | -include_lib("nklib/include/nklib.hrl").
32 |
33 | -define(APP, nkmedia).
34 |
35 | %% ===================================================================
36 | %% Private
37 | %% ===================================================================
38 |
39 | %% @doc Starts stand alone.
40 | -spec start() ->
41 | ok | {error, Reason::term()}.
42 |
43 | start() ->
44 | case nklib_util:ensure_all_started(?APP, permanent) of
45 | {ok, _Started} ->
46 | ok;
47 | Error ->
48 | Error
49 | end.
50 |
51 |
52 | %% @private OTP standard start callback
53 | start(_Type, _Args) ->
54 | Syntax = #{
55 | admin_url => binary,
56 | admin_pass => binary,
57 | sip_port => integer,
58 | no_docker => boolean,
59 | log_dir => fullpath,
60 | record_dir => fullpath,
61 | docker_log => any,
62 | default_bitrate => {integer, 0, none}
63 | },
64 | Defaults = #{
65 | admin_url => "wss://all:9010",
66 | admin_pass => "nkmedia",
67 | sip_port => 0,
68 | no_docker => false,
69 | log_dir => "./log",
70 | record_dir => "./record",
71 | default_bitrate => 100000
72 | },
73 | case nklib_config:load_env(?APP, Syntax, Defaults) of
74 | {ok, _} ->
75 | ensure_dirs(),
76 | {ok, Vsn} = application:get_key(?APP, vsn),
77 | lager:info("NkMEDIA v~s is starting", [Vsn]),
78 | MainIp = nkpacket_config_cache:main_ip(),
79 | nkmedia_app:put(main_ip, MainIp),
80 | % Erlang IP is used for the media servers to contact to the
81 | % management server
82 | case nkmedia_app:get(no_docker) of
83 | false ->
84 | {ok, #{ip:=DockerIp}} =nkdocker_util:get_conn_info(),
85 | case DockerIp of
86 | {127,0,0,1} ->
87 | nkmedia_app:put(erlang_ip, {127,0,0,1}),
88 | nkmedia_app:put(docker_ip, {127,0,0,1});
89 | _ ->
90 | lager:notice("NkMEDIA: remote docker mode enabled"),
91 | lager:notice("Erlang: ~s, Docker: ~s",
92 | [nklib_util:to_host(MainIp),
93 | nklib_util:to_host(DockerIp)]),
94 | nkmedia_app:put(erlang_ip, MainIp),
95 | nkmedia_app:put(docker_ip, DockerIp)
96 | end;
97 | true ->
98 | lager:warning("No docker support in config")
99 | end,
100 | {ok, Pid} = nkmedia_sup:start_link(),
101 | nkmedia_core:start(),
102 | {ok, Pid};
103 | {error, Error} ->
104 | lager:error("Error parsing config: ~p", [Error]),
105 | error(Error)
106 | end.
107 |
108 |
109 | %% @private OTP standard stop callback
110 | stop(_) ->
111 | ok.
112 |
113 |
114 | %% Configuration access
115 | get(Key) ->
116 | nklib_config:get(?APP, Key).
117 |
118 | get(Key, Default) ->
119 | nklib_config:get(?APP, Key, Default).
120 |
121 | put(Key, Val) ->
122 | nklib_config:put(?APP, Key, Val).
123 |
124 | del(Key) ->
125 | nklib_config:del(?APP, Key).
126 |
127 |
128 |
129 | %% @private
130 | get_env(Key) ->
131 | get_env(Key, undefined).
132 |
133 |
134 | %% @private
135 | get_env(Key, Default) ->
136 | case application:get_env(?APP, Key) of
137 | undefined -> Default;
138 | {ok, Value} -> Value
139 | end.
140 |
141 |
142 | %% @private
143 | set_env(Key, Value) ->
144 | application:set_env(?APP, Key, Value).
145 |
146 |
147 | %% @private
148 | ensure_dirs() ->
149 | Log = nkmedia_app:get(log_dir),
150 | filelib:ensure_dir(filename:join(Log, <<"foo">>)),
151 | Record = nkmedia_app:get(record_dir),
152 | filelib:ensure_dir(filename:join([Record, <<"tmp">>, <<"foo">>])).
153 |
154 | % %% @private
155 | % save_log_dir() ->
156 | % DirPath1 = nklib_parse:fullpath(filename:absname(DirPath)),
157 |
158 |
159 |
160 | % Dir = filename:absname(filename:join(code:priv_dir(?APP), "../log")),
161 | % Path = nklib_parse:fullpath(Dir),
162 | % nkmedia_app:put(log_dir, Path).
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
--------------------------------------------------------------------------------
/src/nkmedia_core.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc NkMEDIA Core service
22 |
23 | -module(nkmedia_core).
24 | -author('Carlos Gonzalez ').
25 |
26 | -export([start/0, stop/0, register_session/2, invite/2]).
27 | -export([plugin_listen/2, plugin_start/2, plugin_stop/2]).
28 | -export([sip_route/5]).
29 | -export([sip_invite/2, sip_reinvite/2, sip_bye/2, sip_register/2]).
30 |
31 | -define(WS_TIMEOUT, 5*60*1000).
32 |
33 | %% ===================================================================
34 | %% Types
35 | %% ===================================================================
36 |
37 |
38 |
39 | %% ===================================================================
40 | %% Public functions
41 | %% ===================================================================
42 |
43 |
44 | %% @private
45 | %% Starts a nkservice called 'nkmedia_core'
46 | start() ->
47 | %% Avoid openning 5060 but open a random port
48 | Dummy = case gen_udp:open(5060, [{ip, {0,0,0,0}}]) of
49 | {ok, Socket} -> Socket;
50 | {error, _} -> false
51 | end,
52 | Opts = #{
53 | class => nkmedia_core,
54 | plugins => [?MODULE, nksip, nksip_uac_auto_auth, nksip_registrar],
55 | nksip_trace => {console, all}, % Add nksip_trace
56 | sip_listen => <<"sip:all">>
57 | },
58 | {ok, SrvId} = nkservice:start(nkmedia_core, Opts),
59 | case Dummy of
60 | false -> ok;
61 | _ -> gen_udp:close(Dummy)
62 | end,
63 | [Listener] = nkservice:get_listeners(SrvId, nksip),
64 | {ok, {nksip_protocol, udp, _Ip, Port2}} = nkpacket:get_local(Listener),
65 | nkmedia_app:put(sip_port, Port2).
66 |
67 |
68 | %% @private
69 | stop() ->
70 | nkservice:stop(nkmedia_core).
71 |
72 |
73 | %% @private
74 | register_session(SessId, Module) ->
75 | nklib_proc:put({?MODULE, session, SessId}, Module).
76 |
77 |
78 | %% @private
79 | invite(Contact, #{sdp:=SDP}) ->
80 | SDP2 = nksip_sdp:parse(SDP),
81 | Opts = [{body, SDP2}, auto_2xx_ack, {meta, [body]}],
82 | nksip_uac:invite(nkmedia_core, Contact, Opts).
83 |
84 |
85 |
86 |
87 | %% ===================================================================
88 | %% Plugin functions
89 | %% ===================================================================
90 |
91 |
92 |
93 | % plugin_syntax() ->
94 | % nkpacket:register_protocol(nkmedia, nkmedia_protocol_server),
95 | % #{
96 | % admin_url => fun parse_listen/3,
97 | % admin_pass => binary
98 | % }.
99 |
100 |
101 | plugin_listen(Config, _Service) ->
102 | Listen = maps:get(admin_url, Config, []),
103 | Opts = #{
104 | class => nkmedia_admin,
105 | idle_timeout => ?WS_TIMEOUT
106 | },
107 | [{Conns, maps:merge(ConnOpts, Opts)} || {Conns, ConnOpts} <- Listen].
108 |
109 |
110 | plugin_start(Config, _Service) ->
111 | lager:info("NkMEDIA Core Service starting"),
112 | {ok, Config}.
113 |
114 |
115 | plugin_stop(Config, _Service) ->
116 | lager:info("NkMEDIA Core Service stopping"),
117 | {ok, Config}.
118 |
119 |
120 |
121 |
122 | %% ===================================================================
123 | %% SIP callbacks
124 | %% ===================================================================
125 |
126 |
127 | sip_route(_Scheme, _User, _Domain, _Req, _Call) ->
128 | process.
129 |
130 |
131 | sip_invite(Req, _Call) ->
132 | apply_mod(Req, nkmedia_sip_invite).
133 |
134 |
135 | sip_reinvite(Req, _Call) ->
136 | apply_mod(Req, nkmedia_sip_reinvite).
137 |
138 |
139 | sip_bye(Req, _Call) ->
140 | apply_mod(Req, nkmedia_sip_bye).
141 |
142 |
143 | sip_register(Req, _Call) ->
144 | {ok, Domain} = nksip_request:meta(from_domain, Req),
145 | case catch binary_to_existing_atom(Domain, latin1) of
146 | {'EXIT', _} ->
147 | {reply, forbidden};
148 | Module ->
149 | {ok, User} = nksip_request:meta(from_user, Req),
150 | Module:nkmedia_sip_register(User, Req)
151 | end.
152 |
153 |
154 |
155 |
156 |
157 | %% ===================================================================
158 | %% Internal
159 | %% ===================================================================
160 |
161 |
162 |
163 |
164 | apply_mod(Req, Fun) ->
165 | case nksip_request:meta(aor, Req) of
166 | {ok, {sip, User, _}} ->
167 | case binary:split(User, <<"-">>) of
168 | [Head, Id] ->
169 | case catch binary_to_existing_atom(Head, latin1) of
170 | {'EXIT', _} ->
171 | {reply, forbidden};
172 | Mod ->
173 | case erlang:function_exported(Mod, Fun, 2) of
174 | true ->
175 | apply(Mod, Fun, [Id, Req]);
176 | false ->
177 | {reply, forbidden}
178 | end
179 | end;
180 | _ ->
181 | {reply, forbidden}
182 | end;
183 | _ ->
184 | lager:error("APPL2"),
185 | {reply, forbidden}
186 | end.
187 |
188 |
189 |
190 |
--------------------------------------------------------------------------------
/src/nkmedia_sup.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @private NkMEDIA main supervisor
22 | -module(nkmedia_sup).
23 | -author('Carlos Gonzalez ').
24 | -behaviour(supervisor).
25 |
26 | -export([start_child/2, start_janus_engine/1]).
27 | -export([init/1, start_link/0]).
28 |
29 | -include("nkmedia.hrl").
30 |
31 |
32 | %% @private
33 | start_child(Module, #{name:=Name}=Config) ->
34 | ChildId = {Module, Name},
35 | Spec = {
36 | ChildId,
37 | {Module, start_link, [Config]},
38 | transient,
39 | 5000,
40 | worker,
41 | [Module]
42 | },
43 | case supervisor:start_child(?MODULE, Spec) of
44 | {ok, Pid} ->
45 | {ok, Pid};
46 | {error, already_present} ->
47 | ok = supervisor:delete_child(?MODULE, ChildId),
48 | start_child(Module, Config);
49 | {error, {already_started, Pid}} ->
50 | {ok, Pid};
51 | {error, Error} ->
52 | {error, Error}
53 | end.
54 |
55 |
56 |
57 |
58 | start_janus_engine(#{name:=Name}=Config) ->
59 | ChildId = {nkmedia_janus_engine, Name},
60 | Spec = {
61 | ChildId,
62 | {nkmedia_janus_engine, start_link, [Config]},
63 | transient,
64 | 5000,
65 | worker,
66 | [nkmedia_janus_engine]
67 | },
68 | case supervisor:start_child(?MODULE, Spec) of
69 | {ok, Pid} ->
70 | {ok, Pid};
71 | {error, already_present} ->
72 | ok = supervisor:delete_child(?MODULE, ChildId),
73 | start_janus_engine(Config);
74 | {error, {already_started, Pid}} ->
75 | {ok, Pid};
76 | {error, Error} ->
77 | {error, Error}
78 | end.
79 |
80 |
81 |
82 | % stop_fs(#{index:=Index}) ->
83 | % ChildId = {nkmedia_fs_server, Index},
84 | % case supervisor:terminate_child(?MODULE, ChildId) of
85 | % ok -> ok = supervisor:delete_child(?MODULE, ChildId);
86 | % {error, Error} -> {error, Error}
87 | % end.
88 |
89 |
90 | %% @private
91 | -spec start_link() ->
92 | {ok, pid()}.
93 |
94 | start_link() ->
95 | Childs = case nkmedia_app:get(no_docker) of
96 | false ->
97 | [
98 | % {
99 | % docker,
100 | % {nkmedia_docker, start_link, []},
101 | % permanent,
102 | % 5000,
103 | % worker,
104 | % [nkmedia_docker]
105 | % }
106 | ];
107 | true ->
108 | []
109 | end,
110 | supervisor:start_link({local, ?MODULE}, ?MODULE, {{one_for_one, 10, 60}, Childs}).
111 |
112 |
113 | %% @private
114 | init(ChildSpecs) ->
115 | {ok, ChildSpecs}.
116 |
117 |
118 |
--------------------------------------------------------------------------------
/src/nkmedia_util.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | -module(nkmedia_util).
22 | -author('Carlos Gonzalez ').
23 |
24 | -export([get_q850/1, add_id/2, add_id/3, filter_codec/3]).
25 | -export([mangle_sdp_ip/1]).
26 | % -export([kill/1]).
27 | -export([remove_sdp_data_channel/1]).
28 | -export([add_certs/1]).
29 | -export_type([stop_reason/0, q850/0]).
30 |
31 | -type stop_reason() :: atom() | q850() | binary() | string().
32 | -type q850() :: 0..609.
33 |
34 | % -type notify() ::
35 | % {Tag::term(), pid()} | {Tag::term(), Info::term(), pid()} | term().
36 |
37 | % -type notify_refs() :: [{notify(), reference()|undefined}].
38 |
39 | -include_lib("nkservice/include/nkservice.hrl").
40 | -include_lib("nksip/include/nksip.hrl").
41 |
42 |
43 | %% @private
44 | add_id(Key, Config) ->
45 | add_id(Key, Config, <<>>).
46 |
47 |
48 | %% @private
49 | add_id(Key, Config, Prefix) ->
50 | case maps:find(Key, Config) of
51 | {ok, Id} when is_binary(Id) ->
52 | {Id, Config};
53 | {ok, Id} ->
54 | Id2 = nklib_util:to_binary(Id),
55 | {Id2, maps:put(Key, Id2, Config)};
56 | _ when Prefix == <<>> ->
57 | Id = nklib_util:uuid_4122(),
58 | {Id, maps:put(Key, Id, Config)};
59 | _ ->
60 | Id1 = nklib_util:uuid_4122(),
61 | Id2 = <<(nklib_util:to_binary(Prefix))/binary, $-, Id1/binary>>,
62 | {Id2, maps:put(Key, Id2, Config)}
63 | end.
64 |
65 |
66 |
67 |
68 | %% @doc Allow only one codec family, removing all other
69 | -spec filter_codec(audio|video, atom()|string()|binary(),
70 | nkmedia:offer()|nkmedia:answer()) ->
71 | nkmedia:offer()|nkmedia:answer().
72 |
73 | filter_codec(Media, Codec, #{sdp:=SDP1}=OffAns) ->
74 | {CodecMap, SDP2} = nksip_sdp_util:extract_codec_map(SDP1),
75 | CodecMap2 = nksip_sdp_util:filter_codec(Media, Codec, CodecMap),
76 | SDP3 = nksip_sdp_util:insert_codec_map(CodecMap2, SDP2),
77 | OffAns#{sdp:=nksip_sdp:unparse(SDP3)}.
78 |
79 |
80 |
81 | %% @doc
82 | mangle_sdp_ip(#{sdp:=SDP}=Map) ->
83 | MainIp = nklib_util:to_host(nkpacket_config_cache:main_ip()),
84 | ExtIp = nklib_util:to_host(nkpacket_config_cache:ext_ip()),
85 | case re:replace(SDP, MainIp, ExtIp, [{return, binary}, global]) of
86 | SDP ->
87 | lager:warning("no SIP mangle, ~s not found!", [MainIp]),
88 | Map;
89 | SDP2 ->
90 | lager:warning("done SIP mangle ~s -> ~s", [MainIp, ExtIp]),
91 | Map#{sdp:=SDP2}
92 | end;
93 |
94 | mangle_sdp_ip(Map) ->
95 | Map.
96 |
97 |
98 |
99 | %% @private
100 | -spec get_q850(q850()) ->
101 | {q850(), binary()}.
102 |
103 | get_q850(Code) when is_integer(Code) ->
104 | case maps:find(Code, q850_map()) of
105 | {ok, {_Sip, Msg}} ->
106 | {999, <<"(", (nklib_util:to_binary(Code))/binary, ") ", Msg/binary>>};
107 | error ->
108 | not_found
109 | end.
110 |
111 |
112 |
113 | %% @private
114 | q850_map() ->
115 | #{
116 | 0 => {none, <<"UNSPECIFIED">>},
117 | 1 => {404, <<"UNALLOCATED_NUMBER">>},
118 | 2 => {404, <<"NO_ROUTE_TRANSIT_NET">>},
119 | 3 => {404, <<"NO_ROUTE_DESTINATION">>},
120 | 6 => {none, <<"CHANNEL_UNACCEPTABLE">>},
121 | 7 => {none, <<"CALL_AWARDED_DELIVERED">>},
122 | 16 => {none, <<"NORMAL_CLEARING">>},
123 | 17 => {486, <<"USER_BUSY">>},
124 | 18 => {408, <<"NO_USER_RESPONSE">>},
125 | 19 => {480, <<"NO_ANSWER">>},
126 | 20 => {480, <<"SUBSCRIBER_ABSENT">>},
127 | 21 => {603, <<"CALL_REJECTED">>},
128 | 22 => {410, <<"NUMBER_CHANGED">>},
129 | 23 => {410, <<"REDIRECTION_TO_NEW_DESTINATION">>},
130 | 25 => {483, <<"EXCHANGE_ROUTING_ERROR">>},
131 | 27 => {502, <<"DESTINATION_OUT_OF_ORDER">>},
132 | 28 => {484, <<"INVALID_NUMBER_FORMAT">>},
133 | 29 => {501, <<"FACILITY_REJECTED">>},
134 | 30 => {none, <<"RESPONSE_TO_STATUS_ENQUIRY">>},
135 | 31 => {480, <<"NORMAL_UNSPECIFIE">>},
136 | 34 => {503, <<"NORMAL_CIRCUIT_CONGESTION">>},
137 | 38 => {503, <<"NETWORK_OUT_OF_ORDER">>},
138 | 41 => {503, <<"NORMAL_TEMPORARY_FAILURE">>},
139 | 42 => {503, <<"SWITCH_CONGESTION">>},
140 | 43 => {none, <<"ACCESS_INFO_DISCARDED">>},
141 | 44 => {503, <<"REQUESTED_CHAN_UNAVAIL">>},
142 | 45 => {none, <<"PRE_EMPTED">>},
143 | 50 => {none, <<"FACILITY_NOT_SUBSCRIBED">>},
144 | 52 => {403, <<"OUTGOING_CALL_BARRED">>},
145 | 54 => {403, <<"INCOMING_CALL_BARRED">>},
146 | 57 => {403, <<"BEARERCAPABILITY_NOTAUTH">>},
147 | 58 => {503, <<"BEARERCAPABILITY_NOTAVAIL">>},
148 | 63 => {none, <<"SERVICE_UNAVAILABLE">>},
149 | 65 => {488, <<"BEARERCAPABILITY_NOTIMPL">>},
150 | 66 => {none, <<"CHAN_NOT_IMPLEMENTED">>},
151 | 69 => {501, <<"FACILITY_NOT_IMPLEMENTED">>},
152 | 79 => {501, <<"SERVICE_NOT_IMPLEMENTED">>},
153 | 81 => {none, <<"INVALID_CALL_REFERENCE">>},
154 | 88 => {488, <<"INCOMPATIBLE_DESTINATION">>},
155 | 95 => {none, <<"INVALID_MSG_UNSPECIFIED">>},
156 | 96 => {none, <<"MANDATORY_IE_MISSING">>},
157 | 97 => {none, <<"MESSAGE_TYPE_NONEXIST">>},
158 | 98 => {none, <<"WRONG_MESSAGE">>},
159 | 99 => {none, <<"IE_NONEXIST">>},
160 | 100 => {none, <<"INVALID_IE_CONTENTS">>},
161 | 101 => {none, <<"WRONG_CALL_STATE">>},
162 | 102 => {504, <<"RECOVERY_ON_TIMER_EXPIRE">>},
163 | 103 => {none, <<"MANDATORY_IE_LENGTH_ERROR">>},
164 | 111 => {none, <<"PROTOCOL_ERROR">>},
165 | 127 => {none, <<"INTERWORKING">>},
166 | 487 => {487, <<"ORIGINATOR_CANCEL">>},
167 | 500 => {none, <<"CRASH">>},
168 | 501 => {none, <<"SYSTEM_SHUTDOWN">>},
169 | 502 => {none, <<"LOSE_RACE">>},
170 | 503 => {none, <<"MANAGER_REQUEST">>},
171 | 600 => {none, <<"BLIND_TRANSFER">>},
172 | 601 => {none, <<"ATTENDED_TRANSFER">>},
173 | 602 => {none, <<"ALLOTTED_TIMEOUT">>},
174 | 603 => {none, <<"USER_CHALLENGE">>},
175 | 604 => {none, <<"MEDIA_TIMEOUT">>},
176 | 605 => {none, <<"PICKED_OFF">>},
177 | 606 => {none, <<"USER_NOT_REGISTERED">>},
178 | 607 => {none, <<"PROGRESS_TIMEOUT">>},
179 | 609 => {none, <<"GATEWAY_DOWN">>}
180 | }.
181 |
182 |
183 |
184 |
185 | % kill(Type) ->
186 | % Pids = case Type of
187 | % in -> [Pid || {_, inbound, Pid} <- nkmedia_session:get_all()];
188 | % out -> [Pid || {_, outbound, Pid} <- nkmedia_session:get_all()];
189 | % calls -> [Pid || {_, _, Pid} <- nkcollab_call:get_all()]
190 | % end,
191 | % lists:foreach(fun(Pid) -> exit(Pid, kill) end, Pids).
192 |
193 |
194 |
195 | %% @private Removes the datachannel (m=application)
196 | remove_sdp_data_channel(SDP) ->
197 | #sdp{medias=Medias} = SDP2 = nksip_sdp:parse(SDP),
198 | Medias2 = [Media || #sdp_m{media=Name}=Media <- Medias, Name /= <<"application">>],
199 | SDP3 = SDP2#sdp{medias=Medias2},
200 | nksip_sdp:unparse(SDP3).
201 |
202 |
203 |
204 | add_certs(Spec) ->
205 | Dir = "./priv/certs",
206 | case file:read_file(filename:join(Dir, "cert.pem")) of
207 | {ok, _} ->
208 | Spec#{
209 | tls_certfile => filename:join(Dir, "cert.pem"),
210 | tls_keyfile => filename:join(Dir, "privkey.pem"),
211 | tls_cacertfile => filename:join(Dir, "fullchain.pem")
212 | };
213 | _ ->
214 | Spec
215 | end.
--------------------------------------------------------------------------------
/src/plugins/nkmedia_room_api.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc Room Plugin API
22 | -module(nkmedia_room_api).
23 | -author('Carlos Gonzalez ').
24 |
25 | -export([cmd/3]).
26 |
27 | -include_lib("nkservice/include/nkservice.hrl").
28 |
29 |
30 | %% ===================================================================
31 | %% Commands
32 | %% ===================================================================
33 |
34 |
35 | cmd(<<"create">>, #api_req{srv_id=SrvId, data=Data}, State) ->
36 | case nkmedia_room:start(SrvId, Data) of
37 | {ok, Id, _Pid} ->
38 | {ok, #{room_id=>Id}, State};
39 | {error, Error} ->
40 | {error, Error, State}
41 | end;
42 |
43 | cmd(<<"destroy">>, #api_req{data=#{room_id:=Id}}, State) ->
44 | case nkmedia_room:stop(Id, api_stop) of
45 | ok ->
46 | {ok, #{}, State};
47 | {error, Error} ->
48 | {error, Error, State}
49 | end;
50 |
51 | cmd(<<"get_list">>, _Req, State) ->
52 | Ids = [#{room_id=>Id, class=>Class} || {Id, Class, _Pid} <- nkmedia_room:get_all()],
53 | {ok, Ids, State};
54 |
55 | cmd(<<"get_info">>, #api_req{data=#{room_id:=RoomId}}, State) ->
56 | case nkmedia_room:get_room(RoomId) of
57 | {ok, Room} ->
58 | {ok, nkmedia_room_api_syntax:get_info(Room), State};
59 | {error, Error} ->
60 | {error, Error, State}
61 | end;
62 |
63 | cmd(_Cmd, _Data, _State) ->
64 | continue.
65 |
66 |
--------------------------------------------------------------------------------
/src/plugins/nkmedia_room_api_events.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc Room Plugin API
22 | -module(nkmedia_room_api_events).
23 | -author('Carlos Gonzalez ').
24 |
25 | -export([event/3]).
26 |
27 | % -include_lib("nkservice/include/nkservice.hrl").
28 |
29 |
30 | %% ===================================================================
31 | %% Events
32 | %% ===================================================================
33 |
34 |
35 | %% @private
36 | -spec event(nkmedia_room:id(), nkmedia_room:event(), nkmedia_room:room()) ->
37 | {ok, nkmedia_room:room()}.
38 |
39 | event(RoomId, created, Room) ->
40 | Data = nkmedia_room_api_syntax:get_info(Room),
41 | send_event(RoomId, created, Data, Room);
42 |
43 | event(RoomId, {destroyed, Reason}, #{srv_id:=SrvId}=Room) ->
44 | {Code, Txt} = nkservice_util:error_code(SrvId, Reason),
45 | send_event(RoomId, destroyed, #{code=>Code, reason=>Txt}, Room);
46 |
47 | event(RoomId, {started_member, SessId, Info}, Room) ->
48 | send_event(RoomId, started_member, Info#{session_id=>SessId}, Room);
49 |
50 | event(RoomId, {stopped_member, SessId, Info}, Room) ->
51 | send_event(RoomId, stopped_member, Info#{session_id=>SessId}, Room);
52 |
53 | event(SessId, {status, Class, Data}, Session) ->
54 | send_event(SessId, status, Data#{class=>Class}, Session);
55 |
56 | event(RoomId, {info, Info, Meta}, Room) ->
57 | send_event(RoomId, info, Meta#{info=>Info}, Room);
58 |
59 | event(_RoomId, _Event, Room) ->
60 | {ok, Room}.
61 |
62 |
63 | %% @private
64 | send_event(RoomId, Type, Body, #{srv_id:=SrvId}=Room) ->
65 | nkmedia_api_events:send_event(SrvId, room, RoomId, Type, Body),
66 | {ok, Room}.
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/src/plugins/nkmedia_room_api_syntax.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc Room Plugin API Syntax
22 | -module(nkmedia_room_api_syntax).
23 | -author('Carlos Gonzalez ').
24 |
25 | -export([syntax/4, get_info/1]).
26 |
27 | % -include_lib("nkservice/include/nkservice.hrl").
28 |
29 |
30 | %% ===================================================================
31 | %% Syntax
32 | %% ===================================================================
33 |
34 | syntax(<<"create">>, Syntax, Defaults, Mandatory) ->
35 | {
36 | Syntax#{
37 | class => atom,
38 | room_id => binary,
39 | backend => atom,
40 | timeout => {integer, 5, 3*24*60*60},
41 | bitrate => {integer, 0, none},
42 | audio_codec => {enum, [opus, isac32, isac16, pcmu, pcma]},
43 | video_codec => {enum , [vp8, vp9, h264]}
44 | },
45 | Defaults,
46 | [class|Mandatory]
47 | };
48 |
49 | syntax(<<"destroy">>, Syntax, Defaults, Mandatory) ->
50 | {
51 | Syntax#{room_id => binary},
52 | Defaults,
53 | [room_id|Mandatory]
54 | };
55 |
56 | syntax(<<"get_list">>, Syntax, Defaults, Mandatory) ->
57 | {
58 | Syntax#{service => fun nkservice_api:parse_service/1},
59 | Defaults,
60 | Mandatory
61 | };
62 |
63 | syntax(<<"get_info">>, Syntax, Defaults, Mandatory) ->
64 | {
65 | Syntax#{room_id => binary},
66 | Defaults,
67 | [room_id|Mandatory]
68 | };
69 |
70 | syntax(_Cmd, Syntax, Defaults, Mandatory) ->
71 | {Syntax, Defaults, Mandatory}.
72 |
73 |
74 |
75 | %% ===================================================================
76 | %% Keys
77 | %% ===================================================================
78 |
79 |
80 | get_info(Room) ->
81 | Keys = [audio_codec, video_codec, bitrate, class, backend, members, status],
82 | maps:with(Keys, Room).
83 |
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/src/plugins/nkmedia_room_callbacks.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc Room Plugin Callbacks
22 | -module(nkmedia_room_callbacks).
23 | -author('Carlos Gonzalez ').
24 |
25 | -export([plugin_deps/0, plugin_start/2, plugin_stop/2]).
26 | -export([nkmedia_room_init/2, nkmedia_room_terminate/2,
27 | nkmedia_room_event/3, nkmedia_room_reg_event/4, nkmedia_room_reg_down/4,
28 | nkmedia_room_timeout/2,
29 | nkmedia_room_handle_call/3, nkmedia_room_handle_cast/2,
30 | nkmedia_room_handle_info/2]).
31 | -export([error_code/1]).
32 | -export([api_cmd/2, api_syntax/4]).
33 |
34 |
35 | -include("../../include/nkmedia.hrl").
36 | -include_lib("nkservice/include/nkservice.hrl").
37 |
38 |
39 | -type continue() :: continue | {continue, list()}.
40 |
41 |
42 |
43 |
44 | %% ===================================================================
45 | %% Plugin callbacks
46 | %% ===================================================================
47 |
48 |
49 | plugin_deps() ->
50 | [nkmedia].
51 |
52 |
53 | plugin_start(Config, #{name:=Name}) ->
54 | lager:info("Plugin NkMEDIA ROOM (~s) starting", [Name]),
55 | {ok, Config}.
56 |
57 |
58 | plugin_stop(Config, #{name:=Name}) ->
59 | lager:info("Plugin NkMEDIA ROOM (~p) stopping", [Name]),
60 | {ok, Config}.
61 |
62 |
63 |
64 | %% ===================================================================
65 | %% Error Codes
66 | %% ===================================================================
67 |
68 | %% @doc See nkservice_callbacks
69 | -spec error_code(term()) ->
70 | {integer(), binary()} | continue.
71 |
72 | error_code(room_not_found) -> {304001, "Room not found"};
73 | error_code(room_already_exists) -> {304002, "Room already exists"};
74 | error_code(room_destroyed) -> {304003, "Room destroyed"};
75 | error_code(no_room_members) -> {304004, "No remaining room members"};
76 | error_code(invalid_publisher) -> {304005, "Invalid publisher"};
77 | error_code(publisher_stop) -> {304006, "Publisher stopped"};
78 |
79 | error_code(_) -> continue.
80 |
81 |
82 |
83 | %% ===================================================================
84 | %% Room Callbacks - Generated from nkmedia_room
85 | %% ===================================================================
86 |
87 | -type room_id() :: nkmedia_room:id().
88 | -type room() :: nkmedia_room:room().
89 |
90 |
91 |
92 | %% @doc Called when a new room starts
93 | -spec nkmedia_room_init(room_id(), room()) ->
94 | {ok, room()} | {error, term()}.
95 |
96 | nkmedia_room_init(_RoomId, Room) ->
97 | {ok, Room}.
98 |
99 |
100 | %% @doc Called when the room stops
101 | -spec nkmedia_room_terminate(Reason::term(), room()) ->
102 | {ok, room()}.
103 |
104 | nkmedia_room_terminate(_Reason, Room) ->
105 | {ok, Room}.
106 |
107 |
108 | %% @doc Called when the status of the room changes
109 | -spec nkmedia_room_event(room_id(), nkmedia_room:event(), room()) ->
110 | {ok, room()} | continue().
111 |
112 | nkmedia_room_event(RoomId, Event, Room) ->
113 | nkmedia_room_api_events:event(RoomId, Event, Room).
114 |
115 |
116 | %% @doc Called when the status of the room changes, for each registered
117 | %% process to the room
118 | -spec nkmedia_room_reg_event(room_id(), nklib:link(), nkmedia_room:event(), room()) ->
119 | {ok, room()} | continue().
120 |
121 | nkmedia_room_reg_event(_RoomId, _Link, _Event, Room) ->
122 | {ok, Room}.
123 |
124 |
125 | %% @doc Called when a registered process fails
126 | -spec nkmedia_room_reg_down(room_id(), nklib:link(), term(), room()) ->
127 | {ok, room()} | {stop, Reason::term(), room()} | continue().
128 |
129 | nkmedia_room_reg_down(_RoomId, _Link, _Reason, Room) ->
130 | {stop, registered_down, Room}.
131 |
132 |
133 | %% @doc Called when the timeout timer fires
134 | -spec nkmedia_room_timeout(room_id(), room()) ->
135 | {ok, room()} | {stop, nkservice:error(), room()} | continue().
136 |
137 | nkmedia_room_timeout(_RoomId, Room) ->
138 | {stop, timeout, Room}.
139 |
140 |
141 | %% @doc
142 | -spec nkmedia_room_handle_call(term(), {pid(), term()}, room()) ->
143 | {reply, term(), room()} | {noreply, room()} | continue().
144 |
145 | nkmedia_room_handle_call(Msg, _From, Room) ->
146 | lager:error("Module nkmedia_room received unexpected call: ~p", [Msg]),
147 | {noreply, Room}.
148 |
149 |
150 | %% @doc
151 | -spec nkmedia_room_handle_cast(term(), room()) ->
152 | {noreply, room()} | continue().
153 |
154 | nkmedia_room_handle_cast(Msg, Room) ->
155 | lager:error("Module nkmedia_room received unexpected cast: ~p", [Msg]),
156 | {noreply, Room}.
157 |
158 |
159 | %% @doc
160 | -spec nkmedia_room_handle_info(term(), room()) ->
161 | {noreply, room()} | continue().
162 |
163 | nkmedia_room_handle_info(Msg, Room) ->
164 | lager:warning("Module nkmedia_room received unexpected info: ~p", [Msg]),
165 | {noreply, Room}.
166 |
167 |
168 |
169 | %% ===================================================================
170 | %% API CMD
171 | %% ===================================================================
172 |
173 | %% @private
174 | api_cmd(#api_req{class = <<"media">>, subclass = <<"room">>, cmd=Cmd}=Req, State) ->
175 | nkmedia_room_api:cmd(Cmd, Req, State);
176 |
177 | api_cmd(_Req, _State) ->
178 | continue.
179 |
180 |
181 | %% @private
182 | api_syntax(#api_req{class = <<"media">>, subclass = <<"room">>, cmd=Cmd},
183 | Syntax, Defaults, Mandatory) ->
184 | nkmedia_room_api_syntax:syntax(Cmd, Syntax, Defaults, Mandatory);
185 |
186 | api_syntax(_Req, _Syntax, _Defaults, _Mandatory) ->
187 | continue.
188 |
189 |
190 |
191 | %% ===================================================================
192 | %% API Server
193 | %% ===================================================================
194 |
195 |
--------------------------------------------------------------------------------
/src/plugins/nkmedia_room_msglog.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | %% @doc Plugin implementing a Verto server
22 | -module(nkmedia_room_msglog).
23 | -author('Carlos Gonzalez ').
24 |
25 | -export([send_msg/2, get_msgs/2]).
26 | -export([plugin_deps/0, plugin_start/2, plugin_stop/2]).
27 | -export([error_code/1]).
28 | -export([nkmedia_room_init/2, nkmedia_room_handle_call/3]).
29 | -export([api_cmd/2, api_syntax/4]).
30 |
31 | -include("../../include/nkmedia_room.hrl").
32 | -include_lib("nkservice/include/nkservice.hrl").
33 |
34 |
35 | %% ===================================================================
36 | %% Types
37 | %% ===================================================================
38 |
39 | -type filters() ::
40 | #{}.
41 |
42 |
43 | -type msg_id() ::
44 | integer().
45 |
46 |
47 | -type msg() ::
48 | #{
49 | msg_id => msg_id(),
50 | user_id => binary(),
51 | session_id => binary(),
52 | timestamp => nklib_util:l_timestamp()
53 | }.
54 |
55 |
56 | -record(state, {
57 | pos = 1 :: integer(),
58 | msgs :: orddict:orddict()
59 | }).
60 |
61 |
62 |
63 |
64 | %% ===================================================================
65 | %% Public
66 | %% ===================================================================
67 |
68 |
69 | %% @doc Sends a message to the room
70 | -spec send_msg(nkmedia_room:id(), map()) ->
71 | {ok, msg()} | {error, term()}.
72 |
73 | send_msg(RoomId, Msg) when is_map(Msg) ->
74 | Msg2 = Msg#{timestamp=>nklib_util:l_timestamp()},
75 | case nkmedia_room:do_call(RoomId, {?MODULE, send, Msg2}) of
76 | {ok, MsgId} ->
77 | {ok, Msg2#{msg_id=>MsgId}};
78 | {error, Error} ->
79 | {error, Error}
80 | end.
81 |
82 |
83 | %% @doc Get all msgs
84 | -spec get_msgs(nkmedia_room:id(), filters()) ->
85 | {ok, [msg()]} | {error, term()}.
86 |
87 | get_msgs(RoomId, Filters) ->
88 | nkmedia_room:do_call(RoomId, {?MODULE, get, Filters}).
89 |
90 |
91 |
92 | %% ===================================================================
93 | %% Plugin callbacks
94 | %% ===================================================================
95 |
96 |
97 | %% @private
98 | plugin_deps() ->
99 | [nkmedia_room].
100 |
101 |
102 | %% @private
103 | plugin_start(Config, #{name:=Name}) ->
104 | lager:info("Plugin NkMEDIA ROOM MsgLog (~s) starting", [Name]),
105 | {ok, Config}.
106 |
107 |
108 | %% @private
109 | plugin_stop(Config, #{name:=Name}) ->
110 | lager:info("Plugin NkMEDIA ROOM MsgLog (~p) stopping", [Name]),
111 | {ok, Config}.
112 |
113 |
114 | %% @private
115 | error_code(_) -> continue.
116 |
117 |
118 |
119 | %% ===================================================================
120 | %% Room callbacks
121 | %% ===================================================================
122 |
123 | %% @private
124 | nkmedia_room_init(_RoomId, Room) ->
125 | State = #state{msgs=orddict:new()},
126 | {ok, Room#{?MODULE=>State}}.
127 |
128 |
129 | %% @private
130 | nkmedia_room_handle_call({?MODULE, send, Msg}, _From, #{?MODULE:=State}=Room) ->
131 | nkmedia_room:restart_timer(Room),
132 | #state{pos=Pos, msgs=Msgs} = State,
133 | Msg2 = Msg#{msg_id => Pos},
134 | State2 = State#state{
135 | pos = Pos+1,
136 | msgs = orddict:store(Pos, Msg2, Msgs)
137 | },
138 | {reply, {ok, Pos}, update(State2, Room)};
139 |
140 | nkmedia_room_handle_call({?MODULE, get, _Filters}, _From,
141 | #{?MODULE:=State}=Room) ->
142 | nkmedia_room:restart_timer(Room),
143 | #state{msgs=Msgs} = State,
144 | Reply = [Msg || {_Id, Msg} <- orddict:to_list(Msgs)],
145 | {reply, {ok, Reply}, Room};
146 |
147 | nkmedia_room_handle_call(_Msg, _From, _Room) ->
148 | continue.
149 |
150 |
151 | %% ===================================================================
152 | %% API Callbacks
153 | %% ===================================================================
154 |
155 | %% @private
156 | api_cmd(#api_req{class = <<"media">>, subclass = <<"room">>, cmd=Cmd}=Req, State)
157 | when Cmd == <<"msglog_send">>; Cmd == <<"msglog_get">> ->
158 | #api_req{cmd=Cmd} = Req,
159 | do_api_cmd(Cmd, Req, State);
160 |
161 | api_cmd(_Req, _State) ->
162 | continue.
163 |
164 |
165 | %% @private
166 | api_syntax(#api_req{class = <<"media">>, subclass = <<"room">>, cmd=Cmd}=Req,
167 | Syntax, Defaults, Mandatory)
168 | when Cmd == <<"msglog_send">>; Cmd == <<"msglog_get">> ->
169 | #api_req{cmd=Cmd} = Req,
170 | {S2, D2, M2} = do_api_syntax(Cmd, Syntax, Defaults, Mandatory),
171 | {continue, [Req, S2, D2, M2]};
172 |
173 | api_syntax(_Req, _Syntax, _Defaults, _Mandatory) ->
174 | continue.
175 |
176 |
177 | %% ===================================================================
178 | %% Internal
179 | %% ===================================================================
180 |
181 | %% @private
182 | update(State, Room) ->
183 | ?ROOM(#{?MODULE=>State}, Room).
184 |
185 |
186 | do_api_cmd(<<"msglog_send">>, ApiReq, State) ->
187 | #api_req{srv_id=SrvId, data=Data, user=User, session=SessId} = ApiReq,
188 | #{room_id:=RoomId, msg:=Msg} = Data,
189 | RoomMsg = Msg#{user_id=>User, session_id=>SessId},
190 | case send_msg(RoomId, RoomMsg) of
191 | {ok, #{msg_id:=MsgId}=SentMsg} ->
192 | nkmedia_api_events:send_event(SrvId, room, RoomId, msglog_new_msg, SentMsg),
193 | {ok, #{msg_id=>MsgId}, State};
194 | {error, Error} ->
195 | {error, Error, State}
196 | end;
197 |
198 | do_api_cmd(<<"msglog_get">>, #api_req{data=Data}, State) ->
199 | #{room_id:=RoomId} = Data,
200 | case get_msgs(RoomId, #{}) of
201 | {ok, List} ->
202 | {ok, List, State};
203 | {error, Error} ->
204 | {error, Error, State}
205 | end;
206 |
207 | do_api_cmd(_Cmd, _ApiReq, State) ->
208 | {error, not_implemented, State}.
209 |
210 |
211 | %% @private
212 | do_api_syntax(<<"msglog_send">>, Syntax, Defaults, Mandatory) ->
213 | {
214 | Syntax#{
215 | room_id => binary,
216 | msg => map
217 | },
218 | Defaults,
219 | [room_id, msg|Mandatory]
220 | };
221 |
222 | do_api_syntax(<<"msglog_get">>, Syntax, Defaults, Mandatory) ->
223 | {
224 | Syntax#{
225 | room_id => binary
226 | },
227 | Defaults,
228 | [room_id|Mandatory]
229 | };
230 |
231 | do_api_syntax(_Cmd, Syntax, Defaults, Mandatory) ->
232 | {Syntax, Defaults, Mandatory}.
233 |
234 |
235 |
236 |
237 |
238 |
239 |
--------------------------------------------------------------------------------
/test/app.config:
--------------------------------------------------------------------------------
1 | [
2 | {nkworker, [
3 | {password, "123"},
4 | {agent_start, true},
5 | {agent_meta, "f,g;a=1"},
6 | {agent_announce, [
7 | % ""
8 | ]},
9 | {agent_announce_time, 360000},
10 | {agent_listen, ""},
11 |
12 | {control_start, true},
13 | {control_listen,
14 | ","}
15 | ]},
16 |
17 | {lager, [
18 | {handlers, [
19 | {lager_console_backend, warning},
20 | {lager_file_backend, [{file, "log/error.log"}, {level, error}]},
21 | {lager_file_backend, [{file, "log/console.log"}, {level, info}]}
22 | ]},
23 | {error_logger_redirect, false},
24 | {crash_log, "log/crash.log"},
25 | {colored, true},
26 | {colors, [
27 | {debug, "\e[0;38m" },
28 | {info, "\e[0;32m" },
29 | {notice, "\e[1;36m" },
30 | {warning, "\e[1;33m" },
31 | {error, "\e[1;31m" }
32 | ]}
33 | ]},
34 |
35 | {sasl, [
36 | {sasl_error_logger, false}
37 | ]}
38 | ].
39 |
--------------------------------------------------------------------------------
/test/basic_test.erl:
--------------------------------------------------------------------------------
1 | %% -------------------------------------------------------------------
2 | %%
3 | %% Copyright (c) 2015 Carlos Gonzalez Florido. All Rights Reserved.
4 | %%
5 | %% This file is provided to you under the Apache License,
6 | %% Version 2.0 (the "License"); you may not use this file
7 | %% except in compliance with the License. You may obtain
8 | %% a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing,
13 | %% software distributed under the License is distributed on an
14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | %% KIND, either express or implied. See the License for the
16 | %% specific language governing permissions and limitations
17 | %% under the License.
18 | %%
19 | %% -------------------------------------------------------------------
20 |
21 | -module(basic_test).
22 | -author('Carlos Gonzalez ').
23 |
24 | -compile([export_all]).
25 | -include_lib("eunit/include/eunit.hrl").
26 |
27 | basic_test_() ->
28 | {setup, spawn,
29 | fun() ->
30 | ok = nkmedia_app:start()
31 | end,
32 | fun(_) ->
33 | ok
34 | end,
35 | fun(_) ->
36 | [
37 | fun() -> connect() end,
38 | fun() -> regs() end,
39 | fun() -> transports() end
40 | ]
41 | end
42 | }.
43 |
44 |
45 | connect() ->
46 | ?debugMsg("Starting CONNECT test"),
47 | {error, invalid_uri} = nkmedia_worker:start_link("http://localhost", #{}),
48 | {ok, P1} = nkmedia_worker:start_link("nkmedia://localhost:9999", #{}),
49 | timer:sleep(200),
50 | {ok, error, []} = nkmedia_worker:get_status(P1),
51 | nkmedia_worker:stop(P1),
52 | timer:sleep(100),
53 | false = is_process_alive(P1),
54 |
55 | {ok, Listen, UUID} = nkmedia_agent:get_listen(),
56 | {ok, P2} = nkmedia_worker:start_link(Listen, #{password=>"invalid"}),
57 | timer:sleep(200),
58 | {ok, error, []}= nkmedia_worker:get_status(P2),
59 | {error, not_connected} = nkmedia_worker:send_rpc(P2, core, get_meta, #{}),
60 | nkmedia_worker:stop(P2),
61 |
62 | {ok, P3} = nkmedia_worker:start_link(Listen, #{password=>"123"}),
63 | timer:sleep(200),
64 | {ok, ok, _} = nkmedia_worker:get_status(P3),
65 | [{Listen, UUID, P3, ok}] = nkmedia_worker:get_all(),
66 | {ok, P3} = nkmedia_worker:find_pid(UUID),
67 | {ok, P3} = nkmedia_worker:find_pid(Listen),
68 | nkmedia_worker:stop(P3).
69 |
70 |
71 | regs() ->
72 | ?debugMsg("Starting REGS test"),
73 | {ok, Listen, _UUID} = nkmedia_agent:get_listen(),
74 | {ok, P1} = nkmedia_worker:start_link(Listen),
75 | timer:sleep(100),
76 | {ok, Conn} = nkmedia_worker:get_conn(P1, #{}),
77 | [{{job, stats}, [_]}] = gen_server:call(Conn, get_regs),
78 |
79 | {ok, Conn} = nkmedia_worker:reg_job(P1, job1),
80 | {ok, Conn} = nkmedia_worker:reg_job(P1, job1),
81 | {ok, Conn} = nkmedia_worker:reg_job(P1, job2),
82 | {ok, Conn} = nkmedia_worker:reg_class(P1, class1),
83 | {ok, Conn} = nkmedia_worker:reg_class(P1, class1),
84 | {ok, Conn} = nkmedia_worker:reg_class(P1, class2),
85 | Self = self(),
86 | [
87 | {{class,class1}, [Self]},
88 | {{class,class2}, [Self]},
89 | {{job,job1}, [Self]},
90 | {{job,job2}, [Self]},
91 | {{job,stats}, [_]}
92 | ] = Regs1 = lists:sort(gen_server:call(Conn, get_regs)),
93 | Ref = make_ref(),
94 | Pid2 = spawn_link(
95 | fun() ->
96 | {ok, Conn} = nkmedia_worker:reg_job(P1, job1),
97 | {ok, Conn} = nkmedia_worker:reg_class(P1, class2),
98 | Self ! {Ref, lists:sort(gen_server:call(Conn, get_regs))}
99 | end),
100 | receive
101 | {Ref,
102 | [
103 | {{class,class1}, [Self]},
104 | {{class,class2}, [Pid2, Self]},
105 | {{job,job1}, [Pid2, Self]},
106 | {{job,job2}, [Self]},
107 | {{job,stats}, [_]}
108 | ]} ->
109 | ok
110 | after 1000 ->
111 | error(?LINE)
112 | end,
113 | timer:sleep(100),
114 | Regs1 = lists:sort(gen_server:call(Conn, get_regs)),
115 | ok = nkmedia_worker:unreg_job(P1, job3),
116 | Regs1 = lists:sort(gen_server:call(Conn, get_regs)),
117 | ok = nkmedia_worker:unreg_job(P1, job1),
118 | ok = nkmedia_worker:unreg_class(P1, class1),
119 | [
120 | {{class,class2}, [Self]},
121 | {{job,job2}, [Self]},
122 | {{job,stats}, [_]}
123 | ] = lists:sort(gen_server:call(Conn, get_regs)),
124 | ok = nkmedia_worker:unreg_job(P1, job2),
125 | ok = nkmedia_worker:unreg_class(P1, class2),
126 | [{{job, stats}, [_]}] = gen_server:call(Conn, get_regs),
127 | nkmedia_worker:stop(P1).
128 |
129 |
130 | transports() ->
131 | ?debugMsg("Starting TRANSPORTS test"),
132 | Opts = #{
133 | tcp_packet => 4,
134 | ws_proto => <<"nkmedia">>,
135 | user => #{class=>{agent, <<"test1">>, #{}}, password=><<"123">>}
136 | },
137 |
138 | Conn1 = "nkmedia:all;transport=tls",
139 | {ok, Listen1} = nkpacket:start_listener(nkmedia_agent, Conn1, Opts),
140 | {ok, {tls, _, Port1}} = nkpacket:get_local(Listen1),
141 | Conn1B = "nkmedia:all:" ++ integer_to_list(Port1) ++ ";transport=tls",
142 | {ok, Worker1} = nkmedia_worker:start_link(Conn1B),
143 | timer:sleep(100),
144 | {ok, ok, _} = nkmedia_worker:get_status(Worker1),
145 | nkmedia_worker:stop(Worker1),
146 | nkpacket:stop_listener(Listen1),
147 |
148 | Conn2 = "nkmedia:all;transport=ws",
149 | {ok, Listen2} = nkpacket:start_listener(nkmedia_agent, Conn2, Opts),
150 | {ok, {ws, _, Port2}} = nkpacket:get_local(Listen2),
151 | Conn2B = "nkmedia:all:" ++ integer_to_list(Port2) ++ ";transport=ws",
152 | {ok, Worker2} = nkmedia_worker:start_link(Conn2B),
153 | timer:sleep(100),
154 | {ok, ok, _} = nkmedia_worker:get_status(Worker2),
155 | nkmedia_worker:stop(Worker2),
156 | nkpacket:stop_listener(Listen2),
157 |
158 | Conn3 = "nkmedia:all;transport=wss",
159 | {ok, Listen3} = nkpacket:start_listener(nkmedia_agent, Conn3, Opts),
160 | {ok, {wss, _, Port3}} = nkpacket:get_local(Listen3),
161 | Conn3B = "nkmedia:all:" ++ integer_to_list(Port3) ++ ";transport=wss",
162 | {ok, Worker3} = nkmedia_worker:start_link(Conn3B),
163 | timer:sleep(100),
164 | {ok, ok, _} = nkmedia_worker:get_status(Worker3),
165 | nkmedia_worker:stop(Worker3),
166 | nkpacket:stop_listener(Listen3).
167 |
168 |
169 |
170 |
171 |
172 |
173 |
--------------------------------------------------------------------------------
/test/vm.args:
--------------------------------------------------------------------------------
1 | -pa deps/eper/ebin
2 | -pa deps/goldrush/ebin
3 | -pa deps/lager/ebin
4 | -pa deps/nkpacket/ebin
5 | -pa deps/nklib/ebin
6 | -pa deps/cowboy/ebin
7 | -pa deps/cowlib/ebin
8 | -pa deps/ranch/ebin
9 | -pa deps/gun/ebin
10 | -pa deps/jiffy/ebin
11 | -pa deps/nkdocker/ebin
12 | -pa ../nkworker/ebin
13 |
14 | ## Name of the node
15 | -name nkworker@127.0.0.1
16 | -setcookie nksip
17 |
18 | ## More processes
19 | +P 1000000
20 |
21 | ## Treat error_logger warnings as warnings
22 | +W w
23 |
24 | ## Increase number of concurrent ports/sockets
25 | -env ERL_MAX_PORTS 65535
26 |
27 | ## Tweak GC to run more often
28 | #-env ERL_FULLSWEEP_AFTER 0
29 |
30 | ## Set the location of crash dumps
31 | -env ERL_CRASH_DUMP .
32 |
33 | # Start apps
34 | # -s nkrest_app
35 |
36 |
37 |
--------------------------------------------------------------------------------
/util/shell_app.config:
--------------------------------------------------------------------------------
1 | [
2 | {nkmedia, [
3 | ]},
4 |
5 | {lager, [
6 | {handlers, [
7 | {lager_console_backend, info},
8 | {lager_file_backend, [{file, "log/error.log"}, {level, error}]},
9 | {lager_file_backend, [{file, "log/console.log"}, {level, info}]}
10 | ]},
11 | {error_logger_redirect, false},
12 | {crash_log, "log/crash.log"},
13 | {colored, true},
14 | {colors, [
15 | {debug, "\e[0;38m" },
16 | {info, "\e[0;32m" },
17 | {notice, "\e[1;36m" },
18 | {warning, "\e[1;33m" },
19 | {error, "\e[1;31m" }
20 | ]}
21 | ]},
22 |
23 | {sasl, [
24 | {sasl_error_logger, false}
25 | ]}
26 | ].
27 |
--------------------------------------------------------------------------------
/util/shell_vm.args:
--------------------------------------------------------------------------------
1 | -pa deps/eper/ebin
2 | -pa deps/goldrush/ebin
3 | -pa deps/lager/ebin
4 | -pa deps/nkpacket/ebin
5 | -pa deps/nklib/ebin
6 | -pa deps/cowboy/ebin
7 | -pa deps/cowlib/ebin
8 | -pa deps/ranch/ebin
9 | -pa deps/gun/ebin
10 | -pa deps/jsx/ebin
11 | -pa deps/nkdocker/ebin
12 | -pa deps/nksip/ebin
13 | -pa deps/nksip/plugins/ebin
14 | -pa deps/nkservice/ebin
15 | -pa deps/meck/ebin
16 | -pa deps/mustache/ebin
17 | -pa deps/enm/ebin
18 | -pa ../nkmedia/ebin
19 |
20 |
21 | ## Name of the node
22 | -name nkmedia@127.0.0.1
23 | -setcookie nkmedia
24 |
25 | ## More processes
26 | +P 1000000
27 |
28 | ## Treat error_logger warnings as warnings
29 | +W w
30 |
31 | ## Increase number of concurrent ports/sockets
32 | -env ERL_MAX_PORTS 65535
33 |
34 | ## Tweak GC to run more often
35 | #-env ERL_FULLSWEEP_AFTER 0
36 |
37 | ## Set the location of crash dumps
38 | -env ERL_CRASH_DUMP .
39 |
40 | # Start apps
41 | # -s nkrest_app
42 |
43 |
44 |
--------------------------------------------------------------------------------