C++音视频开发面试题集锦

老规矩,先上面试题目:

  • 1、iOS 中系统 API 提供了哪些视频编码的方式?
  • 2、VideoToolbox 视频帧解码失败以后应该如何重试?
  • 3、如何使用 PSNR 对视频转码质量进行评估?
  • 4、什么是 VAO,什么是 VBO,它们的作用是什么?
  • 5、介绍一下 FFmpeg 中关于 timebase 的基础知识与应用?
  • 6、如何识别一个视频是 HDR 视频?
  • 7、如何通过优化播放器来优化音乐播放体验,比如提升音质或音效?
  • 8、介绍一下 SIMD 以及它在音视频处理中的应用?
  • 9、AVPlayer 中如何实现视频片段加速预览播放?
  • 10、如何高效获取一个视频的关键帧序列?
  • 11、SPS 和 PPS 在 extradata 中的作用是什么?
  • 12、I 帧和 IDR 帧有什么区别?在什么情况下 I 帧不是 IDR 帧?

1、iOS 中系统 API 提供了哪些视频编码的方式?

在 iOS 中,实现视频编码的方式主要包括以下两种:

  • AVFoundation 框架:AVFoundation 是苹果提供的一个用于处理音视频数据的框架,它提供了一系列用于捕获、处理和输出音视频数据的类和方法。通过 AVFoundation 框架,可以使用 AVAssetWriter 和 AVAssetWriterInput 类来实现编码视频。
  • VideoToolbox 框架:VideoToolbox 是苹果提供的一个专门用于处理视频数据的框架,它提供了硬件加速的视频编码和解码功能。使用 VideoToolbox,可以利用 iOS 设备上的硬件编码器来实现高效的视频编码。

相比而言,AVFoundation 框架则提供了更加上层的接口,更简单易用,但因此对于一些特殊需求和高级功能,可能无法满足。VideoToolbox 则提供了更直接的对硬件编码器的访问,允许开发者能更细致的控制编码器的配置和参数,并且可以直接操作编码器的输入和输出数据,灵活性更好。

2、Videotoolbox 视频帧解码失败以后应该如何重试?

  • 1、重新初始化解码器:尝试重新初始化 Videotoolbox 解码器,有时候重新初始化可以解决解码过程中的一些临时问题。
  • 2、检查视频文件:确保视频文件没有损坏或者格式不正确。有时候解码失败是因为视频文件本身的问题,可以尝试使用其他工具或者重新获取视频文件。
  • 3、检查当前内存:在解码过程中如果 CMSampleBuffer 不及时释放,可能会导致内存过高导致解码器报 -11800 通用错误。
  • 4、尝试重新解码当前帧:将当前帧以及当前 gop 内前序帧都重新输入给解码器。

3、如何使用 PSNR 对视频转码质量进行评估?

  • 1、计算图像差异:获得原始视频帧和转码后的未经过任何图像效果处理的视频帧使用同一解码器解码,并将它们的每一帧转换成相同的格式(比如 YUV 格式)。
  • 2、计算 PSNR 值:使用以下公式计算每一帧的 PSNR 值。
  • 3、计算平均 PSNR:将所有帧的 PSNR 值求平均,得到视频的平均 PSNR 值。
  • 4、分析结果:根据平均 PSNR 值来评估转码后视频的质量。较高的 PSNR 值表示转码后的视频质量与原始视频相似度较高,而较低的 PSNR 值则表示质量损失较大。

举例来说两个宽高为 m×n 视频帧 I 和 K, I 为转码前视频帧,K 为转码后的视频帧,那么它们的均方误差(MSE)定义为:

他们的 PSNR 计算公式如下:

其中,MAXI 是表示图像点颜色的最大数值,如果每个采样点用 8 位表示,那么就是 255。

4、什么是 VAO,什么是 VBO,它们的作用是什么?

1、Vertex Buffer Object (VBO)

  • VBO 主要用于存储顶点数据,如顶点坐标、法线、颜色等。
  • 通过将顶点数据存储在 GPU 的显存中,可以提高渲染效率,因为 GPU 能够更快地访问这些数据,而无需反复从 CPU 内存中读取。

