들어가기
Janus을 사용하면 분석하고 활용했던 API을 정리해보았다.
작성자: ospace114@empal.com, http://ospace.tistory.com/
메시지 규약
janus에 메소드는 GET과 POST 형식을 사용한다.
- GET: 이벤트 및 메시지 long polling
- POST: 핸들 생성, 세션 조작
모든 요청 메시지의 body에는 두가지 필드가 필수이다.
- janus: 요청이나 이벤트 (ex. create, attache, message, etc.)
- transaction: 클라이언트가 서버에서 응답 메시지 확인에 사용
오류가 발생할 경우 응답 메시지 필드이다.
- janus: "error" 에러 메시지 표시
- transaction: 실패한 요청 트랜잭션 식별자
- error: 에러 내용
- code: apierror.h(https://janus.conf.meetecho.com/docs/apierror_8h.html)에%EC%97%90) 정의된 오류 코드 참고
- reason: 실패 자세한 에러 메시지
응답메시지 예
{
"janus" : "error",
"transaction" : "a1b2c3d4"
"error" : {
"code" : 458
"reason" : "Could not find session 12345678"
}
}
API
기본적인 API의 루트 경로는 "/janus"으로 되어 있다. 이 값도 janus 서버 설정으로 변경할 수 있다.
janus 정보 조회
- GET http://{ip_address}/janus/info
session 생성
- POST http://{ip_address}/janus
- 요청 메시지session이 생성되면 새로운 endpoint 연결할 수 있다. (ex. /janus/12345678)
{
"janus" : "create",
"transaction" : "<random alphanumeric string>"
}
session long polling
- GET http://{ip_address}/janus/12345678
- 30초 대기시간 초과되면 keep-alive 메시지가 응답됨
- long polling은 기본이 단일 이벤트만 가져오지만, maxev로 가져올 이벤트 개수를 설정 가능
- GET http://{ip_address}/janus/12345678?maxev=5
- 응답 데이터
{
"janus" : "event",
"sender" : 1815153248,
"transaction" : "sBJNyUhH6Vc6",
"plugindata" : {
"plugin": "janus.plugin.echotest",
"data" : {
"echotest" : "event",
"result" : "ok"
}
},
}
interacting with the session
세션에 플러그인을 장착
- POST http://{ip_address}/janus/12345678
- plugin 필드에 사용할 플러그인 이름 설정
- 요청 메시지
{
"janus" : "attach",
"plugin" : "<the plugin's unique package name>",
"transaction" : "<random string>"
}
- 응답 데이터
{
"janus" : "success",
"transaction" : "<same as the request>",
"data" : {
"id" : <unique integer plugin handle ID>
}
}
- 성공하면 세션처럼 플로그인 식별자를 생성해서 반환됨
- 요청시 기존 세션 식별자와 같이 포함해서 요청함.
- 예) /janus/12345678/87654321
- 플러그인 식별자로 WebRTC 처리위한 협상 작업을 처리
- janus_echotest.c(https://janus.conf.meetecho.com/docs/janus__echotest_8c.html) 플그인으로 요청 예
{
"janus" : "message",
"transaction" : "sBJNyUhH6Vc6",
"body" : {
"audio" : false
}
}
- 협상위한 정보를 포함하는 예
{
"janus" : "message",
"transaction" : "sBJNyUhH6Vc6",
"body" : {
"audio" : false
},
"jsep" : {
"type" : "offer",
"sdp" : "v=0\r\no=[..more sdp stuff..]"
}
}
- trickle의 단일 candidate 을 사용한 경우
{
"janus" : "trickle",
"transaction" : "hehe83hd8dw12e",
"candidate" : {
"sdpMid" : "video",
"sdpMLineIndex" : 1,
"candidate" : "..."
}
}
- trickle의 복수 candidate을 사용하는 경우
{
"janus" : "trickle",
"transaction" : "hehe83hd8dw12e",
"candidates" : [
{
"sdpMid" : "video",
"sdpMLineIndex" : 1,
"candidate" : "..."
},
{
"sdpMid" : "video",
"sdpMLineIndex" : 1,
"candidate" : "..."
},
[..]
]
}
- trickle의 마지막 candidate을 표시하는 경우
{
"janus" : "trickle",
"transaction" : "hehe83hd8dw12e",
"candidate" : {
"completed" : true
}
}
- 요청 처리가 비동기적으로도 처리되며 이때 응답은 ack로 보낸다.
{
"janus": "ack",
"transaction": "sBJNyUhH6Vc6"
}
- 플러그인을 사용하지 않으려면 "detach"를 사용
{
"janus" : "detach",
"transaction" : "<random string>"
}
- 핸들을 유지하고 싶고 PeerConnection만 끝으려면 "hangup"을 사용
{
"janus" : "hangup",
"transaction" : "<random string>"
}
WebRTC관련 이벤트
long polling으로 이벤트 및 알림을 보낼 수 있음.
PeerConnection 관련된 핸들에 대해 수신할 수 있는 이벤트
- webrtcup: ICE와 DTLS 성공으로 PeerConnection이 올바르게 동작
- media: janus가 오디오/비디오(type: "audio/video")를 PeerConnection에서 수신했는지 여부(receiving: true/false)
- slowlink: PeerConnection에서 미디오 송수신(uplink: true/false) 문제 리포팅 여부
- hangup: PeerConnection이 닫힘.(janus나 사용자 어플리케이션에서)
이벤트 예
- PeerConnection이 준비된 경우
{
"janus" : "webrtcup",
session_id: <the session identifier>,
sender: <the handle identifier>
}
- Janus에 의해 먼저 오디오 바이트가 수신된 경우
{
"janus" : "media",
session_id: <the session identifier>,
sender: <the handle identifier>,
"type" : "audio",
"receiving" : true
}
- 어떤 이유로 더 이상 Janus에서 오디오를 획득하지 못한 경우
{
"janus" : "media",
"session_id" : <the session identifier>,
"sender" : <the handle identifier>
"type" : "audio",
"receiving" : false
}
- Janus에서 다시 오디오를 획득한 경우
{
"janus" : "media",
"session_id" : <the session identifier>,
"sender" : <the handle identifier>
"type" : "audio",
"receiving" : true
}
- Janus가 사용자가 보낸 미디어에 문제가 있다고 리포팅하는 경우
{
"janus" : "slowlink",
"session_id" : <the session identifier>,
"sender" : <the handle identifier>
"uplink" : true,
"nacks" : <number of NACKs in the last second>
}
- PeerConnection이 종료된 경우(DTLS 알람에 의함)
{
"janus" : "hangup",
"session_id" : <the session identifier>
"sender" : <the handle identifier>,
"reason" : "DTLS alert"
}
- media 이벤트 알림은 Janus에서 미디어를 보내는 경우에만 적용
WebSockets 인터페이스
사용할 하위 프로토콜(janus-protocol) 지정이 필요
var ws = new WebSocket('ws://{ip\_address}:8188', 'janus-protocol');
janus.js 라이브러에서 자동으로 처리됨.
현재 세션을 종료하려면
- POST http://{ip_address}/janus/12345678
- 요청 메시지
{ "janus" : "destroy", "transaction" : "<random string>" }
AudioBridge 플러그인
- 플러그인을 사용하려면 attach해야 함
{
"janus": "attach",
"plugin": "janus.plugin.audiobridge",
"session_id": <the session identifier>
}
- 성공이면 핸들 식별자 반환됨.
{
"janus": "success",
"session_id": <the session identifier>,
"transaction": "<random string>",
"sender": 4444098909079181,
"data": {
"id": <the handle identifier>
}
}
- 플러그인으로 메시지 보낸는 경우는 "message"로 설정하고 실제 내용은 "body"에 저장
- 방생성하는 경우 요청
{
"janus": "message",
"transaction": "<random string>",
"session_id": <the session identifier>,
"handle_id": <the handle identifier>,
"body": {
"request":"create",
"room": <the room number>,
"pin": "<the room secret>",
"is_private": true, /* 방노출여부 _/
"admin_key":"" /*관리키를 설정한 경우_/
}
}
- 방생성하는 경우 응답
{
"janus": "success",
"session_id": <the session identifier>,
"transaction": "<random string>",
"sender": <the session identifier>,
"plugindata": {
"plugin": "janus.plugin.audiobridge",
"data": {
"audiobridge": "created",
"room": <the room number>,
"permanent": false
}
}
}
- 방제거하는 경우 요청
{
"janus": "message",
"transaction": "<random string>",
"session_id": <the session identifier>,,
"handle_id": <the handle identifier>,
"body": {
"request":"destroy",
"room":<the room number>,
"pin":"<the room secret>",
"admin_key":"" /_관리키를 설정한 경우_/
}
}
- 방을 제거하는 경우 응답
{
"janus": "success",
"transaction": "<random string>",
"data": {
"id": <the session identifier>
}
}
- 방이 있는 여부 요청
{
"janus": "message",
"transaction": "<random string>",
"session_id": <the session identifier>,
"handle_id": <the session identifier>,
"body": {
"request":"exists",
"room":,
"secret":""
}
}
샘플 코드
<!DOCTYPE html>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.9.1/jquery.min.js" ></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/js-polyfills/0.1.43/polyfill.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/6.4.0/adapter.min.js" ></script>
<script type="text/javascript" src="/janus.js"></script>
<body>
<div>
<p>
<input type="radio" name="server_type" id="type_ws" onchange="onChangeType(event)">
<label for="type_ws">websocket</label>
<input type="radio" name="server_type" id="type_rest" onchange="onChangeType(event)">
<label for="type_rest">rest</label>
<button onclick="onConnect(event)">Connect</button>
<button onclick="onDisconnect(event)">Disconnect</button>
</p>
userId : <span id="userId"></span>,
Room# : <input type="text" id="roomNm" onchange="onChangeRoom(this)" size=4 onkeypress="return 48 <= event.charCode && event.charCode <= 57">
<button id="enterBtn" onclick="enterRoom()">Enter</button>
<button onclick="onCreateRoom(event)">Create</button>
<button onclick="onDestroyRoom(event)">Destory</button>
</div>
<div id="clientList"></div>
<div id="audio"></div>
<script>
var janus = null;
var audioRtc = null;
var webrtcUp = false;
var opaqueId = "audiobridgetest-"+Janus.randomString(12);
var roomNm = null;
var serverType = null;
var server = undefined;
console.log('> opaqueId:', opaqueId);
var userId = 'user' + randomNum(2);
var userIdObj = document.getElementById('userId');
userIdObj.innerHTML = userId;
function onDisconnect() {
if(!janus) {
alert("Not connected!");
return;
}
janus.destroy();
janus = null;
audioRtc = null;
}
function onConnect() {
if(!server) {
alert('Select a type of server!!!');
return;
}
// Janus 라이브러리 초기화
Janus.init({
debug: true,
dependencies: Janus.useDefaultDependencies(), // or: Janus.useOldDependencies() to get the behaviour of previous Janus versions
callback: function() {
// Janus 서버 연결
janus = new Janus({
server: server,
opaqueId: opaqueId,
success: function(ev) {
console.log("janus connect:");
janus.attach({
plugin: "janus.plugin.audiobridge",
success: function(pluginHandle) {
audioRtc = pluginHandle;
console.log('> pluginHandle:', pluginHandle);
Janus.log("Plugin attached! (" + audioRtc.getPlugin() + ", id=" + audioRtc.getId() + ")");
},
error: function(cause) {
console.error(" -- Error attaching plugin...", cause);
},
consentDialog: function(on) {
// e.g., Darken the screen if on=true (getUserMedia incoming), restore it otherwise
Janus.debug("Consent dialog should be " + (on ? "on" : "off") + " now");
},
iceState: function(state) {
Janus.log("ICE state changed to " + state);
},
mediaState: function(medium, on) {
Janus.log("Janus " + (on ? "started" : "stopped") + " receiving our " + medium);
},
webrtcState: function(on) {
Janus.log("Janus says our WebRTC PeerConnection is " + (on ? "up" : "down") + " now");
},
onmessage: function(msg, jsep) {
Janus.log(" ::: Got a message :::", msg);
var event = msg["audiobridge"];
if(event) {
//방입장 입장 Start
if(event === "joined") {
// Successfully joined, negotiate WebRTC now
if(msg["id"]) {
myid = msg["id"];
Janus.log("Successfully joined room " + msg["room"] + " with ID " + myid);
if(!webrtcUp) {
webrtcUp = true;
// Publish our stream
audioRtc.createOffer({
media: { video: false}, // This is an audio only room
success: function(jsep) {
Janus.debug("Got SDP!", jsep);
var publish = { request: "configure", muted: false };
audioRtc.send({ message: publish, jsep: jsep });
},
error: function(error) {
Janus.error("WebRTC error:", error);
}
});
}
}
// Any room participant?
if(msg["participants"]) {
var list = msg["participants"];
Janus.log("Got a list of participants:", list);
for(var n in list) {
if($('#audio__' + list[n].id).length === 0) {
$('#clientList').append("<div id='audio_" + list[n].id + "'>" + list[n].display + "</div>");
}
}
}
}
//방입장 입장 End
if(event === "event") {
if(0 < msg.error_code) {
Janus.log('ERROR:' + msg.error);
setEnterDisabled(false);
}
}
if(event === "destroyed") {
Janus.log('destroyed room:' + msg.room);
//$('#audio_' + msg.room).remove();
setEnterDisabled(false);
}
// Any new feed to attach to?
if(msg["leaving"]) {
// One of the participants has gone away?
var leaving = msg["leaving"];
Janus.log("Participant left: " + leaving + " (we have " + $('#rp'+leaving).length + " elements with ID #rp" +leaving + ")");
$('#audio_' + leaving).remove();
setEnterDisabled(false);
}
}
if(jsep) {
Janus.debug("Handling SDP as well...", jsep);
audioRtc.handleRemoteJsep({ jsep: jsep });
}
},
onlocalstream: function(stream) {
// We have a local stream (getUserMedia worked!) to display
console.log('> onlocalstream:', stream);
},
onremotestream: function(stream) {
// We have a remote stream (working PeerConnection!) to display
console.log('> onremotestream:', stream);
$('#audio').append('<audio class="rounded centered" id="roomaudio" width="100%" height="100%" autoplay/>');
Janus.attachMediaStream($('#roomaudio').get(0), stream);
},
oncleanup: function() {
// PeerConnection with the plugin closed, clean the UI
// The plugin handle is still valid so we can create a new one
console.log('> oncleanup');
setEnterDisabled(false);
},
detached: function() {
// Connection with the plugin closed, get rid of its features
// The plugin handle is not valid anymore
console.log('> detached');
setEnterDisabled(false);
}
});
},
error: function(cause) {
console.log("error", cause)
},
destroyed: function() {
console.log("destroyed")
}
});
}
});
}
function makeid(length) {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for ( var i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
function randomNum(length) {
var result = '';
var characters = '0123456789';
var charactersLength = characters.length;
for ( var i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
function setEnterDisabled(disabled) {
var enterBtn = document.getElementById('enterBtn');
enterBtn && (enterBtn.disabled = disabled);
var roomNm = document.getElementById('roomNm');
roomNm && (roomNm.disabled = disabled);
}
function onChangeRoom(ev) {
console.log('> onchageroom:', ev.value);
roomNm = parseInt(ev.value);
}
function onChangeType(ev) {
var serverType = ev.target.id;
if('type_ws' === serverType) {
server = 'wss://127.0.0.1:8989';
} else if('type_rest' === serverType) {
server = 'https://127.0.0.1:8089/janus';
} else {
alsert('unknown Server Type: ' + id);
}
}
var pin = 'bar';
var admin_key = 'supersecret';
var secret = 'foo';
function enterRoom() {
if(!audioRtc) {
alert("Not connected!");
return;
}
var register = { request: "join", room: roomNm, pin:pin, display: userId };
audioRtc.send({ message: register});
setEnterDisabled(true);
}
function onCreateRoom(ev) {
console.log('> onCreateeRoom:', roomNm);
if(!audioRtc) {
alert("Not connected!");
return;
}
audioRtc.send({ message: {"request": "create", "room": roomNm, "secret": secret, "pin":pin, "is_private":true, "admin_key":admin_key}});
}
function onDestroyRoom(ev) {
console.log('> onDestroyRoom:', roomNm);
if(!audioRtc) {
alert("Not connected!");
return;
}
audioRtc.send({message: {"request": "destroy", "room": roomNm,"secret": secret}});
}
</script>
</body>
참고
반응형
'3.구현 > HTML5&Javascript' 카테고리의 다른 글
날씨 바람 (0) | 2022.05.24 |
---|---|
[dat.gui] dat.gui를 사용한 간단한 설정 관리 (0) | 2022.05.04 |
[HTML] 간단한 로딩화면 스피너 만들기 (0) | 2022.01.25 |
Javascript XLSX 파일 읽기 (0) | 2021.12.24 |
[node.js] 단위테스트 사용하기 (0) | 2021.11.15 |