音视频中的一些基本概念

1、YUV

YUV是一种颜色编码方法,Y表示明亮度(Luminance、Luma),U和V表示色度、浓度(Chrominance、Chroma)

Y表示亮度分量,也就是灰度图

U(Cb)表示色度分量,是照片蓝色部分去掉亮度

V(Cr)表示色度分量,是照片红色部分去掉亮度

1.1、YUV的采样格式

采集图像视频时是如何获取每个像素的YUV分量的,常见采样格式有:

YUV444:表示完全采样,每个Y对应一组UV分量

YUV422:表示2:1的水平采样,垂直完全采样,每两个Y共用一组UV分量

YUV420:表示2:1的水平采样,2:1的垂直采样,每四个Y共用一组UV分量

以上是UV分量的共享方式

 

 

 

 上面四幅图是实际的UV分量的采样方式

 

 

 

1.2、YUV的存储格式

YUV分量在内存中存储的顺序

紧缩格式(packed formats):将Y、U、V存储为Macro Pixels阵列,和RGB类似,分量是混合在一起的,适用于YUV444

平面格式(planar formats):将Y、U、V三个分量分别存放在不同的阵列当中

1.2.1、YUV422的三种存储方式

YUVY

 

 

 UYVY

 

 

 YUV422P

 

 

1.2.2、YUV420的四种存储方式

YV12、YU12

 

 

 NV12、NV21

 

 

 NV12是UV交替存储,NV21是VU交替存储

 参考:YUV详解数据和格式_Alex_designer的博客-CSDN博客_yuv数据

 

1.3、YUV内存对齐

为了实现内存对齐,每一行像素在内存中占用的空间并不是图像的宽度

 

 

 上图中的stride是内存对齐之后的长度,有很多别名,比如pitch、linesize,它的出现主要是为了提升数据读取的速率。

如果图像的宽度是内存对齐长度的整数倍,那么间距就会等于宽度;如果宽度不等于内存对齐的整数倍,那么间距就是大于宽度的最小内存对齐数。

在做数据拷贝时需要一行一行复制,下一行的数据的起始位置是上一行的起始位置加上stride,如果是YV12或者是YU12,拷贝UV通道时需要将stride除以2。

参考:YUV格式解释,步长(间距)解释 - 简书 (jianshu.com)

 

为什么解码失败显示绿屏?解码出了YUV数据之后需要转化为RGB输出,如果解码失败则YUV值都为0,根据转化公式转化出的RGB颜色就为0

 

2、分辨率、帧率、码率

分辨率指图像大小

帧率FPS每秒显示帧数量(frames per second)

码率Bps(bits per second)单位时间播放连续媒体的比特数量,文件大小 = bps * time

采样率:每秒采样点个数

 

3、I P B帧

I帧:关键帧,采用帧内压缩技术,解码时只需要本帧数据就可以完成

P帧:向前参考帧,采用帧间压缩技术,P帧表示这一帧跟之前的关键帧或者P帧的差别

B帧:双向参考帧,采用帧间压缩技术,通过前后画面与本帧数据的叠加取得最终画面,压缩率最高

H264 IDR帧:Instance Decode Refresh,IDR帧是I帧,但I帧不一定是IDR帧;IDR帧是为了解码的重同步,解码器解码到IDR图像时,立即将参考帧队列清空,重新查找参数集,开始新的序列;IDR之后的图像不能引用IDR帧之前的内容,这样就可以获得重同步的机会。IDR帧是一个GOP的首个I帧,重新开始一个序列,可以提供随机访问能力,所以在H264中必须要有IDR

H264 GOP:group of picture,序列,一个序列就是一段内容差异不太大的图像编码后生成的一串数据流,运动变化少时,一个序列可以很长;运动变化多时一个序列可能就会比较短

H264 SPS:Sequence Parameter Set 序列参数集,保存了一组编码视频序列的全局参数,包括帧数、参考帧数据、解码图像尺寸等信息,