2、Vertex Array Object (VAO)

  • VAO(Vertex Array Object)顶点数组对象,主要作用是用于管理 VBO 或 EBO。
  • VBO 保存了一个模型的顶点属性信息,每次绘制模型之前需要绑定顶点的所有信息,当数据量很大时,重复这样的动作变得非常麻烦。VAO 可以把这些所有的配置都存储在一个对象中,每次绘制模型时,只需要绑定这个 VAO 对象就可以了,可以减少 glBindBuffer 、glEnableVertexAttribArray、 glVertexAttribPointer 这些调用操作,高效地实现在顶点数组配置之间切换。

5、介绍一下 FFmpeg 中关于 timebase 的基础知识与应用?

1)timebase 定义

在 FFmpeg 中,time_base 是一个关键概念,它用于表示时间单位。在处理音频或视频流时,time_base 可以根据不同的采样频率或帧率来定义。timebase 在 FFmpeg 的定义是一个 AVRational 结构体:

typedef struct AVRational{
    int num; ///< numerator  
    int den; ///< denominator  
} AVRational;

2)timebase 的使用

在某些情况下,time_base 是根据采样频率来定义的。例如:对于视频采样频率为 90KHz(90000Hz)的情况,time_base 就相当于 1/90000 秒。另一种定义 time_base 的方式是根据帧率。例如:对于视频帧率为 24fps 的情况,time_base 就相当于 1/24 秒。在 FFmpeg 的分层结构中,原始数据层、编解码层和封装层都有对应的 time_base。原始数据层和封装层都通过 AVStream 进行处理,而编解码层则对应 AVCodec。

3)封装层 timebase,视频流/音频流 timebase 和现实时间戳的的关系和转换

封装层 tbn、视频 tbc 和音频 tbc 可以各不相同,相互不影响。现实时间基我们一般选用 1us 即 (1/1000000)s。因为每一层用的时间基不同,在函数参数传递上只会使用时间基前面的倍数值,timebase 是统一的,因此时间在不同的时间基上面需要做一层转换。 例如:现实时间 1s 转换到音频流时间实现为 1000000 * (1/1000000) = 44100 * (1/44100),那么现实时间 1000000 在音频流时间值则为 44100。举一个开发中的实例:如果想 seek 视频到现实时间的 X ms。

int64_t seekTime = (int64_t)(( X / 1000 )  / av_q2d(videoStream->time_base)); 
av_seek_frame(videoFormatCtx_, video_index_, seekTime, AVSEEK_FLAG_BACKWARD);

因为 av_seek_frame 是在视频流层面,时间基与现实时间不同,需要转换并将转换后的值作为参数才能得到正确的结果。

4)转换函数解析

double av_q2d(AVRational a) //将AVRational 对象转换为小数,便于转换
// 将一个时间戳a从时基bq转换到时基cq下
int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq)

例如,将视频流的一帧 pts(a * atbr) 转换到封装层打包成 AVPacket,封装层 timebase 为 tbn,此时需要转换 int64 t = av_rescale_q_rnd(a, atbr, tbn);。

6、如何识别一个视频是 HDR 视频?

iOS 判断一个视频是否是 HDR 视频的方法:判断是否带有 HDR 特征的 track 即可,如下:

NSArray<AVAssetTrack *> *hdrTracks = 
[asset tracksWithMediaCharacteristic:AVMediaCharacteristicContainsHDRVideo];
if (hdrTracks.count > 0){
   return YES;
}

Android 需要我们自己解析出 colortransforfunction和ccolorStandard,如下:

@RequiresApi(api = Build.VERSION_CODES.R)
public static boolean isHDR(MediaMetadataRetriever mediaMetadataRetriever)
        throws NumberFormatException {
    String colorTransferString =
            mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COLOR_TRANSFER);
    Log.e("isHDR", colorTransferString);
    String colorStandardString =
            mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COLOR_STANDARD);
    Log.e("isHDR", colorStandardString);
    int colorTransfer = Integer.parseInt(colorTransferString);
    int colorStandard = Integer.parseInt(colorStandardString);
    // This check needs to match the isHDR check in
    // frameworks/av/media/libstagefright/FrameDecoder.cpp.
    return (colorTransfer == MediaFormat.COLOR_TRANSFER_HLG
            || colorTransfer == MediaFormat.COLOR_TRANSFER_ST2084)
            && colorStandard == MediaFormat.COLOR_STANDARD_BT2020;
}

