Asterisk - The Open Source Telephony Project GIT-master-a358458
app_record.c
Go to the documentation of this file.
1/*
2 * Asterisk -- An open source telephony toolkit.
3 *
4 * Copyright (C) 1999 - 2005, Digium, Inc.
5 *
6 * Matthew Fredrickson <creslin@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 Trivial application to record a sound file
22 *
23 * \author Matthew Fredrickson <creslin@digium.com>
24 *
25 * \ingroup applications
26 */
27
28/*** MODULEINFO
29 <support_level>core</support_level>
30 ***/
31
32#include "asterisk.h"
33
34#include "asterisk/file.h"
35#include "asterisk/pbx.h"
36#include "asterisk/module.h"
37#include "asterisk/app.h"
38#include "asterisk/channel.h"
39#include "asterisk/dsp.h" /* use dsp routines for silence detection */
41#include "asterisk/paths.h"
42
43/*** DOCUMENTATION
44 <application name="Record" language="en_US">
45 <synopsis>
46 Record to a file.
47 </synopsis>
48 <syntax>
49 <parameter name="filename" required="true" argsep=".">
50 <argument name="filename" required="true" />
51 <argument name="format" required="true">
52 <para>Is the format of the file type to be recorded (wav, gsm, etc).</para>
53 </argument>
54 </parameter>
55 <parameter name="silence">
56 <para>Is the number of seconds of silence to allow before returning.</para>
57 </parameter>
58 <parameter name="maxduration">
59 <para>Is the maximum recording duration in seconds. If missing
60 or 0 there is no maximum.</para>
61 </parameter>
62 <parameter name="options">
63 <optionlist>
64 <option name="a">
65 <para>Append to existing recording rather than replacing.</para>
66 </option>
67 <option name="n">
68 <para>Do not answer, but record anyway if line not yet answered.</para>
69 </option>
70 <option name="o">
71 <para>Exit when 0 is pressed, setting the variable <variable>RECORD_STATUS</variable>
72 to <literal>OPERATOR</literal> instead of <literal>DTMF</literal></para>
73 </option>
74 <option name="q">
75 <para>quiet (do not play a beep tone).</para>
76 </option>
77 <option name="s">
78 <para>skip recording if the line is not yet answered.</para>
79 </option>
80 <option name="t">
81 <para>use alternate '*' terminator key (DTMF) instead of default '#'</para>
82 </option>
83 <option name="u">
84 <para>Don't truncate recorded silence.</para>
85 </option>
86 <option name="x">
87 <para>Ignore all terminator keys (DTMF) and keep recording until hangup.</para>
88 </option>
89 <option name="k">
90 <para>Keep recorded file upon hangup.</para>
91 </option>
92 <option name="y">
93 <para>Terminate recording if *any* DTMF digit is received.</para>
94 </option>
95 </optionlist>
96 </parameter>
97 </syntax>
98 <description>
99 <para>If filename contains <literal>%d</literal>, these characters will be replaced with a number
100 incremented by one each time the file is recorded.
101 Use <astcli>core show file formats</astcli> to see the available formats on your system
102 User can press <literal>#</literal> to terminate the recording and continue to the next priority.
103 If the user hangs up during a recording, all data will be lost and the application will terminate.</para>
104 <variablelist>
105 <variable name="RECORDED_FILE">
106 <para>Will be set to the final filename of the recording, without an extension.</para>
107 </variable>
108 <variable name="RECORD_STATUS">
109 <para>This is the final status of the command</para>
110 <value name="DTMF">A terminating DTMF was received ('#' or '*', depending upon option 't')</value>
111 <value name="SILENCE">The maximum silence occurred in the recording.</value>
112 <value name="SKIP">The line was not yet answered and the 's' option was specified.</value>
113 <value name="TIMEOUT">The maximum length was reached.</value>
114 <value name="HANGUP">The channel was hung up.</value>
115 <value name="ERROR">An unrecoverable error occurred, which resulted in a WARNING to the logs.</value>
116 </variable>
117 </variablelist>
118 </description>
119 </application>
120
121 ***/
122
123#define OPERATOR_KEY '0'
124
125static char *app = "Record";
126
127enum {
128 OPTION_APPEND = (1 << 0),
129 OPTION_NOANSWER = (1 << 1),
130 OPTION_QUIET = (1 << 2),
131 OPTION_SKIP = (1 << 3),
134 OPTION_KEEP = (1 << 6),
138};
139
144};
145
157});
158
159/*!
160 * \internal
161 * \brief Used to determine what action to take when DTMF is received while recording
162 * \since 13.0.0
163 *
164 * \param chan channel being recorded
165 * \param flags option flags in use by the record application
166 * \param dtmf_integer the integer value of the DTMF key received
167 * \param terminator key currently set to be pressed for normal termination
168 *
169 * \returns One of enum dtmf_response
170 */
172 struct ast_flags *flags, int dtmf_integer, int terminator)
173{
174 if ((dtmf_integer == OPERATOR_KEY) &&
176 return RESPONSE_OPERATOR;
177 }
178
179 if ((dtmf_integer == terminator) ||
181 return RESPONSE_DTMF;
182 }
183
184 return RESPONSE_NO_MATCH;
185}
186
187static int create_destination_directory(const char *path)
188{
189 int res;
190 char directory[PATH_MAX], *file_sep;
191
192 if (!(file_sep = strrchr(path, '/'))) {
193 /* No directory to create */
194 return 0;
195 }
196
197 /* Overwrite temporarily */
198 *file_sep = '\0';
199
200 /* Absolute path? */
201 if (path[0] == '/') {
202 res = ast_mkdir(path, 0777);
203 *file_sep = '/';
204 return res;
205 }
206
207 /* Relative path */
208 res = snprintf(directory, sizeof(directory), "%s/sounds/%s",
210
211 *file_sep = '/';
212
213 if (res >= sizeof(directory)) {
214 /* We truncated, so we fail */
215 return -1;
216 }
217
218 return ast_mkdir(directory, 0777);
219}
220
221static int record_exec(struct ast_channel *chan, const char *data)
222{
223 int res = 0;
224 char *ext = NULL, *opts[0];
225 char *parse;
226 int i = 0;
227 char tmp[PATH_MAX];
228
229 struct ast_filestream *s = NULL;
230 struct ast_frame *f = NULL;
231
232 struct ast_dsp *sildet = NULL; /* silence detector dsp */
233 int totalsilence = 0;
234 int dspsilence = 0;
235 int silence = 0; /* amount of silence to allow */
236 int gotsilence = 0; /* did we timeout for silence? */
237 int truncate_silence = 1; /* truncate on complete silence recording */
238 int maxduration = 0; /* max duration of recording in milliseconds */
239 int gottimeout = 0; /* did we timeout for maxduration exceeded? */
240 int terminator = '#';
241 RAII_VAR(struct ast_format *, rfmt, NULL, ao2_cleanup);
242 int ioflags;
243 struct ast_silence_generator *silgen = NULL;
244 struct ast_flags flags = { 0, };
246 AST_APP_ARG(filename);
247 AST_APP_ARG(silence);
248 AST_APP_ARG(maxduration);
250 );
251 int ms;
252 struct timeval start;
253 const char *status_response = "ERROR";
254
255 /* The next few lines of code parse out the filename and header from the input string */
256 if (ast_strlen_zero(data)) { /* no data implies no filename or anything is present */
257 ast_log(LOG_WARNING, "Record requires an argument (filename)\n");
258 pbx_builtin_setvar_helper(chan, "RECORD_STATUS", "ERROR");
259 return -1;
260 }
261
262 parse = ast_strdupa(data);
264 if (args.argc == 4)
265 ast_app_parse_options(app_opts, &flags, opts, args.options);
266
267 if (!ast_strlen_zero(args.filename)) {
268 ext = strrchr(args.filename, '.'); /* to support filename with a . in the filename, not format */
269 if (!ext)
270 ext = strchr(args.filename, ':');
271 if (ext) {
272 *ext = '\0';
273 ext++;
274 }
275 }
276 if (!ext) {
277 ast_log(LOG_WARNING, "No extension specified to filename!\n");
278 pbx_builtin_setvar_helper(chan, "RECORD_STATUS", "ERROR");
279 return -1;
280 }
281 if (args.silence) {
282 if ((sscanf(args.silence, "%30d", &i) == 1) && (i > -1)) {
283 silence = i * 1000;
284 } else if (!ast_strlen_zero(args.silence)) {
285 ast_log(LOG_WARNING, "'%s' is not a valid silence duration\n", args.silence);
286 }
287 }
288
290 truncate_silence = 0;
291
292 if (args.maxduration) {
293 if ((sscanf(args.maxduration, "%30d", &i) == 1) && (i > -1))
294 /* Convert duration to milliseconds */
295 maxduration = i * 1000;
296 else if (!ast_strlen_zero(args.maxduration))
297 ast_log(LOG_WARNING, "'%s' is not a valid maximum duration\n", args.maxduration);
298 }
299
301 terminator = '*';
303 terminator = '\0';
304
305 /*
306 If a '%d' is specified as part of the filename, we replace that token with
307 sequentially incrementing numbers until we find a unique filename.
308 */
309 if (strchr(args.filename, '%')) {
310 size_t src, dst, count = 0;
311 size_t src_len = strlen(args.filename);
312 size_t dst_len = sizeof(tmp) - 1;
313
314 do {
315 for (src = 0, dst = 0; src < src_len && dst < dst_len; src++) {
316 if (!strncmp(&args.filename[src], "%d", 2)) {
317 int s = snprintf(&tmp[dst], PATH_MAX - dst, "%zu", count);
318 if (s >= PATH_MAX - dst) {
319 /* We truncated, so we need to bail */
320 ast_log(LOG_WARNING, "Failed to create unique filename from template: %s\n", args.filename);
321 pbx_builtin_setvar_helper(chan, "RECORD_STATUS", "ERROR");
322 return -1;
323 }
324 dst += s;
325 src++;
326 } else {
327 tmp[dst] = args.filename[src];
328 tmp[++dst] = '\0';
329 }
330 }
331 count++;
332 } while (ast_fileexists(tmp, ext, ast_channel_language(chan)) > 0);
333 } else
334 ast_copy_string(tmp, args.filename, sizeof(tmp));
335
336 pbx_builtin_setvar_helper(chan, "RECORDED_FILE", tmp);
337
338 if (ast_channel_state(chan) != AST_STATE_UP) {
339 if (ast_test_flag(&flags, OPTION_SKIP)) {
340 /* At the user's option, skip if the line is not up */
341 pbx_builtin_setvar_helper(chan, "RECORD_STATUS", "SKIP");
342 return 0;
343 } else if (!ast_test_flag(&flags, OPTION_NOANSWER)) {
344 /* Otherwise answer unless we're supposed to record while on-hook */
345 res = ast_answer(chan);
346 }
347 }
348
349 if (res) {
350 ast_log(LOG_WARNING, "Could not answer channel '%s'\n", ast_channel_name(chan));
351 status_response = "ERROR";
352 goto out;
353 }
354
355 if (!ast_test_flag(&flags, OPTION_QUIET)) {
356 /* Some code to play a nice little beep to signify the start of the record operation */
357 res = ast_streamfile(chan, "beep", ast_channel_language(chan));
358 if (!res) {
359 res = ast_waitstream(chan, "");
360 } else {
361 ast_log(LOG_WARNING, "ast_streamfile(beep) failed on %s\n", ast_channel_name(chan));
362 res = 0;
363 }
364 ast_stopstream(chan);
365 }
366
367 /* The end of beep code. Now the recording starts */
368
369 if (silence > 0) {
370 rfmt = ao2_bump(ast_channel_readformat(chan));
372 if (res < 0) {
373 ast_log(LOG_WARNING, "Unable to set to linear mode, giving up\n");
374 pbx_builtin_setvar_helper(chan, "RECORD_STATUS", "ERROR");
375 return -1;
376 }
377 sildet = ast_dsp_new();
378 if (!sildet) {
379 ast_log(LOG_WARNING, "Unable to create silence detector :(\n");
380 pbx_builtin_setvar_helper(chan, "RECORD_STATUS", "ERROR");
381 return -1;
382 }
384 }
385
387 ast_log(LOG_WARNING, "Could not create directory for file %s\n", args.filename);
388 status_response = "ERROR";
389 goto out;
390 }
391
392 ioflags = ast_test_flag(&flags, OPTION_APPEND) ? O_CREAT|O_APPEND|O_WRONLY : O_CREAT|O_TRUNC|O_WRONLY;
393 s = ast_writefile(tmp, ext, NULL, ioflags, 0, AST_FILE_MODE);
394
395 if (!s) {
396 ast_log(LOG_WARNING, "Could not create file %s\n", args.filename);
397 status_response = "ERROR";
398 goto out;
399 }
400
403
404 /* Request a video update */
406
407 if (maxduration <= 0)
408 maxduration = -1;
409
410 start = ast_tvnow();
411 while ((ms = ast_remaining_ms(start, maxduration))) {
412 ms = ast_waitfor(chan, ms);
413 if (ms < 0) {
414 break;
415 }
416
417 if (maxduration > 0 && ms == 0) {
418 break;
419 }
420
421 f = ast_read(chan);
422 if (!f) {
423 res = -1;
424 break;
425 }
426 if (f->frametype == AST_FRAME_VOICE) {
427 res = ast_writestream(s, f);
428
429 if (res) {
430 ast_log(LOG_WARNING, "Problem writing frame\n");
431 ast_frfree(f);
432 status_response = "ERROR";
433 break;
434 }
435
436 if (silence > 0) {
437 dspsilence = 0;
438 ast_dsp_silence(sildet, f, &dspsilence);
439 if (dspsilence) {
440 totalsilence = dspsilence;
441 } else {
442 totalsilence = 0;
443 }
444 if (totalsilence > silence) {
445 /* Ended happily with silence */
446 ast_frfree(f);
447 gotsilence = 1;
448 status_response = "SILENCE";
449 break;
450 }
451 }
452 } else if (f->frametype == AST_FRAME_VIDEO) {
453 res = ast_writestream(s, f);
454
455 if (res) {
456 ast_log(LOG_WARNING, "Problem writing frame\n");
457 status_response = "ERROR";
458 ast_frfree(f);
459 break;
460 }
461 } else if (f->frametype == AST_FRAME_DTMF) {
462 enum dtmf_response rc =
463 record_dtmf_response(chan, &flags, f->subclass.integer, terminator);
464 switch(rc) {
466 break;
468 status_response = "OPERATOR";
469 ast_debug(1, "Got OPERATOR\n");
470 break;
471 case RESPONSE_DTMF:
472 status_response = "DTMF";
473 ast_debug(1, "Got DTMF\n");
474 break;
475 }
476 if (rc != RESPONSE_NO_MATCH) {
477 ast_frfree(f);
478 break;
479 }
480 }
481 ast_frfree(f);
482 }
483
484 if (maxduration > 0 && !ms) {
485 gottimeout = 1;
486 status_response = "TIMEOUT";
487 }
488
489 if (!f) {
490 ast_debug(1, "Got hangup\n");
491 res = -1;
492 status_response = "HANGUP";
493 if (!ast_test_flag(&flags, OPTION_KEEP)) {
494 ast_filedelete(args.filename, NULL);
495 }
496 }
497
498 if (gotsilence && truncate_silence) {
499 ast_stream_rewind(s, silence - 1000);
501 } else if (!gottimeout && f) {
502 /*
503 * Strip off the last 1/4 second of it, if we didn't end because of a timeout,
504 * or a hangup. This must mean we ended because of a DTMF tone and while this
505 * 1/4 second stripping is very old code the most likely explanation is that it
506 * relates to stripping a partial DTMF tone.
507 */
508 ast_stream_rewind(s, 250);
510 }
512
513 if (silgen)
515
516out:
517 if ((silence > 0) && rfmt) {
518 res = ast_set_read_format(chan, rfmt);
519 if (res) {
520 ast_log(LOG_WARNING, "Unable to restore read format on '%s'\n", ast_channel_name(chan));
521 }
522 }
523
524 if (sildet) {
525 ast_dsp_free(sildet);
526 }
527
528 pbx_builtin_setvar_helper(chan, "RECORD_STATUS", status_response);
529
530 return res;
531}
532
533static int unload_module(void)
534{
536}
537
538static int load_module(void)
539{
541}
542
543AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "Trivial Record Application");
static int record_exec(struct ast_channel *chan, const char *data)
Definition: app_record.c:221
#define OPERATOR_KEY
Definition: app_record.c:123
static enum dtmf_response record_dtmf_response(struct ast_channel *chan, struct ast_flags *flags, int dtmf_integer, int terminator)
Definition: app_record.c:171
static int create_destination_directory(const char *path)
Definition: app_record.c:187
static const struct ast_app_option app_opts[128]
Definition: app_record.c:157
static char * app
Definition: app_record.c:125
static int load_module(void)
Definition: app_record.c:538
static int unload_module(void)
Definition: app_record.c:533
@ OPTION_NOANSWER
Definition: app_record.c:129
@ OPTION_STAR_TERMINATE
Definition: app_record.c:132
@ OPTION_KEEP
Definition: app_record.c:134
@ OPTION_NO_TRUNCATE
Definition: app_record.c:137
@ OPTION_QUIET
Definition: app_record.c:130
@ OPTION_OPERATOR_EXIT
Definition: app_record.c:136
@ OPTION_IGNORE_TERMINATE
Definition: app_record.c:133
@ OPTION_ANY_TERMINATE
Definition: app_record.c:135
@ OPTION_SKIP
Definition: app_record.c:131
@ OPTION_APPEND
Definition: app_record.c:128
dtmf_response
Definition: app_record.c:140
@ RESPONSE_OPERATOR
Definition: app_record.c:142
@ RESPONSE_DTMF
Definition: app_record.c:143
@ RESPONSE_NO_MATCH
Definition: app_record.c:141
Asterisk main include file. File version handling, generic pbx functions.
#define AST_FILE_MODE
Definition: asterisk.h:32
#define PATH_MAX
Definition: asterisk.h:40
#define ast_strdupa(s)
duplicate a string in memory from the stack
Definition: astmm.h:298
#define ast_log
Definition: astobj2.c:42
#define ao2_cleanup(obj)
Definition: astobj2.h:1934
#define ao2_bump(obj)
Bump refcount on an AO2 object by one, returning the object.
Definition: astobj2.h:480
static int tmp()
Definition: bt_open.c:389
General Asterisk PBX channel definitions.
const char * ast_channel_name(const struct ast_channel *chan)
struct ast_silence_generator * ast_channel_start_silence_generator(struct ast_channel *chan)
Starts a silence generator on the given channel.
Definition: channel.c:8164
int ast_waitfor(struct ast_channel *chan, int ms)
Wait for input on a channel.
Definition: channel.c:3162
void ast_channel_stop_silence_generator(struct ast_channel *chan, struct ast_silence_generator *state)
Stops a previously-started silence generator on the given channel.
Definition: channel.c:8210
struct ast_frame * ast_read(struct ast_channel *chan)
Reads a frame.
Definition: channel.c:4257
int ast_set_read_format(struct ast_channel *chan, struct ast_format *format)
Sets read format on channel chan.
Definition: channel.c:5762
const char * ast_channel_language(const struct ast_channel *chan)
int ast_answer(struct ast_channel *chan)
Answer a channel.
Definition: channel.c:2805
int ast_indicate(struct ast_channel *chan, int condition)
Indicates condition of channel.
Definition: channel.c:4277
struct ast_format * ast_channel_readformat(struct ast_channel *chan)
ast_channel_state
ast_channel states
Definition: channelstate.h:35
@ AST_STATE_UP
Definition: channelstate.h:42
Convenient Signal Processing routines.
void ast_dsp_set_threshold(struct ast_dsp *dsp, int threshold)
Set the minimum average magnitude threshold to determine talking by the DSP.
Definition: dsp.c:1788
void ast_dsp_free(struct ast_dsp *dsp)
Definition: dsp.c:1783
@ THRESHOLD_SILENCE
Definition: dsp.h:73
int ast_dsp_silence(struct ast_dsp *dsp, struct ast_frame *f, int *totalsilence)
Process the audio frame for silence.
Definition: dsp.c:1488
int ast_dsp_get_threshold_from_settings(enum threshold which)
Get silence threshold from dsp.conf.
Definition: dsp.c:2009
struct ast_dsp * ast_dsp_new(void)
Allocates a new dsp, assumes 8khz for internal sample rate.
Definition: dsp.c:1758
Generic File Format Support. Should be included by clients of the file handling routines....
int ast_stopstream(struct ast_channel *c)
Stops a stream.
Definition: file.c:222
int ast_writestream(struct ast_filestream *fs, struct ast_frame *f)
Writes a frame to a stream.
Definition: file.c:244
int ast_stream_rewind(struct ast_filestream *fs, off_t ms)
Rewind stream ms.
Definition: file.c:1100
int ast_streamfile(struct ast_channel *c, const char *filename, const char *preflang)
Streams a file.
Definition: file.c:1293
struct ast_filestream * ast_writefile(const char *filename, const char *type, const char *comment, int flags, int check, mode_t mode)
Starts writing a file.
Definition: file.c:1423
int ast_truncstream(struct ast_filestream *fs)
Trunc stream at current location.
Definition: file.c:1080
int ast_closestream(struct ast_filestream *f)
Closes a stream.
Definition: file.c:1111
int ast_fileexists(const char *filename, const char *fmt, const char *preflang)
Checks for the existence of a given file.
Definition: file.c:1129
int ast_filedelete(const char *filename, const char *fmt)
Deletes a file.
Definition: file.c:1141
int ast_waitstream(struct ast_channel *c, const char *breakon)
Waits for a stream to stop or digit to be pressed.
Definition: file.c:1840
Media Format Cache API.
struct ast_format * ast_format_slin
Built-in cached signed linear 8kHz format.
Definition: format_cache.c:41
const char * ext
Definition: http.c:150
Application convenience functions, designed to give consistent look and feel to Asterisk apps.
#define AST_APP_ARG(name)
Define an application argument.
#define AST_APP_OPTIONS(holder, options...)
Declares an array of options for an application.
#define AST_DECLARE_APP_ARGS(name, arglist)
Declare a structure to hold an application's arguments.
#define AST_STANDARD_APP_ARGS(args, parse)
Performs the 'standard' argument separation process for an application.
#define AST_APP_OPTION(option, flagno)
Declares an application option that does not accept an argument.
int ast_app_parse_options(const struct ast_app_option *options, struct ast_flags *flags, char **args, char *optstr)
Parses a string containing application options and sets flags/arguments.
Definition: main/app.c:3056
#define AST_FRAME_DTMF
#define ast_frfree(fr)
@ AST_FRAME_VIDEO
@ AST_FRAME_VOICE
@ AST_CONTROL_VIDUPDATE
#define ast_debug(level,...)
Log a DEBUG message.
#define LOG_WARNING
Asterisk module definitions.
#define AST_MODULE_INFO_STANDARD(keystr, desc)
Definition: module.h:567
#define ASTERISK_GPL_KEY
The text the key() function should return.
Definition: module.h:46
int ast_unregister_application(const char *app)
Unregister an application.
Definition: pbx_app.c:392
#define ast_register_application_xml(app, execute)
Register an application using XML documentation.
Definition: module.h:626
#define ast_opt_transmit_silence
Definition: options.h:124
Asterisk file paths, configured in asterisk.conf.
const char * ast_config_AST_DATA_DIR
Definition: options.c:158
Core PBX routines and definitions.
int pbx_builtin_setvar_helper(struct ast_channel *chan, const char *name, const char *value)
Add a variable to the channel variable stack, removing the most recently set value for the same name.
#define NULL
Definition: resample.c:96
static force_inline int attribute_pure ast_strlen_zero(const char *s)
Definition: strings.h:65
void ast_copy_string(char *dst, const char *src, size_t size)
Size-limited null-terminating string copy.
Definition: strings.h:425
Main Channel structure associated with a channel.
Definition: dsp.c:407
int totalsilence
Definition: dsp.c:411
This structure is allocated by file.c in one chunk, together with buf_size and desc_size bytes of mem...
Definition: mod_format.h:101
Structure used to handle boolean flags.
Definition: utils.h:199
unsigned int flags
Definition: utils.h:200
Definition of a media format.
Definition: format.c:43
Data structure associated with a single frame of data.
struct ast_frame_subclass subclass
enum ast_frame_type frametype
const char * args
static struct test_options options
int ast_remaining_ms(struct timeval start, int max_ms)
Calculate remaining milliseconds given a starting timestamp and upper bound.
Definition: utils.c:2281
struct timeval ast_tvnow(void)
Returns current timeval. Meant to replace calls to gettimeofday().
Definition: time.h:159
FILE * out
Definition: utils/frame.c:33
#define ast_test_flag(p, flag)
Definition: utils.h:63
#define RAII_VAR(vartype, varname, initval, dtor)
Declare a variable that will call a destructor function when it goes out of scope.
Definition: utils.h:941
int ast_mkdir(const char *path, int mode)
Recursively create directory path.
Definition: utils.c:2479