ARI: Add recording controls
authorDavid M. Lee <dlee@digium.com>
Tue, 6 Aug 2013 14:44:45 +0000 (14:44 +0000)
committerDavid M. Lee <dlee@digium.com>
Tue, 6 Aug 2013 14:44:45 +0000 (14:44 +0000)
This patch implements the controls from ARI recordings. The controls
are:

 * DELETE /recordings/live/{recordingName} - stop recording and
   discard it
 * POST /recordings/live/{recordingName}/stop - stop recording
 * POST /recordings/live/{recordingName}/pause - pause recording
 * POST /recordings/live/{recordingName}/unpause - resume recording
 * POST /recordings/live/{recordingName}/mute - mute recording (record
   silence to the file)
 * POST /recordings/live/{recordingName}/unmute - unmute recording.

Since this underlying functionality did not already exist, is was
added to app.c by a set of control frames, similar to how playback
control works. The pause/mute control frames are toggles, even though
the ARI controls are idempotent, to be consistent with the playback
control frames.

(closes issue ASTERISK-22181)
Review: https://reviewboard.asterisk.org/r/2697/

git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@396331 65c4cc65-6c06-0410-ace0-fbb531ad65f3

13 files changed:
apps/app_minivm.c
apps/app_voicemail.c
funcs/func_frame_trace.c
include/asterisk/app.h
include/asterisk/frame.h
include/asterisk/stasis_app_recording.h
main/app.c
main/channel.c
res/ari/resource_recordings.c
res/ari/resource_recordings.h
res/res_ari_recordings.c
res/res_stasis_recording.c
rest-api/api-docs/recordings.json

