res_stasis_recording.c 18 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", ast_json_deep_copy(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 AST_RECORD_IF_EXISTS_ERROR;
  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. /*
  221. * XXX This only works because there is one and only one rule in
  222. * the system so it can be added to any number of channels
  223. * without issue. However, as soon as there is another rule then
  224. * watch out for weirdness because of cross linked lists.
  225. */
  226. static struct stasis_app_control_rule rule_recording = {
  227. .check_rule = check_rule_recording
  228. };
  229. static void recording_fail(struct stasis_app_control *control,
  230. struct stasis_app_recording *recording,
  231. const char *cause)
  232. {
  233. stasis_app_control_unregister_add_rule(control, &rule_recording);
  234. recording_set_state(
  235. recording, STASIS_APP_RECORDING_STATE_FAILED, cause);
  236. }
  237. static void recording_cleanup(void *data)
  238. {
  239. struct stasis_app_recording *recording = data;
  240. ao2_unlink_flags(recordings, recording,
  241. OBJ_POINTER | OBJ_UNLINK | OBJ_NODATA);
  242. ao2_ref(recording, -1);
  243. }
  244. static int record_file(struct stasis_app_control *control,
  245. struct ast_channel *chan, void *data)
  246. {
  247. struct stasis_app_recording *recording = data;
  248. char *acceptdtmf;
  249. int res;
  250. ast_assert(recording != NULL);
  251. if (stasis_app_get_bridge(control)) {
  252. ast_log(LOG_ERROR, "Cannot record channel while in bridge\n");
  253. recording_fail(control, recording, "Cannot record channel while in bridge");
  254. return -1;
  255. }
  256. switch (recording->options->terminate_on) {
  257. case STASIS_APP_RECORDING_TERMINATE_NONE:
  258. case STASIS_APP_RECORDING_TERMINATE_INVALID:
  259. acceptdtmf = "";
  260. break;
  261. case STASIS_APP_RECORDING_TERMINATE_ANY:
  262. acceptdtmf = "#*0123456789abcd";
  263. break;
  264. default:
  265. acceptdtmf = ast_alloca(2);
  266. acceptdtmf[0] = recording->options->terminate_on;
  267. acceptdtmf[1] = '\0';
  268. }
  269. res = ast_auto_answer(chan);
  270. if (res != 0) {
  271. ast_debug(3, "%s: Failed to answer\n",
  272. ast_channel_uniqueid(chan));
  273. recording_fail(control, recording, "Failed to answer channel");
  274. return -1;
  275. }
  276. recording_set_state(
  277. recording, STASIS_APP_RECORDING_STATE_RECORDING, NULL);
  278. ast_play_and_record_full(chan,
  279. NULL, /* playfile */
  280. recording->absolute_name,
  281. recording->options->max_duration_seconds,
  282. recording->options->format,
  283. &recording->duration.total,
  284. recording->options->max_silence_seconds ? &recording->duration.energy_only : NULL,
  285. recording->options->beep,
  286. -1, /* silencethreshold */
  287. recording->options->max_silence_seconds * 1000,
  288. NULL, /* path */
  289. acceptdtmf,
  290. NULL, /* canceldtmf */
  291. 1, /* skip_confirmation_sound */
  292. recording->options->if_exists);
  293. ast_debug(3, "%s: Recording complete\n", ast_channel_uniqueid(chan));
  294. recording_set_state(
  295. recording, STASIS_APP_RECORDING_STATE_COMPLETE, NULL);
  296. stasis_app_control_unregister_add_rule(control, &rule_recording);
  297. return 0;
  298. }
  299. static void recording_dtor(void *obj)
  300. {
  301. struct stasis_app_recording *recording = obj;
  302. ast_free(recording->absolute_name);
  303. ao2_cleanup(recording->control);
  304. ao2_cleanup(recording->options);
  305. }
  306. struct stasis_app_recording *stasis_app_control_record(
  307. struct stasis_app_control *control,
  308. struct stasis_app_recording_options *options)
  309. {
  310. struct stasis_app_recording *recording;
  311. char *last_slash;
  312. errno = 0;
  313. if (options == NULL ||
  314. ast_strlen_zero(options->name) ||
  315. ast_strlen_zero(options->format) ||
  316. options->max_silence_seconds < 0 ||
  317. options->max_duration_seconds < 0) {
  318. errno = EINVAL;
  319. return NULL;
  320. }
  321. ast_debug(3, "%s: Sending record(%s.%s) command\n",
  322. stasis_app_control_get_channel_id(control), options->name,
  323. options->format);
  324. recording = ao2_alloc(sizeof(*recording), recording_dtor);
  325. if (!recording) {
  326. errno = ENOMEM;
  327. return NULL;
  328. }
  329. recording->duration.total = -1;
  330. recording->duration.energy_only = -1;
  331. ast_asprintf(&recording->absolute_name, "%s/%s",
  332. ast_config_AST_RECORDING_DIR, options->name);
  333. if (recording->absolute_name == NULL) {
  334. errno = ENOMEM;
  335. ao2_ref(recording, -1);
  336. return NULL;
  337. }
  338. if ((last_slash = strrchr(recording->absolute_name, '/'))) {
  339. *last_slash = '\0';
  340. if (ast_safe_mkdir(ast_config_AST_RECORDING_DIR,
  341. recording->absolute_name, 0777) != 0) {
  342. /* errno set by ast_mkdir */
  343. ao2_ref(recording, -1);
  344. return NULL;
  345. }
  346. *last_slash = '/';
  347. }
  348. ao2_ref(options, +1);
  349. recording->options = options;
  350. ao2_ref(control, +1);
  351. recording->control = control;
  352. recording->state = STASIS_APP_RECORDING_STATE_QUEUED;
  353. if ((recording->options->if_exists == AST_RECORD_IF_EXISTS_FAIL) &&
  354. (ast_fileexists(recording->absolute_name, NULL, NULL))) {
  355. ast_log(LOG_WARNING, "Recording file '%s' already exists and ifExists option is failure.\n",
  356. recording->absolute_name);
  357. errno = EEXIST;
  358. ao2_ref(recording, -1);
  359. return NULL;
  360. }
  361. {
  362. RAII_VAR(struct stasis_app_recording *, old_recording, NULL,
  363. ao2_cleanup);
  364. SCOPED_AO2LOCK(lock, recordings);
  365. old_recording = ao2_find(recordings, options->name,
  366. OBJ_KEY | OBJ_NOLOCK);
  367. if (old_recording) {
  368. ast_log(LOG_WARNING,
  369. "Recording %s already in progress\n",
  370. recording->options->name);
  371. errno = EEXIST;
  372. ao2_ref(recording, -1);
  373. return NULL;
  374. }
  375. ao2_link(recordings, recording);
  376. }
  377. stasis_app_control_register_add_rule(control, &rule_recording);
  378. stasis_app_send_command_async(control, record_file, ao2_bump(recording), recording_cleanup);
  379. return recording;
  380. }
  381. enum stasis_app_recording_state stasis_app_recording_get_state(
  382. struct stasis_app_recording *recording)
  383. {
  384. return recording->state;
  385. }
  386. const char *stasis_app_recording_get_name(
  387. struct stasis_app_recording *recording)
  388. {
  389. return recording->options->name;
  390. }
  391. struct stasis_app_recording *stasis_app_recording_find_by_name(const char *name)
  392. {
  393. return ao2_find(recordings, name, OBJ_KEY);
  394. }
  395. struct ast_json *stasis_app_recording_to_json(
  396. const struct stasis_app_recording *recording)
  397. {
  398. RAII_VAR(struct ast_json *, json, NULL, ast_json_unref);
  399. if (recording == NULL) {
  400. return NULL;
  401. }
  402. json = ast_json_pack("{s: s, s: s, s: s, s: s}",
  403. "name", recording->options->name,
  404. "format", recording->options->format,
  405. "state", state_to_string(recording->state),
  406. "target_uri", recording->options->target);
  407. if (json && recording->duration.total > -1) {
  408. ast_json_object_set(json, "duration",
  409. ast_json_integer_create(recording->duration.total));
  410. }
  411. if (json && recording->duration.energy_only > -1) {
  412. ast_json_object_set(json, "talking_duration",
  413. ast_json_integer_create(recording->duration.energy_only));
  414. ast_json_object_set(json, "silence_duration",
  415. ast_json_integer_create(recording->duration.total - recording->duration.energy_only));
  416. }
  417. return ast_json_ref(json);
  418. }
  419. typedef int (*recording_operation_cb)(struct stasis_app_recording *recording);
  420. static int recording_noop(struct stasis_app_recording *recording)
  421. {
  422. return 0;
  423. }
  424. static int recording_disregard(struct stasis_app_recording *recording)
  425. {
  426. recording->state = STASIS_APP_RECORDING_STATE_CANCELED;
  427. return 0;
  428. }
  429. static int recording_cancel(struct stasis_app_recording *recording)
  430. {
  431. int res = 0;
  432. recording->state = STASIS_APP_RECORDING_STATE_CANCELED;
  433. res |= stasis_app_control_queue_control(recording->control,
  434. AST_CONTROL_RECORD_CANCEL);
  435. res |= ast_filedelete(recording->absolute_name, NULL);
  436. return res;
  437. }
  438. static int recording_stop(struct stasis_app_recording *recording)
  439. {
  440. recording->state = STASIS_APP_RECORDING_STATE_COMPLETE;
  441. return stasis_app_control_queue_control(recording->control,
  442. AST_CONTROL_RECORD_STOP);
  443. }
  444. static int recording_pause(struct stasis_app_recording *recording)
  445. {
  446. recording->state = STASIS_APP_RECORDING_STATE_PAUSED;
  447. return stasis_app_control_queue_control(recording->control,
  448. AST_CONTROL_RECORD_SUSPEND);
  449. }
  450. static int recording_unpause(struct stasis_app_recording *recording)
  451. {
  452. recording->state = STASIS_APP_RECORDING_STATE_RECORDING;
  453. return stasis_app_control_queue_control(recording->control,
  454. AST_CONTROL_RECORD_SUSPEND);
  455. }
  456. static int recording_mute(struct stasis_app_recording *recording)
  457. {
  458. if (recording->muted) {
  459. /* already muted */
  460. return 0;
  461. }
  462. recording->muted = 1;
  463. return stasis_app_control_queue_control(recording->control,
  464. AST_CONTROL_RECORD_MUTE);
  465. }
  466. static int recording_unmute(struct stasis_app_recording *recording)
  467. {
  468. if (!recording->muted) {
  469. /* already unmuted */
  470. return 0;
  471. }
  472. return stasis_app_control_queue_control(recording->control,
  473. AST_CONTROL_RECORD_MUTE);
  474. }
  475. recording_operation_cb operations[STASIS_APP_RECORDING_STATE_MAX][STASIS_APP_RECORDING_OPER_MAX] = {
  476. [STASIS_APP_RECORDING_STATE_QUEUED][STASIS_APP_RECORDING_CANCEL] = recording_disregard,
  477. [STASIS_APP_RECORDING_STATE_QUEUED][STASIS_APP_RECORDING_STOP] = recording_disregard,
  478. [STASIS_APP_RECORDING_STATE_RECORDING][STASIS_APP_RECORDING_CANCEL] = recording_cancel,
  479. [STASIS_APP_RECORDING_STATE_RECORDING][STASIS_APP_RECORDING_STOP] = recording_stop,
  480. [STASIS_APP_RECORDING_STATE_RECORDING][STASIS_APP_RECORDING_PAUSE] = recording_pause,
  481. [STASIS_APP_RECORDING_STATE_RECORDING][STASIS_APP_RECORDING_UNPAUSE] = recording_noop,
  482. [STASIS_APP_RECORDING_STATE_RECORDING][STASIS_APP_RECORDING_MUTE] = recording_mute,
  483. [STASIS_APP_RECORDING_STATE_RECORDING][STASIS_APP_RECORDING_UNMUTE] = recording_unmute,
  484. [STASIS_APP_RECORDING_STATE_PAUSED][STASIS_APP_RECORDING_CANCEL] = recording_cancel,
  485. [STASIS_APP_RECORDING_STATE_PAUSED][STASIS_APP_RECORDING_STOP] = recording_stop,
  486. [STASIS_APP_RECORDING_STATE_PAUSED][STASIS_APP_RECORDING_PAUSE] = recording_noop,
  487. [STASIS_APP_RECORDING_STATE_PAUSED][STASIS_APP_RECORDING_UNPAUSE] = recording_unpause,
  488. [STASIS_APP_RECORDING_STATE_PAUSED][STASIS_APP_RECORDING_MUTE] = recording_mute,
  489. [STASIS_APP_RECORDING_STATE_PAUSED][STASIS_APP_RECORDING_UNMUTE] = recording_unmute,
  490. };
  491. enum stasis_app_recording_oper_results stasis_app_recording_operation(
  492. struct stasis_app_recording *recording,
  493. enum stasis_app_recording_media_operation operation)
  494. {
  495. recording_operation_cb cb;
  496. SCOPED_AO2LOCK(lock, recording);
  497. if ((unsigned int)recording->state >= STASIS_APP_RECORDING_STATE_MAX) {
  498. ast_log(LOG_WARNING, "Invalid recording state %u\n",
  499. recording->state);
  500. return -1;
  501. }
  502. if ((unsigned int)operation >= STASIS_APP_RECORDING_OPER_MAX) {
  503. ast_log(LOG_WARNING, "Invalid recording operation %u\n",
  504. operation);
  505. return -1;
  506. }
  507. cb = operations[recording->state][operation];
  508. if (!cb) {
  509. if (recording->state != STASIS_APP_RECORDING_STATE_RECORDING) {
  510. /* So we can be specific in our error message. */
  511. return STASIS_APP_RECORDING_OPER_NOT_RECORDING;
  512. } else {
  513. /* And, really, all operations should be valid during
  514. * recording */
  515. ast_log(LOG_ERROR,
  516. "Unhandled operation during recording: %u\n",
  517. operation);
  518. return STASIS_APP_RECORDING_OPER_FAILED;
  519. }
  520. }
  521. return cb(recording) ?
  522. STASIS_APP_RECORDING_OPER_FAILED : STASIS_APP_RECORDING_OPER_OK;
  523. }
  524. static int load_module(void)
  525. {
  526. int r;
  527. r = STASIS_MESSAGE_TYPE_INIT(stasis_app_recording_snapshot_type);
  528. if (r != 0) {
  529. return AST_MODULE_LOAD_DECLINE;
  530. }
  531. recordings = ao2_container_alloc_hash(AO2_ALLOC_OPT_LOCK_MUTEX, 0, RECORDING_BUCKETS,
  532. recording_hash, NULL, recording_cmp);
  533. if (!recordings) {
  534. STASIS_MESSAGE_TYPE_CLEANUP(stasis_app_recording_snapshot_type);
  535. return AST_MODULE_LOAD_DECLINE;
  536. }
  537. return AST_MODULE_LOAD_SUCCESS;
  538. }
  539. static int unload_module(void)
  540. {
  541. ao2_cleanup(recordings);
  542. recordings = NULL;
  543. STASIS_MESSAGE_TYPE_CLEANUP(stasis_app_recording_snapshot_type);
  544. return 0;
  545. }
  546. AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_ORDER, "Stasis application recording support",
  547. .support_level = AST_MODULE_SUPPORT_CORE,
  548. .load = load_module,
  549. .unload = unload_module,
  550. .load_pri = AST_MODPRI_APP_DEPEND,
  551. );