ffmpeg 是一个完整的视频流解决方案,开源且有良好的跨平台性,ffmpeg具有强大的多媒体数据处理能力,能够实现视频的采集,多种视频格式间转换,给视频添加水印等多种功能,已被 VLC、Mplayer 等多种开源项目所采用,本系统客户端在进行 H.264 数据解码时利用 ffmpeg 来进行实现。

                              ffmpeg库 在vs2012中的调用

(1)下载地址:http://ffmpeg.zeranoe.com/builds/ 所需文件Builds(Dev)和Builds(Shared),ffmpeg库是在网上下载的最新版本20160409版

Builds(Dev):包含了所需要的.h头文件和.lib库文件

Builds(Shared):包含了所需要的dll文件

为了解决C99的兼容问题,在vs2012/vc/include中自行添加了auto_stdint.h;auto_inttype.h 如果不添加则会出现编译错误。对应的某些ffmpeg头文件 #include<stdint.h>和#include<inttype.h>都要相应的改为#include<auto_stdint.h>和#include <auto_inttype.h>
(2)包含文件路径,将Build(Dev)和Build(Shared)文件夹放到D:\FFMPEG中,在MFC工程中设置ffmpeg头文件位置

左侧 属性管理器->双击工程名->配置属性 -> C/C++ -> 常规 -> 附加包含目录,添加包含文件路径。

设置ffmpeg的lib文件位置 ,鼠标右键点击工程名,选择属性,然后选择 配置属性 -> 链接器 -> 常规 -> 附加库目录,添加库文件路径。

设置ffmpeg的所引用的lib文件 鼠标右键点击工程名,选择属性,然后选择 配置属性 -> 链接器 -> 输入 -> 附加依赖项,添加的文件为你下载的 Builds (Dev)中的lib 文件。avcodec.lib; avformat.lib; avutil.lib; swscale.lib; swresample.lib; avfilter.lib;swscale.lib。这些lib库在程序编译的时候没影响,但程序运行的时候需要他们。

经过测试发现由于FFMPEG版本的不同,很多函数在的使用方法都做了改变。该版本库有些函数已经声明为已否决,但使用中需要用到需要用到。ffmpeg该版本库使用过程中使用了三个声明已否决(attribute_deprecated)的函数avpicture_get_size; avpicture_fill; av_free_packet暂时未找到替代函数。若要使用这些函数,要将声明这些函数的头文件中的attribute_deprecated注释掉。

用ffmpeg实现H.264视频数据的解码

我们平时看到的视频文件有许多格式,比如 avi, mkv, rmvb, mov, mp4等等,这些被称为容器(Container), 不同的容器格式规定了其中音视频数据的组织方式(也包括其他数据,比如字幕等)。容器中一般会封装有视频和音频轨,也称为视频流(stream)和音频流,播放视频文件的第一步就是根据视频文件的格式,解析(demux)出其中封装的视频流、音频流以及字幕,解析的数据读到包 (packet)中,每个包里保存的是视频帧(frame)或音频帧,然后分别对视频帧和音频帧调用相应的解码器(decoder)进行解码,使用 H.264编码的视频会相应的调用H.264解码器,解码之后得到的就是原始的图像(YUV or RGB)。接下来简略介绍使用FFMPEG解码的步骤
(1)首先定义需要用到的结构体,在本客户端中,主要用到了以下结构体。
        AVFormatContext 保存需要读入的文件的格式信息,比如流的个数以及流数据等
        AVCodecContext  保存了相应流的详细编码信息,比如视频的宽、高,编码类型等。
        AVCodec 真正的编解码器,其中有编解码需要调用的函数
        AVFrame用于保存数据帧的数据结构
        AVFrame 用于保存转换之后的帧
        SwsContext 转换器,用于将YUV420P类型的图片转换为RGB类型
        AVPacket 解析文件时会将音/视频帧读入到packet中
(2)注册解码器,并且初始化自定义的AVIOContext,目的是在主机内存中申请内存空间,并将AVFormatContext的pb指针指向它。在使用avformat_open_input()打开媒体数据的时候,就可以不指定文件的URL了,即其第2个参数为NULL,读取的数据是由read_buffer()提供,read_buffer是回调函数,需要自定义read_buffer使其在视频解码时得到对应的数据。
        av_register_all();  
        pFormatCtx = avformat_alloc_context();  
        unsigned char *aviobuffer=(unsigned char *)av_malloc(1024*15); 
        AVIOContext*avio=avio_alloc_context(aviobuffer,1024*15 ,0,NULL,read_buffer, NU L L,NULL);  
        pFormatCtx->pb=avio;  
        int read_buffer(void *opaque, uint8_t *buf, int buf_size){......}
