关闭

基于v4l2的webcam应用, 本地预监

275人阅读 评论(0) 收藏 举报
分类:

转自:http://blog.csdn.net/sunkwei/article/details/6530343

今天尝试编写了一个基于 v4l2 的摄像头应用, 目前仅仅实现从摄像头捕捉视频, 然后本地回显.

 

照例先上效果图, 其中左侧小点为预监窗口, 右侧为经过 x264 压缩, tcp 传输, libavcodec 解压, 再用 qt 显示的效果., 延迟很低很低 :)

 

 

主要就是以下几个知识点: 

    1. v4l2接口:

    2. X11的本地回显:

    3. 使用 libswscale 进行拉伸:

    4. 使用 libx264 压缩:

 

1. v4l2接口: 大眼一看, 密密丫丫的 VIDIOC_XXXX, 其实静下心来, 也没多少, 很清晰, 大体流程如下:

             capture_open(name)

                      open /dev/video0         // 打开设备

                      check driver caps          // 检查一些 caps

                      VIDIOC_REQBUFS         // 使用 streaming mode,  mmap mode, 分配

                          VIDIOC_QUERYBUF       // 获取分配的buf, 并且mmap到进程空间

                          mmap

                       VIDIOC_QBUF              // buf 入列

                       VIDIOC_STREAMON      // 开始

 

使用的数据结构

  1. struct Buffer  
  2. {  
  3.     void *start;  
  4.     size_t length;  
  5. };  
  6. typedef struct Buffer Buffer;  
  7. struct Ctx  
  8. {  
  9.     int vid;  
  10.     int width, height;  // 输出图像大小  
  11.     struct SwsContext *sws; // 用于转换  
  12.     int rows;   // 用于 sws_scale()  
  13.     int bytesperrow; // 用于cp到 pic_src  
  14.     AVPicture pic_src, pic_target;  // 用于 sws_scale  
  15.     Buffer bufs[2];     // 用于 mmap  
  16. };  
  17. typedef struct Ctx Ctx;  
 

