2019-10-24更新:

1.更新为ffmpeg4.1,同时支持播放h265。

下载地址:https://download.csdn.net/download/qq214517703/11914710

Github地址:https://github.com/yundiantech/FFMPEG_DEMO/tree/master/source/VideoDecode

代码讲解视频地址:http://blog.yundiantech.com/?log=blog&id=41

以下例子中的代码使用的是ffmpeg2.5.2,我已经放弃它了,也不建议大家看了。

我不打算删除,留着做个纪念吧。

现在,我们已经简单的掌握了h.264数据的结构。是时候干点什么了,那就先来写一个H.264视频播放器吧。。

前面我们开发视频播放器的时候是通过:

avformat_open_input打开视频文件,然后再调用av_read_frame就可以读到一帧帧的数据了,

当然用这样的方法也可以直接打开并读取一个h.264文件,但是这样就违背了我们的初衷了,我们的目的是对上一节《H264数据格式讲解》的实践,因此我们采用C语言的文件操作直接读取文件然后再解析。

一个H264播放器的实现步骤大致如下:

一、从H.264文件中获取一个NALU

从上节可以知道,h264的NALU直接是用帧头(0x00000001或0x000001)隔开的,因此我们就逐个字节搜索,直到遇到h264的帧头,2个帧头之间的数据就是一个Nalu,既视为获取到了一帧h264视频数据。

查找一个nalu数据的代码大致如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

NALU_t* h264Reader::getNextNal()

{

    ///首先查找第一个起始码

    int pos = 0; //记录当前处理的数据偏移量

    int StartCode = 0;

    while(1)

    {

        unsigned char* Buf = mH264Buffer + pos;

        int lenth = mBufferSize - pos; //剩余没有处理的数据长度

        if (lenth <= 4)

        {

            return NULL;

        }

        ///查找起始码(0x000001或者0x00000001)

        if(Buf[0]==0 && Buf[1]==0 && Buf[2] ==1)

            //Check whether buf is 0x000001

        {

            StartCode = 3;

            break;

        }

        else if(Buf[0]==0 && Buf[1]==0 && Buf[2] ==0 && Buf[3] ==1)

         //Check whether buf is 0x00000001

        {

            StartCode = 4;

            break;

        }

        else

        {

            //否则 往后查找一个字节

            pos++;

        }

    }

    ///然后查找下一个起始码查找第一个起始码

    int pos_2 = pos + StartCode; //记录当前处理的数据偏移量

    int StartCode_2 = 0;

    while(1)

    {

        unsigned char* Buf = mH264Buffer + pos_2;

        int lenth = mBufferSize - pos_2; //剩余没有处理的数据长度

        if (lenth <= 4)

        {

            return NULL;

        }

        ///查找起始码(0x000001或者0x00000001)

        if(Buf[0]==0 && Buf[1]==0 && Buf[2] ==1)

            //Check whether buf is 0x000001

        {

            StartCode_2 = 3;

            break;

        }

        else if(Buf[0]==0 && Buf[1]==0 && Buf[2] ==0 && Buf[3] ==1)

         //Check whether buf is 0x00000001

        {

            StartCode_2 = 4;

            break;

        }

        else

        {

            //否则 往后查找一个字节

            pos_2++;

        }

    }

    /// 现在 pos和pos_2之间的数据就是一个Nalu了

    /// 把他取出来

    ///由于传递给ffmpeg解码的数据 需要带上起始码 因此这里的nalu带上了起始码

    unsigned char* Buf = mH264Buffer + pos; //这帧数据的起始数据(包含起始码)

    int naluSize = pos_2 - pos; //nalu数据大小 包含起始码

    NALU_HEADER *nalu_header = (NALU_HEADER *)Buf;

    NALU_t * nalu = AllocNALU(naluSize);//分配nal 资源

    nalu->startcodeprefix_len = StartCode;      //! 4 for parameter sets and first slice in picture, 3 for everything else (suggested)

    nalu->len = naluSize;                 //! Length of the NAL unit (Excluding the start code, which does not belong to the NALU)

    nalu->forbidden_bit = 0;            //! should be always FALSE

    nalu->nal_reference_idc = nalu_header->NRI;        //! NALU_PRIORITY_xxxx

    nalu->nal_unit_type = nalu_header->TYPE;            //! NALU_TYPE_xxxx

    nalu->lost_packets = false;  //! true, if packet loss is detected

    memcpy(nalu->buf, Buf, naluSize);  //! contains the first byte followed by the EBSP

    /// 将这一帧数据去掉

    /// 把后一帧数据覆盖上来

    int leftSize = mBufferSize - pos_2;

    memcpy(mH264Buffer, mH264Buffer + pos_2, leftSize);

    mBufferSize = leftSize;

    return nalu;

}