H264 PPS:Picture Parameter Set 图像参数集,存放熵编码模式选择标识、片组数目等内容。SPS和PPS通常保存在H264流的前两个NALU中,一组帧之前首先要收到SPS和PPS;如果码流中参数发生变化,那么变化会反映在SPS和PPS中,否则解码会出错。

参考:H264的基本原理(一)------视频的基本知识_Paul_0920的博客-CSDN博客_h264基本原理

 

H264码流结构

参考:H264的基本原理(三)------ H264结构与码流_Paul_0920的博客-CSDN博客_h264原理

参考:《Android音视频开发》Page  397

看看avc_utils.cpp中是如何解析H264码流的

status_t getNextNALUnit(
        const uint8_t **_data, size_t *_size,
        const uint8_t **nalStart, size_t *nalSize,
        bool startCodeFollows) {
    const uint8_t *data = *_data;
    size_t size = *_size;
   // 传入参数size是当前ESQueue中的buffer大小
    *nalStart = NULL;
    *nalSize = 0;

    if (size < 3) {
        return -EAGAIN;
    }

    size_t offset = 0;

    // A valid startcode consists of at least two 0x00 bytes followed by 0x01.
   // 先查找start code  0x000001
    for (; offset + 2 < size; ++offset) {
        if (data[offset + 2] == 0x01 && data[offset] == 0x00
                && data[offset + 1] == 0x00) {
            break;
        }
    }
    if (offset + 2 >= size) {
        *_data = &data[offset];
        *_size = 2;
        return -EAGAIN;
    }
    offset += 3;
    // 确定起始数据位置
    size_t startOffset = offset;
    // 以下部分是查找下一个NALU的start code
    for (;;) {
        // 先确定start code中的0x01
        while (offset < size && data[offset] != 0x01) {
            ++offset;
        }

        if (offset == size) {
            if (startCodeFollows) {
                offset = size + 2;
                break;
            }

            return -EAGAIN;
        }
      // 找到0x01之后检查前两位是否为0x0000
        if (data[offset - 1] == 0x00 && data[offset - 2] == 0x00) {
            break;
        }

        ++offset;
    }
    // 这样就找到了当前NALU的末尾位置
    size_t endOffset = offset - 2;
    // 由于RBSP中包含有用于字节对齐的填充数据0x01 和 n位0x00,向前搜索
    while (endOffset > startOffset + 1 && data[endOffset - 1] == 0x00) {
        --endOffset;
    }
    // 这样就找到了SODB的起始数据位置和数据大小,这时候的endOffset位置是0x01,我们在copy时并不会copy到decoder中
    *nalStart = &data[startOffset];
    *nalSize = endOffset - startOffset;

    if (offset + 2 < size) {
        *_data = &data[offset - 2];
        *_size = size - offset + 2;
    } else {
        *_data = NULL;
        *_size = 0;
    }

    return OK;
}

看看ESQueue中的对H264码流的处理方法