capture_open(...) 打开设备

  1. void *capture_open (const char *dev_name, int t_width, int t_height)  
  2. {  
  3.     int id = open(dev_name, O_RDWR);  
  4.     if (id < 0) return 0;  
  5.     Ctx *ctx = new Ctx;  
  6.     ctx->vid = id;  
  7.     // to query caps  
  8.     v4l2_capability caps;  
  9.     ioctl(id, VIDIOC_QUERYCAP, &caps);  
  10.     if (caps.capabilities & V4L2_CAP_VIDEO_CAPTURE) {  
  11.         if (caps.capabilities & V4L2_CAP_READWRITE) {  
  12.             // TODO: ...  
  13.         }  
  14.         if (caps.capabilities & V4L2_CAP_STREAMING) {  
  15.             // 检查是否支持 MMAP, 还是 USERPTR  
  16.             v4l2_requestbuffers bufs;  
  17.             memset(&bufs, 0, sizeof(bufs));  
  18.             bufs.count = 2;  
  19.             bufs.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;  
  20.             bufs.memory = V4L2_MEMORY_MMAP;  
  21.             if (ioctl(id, VIDIOC_REQBUFS, &bufs) < 0) {  
  22.                 fprintf(stderr, "%s: don't support MEMORY_MMAP mode!/n", __func__);  
  23.                 close(id);  
  24.                 delete ctx;  
  25.                 return 0;  
  26.             }  
  27.             fprintf(stderr, "%s: using MEMORY_MMAP mode, buf cnt=%d/n", __func__, bufs.count);  
  28.             // mmap  
  29.             for (int i = 0; i < 2; i++) {  
  30.                 v4l2_buffer buf;  
  31.                 memset(&buf, 0, sizeof(buf));  
  32.                 buf.type = bufs.type;  
  33.                 buf.memory = bufs.memory;  
  34.                 if (ioctl(id, VIDIOC_QUERYBUF, &buf) < 0) {  
  35.                     fprintf(stderr, "%s: VIDIOC_QUERYBUF ERR/n", __func__);  
  36.                     close(id);  
  37.                     delete ctx;  
  38.                     return 0;  
  39.                 }  
  40.                 ctx->bufs[i].length = buf.length;  
  41.                 ctx->bufs[i].start = mmap(0, buf.length, PROT_READ|PROT_WRITE,  
  42.                         MAP_SHARED, id, buf.m.offset);  
  43.             }  
  44.         }  
  45.         else {  
  46.             fprintf(stderr, "%s: can't support read()/write() mode and streaming mode/n", __func__);  
  47.             close(id);  
  48.             delete ctx;  
  49.             return 0;  
  50.         }  
  51.     }  
  52.     else {  
  53.         fprintf(stderr, "%s: can't support video capture!/n", __func__);  
  54.         close(id);  
  55.         delete ctx;  
  56.         return 0;  
  57.     }  
  58.     int rc;  
  59.     // enum all support image fmt  
  60.     v4l2_fmtdesc fmt_desc;  
  61.     uint32_t index = 0;  
  62.     // 看起来, 不支持 plane fmt, 直接使用 yuyv 吧, 然后使用 libswscale 转换  
  63. #if 0     
  64.     do {  
  65.         fmt_desc.index = index;  
  66.         fmt_desc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;  
  67.         rc = ioctl(id, VIDIOC_ENUM_FMT, &fmt_desc);  
  68.         if (rc >= 0) {  
  69.             fprintf(stderr, "/t support %s/n", fmt_desc.description);  
  70.         }  
  71.         index++;  
  72.     } while (rc >= 0);  
  73. #endif // 0  
  74.     v4l2_format fmt;  
  75.     fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;  
  76.     rc = ioctl(id, VIDIOC_G_FMT, &fmt);  
  77.     if (rc < 0) {  
  78.         fprintf(stderr, "%s: can't VIDIOC_G_FMT.../n", __func__);  
  79.         return 0;  
  80.     }  
  81.     PixelFormat pixfmt = PIX_FMT_NONE;  
  82.     switch (fmt.fmt.pix.pixelformat) {  
  83.     case V4L2_PIX_FMT_YUYV:  
  84.         pixfmt = PIX_FMT_YUYV422;  
  85.         break;  
  86.     }  
  87.     if (pixfmt == PIX_FMT_NONE) {  
  88.         fprintf(stderr, "%s: can't support %4s/n", __func__, (char*)&fmt.fmt.pix.pixelformat);  
  89.         return 0;  
  90.     }  
  91.     // 构造转换器  
  92.     fprintf(stderr, "capture_width=%d, height=%d, stride=%d/n", fmt.fmt.pix.width, fmt.fmt.pix.height,  
  93.             fmt.fmt.pix.bytesperline);  
  94.     ctx->width = t_width;  
  95.     ctx->height = t_height;  
  96.     ctx->sws = sws_getContext(fmt.fmt.pix.width, fmt.fmt.pix.height, pixfmt,  
  97.             ctx->width, ctx->height, PIX_FMT_YUV420P,     // PIX_FMT_YUV420P 对应 X264_CSP_I420  
  98.             SWS_FAST_BILINEAR, 0, 0, 0);  
  99.     ctx->rows = fmt.fmt.pix.height;  
  100.     ctx->bytesperrow = fmt.fmt.pix.bytesperline;  
  101.     avpicture_alloc(&ctx->pic_target, PIX_FMT_YUV420P, ctx->width, ctx->height);  
  102.     // queue buf  
  103.     for (int i = 0; i < sizeof(ctx->bufs)/sizeof(Buffer); i++) {  
  104.         v4l2_buffer buf;  
  105.         memset(&buf, 0, sizeof(buf));  
  106.         buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;  
  107.         buf.memory = V4L2_MEMORY_MMAP;  
  108.         buf.index = i;  
  109.         if (ioctl(id, VIDIOC_QBUF, &buf) < 0) {  
  110.             fprintf(stderr, "%s: VIDIOC_QBUF err/n", __func__);  
  111.             exit(-1);  
  112.         }  
  113.     }  
  114.     int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;  
  115.     if (ioctl(id, VIDIOC_STREAMON, &type) < 0) {  
  116.         fprintf(stderr, "%s: VIDIOC_STREAMON err/n", __func__);  
  117.         exit(-1);  
  118.     }  
  119.     return ctx;  
  120. }  
 

 

              capture_get_pic()

                       VIDIOC_DQBUF            // 出列, 

                       sws_scale                    // 格式转换/拉伸到 PIX_FMT_YUV420P, 准备方便压缩

                       VIDIOC_QBUF              // 重新入列

