ffmpeg RTMP 推流demo

 ffmpeg官方的转码例子transcoding.c不涉及mp4的转码。

本例推流包括两种方式推流:用mp4文件推流,或者通过dshow采集摄像头和麦克风的音视频流再推流。

详细注释已添加到代码中,后面会持续完善。

        待跟踪项:

        1.硬件编码(h264_nvenc)的支持

        2.声音/视频pts、dts未完全同步。

transcodingRtmpThread1.cpp 

#include "transcodingRtmpThread1.h"

RTMPClient::~RTMPClient()
{

    int i;
    for (i = 0; i < ifmt_ctx->nb_streams; i++)
    {
        avcodec_free_context(&scs[i].dec_ctx);
        if (ofmt_ctx && ofmt_ctx->nb_streams > i && ofmt_ctx->streams[i] && scs[i].enc_ctx)
            avcodec_free_context(&scs[i].enc_ctx);
        if (filter_ctx && filter_ctx[i].filter_graph)
        {
            avfilter_graph_free(&filter_ctx[i].filter_graph);
            av_packet_free(&filter_ctx[i].enc_pkt);
            av_frame_free(&filter_ctx[i].filtered_frame);
        }

        av_frame_free(&scs[i].dec_frame);
    }
    av_free(filter_ctx);
    av_free(scs);
    avformat_close_input(&ifmt_ctx);
    if (ofmt_ctx && !(ofmt_ctx->oformat->flags & AVFMT_NOFILE))
        avio_closep(&ofmt_ctx->pb);
    avformat_free_context(ofmt_ctx);
}

int RTMPClient::open_input_file()
{
    int ret;
    unsigned int i;
    AVDictionary *dictionary = NULL;
    AVInputFormat *ifmt = NULL;
    ifmt_ctx = avformat_alloc_context();
    avformat_network_init();
    av_dict_set_int(&dictionary, "rtbufsize", 3041280 * 100, 0);
#if USE_CAM
    avdevice_register_all();
     Logitech HD Webcam C525  Integrated Webcam
    const char *video = "video=Logitech HD Webcam C525:audio=麦克风 (HD Webcam C525)";
    ifmt = av_find_input_format("dshow");
    // av_dict_set(&dictionary, "video_size", "1280x720", 0);
    av_dict_set(&dictionary, "video_size", "1600*896", 0);
    // av_dict_set(&dictionary, "input_format", "mjpeg", 0);
    av_dict_set(&dictionary, "framerate", "30", 0);
    // ifmt_ctx->video_codec_id = AV_CODEC_ID_MJPEG;
    // av_format_set_video_codec(ifmt_ctx, opened_mjpeg_codec);
#else
    const char *video = "d:\\ss_1min.mp4";
#endif

    if ((ret = avformat_open_input(&ifmt_ctx, video, ifmt, &dictionary)) != 0)
    {
        printf("Couldn't open input videostream.\n");
        return -1;
    }

    if ((ret = avformat_find_stream_info(ifmt_ctx, NULL)) < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "Cannot find stream information\n");
        return ret;
    }

    scs = (StreamContext *)av_mallocz_array(ifmt_ctx->nb_streams, sizeof(*scs));
    if (!scs)
        return AVERROR(ENOMEM);

    for (i = 0; i < ifmt_ctx->nb_streams; i++)
    {
        AVStream *stream = ifmt_ctx->streams[i];
        AVCodec *dec = avcodec_find_decoder(stream->codecpar->codec_id);
        AVCodecContext *codec_ctx;
        if (!dec)
        {
            av_log(NULL, AV_LOG_ERROR, "Failed to find decoder for stream #%u\n", i);
            return AVERROR_DECODER_NOT_FOUND;
        }
        codec_ctx = avcodec_alloc_context3(dec);

        if (!codec_ctx)
        {
            av_log(NULL, AV_LOG_ERROR, "Failed to allocate the decoder context for stream #%u\n", i);
            return AVERROR(ENOMEM);
        }
        ret = avcodec_parameters_to_context(codec_ctx, stream->codecpar);
        if (ret < 0)
        {
            av_log(NULL, AV_LOG_ERROR, "Failed copy decoder parameters to  dec_context for stream #%u\n", i);
            return ret;
        }
        /* Reencode video & audio and remux subtitles etc. */
        if (codec_ctx->codec_type == AVMEDIA_TYPE_VIDEO || codec_ctx->codec_type == AVMEDIA_TYPE_AUDIO)
        {
            if (codec_ctx->codec_type == AVMEDIA_TYPE_VIDEO)
                codec_ctx->framerate = av_guess_frame_rate(ifmt_ctx, stream, NULL);
            /* Open decoder */
            ret = avcodec_open2(codec_ctx, dec, NULL);
            if (ret < 0)
            {
                av_log(NULL, AV_LOG_ERROR, "Failed to open decoder for stream #%u\n", i);
                return ret;
            }
        }
        scs[i].dec_ctx = codec_ctx;
        scs[i].dec_frame = av_frame_alloc();
        if (!scs[i].dec_frame)
            return AVERROR(ENOMEM);
    }

    av_dump_format(ifmt_ctx, 0, video, 0);
    return 0;
}

