音视频编解码的部分基础知识

89 阅读4分钟

我正在参加「掘金·启航计划」

本文主要介绍一些音视频方面的编解码知识,包括H264编解码的格式,Android MediaCodec编解码的信息。

1. H264码流分为两种格式:AnnexBAVCC格式。

1.1 AnnexB格式:

 [start code]NALU | [start code]NALU | [start code]NALU

每个帧前面都有0x00 00 00 01 或者0x00 00 01作为起始码。SPS和PPS作为一类NALU存储在码流中,
一般在码流的最前面。

1.2 AVCC格式:

([extradata]) | ([length]NALU) | ([length]NALU)

没有起始码,每帧最前面的4个字节表示帧长度,这种格式中的NALU一般没有SPS和PPS等参数信息,参数
信息存储在extradata中。在ffmpeg中解析Mp4文件后,sps和pps信息存储在avStream->codecpar->extradata
中。

1.3 sps和pps

在FFMPEG的extradata数组中包含了sps和pps的数据,数据由一些固定参数、sps和pps的相关数据计算得出,

第1字节:version (通常0x01)
第2字节:avc profile (值同第1个sps的第2字节,sps[1],如果是sps组成的NALU,那就是sps[4+1])
第3字节:avc compatibility (值同第1个sps的第3字节,sps[2]或者sps[4+2])
第4字节:avc level (值同第1个sps的第3字节,sps[3]huozhe sps[4+3])

//通常在AVCC格式的码流中每个NALU单元使用四个字节表示长度,所以这里一般表示为:0xFC | 3
第5字节前6位:保留全1
第5字节后2位:NALU Length 字段大小减1,通常这个值为3,即NAL码流中使用3+1=4字节表示NALU的长度

// 这里一般默认为1,使用0xE0 | 1表示
第6字节前3位:保留,全1
第6字节后5位:SPS NALU的个数,通常为1

// 采用两个字节,16位来表示sps的长度,可以用[7]=(spslen>>8)&0x00ff,[8]=spslen & 0x00ff来表示
第7字节、第8字节:SPS结构 [16位 SPS长度][SPS NALU data]

// 后面拼接sps的数据 
sps data

// 后面接入pps的相关参数
// 表示pps的个数,一般为1,用[8+spslen+1]=0x01显示
第[8+spslen+1]字节:PPS的个数,通常为1

// 采用两个字节,16位表示pps的长度,[8+spslen+2]=(ppslen >>8)&0x00ff、[8+spslen+3] = ppslen & 0x00ff
第[8+spslen+2][8+spslen+3]字节:
 PPS结构 [16位 PPS长度][PPS NALU data]
 
// 后面拼接pps的数据
pps data

1.4 H264的打包方式:

AVCC和Annex-B,MediaCodec就是使用Annex-B。

2. MediaCodec

通过MediaCodec提供的硬编解码能力,来实现音视频的处理。下面是MediaCodec编解码相关联的一些类信息,如MediaFormat、MediaExtractor等

2.1 MediaFormat

描述媒体数据格式的信息,分为音频和视频相关。

音频
// mime          音频格式
// sampleRate    采样率
// channelCount  通道数
    public static final @NonNull MediaFormat createAudioFormat(
            @NonNull String mime,
            int sampleRate,
            int channelCount) {
        MediaFormat format = new MediaFormat();
        format.setString(KEY_MIME, mime);
        format.setInteger(KEY_SAMPLE_RATE, sampleRate);
        format.setInteger(KEY_CHANNEL_COUNT, channelCount);

        return format;
    }

音频格式主要使用的是:

String MIMETYPE_AUDIO_AAC = "audio/mp4a-latm"

采样率一般使用44.1HZ,通道数分为单通道和双通道。

在配置音频相关的MediaFormat时,比特率是一定要配置的。比特率也可以叫做码率,指的是单位时间内的数据传输速率,单位以秒来计算。码率越高音频的质量就越高,但同时体积也会增大。

audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, config.audioBitrate.toInt())
视频
// videoType
// 底层支持的宽高来设置
        val format = MediaFormat.createVideoFormat(MIME_TYPE, config.width, config.height)
        //指定编码器的颜色格式
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
                MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
        //指定比特率
        format.setInteger(MediaFormat.KEY_BIT_RATE, config.videoBitrate.toInt())
        //指定帧率
        format.setFloat(MediaFormat.KEY_FRAME_RATE, config.fps)
        //指定关键帧的间隔时间,设置为2s
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2)

videoType一般来说常用的就两种

//H264
String MIMETYPE_VIDEO_AVC = "video/avc"
//H265
String MIMETYPE_VIDEO_HEVC = "video/hevc"

帧率指的是每秒钟包含多少张画面,单位为fps,如25fps。

2.2 MediaExtractor

从数据源提取编码后的媒体数据,即音视频解析,分离。 具体的使用:

// 创建一个MediaExtractor对象并设置数据源
   MediaExtractor extractor = new MediaExtractor();
   extractor.setDataSource(...);
// 获取媒体资源中的轨道数,一般包括视轨、两个音轨、字幕轨
   int numTracks = extractor.getTrackCount();
   for (int i = 0; i < numTracks; ++i) {
   // 根据下标获取对应的MediaFormat,内部包含媒体数据的信息
     MediaFormat format = extractor.getTrackFormat(i);
     //mime表示轨道的信息,比如video、audio
     String mime = format.getString(MediaFormat.KEY_MIME);
     //判断是需要的轨道,则选中该轨道后续解析
     if (weAreInterestedInThisTrack) {
       extractor.selectTrack(i);
     }
   }
   //获取一个Buffer对象,一般是通过MediaCodec获取对应的队列中的Buffer
   ByteBuffer inputBuffer = ByteBuffer.allocate(...)
   //解析数据到Buffer中,大于0表示解析成功
   while (extractor.readSampleData(inputBuffer, ...) >= 0) {
     int trackIndex = extractor.getSampleTrackIndex();
     //返回当前数据的显示时间,以微秒为单位
     long presentationTimeUs = extractor.getSampleTime();
     ...
     //移动到下一个解析点,读取下一帧数据v
     extractor.advance();
   }
  
  //释放对象
   extractor.release();
   extractor = null;

2.3 编解码器特定的数据(Codec-specific Data)

一些格式比如AAC、H264等编解码器处理数据时,需要添加特定的数据作为缓冲区。比如H264需要sps和pps信息,AAC需要ADTS信息等。

 val sps = byteArrayOf(0, 0, 0, 1, 103, 66, 0, 41, -115, -115, 64, 80, 30, -48, 15, 8, -124, 83, -128)
 mediaFormat.setByteBuffer("csd-0", ByteBuffer.wrap(sps))

 val pps = byteArrayOf(0, 0, 0, 1, 104, -54, 67, -56)
 mediaFormat.setByteBuffer("csd-1",ByteBuffer.wrap(pps))

2.4 createInputSurface

在MediaCodec中通过createInputSurface 可以创建一个Surface作为输入源.