capture_get_picture(...) 从摄像头得到一帧图片

  1. int capture_get_picture (void *id, Picture *pic)  
  2. {  
  3.     // 获取, 转换  
  4.     Ctx *ctx = (Ctx*)id;  
  5.     v4l2_buffer buf;  
  6.     memset(&buf, 0, sizeof(buf));  
  7.     buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;  
  8.     buf.memory = V4L2_MEMORY_MMAP;  
  9.     if (ioctl(ctx->vid, VIDIOC_DQBUF, &buf) < 0) {  
  10.         fprintf(stderr, "%s: VIDIOC_DQBUF err/n", __func__);  
  11.         return -1;  
  12.     }  
  13. //  _save_pic(ctx->bufs[buf.index].start, buf.length);  
  14. //  __asm("int $3");  
  15.     ctx->pic_src.data[0] = (unsigned char*)ctx->bufs[buf.index].start;  
  16.     ctx->pic_src.data[1] = ctx->pic_src.data[2] = ctx->pic_src.data[3] = 0;  
  17.     ctx->pic_src.linesize[0] = ctx->bytesperrow;  
  18.     ctx->pic_src.linesize[1] = ctx->pic_src.linesize[2] = ctx->pic_src.linesize[3] = 0;  
  19.     // sws_scale  
  20.     int rs = sws_scale(ctx->sws, ctx->pic_src.data, ctx->pic_src.linesize,  
  21.             0, ctx->rows, ctx->pic_target.data, ctx->pic_target.linesize);  
  22.     // out  
  23.     for (int i = 0; i < 4; i++) {  
  24.         pic->data[i] = ctx->pic_target.data[i];  
  25.         pic->stride[i] = ctx->pic_target.linesize[i];  
  26.     }  
  27.     // re queue buf  
  28.     if (ioctl(ctx->vid, VIDIOC_QBUF, &buf) < 0) {  
  29.         fprintf(stderr, "%s: VIDIOC_QBUF err/n", __func__);  
  30.         return -1;  
  31.     }  
  32.     return 1;  
  33. }  
 

 

2.  X11 的本地回显: 采用 XShm, 效率还行

              vs_open ()

                        XOpenDisplay()

                        XCreateSimpleWindow()

                        XCreateGC()

                        XMapWindow()

                        XShmCreateImage()

                        shmget()

                        shmat()

 

使用的数据结构

  1. struct Ctx  
  2. {  
  3.     Display *display;  
  4.     int screen;  
  5.     Window window;  
  6.     GC gc;  
  7.     XVisualInfo vinfo;  
  8.     XImage *image;  
  9.     XShmSegmentInfo segment;  
  10.     SwsContext *sws;  
  11.     PixelFormat target_pixfmt;  
  12.     AVPicture pic_target;  
  13.     int v_width, v_height;  
  14.     int curr_width, curr_height;  
  15. };  
  16. typedef struct Ctx Ctx;  
 