int RTMPClient::open_output_file()
{

    int ret;
    unsigned int i;

    ofmt_ctx = NULL;

#if USE_RTMP
    const char *outUrl = "rtmp://127.0.0.1:9014/live/logic";
#else
    const char *outUrl = "D:\\ss_10min_out.flv";
#endif
    avformat_alloc_output_context2(&ofmt_ctx, NULL, "flv", outUrl);
    if (!ofmt_ctx)
    {
        av_log(NULL, AV_LOG_ERROR, "Could not create output context\n");
        return AVERROR_UNKNOWN;
    }

    for (i = 0; i < ifmt_ctx->nb_streams; i++)
    {
        AVStream *out_stream;
        AVStream *in_stream;
        AVCodecContext *dec_ctx, *enc_ctx;
        AVCodec *encoder;
        in_stream = ifmt_ctx->streams[i];
        dec_ctx = scs[i].dec_ctx;

        if (dec_ctx->codec_type == AVMEDIA_TYPE_VIDEO || dec_ctx->codec_type == AVMEDIA_TYPE_AUDIO)
        {
            /* in this example, we choose transcoding to same codec */
            encoder = avcodec_find_encoder(dec_ctx->codec_type == AVMEDIA_TYPE_VIDEO ? AV_CODEC_ID_H264 : AV_CODEC_ID_AAC);
            if (!encoder)
            {
                av_log(NULL, AV_LOG_FATAL, "Necessary encoder not found\n");
                return AVERROR_INVALIDDATA;
            }

            out_stream = avformat_new_stream(ofmt_ctx, encoder);
            if (!out_stream)
            {
                av_log(NULL, AV_LOG_ERROR, "Failed allocating output stream\n");
                return AVERROR_UNKNOWN;
            }
            enc_ctx = avcodec_alloc_context3(encoder);
            if (!enc_ctx)
            {
                av_log(NULL, AV_LOG_FATAL, "Failed to allocate the encoder context\n");
                return AVERROR(ENOMEM);
            }

            /* In this example, we transcode to same properties (picture size,
             * sample rate etc.). These properties can be changed for output
             * streams easily using filters */
            if (dec_ctx->codec_type == AVMEDIA_TYPE_VIDEO)
            {
                vedio_index = i;
                enc_ctx->height = 720;
                enc_ctx->width = 1280;
                enc_ctx->sample_aspect_ratio = dec_ctx->sample_aspect_ratio;
                /* take first format from list of supported formats */
                if (encoder->pix_fmts)
                    enc_ctx->pix_fmt = encoder->pix_fmts[0];
                else
                    enc_ctx->pix_fmt = dec_ctx->pix_fmt;
                /* video time_base can be set to whatever is handy and supported by encoder */
                enc_ctx->time_base = av_inv_q(dec_ctx->framerate); //编码器必须设置,解码器不用设置(已经弃用),因为解码器已经确定了频率
                // enc_ctx->framerate = dec_ctx->framerate;
                enc_ctx->bit_rate = 1000000;
                enc_ctx->gop_size = 10;    // 帧组最多10帧一组
                enc_ctx->max_b_frames = 0; //设置无B帧
                enc_ctx->codec_tag = 0;    //
            }
            else
            {
                audio_index = i;

                if (!encoder->channel_layouts)
                    enc_ctx->channel_layout = AV_CH_LAYOUT_STEREO;
                else
                {
                    int best_nb_channels = 0;
                    const uint64_t *p = encoder->channel_layouts;
                    while (*p)
                    {
                        int nb_channels = av_get_channel_layout_nb_channels(*p);
                        if (nb_channels > best_nb_channels)
                        {
                            enc_ctx->channel_layout = *p;
                            best_nb_channels = nb_channels;
                        }
                        p++;
                    }
                }

                enc_ctx->channels = av_get_channel_layout_nb_channels(enc_ctx->channel_layout);
                enc_ctx->sample_rate = 0;
                if (!encoder->supported_samplerates)
                    enc_ctx->sample_rate = 44100;
                else
                {
                    const int *p = encoder->supported_samplerates;
                    while (*p)
                    {
                        if (!enc_ctx->sample_rate || abs(44100 - *p) < abs(44100 - enc_ctx->sample_rate))
                            enc_ctx->sample_rate = *p;
                        p++;
                    }
                }
                enc_ctx->bit_rate = 64000;
                enc_ctx->sample_fmt = encoder->sample_fmts[0];
                const enum AVSampleFormat *p = encoder->sample_fmts;
                while (*p != AV_SAMPLE_FMT_NONE)
                {
                    if (*p == enc_ctx->sample_fmt)
                    {
                        ret = 1;
                        break;
                    }
                    p++;
                }
                if (ret <= 0)
                {
                    fprintf(stderr, "Encoder does not support sample format %s",
                            av_get_sample_fmt_name(enc_ctx->sample_fmt));
                    exit(1);
                }

                // // enc_ctx->channel_layout = av_get_default_channel_layout(AV_CH_LAYOUT_STEREO); // dec_ctx->channel_layout; //
                // enc_ctx->channel_layout = AV_CH_LAYOUT_STEREO; // dec_ctx->channel_layout; //
                // enc_ctx->channels = 2;
                // enc_ctx->sample_rate = dec_ctx->sample_rate;
                // enc_ctx->sample_fmt = encoder->sample_fmts[0];
                // // enc_ctx->time_base = (AVRational){1, enc_ctx->sample_rate}; //???
                // enc_ctx->bit_rate = 96000;
                // enc_ctx->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL;
            }

            if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
                enc_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
            AVDictionary *param = 0;
            av_dict_set(&param, "preset", "medium", 0);
            av_dict_set(&param, "tune", "zerolatency", 0);

            /* Third parameter can be used to pass settings to encoder */
            ret = avcodec_open2(enc_ctx, encoder, &param);
            if (ret < 0)
            {
                av_log(NULL, AV_LOG_ERROR, "Cannot open video encoder for stream #%u\n", i);
                return ret;
            }
            ret = avcodec_parameters_from_context(out_stream->codecpar, enc_ctx);
            if (ret < 0)
            {
                av_log(NULL, AV_LOG_ERROR, "Failed to copy encoder parameters to output stream #%u\n", i);
                return ret;
            }
            //这里设置应该没用的,因为封装格式(MP4\FLV)就决定了输出的timebase,一般都是{1,1000}
            // out_stream->time_base = enc_ctx->time_base;
            scs[i].enc_ctx = enc_ctx;
            scs[i].out_stream = out_stream;
        }
        else if (dec_ctx->codec_type == AVMEDIA_TYPE_UNKNOWN)
        {
            av_log(NULL, AV_LOG_FATAL, "Elementary stream #%d is of unknown type, cannot proceed\n", i);
            return AVERROR_INVALIDDATA;
        }
        else
        {
            /* if this stream must be remuxed */
            ret = avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar);
            if (ret < 0)
            {
                av_log(NULL, AV_LOG_ERROR, "Copying parameters for stream #%u failed\n", i);
                return ret;
            }
            out_stream->time_base = in_stream->time_base;
        }
    }
    av_dump_format(ofmt_ctx, 0, outUrl, 1);

    if (!(ofmt_ctx->oformat->flags & AVFMT_NOFILE))
    {
        ret = avio_open(&ofmt_ctx->pb, outUrl, AVIO_FLAG_WRITE);
        if (ret < 0)
        {
            av_log(NULL, AV_LOG_ERROR, "Could not open output file '%s'", outUrl);
            return ret;
        }
    }

    /* init muxer, write output file header */
    ret = avformat_write_header(ofmt_ctx, NULL);
    if (ret < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "Error occurred when opening output file\n");
        return ret;
    }
    // 输入流:打开输入文件后调用avformat_find_stream_info()可获取到每个流中的time_base
    // 输出流:打开输出文件后调用avformat_write_header()可根据输出文件封装格式确定每个流的time_base并写入输出文件中
    //打印输出流的timebase
    if (vedio_index != -1)
    {
        StreamContext *sc1 = &scs[vedio_index];
        AVRational instream_timebase = ifmt_ctx->streams[vedio_index]->time_base;
        AVRational outstream_timebase = ofmt_ctx->streams[vedio_index]->time_base;
        AVRational dec_ctx_timebase = sc1->dec_ctx->time_base;
        // stream->dec_ctx->time_base = av_inv_q(stream->dec_ctx->framerate); //修正解码流中的时间戳
        av_log(NULL, AV_LOG_ERROR, "vedio_index timebases\n");
    }
    if (audio_index != -1)
    {
        StreamContext *sc2 = &scs[audio_index];
        AVRational instream_timebase = ifmt_ctx->streams[audio_index]->time_base;
        AVRational outstream_timebase = ofmt_ctx->streams[audio_index]->time_base;
        AVRational dec_ctx_timebase = sc2->dec_ctx->time_base;
        av_log(NULL, AV_LOG_ERROR, "audio_index timebases\n");
    }

    return 0;
}

