본문 바로가기

3.구현/HTML5&Javascript

[webrtc] Janus API 활용

들어가기

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" : "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>

참고

[1] https://janus.conf.meetecho.com/docs/rest.html

반응형