(3)读取接收到的数据的基本信息,用于设置解码器类型。avformat_open_input函数只是读取接收到的数据头,并不会填充流信息,因此我们需要接下来调用avformat_find_stream_info获取流信息,并确定所有的流信息。根据读取到的信息设置解码器的解码类型和解码的宽高度,使用avcodec_open2初始化解码器。
      if(avformat_open_input(&pFormatCtx,NULL,NULL,NULL)!=0){......}
      if(avformat_find_stream_info(pFormatCtx,NULL)<0){......}
      videoindex=-1;  
      for(i=0; i<pFormatCtx->nb_streams; i++)   {.....}//在这里查找视频流
      pCodecCtx=pFormatCtx->streams[videoindex]->codec;  
      pCodec=avcodec_find_decoder(pCodecCtx->codec_id);  
      if(avcodec_open2(pCodecCtx, pCodec,NULL)<0){......}
(4)使用av_read_frame可以将从内存中读取的一帧数据存入packet中。通过avcodec_decode_video2调用相应的解码器,将解码后的数据存入AVFrame 中。解码之后存入AVFrame中数数据是YUV420P类型的,Y、U、V分别存放在pFrame->data[0]、pFrame->data[1]、pFrame->data[2]中。主要用到的函数为:
       if(av_read_frame(pFormatCtx, packet)>=0){......}
       avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);

在MFC中实现视频的播放显示


         最终的目的是实现用MFC播放视频,但MFC中没有用于专门的视频播放控件,我们可以利用Picture Control控件实现类似功能。需要将Picture Control的类型改为Frame,通过FFMPEG库将YUV420P数据转换为RGB类型数据,创建BITMAPINFO,调用函数StretchDIBits将一帧数据显示在Picture Control控件上。

(1)客户端在MFC中创建了界面,其中将Picture Control的类型设置为Frame。通过FFMPEG库将YUV420P数据转换为RGB类型数据,由SwsContext类设置转换前和转换后额格式,调用库中的sws_scale函数可以得到RGB的纯数据。主要在使用sws_scale函数之前需要将YUV420p做180度的旋转,不然转换出来的RGB数据是上下颠倒的

(2) 得到了RGB信息之后要将一帧直接显示在Picture Control上还需要给RGB信息加上必要的头信息,使其成为BITMAP。定义BITMAPFILEHEADER ,BITMAPINFOHEADER 并给文件信息头和位图信息头赋值。

(3)在视频显示线程中,通过Picture Control的ID获取控件窗口的句柄、控件所在的矩形区域、控件的DC。调用函数StretchDIBits将一帧数据显示在Picture Control控件上,示例代码如下:

CWnd *pWnd=GetDlgItem(IDC_STATIC_PIC); //获得pictrue控件窗口的句柄
           CRect rect;
           pWnd->GetClientRect(&rect); //获得pictrue控件所在的矩形区域
           CDC *pDC=pWnd->GetDC(); //获得pictrue控件的DC
           pDC->SetStretchBltMode(COLORONCOLOR);
           StretchDIBits(pDC->GetSafeHdc(),0,0,rect.Width(),rect.Height(),0,0,pCodecCtx->width,pCodecCtx->height,pRGBFrame->data[0],                                pBmpInfo,DIB_RGB_COLORS,SRCCOPY);

部分代码:

