Linux下使用FFmpeg实现摄像头采集x264编码(YUYV格式)

    FFmpeg采集摄像头数据
    前言
    一、查看Linux系统下的摄像头设备
    二、代码
    1.在main函数中,所需要用到的参数的声明
    2.解码摄像头原始参数设置
    3.输出H264文件部分
    4.编解码开始部分
    5.收尾部分
    6.源码
    7.Cmake
    1.在main函数中,所需要用到的参数的声明

    点击查看代码
        int ret = 0;
        
        // 注册所有的设备
        avdevice_register_all();
    
        // 输入设备的相关参数
        AVFormatContext *inFmtCtx = avformat_alloc_context();
        AVCodec *inCodec = NULL;
        AVCodecContext *inCodecCtx = NULL;
        int inVideoSteamIndex = -1;
        struct SwsContext *img_ctx = NULL;
        AVFrame *yuvFrame = NULL;
        AVFrame *srcFrame = NULL;
        AVPacket *inPkt = av_packet_alloc();
    
    
        // 输出文件的相关参数
        AVFormatContext *outFmtCtx = avformat_alloc_context();
        AVOutputFormat *outFmt = NULL;
        AVStream *outStream = NULL;
        AVCodecContext *outCodecCtx=NULL;
        AVCodec *outCodec = NULL;
        AVPacket *outPkt = av_packet_alloc();
    
    
    

    2.解码摄像头原始参数设置

    点击查看代码
            // 解码部分
            // 打开v4l2的相机输入
            AVInputFormat *inFmt = av_find_input_format("v4l2");
            if(avformat_open_input(&inFmtCtx,"/dev/video0",inFmt,NULL) < 0){
                fprintf(stderr,"Cannot open camera.\n");
                return -1;
            }
    
            // 查找流
            if(avformat_find_stream_info(inFmtCtx,NULL) < 0){
                fprintf(stderr,"Cannot find any stream in file.\n");
                return -1;
            }
    
            // 寻找视频流
            for(size_t i = 0;i < inFmtCtx->nb_streams;i++){
                if(inFmtCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){
                    inVideoSteamIndex = i;
                    break;
                }
            }
    
            // 没找到视频流
            if(inVideoSteamIndex == -1){
                fprintf(stderr,"Cannot find video stream in file.\n");
                return -1;
            }
    
            // 创建解码器的参数集
            AVCodecParameters* inVideoCodecPara = inFmtCtx->streams[inVideoSteamIndex]->codecpar;
            // 查找解码器
            if(!(inCodec = avcodec_find_decoder(inVideoCodecPara->codec_id))){
                fprintf(stderr,"Cannot find valid video decoder.\n");
                return -1;
            }
    
            if(!(inCodecCtx = avcodec_alloc_context3(inCodec))){
                fprintf(stderr,"Cannot alloc valid decode codec context.\n");
                return -1;
            }
    
            if(avcodec_parameters_to_context(inCodecCtx,inVideoCodecPara) < 0){
                fprintf(stderr,"Cannot initialize parameters.\n");
                return -1;
            }
    
            // 打开编解码器
            if(avcodec_open2(inCodecCtx,inCodec,NULL) < 0){
                fprintf(stderr,"Cannot open codec.\n");
                return -1;
            }
    
            img_ctx = sws_getContext(inCodecCtx->width,inCodecCtx->height,inCodecCtx->pix_fmt,
                                        inCodecCtx->width,inCodecCtx->height,AV_PIX_FMT_YUV420P,SWS_BICUBIC,NULL,NULL,NULL);
            // 获取图像的大小
            int num_bytes = av_image_get_buffer_size(AV_PIX_FMT_YUV420P,inCodecCtx->width,inCodecCtx->height,1);
    
            // 创建out_buffer缓冲区
            uint8_t *out_buffer = (unsigned char *)av_malloc(num_bytes*sizeof(unsigned char));
            yuvFrame = av_frame_alloc();
            srcFrame = av_frame_alloc();
    
            // 将yuvframe和out_buffer进行关联
            int ret = av_image_fill_arrays(yuvFrame->data,yuvFrame->linesize,out_buffer,AV_PIX_FMT_YUV420P,inCodecCtx->width,inCodecCtx->height,1);
            if(ret < 0){
                fprintf(stderr,"Fill arrays failed.\n");
                return -1;
            }
    
    

    3.输出H264文件部分

    点击查看代码
    // 输出文件,编码器部分
            const char* out_file = "output.h264";
            if(avformat_alloc_output_context2(&outFmtCtx,NULL,NULL,out_file)  < 0){
                fprintf(stderr,"Cannot alloc output file context.\n");
                return -1;
            }
            outFmt = outFmtCtx->oformat;
    
            // 打开输出文件
            if(avio_open(&outFmtCtx->pb,out_file,AVIO_FLAG_READ_WRITE) < 0){
                fprintf(stderr,"output file open failed.\n");
                return -1;
            }
    
            // 创建保存的H264流,并设置参数
            outStream = avformat_new_stream(outFmtCtx,outCodec);
            if(outStream == NULL){
                fprintf(stderr,"create new video stream fialed.\n");
                return -1;
            }
    
            // 
            outStream->time_base.den = 30;
            outStream->time_base.num = 1;
    
            // 编码解码器相关的参数集
            // 设置分辨率和bit率
            AVCodecParameters *outCodecPara = outFmtCtx->streams[outStream->index]->codecpar;
            outCodecPara->codec_type=AVMEDIA_TYPE_VIDEO;
            outCodecPara->codec_id = outFmt->video_codec;
            outCodecPara->width = 640;
            outCodecPara->height = 360;
            outCodecPara->bit_rate = 92000;
    
            // 查找编码器
            outCodec = avcodec_find_encoder(outFmt->video_codec);
            if(outCodec == NULL){
                fprintf(stderr,"Cannot find any encoder.\n");
                return -1;
            }
    
            // 设置编码器内容
            outCodecCtx = avcodec_alloc_context3(outCodec);
            avcodec_parameters_to_context(outCodecCtx,outCodecPara);
            if(outCodecCtx==NULL){
                fprintf(stderr,"Cannot alloc output codec content.\n");
                return -1;
            }
    
            outCodecCtx->codec_id = outFmt->video_codec;
            outCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
            outCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
            outCodecCtx->width = inCodecCtx->width;
            outCodecCtx->height = inCodecCtx->height;
            outCodecCtx->time_base.num = 1;
            outCodecCtx->time_base.den = 30;
            outCodecCtx->bit_rate = 92000;
            outCodecCtx->gop_size = 10;
    
            // 根据编码器相关类型设置参数
            // 设置H264相关的参数,q的参数
            if(outCodecCtx->codec_id == AV_CODEC_ID_H264){
                outCodecCtx->qmin = 10;
                outCodecCtx->qmax = 51;
                outCodecCtx->qcompress = (float)0.6;
            }
            else if(outCodecCtx->codec_id == AV_CODEC_ID_MPEG2VIDEO){
                outCodecCtx->max_b_frames = 2;
            }
            else if(outCodecCtx->codec_id == AV_CODEC_ID_MPEG1VIDEO){
                outCodecCtx->mb_decision = 2;
            }
    
            // 打开编码器
            if(avcodec_open2(outCodecCtx,outCodec,NULL) < 0){
                fprintf(stderr,"Open encoder failed.\n");
                return -1;
            }
    
            // 设置yuvframe
            yuvFrame->format = outCodecCtx->pix_fmt;
            yuvFrame->width = outCodecCtx->width;
            yuvFrame->height = outCodecCtx->height;
    
            // 写H264的文件头
            ret = avformat_write_header(outFmtCtx,NULL);
    
    

    4.编解码开始部分
    洗刷编码缓存区的代码

    点击查看代码
    int flush_encoder(AVFormatContext *fmtCtx,AVCodecContext *codecCtx,int StreamaIndex)
    {
        int ret = 0;
        AVPacket enc_pkt;
        enc_pkt.data = NULL;
        enc_pkt.size = 0;
        av_init_packet(&enc_pkt);
    
        printf("Flushing stream #%u encoder\n",StreamaIndex);
        // 进行编码一个frame
        if(avcodec_send_frame(codecCtx,0) >= 0){
            while(avcodec_receive_packet(codecCtx,&enc_pkt) >= 0){
                printf("success encoder 1 frame.\n");
                enc_pkt.stream_index = StreamaIndex;
                av_packet_rescale_ts(&enc_pkt,codecCtx->time_base,fmtCtx->streams[ StreamaIndex ]->time_base);
                // 将编码好的写入到H264的文件
                ret = av_interleaved_write_frame(fmtCtx, &enc_pkt);
                if(ret < 0){
                    break;
                }
            }
        }
    
        return ret;
    }
    
    
    

    编解码部分

    点击查看代码
    int count = 0;
            // 读取一个frame的数据,放入pakcet中
            while(av_read_frame(inFmtCtx,inPkt) >= 0 && count < 50){
                // 判断是否是视频流
                if(inPkt->stream_index == inVideoSteamIndex){
                    // 解码
                    if(avcodec_send_packet(inCodecCtx,inPkt) >= 0){
                        // 判断是否解码完成
                        while((ret = avcodec_receive_frame(inCodecCtx,srcFrame)) >= 0){
                            if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF){
                                return -1;
                            }
                            else if(ret < 0){
                                fprintf(stderr, "Error during decoding\n");
                                exit(1);
                            }
                            // 解码完成
                            // 进行转换,由于摄像头输入的yuyv422的格式,因此需要进行转换,转换到yuv420p的格式
                            sws_scale(img_ctx,(const uint8_t* const*)srcFrame->data,srcFrame->linesize,
                                        0,inCodecCtx->height,yuvFrame->data,yuvFrame->linesize);
                            
                            yuvFrame->pts = srcFrame->pts;
    
                            // 解码完成之后进行编码
                            if(avcodec_send_frame(outCodecCtx,yuvFrame) >= 0){
                                if(avcodec_receive_packet(outCodecCtx,outPkt) >= 0){
                                    printf("----encode one frame-----\n");
                                    ++count;
                                    outPkt->stream_index = outStream->index;
                                    av_packet_rescale_ts(outPkt,outCodecCtx->time_base,outStream->time_base);
                                    outPkt->pos = -1;
                                    av_interleaved_write_frame(outFmtCtx,outPkt);
                                    av_packet_unref(outPkt);
                                }
                            }
    
                            // 短暂的延迟
                            usleep(1000 * 24);
                        }
                    }
                    av_packet_unref(inPkt);
                }
            }
    
            // 洗刷编码区
            ret = flush_encoder(outFmtCtx,outCodecCtx,outStream->index);
            if(ret < 0){
                fprintf(stderr,"flushing encoder failed.\n");
                return -1;
            }
    
            // 写H264的文件尾
            av_write_trailer(outFmtCtx);
    
    

    5.收尾部分
    收尾部分,主要进行内存空间的释放。由于是demo程序,内存空间释放写的比较简陋,其实很多错误检查的时候,应该使用goto语句,然后进行内存空间的释放。

    点击查看代码
        av_packet_free(&inPkt);
        avcodec_free_context(&inCodecCtx);
        avcodec_close(inCodecCtx);
        avformat_close_input(&inFmtCtx);
        av_frame_free(&srcFrame);
        av_frame_free(&yuvFrame);
    
        av_packet_free(&outPkt);
        avcodec_free_context(&outCodecCtx);
        avcodec_close(outCodecCtx);
        avformat_close_input(&outFmtCtx);
    
    
    

    6.源码

    点击查看代码
    
    //jpeg_Yuv420p.cpp
    #include <unistd.h>
    #include "libavcodec/avcodec.h"
    #include "libavdevice/avdevice.h"
    #include "libswresample/swresample.h"
    #include "libavutil/avutil.h"
    #include "libavutil/frame.h"
    #include "libavutil/samplefmt.h"
    #include "libavutil/opt.h"
    #include "libavutil/imgutils.h"
    #include "libavutil/parseutils.h"
    #include "libswscale/swscale.h"
    #include "libavformat/avformat.h"
    
    
    int flush_encoder(AVFormatContext *fmtCtx,AVCodecContext *codecCtx,int StreamaIndex)
    {
        int ret = 0;
        AVPacket enc_pkt;
        enc_pkt.data = NULL;
        enc_pkt.size = 0;
        av_init_packet(&enc_pkt);
    
        printf("Flushing stream #%u encoder\n",StreamaIndex);
        // 进行编码一个frame
        if(avcodec_send_frame(codecCtx,0) >= 0){
            while(avcodec_receive_packet(codecCtx,&enc_pkt) >= 0){
                printf("success encoder 1 frame.\n");
                enc_pkt.stream_index = StreamaIndex;
                av_packet_rescale_ts(&enc_pkt,codecCtx->time_base,fmtCtx->streams[ StreamaIndex ]->time_base);
                // 将编码好的写入到H264的文件
                ret = av_interleaved_write_frame(fmtCtx, &enc_pkt);
                if(ret < 0){
                    break;
                }
            }
        }
    
        return ret;
    }
    
    
    int main(int argc, char* argv[]) {
        int ret = 0;
        
        // 注册所有的设备
        avdevice_register_all();
    
        // 输入设备的相关参数
        AVFormatContext *inFmtCtx = avformat_alloc_context();
        AVCodec *inCodec = NULL;
        AVCodecContext *inCodecCtx = NULL;
        int inVideoSteamIndex = -1;
        struct SwsContext *img_ctx = NULL;
        AVFrame *yuvFrame = NULL;
        AVFrame *srcFrame = NULL;
        AVPacket *inPkt = av_packet_alloc();
    
    
        // 输出文件的相关参数
        AVFormatContext *outFmtCtx = avformat_alloc_context();
        AVOutputFormat *outFmt = NULL;
        AVStream *outStream = NULL;
        AVCodecContext *outCodecCtx=NULL;
        AVCodec *outCodec = NULL;
        AVPacket *outPkt = av_packet_alloc();
    
        do {
    
            // 解码部分
            // 打开v4l2的相机输入
            AVInputFormat *inFmt = av_find_input_format("v4l2");
            if(avformat_open_input(&inFmtCtx,"/dev/video0",inFmt,NULL) < 0){
                fprintf(stderr,"Cannot open camera.\n");
                return -1;
            }
    
            // 查找流
            if(avformat_find_stream_info(inFmtCtx,NULL) < 0){
                fprintf(stderr,"Cannot find any stream in file.\n");
                return -1;
            }
    
            // 寻找视频流
            for(size_t i = 0;i < inFmtCtx->nb_streams;i++){
                if(inFmtCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){
                    inVideoSteamIndex = i;
                    break;
                }
            }
    
            // 没找到视频流
            if(inVideoSteamIndex == -1){
                fprintf(stderr,"Cannot find video stream in file.\n");
                return -1;
            }
    
            // 创建解码器的参数集
            AVCodecParameters* inVideoCodecPara = inFmtCtx->streams[inVideoSteamIndex]->codecpar;
            // 查找解码器
            if(!(inCodec = avcodec_find_decoder(inVideoCodecPara->codec_id))){
                fprintf(stderr,"Cannot find valid video decoder.\n");
                return -1;
            }
    
            if(!(inCodecCtx = avcodec_alloc_context3(inCodec))){
                fprintf(stderr,"Cannot alloc valid decode codec context.\n");
                return -1;
            }
    
            if(avcodec_parameters_to_context(inCodecCtx,inVideoCodecPara) < 0){
                fprintf(stderr,"Cannot initialize parameters.\n");
                return -1;
            }
    
            // 打开编解码器
            if(avcodec_open2(inCodecCtx,inCodec,NULL) < 0){
                fprintf(stderr,"Cannot open codec.\n");
                return -1;
            }
    
            img_ctx = sws_getContext(inCodecCtx->width,inCodecCtx->height,inCodecCtx->pix_fmt,
                                        inCodecCtx->width,inCodecCtx->height,AV_PIX_FMT_YUV420P,SWS_BICUBIC,NULL,NULL,NULL);
            
            // 获取图像的大小
            int num_bytes = av_image_get_buffer_size(AV_PIX_FMT_YUV420P,inCodecCtx->width,inCodecCtx->height,1);
    
            // 创建out_buffer缓冲区
            uint8_t *out_buffer = (unsigned char *)av_malloc(num_bytes*sizeof(unsigned char));
            yuvFrame = av_frame_alloc();
            srcFrame = av_frame_alloc();
    
            // 将yuvframe和out_buffer进行关联
            int ret = av_image_fill_arrays(yuvFrame->data,yuvFrame->linesize,out_buffer,AV_PIX_FMT_YUV420P,inCodecCtx->width,inCodecCtx->height,1);
            if(ret < 0){
                fprintf(stderr,"Fill arrays failed.\n");
                return -1;
            }
            
    
            //----------------------------------------输出H264部分------------------------------------------//
            // 输出文件,编码器部分
            const char* out_file = "output.h264";
            if(avformat_alloc_output_context2(&outFmtCtx,NULL,NULL,out_file)  < 0){
                fprintf(stderr,"Cannot alloc output file context.\n");
                return -1;
            }
            outFmt = outFmtCtx->oformat;
    
            // 打开输出文件
            if(avio_open(&outFmtCtx->pb,out_file,AVIO_FLAG_READ_WRITE) < 0){
                fprintf(stderr,"output file open failed.\n");
                return -1;
            }
    
            // 创建保存的H264流,并设置参数
            outStream = avformat_new_stream(outFmtCtx,outCodec);
            if(outStream == NULL){
                fprintf(stderr,"create new video stream fialed.\n");
                return -1;
            }
    
            // 
            outStream->time_base.den = 30;
            outStream->time_base.num = 1;
    
            // 编码解码器相关的参数集
            // 设置分辨率和bit率
            AVCodecParameters *outCodecPara = outFmtCtx->streams[outStream->index]->codecpar;
            outCodecPara->codec_type=AVMEDIA_TYPE_VIDEO;
            outCodecPara->codec_id = outFmt->video_codec;
            outCodecPara->width = 640;
            outCodecPara->height = 360;
            outCodecPara->bit_rate = 92000;
    
            // 查找编码器
            outCodec = avcodec_find_encoder(outFmt->video_codec);
            if(outCodec == NULL){
                fprintf(stderr,"Cannot find any encoder.\n");
                return -1;
            }
    
            // 设置编码器内容
            outCodecCtx = avcodec_alloc_context3(outCodec);
            avcodec_parameters_to_context(outCodecCtx,outCodecPara);
            if(outCodecCtx==NULL){
                fprintf(stderr,"Cannot alloc output codec content.\n");
                return -1;
            }
    
            outCodecCtx->codec_id = outFmt->video_codec;
            outCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
            outCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
            outCodecCtx->width = inCodecCtx->width;
            outCodecCtx->height = inCodecCtx->height;
            outCodecCtx->time_base.num = 1;
            outCodecCtx->time_base.den = 30;
            outCodecCtx->bit_rate = 92000;
            outCodecCtx->gop_size = 10;
    
            // 根据编码器相关类型设置参数
            // 设置H264相关的参数,q的参数
            if(outCodecCtx->codec_id == AV_CODEC_ID_H264){
                outCodecCtx->qmin = 10;
                outCodecCtx->qmax = 51;
                outCodecCtx->qcompress = (float)0.6;
            }
            else if(outCodecCtx->codec_id == AV_CODEC_ID_MPEG2VIDEO){
                outCodecCtx->max_b_frames = 2;
            }
            else if(outCodecCtx->codec_id == AV_CODEC_ID_MPEG1VIDEO){
                outCodecCtx->mb_decision = 2;
            }
    
            // 打开编码器
            if(avcodec_open2(outCodecCtx,outCodec,NULL) < 0){
                fprintf(stderr,"Open encoder failed.\n");
                return -1;
            }
    
            // 设置yuvframe
            yuvFrame->format = outCodecCtx->pix_fmt;
            yuvFrame->width = outCodecCtx->width;
            yuvFrame->height = outCodecCtx->height;
    
            // 写H264的文件头
            ret = avformat_write_header(outFmtCtx,NULL);
            //------------------------编解码开始----------------------//
            int count = 0;
            // 读取一个frame的数据,放入pakcet中
            while(av_read_frame(inFmtCtx,inPkt) >= 0 && count < 50){
                // 判断是否是视频流
                if(inPkt->stream_index == inVideoSteamIndex){
                    // 解码
                    if(avcodec_send_packet(inCodecCtx,inPkt) >= 0){
                        // 判断是否解码完成
                        while((ret = avcodec_receive_frame(inCodecCtx,srcFrame)) >= 0){
                            if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF){
                                return -1;
                            }
                            else if(ret < 0){
                                fprintf(stderr, "Error during decoding\n");
                                exit(1);
                            }
                            // 解码完成
                            // 进行转换,由于摄像头输入的yuyv422的格式,因此需要进行转换,转换到yuv420p的格式
                            sws_scale(img_ctx,(const uint8_t* const*)srcFrame->data,srcFrame->linesize,
                                        0,inCodecCtx->height,yuvFrame->data,yuvFrame->linesize);
                            
                            yuvFrame->pts = srcFrame->pts;
    
                            // 解码完成之后进行编码
                            if(avcodec_send_frame(outCodecCtx,yuvFrame) >= 0){
                                if(avcodec_receive_packet(outCodecCtx,outPkt) >= 0){
                                    printf("----encode one frame-----\n");
                                    ++count;
                                    outPkt->stream_index = outStream->index;
                                    av_packet_rescale_ts(outPkt,outCodecCtx->time_base,outStream->time_base);
                                    outPkt->pos = -1;
                                    av_interleaved_write_frame(outFmtCtx,outPkt);
                                    av_packet_unref(outPkt);
                                }
                            }
    
                            // 短暂的延迟
                            usleep(1000 * 24);
                        }
                    }
                    av_packet_unref(inPkt);
                }
            }
    
            // 洗刷编码区
            ret = flush_encoder(outFmtCtx,outCodecCtx,outStream->index);
            if(ret < 0){
                fprintf(stderr,"flushing encoder failed.\n");
                return -1;
            }
    
            // 写H264的文件尾
            av_write_trailer(outFmtCtx);
        }while(0);
    
        // 释放内存
        av_packet_free(&inPkt);
        avcodec_free_context(&inCodecCtx);
        avcodec_close(inCodecCtx);
        avformat_close_input(&inFmtCtx);
        av_frame_free(&srcFrame);
        av_frame_free(&yuvFrame);
    
        av_packet_free(&outPkt);
        avcodec_free_context(&outCodecCtx);
        avcodec_close(outCodecCtx);
        avformat_close_input(&outFmtCtx);
    
        return 0;
    }
    
    

    7.Cmake
    本代码使用cmake对源码进行编译,cmake的参考如下所示。

    点击查看代码
    # cmake版本
    cmake_minimum_required(VERSION 3.5.1)
    project(Camera_PRJ)
    
    # 添加编译器选项
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
    
    SET(CMAKE_BUILD_TYPE "Debug")  
    SET(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -Wall -g") 
    
    # 打印相关信息
    message(STATUS "Cmake Version: " ${CMAKE_VERSION})
    
    # 设置可执行文件的目录
    set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
    
    # 添加头文件引用
    include_directories(/home/自己的路径/ffmpeg_build_share/include)
    
    # lib库目录
    link_directories(/home/自己的路径/ffmpeg_build_share/lib)
    
    
    # 可执行文件的输出名
    add_executable(Camera_PRJ encode_camera.c)
    
    # 链接lib库
    target_link_libraries(Camera_PRJ libavformat.so;libavdevice.so;libavcodec.so;libavutil.so;libswresample.so;libavfilter.so;libpostproc.so;libswscale.so;libSDL2.so)
    

    8.命令行编译

    点击查看代码
    g++ -g -o jpeg_Yuv420p  jpeg_Yuv420p.cpp `pkg-config --libs --cflags  libavformat libavcodec libavutil libswscale`
    
    posted @   相对维度  阅读(734)  评论(0编辑  收藏  举报
    相关博文:
    阅读排行:
    · 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
    · 没有源码,如何修改代码逻辑?
    · NetPad:一个.NET开源、跨平台的C#编辑器
    · PowerShell开发游戏 · 打蜜蜂
    · 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!
    点击右上角即可分享
    微信分享提示