天天看點

從零開始學習音視訊程式設計技術(41) H.264播放器

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()

{

pCodec = avcodec_find_decoder(AV_CODEC_ID_H264);

if

(!pCodec) {

fprintf

(stderr, "codec not found

");

}

pCodecCtx = avcodec_alloc_context3(pCodec);

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  

繼續閱讀