老规矩,先上面试题目:
在 iOS 中,实现视频编码的方式主要包括以下两种:
相比而言,AVFoundation 框架则提供了更加上层的接口,更简单易用,但因此对于一些特殊需求和高级功能,可能无法满足。VideoToolbox 则提供了更直接的对硬件编码器的访问,允许开发者能更细致的控制编码器的配置和参数,并且可以直接操作编码器的输入和输出数据,灵活性更好。
举例来说两个宽高为 m×n 视频帧 I 和 K, I 为转码前视频帧,K 为转码后的视频帧,那么它们的均方误差(MSE)定义为:
他们的 PSNR 计算公式如下:
其中,MAXI 是表示图像点颜色的最大数值,如果每个采样点用 8 位表示,那么就是 255。
1、Vertex Buffer Object (VBO)
2、Vertex Array Object (VAO)
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。
int_t seekTime = (int_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下
int_t av_rescale_q(int_t a, AVRational bq, AVRational cq)
例如,将视频流的一帧 pts(a * atbr) 转换到封装层打包成 AVPacket,封装层 timebase 为 tbn,此时需要转换 int t = av_rescale_q_rnd(a, atbr, tbn);。
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;
}
在播放侧可以使用自动增益控制算法(AGC)来提升音效。AGC 算法通过自动调整音频信号的增益,使其保持在一定的范围内,这种算法可以避免因音频信号的幅度变化而引起的声音过大或过小的问题,保证了音频信号的稳定性和可听性,目前有开源的实现例如 webrtcagc,可以把算法移植到自己的项目中。
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.2、H.265 编码器一些实现中,可以通过并行处理视频数据来提高视频编解码的效率和性能。
总之,SIMD 在音视频开发中的合理应用可以提高数据处理速度,降低功耗。
在编辑场景用 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];
获取一个视频的关键帧序列,基于 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<int_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;
int_t ptsTime = ceil(doublePts);
keyframe_time_list_tmp.push_back(ptsTime);
}
}
}
}
SPS(Sequence Parameter Set)和 PPS(Picture Parameter Set)是 H.2 视频编码中的两种重要参数集。它们包含了视频序列的特性和参数信息,对于解码器来说非常重要。
SPS 包含了视频序列的全局参数,如分辨率、帧率、颜色空间等。PPS 则包含了与特定图像相关的参数,如切片组的配置、参考帧的使用等。
在 extradata 中,SPS 和 PPS 的作用是为解码器提供视频序列的配置信息,以确保解码器能够正确地解释和处理视频数据。通过提供这些参数集,解码器能够准确地还原视频序列的特性,从而实现高质量的视频解码。
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 帧无法被成功解码,可能会出现花屏或者报错。
因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- bangwoyixia.com 版权所有 湘ICP备2023022004号-2
违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务