二、使用ffmpeg解码上面获取到的NALU

1.h264解码器初始化

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

int H264Decorder::decoder_Init()

{

    /* find the h264 video decoder */

    pCodec = avcodec_find_decoder(AV_CODEC_ID_H264);

    if (!pCodec) {

        fprintf(stderr, "codec not found

");

    }

    pCodecCtx = avcodec_alloc_context3(pCodec);

    /* open the coderc */

    if (avcodec_open2(pCodecCtx, pCodec,NULL) < 0) {

        fprintf(stderr, "could not open codec

");

    }

    // Allocate video frame

    pFrame = avcodec_alloc_frame();

    if(pFrame == NULL)

        return -1;

    pFrameRGB = avcodec_alloc_frame();

    if(pFrameRGB == NULL)

            return -1;

    return 0;

}

2.解码并转成rgb32

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

int H264Decorder::decodeH264(uint8_t *inputbuf, int frame_size, uint8_t *&outBuf, int &outWidth, int &outHeight)

{

    int             got_picture;

    int             av_result;

    AVPacket pkt;

    av_init_packet(&pkt);

    pkt.data = inputbuf;

    pkt.size = frame_size;

    av_result = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, &pkt); //解码

    if (av_result < 0)

    {

        fprintf(stderr, "decode failed: inputbuf = 0x%x , input_framesize = %d

", inputbuf, frame_size);

        return -1;

    }

    av_free_packet(&pkt);

    //前面初始化解码器的时候 并没有设置视频的宽高信息,

    //因为h264的每一帧数据都带有编码的信息,当然也包括这些宽高信息了,因此解码完之后,便可以知道视频的宽高是多少

    //这就是为什么 初始化编码器的时候 需要初始化高度,而初始化解码器却不需要。

    //解码器可以直接从需要解码的数据中获得宽高信息,这样也才会符合道理。

    //所以一开始没有为bufferRGB分配空间 因为没办法知道 视频宽高

    //一旦解码了一帧之后 就可以知道宽高了  这时候就可以分配了

    if (bufferRGB == NULL)

    {

        int width = pCodecCtx->width;

        int height = pCodecCtx->height;

        

        int numBytes = avpicture_get_size(PIX_FMT_RGB32, width,height);    

        bufferRGB = (uint8_t *)av_malloc(numBytes*sizeof(uint8_t));

    avpicture_fill((AVPicture *)pFrameRGB, bufferRGB, PIX_FMT_RGB32,width, height);

    img_convert_ctx = sws_getContext(width,height,pCodecCtx->pix_fmt,width,height,PIX_FMT_RGB32,SWS_BICUBIC, NULL,NULL,NULL);

        

    }

    if (got_picture)

    {

        //格式转换 解码之后的数据是yuv420p的 把她转换成 rgb的图像数据

        sws_scale(img_convert_ctx,

                (uint8_t const const *) pFrame->data,

                pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data,

                pFrameRGB->linesize);

        outBuf = bufferRGB;

        outWidth = pCodecCtx->width;

        outHeight = pCodecCtx->height;

    }

    return got_picture;

}

三、使用Qt显示图像

直接用QImage加载得到的rgb32数据即可:

1

2

3

4

5

//把这个RGB数据 放入QIMage            

QImage image = QImage((uchar *)bufferRGB, width, height, QImage::Format_RGB32);

//然后传给主线程显示

emit sig_GetOneFrame(image.copy(), ++frameNum);

然后在主线程显示出这个QImage即可。

四、播放速度控制

由于H264数据里面没有包含时间戳信息,因此只能根据帧率来做同步,举个栗子:比如视频帧率是15那么我们就每秒钟播放15张图像,既在显示完一帧图像后,延时(1000/15)毫秒,当然严格来说这个延时不大合理,因为他没有考虑解码消耗的时间,但我们不管他,有兴趣的自己去完善修改。

另外需要注意的是:视频帧率是在h264的Nalu数据里面的,因此需要成功解码一帧图像后才能获取到帧率信息。

主要代码大致如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

void ReadH264FileThread::run()

{

    mH264Decorder->decoder_Init();

    char fileName[512] = {0};

    strcpy(fileName, mFileName.toLocal8Bit()); //GBK编码

    FILE *fp = fopen(fileName,"rb");

    if (fp == NULL)

    {

        qDebug("H264 file not exist!");

    }

    int frameNum = 0; //当前播放的帧序号

    while(!feof(fp))

    {

        char buf[10240];

        int size = fread(buf, 1, 1024, fp);//从h264文件读1024个字节 (模拟从网络收到h264流)

        int nCount = mH264Reader->inputH264Data((uchar*)buf,size);

        while(1)

        {

            //从前面读到的数据中获取一个nalu

            NALU_t* nalu = mH264Reader->getNextNal();

            if (nalu == NULL) break;

            uint8_t *bufferRGB;

            int width;

            int height;

            mH264Decorder->decodeH264(nalu->buf, nalu->len, bufferRGB, width, height);

            int frameRate = mH264Decorder->getFrameRate(); //获取帧率

            /// h264裸数据不包含时间戳信息  因此只能根据帧率做同步

            /// 需要成功解码一帧后 才能获取到帧率

            /// 为0说明还没获取到 则直接显示

            if (frameRate != 0)

            {

                msleep(1000/frameRate);

            }

            //把这个RGB数据 放入QIMage

            QImage image = QImage((uchar *)bufferRGB, width, height, QImage::Format_RGB32);

            //然后传给主线程显示

            emit sig_GetOneFrame(image.copy(), ++frameNum);

        }

    }

    mH264Decorder->decoder_UnInit();

}

至此,一个完整的h264播放器就完成了。

H264测试文件下载地址:https://download.csdn.net/download/qq214517703/10422777

完整工程下载地址:https://download.csdn.net/download/qq214517703/10423384

======Bug修复 Begin =======

2019-01-18更新:

1.修复部分h264文件,一帧存在多个slice,播放花屏的问题。

下载地址:https://download.csdn.net/download/qq214517703/10924057

======  End =======

音视频技术交流讨论欢迎加 QQ群 121376426  

从零开始学习音视频编程技术(41) H.264播放器相关推荐

  1. 从零开始学习音视频编程技术(七) FFMPEG Qt视频播放器之SDL的使用

    从零开始学习音视频编程技术(七) FFMPEG Qt视频播放器之SDL的使用 原文地址:http://blog.yundiantech.com/?log=blog&id=10 前面介绍了使用F ...

  2. 从零开始学习音视频编程技术(六) FFMPEG Qt视频播放器之显示图像

    从零开始学习音视频编程技术(六) FFMPEG Qt视频播放器之显示图像 原文地址:http://blog.yundiantech.com/?log=blog&id=9 前面讲解了如何用FFM ...

  3. 从零开始学习音视频编程技术(二) 音频格式讲解

    从零开始学习音视频编程技术(二) 音频格式讲解 原文地址:http://blog.yundiantech.com/?log=blog&id=5 1. 音频简介 前面我们说过视频有一个每秒钟采集 ...

  4. 从零开始学习音视频编程技术--转自雲天之巔

    此为转载文章,主要是为了个人阅读方便,将博主的系列文章罗列出来,点击直接跳转. 从零开始学习音视频编程技术(一) 视频格式讲解 从零开始学习音视频编程技术(二) 音频格式讲解 从零开始学习音视频编程技 ...

  5. 从零开始学习音视频编程技术(四) FFMPEG的使用

    零开始学习音视频编程技术(四) FFMPEG的使用 原文地址:http://blog.yundiantech.com/?log=blog&id=7 音视频开发中最常做的就是编解码的操作了,以H ...

  6. 从零开始学习音视频编程技术(二十一) 录屏软件开发之最终完善

    原文地址:http://blog.yundiantech.com/?log=blog&id=28 上面我们已经生成了录屏的视频,然而这个视频并不是理想中的那样,随时时间的增加,音视频会越来越不 ...

  7. 从零开始学习音视频编程技术(42) AAC数据解析

    AAC基本格式 AAC音频格式有ADIF和ADTS: ADIF:Audio Data Interchange Format 音频数据交换格式.这种格式的特征是可以确定的找到这个音频数据的开始,不需进行 ...

  8. 音视频从入门到精通——FFmpeg 播放器实现音视频同步的三种方式

    老人们经常说,播放器对音频和视频的播放没有绝对的静态的同步,只有相对的动态的同步,实际上音视频同步就是一个"你追我赶"的过程. 音视频的同步方式有 3 种,即:音视频分别向系统时钟 ...

  9. php音视频边下边播,封装bilibili播放器,自定义边下边播和缓存功能

    image 本项目使用播放器是ijkplay, 并且进行封装和修改主要功能: 1.重新编辑ijkplay的so库, 使其更精简和支持https协议 2.自定义MediaDataSource, 使用ok ...

  10. C++音视频编程探秘

    C++音视频编程探秘(C++ Audio and Video Programming Unveiled) 一.引言(Introduction) C++音视频编程简介(Overview of C++ A ...

最新文章

  1. 一场实验室意外爆炸事故,解决了58年量子难题,让科学家意外发现“核电共振”...
  2. 歌手比赛系统c语言程序注释,C语言程序课程设计—歌手比赛系统(20页)-原创力文档...
  3. weblogic集群的资料
  4. android预加载布局,Android 懒加载优化
  5. 很强大的FFMPEG API Documentation
  6. 淘宝快捷通道——百汇家园
  7. Sizzle.selectors.match/leftMatch
  8. 2.1 linux C 进程与多线程入门--(1)进程和程序的区别
  9. php判断版本号大小,通用javascript代码判断版本号是否在版本范围之间_javascript技巧...
  10. 我的职业规划,大家给点意见吧!
  11. CSS BACKGROUND汇总
  12. java 符_java运算符
  13. mysql 数据导出语句_mysql导出数据语句
  14. REST服务简介和实践
  15. Mac book Pro BootCamp驱动下载地址
  16. 物联网设备接入流程与平台架构
  17. mov格式怎么在线转换成mp4格式
  18. 力扣刷题思考:347. 前 K 个高频元素
  19. 怎么添改计算机程序,注册表怎么样添改注册表, – 手机爱问
  20. C语言基础——执行顺序

热门文章

  1. 移动边缘计算——MEC
  2. STM32——串口通信及实验
  3. 好看的android动画效果
  4. 百度网盘加速下载Motrix多线程下载器
  5. python mock server_python学习笔记6--mockserver
  6. 学习在layui中input、select、date日历的onchange事件无效解决方法
  7. 高中信息技术python及答案_浙江省新高中信息技术教材,将围绕Python进行并增加编程相关知识点...
  8. 博图V13、V14、V15、V15.1、V16版本安装包链接下载
  9. ansys命令流力磁耦合仿真
  10. ansys 命令流学习