<整理总结>H264/265码流数据包格式分析(带mp4v2封装H264/265为MP4的源码示例)

前言:

最近在学习使用MP4v2将H264/H265码流以及AAC音频封装成MP4格式,然后在网上疯狂查资料调试,发现学习过程中,很多知识点比较分散,我在完成这个目标后就打算整理一份完整一点的对H264/265码流数据包格式分析的学习总结,方便自己查看,也希望给有需要的人提供点帮助。
下文总结多来源于各个博主文章修改总结,比较多,就不一一贴出链接来源了,望原创作者理解,下面开始学习。

一、H.264码流解析

一个原始的H.264 NALU 单元常由 [Start Code] [NALU Header] [NALU Payload] 三部分组成,下图是一串H.264码流结构简图:
1

这里首先要了解下IDR帧与非IDR帧:

  • IDR(Instantaneous Decoding Refresh)–即时解码刷新。

  • I帧:帧内编码帧是一种自带全部信息的独立帧,无需参考其它图像便可独立进行解码,视频序列中的第一个帧始终都是I帧。

I和IDR帧都是使用帧内预测的。它们都是同一个东西而已,在编码和解码中为了方便,要首个I帧和其他I帧区别开,所以才把第一个首个I帧叫IDR,这样就方便控制编码和解码流程。
、、、、、、、、、、、、、、、、、、、、 第一次新增(推荐看完整个文档,再浏览新增内容)、、、、、、、、、、、、

I帧P帧B帧说明:

他们在H264码流中的结构图分别如下:
IDR帧:
2
非IDR帧:
3
本文档主要分析码流中第一个I帧之前的数据流,即IDR之前的数据流,详见下文:
NALU 单元组成解析:
[Start Code]:Start Code 用于标示这是一个NALU 单元的开始(也称分隔符),必须是”00 00 00 01” 或”00 00 01”。
[NALU Header]:H264中此部分占一个字节,在一个完整的NALU单元中,它都为第一个字节,用以表示其包含数据的类型及其他信息。头字节又可以被解析成3个部分,详见下面的列子。
[NALU Payload]:顾名思义,这个部分代表着相应[NALU Header]中NALU类型的有效数据集。详见下面的列子。
举个列子分析,通过编辑器打开一个H264文件,如下图:
4
可以看到第一个黄框加蓝框 的组合体就是一个NALU单元,黄框就是[NALU Header],随后的蓝框就是[NALU Payload],所以这一个IDR帧的数据结构大致为下图:
5
我们再更仔细的分析下,我们以第一个头信息字节为0x67作为例子分析:
6
如图所示,头字节[NALU Header]可以被解析成3个部分,其中(B:二进制表示 D:十进制表示):
红框forbidden_zero_bit = B:0[D:0]:占1个bit,禁止位,用以检查传输过程中是否发生错误,0表示正常,1表示违反语法;
黄框nal_ref_idc = B:11 [D:3]:占2个bit,用来表示当前NAL单元的优先级。非0值表示参考字段/帧/图片数据,其他不那么重要的数据则为0。对于非0值,值越大表示NALU重要性越高
蓝框nal_unit_type = B:00111[D:7]:最后5位用以指定NALU类型,NALU类型定义见下文:
头字节[NALU Header]结构解析:
7
** NALU组成结构表**
8
从表中我们可以获知,NALU类型1-5为视频帧,其余则为非视频帧。在解码过程中,我们只需要取出NALU头字节的后5位,即将NALU头字节和0x1F进行与计算即可得知NALU类型,即表达式为:

NALU类型 = NALU头字节 & 0x1F

注意: 可以将[Start Code]理解为不同NALU的分隔符,[NALU Header]是某种类型的key,[NALU Payload]是该key的value。

二、H.265码流解析