7、如何通过优化播放器来优化音乐播放体验,比如提升音质或音效?

在播放侧可以使用自动增益控制算法(AGC)来提升音效。AGC 算法通过自动调整音频信号的增益,使其保持在一定的范围内,这种算法可以避免因音频信号的幅度变化而引起的声音过大或过小的问题,保证了音频信号的稳定性和可听性,目前有开源的实现例如 webrtcagc,可以把算法移植到自己的项目中。

8、介绍一下 SIMD 以及它在音视频处理中的应用?

SIMD(Single Instruction Multiple Data)是一种并行计算的技术,它允许在单个指令中同时处理多个数据元素。SIMD 指令集通常由处理器提供,用于加速向量化计算,从而提高程序的性能。

下面是一个 SIMD 的示例:向量化乘法

假设有两个数组 A 和 B,我们想要将它们的对应元素相乘,并将结果存储在另一个数组 C 中,使用 SIMD 指令,可以一次处理多个元素,提高计算效率。

// 使用 SIMD 指令进行向量化乘法
#include <immintrin.h>

void vectorMultiply(float* A, float* B, float* C, int size) {
    for (int i = 0; i < size; i += 8) {
        __m256 a = _mm256_load_ps(A + i); // 加载 8 个单精度浮点数到向量寄存器 A
        __m256 b = _mm256_load_ps(B + i); // 加载 8 个单精度浮点数到向量寄存器 B
        __m256 result = _mm256_mul_ps(a, b); // 执行向量乘法
        _mm256_store_ps(C + i, result); // 存储结果到数组 C
    }
}

在实际应用中,还可以使用 SIMD 指令进行其他操作,如减法、除法、逻辑运算等,以及应用于不同的数据类型,如整数、双精度浮点数等。通过合理地使用 SIMD 优化,可以显著提高程序的性能。

在音视频开发中,SIMD 也有不少的应用场景。比如:

1)在音频处理中,SIMD 可以用于实时音频效果处理,如均衡器、压缩器、混响器等,通过同时处理多个音频样本,可以提高音频处理的效率和实时性。

2)在视频处理中,SIMD 可以用于加速图像处理算法,如图像滤波、边缘检测、图像压缩等,通过同时处理多个像素,可以提高图像处理的速度和质量。

3)在视频编码中,SIMD 可以用于加速压缩和解压算法,如 H.264、H.265 编码器一些实现中,可以通过并行处理视频数据来提高视频编解码的效率和性能。

总之,SIMD 在音视频开发中的合理应用可以提高数据处理速度,降低功耗。

9、AVPlayer 中如何实现视频片段加速预览播放?

在编辑场景用 AVPlayer 来实现预览播放器时,对视频中某一段内容进行加速播放的实现代码如下:

// 创建 AVMutableComposition 对象
AVMutableComposition *composition = [AVMutableComposition composition];
// 将视频文件加载到 AVURLAsset 对象中
NSURL *videoURL = [[NSBundle mainBundle] URLForResource:@"your_video" withExtension:@"mp4"];
AVURLAsset *videoAsset = [AVURLAsset URLAssetWithURL:videoURL options:nil];
// 将视频的前 3 秒进行加速处理
CMTime startTime = kCMTimeZero;
CMTime duration = CMTimeMake(3, 1); // 加速的时间范围为前 3 秒
CMTimeRange timeRange = CMTimeRangeMake(startTime, duration);
[composition scaleTimeRange:timeRange toDuration:CMTimeMake(1, 1)]; 
// 将时间范围加速到 1 秒
// 创建 AVPlayerItem 对象并将组合后的视频添加到其中
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:composition];
// 创建 AVPlayer 对象并将 AVPlayerItem 对象添加到其中
AVPlayer *player = [AVPlayer playerWithPlayerItem:playerItem];

10、如何高效获取一个视频的关键帧序列?

获取一个视频的关键帧序列,基于 Android 平台 API 实现:

MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(getVideoPath());

int trackIndex = MediaExtractorUtil.selectVideoTrack(extractor);
extractor.selectTrack(trackIndex);

List<Long> keyframeTimestampsMS = new ArrayList<Long>();

