transcode_init()函数介绍
来源:互联网 发布:au录音软件win10 编辑:程序博客网 时间:2024/05/29 12:33
transcode_init()
ffmpeg版本是3.1.3
此函数是为转码做初始化。设置编码参数,打开所有输出流的编码器,打开所有输入流的解码器,写入所有输出文件的文件头,具体看注释
static int transcode_init(void){ int ret = 0, i, j, k; AVFormatContext *oc; OutputStream *ost; InputStream *ist; char error[1024] = {0}; int want_sdp = 1; //大致先解释一下,不一定对。 /*此处是处理在命令行中设置的滤镜,最终目的是对ofilter->ost->source_index进行设置 也就是对输出流针对的输入流设置*/ for (i = 0; i < nb_filtergraphs; i++) { FilterGraph *fg = filtergraphs[i]; //循环所有输出流 for (j = 0; j < fg->nb_outputs; j++) { OutputFilter *ofilter = fg->outputs[j]; //如果已经有值就看下一个输出流 if (!ofilter->ost || ofilter->ost->source_index >= 0) continue; if (fg->nb_inputs != 1) continue; for (k = nb_input_streams-1; k >= 0 ; k--) if (fg->inputs[0]->ist == input_streams[k]) break; ofilter->ost->source_index = k; } } /* init framerate emulation */ /*对帧率仿真做初始化,循环所有输入文件,和所有输入流*/ for (i = 0; i < nb_input_files; i++) { InputFile *ifile = input_files[i]; if (ifile->rate_emu) //ifile->ist_index是本文件的流在input_streams中的第一个流的下标,在open_input_file里面设置 for (j = 0; j < ifile->nb_streams; j++) //设置此流的开始时间 input_streams[j + ifile->ist_index]->start = av_gettime_relative(); } /* for each output stream, we compute the right encoding parameters */ for (i = 0; i < nb_output_streams; i++) { AVCodecContext *enc_ctx; AVCodecContext *dec_ctx = NULL; ost = output_streams[i]; oc = output_files[ost->file_index]->ctx; //从input_streams中将输出流对应的输入流找到,对应关系在new_output_stream中传参设置 ist = get_input_stream(ost); //如果输出是attachment类型就不处理。这个还不是很清楚。 if (ost->attachment_filename) continue; //看是否流拷贝对enc_ctx赋值不同的编码器 enc_ctx = ost->stream_copy ? ost->st->codec : ost->enc_ctx; if (ist) { //找到了对应的输入流,对一些参数进行拷贝 dec_ctx = ist->dec_ctx; ost->st->disposition = ist->st->disposition; //对应参数bits_per_raw_sample,在new_video_stream()中设置 enc_ctx->bits_per_raw_sample = dec_ctx->bits_per_raw_sample; enc_ctx->chroma_sample_location = dec_ctx->chroma_sample_location; } else { for (j=0; j<oc->nb_streams; j++) { AVStream *st = oc->streams[j]; if (st != ost->st && st->codec->codec_type == enc_ctx->codec_type) break; } if (j == oc->nb_streams) if (enc_ctx->codec_type == AVMEDIA_TYPE_AUDIO || enc_ctx->codec_type == AVMEDIA_TYPE_VIDEO) ost->st->disposition = AV_DISPOSITION_DEFAULT; } //如果只是复制一个流(不用解码后再编码),则把输入流的编码参数直接复制给输出流 //此时是不需要解码也不需要编码的,所以不需打开解码器和编码器 if (ost->stream_copy) { AVRational sar; uint64_t extra_size; av_assert0(ist && !ost->filter); //计算输出流的编解码器的extradata的大小,然后分配容纳extradata的缓冲 //加一个AV_INPUT_BUFFER_PADDING_SIZE的大小,好像是因为四字节对齐的读入方式,防止读多了 extra_size = (uint64_t)dec_ctx->extradata_size + AV_INPUT_BUFFER_PADDING_SIZE; if (extra_size > INT_MAX) { return AVERROR(EINVAL); } /* if stream_copy is selected, no need to decode or encode */ enc_ctx->codec_id = dec_ctx->codec_id;//编码方式h264,mpeg4 enc_ctx->codec_type = dec_ctx->codec_type;//编码类型:视频/音频 //对应参数"tag" if (!enc_ctx->codec_tag) { unsigned int codec_tag; if (!oc->oformat->codec_tag || av_codec_get_id (oc->oformat->codec_tag, dec_ctx->codec_tag) == enc_ctx->codec_id || !av_codec_get_tag2(oc->oformat->codec_tag, dec_ctx->codec_id, &codec_tag)) enc_ctx->codec_tag = dec_ctx->codec_tag; } enc_ctx->bit_rate = dec_ctx->bit_rate;//平均码率 enc_ctx->rc_max_rate = dec_ctx->rc_max_rate;//最大码率,rc_min_rate为最小码率 enc_ctx->rc_buffer_size = dec_ctx->rc_buffer_size;//decoder bitstream buffer size enc_ctx->field_order = dec_ctx->field_order;//场序,顶场和底场谁先编码谁先显示的问题。不是很懂 if (dec_ctx->extradata_size) { enc_ctx->extradata = av_mallocz(extra_size); if (!enc_ctx->extradata) { return AVERROR(ENOMEM); } //然后把输入流的编解码器的extradata复制到输出流的编解码器中 memcpy(enc_ctx->extradata, dec_ctx->extradata, dec_ctx->extradata_size); } enc_ctx->extradata_size= dec_ctx->extradata_size; enc_ctx->bits_per_coded_sample = dec_ctx->bits_per_coded_sample; //帧率 enc_ctx->time_base = ist->st->time_base; /* * Avi is a special case here because it supports variable fps but * having the fps and timebase differe significantly adds quite some * overhead */ //copy_tb对应参数"copytb","copy input stream time base when stream copying" if(!strcmp(oc->oformat->name, "avi")) { if ( copy_tb<0 && ist->st->r_frame_rate.num && av_q2d(ist->st->r_frame_rate) >= av_q2d(ist->st->avg_frame_rate) && 0.5/av_q2d(ist->st->r_frame_rate) > av_q2d(ist->st->time_base) && 0.5/av_q2d(ist->st->r_frame_rate) > av_q2d(dec_ctx->time_base) && av_q2d(ist->st->time_base) < 1.0/500 && av_q2d(dec_ctx->time_base) < 1.0/500 || copy_tb==2){ enc_ctx->time_base.num = ist->st->r_frame_rate.den; enc_ctx->time_base.den = 2*ist->st->r_frame_rate.num; enc_ctx->ticks_per_frame = 2; } else if ( copy_tb<0 && av_q2d(dec_ctx->time_base)*dec_ctx->ticks_per_frame > 2*av_q2d(ist->st->time_base) && av_q2d(ist->st->time_base) < 1.0/500 || copy_tb==0){ enc_ctx->time_base = dec_ctx->time_base; enc_ctx->time_base.num *= dec_ctx->ticks_per_frame; enc_ctx->time_base.den *= 2; enc_ctx->ticks_per_frame = 2; } } else if(!(oc->oformat->flags & AVFMT_VARIABLE_FPS) && strcmp(oc->oformat->name, "mov") && strcmp(oc->oformat->name, "mp4") && strcmp(oc->oformat->name, "3gp") && strcmp(oc->oformat->name, "3g2") && strcmp(oc->oformat->name, "psp") && strcmp(oc->oformat->name, "ipod") && strcmp(oc->oformat->name, "f4v") ) { if( copy_tb<0 && dec_ctx->time_base.den && av_q2d(dec_ctx->time_base)*dec_ctx->ticks_per_frame > av_q2d(ist->st->time_base) && av_q2d(ist->st->time_base) < 1.0/500 || copy_tb==0){ enc_ctx->time_base = dec_ctx->time_base; enc_ctx->time_base.num *= dec_ctx->ticks_per_frame; } } if ( enc_ctx->codec_tag == AV_RL32("tmcd") && dec_ctx->time_base.num < dec_ctx->time_base.den && dec_ctx->time_base.num > 0 && 121LL*dec_ctx->time_base.num > dec_ctx->time_base.den) { enc_ctx->time_base = dec_ctx->time_base; } //如果命令行没设置帧率就用输入流的 if (!ost->frame_rate.num) ost->frame_rate = ist->framerate; //帧率取反就是time_base if(ost->frame_rate.num) enc_ctx->time_base = av_inv_q(ost->frame_rate); //再修正一下帧率 av_reduce(&enc_ctx->time_base.num, &enc_ctx->time_base.den, enc_ctx->time_base.num, enc_ctx->time_base.den, INT_MAX); //拷贝side_data if (ist->st->nb_side_data) { ost->st->side_data = av_realloc_array(NULL, ist->st->nb_side_data, sizeof(*ist->st->side_data)); if (!ost->st->side_data) return AVERROR(ENOMEM); ost->st->nb_side_data = 0; for (j = 0; j < ist->st->nb_side_data; j++) { const AVPacketSideData *sd_src = &ist->st->side_data[j]; AVPacketSideData *sd_dst = &ost->st->side_data[ost->st->nb_side_data]; if (ost->rotate_overridden && sd_src->type == AV_PKT_DATA_DISPLAYMATRIX) continue; sd_dst->data = av_malloc(sd_src->size); if (!sd_dst->data) return AVERROR(ENOMEM); memcpy(sd_dst->data, sd_src->data, sd_src->size); sd_dst->size = sd_src->size; sd_dst->type = sd_src->type; ost->st->nb_side_data++; } } ost->parser = av_parser_init(enc_ctx->codec_id); //针对不同类型的流做拷贝。 switch (enc_ctx->codec_type) { case AVMEDIA_TYPE_AUDIO: if (audio_volume != 256) { av_log(NULL, AV_LOG_FATAL, "-acodec copy and -vol are incompatible (frames are not decoded)\n"); exit_program(1); } enc_ctx->channel_layout = dec_ctx->channel_layout; enc_ctx->sample_rate = dec_ctx->sample_rate; enc_ctx->channels = dec_ctx->channels; enc_ctx->frame_size = dec_ctx->frame_size; enc_ctx->audio_service_type = dec_ctx->audio_service_type; enc_ctx->block_align = dec_ctx->block_align; enc_ctx->initial_padding = dec_ctx->delay; enc_ctx->profile = dec_ctx->profile;#if FF_API_AUDIOENC_DELAY enc_ctx->delay = dec_ctx->delay;#endif if((enc_ctx->block_align == 1 || enc_ctx->block_align == 1152 || enc_ctx->block_align == 576) && enc_ctx->codec_id == AV_CODEC_ID_MP3) enc_ctx->block_align= 0; if(enc_ctx->codec_id == AV_CODEC_ID_AC3) enc_ctx->block_align= 0; break; case AVMEDIA_TYPE_VIDEO: enc_ctx->pix_fmt = dec_ctx->pix_fmt; enc_ctx->colorspace = dec_ctx->colorspace; enc_ctx->color_range = dec_ctx->color_range; enc_ctx->color_primaries = dec_ctx->color_primaries; enc_ctx->color_trc = dec_ctx->color_trc; enc_ctx->width = dec_ctx->width; enc_ctx->height = dec_ctx->height; enc_ctx->has_b_frames = dec_ctx->has_b_frames; if (ost->frame_aspect_ratio.num) { // overridden by the -aspect cli option sar = av_mul_q(ost->frame_aspect_ratio, (AVRational){ enc_ctx->height, enc_ctx->width }); av_log(NULL, AV_LOG_WARNING, "Overriding aspect ratio " "with stream copy may produce invalid files\n"); } else if (ist->st->sample_aspect_ratio.num) sar = ist->st->sample_aspect_ratio; else sar = dec_ctx->sample_aspect_ratio; ost->st->sample_aspect_ratio = enc_ctx->sample_aspect_ratio = sar; ost->st->avg_frame_rate = ist->st->avg_frame_rate; ost->st->r_frame_rate = ist->st->r_frame_rate; break; case AVMEDIA_TYPE_SUBTITLE: enc_ctx->width = dec_ctx->width; enc_ctx->height = dec_ctx->height; break; case AVMEDIA_TYPE_UNKNOWN: case AVMEDIA_TYPE_DATA: case AVMEDIA_TYPE_ATTACHMENT: break; default: abort(); } } else {//不是流拷贝时,需要重新解码,编码 if (!ost->enc) ost->enc = avcodec_find_encoder(enc_ctx->codec_id);//找到编码器 if (!ost->enc) { /* should only happen when a default codec is not present. */ snprintf(error, sizeof(error), "Encoder (codec %s) not found for output stream #%d:%d", avcodec_get_name(ost->st->codec->codec_id), ost->file_index, ost->index); ret = AVERROR(EINVAL); goto dump_format; } //在ost->st->metadata中设置编码器的信息 set_encoder_id(output_files[ost->file_index], ost);#if CONFIG_LIBMFX if (qsv_transcode_init(ost)) exit_program(1);#endif#if CONFIG_CUVID if (cuvid_transcode_init(ost)) exit_program(1);#endif //在没有设置ost->filter时,命令行没有对滤镜的复杂设置,可见是二选一的。 if (!ost->filter && (enc_ctx->codec_type == AVMEDIA_TYPE_VIDEO || enc_ctx->codec_type == AVMEDIA_TYPE_AUDIO)) { FilterGraph *fg; fg = init_simple_filtergraph(ist, ost);//后面详细介绍 if (configure_filtergraph(fg)) {//后面详细介绍 av_log(NULL, AV_LOG_FATAL, "Error opening filters!\n"); exit_program(1); } } //如果使视频,就对帧率做处理。 if (enc_ctx->codec_type == AVMEDIA_TYPE_VIDEO) { //首先想办法获得一个帧率 if (!ost->frame_rate.num) ost->frame_rate = av_buffersink_get_frame_rate(ost->filter->filter); if (ist && !ost->frame_rate.num) ost->frame_rate = ist->framerate; if (ist && !ost->frame_rate.num) ost->frame_rate = ist->st->r_frame_rate; if (ist && !ost->frame_rate.num) { //默认帧率 ost->frame_rate = (AVRational){25, 1}; av_log(NULL, AV_LOG_WARNING, "No information " "about the input framerate is available. Falling " "back to a default value of 25fps for output stream #%d:%d. Use the -r option " "if you want a different framerate.\n", ost->file_index, ost->index); }// ost->frame_rate = ist->st->avg_frame_rate.num ? ist->st->avg_frame_rate : (AVRational){25, 1}; //根据编码器所支持的帧率找到最接近的 if (ost->enc && ost->enc->supported_framerates && !ost->force_fps) { int idx = av_find_nearest_q_idx(ost->frame_rate, ost->enc->supported_framerates); ost->frame_rate = ost->enc->supported_framerates[idx]; } // reduce frame rate for mpeg4 to be within the spec limits //如果编码器是mpeg4,帧率有限制,单独设置。 if (enc_ctx->codec_id == AV_CODEC_ID_MPEG4) { av_reduce(&ost->frame_rate.num, &ost->frame_rate.den, ost->frame_rate.num, ost->frame_rate.den, 65535); } } //对一些参数的值进行拷贝。设置到enc_ctx中 switch (enc_ctx->codec_type) { case AVMEDIA_TYPE_AUDIO: enc_ctx->sample_fmt = ost->filter->filter->inputs[0]->format; enc_ctx->sample_rate = ost->filter->filter->inputs[0]->sample_rate; enc_ctx->channel_layout = ost->filter->filter->inputs[0]->channel_layout; enc_ctx->channels = avfilter_link_get_channels(ost->filter->filter->inputs[0]); enc_ctx->time_base = (AVRational){ 1, enc_ctx->sample_rate }; break; case AVMEDIA_TYPE_VIDEO: enc_ctx->time_base = av_inv_q(ost->frame_rate); if (!(enc_ctx->time_base.num && enc_ctx->time_base.den)) enc_ctx->time_base = ost->filter->filter->inputs[0]->time_base; if ( av_q2d(enc_ctx->time_base) < 0.001 && video_sync_method != VSYNC_PASSTHROUGH && (video_sync_method == VSYNC_CFR || video_sync_method == VSYNC_VSCFR || (video_sync_method == VSYNC_AUTO && !(oc->oformat->flags & AVFMT_VARIABLE_FPS)))){ av_log(oc, AV_LOG_WARNING, "Frame rate very high for a muxer not efficiently supporting it.\n" "Please consider specifying a lower framerate, a different muxer or -vsync 2\n"); } for (j = 0; j < ost->forced_kf_count; j++) ost->forced_kf_pts[j] = av_rescale_q(ost->forced_kf_pts[j], AV_TIME_BASE_Q, enc_ctx->time_base); enc_ctx->width = ost->filter->filter->inputs[0]->w; enc_ctx->height = ost->filter->filter->inputs[0]->h; enc_ctx->sample_aspect_ratio = ost->st->sample_aspect_ratio = ost->frame_aspect_ratio.num ? // overridden by the -aspect cli option av_mul_q(ost->frame_aspect_ratio, (AVRational){ enc_ctx->height, enc_ctx->width }) : ost->filter->filter->inputs[0]->sample_aspect_ratio; if (!strncmp(ost->enc->name, "libx264", 7) && enc_ctx->pix_fmt == AV_PIX_FMT_NONE && ost->filter->filter->inputs[0]->format != AV_PIX_FMT_YUV420P) av_log(NULL, AV_LOG_WARNING, "No pixel format specified, %s for H.264 encoding chosen.\n" "Use -pix_fmt yuv420p for compatibility with outdated media players.\n", av_get_pix_fmt_name(ost->filter->filter->inputs[0]->format)); if (!strncmp(ost->enc->name, "mpeg2video", 10) && enc_ctx->pix_fmt == AV_PIX_FMT_NONE && ost->filter->filter->inputs[0]->format != AV_PIX_FMT_YUV420P) av_log(NULL, AV_LOG_WARNING, "No pixel format specified, %s for MPEG-2 encoding chosen.\n" "Use -pix_fmt yuv420p for compatibility with outdated media players.\n", av_get_pix_fmt_name(ost->filter->filter->inputs[0]->format)); enc_ctx->pix_fmt = ost->filter->filter->inputs[0]->format; ost->st->avg_frame_rate = ost->frame_rate; //frame_bits_per_raw_sample,new_video_stream()中有介绍 if (!dec_ctx || enc_ctx->width != dec_ctx->width || enc_ctx->height != dec_ctx->height || enc_ctx->pix_fmt != dec_ctx->pix_fmt) { enc_ctx->bits_per_raw_sample = frame_bits_per_raw_sample; } //在指定的时间戳强制关键帧,在ost->forced_keyframes,new_video_stream()中有介绍 if (ost->forced_keyframes) { if (!strncmp(ost->forced_keyframes, "expr:", 5)) { ret = av_expr_parse(&ost->forced_keyframes_pexpr, ost->forced_keyframes+5, forced_keyframes_const_names, NULL, NULL, NULL, NULL, 0, NULL); if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "Invalid force_key_frames expression '%s'\n", ost->forced_keyframes+5); return ret; } ost->forced_keyframes_expr_const_values[FKF_N] = 0; ost->forced_keyframes_expr_const_values[FKF_N_FORCED] = 0; ost->forced_keyframes_expr_const_values[FKF_PREV_FORCED_N] = NAN; ost->forced_keyframes_expr_const_values[FKF_PREV_FORCED_T] = NAN; // Don't parse the 'forced_keyframes' in case of 'keep-source-keyframes', // parse it only for static kf timings } else if(strncmp(ost->forced_keyframes, "source", 6)) { parse_forced_key_frames(ost->forced_keyframes, ost, ost->enc_ctx); } } break; case AVMEDIA_TYPE_SUBTITLE: enc_ctx->time_base = (AVRational){1, 1000}; if (!enc_ctx->width) { enc_ctx->width = input_streams[ost->source_index]->st->codec->width; enc_ctx->height = input_streams[ost->source_index]->st->codec->height; } break; case AVMEDIA_TYPE_DATA: break; default: abort(); break; } } //设置了disposition参数的情况下做如下操作。存疑 if (ost->disposition) { static const AVOption opts[] = { { "disposition" , NULL, 0, AV_OPT_TYPE_FLAGS, { .i64 = 0 }, INT64_MIN, INT64_MAX, .unit = "flags" }, { "default" , NULL, 0, AV_OPT_TYPE_CONST, { .i64 = AV_DISPOSITION_DEFAULT }, .unit = "flags" }, { "dub" , NULL, 0, AV_OPT_TYPE_CONST, { .i64 = AV_DISPOSITION_DUB }, .unit = "flags" }, { "original" , NULL, 0, AV_OPT_TYPE_CONST, { .i64 = AV_DISPOSITION_ORIGINAL }, .unit = "flags" }, { "comment" , NULL, 0, AV_OPT_TYPE_CONST, { .i64 = AV_DISPOSITION_COMMENT }, .unit = "flags" }, { "lyrics" , NULL, 0, AV_OPT_TYPE_CONST, { .i64 = AV_DISPOSITION_LYRICS }, .unit = "flags" }, { "karaoke" , NULL, 0, AV_OPT_TYPE_CONST, { .i64 = AV_DISPOSITION_KARAOKE }, .unit = "flags" }, { "forced" , NULL, 0, AV_OPT_TYPE_CONST, { .i64 = AV_DISPOSITION_FORCED }, .unit = "flags" }, { "hearing_impaired" , NULL, 0, AV_OPT_TYPE_CONST, { .i64 = AV_DISPOSITION_HEARING_IMPAIRED }, .unit = "flags" }, { "visual_impaired" , NULL, 0, AV_OPT_TYPE_CONST, { .i64 = AV_DISPOSITION_VISUAL_IMPAIRED }, .unit = "flags" }, { "clean_effects" , NULL, 0, AV_OPT_TYPE_CONST, { .i64 = AV_DISPOSITION_CLEAN_EFFECTS }, .unit = "flags" }, { "captions" , NULL, 0, AV_OPT_TYPE_CONST, { .i64 = AV_DISPOSITION_CAPTIONS }, .unit = "flags" }, { "descriptions" , NULL, 0, AV_OPT_TYPE_CONST, { .i64 = AV_DISPOSITION_DESCRIPTIONS }, .unit = "flags" }, { "metadata" , NULL, 0, AV_OPT_TYPE_CONST, { .i64 = AV_DISPOSITION_METADATA }, .unit = "flags" }, { NULL }, }; static const AVClass class = { .class_name = "", .item_name = av_default_item_name, .option = opts, .version = LIBAVUTIL_VERSION_INT, }; const AVClass *pclass = &class; ret = av_opt_eval_flags(&pclass, &opts[0], ost->disposition, &ost->st->disposition); if (ret < 0) goto dump_format; } } /* init input streams */ for (i = 0; i < nb_input_streams; i++) if ((ret = init_input_stream(i, error, sizeof(error))) < 0) {//后面详细介绍 for (i = 0; i < nb_output_streams; i++) { //出错关闭所有输入流对应的编码器 ost = output_streams[i]; avcodec_close(ost->enc_ctx); } goto dump_format; } /* open each encoder */ for (i = 0; i < nb_output_streams; i++) { ret = init_output_stream(output_streams[i], error, sizeof(error)); if (ret < 0) goto dump_format; } /* discard unused programs */ for (i = 0; i < nb_input_files; i++) { InputFile *ifile = input_files[i]; for (j = 0; j < ifile->ctx->nb_programs; j++) { AVProgram *p = ifile->ctx->programs[j]; int discard = AVDISCARD_ALL; for (k = 0; k < p->nb_stream_indexes; k++) if (!input_streams[ifile->ist_index + p->stream_index[k]]->discard) { discard = AVDISCARD_DEFAULT; break; } p->discard = discard; } } /* open files and write file headers */ for (i = 0; i < nb_output_files; i++) { oc = output_files[i]->ctx; oc->interrupt_callback = int_cb; if ((ret = avformat_write_header(oc, &output_files[i]->opts)) < 0) { snprintf(error, sizeof(error), "Could not write header for output file #%d " "(incorrect codec parameters ?): %s", i, av_err2str(ret)); ret = AVERROR(EINVAL); goto dump_format; }// assert_avoptions(output_files[i]->opts); if (strcmp(oc->oformat->name, "rtp")) { want_sdp = 0; } } dump_format: /* dump the file output parameters - cannot be done before in case of stream copy */ for (i = 0; i < nb_output_files; i++) { av_dump_format(output_files[i]->ctx, i, output_files[i]->ctx->filename, 1); } /* dump the stream mapping */ av_log(NULL, AV_LOG_INFO, "Stream mapping:\n"); for (i = 0; i < nb_input_streams; i++) { ist = input_streams[i]; for (j = 0; j < ist->nb_filters; j++) { if (ist->filters[j]->graph->graph_desc) { av_log(NULL, AV_LOG_INFO, " Stream #%d:%d (%s) -> %s", ist->file_index, ist->st->index, ist->dec ? ist->dec->name : "?", ist->filters[j]->name); if (nb_filtergraphs > 1) av_log(NULL, AV_LOG_INFO, " (graph %d)", ist->filters[j]->graph->index); av_log(NULL, AV_LOG_INFO, "\n"); } } } for (i = 0; i < nb_output_streams; i++) { ost = output_streams[i]; if (ost->attachment_filename) { /* an attached file */ av_log(NULL, AV_LOG_INFO, " File %s -> Stream #%d:%d\n", ost->attachment_filename, ost->file_index, ost->index); continue; } if (ost->filter && ost->filter->graph->graph_desc) { /* output from a complex graph */ av_log(NULL, AV_LOG_INFO, " %s", ost->filter->name); if (nb_filtergraphs > 1) av_log(NULL, AV_LOG_INFO, " (graph %d)", ost->filter->graph->index); av_log(NULL, AV_LOG_INFO, " -> Stream #%d:%d (%s)\n", ost->file_index, ost->index, ost->enc ? ost->enc->name : "?"); continue; } av_log(NULL, AV_LOG_INFO, " Stream #%d:%d -> #%d:%d", input_streams[ost->source_index]->file_index, input_streams[ost->source_index]->st->index, ost->file_index, ost->index); if (ost->sync_ist != input_streams[ost->source_index]) av_log(NULL, AV_LOG_INFO, " [sync #%d:%d]", ost->sync_ist->file_index, ost->sync_ist->st->index); if (ost->stream_copy) av_log(NULL, AV_LOG_INFO, " (copy)"); else { const AVCodec *in_codec = input_streams[ost->source_index]->dec; const AVCodec *out_codec = ost->enc; const char *decoder_name = "?"; const char *in_codec_name = "?"; const char *encoder_name = "?"; const char *out_codec_name = "?"; const AVCodecDescriptor *desc; if (in_codec) { decoder_name = in_codec->name; desc = avcodec_descriptor_get(in_codec->id); if (desc) in_codec_name = desc->name; if (!strcmp(decoder_name, in_codec_name)) decoder_name = "native"; } if (out_codec) { encoder_name = out_codec->name; desc = avcodec_descriptor_get(out_codec->id); if (desc) out_codec_name = desc->name; if (!strcmp(encoder_name, out_codec_name)) encoder_name = "native"; } av_log(NULL, AV_LOG_INFO, " (%s (%s) -> %s (%s))", in_codec_name, decoder_name, out_codec_name, encoder_name); } av_log(NULL, AV_LOG_INFO, "\n"); } if (ret) { av_log(NULL, AV_LOG_ERROR, "%s\n", error); return ret; } if (sdp_filename || want_sdp) { print_sdp(); } transcode_init_done = 1; return 0;}
简单滤镜涉及到的结构体
typedef struct InputFilter { AVFilterContext *filter; struct InputStream *ist; struct FilterGraph *graph; uint8_t *name;} InputFilter;typedef struct OutputFilter { AVFilterContext *filter; struct OutputStream *ost; struct FilterGraph *graph; uint8_t *name; /* temporary storage until stream maps are processed */ AVFilterInOut *out_tmp; enum AVMediaType type;} OutputFilter;typedef struct FilterGraph { int index; const char *graph_desc; AVFilterGraph *graph; int reconfiguration; InputFilter **inputs; int nb_inputs; OutputFilter **outputs; int nb_outputs;} FilterGraph;
init_simple_filtergraph()
绿线表示赋值的指向。
FilterGraph *init_simple_filtergraph(InputStream *ist, OutputStream *ost){ FilterGraph *fg = av_mallocz(sizeof(*fg)); if (!fg) exit_program(1); //针对一路流有一个fg,此句表示这个fg在全局链表filtergraphs中的下标 fg->index = nb_filtergraphs; //对输出流处理赋值 GROW_ARRAY(fg->outputs, fg->nb_outputs); if (!(fg->outputs[0] = av_mallocz(sizeof(*fg->outputs[0])))) exit_program(1); fg->outputs[0]->ost = ost; fg->outputs[0]->graph = fg; ost->filter = fg->outputs[0]; GROW_ARRAY(fg->inputs, fg->nb_inputs); if (!(fg->inputs[0] = av_mallocz(sizeof(*fg->inputs[0])))) exit_program(1); fg->inputs[0]->ist = ist; fg->inputs[0]->graph = fg; //是否因为多个输出流可以针对一个输入流所以输入流中有多个滤镜信息。这个需要再看,暂且这么理解 GROW_ARRAY(ist->filters, ist->nb_filters); ist->filters[ist->nb_filters - 1] = fg->inputs[0]; //把这个fg放到filtergraphs中 GROW_ARRAY(filtergraphs, nb_filtergraphs); filtergraphs[nb_filtergraphs - 1] = fg; return fg;}
configure_filtergraph()
int configure_filtergraph(FilterGraph *fg){ AVFilterInOut *inputs, *outputs, *cur; int ret, i, simple = !fg->graph_desc;//NULL时simple为1;否则为0; /*ost->avfilter在new_video_stream()中设置,视频为"null" ,音频为 "anull",或者命令行设置 */ const char *graph_desc = simple ? fg->outputs[0]->ost->avfilter : fg->graph_desc; //初始化fg->graph avfilter_graph_free(&fg->graph); if (!(fg->graph = avfilter_graph_alloc())) return AVERROR(ENOMEM); /*把命令行中设置到ost->sws_dict、ost->swr_opts、ost->resample_opts中的配置信息, 配置到fg->graph中 */ if (simple) { OutputStream *ost = fg->outputs[0]->ost;//在init_simple_filtergraph()中赋值了。 char args[512]; AVDictionaryEntry *e = NULL; args[0] = 0; /*ost->sws_dict->elems{key = "flags", value = "bicubic"}。这里至少有这个值。 它是图像放大的一种插值方法(双三次插值),应该是在放大分辨率的时候用*/ while ((e = av_dict_get(ost->sws_dict, "", e, AV_DICT_IGNORE_SUFFIX))) { av_strlcatf(args, sizeof(args), "%s=%s:", e->key, e->value); } if (strlen(args)) args[strlen(args)-1] = 0; fg->graph->scale_sws_opts = av_strdup(args); args[0] = 0; while ((e = av_dict_get(ost->swr_opts, "", e, AV_DICT_IGNORE_SUFFIX))) { av_strlcatf(args, sizeof(args), "%s=%s:", e->key, e->value); } if (strlen(args)) args[strlen(args)-1] = 0; av_opt_set(fg->graph, "aresample_swr_opts", args, 0); args[0] = '\0'; while ((e = av_dict_get(fg->outputs[0]->ost->resample_opts, "", e, AV_DICT_IGNORE_SUFFIX))) { av_strlcatf(args, sizeof(args), "%s=%s:", e->key, e->value); } if (strlen(args)) args[strlen(args) - 1] = '\0'; fg->graph->resample_lavr_opts = av_strdup(args); e = av_dict_get(ost->encoder_opts, "threads", NULL, 0); if (e) av_opt_set(fg->graph, "threads", e->value, 0); } if ((ret = avfilter_graph_parse2(fg->graph, graph_desc, &inputs, &outputs)) < 0) return ret; if (hw_device_ctx) { for (i = 0; i < fg->graph->nb_filters; i++) { fg->graph->filters[i]->hw_device_ctx = av_buffer_ref(hw_device_ctx); } } //对avfilter_graph_parse2()返回的inputs和outputs进行判断,并输出错误原因。 if (simple && (!inputs || inputs->next || !outputs || outputs->next)) { const char *num_inputs; const char *num_outputs; if (!outputs) { num_outputs = "0"; } else if (outputs->next) { num_outputs = ">1"; } else { num_outputs = "1"; } if (!inputs) { num_inputs = "0"; } else if (inputs->next) { num_inputs = ">1"; } else { num_inputs = "1"; } av_log(NULL, AV_LOG_ERROR, "Simple filtergraph '%s' was expected " "to have exactly 1 input and 1 output." " However, it had %s input(s) and %s output(s)." " Please adjust, or use a complex filtergraph (-filter_complex) instead.\n", graph_desc, num_inputs, num_outputs); return AVERROR(EINVAL); } //在simple为0时,inputs->next和outputs->next可能有值。 //配置输入滤镜 for (cur = inputs, i = 0; cur; cur = cur->next, i++) if ((ret = configure_input_filter(fg, fg->inputs[i], cur)) < 0) {//在后面有详细讨论 avfilter_inout_free(&inputs); avfilter_inout_free(&outputs); return ret; } avfilter_inout_free(&inputs); //配置输出滤镜 for (cur = outputs, i = 0; cur; cur = cur->next, i++) configure_output_filter(fg, fg->outputs[i], cur);//在后面有详细讨论 avfilter_inout_free(&outputs); if ((ret = avfilter_graph_config(fg->graph, NULL)) < 0) return ret; //只在configure_input_audio_filter中用过。用处存疑 fg->reconfiguration = 1; for (i = 0; i < fg->nb_outputs; i++) { OutputStream *ost = fg->outputs[i]->ost; if (!ost->enc) { /* identical to the same check in ffmpeg.c, needed because complex filter graphs are initialized earlier */ av_log(NULL, AV_LOG_ERROR, "Encoder (codec %s) not found for output stream #%d:%d\n", avcodec_get_name(ost->st->codec->codec_id), ost->file_index, ost->index); return AVERROR(EINVAL); } if (ost->enc->type == AVMEDIA_TYPE_AUDIO && !(ost->enc->capabilities & AV_CODEC_CAP_VARIABLE_FRAME_SIZE)) av_buffersink_set_frame_size(ost->filter->filter, ost->enc_ctx->frame_size); } return 0;}
configure_input_filter()
static int configure_input_filter(FilterGraph *fg, InputFilter *ifilter, AVFilterInOut *in){ av_freep(&ifilter->name); DESCRIBE_FILTER_LINK(ifilter, in, 1);//后面介绍 if (!ifilter->ist->dec) { av_log(NULL, AV_LOG_ERROR, "No decoder for stream #%d:%d, filtering impossible\n", ifilter->ist->file_index, ifilter->ist->st->index); return AVERROR_DECODER_NOT_FOUND; } switch (avfilter_pad_get_type(in->filter_ctx->input_pads, in->pad_idx)) { case AVMEDIA_TYPE_VIDEO: return configure_input_video_filter(fg, ifilter, in); case AVMEDIA_TYPE_AUDIO: return configure_input_audio_filter(fg, ifilter, in); default: av_assert0(0); }}
DESCRIBE_FILTER_LINK()
这个需要分析完滤镜的原理后回过头来再解释。存疑
#define DESCRIBE_FILTER_LINK(f, inout, in) \{ \ AVFilterContext *ctx = inout->filter_ctx; \ AVFilterPad *pads = in ? ctx->input_pads : ctx->output_pads; \ int nb_pads = in ? ctx->nb_inputs : ctx->nb_outputs; \ AVIOContext *pb; \ \ if (avio_open_dyn_buf(&pb) < 0) \ exit_program(1); \ \ avio_printf(pb, "%s", ctx->filter->name); \ if (nb_pads > 1) \ avio_printf(pb, ":%s", avfilter_pad_get_name(pads, inout->pad_idx));\ avio_w8(pb, 0); \ avio_close_dyn_buf(pb, &f->name); \}//例子DESCRIBE_FILTER_LINK(ifilter, in, 1);{ AVFilterContext *ctx = in->filter_ctx; \ AVFilterPad *pads = 1 ? ctx->input_pads : ctx->output_pads; \ int nb_pads = 1 ? ctx->nb_inputs : ctx->nb_outputs; \ AVIOContext *pb; \ \ if (avio_open_dyn_buf(&pb) < 0) \ exit_program(1); \ \ avio_printf(pb, "%s", ctx->filter->name); \ if (nb_pads > 1) \ avio_printf(pb, ":%s", avfilter_pad_get_name(pads, in->pad_idx));\ avio_w8(pb, 0); \ avio_close_dyn_buf(pb, &ifilter->name); }
configure_input_video_filter()
配置输入视频滤镜。
static int configure_input_video_filter(FilterGraph *fg, InputFilter *ifilter, AVFilterInOut *in){ AVFilterContext *last_filter; //buffer型滤镜定义在libavfilter\buffersrc.c中 const AVFilter *buffer_filt = avfilter_get_by_name("buffer"); //取输入流的基本信息 InputStream *ist = ifilter->ist; InputFile *f = input_files[ist->file_index]; AVRational tb = ist->framerate.num ? av_inv_q(ist->framerate) : ist->st->time_base; AVRational fr = ist->framerate; AVRational sar; AVBPrint args; char name[255]; int ret, pad_idx = 0; int64_t tsoffset = 0; //此结构体描述将要通过此滤镜的帧信息 AVBufferSrcParameters *par = av_buffersrc_parameters_alloc(); //对par初始化 if (!par) return AVERROR(ENOMEM); memset(par, 0, sizeof(*par)); par->format = AV_PIX_FMT_NONE; if (ist->dec_ctx->codec_type == AVMEDIA_TYPE_AUDIO) { av_log(NULL, AV_LOG_ERROR, "Cannot connect video filter to audio input\n"); ret = AVERROR(EINVAL); goto fail; } //如果输入流的帧率之前不知道的话,从这里获取,也许不准 if (!fr.num) fr = av_guess_frame_rate(input_files[ist->file_index]->ctx, ist->st, NULL); if (ist->dec_ctx->codec_type == AVMEDIA_TYPE_SUBTITLE) { //为字幕流转视频做准备 ret = sub2video_prepare(ist); if (ret < 0) goto fail; } //采样的纵横比 sar = ist->st->sample_aspect_ratio.num ? ist->st->sample_aspect_ratio : ist->dec_ctx->sample_aspect_ratio; if(!sar.den) sar = (AVRational){0,1}; //args是一个自增型的buf,不用担心越界。 av_bprint_init(&args, 0, 1); //组avfilter_graph_create_filter函数需要用到的字符串。 av_bprintf(&args, "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:" "pixel_aspect=%d/%d:sws_param=flags=%d", ist->resample_width, ist->resample_height, ist->hwaccel_retrieve_data ? ist->hwaccel_retrieved_pix_fmt : ist->resample_pix_fmt, tb.num, tb.den, sar.num, sar.den, SWS_BILINEAR + ((ist->dec_ctx->flags&AV_CODEC_FLAG_BITEXACT) ? SWS_BITEXACT:0)); if (fr.num && fr.den) av_bprintf(&args, ":frame_rate=%d/%d", fr.num, fr.den); snprintf(name, sizeof(name), "graph %d input from stream %d:%d", fg->index, ist->file_index, ist->st->index); if ((ret = avfilter_graph_create_filter(&ifilter->filter, buffer_filt, name, args.str, NULL, fg->graph)) < 0) goto fail; par->hw_frames_ctx = ist->hw_frames_ctx; ret = av_buffersrc_parameters_set(ifilter->filter, par); if (ret < 0) goto fail; av_freep(&par); last_filter = ifilter->filter; /*在add_input_streams()中设置。默认是1.init_options中设置的此值是0; "automatically insert correct rotate filters"自动插入正确的旋转过滤器 */ if (ist->autorotate) { double theta = get_rotation(ist->st); if (fabs(theta - 90) < 1.0) { ret = insert_filter(&last_filter, &pad_idx, "transpose", "clock"); } else if (fabs(theta - 180) < 1.0) { ret = insert_filter(&last_filter, &pad_idx, "hflip", NULL); if (ret < 0) return ret; ret = insert_filter(&last_filter, &pad_idx, "vflip", NULL); } else if (fabs(theta - 270) < 1.0) { ret = insert_filter(&last_filter, &pad_idx, "transpose", "cclock"); } else if (fabs(theta) > 1.0) { char rotate_buf[64]; snprintf(rotate_buf, sizeof(rotate_buf), "%f*PI/180", theta); ret = insert_filter(&last_filter, &pad_idx, "rotate", rotate_buf); } if (ret < 0) return ret; } //设置帧率 if (ist->framerate.num) { AVFilterContext *setpts; snprintf(name, sizeof(name), "force CFR for input from stream %d:%d", ist->file_index, ist->st->index); if ((ret = avfilter_graph_create_filter(&setpts, avfilter_get_by_name("setpts"), name, "N", NULL, fg->graph)) < 0) return ret; if ((ret = avfilter_link(last_filter, 0, setpts, 0)) < 0) return ret; last_filter = setpts; } //设置反交错滤镜,处理视频由于缩放导致的拉丝现象 if (do_deinterlace) { AVFilterContext *yadif; snprintf(name, sizeof(name), "deinterlace input from stream %d:%d", ist->file_index, ist->st->index); if ((ret = avfilter_graph_create_filter(&yadif, avfilter_get_by_name("yadif"), name, "", NULL, fg->graph)) < 0) return ret; if ((ret = avfilter_link(last_filter, 0, yadif, 0)) < 0) return ret; last_filter = yadif; } snprintf(name, sizeof(name), "trim for input stream %d:%d", ist->file_index, ist->st->index); //copy_ts对应参数"copyts","copy timestamps"; //start_at_zero对应参数"start_at_zero" if (copy_ts) { tsoffset = f->start_time == AV_NOPTS_VALUE ? 0 : f->start_time; if (!start_at_zero && f->ctx->start_time != AV_NOPTS_VALUE) tsoffset += f->ctx->start_time; } //根据传入的第一开始时间,第二参数时长,设置裁剪视频滤镜 ret = insert_trim(((f->start_time == AV_NOPTS_VALUE) || !f->accurate_seek) ? AV_NOPTS_VALUE : tsoffset, f->recording_time, &last_filter, &pad_idx, name); if (ret < 0) return ret; if ((ret = avfilter_link(last_filter, 0, in->filter_ctx, in->pad_idx)) < 0) return ret; return 0;fail: av_freep(&par); return ret;}
configure_output_filter
配置输出滤镜
int configure_output_filter(FilterGraph *fg, OutputFilter *ofilter, AVFilterInOut *out){ av_freep(&ofilter->name); DESCRIBE_FILTER_LINK(ofilter, out, 0); if (!ofilter->ost) { av_log(NULL, AV_LOG_FATAL, "Filter %s has an unconnected output\n", ofilter->name); exit_program(1); } switch (avfilter_pad_get_type(out->filter_ctx->output_pads, out->pad_idx)) { case AVMEDIA_TYPE_VIDEO: return configure_output_video_filter(fg, ofilter, out); case AVMEDIA_TYPE_AUDIO: return configure_output_audio_filter(fg, ofilter, out); default: av_assert0(0); }}
configure_output_video_filter
static int configure_output_video_filter(FilterGraph *fg, OutputFilter *ofilter, AVFilterInOut *out){ char *pix_fmts; OutputStream *ost = ofilter->ost; OutputFile *of = output_files[ost->file_index]; AVCodecContext *codec = ost->enc_ctx; AVFilterContext *last_filter = out->filter_ctx; int pad_idx = out->pad_idx; int ret; char name[255]; snprintf(name, sizeof(name), "output stream %d:%d", ost->file_index, ost->index); ret = avfilter_graph_create_filter(&ofilter->filter, avfilter_get_by_name("buffersink"), name, NULL, NULL, fg->graph); if (ret < 0) return ret; if (!hw_device_ctx && (codec->width || codec->height)) { char args[255]; AVFilterContext *filter; AVDictionaryEntry *e = NULL; //设置有关libswscale库参数的滤镜,修改视频的长宽就是用此滤镜 snprintf(args, sizeof(args), "%d:%d", codec->width, codec->height); //在cmdutils.c的opt_default()函数中判断为libswscale库相关参数被存放在ost->sws_dict中,逐个取出,待设置 while ((e = av_dict_get(ost->sws_dict, "", e, AV_DICT_IGNORE_SUFFIX))) { av_strlcatf(args, sizeof(args), ":%s=%s", e->key, e->value); } snprintf(name, sizeof(name), "scaler for output stream %d:%d", ost->file_index, ost->index); if ((ret = avfilter_graph_create_filter(&filter, avfilter_get_by_name("scale"), name, args, NULL, fg->graph)) < 0) return ret; if ((ret = avfilter_link(last_filter, pad_idx, filter, 0)) < 0) return ret; last_filter = filter; pad_idx = 0; } //设置像素格式4:2:0等 if ((pix_fmts = choose_pix_fmts(ost))) { AVFilterContext *filter; snprintf(name, sizeof(name), "pixel format for output stream %d:%d", ost->file_index, ost->index); ret = avfilter_graph_create_filter(&filter, avfilter_get_by_name("format"), "format", pix_fmts, NULL, fg->graph); av_freep(&pix_fmts); if (ret < 0) return ret; if ((ret = avfilter_link(last_filter, pad_idx, filter, 0)) < 0) return ret; last_filter = filter; pad_idx = 0; } //设置输出帧率 if (ost->frame_rate.num && 0) { AVFilterContext *fps; char args[255]; snprintf(args, sizeof(args), "fps=%d/%d", ost->frame_rate.num, ost->frame_rate.den); snprintf(name, sizeof(name), "fps for output stream %d:%d", ost->file_index, ost->index); ret = avfilter_graph_create_filter(&fps, avfilter_get_by_name("fps"), name, args, NULL, fg->graph); if (ret < 0) return ret; ret = avfilter_link(last_filter, pad_idx, fps, 0); if (ret < 0) return ret; last_filter = fps; pad_idx = 0; } //设置裁剪的时间。 snprintf(name, sizeof(name), "trim for output stream %d:%d", ost->file_index, ost->index); ret = insert_trim(of->start_time, of->recording_time, &last_filter, &pad_idx, name); if (ret < 0) return ret; if ((ret = avfilter_link(last_filter, pad_idx, ofilter->filter, 0)) < 0) return ret; return 0;}
init_input_stream
赋值读帧的回调函数,打开对应的解码器
static int init_input_stream(int ist_index, char *error, int error_len){ int ret; InputStream *ist = input_streams[ist_index]; if (ist->decoding_needed) { AVCodec *codec = ist->dec; if (!codec) { snprintf(error, error_len, "Decoder (codec %s) not found for input stream #%d:%d", avcodec_get_name(ist->dec_ctx->codec_id), ist->file_index, ist->st->index); return AVERROR(EINVAL); } ist->dec_ctx->opaque = ist; //用于协商一个像素格式。 ist->dec_ctx->get_format = get_format; //读取数据的回调函数,默认调用avcodec_default_get_buffer2() ist->dec_ctx->get_buffer2 = get_buffer; ist->dec_ctx->thread_safe_callbacks = 1; av_opt_set_int(ist->dec_ctx, "refcounted_frames", 1, 0); if (ist->dec_ctx->codec_id == AV_CODEC_ID_DVB_SUBTITLE && (ist->decoding_needed & DECODING_FOR_OST)) { av_dict_set(&ist->decoder_opts, "compute_edt", "1", AV_DICT_DONT_OVERWRITE); if (ist->decoding_needed & DECODING_FOR_FILTER) av_log(NULL, AV_LOG_WARNING, "Warning using DVB subtitles for filtering and output at the same time is not fully supported, also see -compute_edt [0|1]\n"); } av_dict_set(&ist->decoder_opts, "sub_text_format", "ass", AV_DICT_DONT_OVERWRITE); if (!av_dict_get(ist->decoder_opts, "threads", NULL, 0)) av_dict_set(&ist->decoder_opts, "threads", "auto", 0); //打开解码器 if ((ret = avcodec_open2(ist->dec_ctx, codec, &ist->decoder_opts)) < 0) { if (ret == AVERROR_EXPERIMENTAL) abort_codec_experimental(codec, 0); snprintf(error, error_len, "Error while opening decoder for input stream " "#%d:%d : %s", ist->file_index, ist->st->index, av_err2str(ret)); return ret; } assert_avoptions(ist->decoder_opts); } ist->next_pts = AV_NOPTS_VALUE; ist->next_dts = AV_NOPTS_VALUE; return 0;}
0 0
- transcode_init()函数介绍
- transcode_init()函数
- ffmpeg源码分析:transcode_init()函数
- 最新版ffmpeg源码分析三:transcode_init()函数
- 最新版ffmpeg源码分析三:transcode_init()函数
- ffmpeg源码分析三:transcode_init函数
- ffmpeg源码分析三:transcode_init函数 (转3)
- 【算法函数介绍】Hash函数介绍
- wave函数介绍
- waveOutWrite函数介绍
- waveOutOpen函数介绍
- oracle 日期函数介绍
- GetLastError()函数的介绍
- bioskey函数介绍
- oracle 日期函数介绍
- oracle 日期函数介绍
- Linux C 函数介绍
- CreateMutex函数介绍
- EventBus 3使用文档(一)
- Java设计模式之单例模式(Singleton Pattern)
- this指针 构造函数
- OpenCV 90°旋转
- linux nc命令相互发消息及传文件
- transcode_init()函数介绍
- CSS-字体
- 最小的应用(tkinter,Python3.x )
- 机器学习模型评价指标 -- 混淆矩阵
- 数据结构与算法之经典排序
- Codeforces 516A Drazil and Factorial【暴搜找规律+贪心】
- EventBus 3使用文档(二)
- 轻快的VIM(一):移动
- centos7 安装Nginx