diff --git a/html/recordplaytest.html b/html/recordplaytest.html index 290ef3bfdc..83595e019e 100644 --- a/html/recordplaytest.html +++ b/html/recordplaytest.html @@ -89,7 +89,11 @@

Recorder/Playout

-

Remote Video

+

+ Remote Video + + +

diff --git a/html/recordplaytest.js b/html/recordplaytest.js index 9000740458..74ee7bed95 100644 --- a/html/recordplaytest.js +++ b/html/recordplaytest.js @@ -453,6 +453,7 @@ function startRecording() { $('#list').unbind('click').attr('disabled', true); $('#recset').attr('disabled', true); $('#recslist').attr('disabled', true); + $('#pause-resume').removeClass('hide'); // bitrate and keyframe interval can be set at any time: // before, after, during recording @@ -497,6 +498,15 @@ function startRecording() { recordplay.hangup(); } }); + $('#pause-resume').unbind('click').on('click', function() { + if($(this).text() === 'Pause') { + recordplay.send({message: {request: 'pause'}}); + $(this).text('Resume'); + } else { + recordplay.send({message: {request: 'resume'}}); + $(this).text('Pause'); + } + }); }); } @@ -515,6 +525,7 @@ function startPlayout() { $('#list').unbind('click').attr('disabled', true); $('#recset').attr('disabled', true); $('#recslist').attr('disabled', true); + $('#pause-resume').addClass('hide'); var play = { request: "play", id: parseInt(selectedRecording) }; recordplay.send({ message: play }); } diff --git a/plugins/janus_recordplay.c b/plugins/janus_recordplay.c index 7055e4be40..571528d18e 100644 --- a/plugins/janus_recordplay.c +++ b/plugins/janus_recordplay.c @@ -418,6 +418,7 @@ typedef struct janus_recordplay_recording { char *offer; /* The SDP offer that will be sent to watchers */ gboolean e2ee; /* Whether media in the recording is encrypted, e.g., using Insertable Streams */ GList *viewers; /* List of users watching this recording */ + volatile gint paused; /* Whether this recording is paused */ volatile gint completed; /* Whether this recording was completed or still going on */ volatile gint destroyed; /* Whether this recording has been marked as destroyed */ janus_refcount ref; /* Reference counter */ @@ -1104,7 +1105,8 @@ struct janus_plugin_result *janus_recordplay_handle_message(janus_plugin_session json_object_set_new(response, "settings", settings); goto plugin_response; } else if(!strcasecmp(request_text, "record") || !strcasecmp(request_text, "play") - || !strcasecmp(request_text, "start") || !strcasecmp(request_text, "stop")) { + || !strcasecmp(request_text, "start") || !strcasecmp(request_text, "stop") + || !strcasecmp(request_text, "pause") || !strcasecmp(request_text, "resume")) { /* These messages are handled asynchronously */ janus_recordplay_message *msg = g_malloc(sizeof(janus_recordplay_message)); msg->handle = handle; @@ -1634,6 +1636,7 @@ static void *janus_recordplay_handler(void *data) { rec->acodec = JANUS_AUDIOCODEC_NONE; rec->vcodec = JANUS_VIDEOCODEC_NONE; rec->e2ee = e2ee; + g_atomic_int_set(&rec->paused, 0); g_atomic_int_set(&rec->destroyed, 0); g_atomic_int_set(&rec->completed, 0); janus_refcount_init(&rec->ref, janus_recordplay_recording_free); @@ -2007,6 +2010,35 @@ static void *janus_recordplay_handler(void *data) { } /* Tell the core to tear down the PeerConnection, hangup_media will do the rest */ gateway->close_pc(session->handle); + } else if (!strcasecmp(request_text, "pause") || !strcasecmp(request_text, "resume")) { + JANUS_LOG(LOG_VERB, "Record&Play: Got pause/resume request\n"); + if(session->recording) { + gboolean pause = !strcasecmp(request_text, "pause"); + result = json_object(); + json_object_set_new(result, "status", json_string(pause ? "paused" : "resumed")); + json_object_set_new(result, "id", json_integer(session->recording->id)); + /* Also notify event handlers */ + if(notify_events && gateway->events_is_enabled()) { + json_t *info = json_object(); + json_object_set_new(info, "event", json_string(pause ? "paused" : "resumed")); + json_object_set_new(info, "id", json_integer(session->recording->id)); + gateway->notify_event(&janus_recordplay_plugin, session->handle, info); + } + if (g_atomic_int_compare_and_exchange(&session->recording->paused, !pause, pause)) { + if(pause) { + janus_recorder_pause(session->arc); + janus_recorder_pause(session->vrc); + janus_recorder_pause(session->drc); + } else { + janus_recorder_resume(session->arc); + janus_recorder_resume(session->vrc); + janus_recorder_resume(session->drc); + gateway->send_pli(session->handle); + } + } + } else { + JANUS_LOG(LOG_VERB, "Record&Play: Not recording, ignoring pause/resume request\n"); + } } else { JANUS_LOG(LOG_ERR, "Unknown request '%s'\n", request_text); error_code = JANUS_RECORDPLAY_ERROR_INVALID_REQUEST; @@ -2203,6 +2235,7 @@ void janus_recordplay_update_recordings_list(void) { if(janus_recordplay_generate_offer(rec) < 0) { JANUS_LOG(LOG_WARN, "Could not generate offer for recording %"SCNu64"...\n", rec->id); } + g_atomic_int_set(&rec->paused, 0); g_atomic_int_set(&rec->destroyed, 0); g_atomic_int_set(&rec->completed, 1); janus_refcount_init(&rec->ref, janus_recordplay_recording_free); diff --git a/plugins/janus_sip.c b/plugins/janus_sip.c index f4e4c9368e..03106f2967 100644 --- a/plugins/janus_sip.c +++ b/plugins/janus_sip.c @@ -4357,10 +4357,11 @@ static void *janus_sip_handler(void *data) { goto error; json_t *action = json_object_get(root, "action"); const char *action_text = json_string_value(action); - if(strcasecmp(action_text, "start") && strcasecmp(action_text, "stop")) { - JANUS_LOG(LOG_ERR, "Invalid action (should be start|stop)\n"); + if(strcasecmp(action_text, "start") && strcasecmp(action_text, "stop") && + strcasecmp(action_text, "pause") && strcasecmp(action_text, "resume")) { + JANUS_LOG(LOG_ERR, "Invalid action (should be start|stop|pause|resume)\n"); error_code = JANUS_SIP_ERROR_INVALID_ELEMENT; - g_snprintf(error_cause, 512, "Invalid action (should be start|stop)"); + g_snprintf(error_cause, 512, "Invalid action (should be start|stop|pause|resume)"); goto error; } gboolean record_audio = FALSE, record_video = FALSE, /* No media is recorded by default */ @@ -4502,6 +4503,24 @@ static void *janus_sip_handler(void *data) { gateway->send_pli(session->handle); } } + } else if(!strcasecmp(action_text, "pause")) { + if(record_audio) + janus_recorder_pause(session->arc); + if(record_video) + janus_recorder_pause(session->vrc); + if(record_peer_audio) + janus_recorder_pause(session->arc_peer); + if(record_peer_video) + janus_recorder_pause(session->vrc_peer); + } else if(!strcasecmp(action_text, "resume")) { + if(record_audio) + janus_recorder_resume(session->arc); + if(record_video && !janus_recorder_resume(session->vrc)) + gateway->send_pli(session->handle); + if(record_peer_audio) + janus_recorder_resume(session->arc_peer); + if(record_peer_video) + janus_recorder_resume(session->vrc_peer); } else { /* Stop recording something: notice that this never returns an error, even when we were not recording anything */ janus_sip_recorder_close(session, record_audio, record_peer_audio, record_video, record_peer_video); diff --git a/record.c b/record.c index c24c86a22b..11e562957a 100644 --- a/record.c +++ b/record.c @@ -110,6 +110,7 @@ janus_recorder *janus_recorder_create_full(const char *dir, const char *codec, c /* Create the recorder */ janus_recorder *rc = g_malloc0(sizeof(janus_recorder)); janus_refcount_init(&rc->ref, janus_recorder_free); + janus_rtp_switching_context_reset(&rc->context); rc->dir = NULL; rc->filename = NULL; rc->file = NULL; @@ -255,6 +256,35 @@ janus_recorder *janus_recorder_create_full(const char *dir, const char *codec, c return rc; } +int janus_recorder_pause(janus_recorder *recorder) { + if(!recorder) + return -1; + if(g_atomic_int_compare_and_exchange(&recorder->paused, 0, 1)) + return 0; + return -2; +} + +int janus_recorder_resume(janus_recorder *recorder) { + if(!recorder) + return -1; + janus_mutex_lock_nodebug(&recorder->mutex); + if(g_atomic_int_compare_and_exchange(&recorder->paused, 1, 0)) { + if(recorder->type == JANUS_RECORDER_AUDIO) { + recorder->context.a_ts_reset = TRUE; + recorder->context.a_seq_reset = TRUE; + recorder->context.a_last_time = janus_get_monotonic_time(); + } else if(recorder->type == JANUS_RECORDER_VIDEO) { + recorder->context.v_ts_reset = TRUE; + recorder->context.v_seq_reset = TRUE; + recorder->context.v_last_time = janus_get_monotonic_time(); + } + janus_mutex_unlock_nodebug(&recorder->mutex); + return 0; + } + janus_mutex_unlock_nodebug(&recorder->mutex); + return -2; +} + int janus_recorder_add_extmap(janus_recorder *recorder, int id, const char *extmap) { if(!recorder || g_atomic_int_get(&recorder->header) || id < 1 || id > 15 || extmap == NULL) return -1; @@ -292,6 +322,10 @@ int janus_recorder_save_frame(janus_recorder *recorder, char *buffer, uint lengt janus_mutex_unlock_nodebug(&recorder->mutex); return -4; } + if(g_atomic_int_get(&recorder->paused)) { + janus_mutex_unlock_nodebug(&recorder->mutex); + return -5; + } gint64 now = janus_get_monotonic_time(); if(!g_atomic_int_get(&recorder->header)) { /* Write info header as a JSON formatted info */ @@ -384,17 +418,39 @@ int janus_recorder_save_frame(janus_recorder *recorder, char *buffer, uint lengt res, sizeof(gint64), g_strerror(errno)); } } + /* Edit packet header if needed */ + janus_rtp_header *header = (janus_rtp_header *)buffer; + uint32_t ssrc = 0; + uint16_t seq = 0; + if(recorder->type != JANUS_RECORDER_DATA) { + ssrc = ntohl(header->ssrc); + seq = ntohs(header->seq_number); + timestamp = ntohl(header->timestamp); + janus_rtp_header_update(header, &recorder->context, recorder->type == JANUS_RECORDER_VIDEO, 0); + } /* Save packet on file */ int temp = 0, tot = length; while(tot > 0) { temp = fwrite(buffer+length-tot, sizeof(char), tot, recorder->file); if(temp <= 0) { JANUS_LOG(LOG_ERR, "Error saving frame...\n"); + if(recorder->type != JANUS_RECORDER_DATA) { + /* Restore packet header data */ + header->ssrc = htonl(ssrc); + header->seq_number = htons(seq); + header->timestamp = htonl(timestamp); + } janus_mutex_unlock_nodebug(&recorder->mutex); return -6; } tot -= temp; } + if(recorder->type != JANUS_RECORDER_DATA) { + /* Restore packet header data */ + header->ssrc = htonl(ssrc); + header->seq_number = htons(seq); + header->timestamp = htonl(timestamp); + } /* Done */ janus_mutex_unlock_nodebug(&recorder->mutex); return 0; diff --git a/record.h b/record.h index 482c9fc060..6e75a0f548 100644 --- a/record.h +++ b/record.h @@ -27,6 +27,7 @@ #include "mutex.h" #include "refcount.h" +#include "rtp.h" /*! \brief Media types we can record */ @@ -60,6 +61,10 @@ typedef struct janus_recorder { volatile int header; /*! \brief Whether this recorder instance can be used for writing or not */ volatile int writable; + /*! \brief Whether writing s/RTP packets/data is paused */ + volatile int paused; + /*! \brief RTP switching context for rewriting RTP headers */ + janus_rtp_switching_context context; /*! \brief Mutex to lock/unlock this recorder instance */ janus_mutex mutex; /*! \brief Atomic flag to check if this instance has been destroyed */ @@ -92,6 +97,16 @@ janus_recorder *janus_recorder_create(const char *dir, const char *codec, const * @param[in] filename Filename to use for the recording * @returns A valid janus_recorder instance in case of success, NULL otherwise */ janus_recorder *janus_recorder_create_full(const char *dir, const char *codec, const char *fmtp, const char *filename); +/*! \brief Pause recording packets + * \note This is to allow pause and resume recorder functionality. + * @param[in] recorder The janus_recorder to pause + * @returns 0 in case of success, a negative integer otherwise */ +int janus_recorder_pause(janus_recorder *recorder); +/*! \brief Resume recording packets + * \note This is to allow pause and resume recorder functionality. + * @param[in] recorder The janus_recorder to resume + * @returns 0 in case of success, a negative integer otherwise */ +int janus_recorder_resume(janus_recorder *recorder); /*! \brief Add an RTP extension to this recording * \note This will only be possible BEFORE the first frame is written, as it needs to * be reflected in the .mjr header: doing this after that will return an error. diff --git a/rtp.c b/rtp.c index 18c634a135..b547ce109a 100644 --- a/rtp.c +++ b/rtp.c @@ -601,10 +601,17 @@ void janus_rtp_header_update(janus_rtp_header *header, janus_rtp_switching_conte JANUS_LOG(LOG_VERB, "Video SSRC changed, %"SCNu32" --> %"SCNu32"\n", context->v_last_ssrc, ssrc); context->v_last_ssrc = ssrc; + context->v_ts_reset = TRUE; + context->v_seq_reset = TRUE; + /* Reset skew compensation data */ + context->v_new_ssrc = TRUE; + } + if(context->v_ts_reset) { + /* Video timestamp was paused for a while */ + JANUS_LOG(LOG_HUGE, "Video RTP timestamp reset requested"); + context->v_ts_reset = FALSE; context->v_base_ts_prev = context->v_last_ts; context->v_base_ts = timestamp; - context->v_base_seq_prev = context->v_last_seq; - context->v_base_seq = seq; /* How much time since the last video RTP packet? We compute an offset accordingly */ if(context->v_last_time > 0) { gint64 time_diff = janus_get_monotonic_time() - context->v_last_time; @@ -613,13 +620,12 @@ void janus_rtp_header_update(janus_rtp_header *header, janus_rtp_switching_conte time_diff = 1; context->v_base_ts_prev += (guint32)time_diff; context->v_last_ts += (guint32)time_diff; - JANUS_LOG(LOG_VERB, "Computed offset for video RTP timestamp: %"SCNu32"\n", (guint32)time_diff); + JANUS_LOG(LOG_HUGE, "Computed offset for video RTP timestamp: %"SCNu32"\n", (guint32)time_diff); } - /* Reset skew compensation data */ - context->v_new_ssrc = TRUE; } if(context->v_seq_reset) { - /* Video sequence number was paused for a while: just update that */ + /* Video sequence number was paused for a while */ + JANUS_LOG(LOG_HUGE, "Video RTP sequence number reset requested"); context->v_seq_reset = FALSE; context->v_base_seq_prev = context->v_last_seq; context->v_base_seq = seq; @@ -640,10 +646,17 @@ void janus_rtp_header_update(janus_rtp_header *header, janus_rtp_switching_conte JANUS_LOG(LOG_VERB, "Audio SSRC changed, %"SCNu32" --> %"SCNu32"\n", context->a_last_ssrc, ssrc); context->a_last_ssrc = ssrc; + context->a_ts_reset = TRUE; + context->a_seq_reset = TRUE; + /* Reset skew compensation data */ + context->a_new_ssrc = TRUE; + } + if(context->a_ts_reset) { + /* Audio timestamp was paused for a while */ + JANUS_LOG(LOG_HUGE, "Audio RTP timestamp reset requested"); + context->a_ts_reset = FALSE; context->a_base_ts_prev = context->a_last_ts; context->a_base_ts = timestamp; - context->a_base_seq_prev = context->a_last_seq; - context->a_base_seq = seq; /* How much time since the last audio RTP packet? We compute an offset accordingly */ if(context->a_last_time > 0) { gint64 time_diff = janus_get_monotonic_time() - context->a_last_time; @@ -656,13 +669,12 @@ void janus_rtp_header_update(janus_rtp_header *header, janus_rtp_switching_conte context->a_base_ts_prev += (guint32)time_diff; context->a_prev_ts += (guint32)time_diff; context->a_last_ts += (guint32)time_diff; - JANUS_LOG(LOG_VERB, "Computed offset for audio RTP timestamp: %"SCNu32"\n", (guint32)time_diff); + JANUS_LOG(LOG_HUGE, "Computed offset for audio RTP timestamp: %"SCNu32"\n", (guint32)time_diff); } - /* Reset skew compensation data */ - context->a_new_ssrc = TRUE; } if(context->a_seq_reset) { - /* Audio sequence number was paused for a while: just update that */ + /* Audio sequence number was paused for a while */ + JANUS_LOG(LOG_HUGE, "Audio RTP sequence number reset requested"); context->a_seq_reset = FALSE; context->a_base_seq_prev = context->a_last_seq; context->a_base_seq = seq; diff --git a/rtp.h b/rtp.h index 34a6705f9a..9c5e21691b 100644 --- a/rtp.h +++ b/rtp.h @@ -226,8 +226,8 @@ typedef struct janus_rtp_switching_context { v_last_ssrc, v_last_ts, v_base_ts, v_base_ts_prev, v_prev_ts, v_target_ts, v_start_ts; uint16_t a_last_seq, a_prev_seq, a_base_seq, a_base_seq_prev, v_last_seq, v_prev_seq, v_base_seq, v_base_seq_prev; - gboolean a_seq_reset, a_new_ssrc, - v_seq_reset, v_new_ssrc; + gboolean a_ts_reset, a_seq_reset, a_new_ssrc, + v_ts_reset, v_seq_reset, v_new_ssrc; gint16 a_seq_offset, v_seq_offset; gint32 a_prev_delay, a_active_delay, a_ts_offset,