H.265标准围绕H.264编码标准,保留原有的某些技术,同时对一些技术进行改进,编码结构大致上和H.264的架构类似。
同H.264一样,H.265也是以NALU的形式组织起来。而在[NALU Header]上,H.264是一个字节,而H.265则是两个字节。
H.265的NALU 单元组成结构和H.264类似,也是由[Start Code] [NALU Header] [NALU Payload] 三部分组成,下面主要解析它们的主要区别,也就是在[NALU Header]上的区别。
H.265的[NALU Header]的组成结构中,舍弃了H.264的nal_ref_idc,此信息合并到了Type中,其组成结构定义如下:
9
H.265的 NALU 单元NALType 的成员:
10
11
这些其实和H264都大同小异,了解下即可。那么在解码过程中,我们只需要取出NALU头字节的后6位,即将NALU头字节和0x7E进行与计算并左移1位即可得知NALU类型,即表达式为:

NALU类型 = (NALU头字节 & 0x7E)>> 1
同样举个列子分析,通过编辑器打开一个H265文件,如下图:12
可以看到每个NALU单元的结构和H.264的都是一样的,由三部分组成,不过在一帧数据上的结构就有一点差异了,H.265的帧数据结构大致为:13

三、主要源码

支持h265的库的下载地址: 链接: https://github.com/Pandalzm/mp4v2-h265.
生成makefile指令:(需安装arm交叉编译工具链)
./configure --prefix=/home/wzj/work/nihui/output/ --host=arm-linux --disable-debug --enable-shared CC=arm-linux-gcc CXX=arm-linux-g++
输出库与头文件至output/
make clean;make;make install
下面是需要自己写的代码,其余都是调用库以及包含头文件即可,具体通过最后的makefile应该都能看出需要什么库和头文件,就不一一细说了。
main.c

#include "mp4v2_code.h"

#define H264_PATH "/mnt/nfs/1.h264"
#define H265_PATH "/mnt/nfs/1.h265"
int main(int argc, char *argv[])
{
    switch(atoi(argv[1]))
    {
        case 1:
        {
            if (packet2Mp4(H264_PATH, "h264.mp4"))
            {
                printf("Error:Packet to Mp4 fail.\n");
                return -1;
            }  
        }
        break;
        case 2:
        {
            if (packet2Mp4(H264_PATH, "h264.mp4"))
            {
            printf("Error:Packet to Mp4 fail.\n");
            return -1;
            }  
        }
        break;
        default:
            printf("Error:format error\n");
            return -1;
    }
    return 0;
}

mp4v2_code.c

#include "mp4v2_code.h"

static int h264_getNalu(FILE *pFile, unsigned char *pNalu)
{
    unsigned char c;
    int pos = 0;
    int len;

    if(!pFile)
        return -1;

    if((len = fread(pNalu, 1, 4, pFile)) <= 0)
        return -1;

    if(pNalu[0] != 0 || pNalu[1] != 0 || pNalu[2] != 0 || pNalu[3] != 1)
        return -1;

    pos = 4;

    while(1)
    {
        if(feof(pFile))
            break;

        pNalu[pos] = fgetc(pFile);
        
        if(pNalu[pos-3] == 0 && pNalu[pos-2] == 0 && pNalu[pos-1] == 0 && pNalu[pos] == 1)
        {
            fseek(pFile, -4, SEEK_CUR);
            pos -= 4;
            break;
        }

        pos++;
    }

    len = pos+1;

    return len;
}