while (extractor.getSampleTime() != -1) {
    long sampleTime = extractor.getSampleTime();

    if ((extractor.getSampleFlags() & MediaExtractor.SAMPLE_FLAG_SYNC) > 0) {
        keyframeTimestampsMS.add(sampleTime / 1000);
    }
    // 此处表示 extractor seek 的间隔为 1000 微妙
    extractor.seekTo(sampleTime + 1000, MediaExtractor.SEEK_TO_NEXT_SYNC);
}

获取一个视频的关键帧序列,基于 FFmpeg 实现:

#include "libavcodec/avcodec.h"  
#include "libavutil/ff_time.h"  
#include "libavformat/avformat.h"  
#include "libavformat/avc.h"

AVFormatContext* formatCtx = avformat_alloc_context();  
avformat_open_input(&formatCtx, path.c_str(), NULL, NULL);  
int videoIndex = -1;  
  
for (int i = 0; i < formatCtx->nb_streams; i++) {  
    AVStream* stream = formatCtx->streams[i];  
    AVMediaType type = stream->codecpar->codec_type;  
    if (type == AVMEDIA_TYPE_VIDEO) {  
        videoIndex = i;  
        break;  
    }
}  
if (videoIndex > 0) {
    AVStream* videoStream = formatCtx->streams[videoIndex];  
    AVInputFormat* iformat = formatCtx->iformat;  
    if (strcmp(iformat->name, "mov,mp4,m4a,3gp,3g2,mj2") == 0) {  
        std::vector<int64_t> keyframe_time_list_tmp;  
        MOVStreamContext* sc = (MOVStreamContext*) videoStream->priv_data;  
        for (int videoIndex = 0; videoIndex < videoStream->nb_index_entries; videoIndex++) {  
            AVIndexEntry indexEntry = videoStream->index_entries[videoIndex];  
            if (indexEntry.flags & AVINDEX_KEYFRAME) {  
                MOVStts cttsData = {0};  
                if (sc && sc->ctts_count == videoStream->nb_index_entries) {  
                    cttsData = sc->ctts_data[videoIndex];  
                } 
                double doublePts = (indexEntry.timestamp + sc->dts_shift + cttsData.duration) * av_q2d(videoStream->time_base) * 1000.0;  
                int64_t ptsTime = ceil(doublePts);  
                keyframe_time_list_tmp.push_back(ptsTime);  
            }    
        }  
    }
}

11、SPS 和 PPS 在 extradata 中的作用是什么?

SPS(Sequence Parameter Set)和 PPS(Picture Parameter Set)是 H.264 视频编码中的两种重要参数集。它们包含了视频序列的特性和参数信息,对于解码器来说非常重要。

SPS 包含了视频序列的全局参数,如分辨率、帧率、颜色空间等。PPS 则包含了与特定图像相关的参数,如切片组的配置、参考帧的使用等。

在 extradata 中,SPS 和 PPS 的作用是为解码器提供视频序列的配置信息,以确保解码器能够正确地解释和处理视频数据。通过提供这些参数集,解码器能够准确地还原视频序列的特性,从而实现高质量的视频解码。

12、I 帧和 IDR 帧有什么区别?在什么情况下 I 帧不是 IDR 帧?

I 帧:I 帧是视频序列中的关键帧,它是一个完整的图像帧,类似于 JPEG 或 BMP 图像文件。I 帧不依赖于其他帧,因此可以独立解码和显示。在视频序列中,I 帧通常用于随机访问点,也作为其他帧解码的参考。

IDR 帧:IDR 帧是一种特殊的 I 帧,它具有刷新解码器缓冲区的功能。当解码器接收到 IDR 帧时,它会清除之前的解码状态,确保从该帧开始解码,从而避免错误传播。IDR 帧通常用于视频序列的随机访问点,以及在视频传输或存储中用于错误恢复。

因此 IDR 帧一定是 I 帧,但是 I 帧则不一定是 IDR 帧。在遇到 OpenGOP 的情况下,就会出现 I 帧为非 IDR 帧的情况。

如上图所示右数第一个 I 帧就是一个非 IDR 的 I 帧,前一个 GOP 中的 B 帧依赖了当前 GOP 的 I 帧。所以右数第一个 I 帧接受时,不能刷新解码器,否则上一个 GOP 中的 B 帧无法被成功解码,可能会出现花屏或者报错。

粉丝福利, 免费领取C++音视频学习资料包+学习路线大纲、技术视频/代码,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值