이전 포스팅에서는 Janus-Gateway 의 API에 대해서 개략적으로 알아보았다. 이번 포스팅에서는 Janus-Gateway 의 Audiobridge Plugin을 이용하는 방법에 대해서 설명한다.
Janus-Gateway 는 WebRTC 기반 서비스 또는 기능은 Plugin 을 통해서 제공한다. 기본적으로 제공하는 Plugin 중 audiobridge 는 음성 그룹 채팅이 가능한 plugin 이다. 그리고 MCU의 특성, 다시 말해 음성 그룹 채팅에 참여하는 Peer 의 음성을 Mixing 해서 전달한다.
Mixing 기능을 제공하기 때문에 서버의 CPU를 많이 소모하며, 참여자가 늘어날 수록 CPU 사용율이 Linear 하게 증가되게 된다. Plugin 의 API는 janus 홈페이지에서는 확인이 불가능하고 실제 plugin 코드를 보거나, 데모 웹을 통해서 동작 순서를 분석해야 한다.
1. configuration
janus audiobridge plugin의 config 파일은 /usr/local/etc/janus/janus.plugin.audiobridge.cfg 이다. 이 파일을 열어보면 default room 정보가 들어 있고 [general] 항목에 admin_key를 설정하는 항목이 있다. admin_key를 설정하면 room의 create api에 admin_key를 설정해서 보내야 한다.
2. create room
audiobridge에 mixing room 을 생성한다. 다음은 create room 파라미터 설명이다.
{ "request" : "create", "room" : <unique numeric ID, optional, chosen by plugin if missing>, "permanent" : <true|false, whether the room should be saved in the config file, default false>, "description" : "<pretty name of the room, optional>", "secret" : "<password required to edit/destroy the room, optional>", "pin" : "<password required to join the room, optional>", "is_private" : <true|false, whether the room should appear in a list request>, "allowed" : [ array of string tokens users can use to join this room, optional], "sampling" : <sampling rate of the room, optional, 16000 by default>, "audiolevel_ext" : <true|false, whether the ssrc-audio-level RTP extension must be negotiated for new joins, default true>, "record" : <true|false, whether to record the room or not, default false>, "record_file" : "</path/to/the/recording.wav, optional>", } |
다음은 websocket 으로 create 를 호출하는 코드이다.
janus.plugin={}; janus.plugin.audiobridge={}; janus.plugin.audiobridge.create=(ws,session_id,handle_id,roomopt)=>{ let trxid=getTrxId(); let request={}; request.janus='message'; request.transaction=trxid; request.session_id=session_id; request.handle_id=handle_id; request.body={}; request.body.request='create'; request.body.room=roomopt.room; request.body.permanent=roomopt.permanent; if(roomopt.description) request.body.permanent=roomopt.description; if(roomopt.secret) request.body.permanent=roomopt.secret; if(roomopt.pin) request.body.permanent=roomopt.pin; if(roomopt.is_private) request.body.permanent=roomopt.is_private; if(roomopt.allowed) request.body.permanent=roomopt.allowed; if(roomopt.sampling) request.body.permanent=roomopt.sampling; if(roomopt.audiolevel_ext) request.body.permanent=roomopt.audiolevel_ext; if(roomopt.record) request.body.permanent=roomopt.record; if(roomopt.record_file) request.body.permanent=roomopt.record_file; if(roomopt.admin_key) request.body.admin_key=roomopt.admin_key; console.log('janus.plugin.audiobridge.create msg:'+JSON.stringify(request)); ws.send(JSON.stringify(request)); }; |
다음과 같이 호출할 경우 요청-응답은 다음과 같다.
호출코드> let roomopt={}; roomopt.room=5555; roomopt.permanent=false; roomopt.admin_key='janushello'; janus.plugin.audiobridge.create(ws,janusSessionId,handleId,roomopt); |
요청> {"janus":"message","transaction":"Ik7z2RcMbxgO","session_id":273910267729177, "handle_id":6812743015768869,"body":{"request":"create","room":5555,"permanent":false, "admin_key":"janushello"}} |
응답> {"janus":"success","session_id":273910267729177,"sender":6812743015768869, "transaction":"Ik7z2RcMbxgO","plugindata":{"plugin":"janus.plugin.audiobridge","data":{"audiobridge":"created","room":5555}}} |
3. destroy room
생성한 audiobridge room 을 파괴한다. 다음은 destroy room 파라미터에 대한 설명이다.
{ "request" : "destroy", "room" : <unique numeric ID of the room to destroy>, "secret" : "<room secret, mandatory if configured>", "permanent" : <true|false, whether the room should be also removed from the config file, default false> } |
다음은 websocket으로 destroy 를 호출하는 코드이다.
janus.plugin.audiobridge.destroy=(ws,session_id,handle_id,roomopt)=>{ let trxid=getTrxId(); let request={}; request.janus='message'; request.transaction=trxid; request.session_id=session_id; request.handle_id=handle_id; request.body={}; request.body.request='destroy'; request.body.room=roomopt.room; if(roomopt.admin_key) request.body.admin_key=roomopt.admin_key; console.log('janus.plugin.audiobridge.create msg:'+JSON.stringify(request)); ws.send(JSON.stringify(request)); }; |
다음과 같이 호출할 경우 요청-응답은 다음과 같다.
호출코드> let roomopt={}; roomopt.room=5555; roomopt.admin_key='janushello'; janus.plugin.audiobridge.destroy(ws,janusSessionId,handleId,roomopt); |
요청> {"janus":"message","transaction":"9qLWxeUm2XqH","session_id":4108642482605178, "handle_id":3115692898921259,"body":{"request":"destroy","room":5555,"admin_key":"janushello"}} |
응답> {"janus":"success","session_id":4108642482605178,"sender":3115692898921259, "transaction":"9qLWxeUm2XqH","plugindata":{"plugin":"janus.plugin.audiobridge","data":{"audiobridge":"destroyed","room":5555}}} |
4. join room
생성되어져 있는 room 에 join 한다.
{ "request" : "join", "room" : <numeric ID of the room to join>, "id" : <unique ID to assign to the participant; optional, assigned by the plugin if missing>, "pin" : "<password required to join the room, if any; optional>", "display" : "<display name to have in the room; optional>", "token" : "<invitation token, in case the room has an ACL; optional>", "muted" : <true|false, whether to start unmuted or muted>, "quality" : <0-10,Opus-related complexity to use, lower is higher quality; optional, default is 4>, "volume" : <percent value, <100 reduces volume, >100 increases volume; optional, default is 100 (no volume change)> } |
다음은 websocket으로 join 을 호출하는 코드이다.
janus.plugin.audiobridge.join=(ws,session_id,handle_id,roomopt)=>{ let trxid=getTrxId(); let request={}; request.janus='message'; request.transaction=trxid; request.session_id=session_id; request.handle_id=handle_id; request.body={}; request.body.request='join'; request.body.room=roomopt.room; if(roomopt.id) request.body.id; if(roomopt.pin) request.body.pin; if(roomopt.display) request.body.display; if(roomopt.token) request.body.token; if(roomopt.muted) request.body.muted; if(roomopt.quality) request.body.quality; if(roomopt.volume) request.body.volume; console.log('janus.plugin.audiobridge.join msg:'+JSON.stringify(request)); ws.send(JSON.stringify(request)); return trxid; }; |
다음과 같이 호출할 경우 요청-응답은 다음과 같다.
호출코드> let roomopt={}; roomopt.room=5555; joinTransaction=janus.plugin.audiobridge.join(ws,janusSessionId,handleId,roomopt); |
요청> {"janus":"message","transaction":"bnkSWjbJlc85","session_id":6043633968408612, "handle_id":3174356917265656,"body":{"request":"join","room":5555}} |
응답> {"janus":"event","session_id":6043633968408612,"sender":3174356917265656, "transaction":"bnkSWjbJlc85","plugindata":{"plugin":"janus.plugin.audiobridge","data":{"audiobridge":"joined","room":5555,"id":1426105010077627,"participants":[{"id":6900479519878206,"muted":false}]}}} |
5. leave room
Join한 Room에서 나간다. 참여하고 있는 다른 Client에 Leave하는 client의 ID를 event notification으로 받는다.
{ "request" : "leave" } |
다음은 websocket으로 leave를 호출하는 코드이다.
janus.plugin.audiobridge.leave=(ws,session_id,handle_id,roomopt)=>{ let trxid=getTrxId(); let request={}; request.janus='message'; request.transaction=trxid; request.session_id=session_id; request.handle_id=handle_id; request.body={}; request.body.request='leave'; ws.send(JSON.stringify(request)); return trxid; }; |
호출시 다음의 요청-응답을 보인다.
요청> {"janus":"message","transaction":"ZPhUC0UnQ0b4","session_id":6924157701703087, "handle_id":154472435867945,"body":{"request":"leave"}} |
응답> {"janus":"event","session_id":6924157701703087,"sender":154472435867945, "transaction":"ZPhUC0UnQ0b4","plugindata":{"plugin":"janus.plugin.audiobridge","data":{"audiobridge":"left","room":5555,"id":3424150840600147}}} |
6. configure
Join 이후에 WebRTC Channel 생성을 위한 jsep 메시지(offer)를 전달한다. plugin code상에는 다음처럼 기술되어 있으나, websocket 으로 연동시 jsep 메시지를 첨가해야 한다. 즉 configure 메시지를 보내기 전에 local의 sdp 가 나와야 한다는 것이다.
{ "request" : "configure", "muted" : <true|false, whether to unmute or mute>, "display" : "<new display name to have in the room>", "quality" : <0-10, Opus-related complexity to use, lower is higher quality; optional, default is 4>, "volume" : <percent value, <100 reduces volume, >100 increases volume; optional, default is 100 (no volume change)>, "record": <true|false, whether to record this user's contribution to a .mjr file (mixer not involved), "filename": "<basename of the file to record to, -audio.mjr will be added by the plugin>" } |
다음은 websocket으로 configure를 호출하는 코드이다.
janus.plugin.audiobridge.configure=(ws,session_id,handle_id,description,roomopt)=>{ let trxid=getTrxId(); let jsep={ type:description.type, sdp: description.sdp }; let request={ "janus":"message", "transaction":trxid, "session_id":session_id, "handle_id":handle_id, "jsep":jsep }; request.body={}; request.body.request='configure'; request.body.muted=roomopt.muted; if(roomopt.display) request.body.display=roomopt.display; if(roomopt.quality) request.body.quality=roomopt.quality; if(roomopt.volume) request.body.volume=roomopt.volume; if(roomopt.record) request.body.record=roomopt.record; if(roomopt.filename) request.body.filename=roomopt.filename; ws.send(JSON.stringify(request)); return trxid; }; |
다음은 이를 호출하는 코드 및 요청-응답이다. 호출 코드는 peerconnection의 createOffer 함수 상에서 진행된다. configure에 대한 응답으로, answer가 온다. 따라서 peerconnection의 setLocalDescription 이후 configure로 offer를 보내고 이에 대한 응답을 받아서 setRemoteDescription 을 설정하면 webrtc channel이 생성되는 것이다.
pc.createOffer(pc_constraints).then(function (offer) { return pc.setLocalDescription(offer); }).then(function () { let roomopt={}; roomopt.muted=false; offerToTransaction=janus.plugin.audiobridge.configure(ws,janusSessionId,handleId,pc.localDescription,roomopt); }).catch(logError); |
요청> {"janus":"message","transaction":"FkFWawuOYpQh","session_id":5685216088527405, "handle_id":4526015082420804,"body":{"request":"configure","muted":false}, "jsep":{"type":"offer","sdp":"v=0\r\no=- 6251931514972413769 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0 \r\na=group:BUNDLE audio \r\na=msid-semantic: WMS i5Cn6h0gKzQ8fQ2F90UCKNPdaVZgNRWyzlej \r\nm=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126 \r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:pWcr \r\na=ice-pwd:ss1s/Hdf/FDXp0gr/+rMz5rE\r\na=fingerprint:sha-256 F5:88:59:11:9A:5D:F2:17:5C:6B:65:23:C5:AA:0D:63:AD:7D:2C:46:8F:83:47:5E:B3:D0:96:2F:85:68:81:53 \r\na=setup:actpass\r\na=mid:audio\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=sendrecv\r\na=rtcp-mux\r\na=rtpmap:111 opus/48000/2\r\na=rtcp-fb:111 transport-cc\r\na=fmtp:111 minptime=10;useinbandfec=1\r\na=rtpmap:103 ISAC/16000\r\na=rtpmap:104 ISAC/32000\r\na=rtpmap:9 G722/8000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:106 CN/32000\r\na=rtpmap:105 CN/16000\r\na=rtpmap:13 CN/8000\r\na=rtpmap:110 telephone-event/48000\r\na=rtpmap:112 telephone-event/32000\r\na=rtpmap:113 telephone-event/16000\r\na=rtpmap:126 telephone-event/8000\r\na=ssrc:1620416225 cname:PuvGbj3ECo88axm1\r\na=ssrc:1620416225 msid:i5Cn6h0gKzQ8fQ2F90UCKNPdaVZgNRWyzlej 3720a2b9-2600-46af-b55a-dc598d6476fe\r\na=ssrc:1620416225 mslabel:i5Cn6h0gKzQ8fQ2F90UCKNPdaVZgNRWyzlej\r\na=ssrc:1620416225 label:3720a2b9-2600-46af-b55a-dc598d6476fe\r\n"}} |
응답> \r\na=setup:active\r\na=rtpmap:111 opus/48000/2\r\na=fmtp:111 maxplaybackrate=16000; stereo=0; sprop-stereo=0; useinbandfec=0\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=ssrc:3268582270 cname:janusaudio\r\na=ssrc:3268582270 msid:janus janusa0\r\na=ssrc:3268582270 mslabel:janus\r\na=ssrc:3268582270 label:janusa0\r\na=candidate:1 1 udp 2013266431 192.168.0.55 47767 typ host\r\n"}} |