int h264_packet2Mp4(const char *inputFile, const char *outputFiles)
{
    FILE *pIn = NULL;
    unsigned char *pBuf = malloc(1024*1024);
    unsigned char *pNalu = NULL;
    unsigned char naluType;
    int len;
    int num = 0;
    MP4FileHandle pHandle = NULL;
    MP4TrackId videoId;
	int width = 640;
	int height = 480;
	int frameRate = 15;
	int timeScale = 90000;
    int addStream = 1;

    pIn = fopen(inputFile, "rb");
    if(!pIn)
        return -1;

    pHandle = MP4Create(outputFiles, 0);
    if(pHandle == MP4_INVALID_FILE_HANDLE)
    {
		printf("ERROR:Create mp4 handle fialed.\n");
		return -1;
    }

    MP4SetTimeScale(pHandle, timeScale);

    while(1)
    {
        len = getNalu(pIn, pBuf);
        if (len <= 0)
            break;

        if (pBuf[0] != 0 || pBuf[1] != 0 || pBuf[2] != 0 || pBuf[3] != 1)
            continue;

        len -= 4;
        pNalu = pBuf+4;
        naluType = pNalu[0]&0x1F;

        switch (naluType)
        {
            case 0x07: // SPS
                printf("sps(%d)\n", len);
                if (addStream)
                {
                    videoId = MP4AddH264VideoTrack
                            (pHandle, 
                            timeScale,              // 一秒钟多少timescale
                            timeScale/frameRate,    // 每个帧有多少个timescale
                            width,                  // width
                            height,                 // height
                            pNalu[1],               // sps[1] AVCProfileIndication
                            pNalu[2],               // sps[2] profile_compat
                            pNalu[3],               // sps[3] AVCLevelIndication
                            3);                     // 4 bytes length before each NAL unit
                    if (videoId == MP4_INVALID_TRACK_ID)
                    {
                        printf("Error:Can't add track.\n");
                        return -1;
                    }
                    
                    MP4SetVideoProfileLevel(pHandle, 0x7F);

                    addStream = 0;
                }

                MP4AddH264SequenceParameterSet(pHandle, videoId, pNalu, len);

                break;
            
            case 0x08: // PPS
                printf("pps(%d)\n", len);
                MP4AddH264PictureParameterSet(pHandle, videoId, pNalu, len);
                break;

            default:
                printf("slice(%d)\n", len);
                pBuf[0] = (len>>24)&0xFF;
                pBuf[1] = (len>>16)&0xFF;
                pBuf[2] = (len>>8)&0xFF;
                pBuf[3] = (len>>0)&0xFF;

                MP4WriteSample(pHandle, videoId, pBuf, len+4, MP4_INVALID_DURATION, 0, 1);

                break;
        }
}

    free(pBuf);
    fclose(pIn);
    MP4Close(pHandle, 0);

    return 0;
}

static int h265_getNalu(FILE *pFile, unsigned char *pNalu)
{
    unsigned char c;
    int pos = 0;
    int len;

    if(!pFile)
        return -1;

    if((len = fread(pNalu, 1, 4, pFile)) <= 0)
        return -1;

    if(pNalu[0] != 0 || pNalu[1] != 0 || pNalu[2] != 0 || pNalu[3] != 1)
        return -1;

    pos = 4;

    while(1)
    {
        if(feof(pFile))
            break;

        pNalu[pos] = fgetc(pFile);
        
        if(pNalu[pos-3] == 0 && pNalu[pos-2] == 0 && pNalu[pos-1] == 0 && pNalu[pos] == 1)
        {
            fseek(pFile, -4, SEEK_CUR);
            pos -= 4;
            break;
        }

        pos++;
    }

    len = pos+1;

    return len;
}

