res_stasis_recording.c 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661
  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. /*! \file
  19. *
  20. * \brief res_stasis recording support.
  21. *
  22. * \author David M. Lee, II <dlee@digium.com>
  23. */
  24. /*** MODULEINFO
  25. <depend type="module">res_stasis</depend>
  26. <support_level>core</support_level>
  27. ***/
  28. #include "asterisk.h"
  29. ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
  30. #include "asterisk/dsp.h"
  31. #include "asterisk/file.h"
  32. #include "asterisk/module.h"
  33. #include "asterisk/paths.h"
  34. #include "asterisk/stasis_app_impl.h"
  35. #include "asterisk/stasis_app_recording.h"
  36. #include "asterisk/stasis_channels.h"
  37. /*! Number of hash buckets for recording container. Keep it prime! */
  38. #define RECORDING_BUCKETS 127
  39. /*! Comment is ignored by most formats, so we will ignore it, too. */
  40. #define RECORDING_COMMENT NULL
  41. /*! Recording check is unimplemented. le sigh */
  42. #define RECORDING_CHECK 0
  43. /*! Container of all current recordings */
  44. static struct ao2_container *recordings;
  45. struct stasis_app_recording {
  46. /*! Recording options. */
  47. struct stasis_app_recording_options *options;
  48. /*! Absolute path (minus extension) of the recording */
  49. char *absolute_name;
  50. /*! Control object for the channel we're recording */
  51. struct stasis_app_control *control;
  52. /*! Current state of the recording. */
  53. enum stasis_app_recording_state state;
  54. /*! Duration calculations */
  55. struct {
  56. /*! Total duration */
  57. int total;
  58. /*! Duration minus any silence */
  59. int energy_only;
  60. } duration;
  61. /*! Indicates whether the recording is currently muted */
  62. int muted:1;
  63. };
  64. static struct ast_json *recording_to_json(struct stasis_message *message,
  65. const struct stasis_message_sanitizer *sanitize)
  66. {
  67. struct ast_channel_blob *channel_blob = stasis_message_data(message);
  68. struct ast_json *blob = channel_blob->blob;
  69. const char *state =
  70. ast_json_string_get(ast_json_object_get(blob, "state"));
  71. const char *type;
  72. if (!strcmp(state, "recording")) {
  73. type = "RecordingStarted";
  74. } else if (!strcmp(state, "done") || !strcasecmp(state, "canceled")) {
  75. type = "RecordingFinished";
  76. } else if (!strcmp(state, "failed")) {
  77. type = "RecordingFailed";
  78. } else {
  79. return NULL;
  80. }
  81. return ast_json_pack("{s: s, s: O}",
  82. "type", type,
  83. "recording", blob);
  84. }
  85. STASIS_MESSAGE_TYPE_DEFN(stasis_app_recording_snapshot_type,
  86. .to_json = recording_to_json,
  87. );
  88. static int recording_hash(const void *obj, int flags)
  89. {
  90. const struct stasis_app_recording *recording = obj;
  91. const char *id = flags & OBJ_KEY ? obj : recording->options->name;
  92. return ast_str_hash(id);
  93. }
  94. static int recording_cmp(void *obj, void *arg, int flags)
  95. {
  96. struct stasis_app_recording *lhs = obj;
  97. struct stasis_app_recording *rhs = arg;
  98. const char *rhs_id = flags & OBJ_KEY ? arg : rhs->options->name;
  99. if (strcmp(lhs->options->name, rhs_id) == 0) {
  100. return CMP_MATCH | CMP_STOP;
  101. } else {
  102. return 0;
  103. }
  104. }
  105. static const char *state_to_string(enum stasis_app_recording_state state)
  106. {
  107. switch (state) {
  108. case STASIS_APP_RECORDING_STATE_QUEUED:
  109. return "queued";
  110. case STASIS_APP_RECORDING_STATE_RECORDING:
  111. return "recording";
  112. case STASIS_APP_RECORDING_STATE_PAUSED:
  113. return "paused";
  114. case STASIS_APP_RECORDING_STATE_COMPLETE:
  115. return "done";
  116. case STASIS_APP_RECORDING_STATE_FAILED:
  117. return "failed";
  118. case STASIS_APP_RECORDING_STATE_CANCELED:
  119. return "canceled";
  120. case STASIS_APP_RECORDING_STATE_MAX:
  121. return "?";
  122. }
  123. return "?";
  124. }
  125. static void recording_options_dtor(void *obj)
  126. {
  127. struct stasis_app_recording_options *options = obj;
  128. ast_string_field_free_memory(options);
  129. }
  130. struct stasis_app_recording_options *stasis_app_recording_options_create(
  131. const char *name, const char *format)
  132. {
  133. RAII_VAR(struct stasis_app_recording_options *, options, NULL,
  134. ao2_cleanup);
  135. options = ao2_alloc(sizeof(*options), recording_options_dtor);
  136. if (!options || ast_string_field_init(options, 128)) {
  137. return NULL;
  138. }
  139. ast_string_field_set(options, name, name);
  140. ast_string_field_set(options, format, format);
  141. ao2_ref(options, +1);
  142. return options;
  143. }
  144. char stasis_app_recording_termination_parse(const char *str)
  145. {
  146. if (ast_strlen_zero(str)) {
  147. return STASIS_APP_RECORDING_TERMINATE_NONE;
  148. }
  149. if (strcasecmp(str, "none") == 0) {
  150. return STASIS_APP_RECORDING_TERMINATE_NONE;
  151. }
  152. if (strcasecmp(str, "any") == 0) {
  153. return STASIS_APP_RECORDING_TERMINATE_ANY;
  154. }
  155. if (strcasecmp(str, "#") == 0) {
  156. return '#';
  157. }
  158. if (strcasecmp(str, "*") == 0) {
  159. return '*';
  160. }
  161. return STASIS_APP_RECORDING_TERMINATE_INVALID;
  162. }
  163. enum ast_record_if_exists stasis_app_recording_if_exists_parse(
  164. const char *str)
  165. {
  166. if (ast_strlen_zero(str)) {
  167. /* Default value */
  168. return AST_RECORD_IF_EXISTS_FAIL;
  169. }
  170. if (strcasecmp(str, "fail") == 0) {
  171. return AST_RECORD_IF_EXISTS_FAIL;
  172. }
  173. if (strcasecmp(str, "overwrite") == 0) {
  174. return AST_RECORD_IF_EXISTS_OVERWRITE;
  175. }
  176. if (strcasecmp(str, "append") == 0) {
  177. return AST_RECORD_IF_EXISTS_APPEND;
  178. }
  179. return -1;
  180. }
  181. static void recording_publish(struct stasis_app_recording *recording, const char *cause)
  182. {
  183. RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
  184. RAII_VAR(struct stasis_message *, message, NULL, ao2_cleanup);
  185. ast_assert(recording != NULL);
  186. json = stasis_app_recording_to_json(recording);
  187. if (json == NULL) {
  188. return;
  189. }
  190. if (!ast_strlen_zero(cause)) {
  191. struct ast_json *failure_cause = ast_json_string_create(cause);
  192. if (!failure_cause) {
  193. return;
  194. }
  195. if (ast_json_object_set(json, "cause", failure_cause)) {
  196. return;
  197. }
  198. }
  199. message = ast_channel_blob_create_from_cache(
  200. stasis_app_control_get_channel_id(recording->control),
  201. stasis_app_recording_snapshot_type(), json);
  202. if (message == NULL) {
  203. return;
  204. }
  205. stasis_app_control_publish(recording->control, message);
  206. }
  207. static void recording_set_state(struct stasis_app_recording *recording,
  208. enum stasis_app_recording_state state,
  209. const char *cause)
  210. {
  211. SCOPED_AO2LOCK(lock, recording);
  212. recording->state = state;
  213. recording_publish(recording, cause);
  214. }
  215. static enum stasis_app_control_channel_result check_rule_recording(
  216. const struct stasis_app_control *control)
  217. {
  218. return STASIS_APP_CHANNEL_RECORDING;
  219. }
  220. struct stasis_app_control_rule rule_recording = {
  221. .check_rule = check_rule_recording
  222. };
  223. static void recording_fail(struct stasis_app_control *control,
  224. struct stasis_app_recording *recording,
  225. const char *cause)
  226. {
  227. stasis_app_control_unregister_add_rule(control, &rule_recording);
  228. recording_set_state(
  229. recording, STASIS_APP_RECORDING_STATE_FAILED, cause);
  230. }
  231. static void recording_cleanup(void *data)
  232. {
  233. struct stasis_app_recording *recording = data;
  234. ao2_unlink_flags(recordings, recording,
  235. OBJ_POINTER | OBJ_UNLINK | OBJ_NODATA);
  236. ao2_ref(recording, -1);
  237. }
  238. static int record_file(struct stasis_app_control *control,
  239. struct ast_channel *chan, void *data)
  240. {
  241. struct stasis_app_recording *recording = data;
  242. char *acceptdtmf;
  243. int res;
  244. ast_assert(recording != NULL);
  245. if (stasis_app_get_bridge(control)) {
  246. ast_log(LOG_ERROR, "Cannot record channel while in bridge\n");
  247. recording_fail(control, recording, "Cannot record channel while in bridge");
  248. return -1;
  249. }
  250. switch (recording->options->terminate_on) {
  251. case STASIS_APP_RECORDING_TERMINATE_NONE:
  252. case STASIS_APP_RECORDING_TERMINATE_INVALID:
  253. acceptdtmf = "";
  254. break;
  255. case STASIS_APP_RECORDING_TERMINATE_ANY:
  256. acceptdtmf = "#*0123456789abcd";
  257. break;
  258. default:
  259. acceptdtmf = ast_alloca(2);
  260. acceptdtmf[0] = recording->options->terminate_on;
  261. acceptdtmf[1] = '\0';
  262. }
  263. res = ast_auto_answer(chan);
  264. if (res != 0) {
  265. ast_debug(3, "%s: Failed to answer\n",
  266. ast_channel_uniqueid(chan));
  267. recording_fail(control, recording, "Failed to answer channel");
  268. return -1;
  269. }
  270. recording_set_state(
  271. recording, STASIS_APP_RECORDING_STATE_RECORDING, NULL);
  272. ast_play_and_record_full(chan,
  273. NULL, /* playfile */
  274. recording->absolute_name,
  275. recording->options->max_duration_seconds,
  276. recording->options->format,
  277. &recording->duration.total,
  278. recording->options->max_silence_seconds ? &recording->duration.energy_only : NULL,
  279. recording->options->beep,
  280. -1, /* silencethreshold */
  281. recording->options->max_silence_seconds * 1000,
  282. NULL, /* path */
  283. acceptdtmf,
  284. NULL, /* canceldtmf */
  285. 1, /* skip_confirmation_sound */
  286. recording->options->if_exists);
  287. ast_debug(3, "%s: Recording complete\n", ast_channel_uniqueid(chan));
  288. recording_set_state(
  289. recording, STASIS_APP_RECORDING_STATE_COMPLETE, NULL);
  290. stasis_app_control_unregister_add_rule(control, &rule_recording);
  291. return 0;
  292. }
  293. static void recording_dtor(void *obj)
  294. {
  295. struct stasis_app_recording *recording = obj;
  296. ast_free(recording->absolute_name);
  297. ao2_cleanup(recording->options);
  298. }
  299. struct stasis_app_recording *stasis_app_control_record(
  300. struct stasis_app_control *control,
  301. struct stasis_app_recording_options *options)
  302. {
  303. struct stasis_app_recording *recording;
  304. char *last_slash;
  305. errno = 0;
  306. if (options == NULL ||
  307. ast_strlen_zero(options->name) ||
  308. ast_strlen_zero(options->format) ||
  309. options->max_silence_seconds < 0 ||
  310. options->max_duration_seconds < 0) {
  311. errno = EINVAL;
  312. return NULL;
  313. }
  314. ast_debug(3, "%s: Sending record(%s.%s) command\n",
  315. stasis_app_control_get_channel_id(control), options->name,
  316. options->format);
  317. recording = ao2_alloc(sizeof(*recording), recording_dtor);
  318. if (!recording) {
  319. errno = ENOMEM;
  320. return NULL;
  321. }
  322. recording->duration.total = -1;
  323. recording->duration.energy_only = -1;
  324. ast_asprintf(&recording->absolute_name, "%s/%s",
  325. ast_config_AST_RECORDING_DIR, options->name);
  326. if (recording->absolute_name == NULL) {
  327. errno = ENOMEM;
  328. ao2_ref(recording, -1);
  329. return NULL;
  330. }
  331. if ((last_slash = strrchr(recording->absolute_name, '/'))) {
  332. *last_slash = '\0';
  333. if (ast_safe_mkdir(ast_config_AST_RECORDING_DIR,
  334. recording->absolute_name, 0777) != 0) {
  335. /* errno set by ast_mkdir */
  336. ao2_ref(recording, -1);
  337. return NULL;
  338. }
  339. *last_slash = '/';
  340. }
  341. ao2_ref(options, +1);
  342. recording->options = options;
  343. recording->control = control;
  344. recording->state = STASIS_APP_RECORDING_STATE_QUEUED;
  345. if ((recording->options->if_exists == AST_RECORD_IF_EXISTS_FAIL) &&
  346. (ast_fileexists(recording->absolute_name, NULL, NULL))) {
  347. ast_log(LOG_WARNING, "Recording file '%s' already exists and ifExists option is failure.\n",
  348. recording->absolute_name);
  349. errno = EEXIST;
  350. ao2_ref(recording, -1);
  351. return NULL;
  352. }
  353. {
  354. RAII_VAR(struct stasis_app_recording *, old_recording, NULL,
  355. ao2_cleanup);
  356. SCOPED_AO2LOCK(lock, recordings);
  357. old_recording = ao2_find(recordings, options->name,
  358. OBJ_KEY | OBJ_NOLOCK);
  359. if (old_recording) {
  360. ast_log(LOG_WARNING,
  361. "Recording %s already in progress\n",
  362. recording->options->name);
  363. errno = EEXIST;
  364. ao2_ref(recording, -1);
  365. return NULL;
  366. }
  367. ao2_link(recordings, recording);
  368. }
  369. stasis_app_control_register_add_rule(control, &rule_recording);
  370. stasis_app_send_command_async(control, record_file, ao2_bump(recording), recording_cleanup);
  371. return recording;
  372. }
  373. enum stasis_app_recording_state stasis_app_recording_get_state(
  374. struct stasis_app_recording *recording)
  375. {
  376. return recording->state;
  377. }
  378. const char *stasis_app_recording_get_name(
  379. struct stasis_app_recording *recording)
  380. {
  381. return recording->options->name;
  382. }
  383. struct stasis_app_recording *stasis_app_recording_find_by_name(const char *name)
  384. {
  385. RAII_VAR(struct stasis_app_recording *, recording, NULL, ao2_cleanup);
  386. recording = ao2_find(recordings, name, OBJ_KEY);
  387. if (recording == NULL) {
  388. return NULL;
  389. }
  390. ao2_ref(recording, +1);
  391. return recording;
  392. }
  393. struct ast_json *stasis_app_recording_to_json(
  394. const struct stasis_app_recording *recording)
  395. {
  396. RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
  397. if (recording == NULL) {
  398. return NULL;
  399. }
  400. json = ast_json_pack("{s: s, s: s, s: s, s: s}",
  401. "name", recording->options->name,
  402. "format", recording->options->format,
  403. "state", state_to_string(recording->state),
  404. "target_uri", recording->options->target);
  405. if (json && recording->duration.total > -1) {
  406. ast_json_object_set(json, "duration",
  407. ast_json_integer_create(recording->duration.total));
  408. }
  409. if (json && recording->duration.energy_only > -1) {
  410. ast_json_object_set(json, "talking_duration",
  411. ast_json_integer_create(recording->duration.energy_only));
  412. ast_json_object_set(json, "silence_duration",
  413. ast_json_integer_create(recording->duration.total - recording->duration.energy_only));
  414. }
  415. return ast_json_ref(json);
  416. }
  417. typedef int (*recording_operation_cb)(struct stasis_app_recording *recording);
  418. static int recording_noop(struct stasis_app_recording *recording)
  419. {
  420. return 0;
  421. }
  422. static int recording_disregard(struct stasis_app_recording *recording)
  423. {
  424. recording->state = STASIS_APP_RECORDING_STATE_CANCELED;
  425. return 0;
  426. }
  427. static int recording_cancel(struct stasis_app_recording *recording)
  428. {
  429. int res = 0;
  430. recording->state = STASIS_APP_RECORDING_STATE_CANCELED;
  431. res |= stasis_app_control_queue_control(recording->control,
  432. AST_CONTROL_RECORD_CANCEL);
  433. res |= ast_filedelete(recording->absolute_name, NULL);
  434. return res;
  435. }
  436. static int recording_stop(struct stasis_app_recording *recording)
  437. {
  438. recording->state = STASIS_APP_RECORDING_STATE_COMPLETE;
  439. return stasis_app_control_queue_control(recording->control,
  440. AST_CONTROL_RECORD_STOP);
  441. }
  442. static int recording_pause(struct stasis_app_recording *recording)
  443. {
  444. recording->state = STASIS_APP_RECORDING_STATE_PAUSED;
  445. return stasis_app_control_queue_control(recording->control,
  446. AST_CONTROL_RECORD_SUSPEND);
  447. }
  448. static int recording_unpause(struct stasis_app_recording *recording)
  449. {
  450. recording->state = STASIS_APP_RECORDING_STATE_RECORDING;
  451. return stasis_app_control_queue_control(recording->control,
  452. AST_CONTROL_RECORD_SUSPEND);
  453. }
  454. static int recording_mute(struct stasis_app_recording *recording)
  455. {
  456. if (recording->muted) {
  457. /* already muted */
  458. return 0;
  459. }
  460. recording->muted = 1;
  461. return stasis_app_control_queue_control(recording->control,
  462. AST_CONTROL_RECORD_MUTE);
  463. }
  464. static int recording_unmute(struct stasis_app_recording *recording)
  465. {
  466. if (!recording->muted) {
  467. /* already unmuted */
  468. return 0;
  469. }
  470. return stasis_app_control_queue_control(recording->control,
  471. AST_CONTROL_RECORD_MUTE);
  472. }
  473. recording_operation_cb operations[STASIS_APP_RECORDING_STATE_MAX][STASIS_APP_RECORDING_OPER_MAX] = {
  474. [STASIS_APP_RECORDING_STATE_QUEUED][STASIS_APP_RECORDING_CANCEL] = recording_disregard,
  475. [STASIS_APP_RECORDING_STATE_QUEUED][STASIS_APP_RECORDING_STOP] = recording_disregard,
  476. [STASIS_APP_RECORDING_STATE_RECORDING][STASIS_APP_RECORDING_CANCEL] = recording_cancel,
  477. [STASIS_APP_RECORDING_STATE_RECORDING][STASIS_APP_RECORDING_STOP] = recording_stop,
  478. [STASIS_APP_RECORDING_STATE_RECORDING][STASIS_APP_RECORDING_PAUSE] = recording_pause,
  479. [STASIS_APP_RECORDING_STATE_RECORDING][STASIS_APP_RECORDING_UNPAUSE] = recording_noop,
  480. [STASIS_APP_RECORDING_STATE_RECORDING][STASIS_APP_RECORDING_MUTE] = recording_mute,
  481. [STASIS_APP_RECORDING_STATE_RECORDING][STASIS_APP_RECORDING_UNMUTE] = recording_unmute,
  482. [STASIS_APP_RECORDING_STATE_PAUSED][STASIS_APP_RECORDING_CANCEL] = recording_cancel,
  483. [STASIS_APP_RECORDING_STATE_PAUSED][STASIS_APP_RECORDING_STOP] = recording_stop,
  484. [STASIS_APP_RECORDING_STATE_PAUSED][STASIS_APP_RECORDING_PAUSE] = recording_noop,
  485. [STASIS_APP_RECORDING_STATE_PAUSED][STASIS_APP_RECORDING_UNPAUSE] = recording_unpause,
  486. [STASIS_APP_RECORDING_STATE_PAUSED][STASIS_APP_RECORDING_MUTE] = recording_mute,
  487. [STASIS_APP_RECORDING_STATE_PAUSED][STASIS_APP_RECORDING_UNMUTE] = recording_unmute,
  488. };
  489. enum stasis_app_recording_oper_results stasis_app_recording_operation(
  490. struct stasis_app_recording *recording,
  491. enum stasis_app_recording_media_operation operation)
  492. {
  493. recording_operation_cb cb;
  494. SCOPED_AO2LOCK(lock, recording);
  495. if (recording->state < 0 || recording->state >= STASIS_APP_RECORDING_STATE_MAX) {
  496. ast_log(LOG_WARNING, "Invalid recording state %u\n",
  497. recording->state);
  498. return -1;
  499. }
  500. if (operation < 0 || operation >= STASIS_APP_RECORDING_OPER_MAX) {
  501. ast_log(LOG_WARNING, "Invalid recording operation %u\n",
  502. operation);
  503. return -1;
  504. }
  505. cb = operations[recording->state][operation];
  506. if (!cb) {
  507. if (recording->state != STASIS_APP_RECORDING_STATE_RECORDING) {
  508. /* So we can be specific in our error message. */
  509. return STASIS_APP_RECORDING_OPER_NOT_RECORDING;
  510. } else {
  511. /* And, really, all operations should be valid during
  512. * recording */
  513. ast_log(LOG_ERROR,
  514. "Unhandled operation during recording: %u\n",
  515. operation);
  516. return STASIS_APP_RECORDING_OPER_FAILED;
  517. }
  518. }
  519. return cb(recording) ?
  520. STASIS_APP_RECORDING_OPER_FAILED : STASIS_APP_RECORDING_OPER_OK;
  521. }
  522. static int load_module(void)
  523. {
  524. int r;
  525. r = STASIS_MESSAGE_TYPE_INIT(stasis_app_recording_snapshot_type);
  526. if (r != 0) {
  527. return AST_MODULE_LOAD_FAILURE;
  528. }
  529. recordings = ao2_container_alloc(RECORDING_BUCKETS, recording_hash,
  530. recording_cmp);
  531. if (!recordings) {
  532. return AST_MODULE_LOAD_FAILURE;
  533. }
  534. return AST_MODULE_LOAD_SUCCESS;
  535. }
  536. static int unload_module(void)
  537. {
  538. ao2_cleanup(recordings);
  539. recordings = NULL;
  540. STASIS_MESSAGE_TYPE_CLEANUP(stasis_app_recording_snapshot_type);
  541. return 0;
  542. }
  543. AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_ORDER, "Stasis application recording support",
  544. .support_level = AST_MODULE_SUPPORT_CORE,
  545. .load = load_module,
  546. .unload = unload_module,
  547. .nonoptreq = "res_stasis",
  548. .load_pri = AST_MODPRI_APP_DEPEND);