int RTMPClient::init_filter(FilteringContext *fctx, AVCodecContext *dec_ctx,
                            AVCodecContext *enc_ctx, const char *filter_spec)
{
    char args[512];
    int ret = 0;
    const AVFilter *buffersrc = NULL;
    const AVFilter *buffersink = NULL;
    AVFilterContext *buffersrc_ctx = NULL;
    AVFilterContext *buffersink_ctx = NULL;
    AVFilterInOut *outputs = avfilter_inout_alloc();
    AVFilterInOut *inputs = avfilter_inout_alloc();
    AVFilterGraph *filter_graph = avfilter_graph_alloc();

    if (!outputs || !inputs || !filter_graph)
    {
        ret = AVERROR(ENOMEM);
        goto end;
    }

    if (dec_ctx->codec_type == AVMEDIA_TYPE_VIDEO)
    {
        buffersrc = avfilter_get_by_name("buffer");
        buffersink = avfilter_get_by_name("buffersink");
        if (!buffersrc || !buffersink)
        {
            av_log(NULL, AV_LOG_ERROR, "filtering source or sink element not found\n");
            ret = AVERROR_UNKNOWN;
            goto end;
        }

        snprintf(args, sizeof(args),
                 "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
                 dec_ctx->width, dec_ctx->height, dec_ctx->pix_fmt,
                 dec_ctx->time_base.num, dec_ctx->time_base.den,
                 dec_ctx->sample_aspect_ratio.num,
                 dec_ctx->sample_aspect_ratio.den);

        ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in",
                                           args, NULL, filter_graph);
        if (ret < 0)
        {
            av_log(NULL, AV_LOG_ERROR, "Cannot create buffer source\n");
            goto end;
        }

        ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out",
                                           NULL, NULL, filter_graph);
        if (ret < 0)
        {
            av_log(NULL, AV_LOG_ERROR, "Cannot create buffer sink\n");
            goto end;
        }

        ret = av_opt_set_bin(buffersink_ctx, "pix_fmts",
                             (uint8_t *)&enc_ctx->pix_fmt, sizeof(enc_ctx->pix_fmt),
                             AV_OPT_SEARCH_CHILDREN);
        if (ret < 0)
        {
            av_log(NULL, AV_LOG_ERROR, "Cannot set output pixel format\n");
            goto end;
        }
    }
    else if (dec_ctx->codec_type == AVMEDIA_TYPE_AUDIO)
    {
        buffersrc = avfilter_get_by_name("abuffer");
        buffersink = avfilter_get_by_name("abuffersink");
        if (!buffersrc || !buffersink)
        {
            av_log(NULL, AV_LOG_ERROR, "filtering source or sink element not found\n");
            ret = AVERROR_UNKNOWN;
            goto end;
        }

        if (!dec_ctx->channel_layout)
            dec_ctx->channel_layout =
                av_get_default_channel_layout(dec_ctx->channels);
        snprintf(args, sizeof(args),
                 "time_base=%d/%d:sample_rate=%d:sample_fmt=%s:channel_layout=0x%" PRIx64,
                 dec_ctx->time_base.num, dec_ctx->time_base.den, dec_ctx->sample_rate,
                 av_get_sample_fmt_name(dec_ctx->sample_fmt),
                 dec_ctx->channel_layout);
        ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in",
                                           args, NULL, filter_graph);
        if (ret < 0)
        {
            av_log(NULL, AV_LOG_ERROR, "Cannot create audio buffer source\n");
            goto end;
        }

        ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out",
                                           NULL, NULL, filter_graph);
        if (ret < 0)
        {
            av_log(NULL, AV_LOG_ERROR, "Cannot create audio buffer sink\n");
            goto end;
        }

        ret = av_opt_set_bin(buffersink_ctx, "sample_fmts",
                             (uint8_t *)&enc_ctx->sample_fmt, sizeof(enc_ctx->sample_fmt),
                             AV_OPT_SEARCH_CHILDREN);
        if (ret < 0)
        {
            av_log(NULL, AV_LOG_ERROR, "Cannot set output sample format\n");
            goto end;
        }

        ret = av_opt_set_bin(buffersink_ctx, "channel_layouts",
                             (uint8_t *)&enc_ctx->channel_layout,
                             sizeof(enc_ctx->channel_layout), AV_OPT_SEARCH_CHILDREN);
        if (ret < 0)
        {
            av_log(NULL, AV_LOG_ERROR, "Cannot set output channel layout\n");
            goto end;
        }

        ret = av_opt_set_bin(buffersink_ctx, "sample_rates",
                             (uint8_t *)&enc_ctx->sample_rate, sizeof(enc_ctx->sample_rate),
                             AV_OPT_SEARCH_CHILDREN);
        if (ret < 0)
        {
            av_log(NULL, AV_LOG_ERROR, "Cannot set output sample rate\n");
            goto end;
        }
    }
    else
    {
        ret = AVERROR_UNKNOWN;
        goto end;
    }

    /* Endpoints for the filter graph. */
    outputs->name = av_strdup("in");
    outputs->filter_ctx = buffersrc_ctx;
    outputs->pad_idx = 0;
    outputs->next = NULL;

    inputs->name = av_strdup("out");
    inputs->filter_ctx = buffersink_ctx;
    inputs->pad_idx = 0;
    inputs->next = NULL;

    if (!outputs->name || !inputs->name)
    {
        ret = AVERROR(ENOMEM);
        goto end;
    }

    if ((ret = avfilter_graph_parse_ptr(filter_graph, filter_spec,
                                        &inputs, &outputs, NULL)) < 0)
        goto end;

    if ((ret = avfilter_graph_config(filter_graph, NULL)) < 0)
        goto end;

    /* Fill FilteringContext */
    fctx->buffersrc_ctx = buffersrc_ctx;
    fctx->buffersink_ctx = buffersink_ctx;
    fctx->filter_graph = filter_graph;