sp<ABuffer> ElementaryStreamQueue::dequeueAccessUnitH264() {
    const uint8_t *data = mBuffer->data();

    size_t size = mBuffer->size();
    Vector<NALPosition> nals;

    size_t totalSize = 0;
    size_t seiCount = 0;

    status_t err;
    const uint8_t *nalStart;
    size_t nalSize;
    bool foundSlice = false;
    bool foundIDR = false;

    ALOGV("dequeueAccessUnit_H264[%d] %p/%zu", mAUIndex, data, size);
    // 找到 NALU
    while ((err = getNextNALUnit(&data, &size, &nalStart, &nalSize)) == OK) {
        if (nalSize == 0) continue;
        // 判断NALU type,第一个字节的后5位为type
        unsigned nalType = nalStart[0] & 0x1f;
        bool flush = false;
        // NAL type为1或者5说明当前NALU中的数据为片数据
        if (nalType == 1 || nalType == 5) {
            // NAL type为5,代表当前为IDR中的片
            if (nalType == 5) {
                foundIDR = true;
            }
            if (foundSlice) {
                //TODO: Shouldn't this have been called with nalSize-1?
                // 这里涉及到Slice Data,跳过了slice header
                ABitReader br(nalStart + 1, nalSize);
                // parseUE方法是在找当前片中的第一个宏块的序列号,如果序列号为0,说明当前NALU为一个新的帧
                unsigned first_mb_in_slice = parseUE(&br);
                // 如果序列号为0说明为新的帧,那么就将之前的数据拷贝到decoder中
                if (first_mb_in_slice == 0) {
                    // This slice starts a new frame.
 
                    flush = true;
                }
            }
            // 将找到片flag置为true
            foundSlice = true;
        } else if ((nalType == 9 || nalType == 7) && foundSlice) {
       // 如果nal type 为 9 说明序列结束,如果nal type为9说明来了一个SPS,这都说明之前的帧已经结束了,我们需要将该帧拷贝到decoder中
            // Access unit delimiter and SPS will be associated with the
            // next frame.

            flush = true;
        } else if (nalType == 6 && nalSize > 0) {
            // found non-zero sized SEI
            // 如果nal type为6,说明当前NALU为SEI
            ++seiCount;
        }

        if (flush) {
            // The access unit will contain all nal units up to, but excluding
            // the current one, separated by 0x00 0x00 0x00 0x01 startcodes.

            size_t auSize = 4 * nals.size() + totalSize;
            sp<ABuffer> accessUnit = new ABuffer(auSize);
            sp<ABuffer> sei;

            if (seiCount > 0) {
                sei = new ABuffer(seiCount * sizeof(NALPosition));
                accessUnit->meta()->setBuffer("sei", sei);
            }

#if !LOG_NDEBUG
            AString out;
#endif

            size_t dstOffset = 0;
            size_t seiIndex = 0;
            size_t shrunkBytes = 0;
            for (size_t i = 0; i < nals.size(); ++i) {
                const NALPosition &pos = nals.itemAt(i);

                unsigned nalType = mBuffer->data()[pos.nalOffset] & 0x1f;

                if (nalType == 6 && pos.nalSize > 0) {
                    if (seiIndex >= sei->size() / sizeof(NALPosition)) {
                        ALOGE("Wrong seiIndex");
                        return NULL;
                    }
                    NALPosition &seiPos = ((NALPosition *)sei->data())[seiIndex++];
                    seiPos.nalOffset = dstOffset + 4;
                    seiPos.nalSize = pos.nalSize;
                }
                // 拷贝数据前加上 start code
                memcpy(accessUnit->data() + dstOffset, "\x00\x00\x00\x01", 4);

                if (mSampleDecryptor != NULL && (nalType == 1 || nalType == 5)) {
                    uint8_t *nalData = mBuffer->data() + pos.nalOffset;
                    size_t newSize = mSampleDecryptor->processNal(nalData, pos.nalSize);
                    // Note: the data can shrink due to unescaping, but it can never grow
                    if (newSize > pos.nalSize) {
                        // don't log unless verbose, since this can get called a lot if
                        // the caller is trying to resynchronize
                        ALOGV("expected sample size < %u, got %zu", pos.nalSize, newSize);
                        return NULL;
                    }
                    memcpy(accessUnit->data() + dstOffset + 4,
                            nalData,
                            newSize);
                    dstOffset += newSize + 4;

                    size_t thisShrunkBytes = pos.nalSize - newSize;
                    //ALOGV("dequeueAccessUnitH264[%d]: nalType: %d -> %zu (%zu)",
                    //        nalType, (int)pos.nalSize, newSize, thisShrunkBytes);

                    shrunkBytes += thisShrunkBytes;
                }
                else {
                    memcpy(accessUnit->data() + dstOffset + 4,
                            mBuffer->data() + pos.nalOffset,
                            pos.nalSize);

                    dstOffset += pos.nalSize + 4;
                    //ALOGV("dequeueAccessUnitH264 [%d] %d @%d",
                    //        nalType, (int)pos.nalSize, (int)pos.nalOffset);
                }
            }

#if !LOG_NDEBUG
            ALOGV("accessUnit contains nal types %s", out.c_str());
#endif

            const NALPosition &pos = nals.itemAt(nals.size() - 1);
            size_t nextScan = pos.nalOffset + pos.nalSize;
            // 删除已拷贝的buffer
            memmove(mBuffer->data(),
                    mBuffer->data() + nextScan,
                    mBuffer->size() - nextScan);

            mBuffer->setRange(0, mBuffer->size() - nextScan);
            // 为帧加上pts
            int64_t timeUs = fetchTimestamp(nextScan);
            if (timeUs < 0LL) {
                ALOGE("Negative timeUs");
                return NULL;
            }

            accessUnit->meta()->setInt64("timeUs", timeUs);
            // 将IDR帧作为syncpoint
            if (foundIDR) {
                accessUnit->meta()->setInt32("isSync", 1);
            }

            if (mFormat == NULL) {
                mFormat = new MetaData;
                if (!MakeAVCCodecSpecificData(*mFormat,
                        accessUnit->data(),
                        accessUnit->size())) {
                    mFormat.clear();
                }
            }

            if (mSampleDecryptor != NULL && shrunkBytes > 0) {
                size_t adjustedSize = accessUnit->size() - shrunkBytes;
                ALOGV("dequeueAccessUnitH264[%d]: AU size adjusted %zu -> %zu",
                        mAUIndex, accessUnit->size(), adjustedSize);
                accessUnit->setRange(0, adjustedSize);
            }

            ALOGV("dequeueAccessUnitH264[%d]: AU %p(%zu) dstOffset:%zu, nals:%zu, totalSize:%zu ",
                    mAUIndex, accessUnit->data(), accessUnit->size(),
                    dstOffset, nals.size(), totalSize);
            mAUIndex++;

            return accessUnit;
        }

     // 将当前NALU中的SODB数据位置记录下来
        NALPosition pos;
        pos.nalOffset = nalStart - mBuffer->data();
        pos.nalSize = nalSize;

        nals.push(pos);

        totalSize += nalSize;
    }
    if (err != (status_t)-EAGAIN) {
        ALOGE("Unexpeted err");
        return NULL;
    }

    return NULL;
}

 

