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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
+
+
+
+ Record
+ Play
+ Update
+
+
+
+
+
+ Recordings list
+
+
+
+
+
+
+
+
+
+
+
Remote Video Stop
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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(' ');
+ attachMediaStream($('#thevideo').get(0), stream);
+ $("#thevideo").get(0).muted = "muted";
+ },
+ onremotestream: function(stream) {
+ if(playing === false)
+ return;
+ console.log(" ::: Got a remote stream :::");
+ console.log(JSON.stringify(stream));
+ $('#videotitle').html(selectedRecordingInfo);
+ $('#stop').unbind('click').click(stop);
+ $('#video').removeClass('hide').show();
+ if($('#thevideo').length === 0)
+ $('#videobox').append(' ');
+ attachMediaStream($('#thevideo').get(0), stream);
+ },
+ oncleanup: function() {
+ console.log(" ::: Got a cleanup notification :::");
+ // TODO Reset status
+ $('#videobox').empty();
+ $('#video').hide();
+ recording = false;
+ playing = false;
+ $('#record').removeAttr('disabled').click(startRecording);
+ $('#play').removeAttr('disabled').click(startPlayout);
+ $('#list').removeAttr('disabled').click(updateRecsList);
+ $('#recset').removeAttr('disabled');
+ $('#recslist').removeAttr('disabled');
+ updateRecsList();
+ }
+ });
+ },
+ error: function(error) {
+ console.log(error);
+ bootbox.alert(error, function() {
+ window.location.reload();
+ });
+ },
+ destroyed: function() {
+ window.location.reload();
+ }
+ });
+ });
+ }});
+});
+
+function checkEnter(field, event) {
+ var theCode = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;
+ if(theCode == 13) {
+ if(field.id == 'name')
+ insertName();
+ return false;
+ } else {
+ return true;
+ }
+}
+
+function updateRecsList() {
+ $('#list').unbind('click');
+ $('#update-list').addClass('fa-spin');
+ var body = { "request": "list" };
+ console.log("Sending message (" + JSON.stringify(body) + ")");
+ recordplay.send({"message": body, success: function(result) {
+ setTimeout(function() {
+ $('#list').click(updateRecsList);
+ $('#update-list').removeClass('fa-spin');
+ }, 500);
+ if(result === null || result === undefined) {
+ bootbox.alert("Got no response to our query for available recordings");
+ return;
+ }
+ if(result["list"] !== undefined && result["list"] !== null) {
+ $('#recslist').empty();
+ $('#record').removeAttr('disabled').click(startRecording);
+ $('#list').removeAttr('disabled').click(updateRecsList);
+ var list = result["list"];
+ console.log("Got a list of available recordings:");
+ console.log(list);
+ for(var mp in list) {
+ console.log(" >> [" + list[mp]["id"] + "] " + list[mp]["name"] + " (" + list[mp]["date"] + ")");
+ $('#recslist').append("" + list[mp]["name"] + " [" + list[mp]["date"] + "]" + " ");
+ }
+ $('#recslist a').unbind('click').click(function() {
+ selectedRecording = $(this).attr("id");
+ selectedRecordingInfo = $(this).text();
+ $('#recset').html($(this).html()).parent().removeClass('open');
+ $('#play').removeAttr('disabled').click(startPlayout);
+ return false;
+
+ });
+ }
+ }});
+}
+
+function startRecording() {
+ if(recording)
+ return;
+ // Start a recording
+ recording = true;
+ playing = false;
+ bootbox.prompt("Insert a name for the recording (e.g., John Smith says hello)", function(result) {
+ if(result === null || result === undefined) {
+ recording = false;
+ return;
+ }
+ myname = result;
+ $('#record').unbind('click').attr('disabled', true);
+ $('#play').unbind('click').attr('disabled', true);
+ $('#list').unbind('click').attr('disabled', true);
+ $('#recset').attr('disabled', true);
+ $('#recslist').attr('disabled', true);
+ recordplay.createOffer(
+ {
+ // By default, it's sendrecv for audio and video...
+ success: function(jsep) {
+ console.log("Got SDP!");
+ console.log(jsep);
+ var body = { "request": "record", "name": myname };
+ recordplay.send({"message": body, "jsep": jsep});
+ },
+ error: function(error) {
+ console.log("WebRTC error... " + error);
+ bootbox.alert("WebRTC error... " + error);
+ recordplay.hangup();
+ }
+ });
+ });
+}
+
+function startPlayout() {
+ if(playing)
+ return;
+ // Start a playout
+ recording = false;
+ playing = true;
+ if(selectedRecording === undefined || selectedRecording === null) {
+ playing = false;
+ return;
+ }
+ $('#record').unbind('click').attr('disabled', true);
+ $('#play').unbind('click').attr('disabled', true);
+ $('#list').unbind('click').attr('disabled', true);
+ $('#recset').attr('disabled', true);
+ $('#recslist').attr('disabled', true);
+ var play = { "request": "play", "id": parseInt(selectedRecording) };
+ recordplay.send({"message": play});
+}
+
+function stop() {
+ // Stop a recording/playout
+ $('#stop').unbind('click');
+ var stop = { "request": "stop" };
+ recordplay.send({"message": stop});
+ recordplay.hangup();
+}
diff --git a/html/screensharingtest.html b/html/screensharingtest.html
index e5fdcf11c3..b5a1d5701d 100644
--- a/html/screensharingtest.html
+++ b/html/screensharingtest.html
@@ -43,6 +43,7 @@
Video MCU
Audio Conference
Voice Mail
+ Recorder/Playout
Screen Sharing
Admin/Monitor
diff --git a/html/screensharingtest.js b/html/screensharingtest.js
index fb945ee28f..83a4c8dc8c 100644
--- a/html/screensharingtest.js
+++ b/html/screensharingtest.js
@@ -291,17 +291,17 @@ function shareScreen() {
role = "publisher";
var create = { "request": "create", "description": desc, "bitrate": 0, "publishers": 1 };
screentest.send({"message": create, success: function(result) {
- var event = result["videoroom"];
- console.log("Event: " + event);
- if(event != undefined && event != null) {
+ var event = result["videoroom"];
+ console.log("Event: " + event);
+ if(event != undefined && event != null) {
// Our own screen sharing session has been created, join it
room = result["room"];
console.log("Screen sharing session created: " + room);
myusername = randomString(12);
var register = { "request": "join", "room": room, "ptype": "publisher", "display": myusername };
screentest.send({"message": register});
- }
- }});
+ }
+ }});
}
function checkEnterJoin(field, event) {
diff --git a/html/siptest.html b/html/siptest.html
index ce30c43b49..82e42d770c 100644
--- a/html/siptest.html
+++ b/html/siptest.html
@@ -43,6 +43,7 @@
Video MCU
Audio Conference
Voice Mail
+ Recorder/Playout
Screen Sharing
Admin/Monitor
diff --git a/html/streamingtest.html b/html/streamingtest.html
index f3bac65425..9f5af11300 100644
--- a/html/streamingtest.html
+++ b/html/streamingtest.html
@@ -42,6 +42,7 @@
Video MCU
Audio Conference
Voice Mail
+ Recorder/Playout
Screen Sharing
Admin/Monitor
diff --git a/html/videocalltest.html b/html/videocalltest.html
index 6d7bf3a413..ac1bd34584 100644
--- a/html/videocalltest.html
+++ b/html/videocalltest.html
@@ -43,6 +43,7 @@
Video MCU
Audio Conference
Voice Mail
+ Recorder/Playout
Screen Sharing
Admin/Monitor
diff --git a/html/videomcutest.html b/html/videomcutest.html
index 3ec1e32e11..4f996b3863 100644
--- a/html/videomcutest.html
+++ b/html/videomcutest.html
@@ -42,6 +42,7 @@
Video MCU
Audio Conference
Voice Mail
+ Recorder/Playout
Screen Sharing
Admin/Monitor
diff --git a/html/voicemailtest.html b/html/voicemailtest.html
index f2cd33366f..22734c3009 100644
--- a/html/voicemailtest.html
+++ b/html/voicemailtest.html
@@ -42,6 +42,7 @@
Video MCU
Audio Conference
Voice Mail
+ Recorder/Playout
Screen Sharing
Admin/Monitor
diff --git a/ice.c b/ice.c
index 70b3b48923..99355424ec 100644
--- a/ice.c
+++ b/ice.c
@@ -40,6 +40,13 @@ uint16_t janus_ice_get_stun_port(void) {
}
+/* IPv6 support (still mostly WIP) */
+static gboolean janus_ipv6_enabled;
+gboolean janus_ice_is_ipv6_enabled(void) {
+ return janus_ipv6_enabled;
+}
+
+
/* Interface/IP ignore list */
GList *janus_ice_ignore_list = NULL;
janus_mutex ignore_list_mutex;
@@ -99,7 +106,9 @@ gboolean janus_is_rtcp(gchar *buf) {
/* libnice initialization */
-gint janus_ice_init(gchar *stun_server, uint16_t stun_port, uint16_t rtp_min_port, uint16_t rtp_max_port) {
+gint janus_ice_init(gchar *stun_server, uint16_t stun_port, uint16_t rtp_min_port, uint16_t rtp_max_port, gboolean ipv6) {
+ janus_ipv6_enabled = ipv6;
+ JANUS_LOG(LOG_INFO, "Initializing ICE stuff (IPv6 candidates %s)\n", janus_ipv6_enabled ? "enabled" : "disabled");
if(stun_server == NULL)
return 0; /* No initialization needed */
if(stun_port == 0)
@@ -440,7 +449,22 @@ void janus_ice_webrtc_free(janus_ice_handle *handle) {
g_free(handle->remote_sdp);
handle->remote_sdp = NULL;
}
+ if(handle->queued_packets != NULL) {
+ janus_ice_queued_packet *pkt = NULL;
+ while(g_async_queue_length(handle->queued_packets) > 0) {
+ pkt = g_async_queue_try_pop(handle->queued_packets);
+ if(pkt != NULL) {
+ g_free(pkt->data);
+ pkt->data = NULL;
+ g_free(pkt);
+ pkt = NULL;
+ }
+ }
+ g_async_queue_unref(handle->queued_packets);
+ handle->queued_packets = NULL;
+ }
janus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_READY);
+ janus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_CLEANING);
janus_mutex_unlock(&handle->mutex);
JANUS_LOG(LOG_INFO, "[%"SCNu64"] WebRTC resources freed\n", handle->handle_id);
}
@@ -813,8 +837,13 @@ void *janus_ice_thread(void *data) {
janus_ice_handle *handle = data;
JANUS_LOG(LOG_VERB, "[%"SCNu64"] ICE thread started, looping...\n", handle->handle_id);
GMainLoop *loop = handle->iceloop;
+ if(loop == NULL) {
+ JANUS_LOG(LOG_ERR, "[%"SCNu64"] Invalid loop...\n", handle->handle_id);
+ return NULL;
+ }
g_usleep (100000);
g_main_loop_run (loop);
+ janus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_CLEANING);
if(handle->cdone == 0)
handle->cdone = -1;
JANUS_LOG(LOG_VERB, "[%"SCNu64"] ICE thread ended!\n", handle->handle_id);
@@ -993,6 +1022,7 @@ int janus_ice_setup_local(janus_ice_handle *handle, int offer, int audio, int vi
janus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_READY);
janus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_STOP);
janus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT);
+ janus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_CLEANING);
/* Note: in case this is not an OFFER, we don't know whether DataChannels are supported on the other side or not yet */
if(data) {
@@ -1059,8 +1089,8 @@ int janus_ice_setup_local(janus_ice_handle *handle, int offer, int audio, int vi
family = ifa->ifa_addr->sa_family;
if(family != AF_INET && family != AF_INET6)
continue;
- /* FIXME We skip IPv6 addresses for now: browser don't negotiate them yet anyway */
- if(family == AF_INET6)
+ /* We only add IPv6 addresses if support for them has been explicitly enabled (still WIP, mostly) */
+ if(family == AF_INET6 && !janus_ipv6_enabled)
continue;
/* Check the interface name first: we can ignore that as well */
if(ifa->ifa_name != NULL && janus_ice_is_ignored(ifa->ifa_name))
@@ -1072,8 +1102,9 @@ int janus_ice_setup_local(janus_ice_handle *handle, int offer, int audio, int vi
JANUS_LOG(LOG_ERR, "getnameinfo() failed: %s\n", gai_strerror(s));
continue;
}
- /* Skip localhost */
- if(!strcmp(host, "127.0.0.1") || !strcmp(host, "::1") || !strcmp(host, "0.0.0.0"))
+ /* Skip localhost and 0.0.0.0 */
+ if(!strcmp(host, "127.0.0.1") || !strcmp(host, "0.0.0.0")
+ || !strcmp(host, "::1") || !strcmp(host, "::") || strchr(host, '%'))
continue;
/* Check if this IP address is in the ignore list, now */
if(janus_ice_is_ignored(host))
@@ -1082,7 +1113,10 @@ int janus_ice_setup_local(janus_ice_handle *handle, int offer, int audio, int vi
JANUS_LOG(LOG_VERB, "Adding %s to the addresses to gather candidates for\n", host);
NiceAddress addr_local;
nice_address_init (&addr_local);
- nice_address_set_from_string (&addr_local, host);
+ if(!nice_address_set_from_string (&addr_local, host)) {
+ JANUS_LOG(LOG_WARN, "Skipping invalid address %s\n", host);
+ continue;
+ }
nice_agent_add_local_address (handle->agent, &addr_local);
}
freeifaddrs(ifaddr);
diff --git a/ice.h b/ice.h
index 30a4baa28a..6faea4da11 100644
--- a/ice.h
+++ b/ice.h
@@ -31,8 +31,9 @@
* @param[in] stun_port STUN port to use, if any
* @param[in] rtp_min_port Minimum port to use for RTP/RTCP, if a range is to be used
* @param[in] rtp_max_port Maximum port to use for RTP/RTCP, if a range is to be used
+ * @param[in] ipv6 Whether IPv6 candidates must be negotiated or not
* @returns 0 in case of success, a negative integer on errors */
-gint janus_ice_init(gchar *stun_server, uint16_t stun_port, uint16_t rtp_min_port, uint16_t rtp_max_port);
+gint janus_ice_init(gchar *stun_server, uint16_t stun_port, uint16_t rtp_min_port, uint16_t rtp_max_port, gboolean ipv6);
/*! \brief Method to get the STUN server IP address
* @returns The currently used STUN server IP address, if available, or NULL if not */
char *janus_ice_get_stun_server(void);
@@ -49,6 +50,9 @@ void janus_ice_ignore_interface(const char *ip);
* @param[in] ip Interface/IP to check (e.g., 192.168.244.1 or eth1)
* @returns true if the interface/IP is in the ignore list, false otherwise */
gboolean janus_ice_is_ignored(const char *ip);
+/*! \brief Method to check whether IPv6 candidates are enabled/supported or not (still WIP)
+ * @returns true if IPv6 candidates are enabled/supported, false otherwise */
+gboolean janus_ice_is_ipv6_enabled(void);
/*! \brief Helper method to get a string representation of a libnice ICE state
@@ -79,6 +83,7 @@ typedef struct janus_ice_queued_packet janus_ice_queued_packet;
#define JANUS_ICE_HANDLE_WEBRTC_TRICKLE_SYNCED (1 << 9)
#define JANUS_ICE_HANDLE_WEBRTC_DATA_CHANNELS (1 << 10)
#define JANUS_ICE_HANDLE_WEBRTC_PLAN_B (1 << 11)
+#define JANUS_ICE_HANDLE_WEBRTC_CLEANING (1 << 12)
/*! \brief Janus ICE handle */
diff --git a/janus.c b/janus.c
index 3f0779e4dc..f226d8ed5b 100644
--- a/janus.c
+++ b/janus.c
@@ -340,6 +340,8 @@ gint janus_session_destroy(guint64 session_id) {
}
/* TODO Actually destroy session */
+ //~ janus_session_free(session);
+
return 0;
}
@@ -1068,6 +1070,20 @@ int janus_process_incoming_request(janus_request_source *source, json_t *root) {
goto jsondone;
}
type = NULL;
+ /* Are we still cleaning up from a previous media session? */
+ if(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_CLEANING)) {
+ JANUS_LOG(LOG_INFO, "[%"SCNu64"] Still cleaning up from a previous media session, let's wait a bit...\n", handle->handle_id);
+ gint64 waited = 0;
+ while(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_CLEANING)) {
+ JANUS_LOG(LOG_VERB, "[%"SCNu64"] Still cleaning up from a previous media session, let's wait a bit...\n", handle->handle_id);
+ g_usleep(100000);
+ waited += 100000;
+ if(waited >= 3*G_USEC_PER_SEC) {
+ JANUS_LOG(LOG_VERB, "[%"SCNu64"] -- Waited 3 seconds, that's enough!\n", handle->handle_id);
+ break;
+ }
+ }
+ }
/* Check the JSEP type */
int offer = 0;
if(!strcasecmp(jsep_type, "offer")) {
@@ -1110,15 +1126,15 @@ int janus_process_incoming_request(janus_request_source *source, json_t *root) {
/* FIXME We're only handling single audio/video lines for now... */
JANUS_LOG(LOG_VERB, "[%"SCNu64"] Audio %s been negotiated\n", handle->handle_id, audio ? "has" : "has NOT");
if(audio > 1) {
- JANUS_LOG(LOG_ERR, "[%"SCNu64"] More than one audio line? only going to negotiate one...\n", handle->handle_id);
+ JANUS_LOG(LOG_WARN, "[%"SCNu64"] More than one audio line? only going to negotiate one...\n", handle->handle_id);
}
JANUS_LOG(LOG_VERB, "[%"SCNu64"] Video %s been negotiated\n", handle->handle_id, video ? "has" : "has NOT");
if(video > 1) {
- JANUS_LOG(LOG_ERR, "[%"SCNu64"] More than one video line? only going to negotiate one...\n", handle->handle_id);
+ JANUS_LOG(LOG_WARN, "[%"SCNu64"] More than one video line? only going to negotiate one...\n", handle->handle_id);
}
JANUS_LOG(LOG_VERB, "[%"SCNu64"] SCTP/DataChannels %s been negotiated\n", handle->handle_id, data ? "have" : "have NOT");
if(data > 1) {
- JANUS_LOG(LOG_ERR, "[%"SCNu64"] More than one data line? only going to negotiate one...\n", handle->handle_id);
+ JANUS_LOG(LOG_WARN, "[%"SCNu64"] More than one data line? only going to negotiate one...\n", handle->handle_id);
}
#ifndef HAVE_SCTP
if(data) {
@@ -1134,7 +1150,18 @@ int janus_process_incoming_request(janus_request_source *source, json_t *root) {
/* New session */
if(offer) {
/* Setup ICE locally (we received an offer) */
- janus_ice_setup_local(handle, offer, audio, video, data, bundle, rtcpmux, trickle);
+ if(janus_ice_setup_local(handle, offer, audio, video, data, bundle, rtcpmux, trickle) < 0) {
+ JANUS_LOG(LOG_ERR, "Error setting ICE locally\n");
+ ret = janus_process_error(source, session_id, transaction_text, JANUS_ERROR_UNKNOWN, "Error setting ICE locally");
+ goto jsondone;
+ }
+ } else {
+ /* Make sure we're waiting for an ANSWER in the first place */
+ if(!handle->agent) {
+ JANUS_LOG(LOG_ERR, "Unexpected ANSWER (did we offer?)\n");
+ ret = janus_process_error(source, session_id, transaction_text, JANUS_ERROR_UNEXPECTED_ANSWER, "Unexpected ANSWER (did we offer?)");
+ goto jsondone;
+ }
}
janus_sdp_parse(handle, parsed_sdp);
janus_sdp_free(parsed_sdp);
@@ -2090,6 +2117,7 @@ int janus_process_incoming_admin_request(janus_request_source *source, json_t *r
json_object_set_new(flags, "trickle-synced", json_integer(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_TRICKLE_SYNCED)));
json_object_set_new(flags, "data-channels", json_integer(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_DATA_CHANNELS)));
json_object_set_new(flags, "plan-b", json_integer(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_PLAN_B)));
+ json_object_set_new(flags, "cleaning", json_integer(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_CLEANING)));
json_object_set_new(info, "flags", flags);
json_t *sdps = json_object();
if(json_string(handle->local_sdp))
@@ -2582,9 +2610,29 @@ int janus_wss_onclose(libwebsock_client_state *state) {
}
client->thread = NULL;
client->state = NULL;
- /* TODO: Remove all sessions (and handles) created by this client */
- if(client->sessions != NULL)
+ if(client->sessions != NULL) {
+ /* Remove all sessions (and handles) created by this client */
+ janus_mutex_lock(&sessions_mutex);
+ GHashTableIter iter;
+ gpointer value;
+ g_hash_table_iter_init(&iter, client->sessions);
+ while(g_hash_table_iter_next(&iter, NULL, &value)) {
+ janus_session *session = value;
+ if(!session)
+ continue;
+ session->last_activity = 0; /* This will trigger a timeout */
+ }
+ janus_mutex_unlock(&sessions_mutex);
g_hash_table_destroy(client->sessions);
+ }
+ /* Remove responses queue too, if needed */
+ if(client->responses != NULL) {
+ char *response = NULL;
+ while((response = g_async_queue_try_pop(client->responses)) != NULL) {
+ g_free(response);
+ }
+ g_async_queue_unref(client->responses);
+ }
client->sessions = NULL;
g_free(client);
client = NULL;
@@ -3082,9 +3130,26 @@ json_t *janus_handle_sdp(janus_plugin_session *handle, janus_plugin *plugin, con
JANUS_LOG(LOG_WARN, "[%"SCNu64"] -- DataChannels have been negotiated, but support for them has not been compiled...\n", ice_handle->handle_id);
}
#endif
+ /* Are we still cleaning up from a previous media session? */
+ if(janus_flags_is_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_CLEANING)) {
+ JANUS_LOG(LOG_INFO, "[%"SCNu64"] Still cleaning up from a previous media session, let's wait a bit...\n", ice_handle->handle_id);
+ gint64 waited = 0;
+ while(janus_flags_is_set(&ice_handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_CLEANING)) {
+ JANUS_LOG(LOG_VERB, "[%"SCNu64"] Still cleaning up from a previous media session, let's wait a bit...\n", ice_handle->handle_id);
+ g_usleep(100000);
+ waited += 100000;
+ if(waited >= 3*G_USEC_PER_SEC) {
+ JANUS_LOG(LOG_VERB, "[%"SCNu64"] -- Waited 3 seconds, that's enough!\n", ice_handle->handle_id);
+ break;
+ }
+ }
+ }
if(ice_handle->agent == NULL) {
/* Process SDP in order to setup ICE locally (this is going to result in an answer from the browser) */
- janus_ice_setup_local(ice_handle, 0, audio, video, data, bundle, rtcpmux, trickle);
+ if(janus_ice_setup_local(ice_handle, 0, audio, video, data, bundle, rtcpmux, trickle) < 0) {
+ JANUS_LOG(LOG_ERR, "[%"SCNu64"] Error setting ICE locally\n", ice_handle->handle_id);
+ return NULL;
+ }
} else {
updating = TRUE;
JANUS_LOG(LOG_INFO, "[%"SCNu64"] Updating existing session\n", ice_handle->handle_id);
@@ -3511,6 +3576,9 @@ gint main(int argc, char *argv[])
if(args_info.ice_ignore_list_given) {
janus_config_add_item(config, "nat", "ice_ignore_list", args_info.ice_ignore_list_arg);
}
+ if(args_info.ipv6_candidates_given) {
+ janus_config_add_item(config, "media", "ipv6", "true");
+ }
if(args_info.rtp_port_range_given) {
janus_config_add_item(config, "media", "rtp_port_range", args_info.rtp_port_range_arg);
}
@@ -3661,6 +3729,9 @@ gint main(int argc, char *argv[])
char *stun_server = NULL;
uint16_t stun_port = 0;
uint16_t rtp_min_port = 0, rtp_max_port = 0;
+ gboolean ipv6 = FALSE;
+ item = janus_config_get_item_drilldown(config, "media", "ipv6");
+ ipv6 = (item && item->value) ? janus_is_true(item->value) : FALSE;
item = janus_config_get_item_drilldown(config, "media", "rtp_port_range");
if(item && item->value) {
/* Split in min and max port */
@@ -3688,7 +3759,7 @@ gint main(int argc, char *argv[])
item = janus_config_get_item_drilldown(config, "nat", "stun_port");
if(item && item->value)
stun_port = atoi(item->value);
- if(janus_ice_init(stun_server, stun_port, rtp_min_port, rtp_max_port) < 0) {
+ if(janus_ice_init(stun_server, stun_port, rtp_min_port, rtp_max_port, ipv6) < 0) {
JANUS_LOG(LOG_FATAL, "Invalid STUN address %s:%u\n", stun_server, stun_port);
exit(1);
}
@@ -3752,7 +3823,7 @@ gint main(int argc, char *argv[])
exit(1);
}
struct dirent *pluginent = NULL;
- char pluginpath[255];
+ char pluginpath[1024];
while((pluginent = readdir(dir))) {
int len = strlen(pluginent->d_name);
if (len < 4) {
@@ -3762,8 +3833,8 @@ gint main(int argc, char *argv[])
continue;
}
JANUS_LOG(LOG_INFO, "Loading plugin '%s'...\n", pluginent->d_name);
- memset(pluginpath, 0, 255);
- g_snprintf(pluginpath, 255, "%s/%s", path, pluginent->d_name);
+ memset(pluginpath, 0, 1024);
+ g_snprintf(pluginpath, 1024, "%s/%s", path, pluginent->d_name);
void *plugin = dlopen(pluginpath, RTLD_LAZY);
if (!plugin) {
JANUS_LOG(LOG_ERR, "\tCouldn't load plugin '%s': %s\n", pluginent->d_name, dlerror());
diff --git a/janus.ggo b/janus.ggo
index 79910eb69e..eb56fdbe8e 100644
--- a/janus.ggo
+++ b/janus.ggo
@@ -20,6 +20,7 @@ option "cert-key" k "HTTPS/DTLS certificate key" string typestr="filename" optio
option "stun-server" S "STUN server(:port) to use, if needed (e.g., gateway behind NAT, default=none)" string typestr="ip:port" optional
option "ice-ignore-list" X "Comma-separated list of interfaces or IP addresses to ignore for ICE gathering; partial strings are supported (e.g., vmnet8,192.168.0.1,10.0.0.1 or vmnet,192.168., default=vmnet)" string typestr="list" optional
option "public-ip" e "Public address of the machine, to use in SDP" string typestr="ipaddress" optional
+option "ipv6-candidates" 6 "Whether to enable IPv6 candidates or not (experimental)" flag off
option "rtp-port-range" r "Port range to use for RTP/RTCP" string typestr="min-max" optional
option "debug-level" d "Debug/logging level (0=disable debugging, 7=maximum debug level; default=4)" int typestr="1-7" optional
option "apisecret" a "API secret all requests need to pass in order to be accepted by Janus (useful when wrapping Janus API requests in a server, none by default)" string typestr="randomstring" optional
diff --git a/plugins/janus_audiobridge.c b/plugins/janus_audiobridge.c
index fd11b16989..31ca41dde7 100644
--- a/plugins/janus_audiobridge.c
+++ b/plugins/janus_audiobridge.c
@@ -160,9 +160,10 @@ typedef struct janus_audiobridge_session {
gpointer participant;
gboolean started;
gboolean stopping;
- gboolean destroy;
+ guint64 destroyed; /* Time at which this session was marked as destroyed */
} janus_audiobridge_session;
static GHashTable *sessions;
+static GList *old_sessions;
static janus_mutex sessions_mutex;
typedef struct janus_audiobridge_participant {
@@ -237,6 +238,45 @@ typedef struct wav_header {
#define JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED 499
+/* AudioBridge watchdog/garbage collector (sort of) */
+void *janus_audiobridge_watchdog(void *data);
+void *janus_audiobridge_watchdog(void *data) {
+ JANUS_LOG(LOG_INFO, "AudioBridge watchdog started\n");
+ gint64 now = 0;
+ while(initialized && !stopping) {
+ janus_mutex_lock(&sessions_mutex);
+ /* Iterate on all the sessions */
+ now = janus_get_monotonic_time();
+ if(old_sessions != NULL) {
+ GList *sl = old_sessions;
+ JANUS_LOG(LOG_VERB, "Checking %d old sessions\n", g_list_length(old_sessions));
+ while(sl) {
+ janus_audiobridge_session *session = (janus_audiobridge_session *)sl->data;
+ if(!session || !initialized || stopping) {
+ sl = sl->next;
+ continue;
+ }
+ if(now-session->destroyed >= 5*G_USEC_PER_SEC) {
+ /* We're lazy and actually get rid of the stuff only after a few seconds */
+ GList *rm = sl->next;
+ old_sessions = g_list_delete_link(old_sessions, sl);
+ sl = rm;
+ session->handle = NULL;
+ g_free(session);
+ session = NULL;
+ continue;
+ }
+ sl = sl->next;
+ }
+ }
+ janus_mutex_unlock(&sessions_mutex);
+ g_usleep(2000000);
+ }
+ JANUS_LOG(LOG_INFO, "AudioBridge watchdog stopped\n");
+ return NULL;
+}
+
+
/* Plugin implementation */
int janus_audiobridge_init(janus_callbacks *callback, const char *config_path) {
if(stopping) {
@@ -343,6 +383,12 @@ int janus_audiobridge_init(janus_callbacks *callback, const char *config_path) {
janus_mutex_unlock(&rooms_mutex);
initialized = 1;
+ /* Start the sessions watchdog */
+ GThread *watchdog = g_thread_new("abridge watchdog", &janus_audiobridge_watchdog, NULL);
+ if(!watchdog) {
+ JANUS_LOG(LOG_FATAL, "Couldn't start AudioBridge watchdog...\n");
+ return -1;
+ }
/* Launch the thread that will handle incoming messages */
GError *error = NULL;
handler_thread = g_thread_try_new("janus audiobridge handler", janus_audiobridge_handler, NULL, &error);
@@ -416,7 +462,7 @@ void janus_audiobridge_create_session(janus_plugin_session *handle, int *error)
session->handle = handle;
session->started = FALSE;
session->stopping = FALSE;
- session->destroy = FALSE;
+ session->destroyed = 0;
handle->plugin_handle = session;
janus_mutex_lock(&sessions_mutex);
g_hash_table_insert(sessions, handle, session);
@@ -436,7 +482,7 @@ void janus_audiobridge_destroy_session(janus_plugin_session *handle, int *error)
*error = -2;
return;
}
- if(session->destroy) {
+ if(session->destroyed) {
JANUS_LOG(LOG_WARN, "Session already destroyed...\n");
return;
}
@@ -446,7 +492,10 @@ void janus_audiobridge_destroy_session(janus_plugin_session *handle, int *error)
janus_mutex_unlock(&sessions_mutex);
janus_audiobridge_hangup_media(handle);
/* Cleaning up and removing the session is done in a lazy way */
- session->destroy = TRUE;
+ session->destroyed = janus_get_monotonic_time();
+ janus_mutex_lock(&sessions_mutex);
+ old_sessions = g_list_append(old_sessions, session);
+ janus_mutex_unlock(&sessions_mutex);
return;
}
@@ -480,7 +529,7 @@ void janus_audiobridge_setup_media(janus_plugin_session *handle) {
JANUS_LOG(LOG_ERR, "No session associated with this handle...\n");
return;
}
- if(session->destroy)
+ if(session->destroyed)
return;
/* TODO Only send this peer the audio mix when we get this event */
}
@@ -489,7 +538,7 @@ void janus_audiobridge_incoming_rtp(janus_plugin_session *handle, int video, cha
if(handle == NULL || handle->stopped || stopping || !initialized)
return;
janus_audiobridge_session *session = (janus_audiobridge_session *)handle->plugin_handle;
- if(!session || session->destroy || session->stopping || !session->participant)
+ if(!session || session->destroyed || session->stopping || !session->participant)
return;
janus_audiobridge_participant *participant = (janus_audiobridge_participant *)session->participant;
if(!participant->audio_active)
@@ -534,7 +583,7 @@ void janus_audiobridge_hangup_media(janus_plugin_session *handle) {
JANUS_LOG(LOG_ERR, "No session associated with this handle...\n");
return;
}
- if(session->destroy || !session->participant)
+ if(session->destroyed || !session->participant)
return;
/* Get rid of participant */
janus_audiobridge_participant *participant = (janus_audiobridge_participant *)session->participant;
@@ -562,7 +611,7 @@ void janus_audiobridge_hangup_media(janus_plugin_session *handle) {
g_free(leaving_text);
participant->audio_active = 0;
session->started = FALSE;
- session->destroy = 1;
+ session->destroyed = 1;
/* Get rid of queued packets */
janus_mutex_lock(&participant->qmutex);
while(!g_queue_is_empty(participant->inbuf)) {
@@ -601,7 +650,7 @@ static void *janus_audiobridge_handler(void *data) {
janus_audiobridge_message_free(msg);
continue;
}
- if(session->destroy) {
+ if(session->destroyed) {
janus_audiobridge_message_free(msg);
continue;
}
@@ -1101,7 +1150,6 @@ static void *janus_audiobridge_handler(void *data) {
/* Done */
participant->audio_active = 0;
session->started = FALSE;
- session->destroy = 1;
janus_mutex_unlock(&audiobridge->mutex);
} else {
JANUS_LOG(LOG_ERR, "Unknown request '%s'\n", request_text);
diff --git a/plugins/janus_echotest.c b/plugins/janus_echotest.c
index c5b101e229..241fe65729 100644
--- a/plugins/janus_echotest.c
+++ b/plugins/janus_echotest.c
@@ -110,9 +110,10 @@ typedef struct janus_echotest_session {
gboolean audio_active;
gboolean video_active;
uint64_t bitrate;
- gboolean destroy;
+ guint64 destroyed; /* Time at which this session was marked as destroyed */
} janus_echotest_session;
static GHashTable *sessions;
+static GList *old_sessions;
static janus_mutex sessions_mutex;
void janus_echotest_message_free(janus_echotest_message *msg);
@@ -141,6 +142,45 @@ void janus_echotest_message_free(janus_echotest_message *msg) {
#define JANUS_ECHOTEST_ERROR_INVALID_ELEMENT 413
+/* EchoTest watchdog/garbage collector (sort of) */
+void *janus_echotest_watchdog(void *data);
+void *janus_echotest_watchdog(void *data) {
+ JANUS_LOG(LOG_INFO, "Echotest watchdog started\n");
+ gint64 now = 0;
+ while(initialized && !stopping) {
+ janus_mutex_lock(&sessions_mutex);
+ /* Iterate on all the sessions */
+ now = janus_get_monotonic_time();
+ if(old_sessions != NULL) {
+ GList *sl = old_sessions;
+ JANUS_LOG(LOG_VERB, "Checking %d old sessions\n", g_list_length(old_sessions));
+ while(sl) {
+ janus_echotest_session *session = (janus_echotest_session *)sl->data;
+ if(!session || !initialized || stopping) {
+ sl = sl->next;
+ continue;
+ }
+ if(now-session->destroyed >= 5*G_USEC_PER_SEC) {
+ /* We're lazy and actually get rid of the stuff only after a few seconds */
+ GList *rm = sl->next;
+ old_sessions = g_list_delete_link(old_sessions, sl);
+ sl = rm;
+ session->handle = NULL;
+ g_free(session);
+ session = NULL;
+ continue;
+ }
+ sl = sl->next;
+ }
+ }
+ janus_mutex_unlock(&sessions_mutex);
+ g_usleep(2000000);
+ }
+ JANUS_LOG(LOG_INFO, "EchoTest watchdog stopped\n");
+ return NULL;
+}
+
+
/* Plugin implementation */
int janus_echotest_init(janus_callbacks *callback, const char *config_path) {
if(stopping) {
@@ -170,6 +210,12 @@ int janus_echotest_init(janus_callbacks *callback, const char *config_path) {
gateway = callback;
initialized = 1;
+ /* Start the sessions watchdog */
+ GThread *watchdog = g_thread_new("etest watchdog", &janus_echotest_watchdog, NULL);
+ if(!watchdog) {
+ JANUS_LOG(LOG_FATAL, "Couldn't start EchoTest watchdog...\n");
+ return -1;
+ }
/* Launch the thread that will handle incoming messages */
GError *error = NULL;
handler_thread = g_thread_try_new("janus echotest handler", janus_echotest_handler, NULL, &error);
@@ -240,6 +286,7 @@ void janus_echotest_create_session(janus_plugin_session *handle, int *error) {
session->audio_active = TRUE;
session->video_active = TRUE;
session->bitrate = 0; /* No limit */
+ session->destroyed = 0;
handle->plugin_handle = session;
janus_mutex_lock(&sessions_mutex);
g_hash_table_insert(sessions, handle, session);
@@ -264,7 +311,10 @@ void janus_echotest_destroy_session(janus_plugin_session *handle, int *error) {
g_hash_table_remove(sessions, handle);
janus_mutex_unlock(&sessions_mutex);
/* Cleaning up and removing the session is done in a lazy way */
- session->destroy = TRUE;
+ session->destroyed = janus_get_monotonic_time();
+ janus_mutex_lock(&sessions_mutex);
+ old_sessions = g_list_append(old_sessions, session);
+ janus_mutex_unlock(&sessions_mutex);
return;
}
@@ -297,7 +347,7 @@ void janus_echotest_setup_media(janus_plugin_session *handle) {
JANUS_LOG(LOG_ERR, "No session associated with this handle...\n");
return;
}
- if(session->destroy)
+ if(session->destroyed)
return;
/* We really don't care, as we only send RTP/RTCP we get in the first place back anyway */
}
@@ -313,7 +363,7 @@ void janus_echotest_incoming_rtp(janus_plugin_session *handle, int video, char *
JANUS_LOG(LOG_ERR, "No session associated with this handle...\n");
return;
}
- if(session->destroy)
+ if(session->destroyed)
return;
if((!video && session->audio_active) || (video && session->video_active)) {
gateway->relay_rtp(handle, video, buf, len);
@@ -331,7 +381,7 @@ void janus_echotest_incoming_rtcp(janus_plugin_session *handle, int video, char
JANUS_LOG(LOG_ERR, "No session associated with this handle...\n");
return;
}
- if(session->destroy)
+ if(session->destroyed)
return;
if(session->bitrate > 0)
janus_rtcp_cap_remb(buf, len, session->bitrate);
@@ -349,7 +399,7 @@ void janus_echotest_incoming_data(janus_plugin_session *handle, char *buf, int l
JANUS_LOG(LOG_ERR, "No session associated with this handle...\n");
return;
}
- if(session->destroy)
+ if(session->destroyed)
return;
if(buf == NULL || len <= 0)
return;
@@ -374,7 +424,7 @@ void janus_echotest_hangup_media(janus_plugin_session *handle) {
JANUS_LOG(LOG_ERR, "No session associated with this handle...\n");
return;
}
- if(session->destroy)
+ if(session->destroyed)
return;
/* Send an event to the browser and tell it's over */
json_t *event = json_object();
@@ -414,7 +464,7 @@ static void *janus_echotest_handler(void *data) {
janus_echotest_message_free(msg);
continue;
}
- if(session->destroy) {
+ if(session->destroyed) {
janus_echotest_message_free(msg);
continue;
}
diff --git a/plugins/janus_recordplay.c b/plugins/janus_recordplay.c
new file mode 100644
index 0000000000..dcb8837a4e
--- /dev/null
+++ b/plugins/janus_recordplay.c
@@ -0,0 +1,1548 @@
+/*! \file janus_recordplay.c
+ * \author Lorenzo Miniero
+ * \copyright GNU General Public License v3
+ * \brief Janus Record&Play plugin
+ * \details This is a simple application that implements two different
+ * features: it allows you to record a message you send with WebRTC in
+ * the format defined in recorded.c (MJR recording) and subsequently
+ * replay this recording (or other previously recorded) through WebRTC
+ * as well.
+ *
+ * This application aims at showing how easy recording frames sent by
+ * a peer is, and how this recording can be re-used directly, without
+ * necessarily involving a post-processing process (e.g., through the
+ * tool we provide in janus-pp-rec.c).
+ *
+ * The configuration process is quite easy: just choose where the
+ * recordings should be saved. The same folder will also be used to list
+ * the available recordings that can be replayed.
+ *
+ * \note The application creates a special file in INI format with
+ * \c .nfo extension for each recording that is saved. This is necessary
+ * to map a specific audio .mjr file to a different video .mjr one, as
+ * they always get saved in different files. If you want to replay
+ * recordings you took in a different application (e.g., the streaming
+ * or videoroom plugins) just copy the related files in the folder you
+ * configured this plugin to use and create a .nfo file in the same
+ * folder to create a mapping, e.g.:
+ *
+ * [12345678]
+ * name = My videoroom recording
+ * date = 2014-10-14 17:11:26
+ * audio = mcu-audio.mjr
+ * video = mcu-video.mjr
+ *
+ *
+ * \ingroup plugins
+ * \ref plugins
+ */
+
+#include "plugin.h"
+
+#include
+#include
+#include
+#include
+
+#include "../apierror.h"
+#include "../config.h"
+#include "../mutex.h"
+#include "../record.h"
+#include "../rtp.h"
+#include "../rtcp.h"
+#include "../utils.h"
+
+
+/* Plugin information */
+#define JANUS_RECORDPLAY_VERSION 1
+#define JANUS_RECORDPLAY_VERSION_STRING "0.0.1"
+#define JANUS_RECORDPLAY_DESCRIPTION "This is a trivial Record&Play plugin for Janus, to record WebRTC sessions and replay them."
+#define JANUS_RECORDPLAY_NAME "JANUS Record&Play plugin"
+#define JANUS_RECORDPLAY_AUTHOR "Meetecho s.r.l."
+#define JANUS_RECORDPLAY_PACKAGE "janus.plugin.recordplay"
+
+/* Plugin methods */
+janus_plugin *create(void);
+int janus_recordplay_init(janus_callbacks *callback, const char *onfig_path);
+void janus_recordplay_destroy(void);
+int janus_recordplay_get_version(void);
+const char *janus_recordplay_get_version_string(void);
+const char *janus_recordplay_get_description(void);
+const char *janus_recordplay_get_name(void);
+const char *janus_recordplay_get_author(void);
+const char *janus_recordplay_get_package(void);
+void janus_recordplay_create_session(janus_plugin_session *handle, int *error);
+struct janus_plugin_result *janus_recordplay_handle_message(janus_plugin_session *handle, char *transaction, char *message, char *sdp_type, char *sdp);
+void janus_recordplay_setup_media(janus_plugin_session *handle);
+void janus_recordplay_incoming_rtp(janus_plugin_session *handle, int video, char *buf, int len);
+void janus_recordplay_incoming_rtcp(janus_plugin_session *handle, int video, char *buf, int len);
+void janus_recordplay_incoming_data(janus_plugin_session *handle, char *buf, int len);
+void janus_recordplay_hangup_media(janus_plugin_session *handle);
+void janus_recordplay_destroy_session(janus_plugin_session *handle, int *error);
+
+/* Plugin setup */
+static janus_plugin janus_recordplay_plugin =
+ {
+ .init = janus_recordplay_init,
+ .destroy = janus_recordplay_destroy,
+
+ .get_version = janus_recordplay_get_version,
+ .get_version_string = janus_recordplay_get_version_string,
+ .get_description = janus_recordplay_get_description,
+ .get_name = janus_recordplay_get_name,
+ .get_author = janus_recordplay_get_author,
+ .get_package = janus_recordplay_get_package,
+
+ .create_session = janus_recordplay_create_session,
+ .handle_message = janus_recordplay_handle_message,
+ .setup_media = janus_recordplay_setup_media,
+ .incoming_rtp = janus_recordplay_incoming_rtp,
+ .incoming_rtcp = janus_recordplay_incoming_rtcp,
+ .incoming_data = janus_recordplay_incoming_data,
+ .hangup_media = janus_recordplay_hangup_media,
+ .destroy_session = janus_recordplay_destroy_session,
+ };
+
+/* Plugin creator */
+janus_plugin *create(void) {
+ JANUS_LOG(LOG_VERB, "%s created!\n", JANUS_RECORDPLAY_NAME);
+ return &janus_recordplay_plugin;
+}
+
+
+/* Useful stuff */
+static int initialized = 0, stopping = 0;
+static janus_callbacks *gateway = NULL;
+static GThread *handler_thread;
+static void *janus_recordplay_handler(void *data);
+
+typedef struct janus_recordplay_message {
+ janus_plugin_session *handle;
+ char *transaction;
+ json_t *message;
+ char *sdp_type;
+ char *sdp;
+} janus_recordplay_message;
+static GAsyncQueue *messages = NULL;
+
+typedef struct janus_recordplay_rtp_header_extension {
+ uint16_t type;
+ uint16_t length;
+} janus_recordplay_rtp_header_extension;
+
+typedef struct janus_recordplay_frame_packet {
+ uint16_t seq; /* RTP Sequence number */
+ uint64_t ts; /* RTP Timestamp */
+ int len; /* Length of the data */
+ long offset; /* Offset of the data in the file */
+ struct janus_recordplay_frame_packet *next;
+ struct janus_recordplay_frame_packet *prev;
+} janus_recordplay_frame_packet;
+janus_recordplay_frame_packet *janus_recordplay_get_frames(const char *dir, const char *filename);
+
+typedef struct janus_recordplay_recording {
+ guint64 id; /* Recording unique ID */
+ char *name; /* Name of the recording */
+ char *date; /* Time of the recording */
+ char *arc_file; /* Audio file name */
+ char *vrc_file; /* Video file name */
+} janus_recordplay_recording;
+static GHashTable *recordings;
+static janus_mutex recordings_mutex;
+
+typedef struct janus_recordplay_session {
+ janus_plugin_session *handle;
+ gboolean active;
+ gboolean recorder; /* Whether this session is used to record or to replay a WebRTC session */
+ gboolean firefox; /* We send Firefox users a different kind of FIR */
+ janus_recordplay_recording *recording;
+ janus_recorder *arc; /* Audio recorder */
+ janus_recorder *vrc; /* Video recorder */
+ janus_recordplay_frame_packet *aframes; /* Audio frames (for playout) */
+ janus_recordplay_frame_packet *vframes; /* Video frames (for playout) */
+ guint64 destroyed; /* Time at which this session was marked as destroyed */
+} janus_recordplay_session;
+static GHashTable *sessions;
+static GList *old_sessions;
+static janus_mutex sessions_mutex;
+
+
+static char *recordings_path = NULL;
+void janus_recordplay_update_recordings_list(void);
+static void *janus_recordplay_playout_thread(void *data);
+
+
+/* SDP offer/answer templates for the playout */
+#define OPUS_PT 111
+#define VP8_PT 100
+#define sdp_template \
+ "v=0\r\n" \
+ "o=- %"SCNu64" %"SCNu64" IN IP4 127.0.0.1\r\n" /* We need current time here */ \
+ "s=%s\r\n" /* Recording playout id */ \
+ "t=0 0\r\n" \
+ "%s%s" /* Audio and/or video m-lines */
+#define sdp_a_template \
+ "m=audio 1 RTP/SAVPF %d\r\n" /* Opus payload type */ \
+ "c=IN IP4 1.1.1.1\r\n" \
+ "a=%s\r\n" /* Media direction */ \
+ "a=rtpmap:%d opus/48000/2\r\n" /* Opus payload type */
+#define sdp_v_template \
+ "m=video 1 RTP/SAVPF %d\r\n" /* VP8 payload type */ \
+ "c=IN IP4 1.1.1.1\r\n" \
+ "a=%s\r\n" /* Media direction */ \
+ "a=rtpmap:%d VP8/90000\r\n" /* VP8 payload type */ \
+ "a=rtcp-fb:%d ccm fir\r\n" /* VP8 payload type */ \
+ "a=rtcp-fb:%d nack\r\n" /* VP8 payload type */ \
+ "a=rtcp-fb:%d nack pli\r\n" /* VP8 payload type */ \
+ "a=rtcp-fb:%d goog-remb\r\n" /* VP8 payload type */
+
+
+void janus_recordplay_message_free(janus_recordplay_message *msg);
+void janus_recordplay_message_free(janus_recordplay_message *msg) {
+ if(!msg)
+ return;
+
+ msg->handle = NULL;
+
+ g_free(msg->transaction);
+ msg->transaction = NULL;
+ g_free(msg->message);
+ msg->message = NULL;
+ g_free(msg->sdp_type);
+ msg->sdp_type = NULL;
+ g_free(msg->sdp);
+ msg->sdp = NULL;
+
+ g_free(msg);
+}
+
+
+/* Error codes */
+#define JANUS_RECORDPLAY_ERROR_NO_MESSAGE 411
+#define JANUS_RECORDPLAY_ERROR_INVALID_JSON 412
+#define JANUS_RECORDPLAY_ERROR_INVALID_REQUEST 413
+#define JANUS_RECORDPLAY_ERROR_INVALID_ELEMENT 414
+#define JANUS_RECORDPLAY_ERROR_MISSING_ELEMENT 415
+#define JANUS_RECORDPLAY_ERROR_NOT_FOUND 416
+#define JANUS_RECORDPLAY_ERROR_INVALID_RECORDING 417
+#define JANUS_RECORDPLAY_ERROR_INVALID_STATE 418
+#define JANUS_RECORDPLAY_ERROR_UNKNOWN_ERROR 499
+
+
+/* Record&Play watchdog/garbage collector (sort of) */
+void *janus_recordplay_watchdog(void *data);
+void *janus_recordplay_watchdog(void *data) {
+ JANUS_LOG(LOG_INFO, "Record&Play watchdog started\n");
+ gint64 now = 0;
+ while(initialized && !stopping) {
+ janus_mutex_lock(&sessions_mutex);
+ /* Iterate on all the sessions */
+ now = janus_get_monotonic_time();
+ if(old_sessions != NULL) {
+ GList *sl = old_sessions;
+ JANUS_LOG(LOG_VERB, "Checking %d old sessions\n", g_list_length(old_sessions));
+ while(sl) {
+ janus_recordplay_session *session = (janus_recordplay_session *)sl->data;
+ if(!session || !initialized || stopping) {
+ sl = sl->next;
+ continue;
+ }
+ if(now-session->destroyed >= 5*G_USEC_PER_SEC) {
+ /* We're lazy and actually get rid of the stuff only after a few seconds */
+ GList *rm = sl->next;
+ old_sessions = g_list_delete_link(old_sessions, sl);
+ sl = rm;
+ session->handle = NULL;
+ g_free(session);
+ session = NULL;
+ continue;
+ }
+ sl = sl->next;
+ }
+ }
+ janus_mutex_unlock(&sessions_mutex);
+ g_usleep(2000000);
+ }
+ JANUS_LOG(LOG_INFO, "Record&Play watchdog stopped\n");
+ return NULL;
+}
+
+
+/* Plugin implementation */
+int janus_recordplay_init(janus_callbacks *callback, const char *config_path) {
+ if(stopping) {
+ /* Still stopping from before */
+ return -1;
+ }
+ if(callback == NULL || config_path == NULL) {
+ /* Invalid arguments */
+ return -1;
+ }
+
+ /* Read configuration */
+ char filename[255];
+ g_snprintf(filename, 255, "%s/%s.cfg", config_path, JANUS_RECORDPLAY_PACKAGE);
+ JANUS_LOG(LOG_VERB, "Configuration file: %s\n", filename);
+ janus_config *config = janus_config_parse(filename);
+ if(config != NULL)
+ janus_config_print(config);
+ /* Parse configuration */
+ if(config != NULL) {
+ janus_config_item *path = janus_config_get_item_drilldown(config, "general", "path");
+ if(path && path->value)
+ recordings_path = g_strdup(path->value);
+ /* Done */
+ janus_config_destroy(config);
+ config = NULL;
+ }
+ if(recordings_path == NULL) {
+ recordings_path = g_strdup("/tmp");
+ JANUS_LOG(LOG_WARN, "No recordings path specified, using /tmp...\n");
+ }
+ /* Create the folder, if needed */
+ struct stat st = {0};
+ if(stat(recordings_path, &st) == -1) {
+ int res = janus_mkdir(recordings_path, 0755);
+ JANUS_LOG(LOG_VERB, "Creating folder: %d\n", res);
+ if(res != 0) {
+ JANUS_LOG(LOG_ERR, "%s", strerror(res));
+ return -1; /* No point going on... */
+ }
+ }
+ recordings = g_hash_table_new(NULL, NULL);
+ janus_mutex_init(&recordings_mutex);
+ janus_recordplay_update_recordings_list();
+
+ sessions = g_hash_table_new(NULL, NULL);
+ janus_mutex_init(&sessions_mutex);
+ messages = g_async_queue_new_full((GDestroyNotify) janus_recordplay_message_free);
+ /* This is the callback we'll need to invoke to contact the gateway */
+ gateway = callback;
+
+ initialized = 1;
+ /* Start the sessions watchdog */
+ GThread *watchdog = g_thread_new("rplay watchdog", &janus_recordplay_watchdog, NULL);
+ if(!watchdog) {
+ JANUS_LOG(LOG_FATAL, "Couldn't start Record&Play watchdog...\n");
+ return -1;
+ }
+ /* Launch the thread that will handle incoming messages */
+ GError *error = NULL;
+ handler_thread = g_thread_try_new("janus recordplay handler", janus_recordplay_handler, NULL, &error);
+ if(error != NULL) {
+ initialized = 0;
+ /* Something went wrong... */
+ JANUS_LOG(LOG_ERR, "Got error %d (%s) trying to launch thread...\n", error->code, error->message ? error->message : "??");
+ return -1;
+ }
+ JANUS_LOG(LOG_INFO, "%s initialized!\n", JANUS_RECORDPLAY_NAME);
+ return 0;
+}
+
+void janus_recordplay_destroy(void) {
+ if(!initialized)
+ return;
+ stopping = 1;
+ if(handler_thread != NULL) {
+ g_thread_join(handler_thread);
+ }
+ handler_thread = NULL;
+ /* FIXME We should destroy the sessions cleanly */
+ g_hash_table_destroy(sessions);
+ g_async_queue_unref(messages);
+ messages = NULL;
+ sessions = NULL;
+ initialized = 0;
+ stopping = 0;
+ JANUS_LOG(LOG_INFO, "%s destroyed!\n", JANUS_RECORDPLAY_NAME);
+}
+
+int janus_recordplay_get_version(void) {
+ return JANUS_RECORDPLAY_VERSION;
+}
+
+const char *janus_recordplay_get_version_string(void) {
+ return JANUS_RECORDPLAY_VERSION_STRING;
+}
+
+const char *janus_recordplay_get_description(void) {
+ return JANUS_RECORDPLAY_DESCRIPTION;
+}
+
+const char *janus_recordplay_get_name(void) {
+ return JANUS_RECORDPLAY_NAME;
+}
+
+const char *janus_recordplay_get_author(void) {
+ return JANUS_RECORDPLAY_AUTHOR;
+}
+
+const char *janus_recordplay_get_package(void) {
+ return JANUS_RECORDPLAY_PACKAGE;
+}
+
+void janus_recordplay_create_session(janus_plugin_session *handle, int *error) {
+ if(stopping || !initialized) {
+ *error = -1;
+ return;
+ }
+ janus_recordplay_session *session = (janus_recordplay_session *)calloc(1, sizeof(janus_recordplay_session));
+ if(session == NULL) {
+ JANUS_LOG(LOG_FATAL, "Memory error!\n");
+ *error = -2;
+ return;
+ }
+ session->handle = handle;
+ session->active = FALSE;
+ session->recorder = FALSE;
+ session->firefox = FALSE;
+ session->arc = NULL;
+ session->vrc = NULL;
+ session->destroyed = 0;
+ handle->plugin_handle = session;
+ janus_mutex_lock(&sessions_mutex);
+ g_hash_table_insert(sessions, handle, session);
+ janus_mutex_unlock(&sessions_mutex);
+
+ return;
+}
+
+void janus_recordplay_destroy_session(janus_plugin_session *handle, int *error) {
+ if(stopping || !initialized) {
+ *error = -1;
+ return;
+ }
+ janus_recordplay_session *session = (janus_recordplay_session *)handle->plugin_handle;
+ if(!session) {
+ JANUS_LOG(LOG_ERR, "No session associated with this handle...\n");
+ *error = -2;
+ return;
+ }
+ JANUS_LOG(LOG_VERB, "Removing Echo Test session...\n");
+ janus_mutex_lock(&sessions_mutex);
+ g_hash_table_remove(sessions, handle);
+ janus_mutex_unlock(&sessions_mutex);
+ /* Cleaning up and removing the session is done in a lazy way */
+ session->destroyed = janus_get_monotonic_time();
+ janus_mutex_lock(&sessions_mutex);
+ old_sessions = g_list_append(old_sessions, session);
+ janus_mutex_unlock(&sessions_mutex);
+ return;
+}
+
+struct janus_plugin_result *janus_recordplay_handle_message(janus_plugin_session *handle, char *transaction, char *message, char *sdp_type, char *sdp) {
+ if(stopping || !initialized)
+ return janus_plugin_result_new(JANUS_PLUGIN_ERROR, stopping ? "Shutting down" : "Plugin not initialized");
+ JANUS_LOG(LOG_VERB, "%s\n", message);
+ janus_recordplay_message *msg = calloc(1, sizeof(janus_recordplay_message));
+ if(msg == NULL) {
+ JANUS_LOG(LOG_FATAL, "Memory error!\n");
+ return janus_plugin_result_new(JANUS_PLUGIN_ERROR, "Memory error");
+ }
+
+ /* Pre-parse the message */
+ int error_code = 0;
+ char error_cause[512]; /* FIXME 512 should be enough, but anyway... */
+ json_t *root = NULL;
+ janus_recordplay_session *session = (janus_recordplay_session *)handle->plugin_handle;
+ if(!session) {
+ JANUS_LOG(LOG_ERR, "No session associated with this handle...\n");
+ error_code = JANUS_RECORDPLAY_ERROR_UNKNOWN_ERROR;
+ g_snprintf(error_cause, 512, "%s", "session associated with this handle...");
+ goto error;
+ }
+ if(session->destroyed) {
+ JANUS_LOG(LOG_ERR, "Session has already been destroyed...\n");
+ error_code = JANUS_RECORDPLAY_ERROR_UNKNOWN_ERROR;
+ g_snprintf(error_cause, 512, "%s", "Session has already been destroyed...");
+ goto error;
+ }
+ error_code = 0;
+ JANUS_LOG(LOG_VERB, "Handling message: %s\n", message);
+ if(message == NULL) {
+ JANUS_LOG(LOG_ERR, "No message??\n");
+ error_code = JANUS_RECORDPLAY_ERROR_NO_MESSAGE;
+ g_snprintf(error_cause, 512, "%s", "No message??");
+ goto error;
+ }
+ json_error_t error;
+ root = json_loads(message, 0, &error);
+ if(!root) {
+ JANUS_LOG(LOG_ERR, "JSON error: on line %d: %s\n", error.line, error.text);
+ error_code = JANUS_RECORDPLAY_ERROR_INVALID_JSON;
+ g_snprintf(error_cause, 512, "JSON error: on line %d: %s", error.line, error.text);
+ goto error;
+ }
+ if(!json_is_object(root)) {
+ JANUS_LOG(LOG_ERR, "JSON error: not an object\n");
+ error_code = JANUS_RECORDPLAY_ERROR_INVALID_JSON;
+ g_snprintf(error_cause, 512, "JSON error: not an object");
+ goto error;
+ }
+ /* Get the request first */
+ json_t *request = json_object_get(root, "request");
+ if(!request) {
+ JANUS_LOG(LOG_ERR, "Missing element (request)\n");
+ error_code = JANUS_RECORDPLAY_ERROR_MISSING_ELEMENT;
+ g_snprintf(error_cause, 512, "Missing element (request)");
+ goto error;
+ }
+ if(!json_is_string(request)) {
+ JANUS_LOG(LOG_ERR, "Invalid element (request should be a string)\n");
+ error_code = JANUS_RECORDPLAY_ERROR_INVALID_ELEMENT;
+ g_snprintf(error_cause, 512, "Invalid element (request should be a string)");
+ goto error;
+ }
+ /* Some requests ('create' and 'destroy') can be handled synchronously */
+ const char *request_text = json_string_value(request);
+ if(!strcasecmp(request_text, "update")) {
+ /* Update list of available recordings, scanning the folder again */
+ janus_recordplay_update_recordings_list();
+ /* Send info back */
+ json_t *response = json_object();
+ json_object_set_new(response, "recordplay", json_string("ok"));
+ char *response_text = json_dumps(response, JSON_INDENT(3) | JSON_PRESERVE_ORDER);
+ json_decref(response);
+ janus_plugin_result *result = janus_plugin_result_new(JANUS_PLUGIN_OK, response_text);
+ g_free(response_text);
+ return result;
+ } else if(!strcasecmp(request_text, "list")) {
+ json_t *list = json_array();
+ JANUS_LOG(LOG_VERB, "Request for the list of recordings\n");
+ /* Return a list of all available recordings */
+ janus_mutex_lock(&recordings_mutex);
+ GHashTableIter iter;
+ gpointer value;
+ g_hash_table_iter_init(&iter, recordings);
+ while (g_hash_table_iter_next(&iter, NULL, &value)) {
+ janus_recordplay_recording *rec = value;
+ json_t *ml = json_object();
+ json_object_set_new(ml, "id", json_integer(rec->id));
+ json_object_set_new(ml, "name", json_string(rec->name));
+ json_object_set_new(ml, "date", json_string(rec->date));
+ json_object_set_new(ml, "audio", json_string(rec->arc_file ? "false" : "true"));
+ json_object_set_new(ml, "video", json_string(rec->vrc_file ? "false" : "true"));
+ json_array_append_new(list, ml);
+ }
+ janus_mutex_unlock(&recordings_mutex);
+ /* Send info back */
+ json_t *response = json_object();
+ json_object_set_new(response, "recordplay", json_string("list"));
+ json_object_set_new(response, "list", list);
+ char *response_text = json_dumps(response, JSON_INDENT(3) | JSON_PRESERVE_ORDER);
+ json_decref(response);
+ janus_plugin_result *result = janus_plugin_result_new(JANUS_PLUGIN_OK, response_text);
+ g_free(response_text);
+ return result;
+ } else if(!strcasecmp(request_text, "record") || !strcasecmp(request_text, "play")
+ || !strcasecmp(request_text, "start") || !strcasecmp(request_text, "stop")) {
+ /* These messages are handled asynchronously */
+ goto async;
+ } else {
+ JANUS_LOG(LOG_VERB, "Unknown request '%s'\n", request_text);
+ error_code = JANUS_RECORDPLAY_ERROR_INVALID_REQUEST;
+ g_snprintf(error_cause, 512, "Unknown request '%s'", request_text);
+ goto error;
+ }
+
+error:
+ {
+ if(root != NULL)
+ json_decref(root);
+ /* Prepare JSON error event */
+ json_t *event = json_object();
+ json_object_set_new(event, "videoroom", json_string("event"));
+ json_object_set_new(event, "error_code", json_integer(error_code));
+ json_object_set_new(event, "error", json_string(error_cause));
+ char *event_text = json_dumps(event, JSON_INDENT(3) | JSON_PRESERVE_ORDER);
+ json_decref(event);
+ janus_plugin_result *result = janus_plugin_result_new(JANUS_PLUGIN_OK, event_text);
+ g_free(event_text);
+ return result;
+ }
+
+async:
+ {
+ /* All the other requests to this plugin are handled asynchronously */
+ msg->handle = handle;
+ msg->transaction = transaction;
+ msg->message = root;
+ msg->sdp_type = sdp_type;
+ msg->sdp = sdp;
+
+ g_async_queue_push(messages, msg);
+
+ return janus_plugin_result_new(JANUS_PLUGIN_OK_WAIT, NULL);
+ }
+}
+
+void janus_recordplay_setup_media(janus_plugin_session *handle) {
+ JANUS_LOG(LOG_INFO, "WebRTC media is now available\n");
+ if(stopping || !initialized)
+ return;
+ janus_recordplay_session *session = (janus_recordplay_session *)handle->plugin_handle;
+ if(!session) {
+ JANUS_LOG(LOG_ERR, "No session associated with this handle...\n");
+ return;
+ }
+ if(session->destroyed)
+ return;
+ /* Take note of the fact that the session is now active */
+ session->active = TRUE;
+ if(!session->recorder) {
+ g_thread_new("recordplay playout thread", &janus_recordplay_playout_thread, session);
+ }
+}
+
+void janus_recordplay_incoming_rtp(janus_plugin_session *handle, int video, char *buf, int len) {
+ if(handle == NULL || handle->stopped || stopping || !initialized)
+ return;
+ if(gateway) {
+ janus_recordplay_session *session = (janus_recordplay_session *)handle->plugin_handle;
+ if(!session) {
+ JANUS_LOG(LOG_ERR, "No session associated with this handle...\n");
+ return;
+ }
+ if(session->destroyed)
+ return;
+ /* Are we recording? */
+ if(session->recorder) {
+ if(video && session->vrc)
+ janus_recorder_save_frame(session->vrc, buf, len);
+ else if(!video && session->arc)
+ janus_recorder_save_frame(session->arc, buf, len);
+ }
+ }
+}
+
+void janus_recordplay_incoming_rtcp(janus_plugin_session *handle, int video, char *buf, int len) {
+ if(handle == NULL || handle->stopped || stopping || !initialized)
+ return;
+ /* FIXME We don't care */
+}
+
+void janus_recordplay_incoming_data(janus_plugin_session *handle, char *buf, int len) {
+ if(handle == NULL || handle->stopped || stopping || !initialized)
+ return;
+ /* FIXME We don't care */
+}
+
+void janus_recordplay_hangup_media(janus_plugin_session *handle) {
+ JANUS_LOG(LOG_INFO, "No WebRTC media anymore\n");
+ if(stopping || !initialized)
+ return;
+ janus_recordplay_session *session = (janus_recordplay_session *)handle->plugin_handle;
+ if(!session) {
+ JANUS_LOG(LOG_ERR, "No session associated with this handle...\n");
+ return;
+ }
+ if(session->destroyed)
+ return;
+ session->active = FALSE;
+
+ /* Send an event to the browser and tell it's over */
+ json_t *event = json_object();
+ json_object_set_new(event, "recordplay", json_string("event"));
+ json_object_set_new(event, "result", json_string("done"));
+ char *event_text = json_dumps(event, JSON_INDENT(3) | JSON_PRESERVE_ORDER);
+ json_decref(event);
+ JANUS_LOG(LOG_VERB, "Pushing event: %s\n", event_text);
+ int ret = gateway->push_event(handle, &janus_recordplay_plugin, NULL, event_text, NULL, NULL);
+ JANUS_LOG(LOG_VERB, " >> %d (%s)\n", ret, janus_get_api_error(ret));
+ g_free(event_text);
+
+ /* FIXME Simulate a "stop" coming from the browser */
+ janus_recordplay_message *msg = calloc(1, sizeof(janus_recordplay_message));
+ msg->handle = handle;
+ msg->message = json_loads("{\"request\":\"stop\"}", 0, NULL);
+ msg->transaction = NULL;
+ msg->sdp_type = NULL;
+ msg->sdp = NULL;
+ g_async_queue_push(messages, msg);
+}
+
+/* Thread to handle incoming messages */
+static void *janus_recordplay_handler(void *data) {
+ JANUS_LOG(LOG_VERB, "Joining thread\n");
+ janus_recordplay_message *msg = NULL;
+ int error_code = 0;
+ char *error_cause = calloc(512, sizeof(char)); /* FIXME 512 should be enough, but anyway... */
+ if(error_cause == NULL) {
+ JANUS_LOG(LOG_FATAL, "Memory error!\n");
+ return NULL;
+ }
+ json_t *root = NULL;
+ while(initialized && !stopping) {
+ if(!messages || (msg = g_async_queue_try_pop(messages)) == NULL) {
+ usleep(50000);
+ continue;
+ }
+ janus_recordplay_session *session = (janus_recordplay_session *)msg->handle->plugin_handle;
+ if(!session) {
+ JANUS_LOG(LOG_ERR, "No session associated with this handle...\n");
+ janus_recordplay_message_free(msg);
+ continue;
+ }
+ if(session->destroyed) {
+ janus_recordplay_message_free(msg);
+ continue;
+ }
+ /* Handle request */
+ error_code = 0;
+ root = NULL;
+ if(msg->message == NULL) {
+ JANUS_LOG(LOG_ERR, "No message??\n");
+ error_code = JANUS_RECORDPLAY_ERROR_NO_MESSAGE;
+ g_snprintf(error_cause, 512, "%s", "No message??");
+ goto error;
+ }
+ root = msg->message;
+ /* Get the request first */
+ json_t *request = json_object_get(root, "request");
+ if(!request) {
+ JANUS_LOG(LOG_ERR, "Missing element (request)\n");
+ error_code = JANUS_RECORDPLAY_ERROR_MISSING_ELEMENT;
+ g_snprintf(error_cause, 512, "Missing element (request)");
+ goto error;
+ }
+ if(!json_is_string(request)) {
+ JANUS_LOG(LOG_ERR, "Invalid element (request should be a string)\n");
+ error_code = JANUS_RECORDPLAY_ERROR_INVALID_ELEMENT;
+ g_snprintf(error_cause, 512, "Invalid element (request should be a string)");
+ goto error;
+ }
+ const char *request_text = json_string_value(request);
+ json_t *event = NULL;
+ json_t *result = NULL;
+ char *sdp = NULL;
+ if(!strcasecmp(request_text, "record")) {
+ if(!msg->sdp) {
+ JANUS_LOG(LOG_ERR, "Missing SDP offer\n");
+ error_code = JANUS_RECORDPLAY_ERROR_MISSING_ELEMENT;
+ g_snprintf(error_cause, 512, "Missing SDP offer");
+ goto error;
+ }
+ json_t *name = json_object_get(root, "name");
+ if(!name) {
+ JANUS_LOG(LOG_ERR, "Missing element (name)\n");
+ error_code = JANUS_RECORDPLAY_ERROR_MISSING_ELEMENT;
+ g_snprintf(error_cause, 512, "Missing element (name)");
+ goto error;
+ }
+ if(!json_is_string(name)) {
+ JANUS_LOG(LOG_ERR, "Invalid element (name should be an integer)\n");
+ error_code = JANUS_RECORDPLAY_ERROR_INVALID_ELEMENT;
+ g_snprintf(error_cause, 512, "Invalid element (name should be an integer)");
+ goto error;
+ }
+ const char *name_text = json_string_value(name);
+ guint64 id = 0;
+ while(id == 0) {
+ id = g_random_int();
+ if(g_hash_table_lookup(recordings, GUINT_TO_POINTER(id)) != NULL) {
+ /* Room ID already taken, try another one */
+ id = 0;
+ }
+ }
+ JANUS_LOG(LOG_VERB, "Starting new recording with ID %"SCNu64"\n", id);
+ janus_recordplay_recording *rec = (janus_recordplay_recording *)calloc(1, sizeof(janus_recordplay_recording));
+ rec->id = id;
+ rec->name = g_strdup(name_text);
+ /* Create a date string */
+ time_t t = time(NULL);
+ struct tm *tmv = localtime(&t);
+ char outstr[200];
+ strftime(outstr, sizeof(outstr), "%Y-%m-%d %H:%M:%S", tmv);
+ rec->date = g_strdup(outstr);
+ if(strstr(msg->sdp, "m=audio")) {
+ char filename[256];
+ g_snprintf(filename, 256, "rec-%"SCNu64"-audio", id);
+ rec->arc_file = g_strdup(filename);
+ session->arc = janus_recorder_create(recordings_path, 0, rec->arc_file);
+ }
+ if(strstr(msg->sdp, "m=video")) {
+ char filename[256];
+ g_snprintf(filename, 256, "rec-%"SCNu64"-video", id);
+ rec->vrc_file = g_strdup(filename);
+ session->vrc = janus_recorder_create(recordings_path, 1, rec->vrc_file);
+ }
+ session->recorder = TRUE;
+ session->recording = rec;
+ janus_mutex_lock(&recordings_mutex);
+ g_hash_table_insert(recordings, GINT_TO_POINTER(rec->id), rec);
+ janus_mutex_unlock(&recordings_mutex);
+ /* We need to prepare an answer */
+ int opus_pt = 0, vp8_pt = 0;
+ opus_pt = janus_get_opus_pt(msg->sdp);
+ JANUS_LOG(LOG_VERB, "Opus payload type is %d\n", opus_pt);
+ vp8_pt = janus_get_vp8_pt(msg->sdp);
+ JANUS_LOG(LOG_VERB, "VP8 payload type is %d\n", vp8_pt);
+ char sdptemp[1024], audio_mline[256], video_mline[512];
+ if(opus_pt > 0) {
+ g_snprintf(audio_mline, 256, sdp_a_template,
+ opus_pt, /* Opus payload type */
+ "recvonly", /* Recording is recvonly */
+ opus_pt); /* Opus payload type */
+ } else {
+ audio_mline[0] = '\0';
+ }
+ if(vp8_pt > 0) {
+ g_snprintf(video_mline, 512, sdp_v_template,
+ vp8_pt, /* VP8 payload type */
+ "recvonly", /* Recording is recvonly */
+ vp8_pt, /* VP8 payload type */
+ vp8_pt, /* VP8 payload type */
+ vp8_pt, /* VP8 payload type */
+ vp8_pt, /* VP8 payload type */
+ vp8_pt); /* VP8 payload type */
+ } else {
+ video_mline[0] = '\0';
+ }
+ g_snprintf(sdptemp, 1024, sdp_template,
+ janus_get_monotonic_time(), /* We need current time here */
+ janus_get_monotonic_time(), /* We need current time here */
+ session->recording->name, /* Playout session */
+ audio_mline, /* Audio m-line, if any */
+ video_mline); /* Video m-line, if any */
+ sdp = g_strdup(sdptemp);
+ JANUS_LOG(LOG_VERB, "Going to answer this SDP:\n%s\n", sdp);
+ /* Done! */
+ result = json_object();
+ json_object_set_new(result, "status", json_string("recording"));
+ json_object_set_new(result, "id", json_integer(id));
+ } else if(!strcasecmp(request_text, "play")) {
+ if(msg->sdp) {
+ JANUS_LOG(LOG_ERR, "A play request can't contain an SDP\n");
+ error_code = JANUS_RECORDPLAY_ERROR_INVALID_ELEMENT;
+ g_snprintf(error_cause, 512, "A play request can't contain an SDP");
+ goto error;
+ }
+ JANUS_LOG(LOG_VERB, "Replaying a recording\n");
+ json_t *id = json_object_get(root, "id");
+ if(!id) {
+ JANUS_LOG(LOG_ERR, "Missing element (id)\n");
+ error_code = JANUS_RECORDPLAY_ERROR_MISSING_ELEMENT;
+ g_snprintf(error_cause, 512, "Missing element (id)");
+ goto error;
+ }
+ if(!json_is_integer(id)) {
+ JANUS_LOG(LOG_ERR, "Invalid element (id should be an integer)\n");
+ error_code = JANUS_RECORDPLAY_ERROR_INVALID_ELEMENT;
+ g_snprintf(error_cause, 512, "Invalid element (id should be an integer)");
+ goto error;
+ }
+ guint64 id_value = json_integer_value(id);
+ /* Look for this recording */
+ janus_mutex_lock(&recordings_mutex);
+ janus_recordplay_recording *rec = g_hash_table_lookup(recordings, GINT_TO_POINTER(id_value));
+ janus_mutex_unlock(&recordings_mutex);
+ if(rec == NULL) {
+ JANUS_LOG(LOG_ERR, "No such recording\n");
+ error_code = JANUS_RECORDPLAY_ERROR_NOT_FOUND;
+ g_snprintf(error_cause, 512, "No such recording");
+ goto error;
+ }
+ /* Access the frames */
+ if(rec->arc_file) {
+ session->aframes = janus_recordplay_get_frames(recordings_path, rec->arc_file);
+ if(session->aframes == NULL) {
+ JANUS_LOG(LOG_ERR, "Error opening audio recording\n");
+ error_code = JANUS_RECORDPLAY_ERROR_INVALID_RECORDING;
+ g_snprintf(error_cause, 512, "Error opening audio recording");
+ goto error;
+ }
+ }
+ if(rec->vrc_file) {
+ session->vframes = janus_recordplay_get_frames(recordings_path, rec->vrc_file);
+ if(session->vframes == NULL) {
+ JANUS_LOG(LOG_ERR, "Error opening video recording\n");
+ error_code = JANUS_RECORDPLAY_ERROR_INVALID_RECORDING;
+ g_snprintf(error_cause, 512, "Error opening video recording");
+ goto error;
+ }
+ }
+ session->recording = rec;
+ session->recorder = FALSE;
+ /* We need to prepare an offer */
+ char sdptemp[1024], audio_mline[256], video_mline[512];
+ if(session->recording->arc_file) {
+ g_snprintf(audio_mline, 256, sdp_a_template,
+ OPUS_PT, /* Opus payload type */
+ "sendonly", /* Playout is sendonly */
+ OPUS_PT); /* Opus payload type */
+ } else {
+ audio_mline[0] = '\0';
+ }
+ if(session->recording->vrc_file) {
+ g_snprintf(video_mline, 512, sdp_v_template,
+ VP8_PT, /* VP8 payload type */
+ "sendonly", /* Playout is sendonly */
+ VP8_PT, /* VP8 payload type */
+ VP8_PT, /* VP8 payload type */
+ VP8_PT, /* VP8 payload type */
+ VP8_PT, /* VP8 payload type */
+ VP8_PT); /* VP8 payload type */
+ } else {
+ video_mline[0] = '\0';
+ }
+ g_snprintf(sdptemp, 1024, sdp_template,
+ janus_get_monotonic_time(), /* We need current time here */
+ janus_get_monotonic_time(), /* We need current time here */
+ session->recording->name, /* Playout session */
+ audio_mline, /* Audio m-line, if any */
+ video_mline); /* Video m-line, if any */
+ sdp = g_strdup(sdptemp);
+ JANUS_LOG(LOG_VERB, "Going to offer this SDP:\n%s\n", sdp);
+ /* Done! */
+ result = json_object();
+ json_object_set_new(result, "status", json_string("preparing"));
+ json_object_set_new(result, "id", json_integer(id_value));
+ } else if(!strcasecmp(request_text, "start")) {
+ if(!session->aframes && !session->vframes) {
+ JANUS_LOG(LOG_ERR, "Not a playout session, can't start\n");
+ error_code = JANUS_RECORDPLAY_ERROR_INVALID_STATE;
+ g_snprintf(error_cause, 512, "Not a playout session, can't start");
+ }
+ /* Just a final message we make use of, e.g., to receive an ANSWER to our OFFER for a playout */
+ if(!msg->sdp) {
+ JANUS_LOG(LOG_ERR, "Missing SDP offer\n");
+ error_code = JANUS_RECORDPLAY_ERROR_MISSING_ELEMENT;
+ g_snprintf(error_cause, 512, "Missing SDP offer");
+ goto error;
+ }
+ /* Done! */
+ result = json_object();
+ json_object_set_new(result, "status", json_string("playing"));
+ } else if(!strcasecmp(request_text, "stop")) {
+ /* Stop the recording/playout */
+ session->active = FALSE;
+ if(session->arc) {
+ janus_recorder_close(session->arc);
+ JANUS_LOG(LOG_INFO, "Closed audio recording %s\n", session->arc->filename ? session->arc->filename : "??");
+ }
+ session->arc = NULL;
+ if(session->vrc) {
+ janus_recorder_close(session->vrc);
+ JANUS_LOG(LOG_INFO, "Closed video recording %s\n", session->vrc->filename ? session->vrc->filename : "??");
+ }
+ session->vrc = NULL;
+ if(session->recorder) {
+ /* Create a .nfo file for this recording */
+ char nfofile[1024], nfo[1024];
+ g_snprintf(nfofile, 1024, "%s/%"SCNu64".nfo", recordings_path, session->recording->id);
+ FILE *file = fopen(nfofile, "wt");
+ if(file == NULL) {
+ JANUS_LOG(LOG_ERR, "Error creating file %s...\n", nfofile);
+ } else {
+ if(session->recording->arc_file && session->recording->vrc_file) {
+ g_snprintf(nfo, 1024,
+ "[%"SCNu64"]\r\n"
+ "name = %s\r\n"
+ "date = %s\r\n"
+ "audio = %s.mjr\r\n"
+ "video = %s.mjr\r\n",
+ session->recording->id, session->recording->name, session->recording->date,
+ session->recording->arc_file, session->recording->vrc_file);
+ } else if(session->recording->arc_file) {
+ g_snprintf(nfo, 1024,
+ "[%"SCNu64"]\r\n"
+ "name = %s\r\n"
+ "date = %s\r\n"
+ "audio = %s.mjr\r\n",
+ session->recording->id, session->recording->name, session->recording->date,
+ session->recording->arc_file);
+ } else if(session->recording->vrc_file) {
+ g_snprintf(nfo, 1024,
+ "[%"SCNu64"]\r\n"
+ "name = %s\r\n"
+ "date = %s\r\n"
+ "video = %s.mjr\r\n",
+ session->recording->id, session->recording->name, session->recording->date,
+ session->recording->vrc_file);
+ }
+ /* Write to the file now */
+ fwrite(nfo, strlen(nfo), sizeof(char), file);
+ fclose(file);
+ }
+ }
+ /* Done! */
+ result = json_object();
+ json_object_set_new(result, "status", json_string("stopped"));
+ if(session->recording)
+ json_object_set_new(result, "id", json_integer(session->recording->id));
+ } else {
+ JANUS_LOG(LOG_ERR, "Unknown request '%s'\n", request_text);
+ error_code = JANUS_RECORDPLAY_ERROR_INVALID_REQUEST;
+ g_snprintf(error_cause, 512, "Unknown request '%s'", request_text);
+ goto error;
+ }
+
+ /* Any SDP to handle? */
+ if(msg->sdp) {
+ session->firefox = strstr(msg->sdp, "Mozilla") ? TRUE : FALSE;
+ JANUS_LOG(LOG_VERB, "This is involving a negotiation (%s) as well:\n%s\n", msg->sdp_type, msg->sdp);
+ }
+
+ /* Prepare JSON event */
+ event = json_object();
+ json_object_set_new(event, "recordplay", json_string("event"));
+ if(result != NULL)
+ json_object_set(event, "result", result);
+ char *event_text = json_dumps(event, JSON_INDENT(3) | JSON_PRESERVE_ORDER);
+ json_decref(event);
+ JANUS_LOG(LOG_VERB, "Pushing event: %s\n", event_text);
+ if(!sdp) {
+ int ret = gateway->push_event(msg->handle, &janus_recordplay_plugin, msg->transaction, event_text, NULL, NULL);
+ JANUS_LOG(LOG_VERB, " >> %d (%s)\n", ret, janus_get_api_error(ret));
+ } else {
+ const char *type = session->recorder ? "answer" : "offer";
+ /* How long will the gateway take to push the event? */
+ gint64 start = janus_get_monotonic_time();
+ int res = gateway->push_event(msg->handle, &janus_recordplay_plugin, msg->transaction, event_text, type, sdp);
+ JANUS_LOG(LOG_VERB, " >> Pushing event: %d (took %"SCNu64" us)\n",
+ res, janus_get_monotonic_time()-start);
+ }
+ g_free(event_text);
+ janus_recordplay_message_free(msg);
+ continue;
+
+error:
+ {
+ /* Prepare JSON error event */
+ json_t *event = json_object();
+ json_object_set_new(event, "recordplay", json_string("event"));
+ json_object_set_new(event, "error_code", json_integer(error_code));
+ json_object_set_new(event, "error", json_string(error_cause));
+ char *event_text = json_dumps(event, JSON_INDENT(3) | JSON_PRESERVE_ORDER);
+ json_decref(event);
+ JANUS_LOG(LOG_VERB, "Pushing event: %s\n", event_text);
+ int ret = gateway->push_event(msg->handle, &janus_recordplay_plugin, msg->transaction, event_text, NULL, NULL);
+ JANUS_LOG(LOG_VERB, " >> %d (%s)\n", ret, janus_get_api_error(ret));
+ g_free(event_text);
+ janus_recordplay_message_free(msg);
+ }
+ }
+ g_free(error_cause);
+ JANUS_LOG(LOG_VERB, "Leaving thread\n");
+ return NULL;
+}
+
+void janus_recordplay_update_recordings_list(void) {
+ if(recordings_path == NULL)
+ return;
+ JANUS_LOG(LOG_VERB, "Updating recordings list in %s\n", recordings_path);
+ janus_mutex_lock(&recordings_mutex);
+ /* Open dir */
+ DIR *dir = opendir(recordings_path);
+ if(!dir) {
+ JANUS_LOG(LOG_ERR, "Couldn't open folder...\n");
+ return;
+ }
+ struct dirent *recent = NULL;
+ char recpath[1024];
+ while((recent = readdir(dir))) {
+ int len = strlen(recent->d_name);
+ if(len < 4)
+ continue;
+ if(strcasecmp(recent->d_name+len-4, ".nfo"))
+ continue;
+ JANUS_LOG(LOG_VERB, "Importing recording '%s'...\n", recent->d_name);
+ memset(recpath, 0, 1024);
+ g_snprintf(recpath, 1024, "%s/%s", recordings_path, recent->d_name);
+ janus_config *nfo = janus_config_parse(recpath);
+ if(nfo == NULL) {
+ JANUS_LOG(LOG_ERR, "Invalid recording '%s'...\n", recent->d_name);
+ continue;
+ }
+ janus_config_category *cat = janus_config_get_categories(nfo);
+ if(cat == NULL) {
+ JANUS_LOG(LOG_WARN, "No recording info in '%s', skipping...\n", recent->d_name);
+ janus_config_destroy(nfo);
+ continue;
+ }
+ guint64 id = atol(cat->name);
+ if(id == 0) {
+ JANUS_LOG(LOG_WARN, "Invalid ID, skipping...\n");
+ janus_config_destroy(nfo);
+ continue;
+ }
+ janus_config_item *name = janus_config_get_item(cat, "name");
+ janus_config_item *date = janus_config_get_item(cat, "date");
+ janus_config_item *audio = janus_config_get_item(cat, "audio");
+ janus_config_item *video = janus_config_get_item(cat, "video");
+ if(!name || !name->value || !date || !date->value) {
+ JANUS_LOG(LOG_WARN, "Invalid info, skipping...\n");
+ janus_config_destroy(nfo);
+ continue;
+ }
+ if((!audio || !audio->value) && (!video || !video->value)) {
+ JANUS_LOG(LOG_WARN, "No audio and no video, skipping...\n");
+ janus_config_destroy(nfo);
+ continue;
+ }
+ janus_recordplay_recording *rec = (janus_recordplay_recording *)calloc(1, sizeof(janus_recordplay_recording));
+ rec->id = id;
+ rec->name = g_strdup(name->value);
+ rec->date = g_strdup(date->value);
+ rec->arc_file = g_strdup(audio->value);
+ if(strstr(rec->arc_file, ".mjr")) {
+ char *ext = strstr(rec->arc_file, ".mjr");
+ *ext = '\0';
+ }
+ rec->vrc_file = g_strdup(video->value);
+ if(strstr(rec->vrc_file, ".mjr")) {
+ char *ext = strstr(rec->vrc_file, ".mjr");
+ *ext = '\0';
+ }
+
+ janus_config_destroy(nfo);
+
+ /* FIXME We should clean previous recordings with the same ID */
+ g_hash_table_remove(recordings, GUINT_TO_POINTER(id));
+ g_hash_table_insert(recordings, GUINT_TO_POINTER(id), rec);
+ }
+ closedir(dir);
+ janus_mutex_unlock(&recordings_mutex);
+}
+
+janus_recordplay_frame_packet *janus_recordplay_get_frames(const char *dir, const char *filename) {
+ if(!dir || !filename)
+ return NULL;
+ /* Open the file */
+ char source[1024];
+ if(strstr(filename, ".mjr"))
+ g_snprintf(source, 1024, "%s/%s", dir, filename);
+ else
+ g_snprintf(source, 1024, "%s/%s.mjr", dir, filename);
+ FILE *file = fopen(source, "rb");
+ if(file == NULL) {
+ JANUS_LOG(LOG_ERR, "Could not open file %s\n", source);
+ return NULL;
+ }
+ fseek(file, 0L, SEEK_END);
+ long fsize = ftell(file);
+ fseek(file, 0L, SEEK_SET);
+ JANUS_LOG(LOG_VERB, "File is %zu bytes\n", fsize);
+
+ /* Pre-parse */
+ JANUS_LOG(LOG_VERB, "Pre-parsing file %s to generate ordered index...\n", source);
+ int bytes = 0;
+ long offset = 0;
+ uint16_t len = 0, count = 0;
+ uint32_t first_ts = 0, last_ts = 0, reset = 0; /* To handle whether there's a timestamp reset in the recording */
+ char prebuffer[1500];
+ memset(prebuffer, 0, 1500);
+ /* Let's look for timestamp resets first */
+ while(offset < fsize) {
+ /* Read frame header */
+ fseek(file, offset, SEEK_SET);
+ bytes = fread(prebuffer, sizeof(char), 8, file);
+ if(bytes != 8 || prebuffer[0] != 'M') {
+ JANUS_LOG(LOG_ERR, "Invalid header...\n");
+ fclose(file);
+ return NULL;
+ }
+ offset += 8;
+ bytes = fread(&len, sizeof(uint16_t), 1, file);
+ len = ntohs(len);
+ offset += 2;
+ if(len == 5) {
+ /* This is the main header */
+ bytes = fread(prebuffer, sizeof(char), 5, file);
+ if(prebuffer[0] == 'v') {
+ JANUS_LOG(LOG_VERB, "This is a video recording, assuming VP8\n");
+ } else if(prebuffer[0] == 'a') {
+ JANUS_LOG(LOG_VERB, "This is an audio recording, assuming Opus\n");
+ } else {
+ JANUS_LOG(LOG_ERR, "Unsupported recording media type...\n");
+ fclose(file);
+ return NULL;
+ }
+ offset += len;
+ continue;
+ } else if(len < 12) {
+ /* Not RTP, skip */
+ offset += len;
+ continue;
+ }
+ /* Only read RTP header */
+ bytes = fread(prebuffer, sizeof(char), 16, file);
+ rtp_header *rtp = (rtp_header *)prebuffer;
+ if(last_ts == 0) {
+ first_ts = ntohl(rtp->timestamp);
+ if(first_ts > 1000*1000) /* Just used to check whether a packet is pre- or post-reset */
+ first_ts -= 1000*1000;
+ } else {
+ if(ntohl(rtp->timestamp) < last_ts) {
+ /* The new timestamp is smaller than the next one, is it a timestamp reset or simply out of order? */
+ if(last_ts-ntohl(rtp->timestamp) > 2*1000*1000*1000) {
+ reset = ntohl(rtp->timestamp);
+ JANUS_LOG(LOG_VERB, "Timestamp reset: %"SCNu32"\n", reset);
+ }
+ } else if(ntohl(rtp->timestamp) < reset) {
+ JANUS_LOG(LOG_VERB, "Updating timestamp reset: %"SCNu32" (was %"SCNu32")\n", ntohl(rtp->timestamp), reset);
+ reset = ntohl(rtp->timestamp);
+ }
+ }
+ last_ts = ntohl(rtp->timestamp);
+ /* Skip data for now */
+ offset += len;
+ }
+ /* Now let's parse the frames and order them */
+ offset = 0;
+ janus_recordplay_frame_packet *list = NULL, *last = NULL;
+ while(offset < fsize) {
+ /* Read frame header */
+ fseek(file, offset, SEEK_SET);
+ bytes = fread(prebuffer, sizeof(char), 8, file);
+ prebuffer[8] = '\0';
+ JANUS_LOG(LOG_HUGE, "Header: %s\n", prebuffer);
+ offset += 8;
+ bytes = fread(&len, sizeof(uint16_t), 1, file);
+ len = ntohs(len);
+ JANUS_LOG(LOG_HUGE, " -- Length: %"SCNu16"\n", len);
+ offset += 2;
+ if(len < 12) {
+ /* Not RTP, skip */
+ JANUS_LOG(LOG_HUGE, " -- Not RTP, skipping\n");
+ offset += len;
+ continue;
+ }
+ /* Only read RTP header */
+ bytes = fread(prebuffer, sizeof(char), 16, file);
+ rtp_header *rtp = (rtp_header *)prebuffer;
+ JANUS_LOG(LOG_HUGE, " -- RTP packet (ssrc=%"SCNu32", pt=%"SCNu16", ext=%"SCNu16", seq=%"SCNu16", ts=%"SCNu32")\n",
+ ntohl(rtp->ssrc), rtp->type, rtp->extension, ntohs(rtp->seq_number), ntohl(rtp->timestamp));
+ /* Generate frame packet and insert in the ordered list */
+ janus_recordplay_frame_packet *p = calloc(1, sizeof(janus_recordplay_frame_packet));
+ if(p == NULL) {
+ JANUS_LOG(LOG_ERR, "Memory error!\n");
+ fclose(file);
+ return NULL;
+ }
+ p->seq = ntohs(rtp->seq_number);
+ if(reset == 0) {
+ /* Simple enough... */
+ p->ts = ntohl(rtp->timestamp);
+ } else {
+ /* Is this packet pre- or post-reset? */
+ if(ntohl(rtp->timestamp) > first_ts) {
+ /* Pre-reset... */
+ p->ts = ntohl(rtp->timestamp);
+ } else {
+ /* Post-reset... */
+ uint64_t max32 = UINT32_MAX;
+ max32++;
+ p->ts = max32+ntohl(rtp->timestamp);
+ }
+ }
+ p->len = len;
+ p->offset = offset;
+ p->next = NULL;
+ p->prev = NULL;
+ if(list == NULL) {
+ /* First element becomes the list itself (and the last item), at least for now */
+ list = p;
+ last = p;
+ } else {
+ /* Check where we should insert this, starting from the end */
+ int added = 0;
+ janus_recordplay_frame_packet *tmp = last;
+ while(tmp) {
+ if(tmp->ts < p->ts) {
+ /* The new timestamp is greater than the last one we have, append */
+ added = 1;
+ if(tmp->next != NULL) {
+ /* We're inserting */
+ tmp->next->prev = p;
+ p->next = tmp->next;
+ } else {
+ /* Update the last packet */
+ last = p;
+ }
+ tmp->next = p;
+ p->prev = tmp;
+ break;
+ } else if(tmp->ts == p->ts) {
+ /* Same timestamp, check the sequence number */
+ if(tmp->seq < p->seq && (abs(tmp->seq - p->seq) < 10000)) {
+ /* The new sequence number is greater than the last one we have, append */
+ added = 1;
+ if(tmp->next != NULL) {
+ /* We're inserting */
+ tmp->next->prev = p;
+ p->next = tmp->next;
+ } else {
+ /* Update the last packet */
+ last = p;
+ }
+ tmp->next = p;
+ p->prev = tmp;
+ break;
+ } else if(tmp->seq > p->seq && (abs(tmp->seq - p->seq) > 10000)) {
+ /* The new sequence number (resetted) is greater than the last one we have, append */
+ added = 1;
+ if(tmp->next != NULL) {
+ /* We're inserting */
+ tmp->next->prev = p;
+ p->next = tmp->next;
+ } else {
+ /* Update the last packet */
+ last = p;
+ }
+ tmp->next = p;
+ p->prev = tmp;
+ break;
+ }
+ }
+ /* If either the timestamp ot the sequence number we just got is smaller, keep going back */
+ tmp = tmp->prev;
+ }
+ if(!added) {
+ /* We reached the start */
+ p->next = list;
+ list->prev = p;
+ list = p;
+ }
+ }
+ /* Skip data for now */
+ offset += len;
+ count++;
+ }
+
+ JANUS_LOG(LOG_VERB, "Counted %"SCNu16" RTP packets\n", count);
+ janus_recordplay_frame_packet *tmp = list;
+ count = 0;
+ while(tmp) {
+ count++;
+ JANUS_LOG(LOG_HUGE, "[%10lu][%4d] seq=%"SCNu16", ts=%"SCNu64"\n", tmp->offset, tmp->len, tmp->seq, tmp->ts);
+ tmp = tmp->next;
+ }
+ JANUS_LOG(LOG_VERB, "Counted %"SCNu16" frame packets\n", count);
+
+ /* Done! */
+ fclose(file);
+ return list;
+}
+
+static void *janus_recordplay_playout_thread(void *data) {
+ janus_recordplay_session *session = (janus_recordplay_session *)data;
+ if(!session) {
+ JANUS_LOG(LOG_ERR, "Invalid session, can't start playout thread...\n");
+ return NULL;
+ }
+ if(session->recorder) {
+ JANUS_LOG(LOG_ERR, "This is a recorder, can't start playout thread...\n");
+ return NULL;
+ }
+ if(!session->aframes || !session->vframes) {
+ JANUS_LOG(LOG_ERR, "No audio and no video frames, can't start playout thread...\n");
+ return NULL;
+ }
+ JANUS_LOG(LOG_INFO, "Joining playout thread\n");
+ /* Open the files */
+ FILE *afile = NULL, *vfile = NULL;
+ if(session->aframes) {
+ char source[1024];
+ if(strstr(session->recording->arc_file, ".mjr"))
+ g_snprintf(source, 1024, "%s/%s", recordings_path, session->recording->arc_file);
+ else
+ g_snprintf(source, 1024, "%s/%s.mjr", recordings_path, session->recording->arc_file);
+ afile = fopen(source, "rb");
+ if(afile == NULL) {
+ JANUS_LOG(LOG_ERR, "Could not open audio file %s, can't start playout thread...\n", source);
+ return NULL;
+ }
+ }
+ if(session->vframes) {
+ char source[1024];
+ if(strstr(session->recording->vrc_file, ".mjr"))
+ g_snprintf(source, 1024, "%s/%s", recordings_path, session->recording->vrc_file);
+ else
+ g_snprintf(source, 1024, "%s/%s.mjr", recordings_path, session->recording->vrc_file);
+ vfile = fopen(source, "rb");
+ if(vfile == NULL) {
+ JANUS_LOG(LOG_ERR, "Could not open video file %s, can't start playout thread...\n", source);
+ if(afile)
+ fclose(afile);
+ afile = NULL;
+ return NULL;
+ }
+ }
+
+ /* Timer */
+ gboolean asent = FALSE, vsent = FALSE;
+ struct timeval now, abefore, vbefore;
+ time_t d_s, d_us;
+ gettimeofday(&now, NULL);
+ gettimeofday(&abefore, NULL);
+ gettimeofday(&vbefore, NULL);
+
+ janus_recordplay_frame_packet *audio = session->aframes, *video = session->vframes;
+ char *buffer = (char *)calloc(1500, sizeof(char));
+ memset(buffer, 0, 1500);
+ int bytes = 0;
+ int64_t ts_diff = 0, passed = 0;
+
+ while(!session->destroyed && session->active && (audio || video)) {
+ if(!asent && !vsent) {
+ /* We skipped the last round, so sleep a bit (5ms) */
+ usleep(5000);
+ }
+ asent = FALSE;
+ vsent = FALSE;
+ if(audio) {
+ if(audio == session->aframes) {
+ /* First packet, send now */
+ fseek(afile, audio->offset, SEEK_SET);
+ bytes = fread(buffer, sizeof(char), audio->len, afile);
+ if(bytes != audio->len)
+ JANUS_LOG(LOG_WARN, "Didn't manage to read all the bytes we needed (%d < %d)...\n", bytes, audio->len);
+ /* Update payload type */
+ rtp_header *rtp = (rtp_header *)buffer;
+ rtp->type = OPUS_PT; /* FIXME We assume it's Opus */
+ if(gateway != NULL)
+ gateway->relay_rtp(session->handle, 0, (char *)buffer, bytes);
+ gettimeofday(&now, NULL);
+ abefore.tv_sec = now.tv_sec;
+ abefore.tv_usec = now.tv_usec;
+ asent = TRUE;
+ audio = audio->next;
+ } else {
+ /* What's the timestamp skip from the previous packet? */
+ ts_diff = audio->ts - audio->prev->ts;
+ ts_diff = (ts_diff*1000)/48; /* FIXME Again, we're assuming Opus and it's 48khz */
+ /* Check if it's time to send */
+ gettimeofday(&now, NULL);
+ d_s = now.tv_sec - abefore.tv_sec;
+ d_us = now.tv_usec - abefore.tv_usec;
+ if(d_us < 0) {
+ d_us += 1000000;
+ --d_s;
+ }
+ passed = d_s*1000000 + d_us;
+ if(passed < (ts_diff-5000)) {
+ asent = FALSE;
+ } else {
+ /* Update the reference time */
+ abefore.tv_usec += ts_diff%1000000;
+ if(abefore.tv_usec > 1000000) {
+ abefore.tv_sec++;
+ abefore.tv_usec -= 1000000;
+ }
+ if(ts_diff/1000000 > 0) {
+ abefore.tv_sec += ts_diff/1000000;
+ abefore.tv_usec -= ts_diff/1000000;
+ }
+ /* Send now */
+ fseek(afile, audio->offset, SEEK_SET);
+ bytes = fread(buffer, sizeof(char), audio->len, afile);
+ if(bytes != audio->len)
+ JANUS_LOG(LOG_WARN, "Didn't manage to read all the bytes we needed (%d < %d)...\n", bytes, audio->len);
+ /* Update payload type */
+ rtp_header *rtp = (rtp_header *)buffer;
+ rtp->type = OPUS_PT; /* FIXME We assume it's Opus */
+ if(gateway != NULL)
+ gateway->relay_rtp(session->handle, 0, (char *)buffer, bytes);
+ asent = TRUE;
+ audio = audio->next;
+ }
+ }
+ }
+ if(video) {
+ if(video == session->vframes) {
+ /* First packets: there may be many of them with the same timestamp, send them all */
+ uint64_t ts = video->ts;
+ while(video && video->ts == ts) {
+ fseek(vfile, video->offset, SEEK_SET);
+ bytes = fread(buffer, sizeof(char), video->len, vfile);
+ if(bytes != video->len)
+ JANUS_LOG(LOG_WARN, "Didn't manage to read all the bytes we needed (%d < %d)...\n", bytes, video->len);
+ /* Update payload type */
+ rtp_header *rtp = (rtp_header *)buffer;
+ rtp->type = VP8_PT; /* FIXME We assume it's VP8 */
+ if(gateway != NULL)
+ gateway->relay_rtp(session->handle, 1, (char *)buffer, bytes);
+ video = video->next;
+ }
+ vsent = TRUE;
+ gettimeofday(&now, NULL);
+ vbefore.tv_sec = now.tv_sec;
+ vbefore.tv_usec = now.tv_usec;
+ } else {
+ /* What's the timestamp skip from the previous packet? */
+ ts_diff = video->ts - video->prev->ts;
+ ts_diff = (ts_diff*1000)/90;
+ /* Check if it's time to send */
+ gettimeofday(&now, NULL);
+ d_s = now.tv_sec - vbefore.tv_sec;
+ d_us = now.tv_usec - vbefore.tv_usec;
+ if(d_us < 0) {
+ d_us += 1000000;
+ --d_s;
+ }
+ passed = d_s*1000000 + d_us;
+ if(passed < (ts_diff-5000)) {
+ vsent = FALSE;
+ } else {
+ /* Update the reference time */
+ vbefore.tv_usec += ts_diff%1000000;
+ if(vbefore.tv_usec > 1000000) {
+ vbefore.tv_sec++;
+ vbefore.tv_usec -= 1000000;
+ }
+ if(ts_diff/1000000 > 0) {
+ vbefore.tv_sec += ts_diff/1000000;
+ vbefore.tv_usec -= ts_diff/1000000;
+ }
+ /* There may be multiple packets with the same timestamp, send them all */
+ uint64_t ts = video->ts;
+ while(video && video->ts == ts) {
+ /* Send now */
+ fseek(vfile, video->offset, SEEK_SET);
+ bytes = fread(buffer, sizeof(char), video->len, vfile);
+ if(bytes != video->len)
+ JANUS_LOG(LOG_WARN, "Didn't manage to read all the bytes we needed (%d < %d)...\n", bytes, video->len);
+ /* Update payload type */
+ rtp_header *rtp = (rtp_header *)buffer;
+ rtp->type = VP8_PT; /* FIXME We assume it's VP8 */
+ if(gateway != NULL)
+ gateway->relay_rtp(session->handle, 1, (char *)buffer, bytes);
+ video = video->next;
+ }
+ vsent = TRUE;
+ }
+ }
+ }
+ }
+
+ /* Get rid of the indexes */
+ janus_recordplay_frame_packet *tmp = NULL;
+ audio = session->aframes;
+ while(audio) {
+ tmp = audio->next;
+ g_free(audio);
+ audio = tmp;
+ }
+ session->aframes = NULL;
+ video = session->vframes;
+ while(video) {
+ tmp = video->next;
+ g_free(video);
+ video = tmp;
+ }
+ session->vframes = NULL;
+
+ if(afile)
+ fclose(afile);
+ afile = NULL;
+ if(vfile)
+ fclose(vfile);
+ vfile = NULL;
+
+ /* Tell the core to tear down the PeerConnection, hangup_media will do the rest */
+ gateway->close_pc(session->handle);
+
+ JANUS_LOG(LOG_INFO, "Leaving playout thread\n");
+ return NULL;
+}
diff --git a/plugins/janus_sip.c b/plugins/janus_sip.c
index e1fdba68f7..5806172f2c 100644
--- a/plugins/janus_sip.c
+++ b/plugins/janus_sip.c
@@ -190,10 +190,11 @@ typedef struct janus_sip_session {
janus_sip_status status;
janus_sip_media media;
char *callee;
- gboolean destroy;
+ guint64 destroyed; /* Time at which this session was marked as destroyed */
janus_mutex mutex;
} janus_sip_session;
static GHashTable *sessions;
+static GList *old_sessions;
static janus_mutex sessions_mutex;
@@ -272,6 +273,45 @@ gboolean janus_sip_is_ignored(const char *ip) {
#define JANUS_SIP_ERROR_IO_ERROR 450
+/* SIP watchdog/garbage collector (sort of) */
+void *janus_sip_watchdog(void *data);
+void *janus_sip_watchdog(void *data) {
+ JANUS_LOG(LOG_INFO, "SIP watchdog started\n");
+ gint64 now = 0;
+ while(initialized && !stopping) {
+ janus_mutex_lock(&sessions_mutex);
+ /* Iterate on all the sessions */
+ now = janus_get_monotonic_time();
+ if(old_sessions != NULL) {
+ GList *sl = old_sessions;
+ JANUS_LOG(LOG_VERB, "Checking %d old sessions\n", g_list_length(old_sessions));
+ while(sl) {
+ janus_sip_session *session = (janus_sip_session *)sl->data;
+ if(!session || !initialized || stopping) {
+ sl = sl->next;
+ continue;
+ }
+ if(now-session->destroyed >= 5*G_USEC_PER_SEC) {
+ /* We're lazy and actually get rid of the stuff only after a few seconds */
+ GList *rm = sl->next;
+ old_sessions = g_list_delete_link(old_sessions, sl);
+ sl = rm;
+ session->handle = NULL;
+ g_free(session);
+ session = NULL;
+ continue;
+ }
+ sl = sl->next;
+ }
+ }
+ janus_mutex_unlock(&sessions_mutex);
+ g_usleep(2000000);
+ }
+ JANUS_LOG(LOG_INFO, "SIP watchdog stopped\n");
+ return NULL;
+}
+
+
/* Plugin implementation */
int janus_sip_init(janus_callbacks *callback, const char *config_path) {
if(stopping) {
@@ -377,6 +417,12 @@ int janus_sip_init(janus_callbacks *callback, const char *config_path) {
gateway = callback;
initialized = 1;
+ /* Start the sessions watchdog */
+ GThread *watchdog = g_thread_new("sip watchdog", &janus_sip_watchdog, NULL);
+ if(!watchdog) {
+ JANUS_LOG(LOG_FATAL, "Couldn't start SIP watchdog...\n");
+ return -1;
+ }
/* Launch the thread that will handle incoming messages */
GError *error = NULL;
handler_thread = g_thread_try_new("janus sip handler", janus_sip_handler, NULL, &error);
@@ -482,6 +528,7 @@ void janus_sip_create_session(janus_plugin_session *handle, int *error) {
session->media.remote_video_rtcp_port = 0;
session->media.video_ssrc = 0;
session->media.video_ssrc_peer = 0;
+ session->destroyed = 0;
su_home_init(session->stack->s_home);
janus_mutex_init(&session->mutex);
handle->plugin_handle = session;
@@ -504,7 +551,7 @@ void janus_sip_destroy_session(janus_plugin_session *handle, int *error) {
*error = -2;
return;
}
- if(session->destroy) {
+ if(session->destroyed) {
JANUS_LOG(LOG_VERB, "Session already destroyed...\n");
return;
}
@@ -516,7 +563,10 @@ void janus_sip_destroy_session(janus_plugin_session *handle, int *error) {
/* Shutdown the NUA */
nua_shutdown(session->stack->s_nua);
/* Cleaning up and removing the session is done in a lazy way */
- session->destroy = TRUE;
+ session->destroyed = janus_get_monotonic_time();
+ janus_mutex_lock(&sessions_mutex);
+ old_sessions = g_list_append(old_sessions, session);
+ janus_mutex_unlock(&sessions_mutex);
return;
}
@@ -549,7 +599,7 @@ void janus_sip_setup_media(janus_plugin_session *handle) {
JANUS_LOG(LOG_ERR, "No session associated with this handle...\n");
return;
}
- if(session->destroy)
+ if(session->destroyed)
return;
/* TODO Only relay RTP/RTCP when we get this event */
}
@@ -560,7 +610,7 @@ void janus_sip_incoming_rtp(janus_plugin_session *handle, int video, char *buf,
if(gateway) {
/* Honour the audio/video active flags */
janus_sip_session *session = (janus_sip_session *)handle->plugin_handle;
- if(!session || session->destroy) {
+ if(!session || session->destroyed) {
JANUS_LOG(LOG_ERR, "No session associated with this handle...\n");
return;
}
@@ -594,7 +644,7 @@ void janus_sip_incoming_rtcp(janus_plugin_session *handle, int video, char *buf,
return;
if(gateway) {
janus_sip_session *session = (janus_sip_session *)handle->plugin_handle;
- if(!session || session->destroy) {
+ if(!session || session->destroyed) {
JANUS_LOG(LOG_ERR, "No session associated with this handle...\n");
return;
}
@@ -629,7 +679,7 @@ void janus_sip_hangup_media(janus_plugin_session *handle) {
JANUS_LOG(LOG_ERR, "No session associated with this handle...\n");
return;
}
- if(session->destroy)
+ if(session->destroyed)
return;
if(session->status < janus_sip_status_inviting || session->status > janus_sip_status_incall)
return;
@@ -669,7 +719,7 @@ static void *janus_sip_handler(void *data) {
janus_sip_message_free(msg);
continue;
}
- if(session->destroy) {
+ if(session->destroyed) {
janus_sip_message_free(msg);
continue;
}
@@ -1991,7 +2041,7 @@ static void *janus_sip_relay_thread(void *data) {
FD_ZERO(&readfds);
char buffer[1500];
memset(buffer, 0, 1500);
- while(session != NULL && !session->destroy &&
+ while(session != NULL && !session->destroyed &&
session->status > janus_sip_status_registered &&
session->status < janus_sip_status_closing) { /* FIXME We need a per-call watchdog as well */
/* Wait for some data */
@@ -2008,7 +2058,7 @@ static void *janus_sip_relay_thread(void *data) {
resfd = select(maxfd+1, &readfds, NULL, NULL, &timeout);
if(resfd < 0)
break;
- if(session == NULL || session->destroy ||
+ if(session == NULL || session->destroyed ||
session->status <= janus_sip_status_registered ||
session->status >= janus_sip_status_closing)
break;
diff --git a/plugins/janus_streaming.c b/plugins/janus_streaming.c
index b5913d6992..3ca03c997a 100644
--- a/plugins/janus_streaming.c
+++ b/plugins/janus_streaming.c
@@ -244,9 +244,10 @@ typedef struct janus_streaming_session {
janus_streaming_mountpoint *mountpoint;
gboolean started;
gboolean stopping;
- gboolean destroy;
+ guint64 destroyed; /* Time at which this session was marked as destroyed */
} janus_streaming_session;
static GHashTable *sessions;
+static GList *old_sessions;
static janus_mutex sessions_mutex;
/* Packets we get from gstreamer and relay */
@@ -269,6 +270,45 @@ typedef struct janus_streaming_rtp_relay_packet {
#define JANUS_STREAMING_ERROR_UNKNOWN_ERROR 470
+/* Streaming watchdog/garbage collector (sort of) */
+void *janus_streaming_watchdog(void *data);
+void *janus_streaming_watchdog(void *data) {
+ JANUS_LOG(LOG_INFO, "Streaming watchdog started\n");
+ gint64 now = 0;
+ while(initialized && !stopping) {
+ janus_mutex_lock(&sessions_mutex);
+ /* Iterate on all the sessions */
+ now = janus_get_monotonic_time();
+ if(old_sessions != NULL) {
+ GList *sl = old_sessions;
+ JANUS_LOG(LOG_VERB, "Checking %d old sessions\n", g_list_length(old_sessions));
+ while(sl) {
+ janus_streaming_session *session = (janus_streaming_session *)sl->data;
+ if(!session || !initialized || stopping) {
+ sl = sl->next;
+ continue;
+ }
+ if(now-session->destroyed >= 5*G_USEC_PER_SEC) {
+ /* We're lazy and actually get rid of the stuff only after a few seconds */
+ GList *rm = sl->next;
+ old_sessions = g_list_delete_link(old_sessions, sl);
+ sl = rm;
+ session->handle = NULL;
+ g_free(session);
+ session = NULL;
+ continue;
+ }
+ sl = sl->next;
+ }
+ }
+ janus_mutex_unlock(&sessions_mutex);
+ g_usleep(2000000);
+ }
+ JANUS_LOG(LOG_INFO, "Streaming watchdog stopped\n");
+ return NULL;
+}
+
+
/* Plugin implementation */
int janus_streaming_init(janus_callbacks *callback, const char *config_path) {
if(stopping) {
@@ -506,6 +546,12 @@ int janus_streaming_init(janus_callbacks *callback, const char *config_path) {
gateway = callback;
initialized = 1;
+ /* Start the sessions watchdog */
+ GThread *watchdog = g_thread_new("streaming watchdog", &janus_streaming_watchdog, NULL);
+ if(!watchdog) {
+ JANUS_LOG(LOG_FATAL, "Couldn't start Streaming watchdog...\n");
+ return -1;
+ }
/* Launch the thread that will handle incoming messages */
GError *error = NULL;
handler_thread = g_thread_try_new("janus streaming handler", janus_streaming_handler, NULL, &error);
@@ -579,6 +625,7 @@ void janus_streaming_create_session(janus_plugin_session *handle, int *error) {
session->handle = handle;
session->mountpoint = NULL; /* This will happen later */
session->started = FALSE; /* This will happen later */
+ session->destroyed = 0;
handle->plugin_handle = session;
janus_mutex_lock(&sessions_mutex);
g_hash_table_insert(sessions, handle, session);
@@ -608,7 +655,10 @@ void janus_streaming_destroy_session(janus_plugin_session *handle, int *error) {
g_hash_table_remove(sessions, handle);
janus_mutex_unlock(&sessions_mutex);
/* Cleaning up and removing the session is done in a lazy way */
- session->destroy = TRUE;
+ session->destroyed = janus_get_monotonic_time();
+ janus_mutex_lock(&sessions_mutex);
+ old_sessions = g_list_append(old_sessions, session);
+ janus_mutex_unlock(&sessions_mutex);
return;
}
@@ -633,7 +683,7 @@ struct janus_plugin_result *janus_streaming_handle_message(janus_plugin_session
g_snprintf(error_cause, 512, "%s", "session associated with this handle...");
goto error;
}
- if(session->destroy) {
+ if(session->destroyed) {
JANUS_LOG(LOG_ERR, "Session has already been destroyed...\n");
error_code = JANUS_STREAMING_ERROR_UNKNOWN_ERROR;
g_snprintf(error_cause, 512, "%s", "Session has already been destroyed...");
@@ -1483,7 +1533,7 @@ void janus_streaming_setup_media(janus_plugin_session *handle) {
JANUS_LOG(LOG_ERR, "No session associated with this handle...\n");
return;
}
- if(session->destroy)
+ if(session->destroyed)
return;
/* TODO Only start streaming when we get this event */
session->started = TRUE;
@@ -1523,7 +1573,7 @@ void janus_streaming_hangup_media(janus_plugin_session *handle) {
JANUS_LOG(LOG_ERR, "No session associated with this handle...\n");
return;
}
- if(session->destroy)
+ if(session->destroyed)
return;
/* FIXME Simulate a "stop" coming from the browser */
janus_streaming_message *msg = calloc(1, sizeof(janus_streaming_message));
@@ -1561,7 +1611,7 @@ static void *janus_streaming_handler(void *data) {
janus_streaming_message_free(msg);
continue;
}
- if(session->destroy) {
+ if(session->destroyed) {
janus_streaming_message_free(msg);
continue;
}
@@ -2020,7 +2070,7 @@ static void *janus_streaming_ondemand_thread(void *data) {
/* Loop */
gint read = 0;
janus_streaming_rtp_relay_packet packet;
- while(!stopping && !session->stopping && !session->destroy) {
+ while(!stopping && !session->stopping && !session->destroyed) {
/* See if it's time to prepare a frame */
gettimeofday(&now, NULL);
d_s = now.tv_sec - before.tv_sec;
diff --git a/plugins/janus_videocall.c b/plugins/janus_videocall.c
index cbd1ccd90e..c6984e203d 100644
--- a/plugins/janus_videocall.c
+++ b/plugins/janus_videocall.c
@@ -32,6 +32,7 @@
#include "../config.h"
#include "../mutex.h"
#include "../rtcp.h"
+#include "../utils.h"
/* Plugin information */
@@ -132,9 +133,10 @@ typedef struct janus_videocall_session {
gboolean video_active;
uint64_t bitrate;
struct janus_videocall_session *peer;
- gboolean destroy;
+ guint64 destroyed; /* Time at which this session was marked as destroyed */
} janus_videocall_session;
static GHashTable *sessions;
+static GList *old_sessions;
static janus_mutex sessions_mutex;
@@ -155,6 +157,45 @@ static janus_mutex sessions_mutex;
#define JANUS_VIDEOCALL_ERROR_MISSING_SDP 482
+/* VideoCall watchdog/garbage collector (sort of) */
+void *janus_videocall_watchdog(void *data);
+void *janus_videocall_watchdog(void *data) {
+ JANUS_LOG(LOG_INFO, "VideoCall watchdog started\n");
+ gint64 now = 0;
+ while(initialized && !stopping) {
+ janus_mutex_lock(&sessions_mutex);
+ /* Iterate on all the sessions */
+ now = janus_get_monotonic_time();
+ if(old_sessions != NULL) {
+ GList *sl = old_sessions;
+ JANUS_LOG(LOG_VERB, "Checking %d old sessions\n", g_list_length(old_sessions));
+ while(sl) {
+ janus_videocall_session *session = (janus_videocall_session *)sl->data;
+ if(!session || !initialized || stopping) {
+ sl = sl->next;
+ continue;
+ }
+ if(now-session->destroyed >= 5*G_USEC_PER_SEC) {
+ /* We're lazy and actually get rid of the stuff only after a few seconds */
+ GList *rm = sl->next;
+ old_sessions = g_list_delete_link(old_sessions, sl);
+ sl = rm;
+ session->handle = NULL;
+ g_free(session);
+ session = NULL;
+ continue;
+ }
+ sl = sl->next;
+ }
+ }
+ janus_mutex_unlock(&sessions_mutex);
+ g_usleep(2000000);
+ }
+ JANUS_LOG(LOG_INFO, "VideoCall watchdog stopped\n");
+ return NULL;
+}
+
+
/* Plugin implementation */
int janus_videocall_init(janus_callbacks *callback, const char *config_path) {
if(stopping) {
@@ -184,6 +225,12 @@ int janus_videocall_init(janus_callbacks *callback, const char *config_path) {
gateway = callback;
initialized = 1;
+ /* Start the sessions watchdog */
+ GThread *watchdog = g_thread_new("vcall watchdog", &janus_videocall_watchdog, NULL);
+ if(!watchdog) {
+ JANUS_LOG(LOG_FATAL, "Couldn't start VideoCall watchdog...\n");
+ return -1;
+ }
/* Launch the thread that will handle incoming messages */
GError *error = NULL;
handler_thread = g_thread_try_new("janus videocall handler", janus_videocall_handler, NULL, &error);
@@ -256,6 +303,7 @@ void janus_videocall_create_session(janus_plugin_session *handle, int *error) {
session->bitrate = 0; /* No limit */
session->peer = NULL;
session->username = NULL;
+ session->destroyed = 0;
handle->plugin_handle = session;
return;
@@ -272,7 +320,7 @@ void janus_videocall_destroy_session(janus_plugin_session *handle, int *error) {
*error = -2;
return;
}
- if(session->destroy) {
+ if(session->destroyed) {
JANUS_LOG(LOG_VERB, "Session already destroyed...\n");
return;
}
@@ -285,7 +333,10 @@ void janus_videocall_destroy_session(janus_plugin_session *handle, int *error) {
janus_mutex_unlock(&sessions_mutex);
}
/* Cleaning up and removing the session is done in a lazy way */
- session->destroy = TRUE;
+ session->destroyed = janus_get_monotonic_time();
+ janus_mutex_lock(&sessions_mutex);
+ old_sessions = g_list_append(old_sessions, session);
+ janus_mutex_unlock(&sessions_mutex);
return;
}
@@ -318,7 +369,7 @@ void janus_videocall_setup_media(janus_plugin_session *handle) {
JANUS_LOG(LOG_ERR, "No session associated with this handle...\n");
return;
}
- if(session->destroy)
+ if(session->destroyed)
return;
/* We really don't care, as we only relay RTP/RTCP we get in the first place anyway */
}
@@ -337,7 +388,7 @@ void janus_videocall_incoming_rtp(janus_plugin_session *handle, int video, char
JANUS_LOG(LOG_ERR, "Session has no peer...\n");
return;
}
- if(session->destroy || session->peer->destroy)
+ if(session->destroyed || session->peer->destroyed)
return;
if((!video && session->audio_active) || (video && session->video_active)) {
gateway->relay_rtp(session->peer->handle, video, buf, len);
@@ -358,7 +409,7 @@ void janus_videocall_incoming_rtcp(janus_plugin_session *handle, int video, char
JANUS_LOG(LOG_ERR, "Session has no peer...\n");
return;
}
- if(session->destroy || session->peer->destroy)
+ if(session->destroyed || session->peer->destroyed)
return;
if(session->bitrate > 0)
janus_rtcp_cap_remb(buf, len, session->bitrate);
@@ -379,7 +430,7 @@ void janus_videocall_incoming_data(janus_plugin_session *handle, char *buf, int
JANUS_LOG(LOG_ERR, "Session has no peer...\n");
return;
}
- if(session->destroy || session->peer->destroy)
+ if(session->destroyed || session->peer->destroyed)
return;
if(buf == NULL || len <= 0)
return;
@@ -401,7 +452,7 @@ void janus_videocall_hangup_media(janus_plugin_session *handle) {
JANUS_LOG(LOG_ERR, "No session associated with this handle...\n");
return;
}
- if(session->destroy)
+ if(session->destroyed)
return;
janus_mutex_lock(&sessions_mutex);
if(session->peer) {
@@ -450,7 +501,7 @@ static void *janus_videocall_handler(void *data) {
janus_videocall_message_free(msg);
continue;
}
- if(session->destroy) {
+ if(session->destroyed) {
janus_videocall_message_free(msg);
continue;
}
@@ -590,7 +641,7 @@ static void *janus_videocall_handler(void *data) {
}
janus_mutex_lock(&sessions_mutex);
janus_videocall_session *peer = g_hash_table_lookup(sessions, username_text);
- if(peer == NULL || peer->destroy) {
+ if(peer == NULL || peer->destroyed) {
janus_mutex_unlock(&sessions_mutex);
JANUS_LOG(LOG_ERR, "Username '%s' doesn't exist\n", username_text);
error_code = JANUS_VIDEOCALL_ERROR_NO_SUCH_USERNAME;
diff --git a/plugins/janus_videoroom.c b/plugins/janus_videoroom.c
index 4be321593a..6cc8aded3c 100644
--- a/plugins/janus_videoroom.c
+++ b/plugins/janus_videoroom.c
@@ -305,10 +305,10 @@ int janus_videoroom_muxed_unsubscribe(janus_videoroom_listener_muxed *muxed_list
int janus_videoroom_muxed_offer(janus_videoroom_listener_muxed *muxed_listener, char *transaction, char *event_text);
-/* Videoroom watchdog/garbage collector (sort of) */
+/* VideoRoom watchdog/garbage collector (sort of) */
void *janus_videoroom_watchdog(void *data);
void *janus_videoroom_watchdog(void *data) {
- JANUS_LOG(LOG_INFO, "Videoroom watchdog started\n");
+ JANUS_LOG(LOG_INFO, "VideoRoom watchdog started\n");
gint64 now = 0;
while(initialized && !stopping) {
janus_mutex_lock(&sessions_mutex);
@@ -335,6 +335,8 @@ void *janus_videoroom_watchdog(void *data) {
} else if(session->participant_type == janus_videoroom_p_type_subscriber_muxed) {
janus_videoroom_muxed_listener_free(session->participant);
}
+ session->handle = NULL;
+ g_free(session);
continue;
}
sl = sl->next;
@@ -343,7 +345,7 @@ void *janus_videoroom_watchdog(void *data) {
janus_mutex_unlock(&sessions_mutex);
g_usleep(2000000);
}
- JANUS_LOG(LOG_INFO, "Videoroom watchdog stopped\n");
+ JANUS_LOG(LOG_INFO, "VideoRoom watchdog stopped\n");
return NULL;
}
@@ -464,7 +466,7 @@ int janus_videoroom_init(janus_callbacks *callback, const char *config_path) {
/* Start the sessions watchdog */
GThread *watchdog = g_thread_new("vroom watchdog", &janus_videoroom_watchdog, NULL);
if(!watchdog) {
- JANUS_LOG(LOG_FATAL, "Couldn't start videoroom watchdog...\n");
+ JANUS_LOG(LOG_FATAL, "Couldn't start VideoRoom watchdog...\n");
return -1;
}
/* Launch the thread that will handle incoming messages */
@@ -547,6 +549,7 @@ void janus_videoroom_create_session(janus_plugin_session *handle, int *error) {
session->handle = handle;
session->participant_type = janus_videoroom_p_type_none;
session->participant = NULL;
+ session->destroyed = 0;
handle->plugin_handle = session;
janus_mutex_lock(&sessions_mutex);
g_hash_table_insert(sessions, handle, session);
@@ -1726,6 +1729,7 @@ static void *janus_videoroom_handler(void *data) {
} else if(participant->recording_active && participant->sdp) {
/* We've started recording, send a PLI/FIR and go on */
char filename[255];
+ gint64 now = janus_get_monotonic_time();
if(strstr(participant->sdp, "m=audio")) {
memset(filename, 0, 255);
if(participant->recording_base) {
@@ -1738,7 +1742,7 @@ static void *janus_videoroom_handler(void *data) {
} else {
/* Build a filename */
g_snprintf(filename, 255, "videoroom-%"SCNu64"-user-%"SCNu64"-%"SCNi64"-audio",
- participant->room->room_id, participant->user_id, janus_get_monotonic_time());
+ participant->room->room_id, participant->user_id, now);
participant->arc = janus_recorder_create(participant->room->rec_dir, 0, filename);
if(participant->arc == NULL) {
JANUS_LOG(LOG_ERR, "Couldn't open an audio recording file for this publisher!\n");
@@ -1757,7 +1761,7 @@ static void *janus_videoroom_handler(void *data) {
} else {
/* Build a filename */
g_snprintf(filename, 255, "videoroom-%"SCNu64"-user-%"SCNu64"-%"SCNi64"-video",
- participant->room->room_id, participant->user_id, janus_get_monotonic_time());
+ participant->room->room_id, participant->user_id, now);
participant->vrc = janus_recorder_create(participant->room->rec_dir, 1, filename);
if(participant->vrc == NULL) {
JANUS_LOG(LOG_ERR, "Couldn't open an video recording file for this publisher!\n");
@@ -2144,6 +2148,7 @@ static void *janus_videoroom_handler(void *data) {
/* Is this room recorded? */
if(videoroom->record || participant->recording_active) {
char filename[255];
+ gint64 now = janus_get_monotonic_time();
if(audio) {
memset(filename, 0, 255);
if(participant->recording_base) {
@@ -2156,7 +2161,7 @@ static void *janus_videoroom_handler(void *data) {
} else {
/* Build a filename */
g_snprintf(filename, 255, "videoroom-%"SCNu64"-user-%"SCNu64"-%"SCNi64"-audio",
- videoroom->room_id, participant->user_id, janus_get_monotonic_time());
+ videoroom->room_id, participant->user_id, now);
participant->arc = janus_recorder_create(videoroom->rec_dir, 0, filename);
if(participant->arc == NULL) {
JANUS_LOG(LOG_ERR, "Couldn't open an audio recording file for this publisher!\n");
@@ -2175,7 +2180,7 @@ static void *janus_videoroom_handler(void *data) {
} else {
/* Build a filename */
g_snprintf(filename, 255, "videoroom-%"SCNu64"-user-%"SCNu64"-%"SCNi64"-video",
- videoroom->room_id, participant->user_id, janus_get_monotonic_time());
+ videoroom->room_id, participant->user_id, now);
participant->vrc = janus_recorder_create(videoroom->rec_dir, 1, filename);
if(participant->vrc == NULL) {
JANUS_LOG(LOG_ERR, "Couldn't open an video recording file for this publisher!\n");
diff --git a/plugins/janus_voicemail.c b/plugins/janus_voicemail.c
index 33615cf29d..db95fe53f4 100644
--- a/plugins/janus_voicemail.c
+++ b/plugins/janus_voicemail.c
@@ -137,9 +137,10 @@ typedef struct janus_voicemail_session {
int seq;
gboolean started;
gboolean stopping;
- gboolean destroy;
+ guint64 destroyed; /* Time at which this session was marked as destroyed */
} janus_voicemail_session;
static GHashTable *sessions;
+static GList *old_sessions;
static janus_mutex sessions_mutex;
static char *recordings_path = NULL;
@@ -180,6 +181,45 @@ int ogg_flush(janus_voicemail_session *session);
#define JANUS_VOICEMAIL_ERROR_LIBOGG_ERROR 467
+/* VoiceMail watchdog/garbage collector (sort of) */
+void *janus_voicemail_watchdog(void *data);
+void *janus_voicemail_watchdog(void *data) {
+ JANUS_LOG(LOG_INFO, "VoiceMail watchdog started\n");
+ gint64 now = 0;
+ while(initialized && !stopping) {
+ janus_mutex_lock(&sessions_mutex);
+ /* Iterate on all the sessions */
+ now = janus_get_monotonic_time();
+ if(old_sessions != NULL) {
+ GList *sl = old_sessions;
+ JANUS_LOG(LOG_VERB, "Checking %d old sessions\n", g_list_length(old_sessions));
+ while(sl) {
+ janus_voicemail_session *session = (janus_voicemail_session *)sl->data;
+ if(!session || !initialized || stopping) {
+ sl = sl->next;
+ continue;
+ }
+ if(now-session->destroyed >= 5*G_USEC_PER_SEC) {
+ /* We're lazy and actually get rid of the stuff only after a few seconds */
+ GList *rm = sl->next;
+ old_sessions = g_list_delete_link(old_sessions, sl);
+ sl = rm;
+ session->handle = NULL;
+ g_free(session);
+ session = NULL;
+ continue;
+ }
+ sl = sl->next;
+ }
+ }
+ janus_mutex_unlock(&sessions_mutex);
+ g_usleep(2000000);
+ }
+ JANUS_LOG(LOG_INFO, "VoiceMail watchdog stopped\n");
+ return NULL;
+}
+
+
/* Plugin implementation */
int janus_voicemail_init(janus_callbacks *callback, const char *config_path) {
if(stopping) {
@@ -226,15 +266,21 @@ int janus_voicemail_init(janus_callbacks *callback, const char *config_path) {
/* Create the folder, if needed */
struct stat st = {0};
if(stat(recordings_path, &st) == -1) {
- int res = mkdir(recordings_path, 0755);
+ int res = janus_mkdir(recordings_path, 0755);
JANUS_LOG(LOG_VERB, "Creating folder: %d\n", res);
- if(res < 0) {
+ if(res != 0) {
JANUS_LOG(LOG_ERR, "%s", strerror(res));
return -1; /* No point going on... */
}
}
initialized = 1;
+ /* Start the sessions watchdog */
+ GThread *watchdog = g_thread_new("vmail watchdog", &janus_voicemail_watchdog, NULL);
+ if(!watchdog) {
+ JANUS_LOG(LOG_FATAL, "Couldn't start VoiceMail watchdog...\n");
+ return -1;
+ }
/* Launch the thread that will handle incoming messages */
GError *error = NULL;
handler_thread = g_thread_try_new("janus voicemail handler", janus_voicemail_handler, NULL, &error);
@@ -318,7 +364,7 @@ void janus_voicemail_create_session(janus_plugin_session *handle, int *error) {
session->seq = 0;
session->started = FALSE;
session->stopping = FALSE;
- session->destroy = FALSE;
+ session->destroyed = 0;
handle->plugin_handle = session;
janus_mutex_lock(&sessions_mutex);
g_hash_table_insert(sessions, handle, session);
@@ -338,7 +384,7 @@ void janus_voicemail_destroy_session(janus_plugin_session *handle, int *error) {
*error = -2;
return;
}
- if(session->destroy) {
+ if(session->destroyed) {
JANUS_LOG(LOG_WARN, "Session already destroyed...\n");
return;
}
@@ -348,7 +394,10 @@ void janus_voicemail_destroy_session(janus_plugin_session *handle, int *error) {
janus_mutex_unlock(&sessions_mutex);
janus_voicemail_hangup_media(handle);
/* Cleaning up and removing the session is done in a lazy way */
- session->destroy = TRUE;
+ session->destroyed = janus_get_monotonic_time();
+ janus_mutex_lock(&sessions_mutex);
+ old_sessions = g_list_append(old_sessions, session);
+ janus_mutex_unlock(&sessions_mutex);
return;
}
@@ -382,7 +431,7 @@ void janus_voicemail_setup_media(janus_plugin_session *handle) {
JANUS_LOG(LOG_ERR, "No session associated with this handle...\n");
return;
}
- if(session->destroy)
+ if(session->destroyed)
return;
/* Only start recording this peer when we get this event */
session->start_time = janus_get_monotonic_time();
@@ -403,7 +452,7 @@ void janus_voicemail_incoming_rtp(janus_plugin_session *handle, int video, char
if(handle == NULL || handle->stopped || stopping || !initialized)
return;
janus_voicemail_session *session = (janus_voicemail_session *)handle->plugin_handle;
- if(!session || session->destroy || session->stopping || !session->started || session->start_time == 0)
+ if(!session || session->destroyed || session->stopping || !session->started || session->start_time == 0)
return;
gint64 now = janus_get_monotonic_time();
/* Have 10 seconds passed? */
@@ -451,10 +500,9 @@ void janus_voicemail_hangup_media(janus_plugin_session *handle) {
JANUS_LOG(LOG_ERR, "No session associated with this handle...\n");
return;
}
- if(session->destroy)
+ if(session->destroyed)
return;
session->started = FALSE;
- session->destroy = 1;
/* Close and reset stuff */
if(session->file)
fclose(session->file);
@@ -486,7 +534,7 @@ static void *janus_voicemail_handler(void *data) {
janus_voicemail_message_free(msg);
continue;
}
- if(session->destroy) {
+ if(session->destroyed) {
janus_voicemail_message_free(msg);
continue;
}
diff --git a/plugins/recordings/1234.nfo b/plugins/recordings/1234.nfo
new file mode 100644
index 0000000000..a8d055b92b
--- /dev/null
+++ b/plugins/recordings/1234.nfo
@@ -0,0 +1,5 @@
+[1234]
+name = Lorenzo says hello!
+date = 2014-10-22 14:44:36
+audio = rec-sample-audio.mjr
+video = rec-sample-video.mjr
diff --git a/plugins/recordings/rec-sample-audio.mjr b/plugins/recordings/rec-sample-audio.mjr
new file mode 100644
index 0000000000..7dbd386ad3
Binary files /dev/null and b/plugins/recordings/rec-sample-audio.mjr differ
diff --git a/plugins/recordings/rec-sample-video.mjr b/plugins/recordings/rec-sample-video.mjr
new file mode 100644
index 0000000000..d8c2127412
Binary files /dev/null and b/plugins/recordings/rec-sample-video.mjr differ
diff --git a/record.c b/record.c
index f09a0a5f9a..59dc259b16 100644
--- a/record.c
+++ b/record.c
@@ -22,6 +22,7 @@
#include "record.h"
#include "debug.h"
+#include "utils.h"
/* Frame header in the structured recording*/
@@ -44,7 +45,7 @@ janus_recorder *janus_recorder_create(char *dir, int video, char *filename) {
if(err == -1) {
if(ENOENT == errno) {
/* Directory does not exist, try creating it */
- if(mkdir(dir, 0755) < 0) {
+ if(janus_mkdir(dir, 0755) < 0) {
JANUS_LOG(LOG_ERR, "mkdir error: %d\n", errno);
return NULL;
}
@@ -55,7 +56,7 @@ janus_recorder *janus_recorder_create(char *dir, int video, char *filename) {
} else {
if(S_ISDIR(s.st_mode)) {
/* Directory exists */
- JANUS_LOG(LOG_INFO, "Directory exists: %s\n", dir);
+ JANUS_LOG(LOG_VERB, "Directory exists: %s\n", dir);
} else {
/* File exists but it's not a directory? */
JANUS_LOG(LOG_ERR, "Not a directory? %s\n", dir);
diff --git a/sdp.c b/sdp.c
index 16111a4ed4..fc265421ad 100644
--- a/sdp.c
+++ b/sdp.c
@@ -322,12 +322,19 @@ int janus_sdp_parse_candidate(janus_ice_stream *stream, const char *candidate, i
return -2;
janus_mutex_lock(&handle->mutex);
janus_ice_component *component = NULL;
- char rfoundation[32], rtransport[4], rip[24], rtype[6], rrelip[24];
+ char rfoundation[32], rtransport[4], rip[40], rtype[6], rrelip[40];
guint32 rcomponent, rpriority, rport, rrelport;
- int res = 0;
- if((res = sscanf(candidate, "%31s %30u %3s %30u %23s %30u typ %5s %*s %23s %*s %30u",
+ int res = sscanf(candidate, "%31s %30u %3s %30u %39s %30u typ %5s %*s %39s %*s %30u",
rfoundation, &rcomponent, rtransport, &rpriority,
- rip, &rport, rtype, rrelip, &rrelport)) >= 7) {
+ rip, &rport, rtype, rrelip, &rrelport);
+ if(res < 7) {
+ /* Failed to parse this address, can it be IPv6? */
+ if(!janus_ice_is_ipv6_enabled()) {
+ JANUS_LOG(LOG_WARN, "[%"SCNu64"] Received IPv6 candidate, but IPv6 support is disabled...\n", handle->handle_id);
+ return res;
+ }
+ }
+ if(res >= 7) {
/* Add remote candidate */
JANUS_LOG(LOG_VERB, "[%"SCNu64"] Adding remote candidate for component %d to stream %d\n", handle->handle_id, rcomponent, stream->stream_id);
component = g_hash_table_lookup(stream->components, GUINT_TO_POINTER(rcomponent));
diff --git a/utils.c b/utils.c
index f62c2a304e..9f05d273dd 100644
--- a/utils.c
+++ b/utils.c
@@ -11,7 +11,11 @@
#include
#include
+#include
+#include
+
#include "utils.h"
+#include "debug.h"
gint64 janus_get_monotonic_time(void) {
struct timespec ts;
@@ -127,3 +131,80 @@ char *janus_string_replace(char *message, const char *old_string, const char *ne
return outgoing;
}
}
+
+int janus_mkdir(const char *dir, mode_t mode) {
+ char tmp[256];
+ char *p = NULL;
+ size_t len;
+
+ int res = 0;
+ g_snprintf(tmp, sizeof(tmp), "%s", dir);
+ len = strlen(tmp);
+ if(tmp[len - 1] == '/')
+ tmp[len - 1] = 0;
+ for(p = tmp + 1; *p; p++) {
+ if(*p == '/') {
+ *p = 0;
+ res = mkdir(tmp, mode);
+ if(res != 0 && errno != EEXIST) {
+ JANUS_LOG(LOG_ERR, "Error creating folder %s\n", tmp);
+ return res;
+ }
+ *p = '/';
+ }
+ }
+ res = mkdir(tmp, mode);
+ if(res != 0 && errno != EEXIST)
+ return res;
+ return 0;
+}
+
+int janus_get_opus_pt(const char *sdp) {
+ if(!sdp)
+ return -1;
+ if(!strstr(sdp, "m=audio") || !strstr(sdp, "opus/48000")) /* FIXME Should be case insensitive */
+ return -1;
+ const char *line = strstr(sdp, "m=audio");
+ while(line) {
+ char *next = strchr(line, '\n');
+ if(next) {
+ *next = '\0';
+ if(strstr(line, "a=rtpmap") && strstr(line, "opus/48000")) {
+ /* Gotcha! */
+ int pt = 0;
+ if(sscanf(line, "a=rtpmap:%d opus/48000/2", &pt) == 1) {
+ *next = '\n';
+ return pt;
+ }
+ }
+ *next = '\n';
+ }
+ line = next ? (next+1) : NULL;
+ }
+ return -1;
+}
+
+int janus_get_vp8_pt(const char *sdp) {
+ if(!sdp)
+ return -1;
+ if(!strstr(sdp, "m=video") || !strstr(sdp, "VP8/90000")) /* FIXME Should be case insensitive */
+ return -1;
+ const char *line = strstr(sdp, "m=video");
+ while(line) {
+ char *next = strchr(line, '\n');
+ if(next) {
+ *next = '\0';
+ if(strstr(line, "a=rtpmap") && strstr(line, "VP8/90000")) {
+ /* Gotcha! */
+ int pt = 0;
+ if(sscanf(line, "a=rtpmap:%d VP8/90000", &pt) == 1) {
+ *next = '\n';
+ return pt;
+ }
+ }
+ *next = '\n';
+ }
+ line = next ? (next+1) : NULL;
+ }
+ return -1;
+}
diff --git a/utils.h b/utils.h
index ecafd63445..a00188d2da 100644
--- a/utils.h
+++ b/utils.h
@@ -58,7 +58,23 @@ void janus_flags_clear(janus_flags *flags, uint32_t flag);
* \param[in] flag The flag to check
* \returns true if the flag is set, false otherwise */
gboolean janus_flags_is_set(janus_flags *flags, uint32_t flag);
-
///@}
+/*! \brief Helper to create a new directory, and recursively create parent directories if needed
+ * @param dir Path to the new folder to create
+ * @param mode File permissions for the new directory file
+ * @returns An integer like the regular mkdir does
+ * @note A failure may indicate that creating any of the subdirectories failed: some may still have been created */
+int janus_mkdir(const char *dir, mode_t mode);
+
+/*! \brief Ugly and dirty helper to quickly get the Opus payload type in an SDP
+ * @param sdp The SDP to parse
+ * @returns The Opus payload type, if found, -1 otherwise */
+int janus_get_opus_pt(const char *sdp);
+
+/*! \brief Ugly and dirty helper to quickly get the VP8 payload type in an SDP
+ * @param sdp The SDP to parse
+ * @returns The VP8 payload type, if found, -1 otherwise */
+int janus_get_vp8_pt(const char *sdp);
+
#endif