end:
    avfilter_inout_free(&inputs);
    avfilter_inout_free(&outputs);

    return ret;
}

int RTMPClient::init_filters(void)
{
    const char *filter_spec;
    unsigned int i;
    int ret;
    filter_ctx = (FilteringContext *)av_malloc_array(ifmt_ctx->nb_streams, sizeof(*filter_ctx));
    if (!filter_ctx)
        return AVERROR(ENOMEM);

    for (i = 0; i < ifmt_ctx->nb_streams; i++)
    {
        filter_ctx[i].buffersrc_ctx = NULL;
        filter_ctx[i].buffersink_ctx = NULL;
        filter_ctx[i].filter_graph = NULL;
        if (!(ifmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO || ifmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO))
            continue;
        // PZX  滤镜暂时为空
        if (ifmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
            // filter_spec = "null"; /* passthrough (dummy) filter for video */
            filter_spec = "format=yuv420p,scale=1280:-1,rotate=PI/12"; /* passthrough (dummy) filter for video */
        else
            filter_spec = "anull"; /* passthrough (dummy) filter for audio */
        ret = init_filter(&filter_ctx[i], scs[i].dec_ctx,
                          scs[i].enc_ctx, filter_spec);
        if (ret)
            return ret;

        filter_ctx[i].enc_pkt = av_packet_alloc();
        if (!filter_ctx[i].enc_pkt)
            return AVERROR(ENOMEM);

        filter_ctx[i].filtered_frame = av_frame_alloc();
        if (!filter_ctx[i].filtered_frame)
            return AVERROR(ENOMEM);
    }
    return 0;
}

int RTMPClient::encode_write_frame(unsigned int stream_index, int flush)
{
    StreamContext *sc = &scs[stream_index];
    FilteringContext *filter = &filter_ctx[stream_index];
    AVFrame *filt_frame = flush ? NULL : filter->filtered_frame;
    AVPacket *enc_pkt = filter->enc_pkt;
    int ret;

    av_packet_unref(enc_pkt);
    AVCodecContext *enc_ctx = sc->enc_ctx;
    AVCodecContext *dec_ctx = sc->dec_ctx;
    //音频是否需要重采样?
    bool needResample = stream_index == audio_index && (enc_ctx->sample_fmt != dec_ctx->sample_fmt || enc_ctx->channels != dec_ctx->channels || enc_ctx->sample_rate != dec_ctx->sample_rate);

    if (needResample)
    { //音频的FIFO处理

        // 音频输入源和输出源帧大小可能不一致,需对当前帧或重采样后的帧进行缓冲变换
        if (av_audio_fifo_write(fifo, (void **)filt_frame->data, filt_frame->nb_samples) < 0)
            return 0; //结束本轮转码

        while (av_audio_fifo_size(fifo) >= enc_ctx->frame_size)
        {

            AVFrame *frame = av_frame_alloc();

            frame->nb_samples = enc_ctx->frame_size > 0 ? enc_ctx->frame_size : 1024;
            frame->channel_layout = enc_ctx->channel_layout;
            frame->format = enc_ctx->sample_fmt;
            frame->channels = av_get_default_channel_layout(enc_ctx->channel_layout);
            frame->sample_rate = enc_ctx->sample_rate;
            if (frame->nb_samples)
            {
                int error = 0;
                if ((error = av_frame_get_buffer(frame, 0)) < 0)
                {
                    // av_log(NULL, AV_LOG_ERROR, "[pzx] frame->nb_samples size %d\n", frame->nb_samples);
                    av_frame_free(&frame);
                    return error;
                }
                int ret = av_audio_fifo_read(fifo, (void **)frame->data, (enc_ctx->frame_size > 0 ? enc_ctx->frame_size : 1024));
#if OPENLOG
                av_log(NULL, AV_LOG_INFO, "[pzx] ret:%d ,fifo size:%d,frame->nb_samples size:%d\n", ret, av_audio_fifo_size(fifo), frame->nb_samples);
#endif
                if (ret < 0)
                {
                    fprintf(stderr, "Error read audio buffer\n");
                    av_frame_free(&frame);
                    return -1;
                }
                ret = av_frame_make_writable(frame);
                if (ret < 0)
                {
                    av_log(NULL, AV_LOG_ERROR, "av_frame_make_writable failed");
                    av_frame_free(&frame);
                    return -1;
                }

                ret = avcodec_send_frame(enc_ctx, frame);
                if (ret < 0)
                {
                    av_log(NULL, AV_LOG_ERROR, "[pzx]   1.avcodec_send_frame ret<0 %d\n", ret);
                    av_frame_free(&frame);
                    return -1;
                }
                while (ret >= 0)
                {
                    AVPacket *new_enc_pkt = av_packet_alloc();
                    if (!new_enc_pkt)
                    {
                        av_log(NULL, AV_LOG_ERROR, "[pzx]   1.av_packet_alloc failed\n");
                        av_frame_free(&frame);
                        return -1;
                    }
                    ret = avcodec_receive_packet(enc_ctx, new_enc_pkt);
                    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
                    {
                        //这两种错误应该结束循环
                        av_packet_unref(new_enc_pkt);
                        break;
                    }
                    else if (ret < 0)
                    {
                        av_log(NULL, AV_LOG_ERROR, "[pzx] 1.avcodec_receive_packet ret<0 %d\n", ret);
                        av_frame_free(&frame);
                        av_packet_unref(new_enc_pkt);
                        return ret;
                    }
                    int audioIndex = sc->next_pts;
                    new_enc_pkt->pts = audioIndex * enc_ctx->frame_size;
                    new_enc_pkt->dts = audioIndex * enc_ctx->frame_size;
                    new_enc_pkt->duration = enc_ctx->frame_size;
                    new_enc_pkt->stream_index = stream_index;
                    sc->next_pts++;
                    // audioIndex++;
                    log_packet(ofmt_ctx, new_enc_pkt);
                    // printts(new_enc_pkt);
#if USE_QUEUE
                    // output_packet_queue_.Push(new_enc_pkt); //换成音频队列
#else
                    ret = av_interleaved_write_frame(ofmt_ctx, new_enc_pkt);
                    if (ret < 0)
                    {
                        av_log(NULL, AV_LOG_ERROR, "[pzx] audio write failed???? %d\n", ret);
                        //忽略掉个别帧的异常问题,继续发送
                    }
                    av_packet_free(&new_enc_pkt);
#endif
                }
                av_frame_free(&frame);
            }
        }

        return ret; //处理完返回
    }
    else
    {

        if ((ret = avcodec_send_frame(enc_ctx, filt_frame)) < 0)
        {
            av_log(NULL, AV_LOG_ERROR, "[pzx]  2.avcodec_send_frame ret<0 %d\n", ret);
            return ret;
        }
        while (ret >= 0)
        {
            AVPacket *new_enc_pkt = av_packet_alloc(); //每次都要新申请packet,比较关键
            if (!new_enc_pkt)
            {
                av_log(NULL, AV_LOG_ERROR, "[pzx]   1.av_packet_alloc failed\n");
                return -1;
            }
            ret = avcodec_receive_packet(enc_ctx, new_enc_pkt);
            if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
            {
                av_packet_free(&new_enc_pkt); // EAGAIN或者AVERROR_EOF后要释放
                return 0;
            }
            else if (ret < 0)
            {
                av_log(NULL, AV_LOG_ERROR, "[pzx] 1.avcodec_receive_packet ret<0 %d\n", ret);
                av_packet_free(&new_enc_pkt); // EAGAIN或者AVERROR_EOF后要释放
                return ret;
            }
            // av_log(NULL, AV_LOG_ERROR, "[pzx] stream_index:%d filt_frame--- pts:%d,pkt_pts:%d,pkt_dts %d,\n", new_enc_pkt->stream_index, filt_frame->pts, filt_frame->pkt_pts, filt_frame->pkt_dts);
            // av_log(NULL, AV_LOG_ERROR, "[pzx] stream_index:%d enc_pkt--- pts:%d,dts:%d\n", new_enc_pkt->stream_index, new_enc_pkt->pts, new_enc_pkt->dts);

            /* prepare packet for muxing */
            new_enc_pkt->stream_index = stream_index;

            AVRational time_base_q = {1, AV_TIME_BASE};                           //单位微秒-FFMPEG内部定义
            AVRational in_stream_tb = ifmt_ctx->streams[stream_index]->time_base; //输入视频层(tbn)
            // AVRational in_ctx_tb = dec_ctx->time_base;                             //输入文件层(tbc)-一般废弃不用
            AVRational out_stream_tb = ofmt_ctx->streams[stream_index]->time_base; // FLV封装定义的timebase{1,1000}
            AVRational out_ctx_tb = enc_ctx->time_base;                            //输出文件层-一般是fps

            //这个两个时间都是按微秒算的。
            // pts_time=按ffmpeg内部翻译的时间算,dts解码时间应该将输入流的dts转换成标准ffmpeg处理"刻度"。
            // pts_time可以看成预期编码时间,(假想解码后立刻开始编码,所以pts_time的换算需要以输入为时间戳)
            int64_t pts_time = av_rescale_q(new_enc_pkt->dts, in_stream_tb, time_base_q);
            // now_time可以看成实际解码了多久
            // now_time=(已经流失的"刻度");现在的流"刻度"-开始开始播放的"刻度"
            int64_t now_time = av_gettime() - start_time; //到1970的时间差,单位同AV_TIME_BASE一样也是微秒

#if USE_RTMP //直播文件的时候解码太快了,需要停顿下,控制帧率
            //如果过快(电脑台高级,第二帧解码瞬间完成,需要等待会再发送。如果是转码可以忽略),需要停顿后再解码
            if (pts_time > now_time)
            {
                av_log(NULL, AV_LOG_ERROR, "av_usleep= %d\n", (unsigned int)(pts_time - now_time));
                av_usleep((unsigned int)(pts_time - now_time));
            }
#endif
            av_packet_rescale_ts(new_enc_pkt, out_ctx_tb, out_stream_tb);
            // log_packet(ofmt_ctx, new_enc_pkt);
               printts(new_enc_pkt);
            sc->next_pts++;
/* mux encoded frame */
#if USE_QUEUE
            output_packet_queue_.Push(new_enc_pkt); //换成视频队列
#else
            ret = av_interleaved_write_frame(ofmt_ctx, new_enc_pkt);
            if (ret < 0)
            {
                av_log(NULL, AV_LOG_ERROR, "[pzx] video write failed???? %d\n", ret);
            }
            av_packet_free(&new_enc_pkt);
#endif
        }
        return ret;
    }
}

int RTMPClient::filter_encode_write_frame(AVFrame *frame, unsigned int stream_index)
{
    FilteringContext *filter = &filter_ctx[stream_index];
    int ret;

    // av_log(NULL, AV_LOG_INFO, "Pushing decoded frame to filters\n");
    /* push the decoded frame into the filtergraph */
    ret = av_buffersrc_add_frame_flags(filter->buffersrc_ctx,
                                       frame, 0);
    if (ret < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "Error while feeding the filtergraph\n");
        return ret;
    }
    /* pull filtered frames from the filtergraph */
    while (1)
    {
        // AVFrame *filter_frame = av_frame_alloc();
        // ret = av_buffersink_get_frame(filter->buffersink_ctx, filter_frame);
        ret = av_buffersink_get_frame(filter->buffersink_ctx, filter->filtered_frame);
        if (ret < 0)
        {
            /* if no more frames for output - returns AVERROR(EAGAIN)
             * if flushed and no more frames for output - returns AVERROR_EOF
             * rewrite retcode to 0 to show it as normal procedure completion
             */
            if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
            {
                // av_frame_free(&filter_frame);
                ret = 0;
            }
            break;
        }
        // filter->filtered_frame = filter_frame;

        filter->filtered_frame->pict_type = AV_PICTURE_TYPE_NONE;
        ret = encode_write_frame(stream_index, 0);
        av_frame_unref(filter->filtered_frame);
        if (ret < 0)
            break;
    }

    return ret;
}