index 06ac02a..cdcf8b9 100644 (file)
@@ -1674,7 +1674,7 @@ static int play_record_review(struct ast_channel *chan, char *playfile, char *re
                                ast_channel_setoption(chan, AST_OPTION_RXGAIN, &record_gain, sizeof(record_gain), 0);
                        if (ast_test_flag(vmu, MVM_OPERATOR))
                                canceldtmf = "0";
-                       cmd = ast_play_and_record_full(chan, playfile, recordfile, maxtime, fmt, duration, sound_duration, global_silencethreshold, global_maxsilence, unlockdir, acceptdtmf, canceldtmf, 0, AST_RECORD_IF_EXISTS_OVERWRITE);
+                       cmd = ast_play_and_record_full(chan, playfile, recordfile, maxtime, fmt, duration, sound_duration, 0, global_silencethreshold, global_maxsilence, unlockdir, acceptdtmf, canceldtmf, 0, AST_RECORD_IF_EXISTS_OVERWRITE);
                        if (record_gain)
                                ast_channel_setoption(chan, AST_OPTION_RXGAIN, &zero_gain, sizeof(zero_gain), 0);
                        if (cmd == -1) /* User has hung up, no options to give */
index 3696c74..a258e6a 100644 (file)
@@ -14682,7 +14682,7 @@ static int play_record_review(struct ast_channel *chan, char *playfile, char *re
                                ast_channel_setoption(chan, AST_OPTION_RXGAIN, &record_gain, sizeof(record_gain), 0);
                        if (ast_test_flag(vmu, VM_OPERATOR))
                                canceldtmf = "0";
-                       cmd = ast_play_and_record_full(chan, playfile, tempfile, maxtime, fmt, duration, sound_duration, silencethreshold, maxsilence, unlockdir, acceptdtmf, canceldtmf, 0, AST_RECORD_IF_EXISTS_OVERWRITE);
+                       cmd = ast_play_and_record_full(chan, playfile, tempfile, maxtime, fmt, duration, sound_duration, 0, silencethreshold, maxsilence, unlockdir, acceptdtmf, canceldtmf, 0, AST_RECORD_IF_EXISTS_OVERWRITE);
                        if (strchr(canceldtmf, cmd)) {
                        /* need this flag here to distinguish between pressing '0' during message recording or after */
                                canceleddtmf = 1;
index 94a2c13..17f7c89 100644 (file)
@@ -343,6 +343,18 @@ static void print_frame(struct ast_frame *frame)
                case AST_CONTROL_STREAM_FORWARD:
                        ast_verbose("SubClass: STREAM_FORWARD\n");
                        break;
+               case AST_CONTROL_RECORD_CANCEL:
+                       ast_verbose("SubClass: RECORD_CANCEL\n");
+                       break;
+               case AST_CONTROL_RECORD_STOP:
+                       ast_verbose("SubClass: RECORD_STOP\n");
+                       break;
+               case AST_CONTROL_RECORD_SUSPEND:
+                       ast_verbose("SubClass: RECORD_SUSPEND\n");
+                       break;
+               case AST_CONTROL_RECORD_MUTE:
+                       ast_verbose("SubClass: RECORD_MUTE\n");
+                       break;
                }
 
                if (frame->subclass.integer == -1) {
index 2512885..06b903e 100644 (file)
@@ -709,11 +709,12 @@ enum ast_record_if_exists {
  *        skip_confirmation_sound is false.
  *
  * \param chan the channel being recorded
- * \param playfile Filename of sound to play before recording begins
+ * \param playfile Filename of sound to play before recording begins. A beep is also played when playfile completes, before the recording begins.
  * \param recordfile Filename to save the recording
  * \param maxtime_sec Longest possible message length in seconds
  * \param fmt string containing all formats to be recorded delimited by '|'
  * \param duration pointer to integer for storing length of the recording
+ * \param beep If true, play a beep before recording begins (and doesn't play \a playfile)
  * \param sound_duration pointer to integer for storing length of the recording minus all silence
  * \param silencethreshold tolerance of noise levels that can be considered silence for the purpose of silence timeout, -1 for default
  * \param maxsilence_ms Length of time in milliseconds which will trigger a timeout from silence, -1 for default
@@ -728,7 +729,7 @@ enum ast_record_if_exists {
  * \retval 't' Recording ended from the message exceeding the maximum duration
  * \retval dtmfchar Recording ended via the return value's DTMF character for either cancel or accept.
  */
-int ast_play_and_record_full(struct ast_channel *chan, const char *playfile, const char *recordfile, int maxtime_sec, const char *fmt, int *duration, int *sound_duration, int silencethreshold, int maxsilence_ms, const char *path, const char *acceptdtmf, const char *canceldtmf, int skip_confirmation_sound, enum ast_record_if_exists if_exists);
+int ast_play_and_record_full(struct ast_channel *chan, const char *playfile, const char *recordfile, int maxtime_sec, const char *fmt, int *duration, int *sound_duration, int beep, int silencethreshold, int maxsilence_ms, const char *path, const char *acceptdtmf, const char *canceldtmf, int skip_confirmation_sound, enum ast_record_if_exists if_exists);
 
 /*!
  * \brief Record a file based on input from a channel. Use default accept and cancel DTMF.
index bedc3a2..1cb7d59 100644 (file)
@@ -278,7 +278,11 @@ enum ast_control_frame_type {
        AST_CONTROL_STREAM_RESTART = 1002,      /*!< Indicate to a channel in playback to restart the stream */
        AST_CONTROL_STREAM_REVERSE = 1003,      /*!< Indicate to a channel in playback to rewind */
        AST_CONTROL_STREAM_FORWARD = 1004,      /*!< Indicate to a channel in playback to fast forward */
-
+       /* Control frames to manipulate recording on a channel. */
+       AST_CONTROL_RECORD_CANCEL = 1100,       /*!< Indicated to a channel in record to stop recording and discard the file */
+       AST_CONTROL_RECORD_STOP = 1101, /*!< Indicated to a channel in record to stop recording */
+       AST_CONTROL_RECORD_SUSPEND = 1102,      /*!< Indicated to a channel in record to suspend/unsuspend recording */
+       AST_CONTROL_RECORD_MUTE = 1103, /*!< Indicated to a channel in record to mute/unmute (i.e. write silence) recording */
 };
 
 enum ast_frame_read_action {
index 9c99304..e8b4558 100644 (file)
@@ -44,14 +44,30 @@ enum stasis_app_recording_state {
        STASIS_APP_RECORDING_STATE_PAUSED,
        /*! The media has stopped recording */
        STASIS_APP_RECORDING_STATE_COMPLETE,
-       /*! The media has stopped playing */
+       /*! The media has stopped recording, with error */
        STASIS_APP_RECORDING_STATE_FAILED,
+       /*! The media has stopped recording, discard the recording file */
+       STASIS_APP_RECORDING_STATE_CANCELED,
+       /*! Sentinel */
+       STASIS_APP_RECORDING_STATE_MAX,
 };
 
 /*! Valid operation for controlling a recording. */
 enum stasis_app_recording_media_operation {
-       /*! Stop the recording operation. */
+       /*! Stop the recording, deleting the media file(s) */
+       STASIS_APP_RECORDING_CANCEL,
+       /*! Stop the recording. */
        STASIS_APP_RECORDING_STOP,
+       /*! Pause the recording */
+       STASIS_APP_RECORDING_PAUSE,
+       /*! Unpause the recording */
+       STASIS_APP_RECORDING_UNPAUSE,
+       /*! Mute the recording (record silence) */
+       STASIS_APP_RECORDING_MUTE,
+       /*! Unmute the recording */
+       STASIS_APP_RECORDING_UNMUTE,
+       /*! Sentinel */
+       STASIS_APP_RECORDING_OPER_MAX,
 };
 
 #define STASIS_APP_RECORDING_TERMINATE_INVALID 0
index 8d081fe..ee2bbf4 100644 (file)
@@ -1145,6 +1145,78 @@ int ast_play_and_wait(struct ast_channel *chan, const char *fn)
        return d;
 }
 
+/*!
+ * \brief Construct a silence frame of the same duration as \a orig.
+ *
+ * The \a orig frame must be \ref AST_FORMAT_SLINEAR.
+ *
+ * \param orig Frame as basis for silence to generate.
+ * \return New frame of silence; free with ast_frfree().
+ * \return \c NULL on error.
+ */
+static struct ast_frame *make_silence(const struct ast_frame *orig)
+{
+       struct ast_frame *silence;
+       size_t size;
+       size_t datalen;
+       size_t samples = 0;
+       struct ast_frame *next;
+
+       if (!orig) {
+               return NULL;
+       }
+
+       if (orig->subclass.format.id != AST_FORMAT_SLINEAR) {
+               ast_log(LOG_WARNING, "Attempting to silence non-slin frame\n");
+               return NULL;
+       }
+
+       for (next = AST_LIST_NEXT(orig, frame_list);
+                orig;
+                orig = next, next = orig ? AST_LIST_NEXT(orig, frame_list) : NULL) {
+               samples += orig->samples;
+       }
+
+       ast_verb(4, "Silencing %zd samples\n", samples);
+
+
+       datalen = sizeof(short) * samples;
+       size = sizeof(*silence) + datalen;
+       silence = ast_calloc(1, size);
+       if (!silence) {
+               return NULL;
+       }
+
+       silence->mallocd = AST_MALLOCD_HDR;
+       silence->frametype = AST_FRAME_VOICE;
+       silence->data.ptr = (void *)(silence + 1);
+       silence->samples = samples;
+       silence->datalen = datalen;
+
+       ast_format_set(&silence->subclass.format, AST_FORMAT_SLINEAR, 0);
+
+       return silence;
+}
+
+/*!
+ * \brief Sets a channel's read format to \ref AST_FORMAT_SLINEAR, recording
+ * its original format.
+ *
+ * \param chan Channel to modify.
+ * \param[out] orig_format Output variable to store channel's original read
+ *                         format.
+ * \return 0 on success.
+ * \return -1 on error.
+ */
+static int set_read_to_slin(struct ast_channel *chan, struct ast_format *orig_format)
+{
+       if (!chan || !orig_format) {
+               return -1;
+       }
+       ast_format_copy(orig_format, ast_channel_readformat(chan));
+       return ast_set_read_format_by_id(chan, AST_FORMAT_SLINEAR);
+}
+
 static int global_silence_threshold = 128;
 static int global_maxsilence = 0;
 
@@ -1274,8 +1346,7 @@ static int __ast_play_and_record(struct ast_channel *chan, const char *playfile,
                        return -1;
                }
                ast_dsp_set_threshold(sildet, silencethreshold);
-               ast_format_copy(&rfmt, ast_channel_readformat(chan));
-               res = ast_set_read_format_by_id(chan, AST_FORMAT_SLINEAR);
+               res = set_read_to_slin(chan, &rfmt);
                if (res < 0) {
                        ast_log(LOG_WARNING, "Unable to set to linear mode, giving up\n");
                        ast_dsp_free(sildet);
@@ -1293,9 +1364,15 @@ static int __ast_play_and_record(struct ast_channel *chan, const char *playfile,
        }
 
        if (x == fmtcnt) {
-               /* Loop forever, writing the packets we read to the writer(s), until
-                  we read a digit or get a hangup */
+               /* Loop, writing the packets we read to the writer(s), until
+                * we have reason to stop. */
                struct ast_frame *f;
+               int paused = 0;
+               int muted = 0;
+               time_t pause_start = 0;
+               int paused_secs = 0;
+               int pausedsilence = 0;
+
                for (;;) {
                        if (!(res = ast_waitfor(chan, 2000))) {
                                ast_debug(1, "One waitfor failed, trying another\n");
@@ -1315,11 +1392,29 @@ static int __ast_play_and_record(struct ast_channel *chan, const char *playfile,
                        }
                        if (f->frametype == AST_FRAME_VOICE) {
                                /* write each format */
-                               for (x = 0; x < fmtcnt; x++) {
-                                       if (prepend && !others[x]) {
-                                               break;
+                               if (paused) {
+                                       /* It's all good */
+                                       res = 0;
+                               } else {
+                                       RAII_VAR(struct ast_frame *, silence, NULL, ast_frame_dtor);
+                                       struct ast_frame *orig = f;
+
+                                       if (muted) {
+                                               silence = make_silence(orig);
+                                               if (!silence) {
+                                                       ast_log(LOG_WARNING,
+                                                               "Error creating silence\n");
+                                                       break;
+                                               }
+                                               f = silence;
                                        }
-                                       res = ast_writestream(others[x], f);
+                                       for (x = 0; x < fmtcnt; x++) {
+                                               if (prepend && !others[x]) {
+                                                       break;
+                                               }
+                                               res = ast_writestream(others[x], f);
+                                       }
+                                       f = orig;
                                }
 
                                /* Silence Detection */
@@ -1331,6 +1426,17 @@ static int __ast_play_and_record(struct ast_channel *chan, const char *playfile,
                                        }
                                        olddspsilence = dspsilence;
 
+                                       if (paused) {
+                                               /* record how much silence there was while we are paused */
+                                               pausedsilence = dspsilence;
+                                       } else if (dspsilence > pausedsilence) {
+                                               /* ignore the paused silence */
+                                               dspsilence -= pausedsilence;
+                                       } else {
+                                               /* dspsilence has reset, reset pausedsilence */
+                                               pausedsilence = 0;
+                                       }
+
                                        if (dspsilence > maxsilence) {
                                                /* Ended happily with silence */
                                                ast_verb(3, "Recording automatically stopped after a silence of %d seconds\n", dspsilence/1000);
@@ -1362,15 +1468,51 @@ static int __ast_play_and_record(struct ast_channel *chan, const char *playfile,
                                        break;
                                }
                                if (strchr(canceldtmf, f->subclass.integer)) {
-                                       ast_verb(3, "User cancelled message by pressing %c\n", f->subclass.integer);
+                                       ast_verb(3, "User canceled message by pressing %c\n", f->subclass.integer);
                                        res = f->subclass.integer;
                                        outmsg = 0;
                                        break;
                                }
+                       } else if (f->frametype == AST_FRAME_CONTROL) {
+                               if (f->subclass.integer == AST_CONTROL_RECORD_CANCEL) {
+                                       ast_verb(3, "Message canceled by control\n");
+                                       outmsg = 0; /* cancels the recording */
+                                       res = 0;
+                                       break;
+                               } else if (f->subclass.integer == AST_CONTROL_RECORD_STOP) {
+                                       ast_verb(3, "Message ended by control\n");
+                                       res = 0;
+                                       break;
+                               } else if (f->subclass.integer == AST_CONTROL_RECORD_SUSPEND) {
+                                       paused = !paused;
+                                       ast_verb(3, "Message %spaused by control\n",
+                                               paused ? "" : "un");
+                                       if (paused) {
+                                               pause_start = time(NULL);
+                                       } else {
+                                               paused_secs += time(NULL) - pause_start;
+                                       }
+                               } else if (f->subclass.integer == AST_CONTROL_RECORD_MUTE) {
+                                       muted = !muted;
+                                       ast_verb(3, "Message %smuted by control\n",
+                                               muted ? "" : "un");
+                                       /* We can only silence slin frames, so
+                                        * set the mode, if we haven't already
+                                        * for sildet
+                                        */
+                                       if (muted && !rfmt.id) {
+                                               ast_verb(3, "Setting read format to linear mode\n");
+                                               res = set_read_to_slin(chan, &rfmt);
+                                               if (res < 0) {
+                                                       ast_log(LOG_WARNING, "Unable to set to linear mode, giving up\n");
+                                                       break;
+                                               }
+                                       }
+                               }
                        }
-                       if (maxtime) {
+                       if (maxtime && !paused) {
                                end = time(NULL);
-                               if (maxtime < (end - start)) {
+                               if (maxtime < (end - start - paused_secs)) {
                                        ast_verb(3, "Took too long, cutting it short...\n");
                                        res = 't';
                                        outmsg = 2;
@@ -1493,9 +1635,9 @@ static int __ast_play_and_record(struct ast_channel *chan, const char *playfile,
 static const char default_acceptdtmf[] = "#";
 static const char default_canceldtmf[] = "";
 
-int ast_play_and_record_full(struct ast_channel *chan, const char *playfile, const char *recordfile, int maxtime, const char *fmt, int *duration, int *sound_duration, int silencethreshold, int maxsilence, const char *path, const char *acceptdtmf, const char *canceldtmf, int skip_confirmation_sound, enum ast_record_if_exists if_exists)
+int ast_play_and_record_full(struct ast_channel *chan, const char *playfile, const char *recordfile, int maxtime, const char *fmt, int *duration, int *sound_duration, int beep, int silencethreshold, int maxsilence, const char *path, const char *acceptdtmf, const char *canceldtmf, int skip_confirmation_sound, enum ast_record_if_exists if_exists)
 {
-       return __ast_play_and_record(chan, playfile, recordfile, maxtime, fmt, duration, sound_duration, 0, silencethreshold, maxsilence, path, 0, S_OR(acceptdtmf, default_acceptdtmf), S_OR(canceldtmf, default_canceldtmf), skip_confirmation_sound, if_exists);
+       return __ast_play_and_record(chan, playfile, recordfile, maxtime, fmt, duration, sound_duration, beep, silencethreshold, maxsilence, path, 0, S_OR(acceptdtmf, default_acceptdtmf), S_OR(canceldtmf, default_canceldtmf), skip_confirmation_sound, if_exists);
 }
 
 int ast_play_and_record(struct ast_channel *chan, const char *playfile, const char *recordfile, int maxtime, const char *fmt, int *duration, int *sound_duration, int silencethreshold, int maxsilence, const char *path)
index f1d98de..8e6a4c9 100644 (file)
@@ -4303,6 +4303,10 @@ static int attribute_const is_visible_indication(enum ast_control_frame_type con
        case AST_CONTROL_STREAM_REVERSE:
        case AST_CONTROL_STREAM_FORWARD:
        case AST_CONTROL_STREAM_RESTART:
+       case AST_CONTROL_RECORD_CANCEL:
+       case AST_CONTROL_RECORD_STOP:
+       case AST_CONTROL_RECORD_SUSPEND:
+       case AST_CONTROL_RECORD_MUTE:
                break;
 
        case AST_CONTROL_INCOMPLETE:
@@ -4561,6 +4565,10 @@ int ast_indicate_data(struct ast_channel *chan, int _condition,
        case AST_CONTROL_STREAM_REVERSE:
        case AST_CONTROL_STREAM_FORWARD:
        case AST_CONTROL_STREAM_RESTART:
+       case AST_CONTROL_RECORD_CANCEL:
+       case AST_CONTROL_RECORD_STOP:
+       case AST_CONTROL_RECORD_SUSPEND:
+       case AST_CONTROL_RECORD_MUTE:
                /* Nothing left to do for these. */
                res = 0;
                break;
index 46439ff..f87ff0a 100644 (file)
@@ -71,27 +71,81 @@ void ast_ari_get_live_recording(struct ast_variable *headers,
        ast_ari_response_ok(response, ast_json_ref(json));
 }
 
-void ast_ari_cancel_recording(struct ast_variable *headers, struct ast_cancel_recording_args *args, struct ast_ari_response *response)
+static void control_recording(const char *name,
+       enum stasis_app_recording_media_operation operation,
+       struct ast_ari_response *response)
 {
-       ast_log(LOG_ERROR, "TODO: ast_ari_cancel_recording\n");
+       RAII_VAR(struct stasis_app_recording *, recording, NULL, ao2_cleanup);
+       RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
+       enum stasis_app_recording_oper_results res;
+
+       recording = stasis_app_recording_find_by_name(name);
+       if (recording == NULL) {
+               ast_ari_response_error(response, 404, "Not Found",
+                       "Recording not found");
+               return;
+       }
+
+       res = stasis_app_recording_operation(recording, operation);
+
+       switch (res) {
+       case STASIS_APP_RECORDING_OPER_OK:
+               ast_ari_response_no_content(response);
+               return;
+       case STASIS_APP_RECORDING_OPER_FAILED:
+               ast_ari_response_error(response, 500,
+                       "Internal Server Error", "Recording operation failed");
+               return;
+       case STASIS_APP_RECORDING_OPER_NOT_RECORDING:
+               ast_ari_response_error(response, 409,
+                       "Conflict", "Recording not in session");
+       }
 }
-void ast_ari_stop_recording(struct ast_variable *headers, struct ast_stop_recording_args *args, struct ast_ari_response *response)
+
+void ast_ari_cancel_recording(struct ast_variable *headers,
+       struct ast_cancel_recording_args *args,
+       struct ast_ari_response *response)
 {
-       ast_log(LOG_ERROR, "TODO: ast_ari_stop_recording\n");
+       control_recording(args->recording_name, STASIS_APP_RECORDING_CANCEL,
+               response);
 }
-void ast_ari_pause_recording(struct ast_variable *headers, struct ast_pause_recording_args *args, struct ast_ari_response *response)
+
+void ast_ari_stop_recording(struct ast_variable *headers,
+       struct ast_stop_recording_args *args,
+       struct ast_ari_response *response)
+{
+       control_recording(args->recording_name, STASIS_APP_RECORDING_STOP,
+               response);
+}
+
+void ast_ari_pause_recording(struct ast_variable *headers,
+       struct ast_pause_recording_args *args,
+       struct ast_ari_response *response)
 {
-       ast_log(LOG_ERROR, "TODO: ast_ari_pause_recording\n");
+       control_recording(args->recording_name, STASIS_APP_RECORDING_PAUSE,
+               response);
 }
-void ast_ari_unpause_recording(struct ast_variable *headers, struct ast_unpause_recording_args *args, struct ast_ari_response *response)
+
+void ast_ari_unpause_recording(struct ast_variable *headers,
+       struct ast_unpause_recording_args *args,
+       struct ast_ari_response *response)
 {
-       ast_log(LOG_ERROR, "TODO: ast_ari_unpause_recording\n");
+       control_recording(args->recording_name, STASIS_APP_RECORDING_UNPAUSE,
+               response);
 }
-void ast_ari_mute_recording(struct ast_variable *headers, struct ast_mute_recording_args *args, struct ast_ari_response *response)
+
+void ast_ari_mute_recording(struct ast_variable *headers,
+       struct ast_mute_recording_args *args,
+       struct ast_ari_response *response)
 {
-       ast_log(LOG_ERROR, "TODO: ast_ari_mute_recording\n");
+       control_recording(args->recording_name, STASIS_APP_RECORDING_MUTE,
+               response);
 }
-void ast_ari_unmute_recording(struct ast_variable *headers, struct ast_unmute_recording_args *args, struct ast_ari_response *response)
+
+void ast_ari_unmute_recording(struct ast_variable *headers,
+       struct ast_unmute_recording_args *args,
+       struct ast_ari_response *response)
 {
-       ast_log(LOG_ERROR, "TODO: ast_ari_unmute_recording\n");
+       control_recording(args->recording_name, STASIS_APP_RECORDING_UNMUTE,
+               response);
 }
index e3ee88b..2529766 100644 (file)
@@ -134,7 +134,7 @@ struct ast_pause_recording_args {
 /*!
  * \brief Pause a live recording.
  *
- * Pausing a recording suspends silence detection, which will be restarted when the recording is unpaused.
+ * Pausing a recording suspends silence detection, which will be restarted when the recording is unpaused. Paused time is not included in the accounting for maxDurationSeconds.
  *
  * \param headers HTTP headers
  * \param args Swagger parameters
index 01ad49a..77fc830 100644 (file)
@@ -295,6 +295,7 @@ static void ast_ari_get_live_recording_cb(
                break;
        case 500: /* Internal Server Error */
        case 501: /* Not Implemented */
+       case 404: /* Recording not found */
                is_valid = 1;
                break;
        default:
@@ -351,6 +352,7 @@ static void ast_ari_cancel_recording_cb(
                break;
        case 500: /* Internal Server Error */
        case 501: /* Not Implemented */
+       case 404: /* Recording not found */
                is_valid = 1;
                break;
        default:
@@ -407,6 +409,7 @@ static void ast_ari_stop_recording_cb(
                break;
        case 500: /* Internal Server Error */
        case 501: /* Not Implemented */
+       case 404: /* Recording not found */
                is_valid = 1;
                break;
        default:
@@ -463,6 +466,8 @@ static void ast_ari_pause_recording_cb(
                break;
        case 500: /* Internal Server Error */
        case 501: /* Not Implemented */
+       case 404: /* Recording not found */
+       case 409: /* Recording not in session */
                is_valid = 1;
                break;
        default:
@@ -519,6 +524,8 @@ static void ast_ari_unpause_recording_cb(
                break;
        case 500: /* Internal Server Error */
        case 501: /* Not Implemented */
+       case 404: /* Recording not found */
+       case 409: /* Recording not in session */
                is_valid = 1;
                break;
        default:
@@ -575,6 +582,8 @@ static void ast_ari_mute_recording_cb(
                break;
        case 500: /* Internal Server Error */
        case 501: /* Not Implemented */
+       case 404: /* Recording not found */
+       case 409: /* Recording not in session */
                is_valid = 1;
                break;
        default:
@@ -631,6 +640,8 @@ static void ast_ari_unmute_recording_cb(
                break;
        case 500: /* Internal Server Error */
        case 501: /* Not Implemented */
+       case 404: /* Recording not found */
+       case 409: /* Recording not in session */
                is_valid = 1;
                break;
        default:
index f94f428..575ccae 100644 (file)
@@ -59,11 +59,13 @@ struct stasis_app_recording {
        struct stasis_app_recording_options *options;
        /*! Absolute path (minus extension) of the recording */
        char *absolute_name;
-       /*! Control object for the channel we're playing back to */
+       /*! Control object for the channel we're recording */
        struct stasis_app_control *control;
 
        /*! Current state of the recording. */
        enum stasis_app_recording_state state;
+       /*! Indicates whether the recording is currently muted */
+       int muted:1;
 };
 
 static int recording_hash(const void *obj, int flags)
@@ -99,6 +101,10 @@ static const char *state_to_string(enum stasis_app_recording_state state)
                return "done";
        case STASIS_APP_RECORDING_STATE_FAILED:
                return "failed";
+       case STASIS_APP_RECORDING_STATE_CANCELED:
+               return "canceled";
+       case STASIS_APP_RECORDING_STATE_MAX:
+               return "?";
        }
 
        return "?";
@@ -253,12 +259,13 @@ static void *record_file(struct stasis_app_control *control,
        }
 
        ast_play_and_record_full(chan,
-               recording->options->beep ? "beep" : NULL,
+               NULL, /* playfile */
                recording->absolute_name,
                recording->options->max_duration_seconds,
                recording->options->format,
                &duration,
                NULL, /* sound_duration */
+               recording->options->beep,
                -1, /* silencethreshold */
                recording->options->max_silence_seconds * 1000,
                NULL, /* path */
@@ -403,12 +410,127 @@ struct ast_json *stasis_app_recording_to_json(
        return ast_json_ref(json);
 }
 
+typedef int (*recording_operation_cb)(struct stasis_app_recording *recording);
+
+static int recording_noop(struct stasis_app_recording *recording)
+{
+       return 0;
+}
+
+static int recording_disregard(struct stasis_app_recording *recording)
+{
+       recording->state = STASIS_APP_RECORDING_STATE_CANCELED;
+       return 0;
+}
+
+static int recording_cancel(struct stasis_app_recording *recording)
+{
+       int res = 0;
+       recording->state = STASIS_APP_RECORDING_STATE_CANCELED;
+       res |= stasis_app_control_queue_control(recording->control,
+               AST_CONTROL_RECORD_CANCEL);
+       res |= ast_filedelete(recording->absolute_name, NULL);
+       return res;
+}
+
+static int recording_stop(struct stasis_app_recording *recording)
+{
+       recording->state = STASIS_APP_RECORDING_STATE_COMPLETE;
+       return stasis_app_control_queue_control(recording->control,
+               AST_CONTROL_RECORD_STOP);
+}
+
+static int recording_pause(struct stasis_app_recording *recording)
+{
+       recording->state = STASIS_APP_RECORDING_STATE_PAUSED;
+       return stasis_app_control_queue_control(recording->control,
+               AST_CONTROL_RECORD_SUSPEND);
+}
+
+static int recording_unpause(struct stasis_app_recording *recording)
+{
+       recording->state = STASIS_APP_RECORDING_STATE_RECORDING;
+       return stasis_app_control_queue_control(recording->control,
+               AST_CONTROL_RECORD_SUSPEND);
+}
+
+static int recording_mute(struct stasis_app_recording *recording)
+{
+       if (recording->muted) {
+               /* already muted */
+               return 0;
+       }
+
+       recording->muted = 1;
+       return stasis_app_control_queue_control(recording->control,
+               AST_CONTROL_RECORD_MUTE);
+}
+
+static int recording_unmute(struct stasis_app_recording *recording)
+{
+       if (!recording->muted) {
+               /* already unmuted */
+               return 0;
+       }
+
+       return stasis_app_control_queue_control(recording->control,
+               AST_CONTROL_RECORD_MUTE);
+}
+
+recording_operation_cb operations[STASIS_APP_RECORDING_STATE_MAX][STASIS_APP_RECORDING_OPER_MAX] = {
+       [STASIS_APP_RECORDING_STATE_QUEUED][STASIS_APP_RECORDING_CANCEL] = recording_disregard,
+       [STASIS_APP_RECORDING_STATE_QUEUED][STASIS_APP_RECORDING_STOP] = recording_disregard,
+       [STASIS_APP_RECORDING_STATE_RECORDING][STASIS_APP_RECORDING_CANCEL] = recording_cancel,
+       [STASIS_APP_RECORDING_STATE_RECORDING][STASIS_APP_RECORDING_STOP] = recording_stop,
+       [STASIS_APP_RECORDING_STATE_RECORDING][STASIS_APP_RECORDING_PAUSE] = recording_pause,
+       [STASIS_APP_RECORDING_STATE_RECORDING][STASIS_APP_RECORDING_UNPAUSE] = recording_noop,
+       [STASIS_APP_RECORDING_STATE_RECORDING][STASIS_APP_RECORDING_MUTE] = recording_mute,
+       [STASIS_APP_RECORDING_STATE_RECORDING][STASIS_APP_RECORDING_UNMUTE] = recording_unmute,
+       [STASIS_APP_RECORDING_STATE_PAUSED][STASIS_APP_RECORDING_CANCEL] = recording_cancel,
+       [STASIS_APP_RECORDING_STATE_PAUSED][STASIS_APP_RECORDING_STOP] = recording_stop,
+       [STASIS_APP_RECORDING_STATE_PAUSED][STASIS_APP_RECORDING_PAUSE] = recording_noop,
+       [STASIS_APP_RECORDING_STATE_PAUSED][STASIS_APP_RECORDING_UNPAUSE] = recording_unpause,
+       [STASIS_APP_RECORDING_STATE_PAUSED][STASIS_APP_RECORDING_MUTE] = recording_mute,
+       [STASIS_APP_RECORDING_STATE_PAUSED][STASIS_APP_RECORDING_UNMUTE] = recording_unmute,
+};
+
 enum stasis_app_recording_oper_results stasis_app_recording_operation(
        struct stasis_app_recording *recording,
        enum stasis_app_recording_media_operation operation)
 {
-       ast_assert(0); // TODO
-       return STASIS_APP_RECORDING_OPER_FAILED;
+       recording_operation_cb cb;
+       SCOPED_AO2LOCK(lock, recording);
+
+       if (recording->state < 0 || recording->state >= STASIS_APP_RECORDING_STATE_MAX) {
+               ast_log(LOG_WARNING, "Invalid recording state %d\n",
+                       recording->state);
+               return -1;
+       }
+
+       if (operation < 0 || operation >= STASIS_APP_RECORDING_OPER_MAX) {
+               ast_log(LOG_WARNING, "Invalid recording operation %d\n",
+                       operation);
+               return -1;
+       }
+
+       cb = operations[recording->state][operation];
+
+       if (!cb) {
+               if (recording->state != STASIS_APP_RECORDING_STATE_RECORDING) {
+                       /* So we can be specific in our error message. */
+                       return STASIS_APP_RECORDING_OPER_NOT_RECORDING;
+               } else {
+                       /* And, really, all operations should be valid during
+                        * recording */
+                       ast_log(LOG_ERROR,
+                               "Unhandled operation during recording: %d\n",
+                               operation);
+                       return STASIS_APP_RECORDING_OPER_FAILED;
+               }
+       }
+
+       return cb(recording) ?
+               STASIS_APP_RECORDING_OPER_FAILED : STASIS_APP_RECORDING_OPER_OK;
 }
 
 static int load_module(void)
index b564ede..b4dd6d0 100644 (file)
                                                        "allowMultiple": false,
                                                        "dataType": "string"
                                                }
+                                       ],
+                                       "errorResponses": [
+                                               {
+                                                       "code": 404,
+                                                       "reason": "Recording not found"
+                                               }
                                        ]
                                },
                                {
                                                        "allowMultiple": false,
                                                        "dataType": "string"
                                                }
+                                       ],
+                                       "errorResponses": [
+                                               {
+                                                       "code": 404,
+                                                       "reason": "Recording not found"
+                                               }
                                        ]
                                }
                        ]
                                                        "allowMultiple": false,
                                                        "dataType": "string"
                                                }
+                                       ],
+                                       "errorResponses": [
+                                               {
+                                                       "code": 404,
+                                                       "reason": "Recording not found"
+                                               }
                                        ]
                                }
                        ]
                                {
                                        "httpMethod": "POST",
                                        "summary": "Pause a live recording.",
-                                       "notes": "Pausing a recording suspends silence detection, which will be restarted when the recording is unpaused.",
+                                       "notes": "Pausing a recording suspends silence detection, which will be restarted when the recording is unpaused. Paused time is not included in the accounting for maxDurationSeconds.",
                                        "nickname": "pauseRecording",
                                        "responseClass": "void",
                                        "parameters": [
                                                        "allowMultiple": false,
                                                        "dataType": "string"
                                                }
+                                       ],
+                                       "errorResponses": [
+                                               {
+                                                       "code": 404,
+                                                       "reason": "Recording not found"
+                                               },
+                                               {
+                                                       "code": 409,
+                                                       "reason": "Recording not in session"
+                                               }
                                        ]
                                }
                        ]
                                                        "allowMultiple": false,
                                                        "dataType": "string"
                                                }
+                                       ],
+                                       "errorResponses": [
+                                               {
+                                                       "code": 404,
+                                                       "reason": "Recording not found"
+                                               },
+                                               {
+                                                       "code": 409,
+                                                       "reason": "Recording not in session"
+                                               }
                                        ]
                                }
                        ]
                                                        "allowMultiple": false,
                                                        "dataType": "string"
                                                }
+                                       ],
+                                       "errorResponses": [
+                                               {
+                                                       "code": 404,
+                                                       "reason": "Recording not found"
+                                               },
+                                               {
+                                                       "code": 409,
+                                                       "reason": "Recording not in session"
+                                               }
                                        ]
                                }
                        ]
                                                        "allowMultiple": false,
                                                        "dataType": "string"
                                                }
+                                       ],
+                                       "errorResponses": [
+                                               {
+                                                       "code": 404,
+                                                       "reason": "Recording not found"
+                                               },
+                                               {
+                                                       "code": 409,
+                                                       "reason": "Recording not in session"
+                                               }
                                        ]
                                }
                        ]