832d6fc1b797c55426a82363bcedc6d18359ef8e
[asterisk/asterisk.git] / res / res_stasis_playback.c
1 /*
2  * Asterisk -- An open source telephony toolkit.
3  *
4  * Copyright (C) 2013, Digium, Inc.
5  *
6  * David M. Lee, II <dlee@digium.com>
7  *
8  * See http://www.asterisk.org for more information about
9  * the Asterisk project. Please do not directly contact
10  * any of the maintainers of this project for assistance;
11  * the project provides a web site, mailing lists and IRC
12  * channels for your use.
13  *
14  * This program is free software, distributed under the terms of
15  * the GNU General Public License Version 2. See the LICENSE file
16  * at the top of the source tree.
17  */
18
19 /*! \file
20  *
21  * \brief res_stasis playback support.
22  *
23  * \author David M. Lee, II <dlee@digium.com>
24  */
25
26 /*** MODULEINFO
27         <depend type="module">res_stasis</depend>
28         <depend type="module">res_stasis_recording</depend>
29         <support_level>core</support_level>
30  ***/
31
32 #include "asterisk.h"
33
34 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
35
36 #include "asterisk/app.h"
37 #include "asterisk/astobj2.h"
38 #include "asterisk/bridge.h"
39 #include "asterisk/bridge_internal.h"
40 #include "asterisk/file.h"
41 #include "asterisk/logger.h"
42 #include "asterisk/module.h"
43 #include "asterisk/paths.h"
44 #include "asterisk/stasis_app_impl.h"
45 #include "asterisk/stasis_app_playback.h"
46 #include "asterisk/stasis_app_recording.h"
47 #include "asterisk/stasis_channels.h"
48 #include "asterisk/stringfields.h"
49 #include "asterisk/uuid.h"
50 #include "asterisk/say.h"
51 #include "asterisk/indications.h"
52
53 /*! Number of hash buckets for playback container. Keep it prime! */
54 #define PLAYBACK_BUCKETS 127
55
56 /*! Default number of milliseconds of media to skip */
57 #define PLAYBACK_DEFAULT_SKIPMS 3000
58
59 #define SOUND_URI_SCHEME "sound:"
60 #define RECORDING_URI_SCHEME "recording:"
61 #define NUMBER_URI_SCHEME "number:"
62 #define DIGITS_URI_SCHEME "digits:"
63 #define CHARACTERS_URI_SCHEME "characters:"
64 #define TONE_URI_SCHEME "tone:"
65
66 /*! Container of all current playbacks */
67 static struct ao2_container *playbacks;
68
69 /*! Playback control object for res_stasis */
70 struct stasis_app_playback {
71         AST_DECLARE_STRING_FIELDS(
72                 AST_STRING_FIELD(id);   /*!< Playback unique id */
73                 AST_STRING_FIELD(media);        /*!< Playback media uri */
74                 AST_STRING_FIELD(language);     /*!< Preferred language */
75                 AST_STRING_FIELD(target);       /*!< Playback device uri */
76                 );
77         /*! Control object for the channel we're playing back to */
78         struct stasis_app_control *control;
79         /*! Number of milliseconds to skip before playing */
80         long offsetms;
81         /*! Number of milliseconds to skip for forward/reverse operations */
82         int skipms;
83         /*! Number of milliseconds of media that has been played */
84         long playedms;
85         /*! Current playback state */
86         enum stasis_app_playback_state state;
87         /*! Set when the playback can be controlled */
88         unsigned int controllable:1;
89 };
90
91 static struct ast_json *playback_to_json(struct stasis_message *message,
92         const struct stasis_message_sanitizer *sanitize)
93 {
94         struct ast_channel_blob *channel_blob = stasis_message_data(message);
95         struct ast_json *blob = channel_blob->blob;
96         const char *state =
97                 ast_json_string_get(ast_json_object_get(blob, "state"));
98         const char *type;
99
100         if (!strcmp(state, "playing")) {
101                 type = "PlaybackStarted";
102         } else if (!strcmp(state, "done")) {
103                 type = "PlaybackFinished";
104         } else {
105                 return NULL;
106         }
107
108         return ast_json_pack("{s: s, s: O}",
109                 "type", type,
110                 "playback", blob);
111 }
112
113 STASIS_MESSAGE_TYPE_DEFN(stasis_app_playback_snapshot_type,
114         .to_json = playback_to_json,
115 );
116
117 static void playback_dtor(void *obj)
118 {
119         struct stasis_app_playback *playback = obj;
120
121         ast_string_field_free_memory(playback);
122 }
123
124 static struct stasis_app_playback *playback_create(
125         struct stasis_app_control *control, const char *id)
126 {
127         RAII_VAR(struct stasis_app_playback *, playback, NULL, ao2_cleanup);
128         char uuid[AST_UUID_STR_LEN];
129
130         if (!control) {
131                 return NULL;
132         }
133
134         playback = ao2_alloc(sizeof(*playback), playback_dtor);
135         if (!playback || ast_string_field_init(playback, 128)) {
136                 return NULL;
137         }
138
139         if (!ast_strlen_zero(id)) {
140                 ast_string_field_set(playback, id, id);
141         } else {
142                 ast_uuid_generate_str(uuid, sizeof(uuid));
143                 ast_string_field_set(playback, id, uuid);
144         }
145
146         playback->control = control;
147
148         ao2_ref(playback, +1);
149         return playback;
150 }
151
152 static int playback_hash(const void *obj, int flags)
153 {
154         const struct stasis_app_playback *playback = obj;
155         const char *id = flags & OBJ_KEY ? obj : playback->id;
156         return ast_str_hash(id);
157 }
158
159 static int playback_cmp(void *obj, void *arg, int flags)
160 {
161         struct stasis_app_playback *lhs = obj;
162         struct stasis_app_playback *rhs = arg;
163         const char *rhs_id = flags & OBJ_KEY ? arg : rhs->id;
164
165         if (strcmp(lhs->id, rhs_id) == 0) {
166                 return CMP_MATCH | CMP_STOP;
167         } else {
168                 return 0;
169         }
170 }
171
172 static const char *state_to_string(enum stasis_app_playback_state state)
173 {
174         switch (state) {
175         case STASIS_PLAYBACK_STATE_QUEUED:
176                 return "queued";
177         case STASIS_PLAYBACK_STATE_PLAYING:
178                 return "playing";
179         case STASIS_PLAYBACK_STATE_PAUSED:
180                 return "paused";
181         case STASIS_PLAYBACK_STATE_STOPPED:
182         case STASIS_PLAYBACK_STATE_COMPLETE:
183         case STASIS_PLAYBACK_STATE_CANCELED:
184                 /* It doesn't really matter how we got here, but all of these
185                  * states really just mean 'done' */
186                 return "done";
187         case STASIS_PLAYBACK_STATE_MAX:
188                 break;
189         }
190
191         return "?";
192 }
193
194 static void playback_publish(struct stasis_app_playback *playback)
195 {
196         RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
197         RAII_VAR(struct stasis_message *, message, NULL, ao2_cleanup);
198
199         ast_assert(playback != NULL);
200
201         json = stasis_app_playback_to_json(playback);
202         if (json == NULL) {
203                 return;
204         }
205
206         message = ast_channel_blob_create_from_cache(
207                 stasis_app_control_get_channel_id(playback->control),
208                 stasis_app_playback_snapshot_type(), json);
209         if (message == NULL) {
210                 return;
211         }
212
213         stasis_app_control_publish(playback->control, message);
214 }
215
216 static int playback_first_update(struct stasis_app_playback *playback,
217         const char *uniqueid)
218 {
219         int res;
220         SCOPED_AO2LOCK(lock, playback);
221
222         if (playback->state == STASIS_PLAYBACK_STATE_CANCELED) {
223                 ast_log(LOG_NOTICE, "%s: Playback canceled for %s\n",
224                         uniqueid, playback->media);
225                 res = -1;
226         } else {
227                 res = 0;
228                 playback->state = STASIS_PLAYBACK_STATE_PLAYING;
229         }
230
231         playback_publish(playback);
232         return res;
233 }
234
235 static void playback_final_update(struct stasis_app_playback *playback,
236         long playedms, int res, const char *uniqueid)
237 {
238         SCOPED_AO2LOCK(lock, playback);
239
240         playback->playedms = playedms;
241         if (res == 0) {
242                 playback->state = STASIS_PLAYBACK_STATE_COMPLETE;
243         } else {
244                 if (playback->state == STASIS_PLAYBACK_STATE_STOPPED) {
245                         ast_log(LOG_NOTICE, "%s: Playback stopped for %s\n",
246                                 uniqueid, playback->media);
247                 } else {
248                         ast_log(LOG_WARNING, "%s: Playback failed for %s\n",
249                                 uniqueid, playback->media);
250                         playback->state = STASIS_PLAYBACK_STATE_STOPPED;
251                 }
252         }
253
254         playback_publish(playback);
255 }
256
257 static void play_on_channel(struct stasis_app_playback *playback,
258         struct ast_channel *chan)
259 {
260         int res;
261         long offsetms;
262
263         /* Even though these local variables look fairly pointless, the avoid
264          * having a bunch of NULL's passed directly into
265          * ast_control_streamfile() */
266         const char *fwd = NULL;
267         const char *rev = NULL;
268         const char *stop = NULL;
269         const char *pause = NULL;
270         const char *restart = NULL;
271
272         ast_assert(playback != NULL);
273
274         offsetms = playback->offsetms;
275
276         res = playback_first_update(playback, ast_channel_uniqueid(chan));
277
278         if (res != 0) {
279                 return;
280         }
281
282         if (ast_channel_state(chan) != AST_STATE_UP) {
283                 ast_indicate(chan, AST_CONTROL_PROGRESS);
284         }
285
286         if (ast_begins_with(playback->media, SOUND_URI_SCHEME)) {
287                 playback->controllable = 1;
288
289                 /* Play sound */
290                 res = ast_control_streamfile_lang(chan, playback->media + strlen(SOUND_URI_SCHEME),
291                                 fwd, rev, stop, pause, restart, playback->skipms, playback->language,
292                                 &offsetms);
293         } else if (ast_begins_with(playback->media, RECORDING_URI_SCHEME)) {
294                 /* Play recording */
295                 RAII_VAR(struct stasis_app_stored_recording *, recording, NULL,
296                         ao2_cleanup);
297                 const char *relname =
298                         playback->media + strlen(RECORDING_URI_SCHEME);
299                 recording = stasis_app_stored_recording_find_by_name(relname);
300
301                 if (!recording) {
302                         ast_log(LOG_ERROR, "Attempted to play recording '%s' on channel '%s' but recording does not exist",
303                                 relname, ast_channel_name(chan));
304                         return;
305                 }
306
307                 playback->controllable = 1;
308
309                 res = ast_control_streamfile_lang(chan,
310                         stasis_app_stored_recording_get_file(recording), fwd, rev, stop, pause,
311                         restart, playback->skipms, playback->language, &offsetms);
312         } else if (ast_begins_with(playback->media, NUMBER_URI_SCHEME)) {
313                 int number;
314
315                 if (sscanf(playback->media + strlen(NUMBER_URI_SCHEME), "%30d", &number) != 1) {
316                         ast_log(LOG_ERROR, "Attempted to play number '%s' on channel '%s' but number is invalid",
317                                 playback->media + strlen(NUMBER_URI_SCHEME), ast_channel_name(chan));
318                         return;
319                 }
320
321                 res = ast_say_number(chan, number, stop, playback->language, NULL);
322         } else if (ast_begins_with(playback->media, DIGITS_URI_SCHEME)) {
323                 res = ast_say_digit_str(chan, playback->media + strlen(DIGITS_URI_SCHEME),
324                         stop, playback->language);
325         } else if (ast_begins_with(playback->media, CHARACTERS_URI_SCHEME)) {
326                 res = ast_say_character_str(chan, playback->media + strlen(CHARACTERS_URI_SCHEME),
327                         stop, playback->language, AST_SAY_CASE_NONE);
328         } else if (ast_begins_with(playback->media, TONE_URI_SCHEME)) {
329                 playback->controllable = 1;
330                 res = ast_control_tone(chan, playback->media + strlen(TONE_URI_SCHEME));
331         } else {
332                 /* Play URL */
333                 ast_log(LOG_ERROR, "Attempted to play URI '%s' on channel '%s' but scheme is unsupported\n",
334                         playback->media, ast_channel_name(chan));
335                 return;
336         }
337
338         playback_final_update(playback, offsetms, res,
339                 ast_channel_uniqueid(chan));
340
341         return;
342 }
343
344 /*!
345  * \brief Special case code to play while a channel is in a bridge.
346  *
347  * \param bridge_channel The channel's bridge_channel.
348  * \param playback_id Id of the playback to start.
349  */
350 static void play_on_channel_in_bridge(struct ast_bridge_channel *bridge_channel,
351         const char *playback_id)
352 {
353         RAII_VAR(struct stasis_app_playback *, playback, NULL, ao2_cleanup);
354
355         playback = stasis_app_playback_find_by_id(playback_id);
356         if (!playback) {
357                 ast_log(LOG_ERROR, "Couldn't find playback %s\n",
358                         playback_id);
359                 return;
360         }
361
362         play_on_channel(playback, bridge_channel->chan);
363 }
364
365 /*!
366  * \brief \ref RAII_VAR function to remove a playback from the global list when
367  * leaving scope.
368  */
369 static void remove_from_playbacks(struct stasis_app_playback *playback)
370 {
371         ao2_unlink_flags(playbacks, playback,
372                 OBJ_POINTER | OBJ_UNLINK | OBJ_NODATA);
373 }
374
375 static int play_uri(struct stasis_app_control *control,
376         struct ast_channel *chan, void *data)
377 {
378         RAII_VAR(struct stasis_app_playback *, playback, NULL,
379                 remove_from_playbacks);
380         struct ast_bridge *bridge;
381
382         playback = data;
383
384         if (!control) {
385                 return -1;
386         }
387
388         bridge = stasis_app_get_bridge(control);
389         if (bridge) {
390                 struct ast_bridge_channel *bridge_chan;
391
392                 /* Queue up playback on the bridge */
393                 ast_bridge_lock(bridge);
394                 bridge_chan = ao2_bump(bridge_find_channel(bridge, chan));
395                 ast_bridge_unlock(bridge);
396                 if (bridge_chan) {
397                         ast_bridge_channel_queue_playfile_sync(
398                                 bridge_chan,
399                                 play_on_channel_in_bridge,
400                                 playback->id,
401                                 NULL); /* moh_class */
402                 }
403                 ao2_cleanup(bridge_chan);
404         } else {
405                 play_on_channel(playback, chan);
406         }
407
408         return 0;
409 }
410
411 static void set_target_uri(
412         struct stasis_app_playback *playback,
413         enum stasis_app_playback_target_type target_type,
414         const char *target_id)
415 {
416         const char *type = NULL;
417         switch (target_type) {
418         case STASIS_PLAYBACK_TARGET_CHANNEL:
419                 type = "channel";
420                 break;
421         case STASIS_PLAYBACK_TARGET_BRIDGE:
422                 type = "bridge";
423                 break;
424         }
425
426         ast_assert(type != NULL);
427
428         ast_string_field_build(playback, target, "%s:%s", type, target_id);
429 }
430
431 struct stasis_app_playback *stasis_app_control_play_uri(
432         struct stasis_app_control *control, const char *uri,
433         const char *language, const char *target_id,
434         enum stasis_app_playback_target_type target_type,
435         int skipms, long offsetms, const char *id)
436 {
437         RAII_VAR(struct stasis_app_playback *, playback, NULL, ao2_cleanup);
438
439         if (skipms < 0 || offsetms < 0) {
440                 return NULL;
441         }
442
443         ast_debug(3, "%s: Sending play(%s) command\n",
444                 stasis_app_control_get_channel_id(control), uri);
445
446         playback = playback_create(control, id);
447
448         if (skipms == 0) {
449                 skipms = PLAYBACK_DEFAULT_SKIPMS;
450         }
451
452         ast_string_field_set(playback, media, uri);
453         ast_string_field_set(playback, language, language);
454         set_target_uri(playback, target_type, target_id);
455         playback->skipms = skipms;
456         playback->offsetms = offsetms;
457         ao2_link(playbacks, playback);
458
459         playback->state = STASIS_PLAYBACK_STATE_QUEUED;
460         playback_publish(playback);
461
462         /* A ref is kept in the playbacks container; no need to bump */
463         stasis_app_send_command_async(control, play_uri, playback);
464
465         /* Although this should be bumped for the caller */
466         ao2_ref(playback, +1);
467         return playback;
468 }
469
470 enum stasis_app_playback_state stasis_app_playback_get_state(
471         struct stasis_app_playback *control)
472 {
473         SCOPED_AO2LOCK(lock, control);
474         return control->state;
475 }
476
477 const char *stasis_app_playback_get_id(
478         struct stasis_app_playback *control)
479 {
480         /* id is immutable; no lock needed */
481         return control->id;
482 }
483
484 struct stasis_app_playback *stasis_app_playback_find_by_id(const char *id)
485 {
486         return ao2_find(playbacks, id, OBJ_KEY);
487 }
488
489 struct ast_json *stasis_app_playback_to_json(
490         const struct stasis_app_playback *playback)
491 {
492         RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
493
494         if (playback == NULL) {
495                 return NULL;
496         }
497
498         json = ast_json_pack("{s: s, s: s, s: s, s: s, s: s}",
499                 "id", playback->id,
500                 "media_uri", playback->media,
501                 "target_uri", playback->target,
502                 "language", playback->language,
503                 "state", state_to_string(playback->state));
504
505         return ast_json_ref(json);
506 }
507
508 typedef int (*playback_opreation_cb)(struct stasis_app_playback *playback);
509
510 static int playback_noop(struct stasis_app_playback *playback)
511 {
512         return 0;
513 }
514
515 static int playback_cancel(struct stasis_app_playback *playback)
516 {
517         SCOPED_AO2LOCK(lock, playback);
518         playback->state = STASIS_PLAYBACK_STATE_CANCELED;
519         return 0;
520 }
521
522 static int playback_stop(struct stasis_app_playback *playback)
523 {
524         SCOPED_AO2LOCK(lock, playback);
525
526         if (!playback->controllable) {
527                 return -1;
528         }
529
530         playback->state = STASIS_PLAYBACK_STATE_STOPPED;
531         return stasis_app_control_queue_control(playback->control,
532                 AST_CONTROL_STREAM_STOP);
533 }
534
535 static int playback_restart(struct stasis_app_playback *playback)
536 {
537         SCOPED_AO2LOCK(lock, playback);
538
539         if (!playback->controllable) {
540                 return -1;
541         }
542
543         return stasis_app_control_queue_control(playback->control,
544                 AST_CONTROL_STREAM_RESTART);
545 }
546
547 static int playback_pause(struct stasis_app_playback *playback)
548 {
549         SCOPED_AO2LOCK(lock, playback);
550
551         if (!playback->controllable) {
552                 return -1;
553         }
554
555         playback->state = STASIS_PLAYBACK_STATE_PAUSED;
556         playback_publish(playback);
557
558         return stasis_app_control_queue_control(playback->control,
559                 AST_CONTROL_STREAM_SUSPEND);
560 }
561
562 static int playback_unpause(struct stasis_app_playback *playback)
563 {
564         SCOPED_AO2LOCK(lock, playback);
565
566         if (!playback->controllable) {
567                 return -1;
568         }
569
570         playback->state = STASIS_PLAYBACK_STATE_PLAYING;
571         playback_publish(playback);
572
573         return stasis_app_control_queue_control(playback->control,
574                 AST_CONTROL_STREAM_SUSPEND);
575 }
576
577 static int playback_reverse(struct stasis_app_playback *playback)
578 {
579         SCOPED_AO2LOCK(lock, playback);
580
581         if (!playback->controllable) {
582                 return -1;
583         }
584
585         return stasis_app_control_queue_control(playback->control,
586                 AST_CONTROL_STREAM_REVERSE);
587 }
588
589 static int playback_forward(struct stasis_app_playback *playback)
590 {
591         SCOPED_AO2LOCK(lock, playback);
592
593         if (!playback->controllable) {
594                 return -1;
595         }
596
597         return stasis_app_control_queue_control(playback->control,
598                 AST_CONTROL_STREAM_FORWARD);
599 }
600
601 /*!
602  * \brief A sparse array detailing how commands should be handled in the
603  * various playback states. Unset entries imply invalid operations.
604  */
605 playback_opreation_cb operations[STASIS_PLAYBACK_STATE_MAX][STASIS_PLAYBACK_MEDIA_OP_MAX] = {
606         [STASIS_PLAYBACK_STATE_QUEUED][STASIS_PLAYBACK_STOP] = playback_cancel,
607         [STASIS_PLAYBACK_STATE_QUEUED][STASIS_PLAYBACK_RESTART] = playback_noop,
608
609         [STASIS_PLAYBACK_STATE_PLAYING][STASIS_PLAYBACK_STOP] = playback_stop,
610         [STASIS_PLAYBACK_STATE_PLAYING][STASIS_PLAYBACK_RESTART] = playback_restart,
611         [STASIS_PLAYBACK_STATE_PLAYING][STASIS_PLAYBACK_PAUSE] = playback_pause,
612         [STASIS_PLAYBACK_STATE_PLAYING][STASIS_PLAYBACK_UNPAUSE] = playback_noop,
613         [STASIS_PLAYBACK_STATE_PLAYING][STASIS_PLAYBACK_REVERSE] = playback_reverse,
614         [STASIS_PLAYBACK_STATE_PLAYING][STASIS_PLAYBACK_FORWARD] = playback_forward,
615
616         [STASIS_PLAYBACK_STATE_PAUSED][STASIS_PLAYBACK_STOP] = playback_stop,
617         [STASIS_PLAYBACK_STATE_PAUSED][STASIS_PLAYBACK_PAUSE] = playback_noop,
618         [STASIS_PLAYBACK_STATE_PAUSED][STASIS_PLAYBACK_UNPAUSE] = playback_unpause,
619
620         [STASIS_PLAYBACK_STATE_COMPLETE][STASIS_PLAYBACK_STOP] = playback_noop,
621         [STASIS_PLAYBACK_STATE_CANCELED][STASIS_PLAYBACK_STOP] = playback_noop,
622         [STASIS_PLAYBACK_STATE_STOPPED][STASIS_PLAYBACK_STOP] = playback_noop,
623 };
624
625 enum stasis_playback_oper_results stasis_app_playback_operation(
626         struct stasis_app_playback *playback,
627         enum stasis_app_playback_media_operation operation)
628 {
629         playback_opreation_cb cb;
630         SCOPED_AO2LOCK(lock, playback);
631
632         ast_assert(playback->state >= 0 && playback->state < STASIS_PLAYBACK_STATE_MAX);
633
634         if (operation < 0 || operation >= STASIS_PLAYBACK_MEDIA_OP_MAX) {
635                 ast_log(LOG_ERROR, "Invalid playback operation %u\n", operation);
636                 return -1;
637         }
638
639         cb = operations[playback->state][operation];
640
641         if (!cb) {
642                 if (playback->state != STASIS_PLAYBACK_STATE_PLAYING) {
643                         /* So we can be specific in our error message. */
644                         return STASIS_PLAYBACK_OPER_NOT_PLAYING;
645                 } else {
646                         /* And, really, all operations should be valid during
647                          * playback */
648                         ast_log(LOG_ERROR,
649                                 "Unhandled operation during playback: %u\n",
650                                 operation);
651                         return STASIS_PLAYBACK_OPER_FAILED;
652                 }
653         }
654
655         return cb(playback) ?
656                 STASIS_PLAYBACK_OPER_FAILED : STASIS_PLAYBACK_OPER_OK;
657 }
658
659 static int load_module(void)
660 {
661         int r;
662
663         r = STASIS_MESSAGE_TYPE_INIT(stasis_app_playback_snapshot_type);
664         if (r != 0) {
665                 return AST_MODULE_LOAD_FAILURE;
666         }
667
668         playbacks = ao2_container_alloc(PLAYBACK_BUCKETS, playback_hash,
669                 playback_cmp);
670         if (!playbacks) {
671                 return AST_MODULE_LOAD_FAILURE;
672         }
673         return AST_MODULE_LOAD_SUCCESS;
674 }
675
676 static int unload_module(void)
677 {
678         ao2_cleanup(playbacks);
679         playbacks = NULL;
680         STASIS_MESSAGE_TYPE_CLEANUP(stasis_app_playback_snapshot_type);
681         return 0;
682 }
683
684 AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS, "Stasis application playback support",
685         .support_level = AST_MODULE_SUPPORT_CORE,
686         .load = load_module,
687         .unload = unload_module,
688         .nonoptreq = "res_stasis,res_stasis_recording");