void RTMPClient::send()
{
    while (1)
    {
        AVPacket *output_packet;
        if (!(output_packet = output_packet_queue_.Pop()))
        {
            Sleep(100);
            if (writer_flag_.Get() == false)
                break;
            continue;
        }
        if (av_interleaved_write_frame(ofmt_ctx, output_packet) != 0)
        {
            av_log(NULL, AV_LOG_ERROR, "error in writing video frame\n");
        }
        //  if (av_write_frame(ofmt_ctx, output_packet) != 0)
        // {
        //     av_log(NULL, AV_LOG_ERROR, "error in writing %d-frame\n",output_packet->stream_index);
        // }
        av_packet_free(&output_packet);
    }
    int ret = av_write_trailer(ofmt_ctx);
    if (ret < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "error in writing av trailer\n");
        exit(1);
    }
    return;
}

int RTMPClient::init()
{
    // av_log_set_level(AV_LOG_DEBUG);
    int ret;

    if ((ret = open_input_file()) < 0)
        return ret;
    if ((ret = open_output_file()) < 0)
        return ret;
    if (audio_index != -1)
    {
        //     /* Initialize the resampler to be able to convert audio sample formats. */
        //     if (init_resampler(stream_ctx[audio_index].dec_ctx, stream_ctx[audio_index].enc_ctx,
        //                        &resample_context))
        //         goto end;
        /* Initialize the FIFO buffer to store audio samples to be encoded. */
        if ((ret = init_fifo(&fifo, scs[audio_index].enc_ctx)) < 0)
            return ret;
    }
    if ((ret = init_filters()) < 0)
        return ret;
    return ret;
}
int RTMPClient::run()
{
    int ret = 0;
    start_time = av_gettime();
    writer_flag_.Set(true);
    std::thread sendThread(&RTMPClient::send, this);
    sendThread.detach();
    /* read all packets */
    AVPacket *packet = NULL;
    unsigned int stream_index;
    if (!(packet = av_packet_alloc()))
        return ret;
    while (1)
    {
        if ((ret = av_read_frame(ifmt_ctx, packet)) < 0)
        {
            if (ret == AVERROR_EOF || ret == AVERROR(EAGAIN))
                av_log(NULL, AV_LOG_ERROR, "[PZX]  av_read_frame exit:%d\n", ret);
            break;
        }

        // calculate pts and dts
        if (packet->pts == AV_NOPTS_VALUE)
        {
            av_log(NULL, AV_LOG_ERROR, "[PZX]  packet dts is null error10\n");
            // AVRational time_base = ifmt_ctx->streams[video_index]->time_base;
            // int64_t cal_duration = (int64_t)(AV_TIME_BASE /
            //                                  av_q2d(ifmt_ctx->streams[video_index]->r_frame_rate));
            // packet.pts = (int64_t)((frame_index * cal_duration) /
            //                        (av_q2d(time_base) * AV_TIME_BASE));
            // packet.dts = packet.pts;
            // packet.duration = (int64_t)(cal_duration / (av_q2d(time_base) * AV_TIME_BASE));
        }

        stream_index = packet->stream_index;
        // av_log(NULL, AV_LOG_INFO, "read stream_%u\t  pts:%d\t    dts:%d\r", stream_index, packet->pts, packet->dts);

        if (filter_ctx[stream_index].filter_graph)
        {

            RTMPClient::StreamContext *sc = &scs[stream_index];
#if 1

            AVRational instream_timebase = ifmt_ctx->streams[stream_index]->time_base;
            // AVRational outstream_timebase = ofmt_ctx->streams[stream_index]->time_base;
            AVRational dec_ctx_timebase = sc->dec_ctx->time_base; //解码器的时间基
            // 时间基转换,视频在编码前需要修改paket的pts等数字为framerate;不然就是有问题的
            AVRational raw_video_time_base = av_inv_q(sc->dec_ctx->framerate); //视频解码器需要取framerate
            //官方例子有bug,视频的时间轴需要改成framerate,不然会视频慢一倍
            //解出来的封装需要重新定义时间轴
            av_packet_rescale_ts(packet, instream_timebase, stream_index == vedio_index ? raw_video_time_base : dec_ctx_timebase);
            // av_log(NULL, AV_LOG_INFO, "[PZX] read2 packet  stream_index:%d packet pts:%d dts:%d duration:%d\n", packet->stream_index, packet->pts, packet->dts, packet->duration);
#endif
            ret = avcodec_send_packet(sc->dec_ctx, packet);
            if (ret < 0)
            {
                av_log(NULL, AV_LOG_ERROR, "Decoding failed\n");
                break;
            }

            //初始化过滤器

            while (ret >= 0)
            {
                ret = avcodec_receive_frame(sc->dec_ctx, sc->dec_frame);
                if (ret == AVERROR_EOF || ret == AVERROR(EAGAIN))
                    break;
                else if (ret < 0)
                    return ret;
                //需要设置吗?
                sc->dec_frame->pts = sc->dec_frame->best_effort_timestamp;
                ret = filter_encode_write_frame(sc->dec_frame, stream_index);
                if (ret < 0)
                    return ret;
            }
        }
        else
        {
            av_log(NULL, AV_LOG_ERROR, "[pzx] bu neng jin  error. no way\n");
            /* remux this frame without reencoding */
            av_packet_rescale_ts(packet,
                                 ifmt_ctx->streams[stream_index]->time_base,
                                 ofmt_ctx->streams[stream_index]->time_base);

            ret = av_interleaved_write_frame(ofmt_ctx, packet);
            if (ret < 0)
                return ret;
        }
        av_packet_unref(packet);
        // av_packet_free(&packet);
    }
    av_packet_free(&packet);
    int i;
    /* flush filters and encoders */
    for (i = 0; i < ifmt_ctx->nb_streams; i++)
    {
        /* flush filter */
        if (!filter_ctx[i].filter_graph)
            continue;
        ret = filter_encode_write_frame(NULL, i);
        if (ret < 0)
        {
            av_log(NULL, AV_LOG_ERROR, "Flushing filter failed\n");
            return ret;
        }

        /* flush encoder */
        if (!(scs[i].enc_ctx->codec->capabilities & AV_CODEC_CAP_DELAY))
        {
            continue;
        }

        if ((ret = encode_write_frame(stream_index, 1)) < 0)
        {
            av_log(NULL, AV_LOG_ERROR, "Flushing encoder failed\n");
            return ret;
        }
    }

    av_write_trailer(ofmt_ctx);

    return ret ? 1 : 0;
}
int main()
{
    RTMPClient clinet;
    int ret = -1;
    if ((ret = clinet.init()) < 0)
        return ret;
    ret = clinet.run();
    return ret;
}