int h265_packet2Mp4(const char *inputFile, const char *outputFiles)
{
    FILE *pIn = NULL;
    unsigned char *pBuf = malloc(1024*1024);
    unsigned char *pNalu = NULL;
    unsigned char naluType;
    int len;
    int num = 0;
    MP4FileHandle pHandle = NULL;
    MP4TrackId videoId;
	int width = 640;
	int height = 480;
	int frameRate = 15;
	int timeScale = 90000;
    int addStream = 1;

    pIn = fopen(inputFile, "rb");
    if(!pIn)
        return -1;

    pHandle = MP4Create(outputFiles, 0);
    if(pHandle == MP4_INVALID_FILE_HANDLE)
    {
		printf("ERROR:Create mp4 handle fialed.\n");
		return -1;
    }

    MP4SetTimeScale(pHandle, timeScale);

    while(1)
    {
        len = h265_getNalu(pIn, pBuf);
        if (len <= 0)
            break;

        if (pBuf[0] != 0 || pBuf[1] != 0 || pBuf[2] != 0 || pBuf[3] != 1)
            continue;

        len -= 4;
        pNalu = pBuf+4;
        naluType = (pNalu[0]&0x7E)>>1;

        switch (naluType)
        {
            case 32: //VPS
                printf("vps(%d)\n", len);
                if (addStream)
                {
                    videoId = MP4AddH265VideoTrack
                            (pHandle, 
                            timeScale,              // 一秒钟多少timescale
                            timeScale/frameRate,    // 每个帧有多少个timescale
                            width,                  // width
                            height,                 // height
                            pNalu[2],               // sps[1] AVCProfileIndication
                            pNalu[3],               // sps[2] profile_compat
                            pNalu[4],               // sps[3] AVCLevelIndication
                            3);                     // 4 bytes length before each NAL unit
                    if (videoId == MP4_INVALID_TRACK_ID)
                    {
                        printf("Error:Can't add track.\n");
                        return -1;
                    }
                    
                    MP4SetVideoProfileLevel(pHandle, 0x7F);
                    MP4AddH265VideoParameterSet(pHandle, videoId, pNalu, len);
                    addStream = 0;
                }



            case 33: // SPS
                MP4AddH265SequenceParameterSet(pHandle, videoId, pNalu, len);
                break;
            
            case 34: // PPS
                printf("pps(%d)\n", len);
                MP4AddH265PictureParameterSet(pHandle, videoId, pNalu, len);
                break;

            default:
                printf("slice(%d)\n", len);
                pBuf[0] = (len>>24)&0xFF;
                pBuf[1] = (len>>16)&0xFF;
                pBuf[2] = (len>>8)&0xFF;
                pBuf[3] = (len>>0)&0xFF;
                MP4WriteSample(pHandle, videoId, pBuf, len+4, MP4_INVALID_DURATION, 0, 1);
                break;
        }
    }
    free(pBuf);
    fclose(pIn);
    MP4Close(pHandle, 0);

    return 0;
}

mp4v2_code.h

#ifndef __MP4V2_CODE_H_
#define __MP4V2_CODE_H_

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <mp4v2/mp4v2.h>

int h264_packet2Mp4(const char *inputFile, const char *outputFiles);
int h265_packet2Mp4(const char *inputFile, const char *outputFiles);

#endif

Makefile

CROSS = arm-linux-
CC = $(CROSS)gcc
CXX = $(CROSS)g++
SPWD =  /home/wzj/work/rtt
DEBUG = 
CFLAGS = $(DEBUG) -c 
HEADER_PATH  :=  -I $(SPWD)/include/mp4v2/ -I $(SPWD)/include/
LIB_PATH := $(SPWD)/lib/libmp4v2.a
TARGET = main
HIDE = @
RM = rm -rf
CP = cp $(TARGET) ~/nfsshare/

