├── .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 | --------------------------------------------------------------------------------