vs_open(...) 打开设备

  1. void *vs_open (int v_width, int v_height)  
  2. {  
  3.     Ctx *ctx = new Ctx;  
  4.     ctx->v_width = v_width;  
  5.     ctx->v_height = v_height;  
  6.     // window  
  7.     ctx->display = XOpenDisplay(0);  
  8.     ctx->window = XCreateSimpleWindow(ctx->display, RootWindow(ctx->display, 0),  
  9.             100, 100, v_width, v_height, 0, BlackPixel(ctx->display, 0),  
  10.             WhitePixel(ctx->display, 0));  
  11.     ctx->screen = 0;  
  12.     ctx->gc = XCreateGC(ctx->display, ctx->window, 0, 0);  
  13.       
  14.     XMapWindow(ctx->display, ctx->window);  
  15.     // current screen pix fmt  
  16.     Window root;  
  17.     unsigned int cx, cy, border, depth;  
  18.     int x, y;  
  19.     XGetGeometry(ctx->display, ctx->window, &root, &x, &y, &cx, &cy, &border, &depth);  
  20.     // visual info  
  21.     XMatchVisualInfo(ctx->display, ctx->screen, depth, DirectColor, &ctx->vinfo);  
  22.     // image  
  23.     ctx->image = XShmCreateImage(ctx->display, ctx->vinfo.visual, depth, ZPixmap, 0,  
  24.             &ctx->segment, cx, cy);  
  25.     if (!ctx->image) {  
  26.         fprintf(stderr, "%s: can't XShmCreateImage !/n", __func__);  
  27.         exit(-1);  
  28.     }  
  29.     ctx->segment.shmid = shmget(IPC_PRIVATE,  
  30.             ctx->image->bytes_per_line * ctx->image->height,   
  31.             IPC_CREAT | 0777);  
  32.     if (ctx->segment.shmid < 0) {  
  33.         fprintf(stderr, "%s: shmget err/n", __func__);  
  34.         exit(-1);  
  35.     }  
  36.     ctx->segment.shmaddr = (char*)shmat(ctx->segment.shmid, 0, 0);  
  37.     if (ctx->segment.shmaddr == (char*)-1) {  
  38.         fprintf(stderr, "%s: shmat err/n", __func__);  
  39.         exit(-1);  
  40.     }  
  41.     ctx->image->data = ctx->segment.shmaddr;  
  42.     ctx->segment.readOnly = 0;  
  43.     XShmAttach(ctx->display, &ctx->segment);  
  44.     PixelFormat target_pix_fmt = PIX_FMT_NONE;  
  45.     switch (ctx->image->bits_per_pixel) {  
  46.         case 32:  
  47.             target_pix_fmt = PIX_FMT_RGB32;  
  48.             break;  
  49.         case 24:  
  50.             target_pix_fmt = PIX_FMT_RGB24;  
  51.             break;  
  52.         default:  
  53.             break;  
  54.     }  
  55.     if (target_pix_fmt == PIX_FMT_NONE) {  
  56.         fprintf(stderr, "%s: screen depth format err/n", __func__);  
  57.         delete ctx;  
  58.         return 0;  
  59.     }  
  60.     // sws  
  61.     ctx->target_pixfmt = target_pix_fmt;  
  62.     ctx->curr_width = cx;  
  63.     ctx->curr_height = cy;  
  64.     ctx->sws = sws_getContext(v_width, v_height, PIX_FMT_YUV420P,  
  65.             cx, cy, target_pix_fmt,  
  66.             SWS_FAST_BILINEAR, 0, 0, 0);  
  67.     avpicture_alloc(&ctx->pic_target, target_pix_fmt, cx, cy);  
  68.     XFlush(ctx->display);  
  69.     return ctx;  
  70. }  
 

 

               vs_show()

                        sws_scale()                // 拉伸到当前窗口大小, 转换格式

                        XShmPutImage()        // 显示, 呵呵, 真的很简单

 

vs_show(...) 主要代码都是处理窗口变化的

  1. int vs_show (void *ctx, unsigned char *data[4], int stride[4])  
  2. {  
  3.     // 首选检查 sws 是否有效, 根据当前窗口大小决定  
  4.     Ctx *c = (Ctx*)ctx;  
  5.     Window root;  
  6.     int x, y;  
  7.     unsigned int cx, cy, border, depth;  
  8.     XGetGeometry(c->display, c->window, &root, &x, &y, &cx, &cy, &border, &depth);  
  9.     if (cx != c->curr_width || cy != c->curr_height) {  
  10.         avpicture_free(&c->pic_target);  
  11.         sws_freeContext(c->sws);  
  12.         c->sws = sws_getContext(c->v_width, c->v_height, PIX_FMT_YUV420P,  
  13.                 cx, cy, c->target_pixfmt,   
  14.                 SWS_FAST_BILINEAR, 0, 0, 0);  
  15.         avpicture_alloc(&c->pic_target, c->target_pixfmt, cx, cy);  
  16.         c->curr_width = cx;  
  17.         c->curr_height = cy;  
  18.         // re create image  
  19.         XShmDetach(c->display, &c->segment);  
  20.         shmdt(c->segment.shmaddr);  
  21.         shmctl(c->segment.shmid, IPC_RMID, 0);  
  22.         XDestroyImage(c->image);  
  23.         c->image = XShmCreateImage(c->display, c->vinfo.visual, depth, ZPixmap, 0,  
  24.             &c->segment, cx, cy);  
  25.         c->segment.shmid = shmget(IPC_PRIVATE,  
  26.                 c->image->bytes_per_line * c->image->height,  
  27.                 IPC_CREAT | 0777);  
  28.         c->segment.shmaddr = (char*)shmat(c->segment.shmid, 0, 0);  
  29.         c->image->data = c->segment.shmaddr;  
  30.         c->segment.readOnly = 0;  
  31.         XShmAttach(c->display, &c->segment);  
  32.     }  
  33.     //   
  34.     sws_scale(c->sws, data, stride, 0, c->v_height, c->pic_target.data, c->pic_target.linesize);  
  35.     // cp to image  
  36.     unsigned char *p = c->pic_target.data[0], *q = (unsigned char*)c->image->data;  
  37.     int xx = MIN(c->image->bytes_per_line, c->pic_target.linesize[0]);  
  38.     for (int i = 0; i < c->curr_height; i++) {  
  39.         memcpy(q, p, xx);  
  40.         p += c->image->bytes_per_line;  
  41.         q += c->pic_target.linesize[0];  
  42.     }  
  43.     // 显示到 X 上  
  44.     XShmPutImage(c->display, c->window, c->gc, c->image, 0, 0, 0, 0, c->curr_width, c->curr_height, 1);  
  45.     return 1;  
  46. }  
 