SRCS = $(wildcard $(SPWD)/src/*.c)
OBJS = $(patsubst %.c, %.o, $(SRCS))
$(TARGET):$(OBJS)
    $(CXX)  -o $@ $^ $(LIB_PATH)
$(OBJS):%.o : %.c
    $(HIDE) $(CC) $(CFLAGS) $< -o $@ $(HEADER_PATH)
cp:
    $(CP)
clean:
    $(RM) $(TARGET) *.o $(SPWD)/src/*.o

整理不易,感觉有用的话,希望三连支持一下,wink

以下是一个使用Java实现UDP通信,并输出H264/265码流示例项目: 1. 使用Java Socket API实现UDP通信,通过DatagramSocket类实现UDP数据包的发送和接收。 2. 使用Java开源库JCodec实现H264/265视频编码,输出码流数据。 3. 使用Java开源库ffmpeg实现RTP/RTSP协议的封装和解封装,以及ONVIF协议的处理。 4. 整合以上组件,实现一个完整的UDP视频传输项目。 示例代码如下: ```java import java.net.*; import java.io.*; import org.jcodec.api.*; import org.jcodec.api.specific.*; import org.jcodec.common.*; import org.jcodec.containers.mp4.*; import org.jcodec.scale.*; import org.jcodec.codecs.h264.*; import org.jcodec.codecs.h265.*; import org.jcodec.codecs.mjpeg.*; import org.jcodec.codecs.vpx.*; import org.jcodec.codecs.wav.*; import org.jcodec.codecs.prores.*; import org.jcodec.movtool.*; import org.jcodec.scale.*; import org.jcodec.containers.mps.*; public class UDPVideoStream { private static final int PORT = 5000; private static final String HOSTNAME = "localhost"; private static final int TIMEOUT = 5000; public static void main(String[] args) throws Exception { // Create a DatagramSocket object for sending and receiving UDP packets DatagramSocket socket = new DatagramSocket(); // Create a H264Encoder/HEVCEncoder object for encoding H264/265 video frames H264Encoder encoder = new H264Encoder(); HEVCEncoder hevcEncoder = new HEVCEncoder(); // Create a MP4Muxer object for muxing H264/265 video frames into MP4 container MP4Muxer muxer = new MP4Muxer(new File("output.mp4")); // Create a FrameGrabber object for grabbing video frames from camera FrameGrabber grabber = FrameGrabber.createDefault(0); grabber.start(); // Loop through the video frames and encode them using H264Encoder/HEVCEncoder // then mux the encoded frames into MP4 container for (int i = 0; i < 1000; i++) { Picture picture = grabber.grab(); if (picture == null) { break; } // Encode the picture using H264Encoder/HEVCEncoder SeqParameterSet sps = encoder.initSPS(picture.getWidth(), picture.getHeight()); PictureParameterSet pps = encoder.initPPS(sps); ByteBuffer bb = ByteBuffer.allocate(picture.getWidth() * picture.getHeight() * 4); ByteBuffer hevcBB = ByteBuffer.allocate(picture.getWidth() * picture.getHeight() * 4); BitWriter writer = new BitWriter(bb); BitWriter hevcWriter = new BitWriter(hevcBB); encoder.encodeFrame(picture, writer); hevcEncoder.encodeFrame(picture, hevcWriter); // Mux the encoded frames into MP4 container ByteBuffer packedBB = ByteBuffer.allocate(bb.remaining() + 100); ByteBuffer hevcPackedBB = ByteBuffer.allocate(hevcBB.remaining() + 100); MP4Packet packet = MP4Packet.createPacket(bb, i, grabber.getVideoTrack().getTimescale(), 1, i, true, null, i, 0); MP4Packet hevcPacket = MP4Packet.createPacket(hevcBB, i, grabber.getVideoTrack().getTimescale(), 1, i, true, null, i, 0); muxer.addVideoPacket(packet); muxer.addVideoPacket(hevcPacket); // Send the encoded frames as UDP packets InetAddress address = InetAddress.getByName(HOSTNAME); DatagramPacket packet = new DatagramPacket(packedBB.array(), packedBB.remaining(), address, PORT); DatagramPacket hevcPacket = new DatagramPacket(hevcPackedBB.array(), hevcPackedBB.remaining(), address, PORT); socket.send(packet); socket.send(hevcPacket); // Wait for ACK message from the receiver socket.setSoTimeout(TIMEOUT); byte[] buffer = new byte[1024]; DatagramPacket ackPacket = new DatagramPacket(buffer, buffer.length); socket.receive(ackPacket); System.out.println("Received ACK message: " + new String(ackPacket.getData(), 0, ackPacket.getLength())); } // Close the objects grabber.stop(); socket.close(); muxer.finish(); } } ``` 以上示例代码实现了如下功能: 1. 通过FrameGrabber对象从摄像头获取视频帧数据。 2. 使用H264Encoder/HEVCEncoder对象将视频帧数据编码为H264/265格式。 3. 使用MP4Muxer对象将编码后的H264/265数据封装MP4容器格式。 4. 将封装好的视频数据通过UDP协议发送到指定的主机和端口。 5. 等待接收方发送ACK消息,以确认接收成功。 该示例代码只是一个简单的UDP视频流传输示例,还有很多细节需要考虑,比如错误处理、流量控制、丢包重传等。如果需要在实际项目中使用,还需要进一步完善和优化。
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值