diff --git a/Makefile.am b/Makefile.am index e28142b44b..2786c391b1 100644 --- a/Makefile.am +++ b/Makefile.am @@ -39,12 +39,16 @@ html_DATA = README.md streamdir = $(datadir)/janus/streams stream_DATA = $(NULL) +recordingsdir = $(datadir)/janus/recordings +recordings_DATA = $(NULL) + %.sample: %.sample.in $(MKDIR_P) $(@D) $(AM_V_GEN) sed -e "\ s|[@]confdir[@]|$(confdir)|;\ s|[@]certdir[@]|$(certdir)|;\ s|[@]plugindir[@]|$(plugindir)|;\ + s|[@]recordingsdir[@]|$(recordingsdir)|;\ s|[@]streamdir[@]|$(streamdir)|" \ $< > $@ || rm $@ @@ -146,6 +150,23 @@ conf_DATA += conf/janus.plugin.echotest.cfg.sample EXTRA_DIST += conf/janus.plugin.echotest.cfg.sample endif +if ENABLE_PLUGIN_RECORDPLAY +plugin_LTLIBRARIES += plugins/libjanus_recordplay.la +plugins_libjanus_recordplay_la_SOURCES = plugins/janus_recordplay.c +plugins_libjanus_recordplay_la_CFLAGS = $(plugins_cflags) +plugins_libjanus_recordplay_la_LDFLAGS = $(plugins_ldflags) +plugins_libjanus_recordplay_la_LIBADD = $(plugins_libadd) +conf_DATA += conf/janus.plugin.recordplay.cfg.sample +recordings_DATA += \ + plugins/recordings/1234.nfo \ + plugins/recordings/rec-sample-audio.mjr \ + plugins/recordings/rec-sample-video.mjr +EXTRA_DIST += \ + conf/janus.plugin.recordplay.cfg.sample.in \ + $(recordings_DATA) +CLEANFILES += conf/janus.plugin.recordplay.cfg.sample +endif + if ENABLE_PLUGIN_SIP plugin_LTLIBRARIES += plugins/libjanus_sip.la plugins_libjanus_sip_la_SOURCES = plugins/janus_sip.c diff --git a/apierror.c b/apierror.c index 6165fbc433..2ba7306c3f 100644 --- a/apierror.c +++ b/apierror.c @@ -46,6 +46,8 @@ const char *janus_get_api_error(int error) { return "Invalid element type"; case JANUS_ERROR_SESSION_CONFLICT: return "Session ID already in use"; + case JANUS_ERROR_UNEXPECTED_ANSWER: + return "Unexpected ANSWER (no OFFER)"; default: return "Unknown error"; } diff --git a/apierror.h b/apierror.h index d506870a87..5ece5799d2 100644 --- a/apierror.h +++ b/apierror.h @@ -59,6 +59,8 @@ #define JANUS_ERROR_INVALID_ELEMENT_TYPE 467 /*! \brief The ID provided to create a new session is already in use */ #define JANUS_ERROR_SESSION_CONFLICT 468 +/*! \brief We got an ANSWER to an OFFER we never made */ +#define JANUS_ERROR_UNEXPECTED_ANSWER 469 /*! \brief Helper method to get a string representation of an API error code diff --git a/conf/janus.cfg.sample.in b/conf/janus.cfg.sample.in index 0d2a0a6921..b510ed5f5f 100644 --- a/conf/janus.cfg.sample.in +++ b/conf/janus.cfg.sample.in @@ -93,11 +93,13 @@ cert_pem = @certdir@/mycert.pem cert_key = @certdir@/mycert.key -; Media-related stuff: right now, you can only configure the range of +; Media-related stuff: right now, you can only configure whether you want +; to enable IPv6 support (still WIP, so handle with care) and the range of ; ports to use for RTP and RTCP (by default, no range is envisaged). -; If you configure a range in the lines below, remember to uncomment the -; [media] category as well! +; If you change any setting in the lines below, remember to uncomment the +; [media] category as well, which is commented by default! ;[media] +;ipv6 = true ;rtp_port_range = 20000-40000 diff --git a/conf/janus.plugin.recordplay.cfg.sample.in b/conf/janus.plugin.recordplay.cfg.sample.in new file mode 100644 index 0000000000..159bb8c939 --- /dev/null +++ b/conf/janus.plugin.recordplay.cfg.sample.in @@ -0,0 +1,4 @@ +; path = where to place recordings in the file system + +[general] +path = @recordingsdir@ diff --git a/configure.ac b/configure.ac index a1f77ffa94..17870a70bb 100644 --- a/configure.ac +++ b/configure.ac @@ -114,7 +114,7 @@ AC_CHECK_PROG([DOT], AS_IF([test -z "$DOXYGEN" -o -z "$DOT"], [ AS_IF([test "x$enable_docs" = "xyes"], - [AC_MSG_ERROR([doxygen or dot not found. Docs cannot be built.])]) + [AC_MSG_NOTICE([doxygen or dot not found. Docs cannot be built.])]) ]) AM_CONDITIONAL([ENABLE_DOCS], [test "x$enable_docs" = "xyes"]) @@ -142,6 +142,12 @@ AC_ARG_ENABLE([plugin-echotest], [], [enable_plugin_echotest=yes]) +AC_ARG_ENABLE([plugin-recordplay], + [AS_HELP_STRING([--disable-plugin-recordplay], + [Disable record&play plugin])], + [], + [enable_plugin_recordplay=yes]) + AC_ARG_ENABLE([plugin-sip], [AS_HELP_STRING([--disable-plugin-sip], [Disable sip plugin])], @@ -190,6 +196,7 @@ PKG_CHECK_MODULES([OGG], AM_CONDITIONAL([ENABLE_PLUGIN_AUDIOBRIDGE], [test "x$enable_plugin_audiobridge" = "xyes"]) AM_CONDITIONAL([ENABLE_PLUGIN_ECHOTEST], [test "x$enable_plugin_echotest" = "xyes"]) +AM_CONDITIONAL([ENABLE_PLUGIN_RECORDPLAY], [test "x$enable_plugin_recordplay" = "xyes"]) AM_CONDITIONAL([ENABLE_PLUGIN_SIP], [test "x$enable_plugin_sip" = "xyes"]) AM_CONDITIONAL([ENABLE_PLUGIN_STREAMING], [test "x$enable_plugin_streaming" = "xyes"]) AM_CONDITIONAL([ENABLE_PLUGIN_VIDEOCALL], [test "x$enable_plugin_videocall" = "xyes"]) diff --git a/dtls.c b/dtls.c index c205d7e739..cbf1588689 100644 --- a/dtls.c +++ b/dtls.c @@ -439,6 +439,7 @@ void janus_dtls_srtp_incoming_msg(janus_dtls_srtp *dtls, char *buf, uint16_t len } else { /* Something went wrong in either DTLS or SRTP... tell the plugin about it */ janus_dtls_callback(dtls->ssl, SSL_CB_ALERT, 0); + janus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_CLEANING); } } } @@ -509,7 +510,7 @@ void janus_dtls_callback(const SSL *ssl, int where, int ret) { return; } janus_ice_handle *handle = stream->handle; - if(!stream) { + if(!handle) { JANUS_LOG(LOG_ERR, "No ICE handle related to this alert...\n"); return; } @@ -519,6 +520,7 @@ void janus_dtls_callback(const SSL *ssl, int where, int ret) { return; } JANUS_LOG(LOG_VERB, "[%"SCNu64"] DTLS alert received on stream %"SCNu16", closing...\n", handle->handle_id, stream->stream_id); + janus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_CLEANING); if(!janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT)) { janus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT); if(handle->iceloop) diff --git a/html/admin.html b/html/admin.html index 6f831d931f..5bb1e410bc 100644 --- a/html/admin.html +++ b/html/admin.html @@ -41,9 +41,10 @@
  • Video MCU
  • Audio Conference
  • Voice Mail
  • +
  • Recorder/Playout
  • Screen Sharing
  • -
  • Admin/Monitor
  • +
  • Admin/Monitor
  • Documentation
  • diff --git a/html/audiobridgetest.html b/html/audiobridgetest.html index c95fe25fe7..42a8cbd96a 100644 --- a/html/audiobridgetest.html +++ b/html/audiobridgetest.html @@ -43,6 +43,7 @@
  • Video MCU
  • Audio Conference
  • Voice Mail
  • +
  • Recorder/Playout
  • Screen Sharing
  • Admin/Monitor
  • diff --git a/html/demos.html b/html/demos.html index 97f85d2ea1..fc94a2317c 100644 --- a/html/demos.html +++ b/html/demos.html @@ -38,6 +38,7 @@
  • Video MCU
  • Audio Conference
  • Voice Mail
  • +
  • Recorder/Playout
  • Screen Sharing
  • Admin/Monitor
  • diff --git a/html/docs/index.html b/html/docs/index.html index 3b2e7ba14d..87095509b8 100644 --- a/html/docs/index.html +++ b/html/docs/index.html @@ -38,6 +38,7 @@
  • Video MCU
  • Audio Conference
  • Voice Mail
  • +
  • Recorder/Playout
  • Screen Sharing
  • Admin/Monitor
  • diff --git a/html/echotest.html b/html/echotest.html index 4ae157333a..308d274a2c 100644 --- a/html/echotest.html +++ b/html/echotest.html @@ -43,6 +43,7 @@
  • Video MCU
  • Audio Conference
  • Voice Mail
  • +
  • Recorder/Playout
  • Screen Sharing
  • Admin/Monitor
  • diff --git a/html/index.html b/html/index.html index 09fefbfd68..0830379c26 100644 --- a/html/index.html +++ b/html/index.html @@ -38,6 +38,7 @@
  • Video MCU
  • Audio Conference
  • Voice Mail
  • +
  • Recorder/Playout
  • Screen Sharing
  • Admin/Monitor
  • diff --git a/html/janus.js b/html/janus.js index c7a310b633..28a0dadf6f 100644 --- a/html/janus.js +++ b/html/janus.js @@ -119,6 +119,9 @@ function Janus(gatewayCallbacks) { var iceServers = gatewayCallbacks.iceServers; if(iceServers === undefined || iceServers === null) iceServers = [{"url": "stun:stun.l.google.com:19302"}]; + var ipv6Support = gatewayCallbacks.ipv6; + if(ipv6Support === undefined || ipv6Support === null) + ipv6Support = false; var maxev = null; if(gatewayCallbacks.max_poll_events !== undefined && gatewayCallbacks.max_poll_events !== null) maxev = gatewayCallbacks.max_poll_events; @@ -873,6 +876,11 @@ function Janus(gatewayCallbacks) { var pc_constraints = { "optional": [{"DtlsSrtpKeyAgreement": true}] }; + if(ipv6Support === true) { + // FIXME This is only supported in Chrome right now + // For support in Firefox track this: https://bugzilla.mozilla.org/show_bug.cgi?id=797262 + pc_constraints.optional.push({"googIPv6":true}); + } Janus.log("Creating PeerConnection:"); Janus.log(pc_constraints); config.pc = new RTCPeerConnection(pc_config, pc_constraints); @@ -1168,7 +1176,7 @@ function Janus(gatewayCallbacks) { } } // If we got here, we're not screensharing - if(media.video !== 'screen') { + if(media === null || media === undefined || media.video !== 'screen') { getUserMedia( {audio:isAudioSendEnabled(media), video:videoSupport}, function(stream) { pluginHandle.consentDialog(false); streamsDone(handleId, jsep, media, callbacks, stream); }, diff --git a/html/recordplaytest.html b/html/recordplaytest.html new file mode 100644 index 0000000000..70c68c5db0 --- /dev/null +++ b/html/recordplaytest.html @@ -0,0 +1,137 @@ + + + + + + +Janus WebRTC Gateway: Recorder/Playout Demo + + + + + + + + + + + + +Fork me on GitHub + + + +
    +
    +
    + +
    +
    +
    +

    Demo details

    +

    This demo shows how you can record a WebRTC session, and replay it later. You + can choose to either record a new session (e.g., a videomessage) or watch any of + the recordings that may be available (including those you made yourself).

    +

    This application makes use of the integrated recording feature in Janus, + specifically the individual recording of audio and video streams in .mjr + format: these individual recordings are then used for a live broadcasting + of the dumped RTP packets through a sendonly WebRTC PeerConnection + when you choose to replay them. To post-process these recordings in a + more usable format (e.g., .webm for video or .opus + for audio) you can make use of the janus-pp-rec tool that + is available as part of the Janus code.

    +

    Press the Start button above to launch the demo.

    +
    +
    +
    +
    +
    +
    +
    +
    +

    Recorder/Playout

    +
    +
    +
    + + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +

    Remote Video

    +
    +
    +
    +
    +
    +
    +
    +
    + +
    + +
    + + + diff --git a/html/recordplaytest.js b/html/recordplaytest.js new file mode 100644 index 0000000000..9c7601ebe8 --- /dev/null +++ b/html/recordplaytest.js @@ -0,0 +1,371 @@ +// We make use of this 'server' variable to provide the address of the +// REST Janus API. By default, in this example we assume that Janus is +// co-located with the web server hosting the HTML pages but listening +// on a different port (8088, the default for HTTP in Janus), which is +// why we make use of the 'window.location.hostname' base address. Since +// Janus can also do HTTPS, and considering we don't really want to make +// use of HTTP for Janus if your demos are served on HTTPS, we also rely +// on the 'window.location.protocol' prefix to build the variable, in +// particular to also change the port used to contact Janus (8088 for +// HTTP and 8089 for HTTPS, if enabled). +// In case you place Janus behind an Apache frontend (as we did on the +// online demos at http://janus.conf.meetecho.com) you can just use a +// relative path for the variable, e.g.: +// +// var server = "/janus"; +// +// which will take care of this on its own. +// +// +// If you want to use the WebSockets frontend to Janus, instead, you'll +// have to pass a different kind of address, e.g.: +// +// var server = "ws://" + window.location.hostname + ":8188"; +// +// Of course this assumes that support for WebSockets has been built in +// when compiling the gateway. WebSockets support has not been tested +// as much as the REST API, so handle with care! +// +// +// If you have multiple options available, and want to let the library +// autodetect the best way to contact your gateway (or pool of gateways), +// you can also pass an array of servers, e.g., to provide alternative +// means of access (e.g., try WebSockets first and, if that fails, fall +// back to plain HTTP) or just have failover servers: +// +// var server = [ +// "ws://" + window.location.hostname + ":8188", +// "/janus" +// ]; +// +// This will tell the library to try connecting to each of the servers +// in the presented order. The first working server will be used for +// the whole session. +// +var server = null; +if(window.location.protocol === 'http:') + server = "http://" + window.location.hostname + ":8088/janus"; +else + server = "https://" + window.location.hostname + ":8089/janus"; + +var janus = null; +var recordplay = null; +var started = false; +var spinner = null; + +var myname = null; +var recording = false; +var playing = false; +var recordingId = null; +var selectedRecording = null; +var selectedRecordingInfo = null; + + +$(document).ready(function() { + // Initialize the library (console debug enabled) + Janus.init({debug: true, callback: function() { + // Use a button to start the demo + $('#start').click(function() { + if(started) + return; + started = true; + $(this).attr('disabled', true).unbind('click'); + // Make sure the browser supports WebRTC + if(!Janus.isWebrtcSupported()) { + bootbox.alert("No WebRTC support... "); + return; + } + // Create session + janus = new Janus( + { + server: server, + success: function() { + // Attach to echo test plugin + janus.attach( + { + plugin: "janus.plugin.recordplay", + success: function(pluginHandle) { + $('#details').remove(); + recordplay = pluginHandle; + console.log("Plugin attached! (" + recordplay.getPlugin() + ", id=" + recordplay.getId() + ")"); + // Prepare the name prompt + $('#recordplay').removeClass('hide').show(); + $('#start').removeAttr('disabled').html("Stop") + .click(function() { + $(this).attr('disabled', true); + janus.destroy(); + }); + updateRecsList(); + }, + error: function(error) { + console.log(" -- Error attaching plugin... " + error); + bootbox.alert(" -- Error attaching plugin... " + error); + }, + consentDialog: function(on) { + console.log("Consent dialog should be " + (on ? "on" : "off") + " now"); + if(on) { + // Darken screen and show hint + $.blockUI({ + message: '
    ', + css: { + border: 'none', + padding: '15px', + backgroundColor: 'transparent', + color: '#aaa', + top: '10px', + left: (navigator.mozGetUserMedia ? '-100px' : '300px') + } }); + } else { + // Restore screen + $.unblockUI(); + } + }, + onmessage: function(msg, jsep) { + console.log(" ::: Got a message :::"); + console.log(JSON.stringify(msg)); + var result = msg["result"]; + if(result !== null && result !== undefined) { + if(result["status"] !== undefined && result["status"] !== null) { + var event = result["status"]; + if(event === 'preparing') { + console.log("Preparing the recording playout"); + recordplay.createAnswer( + { + jsep: jsep, + media: { audioSend: false, videoSend: false }, // We want recvonly audio/video + success: function(jsep) { + console.log("Got SDP!"); + console.log(jsep); + var body = { "request": "start" }; + recordplay.send({"message": body, "jsep": jsep}); + }, + error: function(error) { + console.log("WebRTC error:"); + console.log(error); + bootbox.alert("WebRTC error... " + JSON.stringify(error)); + } + }); + } else if(event === 'recording') { + // Got an ANSWER to our recording OFFER + if(jsep !== null && jsep !== undefined) + recordplay.handleRemoteJsep({jsep: jsep}); + var id = result["id"]; + if(id !== null && id !== undefined) { + console.log("The ID of the current recording is " + id); + recordingId = id; + } + } else if(event === 'playing') { + console.log("Playout has started!"); + } else if(event === 'stopped') { + console.log("Session has stopped!"); + var id = result["id"]; + if(recordingId !== null && recordingId !== undefined) { + if(recordingId !== id) { + console.log("Not a stop to our recording?"); + return; + } + bootbox.alert("Recording completed! Check the list of recordings to replay it."); + } + if(selectedRecording !== null && selectedRecording !== undefined) { + if(selectedRecording !== id) { + console.log("Not a stop to our playout?"); + return; + } + } + // TODO Reset status + $('#videobox').empty(); + $('#video').hide(); + recordingId = null; + recording = false; + playing = false; + recordplay.hangup(); + $('#record').removeAttr('disabled').click(startRecording); + $('#play').removeAttr('disabled').click(startPlayout); + $('#list').removeAttr('disabled').click(updateRecsList); + $('#recset').removeAttr('disabled'); + $('#recslist').removeAttr('disabled'); + updateRecsList(); + } + } + } else { + // FIXME Error? + var error = msg["error"]; + bootbox.alert(error); + // TODO Reset status + $('#videobox').empty(); + $('#video').hide(); + recording = false; + playing = false; + recordplay.hangup(); + $('#record').removeAttr('disabled').click(startRecording); + $('#play').removeAttr('disabled').click(startPlayout); + $('#list').removeAttr('disabled').click(updateRecsList); + $('#recset').removeAttr('disabled'); + $('#recslist').removeAttr('disabled'); + updateRecsList(); + } + }, + onlocalstream: function(stream) { + if(playing === true) + return; + console.log(" ::: Got a local stream :::"); + console.log(JSON.stringify(stream)); + $('#videotitle').html("Recording..."); + $('#stop').unbind('click').click(stop); + $('#video').removeClass('hide').show(); + if($('#thevideo').length === 0) + $('#videobox').append('