Commit | Line | Data |
---|---|---|
2ba45a60 DM |
1 | /* |
2 | * ffmpeg filter configuration | |
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 | #include <stdint.h> | |
22 | ||
23 | #include "ffmpeg.h" | |
24 | ||
25 | #include "libavfilter/avfilter.h" | |
26 | #include "libavfilter/buffersink.h" | |
27 | ||
28 | #include "libavresample/avresample.h" | |
29 | ||
30 | #include "libavutil/avassert.h" | |
31 | #include "libavutil/avstring.h" | |
32 | #include "libavutil/bprint.h" | |
33 | #include "libavutil/channel_layout.h" | |
34 | #include "libavutil/opt.h" | |
35 | #include "libavutil/pixdesc.h" | |
36 | #include "libavutil/pixfmt.h" | |
37 | #include "libavutil/imgutils.h" | |
38 | #include "libavutil/samplefmt.h" | |
39 | ||
40 | enum AVPixelFormat choose_pixel_fmt(AVStream *st, AVCodecContext *enc_ctx, AVCodec *codec, enum AVPixelFormat target) | |
41 | { | |
42 | if (codec && codec->pix_fmts) { | |
43 | const enum AVPixelFormat *p = codec->pix_fmts; | |
44 | const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(target); | |
45 | int has_alpha = desc ? desc->nb_components % 2 == 0 : 0; | |
46 | enum AVPixelFormat best= AV_PIX_FMT_NONE; | |
47 | static const enum AVPixelFormat mjpeg_formats[] = | |
48 | { AV_PIX_FMT_YUVJ420P, AV_PIX_FMT_YUVJ422P, AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV422P, AV_PIX_FMT_NONE }; | |
49 | static const enum AVPixelFormat ljpeg_formats[] = | |
50 | { AV_PIX_FMT_YUVJ420P, AV_PIX_FMT_YUVJ422P, AV_PIX_FMT_YUVJ444P, AV_PIX_FMT_YUV420P, | |
51 | AV_PIX_FMT_YUV422P, AV_PIX_FMT_YUV444P, AV_PIX_FMT_BGRA, AV_PIX_FMT_NONE }; | |
52 | ||
53 | if (enc_ctx->strict_std_compliance <= FF_COMPLIANCE_UNOFFICIAL) { | |
54 | if (enc_ctx->codec_id == AV_CODEC_ID_MJPEG) { | |
55 | p = mjpeg_formats; | |
56 | } else if (enc_ctx->codec_id == AV_CODEC_ID_LJPEG) { | |
57 | p =ljpeg_formats; | |
58 | } | |
59 | } | |
60 | for (; *p != AV_PIX_FMT_NONE; p++) { | |
61 | best= avcodec_find_best_pix_fmt_of_2(best, *p, target, has_alpha, NULL); | |
62 | if (*p == target) | |
63 | break; | |
64 | } | |
65 | if (*p == AV_PIX_FMT_NONE) { | |
66 | if (target != AV_PIX_FMT_NONE) | |
67 | av_log(NULL, AV_LOG_WARNING, | |
68 | "Incompatible pixel format '%s' for codec '%s', auto-selecting format '%s'\n", | |
69 | av_get_pix_fmt_name(target), | |
70 | codec->name, | |
71 | av_get_pix_fmt_name(best)); | |
72 | return best; | |
73 | } | |
74 | } | |
75 | return target; | |
76 | } | |
77 | ||
78 | void choose_sample_fmt(AVStream *st, AVCodec *codec) | |
79 | { | |
80 | if (codec && codec->sample_fmts) { | |
81 | const enum AVSampleFormat *p = codec->sample_fmts; | |
82 | for (; *p != -1; p++) { | |
83 | if (*p == st->codec->sample_fmt) | |
84 | break; | |
85 | } | |
86 | if (*p == -1) { | |
87 | if((codec->capabilities & CODEC_CAP_LOSSLESS) && av_get_sample_fmt_name(st->codec->sample_fmt) > av_get_sample_fmt_name(codec->sample_fmts[0])) | |
88 | av_log(NULL, AV_LOG_ERROR, "Conversion will not be lossless.\n"); | |
89 | if(av_get_sample_fmt_name(st->codec->sample_fmt)) | |
90 | av_log(NULL, AV_LOG_WARNING, | |
91 | "Incompatible sample format '%s' for codec '%s', auto-selecting format '%s'\n", | |
92 | av_get_sample_fmt_name(st->codec->sample_fmt), | |
93 | codec->name, | |
94 | av_get_sample_fmt_name(codec->sample_fmts[0])); | |
95 | st->codec->sample_fmt = codec->sample_fmts[0]; | |
96 | } | |
97 | } | |
98 | } | |
99 | ||
100 | static char *choose_pix_fmts(OutputStream *ost) | |
101 | { | |
102 | AVDictionaryEntry *strict_dict = av_dict_get(ost->encoder_opts, "strict", NULL, 0); | |
103 | if (strict_dict) | |
104 | // used by choose_pixel_fmt() and below | |
105 | av_opt_set(ost->enc_ctx, "strict", strict_dict->value, 0); | |
106 | ||
107 | if (ost->keep_pix_fmt) { | |
108 | if (ost->filter) | |
109 | avfilter_graph_set_auto_convert(ost->filter->graph->graph, | |
110 | AVFILTER_AUTO_CONVERT_NONE); | |
111 | if (ost->enc_ctx->pix_fmt == AV_PIX_FMT_NONE) | |
112 | return NULL; | |
113 | return av_strdup(av_get_pix_fmt_name(ost->enc_ctx->pix_fmt)); | |
114 | } | |
115 | if (ost->enc_ctx->pix_fmt != AV_PIX_FMT_NONE) { | |
116 | return av_strdup(av_get_pix_fmt_name(choose_pixel_fmt(ost->st, ost->enc_ctx, ost->enc, ost->enc_ctx->pix_fmt))); | |
117 | } else if (ost->enc && ost->enc->pix_fmts) { | |
118 | const enum AVPixelFormat *p; | |
119 | AVIOContext *s = NULL; | |
120 | uint8_t *ret; | |
121 | int len; | |
122 | ||
123 | if (avio_open_dyn_buf(&s) < 0) | |
124 | exit_program(1); | |
125 | ||
126 | p = ost->enc->pix_fmts; | |
127 | if (ost->enc_ctx->strict_std_compliance <= FF_COMPLIANCE_UNOFFICIAL) { | |
128 | if (ost->enc_ctx->codec_id == AV_CODEC_ID_MJPEG) { | |
129 | p = (const enum AVPixelFormat[]) { AV_PIX_FMT_YUVJ420P, AV_PIX_FMT_YUVJ422P, AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV422P, AV_PIX_FMT_NONE }; | |
130 | } else if (ost->enc_ctx->codec_id == AV_CODEC_ID_LJPEG) { | |
131 | p = (const enum AVPixelFormat[]) { AV_PIX_FMT_YUVJ420P, AV_PIX_FMT_YUVJ422P, AV_PIX_FMT_YUVJ444P, AV_PIX_FMT_YUV420P, | |
132 | AV_PIX_FMT_YUV422P, AV_PIX_FMT_YUV444P, AV_PIX_FMT_BGRA, AV_PIX_FMT_NONE }; | |
133 | } | |
134 | } | |
135 | ||
136 | for (; *p != AV_PIX_FMT_NONE; p++) { | |
137 | const char *name = av_get_pix_fmt_name(*p); | |
138 | avio_printf(s, "%s|", name); | |
139 | } | |
140 | len = avio_close_dyn_buf(s, &ret); | |
141 | ret[len - 1] = 0; | |
142 | return ret; | |
143 | } else | |
144 | return NULL; | |
145 | } | |
146 | ||
147 | /* Define a function for building a string containing a list of | |
148 | * allowed formats. */ | |
149 | #define DEF_CHOOSE_FORMAT(type, var, supported_list, none, get_name) \ | |
150 | static char *choose_ ## var ## s(OutputStream *ost) \ | |
151 | { \ | |
152 | if (ost->enc_ctx->var != none) { \ | |
153 | get_name(ost->enc_ctx->var); \ | |
154 | return av_strdup(name); \ | |
155 | } else if (ost->enc && ost->enc->supported_list) { \ | |
156 | const type *p; \ | |
157 | AVIOContext *s = NULL; \ | |
158 | uint8_t *ret; \ | |
159 | int len; \ | |
160 | \ | |
161 | if (avio_open_dyn_buf(&s) < 0) \ | |
162 | exit_program(1); \ | |
163 | \ | |
164 | for (p = ost->enc->supported_list; *p != none; p++) { \ | |
165 | get_name(*p); \ | |
166 | avio_printf(s, "%s|", name); \ | |
167 | } \ | |
168 | len = avio_close_dyn_buf(s, &ret); \ | |
169 | ret[len - 1] = 0; \ | |
170 | return ret; \ | |
171 | } else \ | |
172 | return NULL; \ | |
173 | } | |
174 | ||
175 | // DEF_CHOOSE_FORMAT(enum AVPixelFormat, pix_fmt, pix_fmts, AV_PIX_FMT_NONE, | |
176 | // GET_PIX_FMT_NAME) | |
177 | ||
178 | DEF_CHOOSE_FORMAT(enum AVSampleFormat, sample_fmt, sample_fmts, | |
179 | AV_SAMPLE_FMT_NONE, GET_SAMPLE_FMT_NAME) | |
180 | ||
181 | DEF_CHOOSE_FORMAT(int, sample_rate, supported_samplerates, 0, | |
182 | GET_SAMPLE_RATE_NAME) | |
183 | ||
184 | DEF_CHOOSE_FORMAT(uint64_t, channel_layout, channel_layouts, 0, | |
185 | GET_CH_LAYOUT_NAME) | |
186 | ||
187 | FilterGraph *init_simple_filtergraph(InputStream *ist, OutputStream *ost) | |
188 | { | |
189 | FilterGraph *fg = av_mallocz(sizeof(*fg)); | |
190 | ||
191 | if (!fg) | |
192 | exit_program(1); | |
193 | fg->index = nb_filtergraphs; | |
194 | ||
195 | GROW_ARRAY(fg->outputs, fg->nb_outputs); | |
196 | if (!(fg->outputs[0] = av_mallocz(sizeof(*fg->outputs[0])))) | |
197 | exit_program(1); | |
198 | fg->outputs[0]->ost = ost; | |
199 | fg->outputs[0]->graph = fg; | |
200 | ||
201 | ost->filter = fg->outputs[0]; | |
202 | ||
203 | GROW_ARRAY(fg->inputs, fg->nb_inputs); | |
204 | if (!(fg->inputs[0] = av_mallocz(sizeof(*fg->inputs[0])))) | |
205 | exit_program(1); | |
206 | fg->inputs[0]->ist = ist; | |
207 | fg->inputs[0]->graph = fg; | |
208 | ||
209 | GROW_ARRAY(ist->filters, ist->nb_filters); | |
210 | ist->filters[ist->nb_filters - 1] = fg->inputs[0]; | |
211 | ||
212 | GROW_ARRAY(filtergraphs, nb_filtergraphs); | |
213 | filtergraphs[nb_filtergraphs - 1] = fg; | |
214 | ||
215 | return fg; | |
216 | } | |
217 | ||
218 | static void init_input_filter(FilterGraph *fg, AVFilterInOut *in) | |
219 | { | |
220 | InputStream *ist = NULL; | |
221 | enum AVMediaType type = avfilter_pad_get_type(in->filter_ctx->input_pads, in->pad_idx); | |
222 | int i; | |
223 | ||
224 | // TODO: support other filter types | |
225 | if (type != AVMEDIA_TYPE_VIDEO && type != AVMEDIA_TYPE_AUDIO) { | |
226 | av_log(NULL, AV_LOG_FATAL, "Only video and audio filters supported " | |
227 | "currently.\n"); | |
228 | exit_program(1); | |
229 | } | |
230 | ||
231 | if (in->name) { | |
232 | AVFormatContext *s; | |
233 | AVStream *st = NULL; | |
234 | char *p; | |
235 | int file_idx = strtol(in->name, &p, 0); | |
236 | ||
237 | if (file_idx < 0 || file_idx >= nb_input_files) { | |
238 | av_log(NULL, AV_LOG_FATAL, "Invalid file index %d in filtergraph description %s.\n", | |
239 | file_idx, fg->graph_desc); | |
240 | exit_program(1); | |
241 | } | |
242 | s = input_files[file_idx]->ctx; | |
243 | ||
244 | for (i = 0; i < s->nb_streams; i++) { | |
245 | enum AVMediaType stream_type = s->streams[i]->codec->codec_type; | |
246 | if (stream_type != type && | |
247 | !(stream_type == AVMEDIA_TYPE_SUBTITLE && | |
248 | type == AVMEDIA_TYPE_VIDEO /* sub2video hack */)) | |
249 | continue; | |
250 | if (check_stream_specifier(s, s->streams[i], *p == ':' ? p + 1 : p) == 1) { | |
251 | st = s->streams[i]; | |
252 | break; | |
253 | } | |
254 | } | |
255 | if (!st) { | |
256 | av_log(NULL, AV_LOG_FATAL, "Stream specifier '%s' in filtergraph description %s " | |
257 | "matches no streams.\n", p, fg->graph_desc); | |
258 | exit_program(1); | |
259 | } | |
260 | ist = input_streams[input_files[file_idx]->ist_index + st->index]; | |
261 | } else { | |
262 | /* find the first unused stream of corresponding type */ | |
263 | for (i = 0; i < nb_input_streams; i++) { | |
264 | ist = input_streams[i]; | |
265 | if (ist->dec_ctx->codec_type == type && ist->discard) | |
266 | break; | |
267 | } | |
268 | if (i == nb_input_streams) { | |
269 | av_log(NULL, AV_LOG_FATAL, "Cannot find a matching stream for " | |
270 | "unlabeled input pad %d on filter %s\n", in->pad_idx, | |
271 | in->filter_ctx->name); | |
272 | exit_program(1); | |
273 | } | |
274 | } | |
275 | av_assert0(ist); | |
276 | ||
277 | ist->discard = 0; | |
278 | ist->decoding_needed |= DECODING_FOR_FILTER; | |
279 | ist->st->discard = AVDISCARD_NONE; | |
280 | ||
281 | GROW_ARRAY(fg->inputs, fg->nb_inputs); | |
282 | if (!(fg->inputs[fg->nb_inputs - 1] = av_mallocz(sizeof(*fg->inputs[0])))) | |
283 | exit_program(1); | |
284 | fg->inputs[fg->nb_inputs - 1]->ist = ist; | |
285 | fg->inputs[fg->nb_inputs - 1]->graph = fg; | |
286 | ||
287 | GROW_ARRAY(ist->filters, ist->nb_filters); | |
288 | ist->filters[ist->nb_filters - 1] = fg->inputs[fg->nb_inputs - 1]; | |
289 | } | |
290 | ||
291 | static int insert_trim(int64_t start_time, int64_t duration, | |
292 | AVFilterContext **last_filter, int *pad_idx, | |
293 | const char *filter_name) | |
294 | { | |
295 | AVFilterGraph *graph = (*last_filter)->graph; | |
296 | AVFilterContext *ctx; | |
297 | const AVFilter *trim; | |
298 | enum AVMediaType type = avfilter_pad_get_type((*last_filter)->output_pads, *pad_idx); | |
299 | const char *name = (type == AVMEDIA_TYPE_VIDEO) ? "trim" : "atrim"; | |
300 | int ret = 0; | |
301 | ||
302 | if (duration == INT64_MAX && start_time == AV_NOPTS_VALUE) | |
303 | return 0; | |
304 | ||
305 | trim = avfilter_get_by_name(name); | |
306 | if (!trim) { | |
307 | av_log(NULL, AV_LOG_ERROR, "%s filter not present, cannot limit " | |
308 | "recording time.\n", name); | |
309 | return AVERROR_FILTER_NOT_FOUND; | |
310 | } | |
311 | ||
312 | ctx = avfilter_graph_alloc_filter(graph, trim, filter_name); | |
313 | if (!ctx) | |
314 | return AVERROR(ENOMEM); | |
315 | ||
316 | if (duration != INT64_MAX) { | |
317 | ret = av_opt_set_int(ctx, "durationi", duration, | |
318 | AV_OPT_SEARCH_CHILDREN); | |
319 | } | |
320 | if (ret >= 0 && start_time != AV_NOPTS_VALUE) { | |
321 | ret = av_opt_set_int(ctx, "starti", start_time, | |
322 | AV_OPT_SEARCH_CHILDREN); | |
323 | } | |
324 | if (ret < 0) { | |
325 | av_log(ctx, AV_LOG_ERROR, "Error configuring the %s filter", name); | |
326 | return ret; | |
327 | } | |
328 | ||
329 | ret = avfilter_init_str(ctx, NULL); | |
330 | if (ret < 0) | |
331 | return ret; | |
332 | ||
333 | ret = avfilter_link(*last_filter, *pad_idx, ctx, 0); | |
334 | if (ret < 0) | |
335 | return ret; | |
336 | ||
337 | *last_filter = ctx; | |
338 | *pad_idx = 0; | |
339 | return 0; | |
340 | } | |
341 | ||
342 | static int configure_output_video_filter(FilterGraph *fg, OutputFilter *ofilter, AVFilterInOut *out) | |
343 | { | |
344 | char *pix_fmts; | |
345 | OutputStream *ost = ofilter->ost; | |
346 | OutputFile *of = output_files[ost->file_index]; | |
347 | AVCodecContext *codec = ost->enc_ctx; | |
348 | AVFilterContext *last_filter = out->filter_ctx; | |
349 | int pad_idx = out->pad_idx; | |
350 | int ret; | |
351 | char name[255]; | |
352 | ||
353 | snprintf(name, sizeof(name), "output stream %d:%d", ost->file_index, ost->index); | |
354 | ret = avfilter_graph_create_filter(&ofilter->filter, | |
355 | avfilter_get_by_name("buffersink"), | |
356 | name, NULL, NULL, fg->graph); | |
357 | ||
358 | if (ret < 0) | |
359 | return ret; | |
360 | ||
361 | if (codec->width || codec->height) { | |
362 | char args[255]; | |
363 | AVFilterContext *filter; | |
364 | ||
365 | snprintf(args, sizeof(args), "%d:%d:0x%X", | |
366 | codec->width, | |
367 | codec->height, | |
368 | (unsigned)ost->sws_flags); | |
369 | snprintf(name, sizeof(name), "scaler for output stream %d:%d", | |
370 | ost->file_index, ost->index); | |
371 | if ((ret = avfilter_graph_create_filter(&filter, avfilter_get_by_name("scale"), | |
372 | name, args, NULL, fg->graph)) < 0) | |
373 | return ret; | |
374 | if ((ret = avfilter_link(last_filter, pad_idx, filter, 0)) < 0) | |
375 | return ret; | |
376 | ||
377 | last_filter = filter; | |
378 | pad_idx = 0; | |
379 | } | |
380 | ||
381 | if ((pix_fmts = choose_pix_fmts(ost))) { | |
382 | AVFilterContext *filter; | |
383 | snprintf(name, sizeof(name), "pixel format for output stream %d:%d", | |
384 | ost->file_index, ost->index); | |
385 | ret = avfilter_graph_create_filter(&filter, | |
f6fa7814 DM |
386 | avfilter_get_by_name("format"), |
387 | "format", pix_fmts, NULL, fg->graph); | |
2ba45a60 DM |
388 | av_freep(&pix_fmts); |
389 | if (ret < 0) | |
390 | return ret; | |
391 | if ((ret = avfilter_link(last_filter, pad_idx, filter, 0)) < 0) | |
392 | return ret; | |
393 | ||
394 | last_filter = filter; | |
395 | pad_idx = 0; | |
396 | } | |
397 | ||
398 | if (ost->frame_rate.num && 0) { | |
399 | AVFilterContext *fps; | |
400 | char args[255]; | |
401 | ||
402 | snprintf(args, sizeof(args), "fps=%d/%d", ost->frame_rate.num, | |
403 | ost->frame_rate.den); | |
404 | snprintf(name, sizeof(name), "fps for output stream %d:%d", | |
405 | ost->file_index, ost->index); | |
406 | ret = avfilter_graph_create_filter(&fps, avfilter_get_by_name("fps"), | |
407 | name, args, NULL, fg->graph); | |
408 | if (ret < 0) | |
409 | return ret; | |
410 | ||
411 | ret = avfilter_link(last_filter, pad_idx, fps, 0); | |
412 | if (ret < 0) | |
413 | return ret; | |
414 | last_filter = fps; | |
415 | pad_idx = 0; | |
416 | } | |
417 | ||
418 | snprintf(name, sizeof(name), "trim for output stream %d:%d", | |
419 | ost->file_index, ost->index); | |
420 | ret = insert_trim(of->start_time, of->recording_time, | |
421 | &last_filter, &pad_idx, name); | |
422 | if (ret < 0) | |
423 | return ret; | |
424 | ||
425 | ||
426 | if ((ret = avfilter_link(last_filter, pad_idx, ofilter->filter, 0)) < 0) | |
427 | return ret; | |
428 | ||
429 | return 0; | |
430 | } | |
431 | ||
432 | static int configure_output_audio_filter(FilterGraph *fg, OutputFilter *ofilter, AVFilterInOut *out) | |
433 | { | |
434 | OutputStream *ost = ofilter->ost; | |
435 | OutputFile *of = output_files[ost->file_index]; | |
436 | AVCodecContext *codec = ost->enc_ctx; | |
437 | AVFilterContext *last_filter = out->filter_ctx; | |
438 | int pad_idx = out->pad_idx; | |
439 | char *sample_fmts, *sample_rates, *channel_layouts; | |
440 | char name[255]; | |
441 | int ret; | |
442 | ||
443 | snprintf(name, sizeof(name), "output stream %d:%d", ost->file_index, ost->index); | |
444 | ret = avfilter_graph_create_filter(&ofilter->filter, | |
445 | avfilter_get_by_name("abuffersink"), | |
446 | name, NULL, NULL, fg->graph); | |
447 | if (ret < 0) | |
448 | return ret; | |
449 | if ((ret = av_opt_set_int(ofilter->filter, "all_channel_counts", 1, AV_OPT_SEARCH_CHILDREN)) < 0) | |
450 | return ret; | |
451 | ||
452 | #define AUTO_INSERT_FILTER(opt_name, filter_name, arg) do { \ | |
453 | AVFilterContext *filt_ctx; \ | |
454 | \ | |
455 | av_log(NULL, AV_LOG_INFO, opt_name " is forwarded to lavfi " \ | |
456 | "similarly to -af " filter_name "=%s.\n", arg); \ | |
457 | \ | |
458 | ret = avfilter_graph_create_filter(&filt_ctx, \ | |
459 | avfilter_get_by_name(filter_name), \ | |
460 | filter_name, arg, NULL, fg->graph); \ | |
461 | if (ret < 0) \ | |
462 | return ret; \ | |
463 | \ | |
464 | ret = avfilter_link(last_filter, pad_idx, filt_ctx, 0); \ | |
465 | if (ret < 0) \ | |
466 | return ret; \ | |
467 | \ | |
468 | last_filter = filt_ctx; \ | |
469 | pad_idx = 0; \ | |
470 | } while (0) | |
471 | if (ost->audio_channels_mapped) { | |
472 | int i; | |
473 | AVBPrint pan_buf; | |
474 | av_bprint_init(&pan_buf, 256, 8192); | |
475 | av_bprintf(&pan_buf, "0x%"PRIx64, | |
476 | av_get_default_channel_layout(ost->audio_channels_mapped)); | |
477 | for (i = 0; i < ost->audio_channels_mapped; i++) | |
478 | if (ost->audio_channels_map[i] != -1) | |
479 | av_bprintf(&pan_buf, ":c%d=c%d", i, ost->audio_channels_map[i]); | |
480 | ||
481 | AUTO_INSERT_FILTER("-map_channel", "pan", pan_buf.str); | |
482 | av_bprint_finalize(&pan_buf, NULL); | |
483 | } | |
484 | ||
485 | if (codec->channels && !codec->channel_layout) | |
486 | codec->channel_layout = av_get_default_channel_layout(codec->channels); | |
487 | ||
488 | sample_fmts = choose_sample_fmts(ost); | |
489 | sample_rates = choose_sample_rates(ost); | |
490 | channel_layouts = choose_channel_layouts(ost); | |
491 | if (sample_fmts || sample_rates || channel_layouts) { | |
492 | AVFilterContext *format; | |
493 | char args[256]; | |
494 | args[0] = 0; | |
495 | ||
496 | if (sample_fmts) | |
497 | av_strlcatf(args, sizeof(args), "sample_fmts=%s:", | |
498 | sample_fmts); | |
499 | if (sample_rates) | |
500 | av_strlcatf(args, sizeof(args), "sample_rates=%s:", | |
501 | sample_rates); | |
502 | if (channel_layouts) | |
503 | av_strlcatf(args, sizeof(args), "channel_layouts=%s:", | |
504 | channel_layouts); | |
505 | ||
506 | av_freep(&sample_fmts); | |
507 | av_freep(&sample_rates); | |
508 | av_freep(&channel_layouts); | |
509 | ||
510 | snprintf(name, sizeof(name), "audio format for output stream %d:%d", | |
511 | ost->file_index, ost->index); | |
512 | ret = avfilter_graph_create_filter(&format, | |
513 | avfilter_get_by_name("aformat"), | |
514 | name, args, NULL, fg->graph); | |
515 | if (ret < 0) | |
516 | return ret; | |
517 | ||
518 | ret = avfilter_link(last_filter, pad_idx, format, 0); | |
519 | if (ret < 0) | |
520 | return ret; | |
521 | ||
522 | last_filter = format; | |
523 | pad_idx = 0; | |
524 | } | |
525 | ||
526 | if (audio_volume != 256 && 0) { | |
527 | char args[256]; | |
528 | ||
529 | snprintf(args, sizeof(args), "%f", audio_volume / 256.); | |
530 | AUTO_INSERT_FILTER("-vol", "volume", args); | |
531 | } | |
532 | ||
533 | if (ost->apad && of->shortest) { | |
534 | char args[256]; | |
535 | int i; | |
536 | ||
537 | for (i=0; i<of->ctx->nb_streams; i++) | |
538 | if (of->ctx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) | |
539 | break; | |
540 | ||
541 | if (i<of->ctx->nb_streams) { | |
542 | snprintf(args, sizeof(args), "%s", ost->apad); | |
543 | AUTO_INSERT_FILTER("-apad", "apad", args); | |
544 | } | |
545 | } | |
546 | ||
547 | snprintf(name, sizeof(name), "trim for output stream %d:%d", | |
548 | ost->file_index, ost->index); | |
549 | ret = insert_trim(of->start_time, of->recording_time, | |
550 | &last_filter, &pad_idx, name); | |
551 | if (ret < 0) | |
552 | return ret; | |
553 | ||
554 | if ((ret = avfilter_link(last_filter, pad_idx, ofilter->filter, 0)) < 0) | |
555 | return ret; | |
556 | ||
557 | return 0; | |
558 | } | |
559 | ||
560 | #define DESCRIBE_FILTER_LINK(f, inout, in) \ | |
561 | { \ | |
562 | AVFilterContext *ctx = inout->filter_ctx; \ | |
563 | AVFilterPad *pads = in ? ctx->input_pads : ctx->output_pads; \ | |
564 | int nb_pads = in ? ctx->nb_inputs : ctx->nb_outputs; \ | |
565 | AVIOContext *pb; \ | |
566 | \ | |
567 | if (avio_open_dyn_buf(&pb) < 0) \ | |
568 | exit_program(1); \ | |
569 | \ | |
570 | avio_printf(pb, "%s", ctx->filter->name); \ | |
571 | if (nb_pads > 1) \ | |
572 | avio_printf(pb, ":%s", avfilter_pad_get_name(pads, inout->pad_idx));\ | |
573 | avio_w8(pb, 0); \ | |
574 | avio_close_dyn_buf(pb, &f->name); \ | |
575 | } | |
576 | ||
577 | int configure_output_filter(FilterGraph *fg, OutputFilter *ofilter, AVFilterInOut *out) | |
578 | { | |
579 | av_freep(&ofilter->name); | |
580 | DESCRIBE_FILTER_LINK(ofilter, out, 0); | |
581 | ||
582 | switch (avfilter_pad_get_type(out->filter_ctx->output_pads, out->pad_idx)) { | |
583 | case AVMEDIA_TYPE_VIDEO: return configure_output_video_filter(fg, ofilter, out); | |
584 | case AVMEDIA_TYPE_AUDIO: return configure_output_audio_filter(fg, ofilter, out); | |
585 | default: av_assert0(0); | |
586 | } | |
587 | } | |
588 | ||
589 | static int sub2video_prepare(InputStream *ist) | |
590 | { | |
591 | AVFormatContext *avf = input_files[ist->file_index]->ctx; | |
592 | int i, w, h; | |
593 | ||
594 | /* Compute the size of the canvas for the subtitles stream. | |
595 | If the subtitles codec has set a size, use it. Otherwise use the | |
596 | maximum dimensions of the video streams in the same file. */ | |
597 | w = ist->dec_ctx->width; | |
598 | h = ist->dec_ctx->height; | |
599 | if (!(w && h)) { | |
600 | for (i = 0; i < avf->nb_streams; i++) { | |
601 | if (avf->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) { | |
602 | w = FFMAX(w, avf->streams[i]->codec->width); | |
603 | h = FFMAX(h, avf->streams[i]->codec->height); | |
604 | } | |
605 | } | |
606 | if (!(w && h)) { | |
607 | w = FFMAX(w, 720); | |
608 | h = FFMAX(h, 576); | |
609 | } | |
610 | av_log(avf, AV_LOG_INFO, "sub2video: using %dx%d canvas\n", w, h); | |
611 | } | |
612 | ist->sub2video.w = ist->dec_ctx->width = ist->resample_width = w; | |
613 | ist->sub2video.h = ist->dec_ctx->height = ist->resample_height = h; | |
614 | ||
615 | /* rectangles are AV_PIX_FMT_PAL8, but we have no guarantee that the | |
616 | palettes for all rectangles are identical or compatible */ | |
617 | ist->resample_pix_fmt = ist->dec_ctx->pix_fmt = AV_PIX_FMT_RGB32; | |
618 | ||
619 | ist->sub2video.frame = av_frame_alloc(); | |
620 | if (!ist->sub2video.frame) | |
621 | return AVERROR(ENOMEM); | |
f6fa7814 | 622 | ist->sub2video.last_pts = INT64_MIN; |
2ba45a60 DM |
623 | return 0; |
624 | } | |
625 | ||
626 | static int configure_input_video_filter(FilterGraph *fg, InputFilter *ifilter, | |
627 | AVFilterInOut *in) | |
628 | { | |
629 | AVFilterContext *last_filter; | |
630 | const AVFilter *buffer_filt = avfilter_get_by_name("buffer"); | |
631 | InputStream *ist = ifilter->ist; | |
632 | InputFile *f = input_files[ist->file_index]; | |
633 | AVRational tb = ist->framerate.num ? av_inv_q(ist->framerate) : | |
634 | ist->st->time_base; | |
635 | AVRational fr = ist->framerate; | |
636 | AVRational sar; | |
637 | AVBPrint args; | |
638 | char name[255]; | |
639 | int ret, pad_idx = 0; | |
f6fa7814 | 640 | int64_t tsoffset = 0; |
2ba45a60 DM |
641 | |
642 | if (ist->dec_ctx->codec_type == AVMEDIA_TYPE_AUDIO) { | |
643 | av_log(NULL, AV_LOG_ERROR, "Cannot connect video filter to audio input\n"); | |
644 | return AVERROR(EINVAL); | |
645 | } | |
646 | ||
647 | if (!fr.num) | |
648 | fr = av_guess_frame_rate(input_files[ist->file_index]->ctx, ist->st, NULL); | |
649 | ||
650 | if (ist->dec_ctx->codec_type == AVMEDIA_TYPE_SUBTITLE) { | |
651 | ret = sub2video_prepare(ist); | |
652 | if (ret < 0) | |
653 | return ret; | |
654 | } | |
655 | ||
656 | sar = ist->st->sample_aspect_ratio.num ? | |
657 | ist->st->sample_aspect_ratio : | |
658 | ist->dec_ctx->sample_aspect_ratio; | |
659 | if(!sar.den) | |
660 | sar = (AVRational){0,1}; | |
661 | av_bprint_init(&args, 0, 1); | |
662 | av_bprintf(&args, | |
663 | "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:" | |
664 | "pixel_aspect=%d/%d:sws_param=flags=%d", ist->resample_width, | |
665 | ist->resample_height, | |
666 | ist->hwaccel_retrieve_data ? ist->hwaccel_retrieved_pix_fmt : ist->resample_pix_fmt, | |
667 | tb.num, tb.den, sar.num, sar.den, | |
668 | SWS_BILINEAR + ((ist->dec_ctx->flags&CODEC_FLAG_BITEXACT) ? SWS_BITEXACT:0)); | |
669 | if (fr.num && fr.den) | |
670 | av_bprintf(&args, ":frame_rate=%d/%d", fr.num, fr.den); | |
671 | snprintf(name, sizeof(name), "graph %d input from stream %d:%d", fg->index, | |
672 | ist->file_index, ist->st->index); | |
673 | ||
674 | if ((ret = avfilter_graph_create_filter(&ifilter->filter, buffer_filt, name, | |
675 | args.str, NULL, fg->graph)) < 0) | |
676 | return ret; | |
677 | last_filter = ifilter->filter; | |
678 | ||
679 | if (ist->framerate.num) { | |
680 | AVFilterContext *setpts; | |
681 | ||
682 | snprintf(name, sizeof(name), "force CFR for input from stream %d:%d", | |
683 | ist->file_index, ist->st->index); | |
684 | if ((ret = avfilter_graph_create_filter(&setpts, | |
685 | avfilter_get_by_name("setpts"), | |
686 | name, "N", NULL, | |
687 | fg->graph)) < 0) | |
688 | return ret; | |
689 | ||
690 | if ((ret = avfilter_link(last_filter, 0, setpts, 0)) < 0) | |
691 | return ret; | |
692 | ||
693 | last_filter = setpts; | |
694 | } | |
695 | ||
696 | if (do_deinterlace) { | |
697 | AVFilterContext *yadif; | |
698 | ||
699 | snprintf(name, sizeof(name), "deinterlace input from stream %d:%d", | |
700 | ist->file_index, ist->st->index); | |
701 | if ((ret = avfilter_graph_create_filter(&yadif, | |
702 | avfilter_get_by_name("yadif"), | |
703 | name, "", NULL, | |
704 | fg->graph)) < 0) | |
705 | return ret; | |
706 | ||
707 | if ((ret = avfilter_link(last_filter, 0, yadif, 0)) < 0) | |
708 | return ret; | |
709 | ||
710 | last_filter = yadif; | |
711 | } | |
712 | ||
713 | snprintf(name, sizeof(name), "trim for input stream %d:%d", | |
714 | ist->file_index, ist->st->index); | |
f6fa7814 DM |
715 | if (copy_ts) { |
716 | tsoffset = f->start_time == AV_NOPTS_VALUE ? 0 : f->start_time; | |
717 | if (!start_at_zero && f->ctx->start_time != AV_NOPTS_VALUE) | |
718 | tsoffset += f->ctx->start_time; | |
719 | } | |
2ba45a60 | 720 | ret = insert_trim(((f->start_time == AV_NOPTS_VALUE) || !f->accurate_seek) ? |
f6fa7814 DM |
721 | AV_NOPTS_VALUE : tsoffset, f->recording_time, |
722 | &last_filter, &pad_idx, name); | |
2ba45a60 DM |
723 | if (ret < 0) |
724 | return ret; | |
725 | ||
726 | if ((ret = avfilter_link(last_filter, 0, in->filter_ctx, in->pad_idx)) < 0) | |
727 | return ret; | |
728 | return 0; | |
729 | } | |
730 | ||
731 | static int configure_input_audio_filter(FilterGraph *fg, InputFilter *ifilter, | |
732 | AVFilterInOut *in) | |
733 | { | |
734 | AVFilterContext *last_filter; | |
735 | const AVFilter *abuffer_filt = avfilter_get_by_name("abuffer"); | |
736 | InputStream *ist = ifilter->ist; | |
737 | InputFile *f = input_files[ist->file_index]; | |
738 | AVBPrint args; | |
739 | char name[255]; | |
740 | int ret, pad_idx = 0; | |
f6fa7814 | 741 | int64_t tsoffset = 0; |
2ba45a60 DM |
742 | |
743 | if (ist->dec_ctx->codec_type != AVMEDIA_TYPE_AUDIO) { | |
744 | av_log(NULL, AV_LOG_ERROR, "Cannot connect audio filter to non audio input\n"); | |
745 | return AVERROR(EINVAL); | |
746 | } | |
747 | ||
748 | av_bprint_init(&args, 0, AV_BPRINT_SIZE_AUTOMATIC); | |
749 | av_bprintf(&args, "time_base=%d/%d:sample_rate=%d:sample_fmt=%s", | |
750 | 1, ist->dec_ctx->sample_rate, | |
751 | ist->dec_ctx->sample_rate, | |
752 | av_get_sample_fmt_name(ist->dec_ctx->sample_fmt)); | |
753 | if (ist->dec_ctx->channel_layout) | |
754 | av_bprintf(&args, ":channel_layout=0x%"PRIx64, | |
755 | ist->dec_ctx->channel_layout); | |
756 | else | |
757 | av_bprintf(&args, ":channels=%d", ist->dec_ctx->channels); | |
758 | snprintf(name, sizeof(name), "graph %d input from stream %d:%d", fg->index, | |
759 | ist->file_index, ist->st->index); | |
760 | ||
761 | if ((ret = avfilter_graph_create_filter(&ifilter->filter, abuffer_filt, | |
762 | name, args.str, NULL, | |
763 | fg->graph)) < 0) | |
764 | return ret; | |
765 | last_filter = ifilter->filter; | |
766 | ||
767 | #define AUTO_INSERT_FILTER_INPUT(opt_name, filter_name, arg) do { \ | |
768 | AVFilterContext *filt_ctx; \ | |
769 | \ | |
770 | av_log(NULL, AV_LOG_INFO, opt_name " is forwarded to lavfi " \ | |
771 | "similarly to -af " filter_name "=%s.\n", arg); \ | |
772 | \ | |
773 | snprintf(name, sizeof(name), "graph %d %s for input stream %d:%d", \ | |
774 | fg->index, filter_name, ist->file_index, ist->st->index); \ | |
775 | ret = avfilter_graph_create_filter(&filt_ctx, \ | |
776 | avfilter_get_by_name(filter_name), \ | |
777 | name, arg, NULL, fg->graph); \ | |
778 | if (ret < 0) \ | |
779 | return ret; \ | |
780 | \ | |
781 | ret = avfilter_link(last_filter, 0, filt_ctx, 0); \ | |
782 | if (ret < 0) \ | |
783 | return ret; \ | |
784 | \ | |
785 | last_filter = filt_ctx; \ | |
786 | } while (0) | |
787 | ||
788 | if (audio_sync_method > 0) { | |
789 | char args[256] = {0}; | |
790 | ||
791 | av_strlcatf(args, sizeof(args), "async=%d", audio_sync_method); | |
792 | if (audio_drift_threshold != 0.1) | |
793 | av_strlcatf(args, sizeof(args), ":min_hard_comp=%f", audio_drift_threshold); | |
794 | if (!fg->reconfiguration) | |
795 | av_strlcatf(args, sizeof(args), ":first_pts=0"); | |
796 | AUTO_INSERT_FILTER_INPUT("-async", "aresample", args); | |
797 | } | |
798 | ||
799 | // if (ost->audio_channels_mapped) { | |
800 | // int i; | |
801 | // AVBPrint pan_buf; | |
802 | // av_bprint_init(&pan_buf, 256, 8192); | |
803 | // av_bprintf(&pan_buf, "0x%"PRIx64, | |
804 | // av_get_default_channel_layout(ost->audio_channels_mapped)); | |
805 | // for (i = 0; i < ost->audio_channels_mapped; i++) | |
806 | // if (ost->audio_channels_map[i] != -1) | |
807 | // av_bprintf(&pan_buf, ":c%d=c%d", i, ost->audio_channels_map[i]); | |
808 | // AUTO_INSERT_FILTER_INPUT("-map_channel", "pan", pan_buf.str); | |
809 | // av_bprint_finalize(&pan_buf, NULL); | |
810 | // } | |
811 | ||
812 | if (audio_volume != 256) { | |
813 | char args[256]; | |
814 | ||
815 | av_log(NULL, AV_LOG_WARNING, "-vol has been deprecated. Use the volume " | |
816 | "audio filter instead.\n"); | |
817 | ||
818 | snprintf(args, sizeof(args), "%f", audio_volume / 256.); | |
819 | AUTO_INSERT_FILTER_INPUT("-vol", "volume", args); | |
820 | } | |
821 | ||
822 | snprintf(name, sizeof(name), "trim for input stream %d:%d", | |
823 | ist->file_index, ist->st->index); | |
f6fa7814 DM |
824 | if (copy_ts) { |
825 | tsoffset = f->start_time == AV_NOPTS_VALUE ? 0 : f->start_time; | |
826 | if (!start_at_zero && f->ctx->start_time != AV_NOPTS_VALUE) | |
827 | tsoffset += f->ctx->start_time; | |
828 | } | |
2ba45a60 | 829 | ret = insert_trim(((f->start_time == AV_NOPTS_VALUE) || !f->accurate_seek) ? |
f6fa7814 DM |
830 | AV_NOPTS_VALUE : tsoffset, f->recording_time, |
831 | &last_filter, &pad_idx, name); | |
2ba45a60 DM |
832 | if (ret < 0) |
833 | return ret; | |
834 | ||
835 | if ((ret = avfilter_link(last_filter, 0, in->filter_ctx, in->pad_idx)) < 0) | |
836 | return ret; | |
837 | ||
838 | return 0; | |
839 | } | |
840 | ||
841 | static int configure_input_filter(FilterGraph *fg, InputFilter *ifilter, | |
842 | AVFilterInOut *in) | |
843 | { | |
844 | av_freep(&ifilter->name); | |
845 | DESCRIBE_FILTER_LINK(ifilter, in, 1); | |
846 | ||
847 | if (!ifilter->ist->dec) { | |
848 | av_log(NULL, AV_LOG_ERROR, | |
849 | "No decoder for stream #%d:%d, filtering impossible\n", | |
850 | ifilter->ist->file_index, ifilter->ist->st->index); | |
851 | return AVERROR_DECODER_NOT_FOUND; | |
852 | } | |
853 | switch (avfilter_pad_get_type(in->filter_ctx->input_pads, in->pad_idx)) { | |
854 | case AVMEDIA_TYPE_VIDEO: return configure_input_video_filter(fg, ifilter, in); | |
855 | case AVMEDIA_TYPE_AUDIO: return configure_input_audio_filter(fg, ifilter, in); | |
856 | default: av_assert0(0); | |
857 | } | |
858 | } | |
859 | ||
860 | int configure_filtergraph(FilterGraph *fg) | |
861 | { | |
862 | AVFilterInOut *inputs, *outputs, *cur; | |
863 | int ret, i, init = !fg->graph, simple = !fg->graph_desc; | |
864 | const char *graph_desc = simple ? fg->outputs[0]->ost->avfilter : | |
865 | fg->graph_desc; | |
866 | ||
867 | avfilter_graph_free(&fg->graph); | |
868 | if (!(fg->graph = avfilter_graph_alloc())) | |
869 | return AVERROR(ENOMEM); | |
870 | ||
871 | if (simple) { | |
872 | OutputStream *ost = fg->outputs[0]->ost; | |
873 | char args[512]; | |
874 | AVDictionaryEntry *e = NULL; | |
875 | ||
876 | snprintf(args, sizeof(args), "flags=0x%X", (unsigned)ost->sws_flags); | |
877 | fg->graph->scale_sws_opts = av_strdup(args); | |
878 | ||
879 | args[0] = 0; | |
880 | while ((e = av_dict_get(ost->swr_opts, "", e, | |
881 | AV_DICT_IGNORE_SUFFIX))) { | |
882 | av_strlcatf(args, sizeof(args), "%s=%s:", e->key, e->value); | |
883 | } | |
884 | if (strlen(args)) | |
885 | args[strlen(args)-1] = 0; | |
886 | av_opt_set(fg->graph, "aresample_swr_opts", args, 0); | |
887 | ||
888 | args[0] = '\0'; | |
889 | while ((e = av_dict_get(fg->outputs[0]->ost->resample_opts, "", e, | |
890 | AV_DICT_IGNORE_SUFFIX))) { | |
891 | av_strlcatf(args, sizeof(args), "%s=%s:", e->key, e->value); | |
892 | } | |
893 | if (strlen(args)) | |
894 | args[strlen(args) - 1] = '\0'; | |
895 | fg->graph->resample_lavr_opts = av_strdup(args); | |
896 | ||
897 | e = av_dict_get(ost->encoder_opts, "threads", NULL, 0); | |
898 | if (e) | |
899 | av_opt_set(fg->graph, "threads", e->value, 0); | |
900 | } | |
901 | ||
902 | if ((ret = avfilter_graph_parse2(fg->graph, graph_desc, &inputs, &outputs)) < 0) | |
903 | return ret; | |
904 | ||
905 | if (simple && (!inputs || inputs->next || !outputs || outputs->next)) { | |
906 | av_log(NULL, AV_LOG_ERROR, "Simple filtergraph '%s' does not have " | |
907 | "exactly one input and output.\n", graph_desc); | |
908 | return AVERROR(EINVAL); | |
909 | } | |
910 | ||
911 | for (cur = inputs; !simple && init && cur; cur = cur->next) | |
912 | init_input_filter(fg, cur); | |
913 | ||
914 | for (cur = inputs, i = 0; cur; cur = cur->next, i++) | |
915 | if ((ret = configure_input_filter(fg, fg->inputs[i], cur)) < 0) { | |
916 | avfilter_inout_free(&inputs); | |
917 | avfilter_inout_free(&outputs); | |
918 | return ret; | |
919 | } | |
920 | avfilter_inout_free(&inputs); | |
921 | ||
922 | if (!init || simple) { | |
923 | /* we already know the mappings between lavfi outputs and output streams, | |
924 | * so we can finish the setup */ | |
925 | for (cur = outputs, i = 0; cur; cur = cur->next, i++) | |
926 | configure_output_filter(fg, fg->outputs[i], cur); | |
927 | avfilter_inout_free(&outputs); | |
928 | ||
929 | if ((ret = avfilter_graph_config(fg->graph, NULL)) < 0) | |
930 | return ret; | |
931 | } else { | |
932 | /* wait until output mappings are processed */ | |
933 | for (cur = outputs; cur;) { | |
934 | GROW_ARRAY(fg->outputs, fg->nb_outputs); | |
935 | if (!(fg->outputs[fg->nb_outputs - 1] = av_mallocz(sizeof(*fg->outputs[0])))) | |
936 | exit_program(1); | |
937 | fg->outputs[fg->nb_outputs - 1]->graph = fg; | |
938 | fg->outputs[fg->nb_outputs - 1]->out_tmp = cur; | |
939 | cur = cur->next; | |
940 | fg->outputs[fg->nb_outputs - 1]->out_tmp->next = NULL; | |
941 | } | |
942 | } | |
943 | ||
944 | fg->reconfiguration = 1; | |
945 | ||
946 | for (i = 0; i < fg->nb_outputs; i++) { | |
947 | OutputStream *ost = fg->outputs[i]->ost; | |
948 | if (ost && | |
949 | ost->enc->type == AVMEDIA_TYPE_AUDIO && | |
950 | !(ost->enc->capabilities & CODEC_CAP_VARIABLE_FRAME_SIZE)) | |
951 | av_buffersink_set_frame_size(ost->filter->filter, | |
952 | ost->enc_ctx->frame_size); | |
953 | } | |
954 | ||
955 | return 0; | |
956 | } | |
957 | ||
958 | int ist_in_filtergraph(FilterGraph *fg, InputStream *ist) | |
959 | { | |
960 | int i; | |
961 | for (i = 0; i < fg->nb_inputs; i++) | |
962 | if (fg->inputs[i]->ist == ist) | |
963 | return 1; | |
964 | return 0; | |
965 | } | |
966 |