3. libswscale: 用于picture格式/大小转换, 占用cpu挺高 :), 用起来很简单, 基本就是

                sws = sws_getContext(....);

                sws_scale(sws, ...)

 

4. libx264 压缩: 考虑主要用于互动, 所以使用 preset=fast, tune=zerolatency, 320x240, 10fps, 300kbps, jj实测延迟很低, 小于 100ms

 

使用的数据结构

  1. struct Ctx  
  2. {  
  3.     x264_t *x264;  
  4.     x264_picture_t picture;  
  5.     x264_param_t param;  
  6.     void *output;       // 用于保存编码后的完整帧  
  7.     int output_bufsize, output_datasize;  
  8.     int64_t pts;        // 输入 pts  
  9.     int64_t (*get_pts)(struct Ctx *);  
  10.     int64_t info_pts, info_dts;  
  11.     int info_key_frame;  
  12.     int info_valid;  
  13. };  
 

 

    vc_open(...) 设置必要的参数, 打开编码器

  1. void *vc_open (int width, int height)  
  2. {  
  3.     Ctx *ctx = new Ctx;  
  4.     // 设置编码属性  
  5.     //x264_param_default(&ctx->param);  
  6.     x264_param_default_preset(&ctx->param, "fast""zerolatency");  
  7.     ctx->param.i_width = width;  
  8.     ctx->param.i_height = height;  
  9.     ctx->param.b_repeat_headers = 1;  // 重复SPS/PPS 放到关键帧前面  
  10.     ctx->param.b_cabac = 1;  
  11.     ctx->param.i_fps_num = 10;  
  12.     ctx->param.i_fps_den = 1;  
  13.     ctx->param.i_keyint_max = 30;  
  14.     ctx->param.i_keyint_min = 10;  
  15.     // rc  
  16.     ctx->param.rc.i_rc_method = X264_RC_CRF;  
  17.     ctx->param.rc.i_bitrate = 300;  
  18.     //ctx->param.rc.f_rate_tolerance = 0.1;  
  19.     //ctx->param.rc.i_vbv_max_bitrate = ctx->param.rc.i_bitrate * 1.3;  
  20.     //ctx->param.rc.f_rf_constant = 600;  
  21.     //ctx->param.rc.f_rf_constant_max = ctx->param.rc.f_rf_constant * 1.3;  
  22. #ifdef DEBUG  
  23.     ctx->param.i_log_level = X264_LOG_WARNING;  
  24. #else  
  25.     ctx->param.i_log_level = X264_LOG_NONE;  
  26. #endif // release  
  27.     ctx->x264 = x264_encoder_open(&ctx->param);  
  28.     if (!ctx->x264) {  
  29.         fprintf(stderr, "%s: x264_encoder_open err/n", __func__);  
  30.         delete ctx;  
  31.         return 0;  
  32.     }  
  33.     x264_picture_init(&ctx->picture);  
  34.     ctx->picture.img.i_csp = X264_CSP_I420;  
  35.     ctx->picture.img.i_plane = 3;  
  36.     ctx->output = malloc(128*1024);  
  37.     ctx->output_bufsize = 128*1024;  
  38.     ctx->output_datasize = 0;  
  39.     ctx->get_pts = first_pts;  
  40.     ctx->info_valid = 0;  
  41.     return ctx;  
  42. }  
 