transcodingRtmpThread1.h 

#define __STDC_CONSTANT_MACROS
extern "C"
{
#include <libavfilter/avfilter.h>
#include <libavutil/avutil.h>
#include <libavcodec/avcodec.h>
#include <libavdevice/avdevice.h>
#include <libavformat/avformat.h>
#include <libavfilter/buffersink.h>
#include <libavfilter/buffersrc.h>
#include <libavutil/opt.h>
#include <libavutil/time.h>
#include <libavutil/pixdesc.h>
#include <libavutil/timestamp.h>
#include <libswresample/swresample.h>
#include <libavutil/audio_fifo.h>
#include <libavutil/avassert.h>
}
#include <stdio.h>
#include <windows.h>
#include <iostream>
#include <list>
#define USE_QUEUE 0  //使用队列开关
#define USE_CAM 1   //摄像头/文件推流切换开关
#define OPENLOG 0   //日志输出开关,对推流效率有影响,先关闭
#define USE_RTMP 1 //如果要播放文件到RTMP,需要控制速率
#include <memory>
#include "thread_safe_data.hpp"
using namespace std;
class RTMPClient
{

public:
    RTMPClient() {}
    ~RTMPClient();
    ThreadSafeQueue<AVFrame *> video_decoded_frame_queue_, video_scaled_frame_queue_;
    ThreadSafeQueue<AVFrame *> audio_decoded_frame_queue_, audio_resampled_frame_queue_;
    ThreadSafeQueue<AVPacket *> output_packet_queue_;
    ThreadSafeFlag writer_flag_;
    AVFormatContext *ifmt_ctx;
    AVFormatContext *ofmt_ctx;
    int inputFormatContext = 0;
    SwrContext *resample_context = NULL;
    AVAudioFifo *fifo = NULL;
    int frameIndex = 0;
    int64_t last_dts=0;
    int64_t current_a_frameIndex = 0;
    int64_t current_v_frameIndex = 0;
    int64_t a_frameIndex = 0;
    int64_t v_frameIndex = 0;
    clock_t audio_start_time=0;
    clock_t video_start_time=0;
int64_t v_start_dts = 0;
    clock_t lastClock=clock();
    int64_t start_time;
    int audio_index = -1;
    int vedio_index = -1;
    long long dts_index = 0;