H265码流

H265码流和H264比较类似,H264的NALU header为一个字节,而H265的NALU header为两个字节

  • forbidden_zero_bit = 0:占1个bit,与H.264相同,禁止位,用以检查传输过程中是否发生错误,0表示正常,1表示违反语法;
  • nal_unit_type = 32:占6个bit,用来用以指定NALU类型
  • nuh_reserved_zero_6bits = 0:占6位,预留位,要求为0,用于未来扩展或3D视频编码
  • nuh_temporal_id_plus1 = 1:占3个bit,表示NAL所在的时间层ID

参考:H.264/H265码流解析_460833359的博客-CSDN博客_h265码流格式

参考:音视频基础:H265/HEVC&码流结构 - 知乎 (zhihu.com)

 

 

Audio

android中可以利用AudioRecord录制声音,录制的声音为模拟电信号,需要数字化,最常见的方法为脉冲编码调制PCM(Pluse Code Modulation),需要经历三个步骤,抽样、量化、编码,对应三个参数采样频率、采样位数、声道数。

采样率:即取样频率,每秒钟取得声音样本的次数,采样频率越高,声音的质量越好,常用采样频率不超过48KHz

采样位数:采样值或取样值,即采样样本幅度量化,用来衡量声音波动变化的参数,数值越大分辨率越高

声道数:单声道和立体声

比特率 (bit rate)= 采样率(sample rate) * 位深(Bit depth) * 声道数

duration = 文件大小 / 比特率

参考:Android手机直播(三)声音采集_bobuddy的博客-CSDN博客_声音采集

posted @ 2022-05-17 18:01  青山渺渺  阅读(397)  评论(0编辑  收藏  举报