vc_compress(...) 压缩, 如果成功, 得到串流

  1. static int encode_nals (Ctx *c, x264_nal_t *nals, int nal_cnt)  
  2. {  
  3.     char *pout = (char*)c->output;  
  4.     c->output_datasize = 0;  
  5.     for (int i = 0; i < nal_cnt; i++) {  
  6.         if (c->output_datasize + nals[i].i_payload > c->output_bufsize) {  
  7.             // 扩展  
  8.             c->output_bufsize = (c->output_datasize+nals[i].i_payload+4095)/4096*4096;  
  9.             c->output = realloc(c->output, c->output_bufsize);  
  10.         }  
  11.         memcpy(pout+c->output_datasize, nals[i].p_payload, nals[i].i_payload);  
  12.         c->output_datasize += nals[i].i_payload;  
  13.     }  
  14.     return c->output_datasize;  
  15. }  
  16. int vc_compress (void *ctx, unsigned char *data[4], int stride[4], const void **outint *len)  
  17. {  
  18.     Ctx *c = (Ctx*)ctx;  
  19.       
  20.     // 设置 picture 数据  
  21.     for (int i = 0; i < 4; i++) {  
  22.         c->picture.img.plane[i] = data[i];  
  23.         c->picture.img.i_stride[i] = stride[i];  
  24.     }  
  25.     // encode  
  26.     x264_nal_t *nals;  
  27.     int nal_cnt;  
  28.     x264_picture_t pic_out;  
  29.       
  30.     c->picture.i_pts = c->get_pts(c);  
  31. #ifdef DEBUG_MORE  
  32.     static int64_t _last_pts = c->picture.i_pts;  
  33.     fprintf(stderr, "DBG: pts delta = %lld/n", c->picture.i_pts - _last_pts);  
  34.     _last_pts = c->picture.i_pts;  
  35. #endif //  
  36.     x264_picture_t *pic = &c->picture;  
  37.     do {  
  38.         // 这里努力消耗掉 delayed frames ???  
  39.         // 实际使用 zerolatency preset 时, 效果足够好了  
  40.         int rc = x264_encoder_encode(c->x264, &nals, &nal_cnt, pic, &pic_out);  
  41.         if (rc < 0) return -1;  
  42.         encode_nals(c, nals, nal_cnt);  
  43.     } while (0);  
  44.     *out = c->output;  
  45.     *len = c->output_datasize;  
  46.     if (nal_cnt > 0) {  
  47.         c->info_valid = 1;  
  48.         c->info_key_frame = pic_out.b_keyframe;  
  49.         c->info_pts = pic_out.i_pts;  
  50.         c->info_dts = pic_out.i_dts;  
  51.     }  
  52.     else {  
  53.         fprintf(stderr, ".");  
  54.         return 0; // 继续  
  55.     }  
  56.  
  57. #ifdef DEBUG_MORE  
  58.     static size_t _seq = 0;  
  59.     fprintf(stderr, "#%lu: [%c] frame type=%d, size=%d/n", _seq,   
  60.             pic_out.b_keyframe ? '*' : '.',   
  61.             pic_out.i_type, c->output_datasize);  
  62.     _seq++;  
  63. #endif // debug  
  64.     return 1;  
  65. }  
 

 

附上源码: 唉, 源码是不停更新的, csdn居然没有一个类似 git, svn 之类的仓库, 算了, 如果有人要, email吧.

      main.cpp          主流程

      capture.cpp, capture.h    获取 v4l2 的图像帧

      vcompress.cpp vcompress.h 实现 x264 的压缩

      vshow.cpp vsho.h   用 X11 显示实时图像


0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:14245次
    • 积分:263
    • 等级:
    • 排名:千里之外
    • 原创:6篇
    • 转载:36篇
    • 译文:0篇
    • 评论:1条