    void printts(AVPacket *packet){
        av_log(NULL, AV_LOG_INFO, "read stream_%u\t  pts:%d\t    dts:%d\r", packet->stream_index, packet->pts, packet->dts);
    }
    void log_packet(const AVFormatContext *fmt_ctx, AVPacket *pkt)
    {

#if OPENLOG
        AVRational *time_base = &fmt_ctx->streams[pkt->stream_index]->time_base;
        printf("%d-pts:%d  dts:%d \n", pkt->stream_index, pkt->pts, pkt->dts);
        // printf("%d-pts:%s pts_time:%s dts:%s dts_time:%s duration:%s duration_time:%s\n",
        //        pkt->stream_index, pzx_av_ts2str(pkt->pts), pzx_ts2timestr(pkt->pts, time_base),
        //        pzx_av_ts2str(pkt->dts), pzx_ts2timestr(pkt->dts, time_base),
        //        pzx_av_ts2str(pkt->duration), pzx_ts2timestr(pkt->duration, time_base));
#endif
    }

    struct FilteringContext
    {
        AVFilterContext *buffersink_ctx;
        AVFilterContext *buffersrc_ctx;
        AVFilterGraph *filter_graph;

        AVPacket *enc_pkt;
        AVFrame *filtered_frame;
    };
    FilteringContext *filter_ctx;

    struct StreamContext
    {

        AVCodecContext *dec_ctx;
        AVCodecContext *enc_ctx;
        AVStream *in_stream;
        AVStream *out_stream;
        int next_pts; //下一帧序号
        int last_dts = -1;
        int offset_dts = -1;
        int currentTimePoint;
        AVFrame *dec_frame;
        AVFrame *audio_frame;
    };
    StreamContext *scs; //分别代表音频和视频包装对象