SOCKET soc;
SOCKET control_soc;
AVFormatContext *pFormatCtx;
int             i, videoindex;
AVCodecContext  *pCodecCtx;
AVCodec         *pCodec;
AVFrame         *pFrame;
AVFrame         *pRGBFrame;
AVFrame         *pRGBFrameSave;
struct SwsContext *pSwsCtx;
int RGBsize;
uint8_t *outBuff;
char revBuffer[1024];
bool haveConnected;
bool isMonitor;
bool isShowContinue;
const char* cstr_c(CString str)
{char *chr;int strSize=0;strSize=WideCharToMultiByte(CP_ACP,0,str,-1,NULL,0,NULL,NULL);chr=(char*)malloc(strSize+1);WideCharToMultiByte(CP_ACP,0,str,-1,chr,strSize,NULL,NULL);const char* cchr=chr;return cchr;
}
DWORD WINAPI ReceiveThread ( LPVOID lParam )
{CMFCApplication1Dlg* pDlg = (CMFCApplication1Dlg*)lParam ;jrtplib::RTPSession rtpsess;jrtplib::RTPSessionParams sessionparams;jrtplib::RTPUDPv4TransmissionParams transparams;unsigned char *recvdata;int status;unsigned char nalu_header;unsigned char frameData[1024*15];int frameDatasize=5;sessionparams.SetOwnTimestampUnit(1/90000);//接收视频sessionparams.SetAcceptOwnPackets(true);transparams.SetPortbase(_ttoi(pDlg->m_port.GetString()));//_ttoi(pDlg->m_port.GetString())status=rtpsess.Create( sessionparams, &transparams );if(status<0)printf("rtpsessipon create error!\n");jrtplib::RTPIPv4Address remoteAdd(ntohl(inet_addr("192.168.2.5")));rtpsess.SetReceiveMode(jrtplib::RTPTransmitter::ReceiveMode::AcceptSome);rtpsess.AddToAcceptList(remoteAdd);av_register_all();  pFrame=av_frame_alloc();if(pFrame == NULL) pDlg->MessageBox ( L"avcodec alloc Outputframe failed!");    pCodec = avcodec_find_decoder(AV_CODEC_ID_H264);  if (!pCodec)   {  pDlg->MessageBox(_T("creat codec error"));}  pCodecCtx = avcodec_alloc_context3(pCodec);pCodecCtx->time_base.num = 1;  pCodecCtx->frame_number = 1; //每包一个视频帧  pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;  pCodecCtx->bit_rate = 0;  pCodecCtx->time_base.den = 25;//帧率  pCodecCtx->width = m_width;//视频宽  pCodecCtx->height =m_height;//视频高  if(avcodec_open2(pCodecCtx, pCodec,NULL) <0){pDlg->MessageBox ( L"Could not open codec.");  }pFrame=av_frame_alloc();AVPacket packet = {0};  int frameFinished = 0;//这个是随便填入数字,没什么作用  pRGBFrame=av_frame_alloc();if(pRGBFrame == NULL) {  pDlg->MessageBox ( L"avcodec alloc RGBframe failed!");   }  RGBsize= avpicture_get_size(AV_PIX_FMT_BGR24 ,m_width, m_height);outBuff = (uint8_t*)av_malloc(RGBsize);  if( outBuff == NULL ) {  pDlg->MessageBox (L"av malloc failed!");  }  avpicture_fill((AVPicture *)pRGBFrame, outBuff, AV_PIX_FMT_RGB24, m_width,m_height );pSwsCtx = sws_getContext(m_width, m_height,AV_PIX_FMT_YUV420P, m_width, m_height, AV_PIX_FMT_RGB24, SWS_BICUBIC, NULL, NULL, NULL); if (pSwsCtx== NULL) {pDlg->MessageBox (L"Cannot initialize the conversion context\n");}while(1){//rtpsess.Poll();rtpsess.BeginDataAccess();if( rtpsess.GotoFirstSourceWithData() ){do{jrtplib::RTPPacket *rtppack;while( ( rtppack = rtpsess.GetNextPacket() ) != NULL ){//printf("the packet length is :%d\n",rtppack->GetPacketLength());//printf("the sequence number is :%d\n",rtppack->GetSequenceNumber());recvdata = rtppack->GetPayloadData();                    memcpy(&frameData[frameDatasize],&recvdata[2],rtppack->GetPayloadLength()-2);  frameDatasize=frameDatasize+rtppack->GetPayloadLength()-2;if((recvdata[1]&0x80)==0x80)//如果作为第一个包则写起始码NALU—header{char strstart[4] = {0x00,0x00,0x00,0x01};memcpy(frameData,&strstart[0],4);nalu_header=(recvdata[0]&0x80) |(recvdata[0]&0x60)|(recvdata[1]&0x1f);memcpy(&frameData[4],&nalu_header,1);}if((recvdata[1]&0x40)==0x40)//作为一帧数据的最后一个包,要处理的事情,一帧接收完毕的处理{packet.data = frameData;packet.size = frameDatasize;//这个填入H264数据帧的大小  avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);  if(frameFinished)//成功解码  {  //反转图像 ,否则生成的图像是上下颠倒的  pFrame->data[0] += pFrame->linesize[0] * (pCodecCtx->height - 1);  pFrame->linesize[0] *= -1;  pFrame->data[1] += pFrame->linesize[1] * (pCodecCtx->height / 2 - 1);  pFrame->linesize[1] *= -1;  pFrame->data[2] += pFrame->linesize[2] * (pCodecCtx->height / 2 - 1);  pFrame->linesize[2] *= -1;  static uint8_t *p = NULL;p = pFrame->data[1];pFrame->data[1] = pFrame->data[2];pFrame->data[2] = p;sws_scale(pSwsCtx, pFrame->data,pFrame->linesize, 0,pCodecCtx->height,pRGBFrame->data, pRGBFrame->linesize); //把转换出来的RGB图像加上bitmap头,显示在picture control上 pDlg->SaveAsBMPandShow(0,NULL);}frameDatasize=5;}rtpsess.DeletePacket( rtppack );}}while( rtpsess.GotoNextSourceWithData() );}rtpsess.EndDataAccess();}rtpsess.BYEDestroy( jrtplib::RTPTime( 10, 0 ), 0, 0 );return 0;
}
void CMFCApplication1Dlg::ConnectToServer(int port)
{UpdateData(true);if(port==0){soc = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if(soc == INVALID_SOCKET){WSACleanup();this->MessageBox ( L"创建socket失败!" ) ;return;}sockaddr_in  serveaddress;serveaddress.sin_family = AF_INET;serveaddress.sin_port = htons(3333);      serveaddress.sin_addr.S_un.S_addr = inet_addr(cstr_c(m_ip.GetString()));       if(connect(soc, (sockaddr*)&serveaddress, sizeof(serveaddress))!=0 ){WSACleanup();this->MessageBox ( L"连接服务器端失败!" ) ;return;}    }Else{control_soc= socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if(control_soc== INVALID_SOCKET){WSACleanup();this->MessageBox ( L"创建控制socket失败!" ) ;return;}sockaddr_in  serveaddress;serveaddress.sin_family = AF_INET;serveaddress.sin_port = htons(port);serveaddress.sin_addr.S_un.S_addr = inet_addr(cstr_c(m_ip.GetString()));if(connect(control_soc, (sockaddr*)&serveaddress, sizeof(serveaddress))!=0 ){WSACleanup();this->MessageBox ( L"作为控制socket连接服务器端失败!" ) ;return;}    }
}
//停止h264解码并且释放所有资源
void CMFCApplication1Dlg::StopH264Decoder()
{if(pCodec!=NULL){pCodec->close(pCodecCtx);}if(pFrame){av_free(pFrame);  pFrame = NULL;  }if(pRGBFrame){av_free (pRGBFrame);pRGBFrame=NULL;}sws_freeContext (pSwsCtx);avformat_close_input(&pFormatCtx);
}
//将解码后的RGB数据加上bitmap的头信息等,并将其显示在picture control上
void CMFCApplication1Dlg::SaveAsBMPandShow(int isSave,CString filePath)
{BITMAPFILEHEADER bmpheader;  BITMAPINFOHEADER bmpinfo;  BITMAPINFO *pBmpInfo;   bmpheader.bfType = 0x4d42;  bmpheader.bfReserved1 = 0;  bmpheader.bfReserved2 = 0;  bmpheader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);  bmpheader.bfSize = bmpheader.bfOffBits +pCodecCtx->width*pCodecCtx->height*24/8;  bmpinfo.biSize = sizeof(BITMAPINFOHEADER);  bmpinfo.biWidth = pCodecCtx->width;  bmpinfo.biHeight =pCodecCtx->height;  bmpinfo.biPlanes = 1;  bmpinfo.biBitCount = 24;  bmpinfo.biCompression = BI_RGB;  bmpinfo.biSizeImage = (pCodecCtx->width*24+31)/32*4*pCodecCtx->height;  bmpinfo.biXPelsPerMeter = 100;  bmpinfo.biYPelsPerMeter = 100;  bmpinfo.biClrUsed = 0;  bmpinfo.biClrImportant = 0;  pBmpInfo = (BITMAPINFO *)new char[sizeof(BITMAPINFOHEADER)];memcpy(pBmpInfo,&bmpinfo,sizeof(BITMAPINFOHEADER));if(isSave==0)//显示{CWnd *pWnd=GetDlgItem(IDC_STATIC_PIC); //获得pictrue控件窗口的句柄CRect rect;pWnd->GetClientRect(&rect); //获得pictrue控件所在的矩形区域CDC *pDC=pWnd->GetDC(); //获得pictrue控件的DCpDC->SetStretchBltMode(COLORONCOLOR);StretchDIBits(pDC->GetSafeHdc(),0,0,rect.Width(),rect.Height(),0,0,pCodecCtx->width,pCodecCtx->height,pRGBFrame->data[0],pBmpInfo,DIB_RGB_COLORS,SRCCOPY);Sleep(1);}if(isSave==1)//c存储{FILE *r_fp1;fopen_s(&r_fp1,cstr_c(filePath),"wb");if(r_fp1==NULL)printf("open file failed!\n");fwrite(&bmpheader, sizeof(bmpheader), 1, r_fp1);  fwrite(&bmpinfo, sizeof(bmpinfo), 1, r_fp1);  fwrite(pRGBFrameSave->data[0],(pCodecCtx->width)*(pCodecCtx->height)*24/8, 1,r_fp1);  fclose(r_fp1);}
}
//初始化,控制连接和数据连接
void CMFCApplication1Dlg::OnBnClickedButton5()
{ConnectToServer(0);ConnectToServer(3334);if(soc!=NULL&&control_soc!=NULL)this->MessageBox ( L"初始化成功!" );elsethis->MessageBox ( L"初始化失败!" );
}
//按下开始,开始监控
void CMFCApplication1Dlg::OnBnClickedButton1()
{if (isMonitor ){char con[10]={"begin"};int i;i=send(control_soc, con, strlen(con), 0);CString videoinfo;CTime tm;tm=CTime::GetCurrentTime();videoinfo.Format(_T("接收到的视频宽度为:%d ,接收到的视频高度为:%d     接收到的视频编码类型为:H.264"),pCodecCtx->width,pCodecCtx->height);videoinfo=tm.Format(_T("视频监控开始的时间为:%X   "))+videoinfo; //%X是显示时间的时分秒m_videoInfo.SetWindowTextW(videoinfo);}else{haveConnected=TRUE;char con[10]={"begin"};int i;i=send(control_soc, con, strlen(con), 0);m_videoInfo.SetWindowTextW(_T("请等待获取视频相关信息....................."));HANDLE hThread = CreateThread ( NULL, 0, MonitorThread, this, 0, NULL ) ;if ( hThread == NULL ){this->MessageBox ( L"创建视频监视器失败!" );}else{isMonitor=TRUE;}}
}
//停止显示,关闭连接并释放相关资源
void CMFCApplication1Dlg::OnBnClickedButton2()
{  //发送结束命令char con[10]={"end"};int i;i=send(control_soc, con, strlen(con), 0);m_videoInfo.SetWindowTextW(_T("视频监控已停止,服务器已停止视频采集。"));
}
void CMFCApplication1Dlg::OnBnClickedButton3()
{  isShowContinue=FALSE;pRGBFrameSave=av_frame_alloc();*pRGBFrameSave=*pRGBFrame;CString defaultDir = L"E:\\FileTest";    //默认打开的文件路径CString fileName = L"1.bmp";          //默认打开的文件名CString filter = L"文件*.bmp;";  //文件过虑的类型CFileDialog openFileDlg(0, defaultDir, fileName, OFN_HIDEREADONLY|OFN_OVERWRITEPROMPT, filter, NULL);openFileDlg.GetOFN().lpstrInitialDir = L"C:\\Users\\Administrator\\Desktop\\1.bmp";INT_PTR result = openFileDlg.DoModal();CString filePath = defaultDir + "\\" + fileName;if(result == IDOK) {filePath = openFileDlg.GetPathName();}SaveAsBMPandShow(1,filePath);if(pRGBFrameSave){av_free(pRGBFrameSave);  pRGBFrameSave = NULL;  }
}
//继续显示
void CMFCApplication1Dlg::OnBnClickedButton4()
{isShowContinue=TRUE;
}

ffmpeg解码H.264视频数据,MFC播放视频相关推荐

  1. 音视频系列2:ffmpeg将H.264解码为RGB

    音视频系列2:ffmpeg将H.264解码为RGB 前言 源码 前言 喜大普奔,终于更新啦,上期说到,如何使用ffmpeg+rtmp进行拉流,不熟悉的小伙伴们,可以先看上一期.今天我们要实现的是使用f ...

  2. FFmpeg的H.264解码器源代码简单分析:宏块解码(Decode)部分-帧间宏块(Inter)

    ===================================================== H.264源代码分析文章列表: [编码 - x264] x264源代码简单分析:概述 x26 ...

  3. FFmpeg的H.264解码器源代码简单分析:宏块解码(Decode)部分-帧间宏块(Inter)...

    ===================================================== H.264源代码分析文章列表: [编码 - x264] x264源代码简单分析:概述 x26 ...

  4. FFmpeg的H.264解码器源代码简单分析:宏块解码(Decode)部分-帧内宏块(Intra)

    ===================================================== H.264源代码分析文章列表: [编码 - x264] x264源代码简单分析:概述 x26 ...

  5. FFmpeg的H.264解码器源代码简单分析:宏块解码(Decode)部分-帧内宏块(Intra)...

    ===================================================== H.264源代码分析文章列表: [编码 - x264] x264源代码简单分析:概述 x26 ...

  6. FFmpeg的H.264解码器源代码简单分析:解码器主干部分

    ===================================================== H.264源代码分析文章列表: [编码 - x264] x264源代码简单分析:概述 x26 ...

  7. FFmpeg的H.264解码器源代码简单分析:环路滤波(Loop Filter)部分

    ===================================================== H.264源代码分析文章列表: [编码 - x264] x264源代码简单分析:概述 x26 ...

  8. FFmpeg的H.264解码器源代码简单分析:概述

    ===================================================== H.264源代码分析文章列表: [编码 - x264] x264源代码简单分析:概述 x26 ...

  9. [工具]利用EasyRTSPClient工具检查摄像机RTSP流不能播放原因以及排查音视频数据无法播放问题...

    出现问题 我们在做流媒体开发的过程中,进程会出现摄像机RTSP流莫名其妙无法播放的问题,而我们常用的vlc经常是直接弹出一个无法播放的提示框就完事了,没有说明出错的原因,或者在vlc的消息里面能看到日 ...

最新文章

  1. 面试官:如果要存ip地址,用什么数据类型比较好
  2. J2ME程序开发新手入门九大要点
  3. VC++动态创建和删除菜单(转)
  4. 简单介绍Python中的几种数据类型
  5. 月收入两千,负债40万,怎么来处理?
  6. VC 对话框背景颜色 控件颜色
  7. Spark Structured : HIve jdbc方式访问待下划线的表,找不到表的错误
  8. 如何提高VS2010的性能,VS2010不再…
  9. latex入门到精通----IEEE模块为例
  10. “Google只认钱!机器学习20年没进步”,CMU学者炮轰AI第一大厂
  11. Revit中导入的CAD标高不统一处理及“标高管理”
  12. [家里蹲大学数学杂志]第039期高等数学习题集
  13. android按钮延迟显示出来,android Toast显示延迟的优化方案
  14. 【图像超分辨率】Deep Learning for Image Super-resolution: A Survey
  15. python udp socket远程主机强迫关闭_Python 远程主机强迫关闭了一个现有的连接
  16. 【MATLAB】P图神器,初露锋芒:第一周作业
  17. 3D游戏设计-天空盒
  18. spark-2.2.0发行说明
  19. ReRes 谷歌浏览器插件使用
  20. 写给养花小白的水培小教程

热门文章

  1. Css 分类 属性 选择器
  2. 代码同时托管到github和git.oschina.net
  3. nginx+tomcat 动静分离
  4. js获取滚动条距离浏览器顶部,底部的高度,兼容ie和firefox
  5. Ubuntu下Android编译环境的配置
  6. 金数据一个不错的调查平台
  7. Applying Multicycle Exceptions in the TimeQuest Timing Analyzer--Altera Note
  8. 远程办公(2)-重新定义“雇佣关系”:交易成果,而不是时间
  9. linux改变文件所属用户和组
  10. C语言二叉树字符统计,C语言实现二叉树-利用二叉树统计单词数目