Commit | Line | Data |
---|---|---|
2ba45a60 DM |
1 | /* |
2 | * Copyright (c) 2012 Stefano Sabatini | |
3 | * | |
4 | * This file is part of FFmpeg. | |
5 | * | |
6 | * FFmpeg is free software; you can redistribute it and/or | |
7 | * modify it under the terms of the GNU Lesser General Public | |
8 | * License as published by the Free Software Foundation; either | |
9 | * version 2.1 of the License, or (at your option) any later version. | |
10 | * | |
11 | * FFmpeg is distributed in the hope that it will be useful, | |
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
14 | * Lesser General Public License for more details. | |
15 | * | |
16 | * You should have received a copy of the GNU Lesser General Public | |
17 | * License along with FFmpeg; if not, write to the Free Software | |
18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | |
19 | */ | |
20 | ||
21 | /** | |
22 | * @file | |
23 | * send commands filter | |
24 | */ | |
25 | ||
26 | #include "libavutil/avstring.h" | |
27 | #include "libavutil/bprint.h" | |
28 | #include "libavutil/file.h" | |
29 | #include "libavutil/opt.h" | |
30 | #include "libavutil/parseutils.h" | |
31 | #include "avfilter.h" | |
32 | #include "internal.h" | |
33 | #include "avfiltergraph.h" | |
34 | #include "audio.h" | |
35 | #include "video.h" | |
36 | ||
37 | #define COMMAND_FLAG_ENTER 1 | |
38 | #define COMMAND_FLAG_LEAVE 2 | |
39 | ||
40 | static inline char *make_command_flags_str(AVBPrint *pbuf, int flags) | |
41 | { | |
42 | static const char * const flag_strings[] = { "enter", "leave" }; | |
43 | int i, is_first = 1; | |
44 | ||
45 | av_bprint_init(pbuf, 0, AV_BPRINT_SIZE_AUTOMATIC); | |
46 | for (i = 0; i < FF_ARRAY_ELEMS(flag_strings); i++) { | |
47 | if (flags & 1<<i) { | |
48 | if (!is_first) | |
49 | av_bprint_chars(pbuf, '+', 1); | |
50 | av_bprintf(pbuf, "%s", flag_strings[i]); | |
51 | is_first = 0; | |
52 | } | |
53 | } | |
54 | ||
55 | return pbuf->str; | |
56 | } | |
57 | ||
58 | typedef struct { | |
59 | int flags; | |
60 | char *target, *command, *arg; | |
61 | int index; | |
62 | } Command; | |
63 | ||
64 | typedef struct { | |
65 | int64_t start_ts; ///< start timestamp expressed as microseconds units | |
66 | int64_t end_ts; ///< end timestamp expressed as microseconds units | |
67 | int index; ///< unique index for these interval commands | |
68 | Command *commands; | |
69 | int nb_commands; | |
70 | int enabled; ///< current time detected inside this interval | |
71 | } Interval; | |
72 | ||
73 | typedef struct { | |
74 | const AVClass *class; | |
75 | Interval *intervals; | |
76 | int nb_intervals; | |
77 | ||
78 | char *commands_filename; | |
79 | char *commands_str; | |
80 | } SendCmdContext; | |
81 | ||
82 | #define OFFSET(x) offsetof(SendCmdContext, x) | |
83 | #define FLAGS AV_OPT_FLAG_FILTERING_PARAM | AV_OPT_FLAG_AUDIO_PARAM | AV_OPT_FLAG_VIDEO_PARAM | |
84 | static const AVOption options[] = { | |
85 | { "commands", "set commands", OFFSET(commands_str), AV_OPT_TYPE_STRING, {.str = NULL}, 0, 0, FLAGS }, | |
86 | { "c", "set commands", OFFSET(commands_str), AV_OPT_TYPE_STRING, {.str = NULL}, 0, 0, FLAGS }, | |
87 | { "filename", "set commands file", OFFSET(commands_filename), AV_OPT_TYPE_STRING, {.str = NULL}, 0, 0, FLAGS }, | |
88 | { "f", "set commands file", OFFSET(commands_filename), AV_OPT_TYPE_STRING, {.str = NULL}, 0, 0, FLAGS }, | |
89 | { NULL } | |
90 | }; | |
91 | ||
92 | #define SPACES " \f\t\n\r" | |
93 | ||
94 | static void skip_comments(const char **buf) | |
95 | { | |
96 | while (**buf) { | |
97 | /* skip leading spaces */ | |
98 | *buf += strspn(*buf, SPACES); | |
99 | if (**buf != '#') | |
100 | break; | |
101 | ||
102 | (*buf)++; | |
103 | ||
104 | /* skip comment until the end of line */ | |
105 | *buf += strcspn(*buf, "\n"); | |
106 | if (**buf) | |
107 | (*buf)++; | |
108 | } | |
109 | } | |
110 | ||
111 | #define COMMAND_DELIMS " \f\t\n\r,;" | |
112 | ||
113 | static int parse_command(Command *cmd, int cmd_count, int interval_count, | |
114 | const char **buf, void *log_ctx) | |
115 | { | |
116 | int ret; | |
117 | ||
118 | memset(cmd, 0, sizeof(Command)); | |
119 | cmd->index = cmd_count; | |
120 | ||
121 | /* format: [FLAGS] target command arg */ | |
122 | *buf += strspn(*buf, SPACES); | |
123 | ||
124 | /* parse flags */ | |
125 | if (**buf == '[') { | |
126 | (*buf)++; /* skip "[" */ | |
127 | ||
128 | while (**buf) { | |
129 | int len = strcspn(*buf, "|+]"); | |
130 | ||
131 | if (!strncmp(*buf, "enter", strlen("enter"))) cmd->flags |= COMMAND_FLAG_ENTER; | |
132 | else if (!strncmp(*buf, "leave", strlen("leave"))) cmd->flags |= COMMAND_FLAG_LEAVE; | |
133 | else { | |
134 | char flag_buf[64]; | |
135 | av_strlcpy(flag_buf, *buf, sizeof(flag_buf)); | |
136 | av_log(log_ctx, AV_LOG_ERROR, | |
137 | "Unknown flag '%s' in interval #%d, command #%d\n", | |
138 | flag_buf, interval_count, cmd_count); | |
139 | return AVERROR(EINVAL); | |
140 | } | |
141 | *buf += len; | |
142 | if (**buf == ']') | |
143 | break; | |
144 | if (!strspn(*buf, "+|")) { | |
145 | av_log(log_ctx, AV_LOG_ERROR, | |
146 | "Invalid flags char '%c' in interval #%d, command #%d\n", | |
147 | **buf, interval_count, cmd_count); | |
148 | return AVERROR(EINVAL); | |
149 | } | |
150 | if (**buf) | |
151 | (*buf)++; | |
152 | } | |
153 | ||
154 | if (**buf != ']') { | |
155 | av_log(log_ctx, AV_LOG_ERROR, | |
156 | "Missing flag terminator or extraneous data found at the end of flags " | |
157 | "in interval #%d, command #%d\n", interval_count, cmd_count); | |
158 | return AVERROR(EINVAL); | |
159 | } | |
160 | (*buf)++; /* skip "]" */ | |
161 | } else { | |
162 | cmd->flags = COMMAND_FLAG_ENTER; | |
163 | } | |
164 | ||
165 | *buf += strspn(*buf, SPACES); | |
166 | cmd->target = av_get_token(buf, COMMAND_DELIMS); | |
167 | if (!cmd->target || !cmd->target[0]) { | |
168 | av_log(log_ctx, AV_LOG_ERROR, | |
169 | "No target specified in interval #%d, command #%d\n", | |
170 | interval_count, cmd_count); | |
171 | ret = AVERROR(EINVAL); | |
172 | goto fail; | |
173 | } | |
174 | ||
175 | *buf += strspn(*buf, SPACES); | |
176 | cmd->command = av_get_token(buf, COMMAND_DELIMS); | |
177 | if (!cmd->command || !cmd->command[0]) { | |
178 | av_log(log_ctx, AV_LOG_ERROR, | |
179 | "No command specified in interval #%d, command #%d\n", | |
180 | interval_count, cmd_count); | |
181 | ret = AVERROR(EINVAL); | |
182 | goto fail; | |
183 | } | |
184 | ||
185 | *buf += strspn(*buf, SPACES); | |
186 | cmd->arg = av_get_token(buf, COMMAND_DELIMS); | |
187 | ||
188 | return 1; | |
189 | ||
190 | fail: | |
191 | av_freep(&cmd->target); | |
192 | av_freep(&cmd->command); | |
193 | av_freep(&cmd->arg); | |
194 | return ret; | |
195 | } | |
196 | ||
197 | static int parse_commands(Command **cmds, int *nb_cmds, int interval_count, | |
198 | const char **buf, void *log_ctx) | |
199 | { | |
200 | int cmd_count = 0; | |
201 | int ret, n = 0; | |
202 | AVBPrint pbuf; | |
203 | ||
204 | *cmds = NULL; | |
205 | *nb_cmds = 0; | |
206 | ||
207 | while (**buf) { | |
208 | Command cmd; | |
209 | ||
210 | if ((ret = parse_command(&cmd, cmd_count, interval_count, buf, log_ctx)) < 0) | |
211 | return ret; | |
212 | cmd_count++; | |
213 | ||
214 | /* (re)allocate commands array if required */ | |
215 | if (*nb_cmds == n) { | |
216 | n = FFMAX(16, 2*n); /* first allocation = 16, or double the number */ | |
217 | *cmds = av_realloc_f(*cmds, n, 2*sizeof(Command)); | |
218 | if (!*cmds) { | |
219 | av_log(log_ctx, AV_LOG_ERROR, | |
220 | "Could not (re)allocate command array\n"); | |
221 | return AVERROR(ENOMEM); | |
222 | } | |
223 | } | |
224 | ||
225 | (*cmds)[(*nb_cmds)++] = cmd; | |
226 | ||
227 | *buf += strspn(*buf, SPACES); | |
228 | if (**buf && **buf != ';' && **buf != ',') { | |
229 | av_log(log_ctx, AV_LOG_ERROR, | |
230 | "Missing separator or extraneous data found at the end of " | |
231 | "interval #%d, in command #%d\n", | |
232 | interval_count, cmd_count); | |
233 | av_log(log_ctx, AV_LOG_ERROR, | |
234 | "Command was parsed as: flags:[%s] target:%s command:%s arg:%s\n", | |
235 | make_command_flags_str(&pbuf, cmd.flags), cmd.target, cmd.command, cmd.arg); | |
236 | return AVERROR(EINVAL); | |
237 | } | |
238 | if (**buf == ';') | |
239 | break; | |
240 | if (**buf == ',') | |
241 | (*buf)++; | |
242 | } | |
243 | ||
244 | return 0; | |
245 | } | |
246 | ||
247 | #define DELIMS " \f\t\n\r,;" | |
248 | ||
249 | static int parse_interval(Interval *interval, int interval_count, | |
250 | const char **buf, void *log_ctx) | |
251 | { | |
252 | char *intervalstr; | |
253 | int ret; | |
254 | ||
255 | *buf += strspn(*buf, SPACES); | |
256 | if (!**buf) | |
257 | return 0; | |
258 | ||
259 | /* reset data */ | |
260 | memset(interval, 0, sizeof(Interval)); | |
261 | interval->index = interval_count; | |
262 | ||
263 | /* format: INTERVAL COMMANDS */ | |
264 | ||
265 | /* parse interval */ | |
266 | intervalstr = av_get_token(buf, DELIMS); | |
267 | if (intervalstr && intervalstr[0]) { | |
268 | char *start, *end; | |
269 | ||
270 | start = av_strtok(intervalstr, "-", &end); | |
271 | if ((ret = av_parse_time(&interval->start_ts, start, 1)) < 0) { | |
272 | av_log(log_ctx, AV_LOG_ERROR, | |
273 | "Invalid start time specification '%s' in interval #%d\n", | |
274 | start, interval_count); | |
275 | goto end; | |
276 | } | |
277 | ||
278 | if (end) { | |
279 | if ((ret = av_parse_time(&interval->end_ts, end, 1)) < 0) { | |
280 | av_log(log_ctx, AV_LOG_ERROR, | |
281 | "Invalid end time specification '%s' in interval #%d\n", | |
282 | end, interval_count); | |
283 | goto end; | |
284 | } | |
285 | } else { | |
286 | interval->end_ts = INT64_MAX; | |
287 | } | |
288 | if (interval->end_ts < interval->start_ts) { | |
289 | av_log(log_ctx, AV_LOG_ERROR, | |
290 | "Invalid end time '%s' in interval #%d: " | |
291 | "cannot be lesser than start time '%s'\n", | |
292 | end, interval_count, start); | |
293 | ret = AVERROR(EINVAL); | |
294 | goto end; | |
295 | } | |
296 | } else { | |
297 | av_log(log_ctx, AV_LOG_ERROR, | |
298 | "No interval specified for interval #%d\n", interval_count); | |
299 | ret = AVERROR(EINVAL); | |
300 | goto end; | |
301 | } | |
302 | ||
303 | /* parse commands */ | |
304 | ret = parse_commands(&interval->commands, &interval->nb_commands, | |
305 | interval_count, buf, log_ctx); | |
306 | ||
307 | end: | |
308 | av_free(intervalstr); | |
309 | return ret; | |
310 | } | |
311 | ||
312 | static int parse_intervals(Interval **intervals, int *nb_intervals, | |
313 | const char *buf, void *log_ctx) | |
314 | { | |
315 | int interval_count = 0; | |
316 | int ret, n = 0; | |
317 | ||
318 | *intervals = NULL; | |
319 | *nb_intervals = 0; | |
320 | ||
321 | while (1) { | |
322 | Interval interval; | |
323 | ||
324 | skip_comments(&buf); | |
325 | if (!(*buf)) | |
326 | break; | |
327 | ||
328 | if ((ret = parse_interval(&interval, interval_count, &buf, log_ctx)) < 0) | |
329 | return ret; | |
330 | ||
331 | buf += strspn(buf, SPACES); | |
332 | if (*buf) { | |
333 | if (*buf != ';') { | |
334 | av_log(log_ctx, AV_LOG_ERROR, | |
335 | "Missing terminator or extraneous data found at the end of interval #%d\n", | |
336 | interval_count); | |
337 | return AVERROR(EINVAL); | |
338 | } | |
339 | buf++; /* skip ';' */ | |
340 | } | |
341 | interval_count++; | |
342 | ||
343 | /* (re)allocate commands array if required */ | |
344 | if (*nb_intervals == n) { | |
345 | n = FFMAX(16, 2*n); /* first allocation = 16, or double the number */ | |
346 | *intervals = av_realloc_f(*intervals, n, 2*sizeof(Interval)); | |
347 | if (!*intervals) { | |
348 | av_log(log_ctx, AV_LOG_ERROR, | |
349 | "Could not (re)allocate intervals array\n"); | |
350 | return AVERROR(ENOMEM); | |
351 | } | |
352 | } | |
353 | ||
354 | (*intervals)[(*nb_intervals)++] = interval; | |
355 | } | |
356 | ||
357 | return 0; | |
358 | } | |
359 | ||
360 | static int cmp_intervals(const void *a, const void *b) | |
361 | { | |
362 | const Interval *i1 = a; | |
363 | const Interval *i2 = b; | |
364 | int64_t ts_diff = i1->start_ts - i2->start_ts; | |
365 | int ret; | |
366 | ||
367 | ret = ts_diff > 0 ? 1 : ts_diff < 0 ? -1 : 0; | |
368 | return ret == 0 ? i1->index - i2->index : ret; | |
369 | } | |
370 | ||
371 | static av_cold int init(AVFilterContext *ctx) | |
372 | { | |
373 | SendCmdContext *sendcmd = ctx->priv; | |
374 | int ret, i, j; | |
375 | ||
376 | if (sendcmd->commands_filename && sendcmd->commands_str) { | |
377 | av_log(ctx, AV_LOG_ERROR, | |
378 | "Only one of the filename or commands options must be specified\n"); | |
379 | return AVERROR(EINVAL); | |
380 | } | |
381 | ||
382 | if (sendcmd->commands_filename) { | |
383 | uint8_t *file_buf, *buf; | |
384 | size_t file_bufsize; | |
385 | ret = av_file_map(sendcmd->commands_filename, | |
386 | &file_buf, &file_bufsize, 0, ctx); | |
387 | if (ret < 0) | |
388 | return ret; | |
389 | ||
390 | /* create a 0-terminated string based on the read file */ | |
391 | buf = av_malloc(file_bufsize + 1); | |
392 | if (!buf) { | |
393 | av_file_unmap(file_buf, file_bufsize); | |
394 | return AVERROR(ENOMEM); | |
395 | } | |
396 | memcpy(buf, file_buf, file_bufsize); | |
397 | buf[file_bufsize] = 0; | |
398 | av_file_unmap(file_buf, file_bufsize); | |
399 | sendcmd->commands_str = buf; | |
400 | } | |
401 | ||
402 | if ((ret = parse_intervals(&sendcmd->intervals, &sendcmd->nb_intervals, | |
403 | sendcmd->commands_str, ctx)) < 0) | |
404 | return ret; | |
405 | ||
406 | qsort(sendcmd->intervals, sendcmd->nb_intervals, sizeof(Interval), cmp_intervals); | |
407 | ||
408 | av_log(ctx, AV_LOG_DEBUG, "Parsed commands:\n"); | |
409 | for (i = 0; i < sendcmd->nb_intervals; i++) { | |
410 | AVBPrint pbuf; | |
411 | Interval *interval = &sendcmd->intervals[i]; | |
412 | av_log(ctx, AV_LOG_VERBOSE, "start_time:%f end_time:%f index:%d\n", | |
413 | (double)interval->start_ts/1000000, (double)interval->end_ts/1000000, interval->index); | |
414 | for (j = 0; j < interval->nb_commands; j++) { | |
415 | Command *cmd = &interval->commands[j]; | |
416 | av_log(ctx, AV_LOG_VERBOSE, | |
417 | " [%s] target:%s command:%s arg:%s index:%d\n", | |
418 | make_command_flags_str(&pbuf, cmd->flags), cmd->target, cmd->command, cmd->arg, cmd->index); | |
419 | } | |
420 | } | |
421 | ||
422 | return 0; | |
423 | } | |
424 | ||
425 | static av_cold void uninit(AVFilterContext *ctx) | |
426 | { | |
427 | SendCmdContext *sendcmd = ctx->priv; | |
428 | int i, j; | |
429 | ||
430 | for (i = 0; i < sendcmd->nb_intervals; i++) { | |
431 | Interval *interval = &sendcmd->intervals[i]; | |
432 | for (j = 0; j < interval->nb_commands; j++) { | |
433 | Command *cmd = &interval->commands[j]; | |
434 | av_free(cmd->target); | |
435 | av_free(cmd->command); | |
436 | av_free(cmd->arg); | |
437 | } | |
438 | av_free(interval->commands); | |
439 | } | |
440 | av_freep(&sendcmd->intervals); | |
441 | } | |
442 | ||
443 | static int filter_frame(AVFilterLink *inlink, AVFrame *ref) | |
444 | { | |
445 | AVFilterContext *ctx = inlink->dst; | |
446 | SendCmdContext *sendcmd = ctx->priv; | |
447 | int64_t ts; | |
448 | int i, j, ret; | |
449 | ||
450 | if (ref->pts == AV_NOPTS_VALUE) | |
451 | goto end; | |
452 | ||
453 | ts = av_rescale_q(ref->pts, inlink->time_base, AV_TIME_BASE_Q); | |
454 | ||
455 | #define WITHIN_INTERVAL(ts, start_ts, end_ts) ((ts) >= (start_ts) && (ts) < (end_ts)) | |
456 | ||
457 | for (i = 0; i < sendcmd->nb_intervals; i++) { | |
458 | Interval *interval = &sendcmd->intervals[i]; | |
459 | int flags = 0; | |
460 | ||
461 | if (!interval->enabled && WITHIN_INTERVAL(ts, interval->start_ts, interval->end_ts)) { | |
462 | flags += COMMAND_FLAG_ENTER; | |
463 | interval->enabled = 1; | |
464 | } | |
465 | if (interval->enabled && !WITHIN_INTERVAL(ts, interval->start_ts, interval->end_ts)) { | |
466 | flags += COMMAND_FLAG_LEAVE; | |
467 | interval->enabled = 0; | |
468 | } | |
469 | ||
470 | if (flags) { | |
471 | AVBPrint pbuf; | |
472 | av_log(ctx, AV_LOG_VERBOSE, | |
473 | "[%s] interval #%d start_ts:%f end_ts:%f ts:%f\n", | |
474 | make_command_flags_str(&pbuf, flags), interval->index, | |
475 | (double)interval->start_ts/1000000, (double)interval->end_ts/1000000, | |
476 | (double)ts/1000000); | |
477 | ||
478 | for (j = 0; flags && j < interval->nb_commands; j++) { | |
479 | Command *cmd = &interval->commands[j]; | |
480 | char buf[1024]; | |
481 | ||
482 | if (cmd->flags & flags) { | |
483 | av_log(ctx, AV_LOG_VERBOSE, | |
484 | "Processing command #%d target:%s command:%s arg:%s\n", | |
485 | cmd->index, cmd->target, cmd->command, cmd->arg); | |
486 | ret = avfilter_graph_send_command(inlink->graph, | |
487 | cmd->target, cmd->command, cmd->arg, | |
488 | buf, sizeof(buf), | |
489 | AVFILTER_CMD_FLAG_ONE); | |
490 | av_log(ctx, AV_LOG_VERBOSE, | |
491 | "Command reply for command #%d: ret:%s res:%s\n", | |
492 | cmd->index, av_err2str(ret), buf); | |
493 | } | |
494 | } | |
495 | } | |
496 | } | |
497 | ||
498 | end: | |
499 | switch (inlink->type) { | |
500 | case AVMEDIA_TYPE_VIDEO: | |
501 | case AVMEDIA_TYPE_AUDIO: | |
502 | return ff_filter_frame(inlink->dst->outputs[0], ref); | |
503 | } | |
504 | ||
505 | return AVERROR(ENOSYS); | |
506 | } | |
507 | ||
508 | #if CONFIG_SENDCMD_FILTER | |
509 | ||
510 | #define sendcmd_options options | |
511 | AVFILTER_DEFINE_CLASS(sendcmd); | |
512 | ||
513 | static const AVFilterPad sendcmd_inputs[] = { | |
514 | { | |
515 | .name = "default", | |
516 | .type = AVMEDIA_TYPE_VIDEO, | |
517 | .filter_frame = filter_frame, | |
518 | }, | |
519 | { NULL } | |
520 | }; | |
521 | ||
522 | static const AVFilterPad sendcmd_outputs[] = { | |
523 | { | |
524 | .name = "default", | |
525 | .type = AVMEDIA_TYPE_VIDEO, | |
526 | }, | |
527 | { NULL } | |
528 | }; | |
529 | ||
530 | AVFilter ff_vf_sendcmd = { | |
531 | .name = "sendcmd", | |
532 | .description = NULL_IF_CONFIG_SMALL("Send commands to filters."), | |
533 | .init = init, | |
534 | .uninit = uninit, | |
535 | .priv_size = sizeof(SendCmdContext), | |
536 | .inputs = sendcmd_inputs, | |
537 | .outputs = sendcmd_outputs, | |
538 | .priv_class = &sendcmd_class, | |
539 | }; | |
540 | ||
541 | #endif | |
542 | ||
543 | #if CONFIG_ASENDCMD_FILTER | |
544 | ||
545 | #define asendcmd_options options | |
546 | AVFILTER_DEFINE_CLASS(asendcmd); | |
547 | ||
548 | static const AVFilterPad asendcmd_inputs[] = { | |
549 | { | |
550 | .name = "default", | |
551 | .type = AVMEDIA_TYPE_AUDIO, | |
552 | .filter_frame = filter_frame, | |
553 | }, | |
554 | { NULL } | |
555 | }; | |
556 | ||
557 | static const AVFilterPad asendcmd_outputs[] = { | |
558 | { | |
559 | .name = "default", | |
560 | .type = AVMEDIA_TYPE_AUDIO, | |
561 | }, | |
562 | { NULL } | |
563 | }; | |
564 | ||
565 | AVFilter ff_af_asendcmd = { | |
566 | .name = "asendcmd", | |
567 | .description = NULL_IF_CONFIG_SMALL("Send commands to filters."), | |
568 | .init = init, | |
569 | .uninit = uninit, | |
570 | .priv_size = sizeof(SendCmdContext), | |
571 | .inputs = asendcmd_inputs, | |
572 | .outputs = asendcmd_outputs, | |
573 | .priv_class = &asendcmd_class, | |
574 | }; | |
575 | ||
576 | #endif |