    /**
     * Initialize a FIFO buffer for the audio samples to be encoded.
     * @param[out] fifo                 Sample buffer
     * @param      output_codec_context Codec context of the output file
     * @return Error code (0 if successful)
     */
    static int init_fifo(AVAudioFifo **fifo, AVCodecContext *output_codec_context)
    {
        /* Create the FIFO buffer based on the specified output sample format. */
        if (!(*fifo = av_audio_fifo_alloc(output_codec_context->sample_fmt,
                                          output_codec_context->channels, 1)))
        {
            fprintf(stderr, "Could not allocate FIFO\n");
            return AVERROR(ENOMEM);
        }
        return 0;
    }
    int putfifo(AVFrame *frame, AVCodecContext *enc_ctx)
    {

        if (av_audio_fifo_size(fifo) < (enc_ctx->frame_size > 0 ? enc_ctx->frame_size : 1024))
        {
            // av_log(NULL, AV_LOG_ERROR, "[pzx] av_audio_fifo_size EOF\n");
            return -1;
        }
        frame = av_frame_alloc();

        frame->nb_samples = enc_ctx->frame_size > 0 ? enc_ctx->frame_size : 1024;
        frame->channel_layout = enc_ctx->channel_layout;
        frame->format = enc_ctx->sample_fmt;
        frame->channels = av_get_default_channel_layout(enc_ctx->channel_layout);
        frame->sample_rate = enc_ctx->sample_rate;
        if (frame->nb_samples)
        {
            int error = 0;
            if ((error = av_frame_get_buffer(frame, 0)) < 0)
            {
                // av_log(NULL, AV_LOG_ERROR, "[pzx] frame->nb_samples size %d\n", frame->nb_samples);
                av_frame_free(&frame);
                return error;
            }
            int ret = av_audio_fifo_read(fifo, (void **)frame->data, (enc_ctx->frame_size > 0 ? enc_ctx->frame_size : 1024));
#if OPENLOG
            av_log(NULL, AV_LOG_INFO, "[pzx] ret:%d ,fifo size:%d,frame->nb_samples size:%d\n", ret, av_audio_fifo_size(fifo), frame->nb_samples);
#endif
            if (ret < 0)
            {
                fprintf(stderr, "Error read audio buffer\n");
                return -1;
            }
            ret = av_frame_make_writable(frame);
            if (ret < 0)
            {
                av_log(NULL, AV_LOG_ERROR, "av_frame_make_writable failed");
                return -1;
            }
            return ret;
        }
        else
        {
            return -1; //继续?
        }
    }
    int putfifoOld(AVFrame *frame, AVCodecContext *enc_ctx)
    {
        if (frame)
        {
            av_frame_free(&frame);
        }
        frame = av_frame_alloc();
        if (av_audio_fifo_size(fifo) < (enc_ctx->frame_size > 0 ? enc_ctx->frame_size : 1024))
        {
            // av_log(NULL, AV_LOG_ERROR, "[pzx] av_audio_fifo_size EOF\n");
            return -1;
        }
        frame->nb_samples = enc_ctx->frame_size > 0 ? enc_ctx->frame_size : 1024;
        frame->channel_layout = enc_ctx->channel_layout;
        frame->format = enc_ctx->sample_fmt;
        // frame->channels = av_get_default_channel_layout(enc_ctx->channel_layout);
        frame->sample_rate = enc_ctx->sample_rate;
        if (frame->nb_samples)
        {
            int error = 0;
            if ((error = av_frame_get_buffer(frame, 0)) < 0)
            {
                // av_log(NULL, AV_LOG_ERROR, "[pzx] frame->nb_samples size %d\n", frame->nb_samples);
                av_frame_free(&frame);
                return error;
            }
            int ret = av_audio_fifo_read(fifo, (void **)frame->data, (enc_ctx->frame_size > 0 ? enc_ctx->frame_size : 1024));
#if OPENLOG
            av_log(NULL, AV_LOG_INFO, "[pzx] ret:%d ,fifo size:%d,frame->nb_samples size:%d\n", ret, av_audio_fifo_size(fifo), frame->nb_samples);
#endif
            if (ret < 0)
            {
                fprintf(stderr, "Error read audio buffer\n");
                return -1;
            }
            ret = av_frame_make_writable(frame);
            if (ret < 0)
            {
                av_log(NULL, AV_LOG_ERROR, "av_frame_make_writable failed");
                return -1;
            }
            return ret;
        }
        else
        {
            return 0; //继续?
        }
    }
    int init_filters(void);
    int init_filter(FilteringContext *fctx, AVCodecContext *dec_ctx,
                    AVCodecContext *enc_ctx, const char *filter_spec);
    int open_input_file();
    int open_output_file();
    int WriteFrame(AVFrame *frame);
    int encode_write_frame(unsigned int stream_index, int flush);
    int filter_encode_write_frame(AVFrame *frame, unsigned int stream_index);
    int init();
    void send();
    int run();
};

thread_safe_data.hpp   

// #ifndef __THREAD_SAFE_QUEUE_HPP__
// #define __THREAD_SAFE_QUEUE_HPP__

extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavdevice/avdevice.h>
#include <libavfilter/avfilter.h>
#include <libavutil/avutil.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
#include <libswresample/swresample.h>
}

#include <mutex>
#include <thread>
#include <list>
#include <assert.h>

template<typename T>
class ThreadSafeQueue {
public:
	ThreadSafeQueue() {};
	void Push(T pFrame) {
		mutex_.lock();
		list_.emplace_back(pFrame);
		mutex_.unlock();
	}
	T Pop() {
		T ret = NULL;
		if (!list_.empty()) {
			mutex_.lock();
			ret = list_.front();
			list_.pop_front();
			mutex_.unlock();
		}
		return ret;
	}
	int Size() {
		int ret = -1;
		mutex_.lock();
		ret = list_.size();
		mutex_.unlock();
		return ret;
	}
private:
	std::list<T> list_;
	std::mutex mutex_;
};

class ThreadSafeFlag {
public:
	void Set(bool value) {
		mutex_.lock();
		flag = value;
		mutex_.unlock();
	}
	bool Get() {
		bool ret = false;
		mutex_.lock();
		ret = flag;
		mutex_.unlock();
		return ret;
	}
private:
	bool flag = true;
	std::mutex mutex_;
};

// #endif

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值