一、C++ 基础与进阶
1. 请解释 C++ 中的多态性及其实现原理
多态性是 C++ 面向对象编程的三大特性之一,指同一操作作用于不同对象会产生不同的行为。
实现原理:
- 编译时多态:通过函数重载和运算符重载实现,由编译器在编译阶段确定调用哪个函数
- 运行时多态:通过虚函数和继承实现,基类指针或引用指向派生类对象时,会调用派生类的虚函数
- 虚函数表(vtable)机制:每个包含虚函数的类都有一个虚函数表,存储虚函数地址;类的每个对象都有一个虚表指针(vptr),指向该类的虚函数表
- 动态绑定:程序运行时通过对象的虚表指针找到对应的虚函数表,从而确定要调用的函数
class Base { public: virtual void func() { cout << "Base::func()" << endl; } }; class Derived : public Base { public: void func() override { cout << "Derived::func()" << endl; } }; int main() { Base* ptr = new Derived(); ptr->func(); // 输出Derived::func(),体现了多态性 delete ptr; return 0; }
2. 什么是智能指针?C++11 提供了哪些智能指针?各自的使用场景是什么?
智能指针是封装了原始指针的类,用于自动管理动态内存,避免内存泄漏。
C++11 提供的智能指针:
1.unique_ptr:
- 独占所有权的智能指针,同一时间只能有一个unique_ptr指向对象
- 不可复制,只能移动
- 适用场景:管理单个对象,避免所有权共享
2.shared_ptr:
- 共享所有权的智能指针,使用引用计数跟踪对象被引用次数
- 当引用计数为 0 时,自动释放对象
- 适用场景:需要多个指针共享同一对象所有权的情况
3.weak_ptr:
- 配合shared_ptr使用,不拥有对象所有权,不增加引用计数
- 用于解决shared_ptr可能导致的循环引用问题
- 适用场景:需要观察对象但不拥有所有权的情况
// unique_ptr示例 std::unique_ptr<int> uptr(new int(10)); // std::unique_ptr<int> uptr2 = uptr; // 错误,不能复制 std::unique_ptr<int> uptr3 = std::move(uptr); // 正确,移动语义 // shared_ptr示例 std::shared_ptr<int> sptr(new int(20)); std::shared_ptr<int> sptr2 = sptr; // 正确,引用计数变为2 // weak_ptr示例 std::weak_ptr<int> wptr = sptr; if (auto temp = wptr.lock()) { // 检查对象是否存在 *temp = 30; }
3. 解释 C++ 中的右值引用和移动语义
右值引用是指向右值的引用,用&&表示,主要用于实现移动语义。
右值是指那些临时的、即将销毁的对象,如字面常量、表达式结果等。
移动语义允许资源(如内存)从一个对象转移到另一个对象,而无需进行昂贵的复制操作:
- 移动构造函数:ClassName(ClassName&& other)
- 移动赋值运算符:ClassName& operator=(ClassName&& other)
优势:
- 减少不必要的内存分配和复制,提高性能
- 避免临时对象的拷贝开销,特别适用于大对象
class MyString { private: char* data; size_t length; public: // 构造函数 MyString(const char* str) { length = strlen(str); data = new char[length + 1]; strcpy(data, str); } // 移动构造函数 MyString(MyString&& other) noexcept : data(other.data), length(other.length) { other.data = nullptr; // 源对象放弃资源所有权 other.length = 0; } // 移动赋值运算符 MyString& operator=(MyString&& other) noexcept { if (this != &other) { delete[] data; // 释放当前资源 // 转移资源 data = other.data; length = other.length; // 源对象放弃资源所有权 other.data = nullptr; other.length = 0; } return *this; } ~MyString() { delete[] data; } };
4. 什么是内存对齐?为什么需要内存对齐?
内存对齐是指数据在内存中的存放位置必须是某个数(对齐系数)的整数倍。
需要内存对齐的原因:
- 硬件限制:某些 CPU 架构只能在特定地址访问特定类型的数据
- 性能优化:对齐的数据可以提高 CPU 访问效率,减少内存访问次数
- 平台兼容性:不同平台有不同的对齐要求,正确对齐可保证跨平台兼容性
C++ 中控制内存对齐的方式:
- alignof:获取类型的对齐要求
- alignas:指定变量或类型的对齐要求
- #pragma pack:编译器指令,设置结构体的对齐方式
struct Example { char a; // 1字节 int b; // 4字节,通常会对齐到4字节边界 short c; // 2字节 }; // 默认情况下,sizeof(Example)通常是12字节,而不是1+4+2=7字节 // 因为存在填充字节:a后填充3字节,c后填充2字节 // 使用alignas指定对齐 struct alignas(16) AlignedStruct { int x; double y; };
5. 解释 C++ 中的线程安全和如何保证线程安全
线程安全是指多线程环境下,一段代码能够正确处理多个线程的并发访问,不会出现数据竞争、死锁等问题。
保证线程安全的方法:
- 互斥锁(std::mutex):确保同一时间只有一个线程访问共享资源
- 读写锁(std::shared_mutex):允许多个读操作同时进行,但写操作需要独占
- 原子操作(std::atomic):对基本数据类型的操作提供原子性保证
- 线程局部存储(thread_local):为每个线程创建独立的变量实例
- 避免共享状态:通过消息传递等方式减少共享数据
#include <mutex> #include <thread> #include <vector> std::mutex mtx; int shared_counter = 0; void increment_counter() { for (int i = 0; i < 10000; ++i) { std::lock_guard<std::mutex> lock(mtx); // RAII方式管理锁 shared_counter++; } } int main() { std::vector<std::thread> threads; for (int i = 0; i < 10; ++i) { threads.emplace_back(increment_counter); } for (auto& t : threads) { t.join(); } // 如果线程安全,结果应为100000 std::cout << "Final counter value: " << shared_counter << std::endl; return 0; }
更多c++八股文观看下面视频讲解:最全C++八股文分享,C++校招面试题总结(附答案)
二、音视频基础概念
1. 请解释什么是 YUV 格式,与 RGB 格式有什么区别?
YUV 是一种颜色编码方式,将亮度信息(Y)与色度信息(U、V)分离:
- Y:亮度(Luminance),表示灰度信息
- U:色度(Chrominance),表示蓝色与亮度的差异
- V:色度,表示红色与亮度的差异
与 RGB 格式的区别:
1.存储方式:
- RGB:每个像素点由红、绿、蓝三个分量表示
- YUV:亮度和色度分离,可利用人眼对亮度更敏感的特性进行压缩
2.应用场景:
- RGB:主要用于显示器、图形卡等设备
- YUV:主要用于视频传输和存储,如摄像头输入、视频编码
3.压缩效率:
- YUV 可以对色度分量进行亚采样(如 4:2:0),减少数据量而视觉效果变化不大
- RGB 通常需要存储全部三个分量的完整信息
常见的 YUV 格式:
- YUV444:每个像素点都有完整的 Y、U、V 分量
- YUV422:水平方向每两个像素共享一组 U、V 分量
- YUV420:每 2x2 的像素块共享一组 U、V 分量(最常用)
2. 什么是帧率、码率和分辨率?它们之间有什么关系?
帧率(Frame Rate):
- 单位时间内显示的帧数,单位 fps(帧 / 秒)
- 常见帧率:24fps(电影)、30fps(视频)、60fps(高帧率视频)
- 帧率越高,视频越流畅,但需要更大的带宽和存储空间
码率(Bit Rate):
- 单位时间内传输或处理的数据量,单位 bps(比特 / 秒)
- 分为固定码率(CBR)和可变码率(VBR)
- 码率越高,视频质量越好,但需要更大的带宽和存储空间
分辨率(Resolution):
- 视频图像的像素尺寸,通常表示为宽度 × 高度
- 常见分辨率:720p (1280×720)、1080p (1920×1080)、4K (3840×2160)
- 分辨率越高,图像越清晰,但需要更高的码率支持
三者关系:
- 分辨率和帧率决定了视频的原始数据量(像素总数 × 帧率)
- 码率则决定了压缩后的视频数据量
- 相同码率下,分辨率越高或帧率越高,视频质量可能越低
- 视频质量取决于码率、分辨率和帧率的平衡
计算公式:
原始数据量(未压缩)≈ 宽度 × 高度 × 每像素位数 × 帧率
压缩后数据量 ≈ 码率 × 时间
3. 解释 I 帧、P 帧和 B 帧的区别
在视频编码中,为了提高压缩效率,通常采用帧间预测和帧内预测技术,将视频帧分为:
I 帧(Intra-coded Picture,帧内编码帧):
- 独立编码的帧,不依赖其他帧
- 采用类似 JPEG 的帧内压缩技术
- 压缩率较低,但作为随机访问点(可直接解码)
- 相当于视频中的关键帧
P 帧(Predictive-coded Picture,预测编码帧):
- 基于前一个 I 帧或 P 帧进行预测编码
- 只存储与参考帧的差异数据
- 压缩率高于 I 帧,但依赖参考帧
- 可提供较高的编码效率
B 帧(Bidirectionally predictive-coded Picture,双向预测编码帧):
- 基于前一个和后一个参考帧(I 帧或 P 帧)进行双向预测
- 压缩率最高,可达到很高的压缩比
- 依赖前后的参考帧,解码复杂度较高
- 可显著提高视频压缩效率
应用特点:
- 视频序列通常按 I-P-B-B-P-B-B-... 的模式排列
- I 帧间隔决定了视频的随机访问能力和错误恢复能力
- 增加 B 帧数量可以提高压缩效率,但会增加编解码延迟
4. 什么是 H.264/AVC 和 H.265/HEVC?它们有什么区别?
H.264/AVC 和 H.265/HEVC 都是视频编码标准:
H.264/AVC(Advanced Video Coding):
- 由 ITU-T 和 ISO/IEC 联合制定,2003 年发布
- 广泛应用于蓝光、视频会议、网络视频等领域
- 相比之前的标准(如 MPEG-2),在相同质量下可节省约 50% 码率
H.265/HEVC(High Efficiency Video Coding):
- 作为 H.264 的继任者,2013 年发布
- 针对高清和超高清视频优化
- 相比 H.264,在相同质量下可再节省约 50% 码率
主要区别:
1.压缩效率:
- H.265 比 H.264 压缩效率提高约一倍
- 相同画质下,H.265 码率约为 H.264 的一半
2.编码工具:
- H.265 采用更大的编码单元(CU),最大 64×64 像素(H.264 为 16×16)
- 更多的帧内预测模式(35 种 vs H.264 的 9 种)
- 更灵活的划分结构(CTU、CU、PU、TU)
3.计算复杂度:
- H.265 编解码复杂度是 H.264 的 2-3 倍
- 需要更强的硬件性能支持
4.应用场景:
- H.264:广泛应用于各种设备和场景,兼容性好
- H.265:主要用于 4K/8K 超高清视频、视频监控等对带宽敏感的场景
5.其他新兴标准:
- H.266/VVC(Versatile Video Coding):HEVC 的继任者,压缩效率再提升约 50%
- AV1:由 AOMedia 联盟开发的开源、免专利费的编码标准
5. 解释音频编码中的采样率、位深度和声道数
采样率(Sample Rate):
- 单位时间内对音频信号的采样次数,单位 Hz
- 表示数字音频对模拟音频的离散化频率
- 常见采样率:44.1kHz(CD 音质)、48kHz(专业音频)、96kHz(高清音频)
- 根据奈奎斯特采样定理,采样率至少需要是信号最高频率的 2 倍才能准确还原信号
位深度(Bit Depth):
- 每个采样点用多少位二进制数表示,决定了动态范围
- 常见位深度:16 位(CD 音质)、24 位(专业音频)、32 位(浮点音频)
- 位深度越大,音频的动态范围越大,细节越丰富
- 16 位音频可表示 65536 个不同的振幅级别,动态范围约 96dB
声道数(Number of Channels):
音频信号的通道数量,决定了空间定位感
常见声道模式:
- 单声道(Mono):1 个声道
- 立体声(Stereo):2 个声道(左、右)
- 5.1 声道:6 个声道(左、右、中置、左环绕、右环绕、低音炮)
- 7.1 声道:8 个声道,提供更丰富的环绕效果
音频数据速率计算:
数据速率(bps)= 采样率(Hz)× 位深度(bit)× 声道数
例如,CD 音质(44.1kHz,16 位,立体声):
44100 × 16 × 2 = 1411200 bps = 1411.2 kbps
音视频开发学习路线:最全音视频学习路线-互联网音视频-嵌入式音视频
三、音视频编解码
1. 什么是 FFmpeg?它包含哪些主要组件?
FFmpeg 是一个开源的跨平台音视频处理库,提供了录制、转换、流媒体传输等功能。
主要组件:
1.核心库:
- libavcodec:音视频编解码库,支持多种编码格式
- libavformat:多媒体容器格式处理库,处理文件格式和协议
- libavutil:通用工具函数库,包含数学运算、字符串处理等
- libavfilter:音视频滤镜库,提供各种音视频特效处理
- libavdevice:输入输出设备库,支持各种硬件设备
- libswscale:视频缩放和格式转换库
- libswresample:音频重采样和格式转换库
2.工具程序:
- ffmpeg:命令行工具,用于格式转换、编码解码等
- ffplay:简单的媒体播放器
- ffprobe:媒体信息分析工具
3.支持的格式:
- 视频编码:H.264、H.265、MPEG-4、VP9、AV1 等
- 音频编码:AAC、MP3、Opus、Vorbis、FLAC 等
- 容器格式:MP4、MKV、FLV、AVI、MOV 等
- 协议:HTTP、RTSP、RTMP、HLS、DASH 等
使用示例(命令行):
# 将视频转换为H.264编码的MP4文件 ffmpeg -i input.avi -c:v libx264 -crf 23 -c:a aac -b:a 128k output.mp4 # 从视频中提取音频 ffmpeg -i input.mp4 -vn -c:a copy output.aac # 调整视频分辨率 ffmpeg -i input.mp4 -s 1280x720 output_720p.mp4
2. 使用 FFmpeg 进行视频解码的基本流程是什么?
使用 FFmpeg 进行视频解码的基本流程如下:
1.注册所有组件:
av_register_all(); // 旧版本FFmpeg需要,新版本已废弃
2.打开输入文件:
AVFormatContext* format_ctx = avformat_alloc_context(); if (avformat_open_input(&format_ctx, input_filename, nullptr, nullptr) != 0) { // 打开文件失败 return -1; }
3.查找流信息:
if (avformat_find_stream_info(format_ctx, nullptr) < 0) { // 查找流信息失败 return -1; }
4.找到视频流并获取解码器:
int video_stream_index = -1; AVCodecParameters* codec_par = nullptr; // 遍历流找到视频流 for (int i = 0; i < format_ctx->nb_streams; i++) { if (format_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { video_stream_index = i; codec_par = format_ctx->streams[i]->codecpar; break; } } if (video_stream_index == -1) { // 未找到视频流 return -1; } // 获取解码器 AVCodec* codec = avcodec_find_decoder(codec_par->codec_id); if (!codec) { // 未找到解码器 return -1; }
5.初始化解码器上下文:
AVCodecContext* codec_ctx = avcodec_alloc_context3(codec); if (avcodec_parameters_to_context(codec_ctx, codec_par) < 0) { // 复制解码器参数失败 return -1; } if (avcodec_open2(codec_ctx, codec, nullptr) < 0) { // 打开解码器失败 return -1; }
6.读取数据包并解码:
AVPacket* packet = av_packet_alloc(); AVFrame* frame = av_frame_alloc(); while (av_read_frame(format_ctx, packet) >= 0) { if (packet->stream_index == video_stream_index) { // 发送数据包到解码器 if (avcodec_send_packet(codec_ctx, packet) < 0) { break; } // 接收解码后的帧 while (avcodec_receive_frame(codec_ctx, frame) == 0) { // 处理解码后的帧数据(frame中包含YUV或RGB数据) process_frame(frame); } } av_packet_unref(packet); // 释放数据包引用 } // 刷新解码器,处理剩余帧 avcodec_send_packet(codec_ctx, nullptr); while (avcodec_receive_frame(codec_ctx, frame) == 0) { process_frame(frame); }
7.释放资源:
av_frame_free(&frame); av_packet_free(&packet); avcodec_close(codec_ctx); avcodec_free_context(&codec_ctx); avformat_close_input(&format_ctx); avformat_free_context(format_ctx);
3. 什么是硬解码?与软解码相比有什么优缺点?
硬解码是指利用专用的硬件(如 GPU、专用编解码芯片)进行音视频编解码的过程。
软解码则是指完全通过 CPU 进行软件编解码。
硬解码的优点:
- 效率高:专用硬件设计用于编解码,效率远高于 CPU
- 功耗低:相比 CPU 满负荷运行,硬解码更省电
- 不占用 CPU 资源:解放 CPU,使其可以处理其他任务
- 支持更高分辨率和帧率:如 4K、8K 视频的实时编解码
硬解码的缺点:
- 兼容性问题:不同硬件支持的格式和功能可能不同
- 灵活性差:硬件功能固定,难以快速支持新的编码标准
- 画质可能不如软解码:部分硬件解码质量可能略逊于高质量软件解码器
- 开发复杂度高:需要针对不同硬件平台进行适配
常见的硬解码 API:
- Windows:DXVA、Media Foundation
- Linux:VA-API、VDPAU
- macOS/iOS:VideoToolbox
- 跨平台:FFmpeg 的硬件加速 API、OpenMAX
应用场景:
- 硬解码:移动设备、机顶盒、实时视频播放等对功耗和性能敏感的场景
- 软解码:专业视频处理、对兼容性和画质要求高的场景
4. 解释视频编码中的码率控制方法(CBR、VBR、CRF)
码率控制是视频编码中控制输出码率的技术,主要方法有:
1.CBR(Constant Bit Rate,固定码率):
- 编码过程中保持码率基本恒定
- 优点:码率稳定,便于网络传输和带宽规划
- 缺点:复杂场景可能导致画质下降,简单场景则可能浪费带宽
- 适用场景:视频会议、直播等对码率稳定性要求高的场景
2.VBR(Variable Bit Rate,可变码率):
- 根据视频内容复杂度动态调整码率
- 复杂场景分配更多码率,简单场景分配较少码率
- 优点:在平均码率相同的情况下,画质优于 CBR
- 缺点:码率波动大,可能超过带宽限制
- 适用场景:预录制视频、存储媒体等对画质要求高的场景
3.CRF(Constant Rate Factor,恒定速率因子):
- 基于质量的编码方式,不直接控制码率
- 通过设置质量因子(0-51,值越小质量越高)控制输出质量
- 优点:可获得一致的主观质量,无需手动计算码率
- 缺点:输出文件大小不可预测
- 适用场景:追求固定质量的视频编码
在 FFmpeg 中使用示例:
# CBR编码 ffmpeg -i input.mp4 -c:v libx264 -x264-params "bitrate=2000:vbv_maxrate=2000:vbv_bufsize=4000" -c:a aac -b:a 128k output_cbr.mp4 # VBR编码 ffmpeg -i input.mp4 -c:v libx264 -b:v 2000k -maxrate 4000k -bufsize 8000k -c:a aac -b:a 128k output_vbr.mp4 # CRF编码 ffmpeg -i input.mp4 -c:v libx264 -crf 23 -c:a aac -b:a 128k output_crf.mp4
5. 音频编码中常见的编码格式有哪些?各有什么特点?
常见的音频编码格式:
1.AAC(Advanced Audio Coding):
- 属于 MPEG 家族,是 MP3 的继任者
- 特点:在相同比特率下音质优于 MP3,尤其在低比特率下表现出色
- 变种:AAC-LC(低复杂度)、AAC-HE(高效,适合低比特率)
- 应用:流媒体、移动设备、数字电视等
2.MP3(MPEG-1 Audio Layer III):
- 经典的音频编码格式,应用广泛
- 特点:压缩率高,兼容性好,但低比特率下音质损失明显
- 比特率范围:32kbps-320kbps
- 应用:音乐下载、便携式播放器
3.Opus:
- 开源、免专利费的音频编码格式
- 特点:低延迟,同时支持语音和音乐,在各种比特率下表现优异
- 比特率范围:6kbps-510kbps
- 应用:实时通信(如 WebRTC)、语音消息、游戏音频
4.Vorbis:
- 开源、免专利费的音频编码格式
- 特点:无专利限制,音质优于同比特率的 MP3
- 通常与 OGG 容器格式结合使用(OGG Vorbis)
- 应用:开源项目、互联网广播
5.FLAC(Free Lossless Audio Codec):
- 无损音频编码格式
- 特点:压缩后不失真,保留原始音频的所有信息
- 压缩率约为 50%-70%
- 应用:高品质音乐存储、音乐收藏
6.WAV:
- 无损音频格式,通常存储 PCM 原始音频数据
- 特点:音质最佳,但文件体积大,不适合网络传输
- 应用:音频编辑、原始音频存储
7.AMR(Adaptive Multi-Rate):
- 专为语音设计的编码格式
- 特点:低比特率下有较好的语音清晰度
- 应用:移动电话语音编码
音视频开发项目:
四、音视频处理与滤镜
1. 如何使用 FFmpeg 滤镜对视频进行处理?
FFmpeg 提供了强大的滤镜系统(libavfilter),可以对音视频进行各种处理。使用滤镜的基本流程如下:
1.定义滤镜 graph:
// 例如:将视频缩放到640x480并添加水印 const char* filter_descr = "scale=640:480,overlay=10:10";
2.创建滤镜上下文相关结构:
AVFilterGraph* filter_graph = avfilter_graph_alloc(); AVFilterInOut* inputs = avfilter_inout_alloc(); AVFilterInOut* outputs = avfilter_inout_alloc();
3.初始化滤镜上下文:
// 获取输入和输出滤镜 const AVFilter* buffersrc = avfilter_get_by_name("buffer"); const AVFilter* buffersink = avfilter_get_by_name("buffersink"); // 设置输入滤镜参数(根据实际视频参数设置) AVCodecContext* codec_ctx = ...; // 解码器上下文 char args[512]; snprintf(args, sizeof(args), "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d", codec_ctx->width, codec_ctx->height, codec_ctx->pix_fmt, codec_ctx->time_base.num, codec_ctx->time_base.den, codec_ctx->sample_aspect_ratio.num, codec_ctx->sample_aspect_ratio.den); // 创建输入滤镜上下文 AVFilterContext* buffersrc_ctx; if (avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in", args, nullptr, filter_graph) < 0) { // 错误处理 } // 创建输出滤镜上下文 AVFilterContext* buffersink_ctx; if (avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out", nullptr, nullptr, filter_graph) < 0) { // 错误处理 } // 设置输出滤镜接受的像素格式 enum AVPixelFormat pix_fmts[] = {AV_PIX_FMT_YUV420P, AV_PIX_FMT_NONE}; if (av_opt_set_int_list(buffersink_ctx, "pix_fmts", pix_fmts, AV_PIX_FMT_NONE, AV_OPT_SEARCH_CHILDREN) < 0) { // 错误处理 }
4.配置滤镜连接:
// 设置输出 outputs->name = av_strdup("in"); outputs->filter_ctx = buffersrc_ctx; outputs->pad_idx = 0; outputs->next = nullptr; // 设置输入 inputs->name = av_strdup("out"); inputs->filter_ctx = buffersink_ctx; inputs->pad_idx = 0; inputs->next = nullptr; // 解析滤镜graph if (avfilter_graph_parse_ptr(filter_graph, filter_descr, &inputs, &outputs, nullptr) < 0) { // 错误处理 } // 验证滤镜graph if (avfilter_graph_config(filter_graph, nullptr) < 0) { // 错误处理 }
5.应用滤镜处理帧:
AVFrame* frame = ...; // 解码后的帧 AVFrame* filtered_frame = av_frame_alloc(); // 向滤镜输入帧 if (av_buffersrc_add_frame_flags(buffersrc_ctx, frame, 0) < 0) { // 错误处理 } // 从滤镜获取处理后的帧 while (av_buffersink_get_frame(buffersink_ctx, filtered_frame) >= 0) { // 处理过滤后的帧 process_filtered_frame(filtered_frame); av_frame_unref(filtered_frame); }
6.释放资源:
av_frame_free(&filtered_frame); avfilter_inout_free(&inputs); avfilter_inout_free(&outputs); avfilter_graph_free(&filter_graph);
常用的视频滤镜:
- scale:调整视频尺寸
- overlay:叠加水印或其他视频
- crop:裁剪视频
- rotate:旋转视频
- eq:调整亮度、对比度、饱和度
- hflip/vflip:水平 / 垂直翻转
2. 如何实现视频的裁剪、缩放和旋转?
使用 FFmpeg 可以方便地实现视频的裁剪、缩放和旋转操作,既可以通过命令行,也可以通过 API 实现。
命令行实现:
1.视频裁剪:
# 裁剪:x:y为起始坐标,width:height为裁剪尺寸 ffmpeg -i input.mp4 -vf "crop=640:480:100:50" output_cropped.mp4
2.视频缩放:
# 缩放到指定尺寸 ffmpeg -i input.mp4 -vf "scale=1280:720" output_scaled.mp4 # 按比例缩放(保持宽高比) ffmpeg -i input.mp4 -vf "scale=1280:-1" output_scaled.mp4
3.视频旋转:
# 旋转90度(顺时针) ffmpeg -i input.mp4 -vf "transpose=1" output_rotated.mp4 # 旋转180度 ffmpeg -i input.mp4 -vf "transpose=2,transpose=2" output_rotated.mp4 # 旋转90度(逆时针) ffmpeg -i input.mp4 -vf "transpose=2" output_rotated.mp4
C++ API 实现:
使用 FFmpeg 的 libavfilter 库实现视频处理:
// 初始化滤镜graph(以缩放为例) const char* filter_desc = "scale=1280:720"; // 缩放滤镜 // const char* filter_desc = "crop=640:480:100:50"; // 裁剪滤镜 // const char* filter_desc = "transpose=1"; // 旋转滤镜 // 创建滤镜graph和上下文(代码与前面滤镜初始化类似) // ... 省略滤镜graph初始化代码 ... // 处理帧 AVFrame* frame = av_frame_alloc(); AVFrame* filtered_frame = av_frame_alloc(); while (av_read_frame(format_ctx, packet) >= 0) { if (packet->stream_index == video_stream_index) { // 解码帧 avcodec_send_packet(codec_ctx, packet); while (avcodec_receive_frame(codec_ctx, frame) == 0) { // 将帧送入滤镜 if (av_buffersrc_add_frame(buffersrc_ctx, frame) < 0) { // 错误处理 } // 获取处理后的帧 while (av_buffersink_get_frame(buffersink_ctx, filtered_frame) >= 0) { // 处理过滤后的帧(缩放/裁剪/旋转后的帧) process_frame(filtered_frame); av_frame_unref(filtered_frame); } } } av_packet_unref(packet); } // 释放资源 // ...
关键参数说明:
1.裁剪参数:crop=width:height:x:y
- width: 裁剪后的宽度
- height: 裁剪后的高度
- x: 起始 x 坐标(从左上角开始)
- y: 起始 y 坐标
2.缩放参数:scale=width:height
- width: 缩放后的宽度
- height: 缩放后的高度
- 使用 - 1 保持宽高比,如scale=1280:-1
3.旋转参数:transpose=direction
- 0: 逆时针旋转 90 度并垂直翻转
- 1: 顺时针旋转 90 度
- 2: 逆时针旋转 90 度
- 3: 顺时针旋转 90 度并水平翻转
3. 音频处理中如何实现音量调节、混音和声道分离?
音频处理是音视频开发中的重要部分,FFmpeg 提供了丰富的音频处理功能。
音量调节:
1.命令行实现:
# 降低音量到50% ffmpeg -i input.mp3 -filter:a "volume=0.5" output.mp3 # 提高音量到200% ffmpeg -i input.mp3 -filter:a "volume=2.0" output.mp3 # 按分贝调节(增加10dB) ffmpeg -i input.mp3 -filter:a "volume=10dB" output.mp3
2.C++ API 实现:
// 初始化音频滤镜(音量调节) const char* filter_desc = "volume=0.5"; // 音量调节到50% // 获取音频解码器上下文等(与视频处理类似) // ... // 创建音频滤镜graph AVFilterGraph* filter_graph = avfilter_graph_alloc(); AVFilterInOut* inputs = avfilter_inout_alloc(); AVFilterInOut* outputs = avfilter_inout_alloc(); // 获取音频输入输出滤镜 const AVFilter* abuffersrc = avfilter_get_by_name("abuffer"); const AVFilter* abuffersink = avfilter_get_by_name("abuffersink"); // 设置音频输入参数 char args[512]; snprintf(args, sizeof(args), "sample_rate=%d:sample_fmt=%s:channels=%d:channel_layout=0x%llx", codec_ctx->sample_rate, av_get_sample_fmt_name(codec_ctx->sample_fmt), codec_ctx->channels, codec_ctx->channel_layout); // 创建输入滤镜上下文 AVFilterContext* abuffersrc_ctx; avfilter_graph_create_filter(&abuffersrc_ctx, abuffersrc, "in", args, nullptr, filter_graph); // 创建输出滤镜上下文 AVFilterContext* abuffersink_ctx; avfilter_graph_create_filter(&abuffersink_ctx, abuffersink, "out", nullptr, nullptr, filter_graph); // 设置输出音频格式(可选) enum AVSampleFormat sample_fmts[] = {AV_SAMPLE_FMT_S16, AV_SAMPLE_FMT_NONE}; av_opt_set_int_list(abuffersink_ctx, "sample_fmts", sample_fmts, AV_SAMPLE_FMT_NONE, AV_OPT_SEARCH_CHILDREN); // 配置滤镜连接 // ... 类似视频滤镜配置 ... // 解析并配置滤镜graph avfilter_graph_parse_ptr(filter_graph, filter_desc, &inputs, &outputs, nullptr); avfilter_graph_config(filter_graph, nullptr); // 处理音频帧 // ... 类似视频帧处理流程 ...
音频混音:
1.命令行实现(混合两个音频文件):
ffmpeg -i input1.mp3 -i input2.mp3 -filter_complex "amix=inputs=2:duration=longest" output.mp3
2.主要参数:
- inputs: 输入音频流数量
- duration: 输出时长(longest: 取最长输入,shortest: 取最短输入,first: 取第一个输入)
- volume: 混合后的音量调整
声道分离:
1.命令行实现(分离立体声为左右声道):
# 提取左声道 ffmpeg -i input_stereo.mp3 -filter:a "pan=mono|c0=c0" left_channel.mp3 # 提取右声道 ffmpeg -i input_stereo.mp3 -filter:a "pan=mono|c0=c1" right_channel.mp3
2.声道映射说明:
- pan=mono|c0=c0: 将输入的第一个声道(左声道)映射到输出的单声道
- pan=mono|c0=c1: 将输入的第二个声道(右声道)映射到输出的单声道
- 对于 5.1 声道等复杂声道,可以指定更复杂的映射关系
4. 什么是 YUV 到 RGB 的转换?如何实现?
YUV 和 RGB 是两种不同的颜色空间表示方式,在音视频处理中经常需要进行相互转换。
YUV 到 RGB 转换的原因:
- 视频编解码通常使用 YUV 格式
- 显示设备(如屏幕)通常使用 RGB 格式
- 不同处理阶段可能需要不同的颜色空间
转换公式(以 BT.601 标准为例):
从 YUV 到 RGB 的转换:
R = Y + 1.402 * (V - 128) G = Y - 0.34414 * (U - 128) - 0.71414 * (V - 128) B = Y + 1.772 * (U - 128)
从 RGB 到 YUV 的转换:
Y = 0.299 * R + 0.587 * G + 0.114 * B U = -0.14713 * R - 0.28886 * G + 0.436 * B + 128 V = 0.615 * R - 0.51499 * G - 0.10001 * B + 128
C++ 实现 YUV420P 到 RGB24 的转换:
void yuv420p_to_rgb24(const unsigned char* y, const unsigned char* u, const unsigned char* v, unsigned char* rgb, int width, int height) { int y_size = width * height; int uv_size = y_size / 4; for (int i = 0; i < height; i++) { for (int j = 0; j < width; j++) { // 获取Y分量 int Y = y[i * width + j]; // 计算对应的U、V分量位置(YUV420P格式) int u_idx = (i / 2) * (width / 2) + (j / 2); int U = u[u_idx] - 128; int V = v[u_idx] - 128; // 转换公式 int R = Y + (int)(1.402 * V); int G = Y - (int)(0.34414 * U + 0.71414 * V); int B = Y + (int)(1.772 * U); // 确保值在0-255范围内 R = std::clamp(R, 0, 255); G = std::clamp(G, 0, 255); B = std::clamp(B, 0, 255); // 存储RGB值(RGB24格式) int rgb_idx = (i * width + j) * 3; rgb[rgb_idx] = R; rgb[rgb_idx + 1] = G; rgb[rgb_idx + 2] = B; } } }
使用 FFmpeg 实现转换:
// 使用libswscale进行格式转换 struct SwsContext* sws_ctx = sws_getContext( width, height, AV_PIX_FMT_YUV420P, // 源宽度、高度、格式 width, height, AV_PIX_FMT_RGB24, // 目标宽度、高度、格式 SWS_BILINEAR, // 缩放算法 nullptr, nullptr, nullptr ); // 源帧(YUV420P)和目标帧(RGB24) AVFrame* yuv_frame = ...; // 已填充YUV数据的帧 AVFrame* rgb_frame = av_frame_alloc(); // 分配RGB帧数据 rgb_frame->width = width; rgb_frame->height = height; rgb_frame->format = AV_PIX_FMT_RGB24; av_frame_get_buffer(rgb_frame, 32); // 执行转换 sws_scale(sws_ctx, yuv_frame->data, yuv_frame->linesize, 0, height, rgb_frame->data, rgb_frame->linesize); // 使用转换后的RGB数据(rgb_frame->data[0]) // ... // 释放资源 sws_freeContext(sws_ctx); av_frame_free(&rgb_frame);
5. 如何实现视频水印的添加(文字水印和图片水印)?
添加水印是视频处理中的常见需求,可以通过 FFmpeg 实现文字水印和图片水印的添加。
文字水印:
1.命令行实现:
# 添加文字水印(需要libfreetype支持) ffmpeg -i input.mp4 -vf "drawtext=text='My Watermark':x=10:y=10:fontsize=24:fontcolor=white:shadowcolor=black:shadowx=2:shadowy=2" output_text.mp4
2.主要参数说明:
- text: 水印文字内容
- x,y: 水印位置坐标
- fontsize: 字体大小
- fontcolor: 字体颜色
- fontfile: 字体文件路径(可选)
- shadowcolor, shadowx, shadowy: 阴影设置
图片水印:
1.命令行实现:
# 添加图片水印 ffmpeg -i input.mp4 -i watermark.png -filter_complex "overlay=10:10" output_image.mp4 # 添加右下角水印(距离右边和底部各10像素) ffmpeg -i input.mp4 -i watermark.png -filter_complex "overlay=W-w-10:H-h-10" output_image.mp4
2.主要参数说明:
- overlay=x:y: 水印位置,x 和 y 为左上角坐标
- W: 视频宽度,H: 视频高度
- w: 水印宽度,h: 水印高度
- 可以使用表达式计算位置,如右下角:W-w-10:H-h-10
C++ API 实现(图片水印):
// 初始化滤镜graph(添加图片水印) const char* filter_descr = "overlay=10:10"; // 水印位置(10,10) // 打开主视频文件和水印图片 AVFormatContext* video_fmt_ctx = ...; // 主视频上下文 AVFormatContext* watermark_fmt_ctx = ...; // 水印图片上下文 // 找到视频流和水印流 int video_stream_idx = ...; int watermark_stream_idx = ...; // 获取解码器和创建解码器上下文 // ... 省略解码器初始化代码 ... // 创建滤镜graph AVFilterGraph* filter_graph = avfilter_graph_alloc(); // 创建输入滤镜(主视频和水印) const AVFilter* buffersrc = avfilter_get_by_name("buffer"); const AVFilter* buffersrc2 = avfilter_get_by_name("buffer"); const AVFilter* buffersink = avfilter_get_by_name("buffersink"); // 为两个输入创建滤镜上下文 AVFilterContext* src1_ctx, *src2_ctx, *sink_ctx; // ... 省略滤镜上下文创建代码 ... // 配置滤镜连接 AVFilterInOut* inputs = avfilter_inout_alloc(); AVFilterInOut* outputs = avfilter_inout_alloc(); // 设置输出 outputs->name = av_strdup("in0"); outputs->filter_ctx = src1_ctx; outputs->pad_idx = 0; outputs->next = avfilter_inout_alloc(); outputs->next->name = av_strdup("in1"); outputs->next->filter_ctx = src2_ctx; outputs->next->pad_idx = 0; outputs->next->next = nullptr; // 设置输入 inputs->name = av_strdup("out"); inputs->filter_ctx = sink_ctx; inputs->pad_idx = 0; inputs->next = nullptr; // 解析滤镜graph avfilter_graph_parse_ptr(filter_graph, filter_descr, &inputs, &outputs, nullptr); avfilter_graph_config(filter_graph, nullptr); // 处理帧(同时处理主视频和水印图片) // ... 省略帧处理代码 ... // 释放资源 // ...
五、流媒体与传输
1. 什么是 RTSP、RTMP 和 HLS 协议?它们有什么区别?
RTSP、RTMP 和 HLS 都是流媒体传输协议,但设计目标和应用场景不同:
RTSP(Real-Time Streaming Protocol,实时流协议):
- 基于文本的应用层协议,通常使用 RTP 作为传输层协议
- 采用客户端 - 服务器模式,支持双向通信和控制(暂停、继续、快进等)
- 主要用于 IP 摄像机、视频监控等场景
- 通常工作在 TCP 554 端口
- 特点:低延迟,适合实时互动,但防火墙穿透性较差
RTMP(Real-Time Messaging Protocol,实时消息协议):
- 由 Adobe 开发的基于 TCP 的二进制协议
- 支持音视频和数据的实时传输
- 分为多个变种:RTMPT(HTTP 封装)、RTMPS(加密)、RTMPTE(加密 + HTTP)
- 通常工作在 TCP 1935 端口
- 特点:延迟较低(1-3 秒),适合直播,曾广泛用于 Flash 播放器
- 目前逐渐被 HLS 和 WebRTC 取代
HLS(HTTP Live Streaming):
- 由 Apple 开发的基于 HTTP 的流媒体协议
- 将视频分割成一系列小的 TS 格式文件(通常 10 秒左右),通过 HTTP 传输
- 使用 M3U8 文件作为索引,描述 TS 文件的序列和时长
- 支持自适应码率(ABR),可根据网络状况自动切换清晰度
- 特点:兼容性好(支持所有平台),防火墙穿透性强,但延迟较高(通常 10-30 秒)
- 广泛用于直播和点播服务
主要区别对比:
2. 如何使用 FFmpeg 进行 RTMP 推流和拉流?
使用 FFmpeg 可以方便地实现 RTMP 协议的推流和拉流功能。
RTMP 推流:
1.命令行推流:
# 将本地文件推送到RTMP服务器 ffmpeg -re -i input.mp4 -c:v libx264 -c:a aac -f flv rtmp://server/live/streamKey # 从摄像头推流 ffmpeg -f v4l2 -i /dev/video0 -f flv rtmp://server/live/cameraStream # 带参数的推流(指定码率、分辨率等) ffmpeg -re -i input.mp4 \ -c:v libx264 -b:v 2000k -s 1280x720 -r 30 \ -c:a aac -b:a 128k -ar 44100 \ -f flv rtmp://server/live/streamKey
参数说明:
- -re:按实际帧率读取输入,用于推流时控制速度
- -c:v:视频编码器
- -c:a:音频编码器
- -b:v:视频比特率
- -b:a:音频比特率
- -s:视频分辨率
- -r:帧率
- -f flv:指定输出格式为 FLV(RTMP 通常使用 FLV 格式)
2.C++ API 推流实现:
// 1. 注册所有组件 av_register_all(); avformat_network_init(); // 2. 打开输入文件 AVFormatContext* in_fmt_ctx = nullptr; if (avformat_open_input(&in_fmt_ctx, "input.mp4", nullptr, nullptr) < 0) { // 错误处理 } avformat_find_stream_info(in_fmt_ctx, nullptr); // 3. 创建输出上下文 AVFormatContext* out_fmt_ctx = nullptr; const char* rtmp_url = "rtmp://server/live/streamKey"; if (avformat_alloc_output_context2(&out_fmt_ctx, nullptr, "flv", rtmp_url) < 0) { // 错误处理 } // 4. 为输出上下文创建流 for (int i = 0; i < in_fmt_ctx->nb_streams; i++) { AVStream* in_stream = in_fmt_ctx->streams[i]; AVStream* out_stream = avformat_new_stream(out_fmt_ctx, nullptr); if (!out_stream) { // 错误处理 } // 复制流参数 if (avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar) < 0) { // 错误处理 } out_stream->codecpar->codec_tag = 0; } // 5. 打开输出IO if (!(out_fmt_ctx->oformat->flags & AVFMT_NOFILE)) { if (avio_open(&out_fmt_ctx->pb, rtmp_url, AVIO_FLAG_WRITE) < 0) { // 错误处理 } } // 6. 写文件头 if (avformat_write_header(out_fmt_ctx, nullptr) < 0) { // 错误处理 } // 7. 读取并发送数据包 AVPacket pkt; while (av_read_frame(in_fmt_ctx, &pkt) >= 0) { AVStream* in_stream = in_fmt_ctx->streams[pkt.stream_index]; AVStream* out_stream = out_fmt_ctx->streams[pkt.stream_index]; // 转换时间戳 pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX); pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX); pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base); pkt.pos = -1; // 发送数据包 if (av_interleaved_write_frame(out_fmt_ctx, &pkt) < 0) { // 错误处理 break; } av_packet_unref(&pkt); } // 8. 写文件尾 av_write_trailer(out_fmt_ctx); // 9. 释放资源 // ...
RTMP 拉流:
1.命令行拉流:
# 拉取RTMP流并保存为本地文件 ffmpeg -i rtmp://server/live/streamKey -c copy output.flv # 拉流并转码 ffmpeg -i rtmp://server/live/streamKey -c:v libx264 -c:a aac output.mp4
2.C++ API 拉流实现与普通文件解码类似,只需将输入 URL 改为 RTMP 地址:
// 打开RTMP流 const char* rtmp_url = "rtmp://server/live/streamKey"; AVFormatContext* fmt_ctx = nullptr; if (avformat_open_input(&fmt_ctx, rtmp_url, nullptr, nullptr) < 0) { // 错误处理 } // 后续流程与解码本地文件相同 // ...
3. 什么是 HLS 协议?如何生成 HLS 流?
HLS(HTTP Live Streaming)是由 Apple 开发的基于 HTTP 的自适应比特率流媒体协议。
HLS 的工作原理:
- 将视频分割成一系列 TS 格式的小片段(通常 5-10 秒)
- 生成 M3U8 格式的索引文件,描述 TS 片段的顺序、时长和地址
- 客户端通过 HTTP 协议获取 M3U8 文件和 TS 片段
- 客户端可以根据网络状况切换不同码率的流
HLS 的主要组成:
- M3U8 文件:UTF-8 编码的文本文件,包含媒体片段信息
- TS 文件:MPEG-2 Transport Stream 格式的媒体片段
- 可选的多个码率流:提供不同清晰度的视频流
生成 HLS 流的方法:
1.使用 FFmpeg 命令行生成:
# 生成单个码率的HLS流 ffmpeg -i input.mp4 -c:v libx264 -c:a aac -hls_time 10 -hls_list_size 0 output.m3u8 # 生成多个码率的HLS流(自适应码率) ffmpeg -i input.mp4 \ -filter_complex "[0:v]split=3[v1][v2][v3]; \ [v1]scale=640:360[v1out]; \ [v2]scale=1280:720[v2out]; \ [v3]scale=1920:1080[v3out]" \ -map "[v1out]" -c:v libx264 -b:v 800k -c:a aac -b:a 64k -hls_time 10 -hls_list_size 0 360p/output.m3u8 \ -map "[v2out]" -c:v libx264 -b:v 2500k -c:a aac -b:a 128k -hls_time 10 -hls_list_size 0 720p/output.m3u8 \ -map "[v3out]" -c:v libx264 -b:v 5000k -c:a aac -b:a 192k -hls_time 10 -hls_list_size 0 1080p/output.m3u8 # 生成主M3U8文件(包含多个码率) echo "#EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=864000,RESOLUTION=640x360 360p/output.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=2628000,RESOLUTION=1280x720 720p/output.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=5192000,RESOLUTION=1920x1080 1080p/output.m3u8" > master.m3u8
参数说明:
- -hls_time:每个 TS 片段的时长(秒)
- -hls_list_size:M3U8 文件中包含的最大片段数,0 表示包含所有片段
- -hls_segment_filename:指定 TS 片段的命名格式
2.M3U8 文件示例(单个码率):
#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:0 #EXTINF:10.0, output0.ts #EXTINF:10.0, output1.ts #EXTINF:5.0, output2.ts #EXT-X-ENDLIST
3.C++ API 生成 HLS 流:
// 基本流程与推流类似,但输出格式为hls AVFormatContext* out_fmt_ctx = nullptr; avformat_alloc_output_context2(&out_fmt_ctx, nullptr, "hls", "output.m3u8"); // 设置HLS参数 AVDictionary* opt = nullptr; av_dict_set(&opt, "hls_time", "10", 0); // 每个片段10秒 av_dict_set(&opt, "hls_list_size", "0", 0); // 包含所有片段 // 打开输出 if (!(out_fmt_ctx->oformat->flags & AVFMT_NOFILE)) { avio_open(&out_fmt_ctx->pb, "output.m3u8", AVIO_FLAG_WRITE); } // 写文件头 avformat_write_header(out_fmt_ctx, &opt); // 处理并写入数据包(与推流类似) // ... // 写文件尾 av_write_trailer(out_fmt_ctx); // 释放资源 // ...
4. 什么是 WebRTC?它与其他流媒体协议有什么区别?
WebRTC(Web Real-Time Communication)是一套支持网页浏览器进行实时音视频通信的 API 和协议。
核心特点:
- 实时性:延迟通常在几百毫秒以内
- 互动性:支持双向实时通信
- 浏览器原生支持:无需插件
- 点对点通信:可直接在两个客户端之间传输数据
- 自适应:根据网络状况动态调整码率和质量
WebRTC 的主要组件:
1.媒体捕获:getUserMedia API 获取音视频流
2.媒体传输:
- RTP(Real-time Transport Protocol):传输实时媒体
- RTCP(RTP Control Protocol):监控传输质量
- SRTP(Secure RTP):提供加密和认证
3.会话建立:
- SDP(Session Description Protocol):描述媒体会话
- ICE(Interactive Connectivity Establishment):处理 NAT 穿越
- STUN/TURN:辅助 ICE 进行 NAT 穿越
4.数据通道:支持非媒体数据的实时传输
与其他流媒体协议的区别:
WebRTC 的应用场景:
- 视频会议系统
- 在线教育实时互动
- 视频聊天应用
- 实时游戏互动
- 远程医疗咨询
5. 如何处理网络抖动和丢包对音视频传输的影响?
网络抖动和丢包是音视频传输中常见的问题,会导致播放卡顿、花屏、声音断续等问题。处理方法包括:
1.抖动处理:
缓冲区(Jitter Buffer):接收端设置缓冲区,暂时存储数据包,平滑输出
- 缓冲区大小需平衡延迟和抗抖动能力
- 动态调整缓冲区大小以适应网络状况
// 简单的抖动缓冲区实现思路 class JitterBuffer { private: std::queue<MediaPacket> buffer; std::chrono::milliseconds target_delay; // 目标延迟 public: // 添加数据包到缓冲区 void push(const MediaPacket& packet) { buffer.push(packet); // 可根据缓冲区大小动态调整目标延迟 } // 获取可播放的数据包 bool pop(MediaPacket& packet) { if (buffer.empty()) return false; // 检查是否达到播放时间 auto now = get_current_time(); auto first_packet_time = buffer.front().timestamp; if (now - first_packet_time >= target_delay) { packet = buffer.front(); buffer.pop(); return true; } return false; } };
2.丢包处理:
前向纠错(FEC):发送额外的冗余数据,接收端可通过冗余数据恢复丢失的数据包
- 例如:每发送 3 个数据包,额外发送 1 个 FEC 包
自动重传请求(ARQ):检测到丢包时,请求发送端重新发送
- 适用于非实时场景,可能增加延迟
丢包隐藏(PLC,Packet Loss Concealment):
- 音频:通过插值、重复等方式生成替代数据
- 视频:使用前一帧数据替代,或跳过丢失的宏块
// 简单的音频丢包隐藏示例 AudioFrame plc_conceal(const AudioFrame& previous_frame, int lost_samples) { AudioFrame concealed; concealed.sample_rate = previous_frame.sample_rate; concealed.channels = previous_frame.channels; concealed.samples = lost_samples; concealed.data = new float[lost_samples * concealed.channels]; // 简单的重复最后几个样本的策略 int repeat_samples = std::min(previous_frame.samples, 100); for (int i = 0; i < lost_samples; i++) { int idx = i % repeat_samples; for (int c = 0; c < concealed.channels; c++) { concealed.data[i * concealed.channels + c] = previous_frame.data[(previous_frame.samples - repeat_samples + idx) * previous_frame.channels + c]; } } return concealed; }
3.自适应码率(ABR):
- 实时监测网络状况(带宽、丢包率)
- 根据网络状况动态调整发送码率
- 网络好时提高码率,网络差时降低码率
4.拥塞控制:
- 使用拥塞控制算法(如 WebRTC 的 GCC)调整发送速率
- 避免网络拥塞加剧
- 平衡发送速率和网络承载能力
5.其他策略:
- 数据包优先级:为关键数据(如 I 帧)设置更高优先级
- 分块传输:将大帧分成小块传输,降低单包丢失影响
- 多路径传输:利用多个网络路径传输,提高可靠性
六、音视频播放与渲染
1. 如何实现音视频同步播放?
音视频同步是多媒体播放中的关键技术,确保音频和视频在正确的时间点播放。
同步基础:
- 时间戳:每个音视频帧都有一个时间戳,通常基于相同的时间基准
- 时钟源:需要一个参考时钟来决定何时播放哪个帧
主要同步策略:
1.以视频时钟为主:
- 以视频帧的时间戳为基准
- 调整音频播放速度以匹配视频
- 优点:视频流畅性好
- 缺点:可能导致音频变调或卡顿
2.以音频时钟为主:
- 以音频帧的时间戳为基准(人耳对音频同步更敏感)
- 调整视频播放速度或丢帧 / 重复帧以匹配音频
- 优点:音频体验更自然
- 缺点:可能导致视频偶尔卡顿
3.以外部时钟为主:
- 使用独立的外部时钟作为基准
- 同时调整音频和视频以匹配外部时钟
- 优点:多设备同步时效果好
- 缺点:实现复杂
实现步骤:
// 音视频同步播放器类 class AVPlayer { private: // 音频和视频解码器 VideoDecoder video_decoder; AudioDecoder audio_decoder; // 音视频渲染器 VideoRenderer video_renderer; AudioRenderer audio_renderer; // 时钟(以音频时钟为基准) double audio_clock = 0.0; // 同步阈值 const double sync_threshold = 0.01; // 10ms const double max_sync_diff = 0.1; // 最大同步误差100ms public: void play() { // 启动解码线程 std::thread video_decode_thread(&AVPlayer::decode_video, this); std::thread audio_decode_thread(&AVPlayer::decode_audio, this); // 播放循环 while (is_playing) { // 获取音频帧并播放 if (audio_decoder.has_frame()) { AudioFrame frame = audio_decoder.get_frame(); play_audio_frame(frame); } // 获取视频帧并同步播放 if (video_decoder.has_frame()) { VideoFrame frame = video_decoder.get_frame(); // 计算视频帧应该播放的时间 double frame_pts = frame.pts; double current_audio_clock = get_audio_clock(); // 计算时间差 double diff = frame_pts - current_audio_clock; if (fabs(diff) < sync_threshold) { // 时间差在阈值内,直接播放 video_renderer.render(frame); } else if (diff > max_sync_diff) { // 视频超前太多,等待 std::this_thread::sleep_for(std::chrono::milliseconds((int)(diff * 1000))); video_renderer.render(frame); } else if (diff < -max_sync_diff) { // 视频滞后太多,跳过该帧 // 或者可以尝试加速播放后续帧 continue; } else { // 小范围误差,直接播放 video_renderer.render(frame); } } } // 等待线程结束 video_decode_thread.join(); audio_decode_thread.join(); } // 更新音频时钟 void update_audio_clock(double pts, int bytes_per_sec, int bytes_played) { // 根据已播放的字节数更新音频时钟 audio_clock = pts + (double)bytes_played / bytes_per_sec; } // 获取当前音频时钟 double get_audio_clock() { // 考虑到音频缓冲区中的数据,计算实际播放时间 return audio_clock - audio_renderer.get_buffer_delay(); } // 播放音频帧 void play_audio_frame(AudioFrame& frame) { audio_renderer.render(frame); // 更新音频时钟 int bytes_per_sec = frame.sample_rate * frame.channels * (frame.bits_per_sample / 8); update_audio_clock(frame.pts, bytes_per_sec, frame.data_size); } };
同步优化:
- 平滑调整:避免突然的大调整,采用渐进式调整
- 动态阈值:根据内容复杂度调整同步阈值
- 缓冲区管理:合理设置音视频缓冲区大小
- 丢帧策略:智能选择丢弃哪些视频帧(优先丢弃 B 帧)
2. 什么是视频渲染?常用的视频渲染技术有哪些?
视频渲染是将解码后的视频帧数据(通常是 YUV 或 RGB 格式)显示到屏幕上的过程。
常用的视频渲染技术:
1.GDI(Graphics Device Interface):
- Windows 系统原生图形接口
- 优点:实现简单,兼容性好
- 缺点:性能较差,不适合高分辨率和高帧率视频
- 适用场景:简单的低性能需求场景
// GDI渲染示例 void render_with_gdi(HDC hdc, const unsigned char* rgb_data, int width, int height) { // 创建DIB位图 BITMAPINFO bmi = {0}; bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); bmi.bmiHeader.biWidth = width; bmi.bmiHeader.biHeight = -height; // 负号表示从上到下 bmi.bmiHeader.biPlanes = 1; bmi.bmiHeader.biBitCount = 24; bmi.bmiHeader.biCompression = BI_RGB; // 将RGB数据绘制到设备上下文 StretchDIBits(hdc, 0, 0, width, height, 0, 0, width, height, rgb_data, &bmi, DIB_RGB_COLORS, SRCCOPY); }
2.DirectX:
- Windows 平台的高性能图形 API
- 包括 DirectDraw(较旧)和 Direct3D
- 优点:性能好,支持硬件加速
- 缺点:Windows 平台专用,学习曲线较陡
3.OpenGL:
- 跨平台的 3D 图形 API
- 通过纹理映射实现视频渲染
- 优点:跨平台,性能好,支持硬件加速
- 适用场景:需要跨平台且高性能的场景
// OpenGL渲染示例(使用纹理) void init_opengl_renderer(int width, int height) { // 创建纹理 GLuint texture_id; glGenTextures(1, &texture_id); glBindTexture(GL_TEXTURE_2D, texture_id); // 设置纹理参数 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // 分配纹理内存 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr); } void render_with_opengl(const unsigned char* rgb_data, int width, int height) { // 更新纹理数据 glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, rgb_data); // 绘制纹理到屏幕 glBegin(GL_QUADS); glTexCoord2f(0.0f, 0.0f); glVertex2f(-1.0f, -1.0f); glTexCoord2f(1.0f, 0.0f); glVertex2f(1.0f, -1.0f); glTexCoord2f(1.0f, 1.0f); glVertex2f(1.0f, 1.0f); glTexCoord2f(0.0f, 1.0f); glVertex2f(-1.0f, 1.0f); glEnd(); // 刷新缓冲区 glFlush(); glutSwapBuffers(); }
4.Vulkan:
- 新一代高性能跨平台图形 API
- 提供更底层的硬件控制,更高的性能
- 优点:性能极佳,多线程友好
- 缺点:学习曲线陡峭,实现复杂
5.Metal:
- Apple 平台(iOS、macOS)的高性能图形 API
- 替代 OpenGL 在 Apple 平台的地位
- 优点:针对 Apple 硬件优化,性能好
- 缺点:仅限 Apple 平台
6.硬件加速渲染:
- 利用 GPU 的专用硬件进行视频渲染
- 支持 YUV 直接渲染,减少格式转换开销
- 常见 API:VA-API(Linux)、DXVA(Windows)、VideoToolbox(Apple)
选择渲染技术的考量因素:
- 性能需求:分辨率、帧率
- 平台兼容性:Windows、Linux、macOS、移动设备
- 开发复杂度
- 硬件资源限制
3. 音频播放的基本原理是什么?如何实现低延迟音频播放?
音频播放的基本原理是将数字音频信号转换为模拟信号,通过扬声器输出声音。
基本流程:
- 音频解码:将压缩的音频数据(如 MP3、AAC)解码为 PCM 数据
- 音频处理:可能包括音量调节、音效处理、重采样等
- 缓冲区管理:将 PCM 数据放入音频缓冲区
- 数模转换:音频硬件将数字 PCM 数据转换为模拟信号
- 模拟输出:通过扬声器或耳机输出声音
音频播放的关键概念:
- 采样率:每秒采样次数,如 44.1kHz
- 位深度:每个采样点的位数,如 16 位
- 声道数:单声道、立体声等
- 缓冲区:用于平滑播放,平衡 CPU 处理和硬件输出速度
实现低延迟音频播放的方法:
1.减小缓冲区大小:
- 音频缓冲区越小,延迟越低
- 但缓冲区过小可能导致播放卡顿
- 需要找到延迟和稳定性的平衡点
2.使用高效的音频 API:
- 不同平台有专门的低延迟音频 API
- Windows:WASAPI(Exclusive Mode)、ASIO
- macOS/iOS:Audio Unit
- Linux:ALSA、JACK
- 跨平台:PortAudio、SDL_audio
3.优化音频处理流程:
- 减少不必要的音频处理步骤
- 使用高效的算法和数据结构
- 避免在音频处理线程中进行阻塞操作
4.实时线程调度:
- 将音频处理线程设置为高优先级
- 避免线程被频繁切换或阻塞
5.硬件加速:
- 利用硬件加速的音频处理功能
- 减少 CPU 负担,提高处理效率
C++ 实现低延迟音频播放示例(使用 PortAudio):
#include "portaudio.h" #include <vector> // 音频回调函数 static int audio_callback(const void* inputBuffer, void* outputBuffer, unsigned long framesPerBuffer, const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags, void* userData) { // 获取音频数据缓冲区 std::vector<float>* audio_data = (std::vector<float>*)userData; float* out = (float*)outputBuffer; // 填充输出缓冲区 for (unsigned int i = 0; i < framesPerBuffer; i++) { if (audio_data->empty()) { // 如果没有数据,输出静音 *out++ = 0.0f; *out++ = 0.0f; // 立体声 } else { // 从缓冲区获取数据 *out++ = audio_data->front(); *out++ = audio_data->front(); // 复制到右声道 audio_data->erase(audio_data->begin()); } } return paContinue; } // 初始化低延迟音频流 PaStream* init_low_latency_audio(std::vector<float>& audio_buffer, int sample_rate = 44100, int channels = 2, int frames_per_buffer = 64) { // 小缓冲区大小 PaError err; PaStream* stream; PaStreamParameters output_params; // 初始化PortAudio err = Pa_Initialize(); if (err != paNoError) { // 错误处理 return nullptr; } // 配置输出参数 output_params.device = Pa_GetDefaultOutputDevice(); output_params.channelCount = channels; output_params.sampleFormat = paFloat32; // 32位浮点格式 output_params.suggestedLatency = Pa_GetDeviceInfo(output_params.device)->defaultLowOutputLatency; output_params.hostApiSpecificStreamInfo = nullptr; // 打开音频流(低延迟设置) err = Pa_OpenStream(&stream, nullptr, // 无输入 &output_params, sample_rate, frames_per_buffer, // 缓冲区大小,越小延迟越低 paClipOff, // 禁用自动裁剪,减少处理 audio_callback, &audio_buffer); if (err != paNoError) { // 错误处理 Pa_Terminate(); return nullptr; } // 启动音频流 Pa_StartStream(stream); return stream; } // 使用示例 int main() { std::vector<float> audio_buffer; PaStream* stream = init_low_latency_audio(audio_buffer); if (stream) { // 向audio_buffer添加PCM音频数据 // ... // 等待播放完成 while (!audio_buffer.empty()) { Pa_Sleep(10); } // 停止并关闭音频流 Pa_StopStream(stream); Pa_CloseStream(stream); Pa_Terminate(); } return 0; }
低延迟音频播放的挑战:
- 系统调度延迟:操作系统对音频线程的调度延迟
- 硬件限制:音频硬件本身的最小延迟
- 数据准备:需要及时提供音频数据,避免缓冲区下溢
- 不同平台差异:各平台的音频架构和性能特性不同
4. 如何实现视频播放器的快进、快退和暂停功能?
视频播放器的快进、快退和暂停功能需要结合音视频解码、时间戳管理和渲染控制来实现。
核心实现原理:
- 暂停:停止渲染新帧,但保持当前状态
- 快进 / 快退:调整播放速度,或直接跳转到目标时间点
实现方法:
1.暂停功能:
class VideoPlayer { private: bool is_playing = false; bool is_paused = false; // 其他成员变量... public: void pause() { if (is_playing && !is_paused) { is_paused = true; // 暂停音频播放 audio_renderer.pause(); // 保留当前状态,不重置解码器 } } void resume() { if (is_playing && is_paused) { is_paused = false; // 恢复音频播放 audio_renderer.resume(); // 继续播放循环 } } // 播放循环 void play_loop() { is_playing = true; is_paused = false; while (is_playing) { if (is_paused) { // 暂停状态,短暂休眠 std::this_thread::sleep_for(std::chrono::milliseconds(10)); continue; } // 正常播放逻辑 // ... } } };
2.快进 / 快退功能:
方法一:调整播放速度
void set_playback_speed(float speed) { if (speed <= 0) return; // 速度不能为零或负数 // 调整音频播放速度 audio_renderer.set_speed(speed); // 调整视频播放速度(通过调整同步时钟) sync_clock.set_speed(speed); } // 2倍速快进 player.set_playback_speed(2.0f); // 0.5倍速慢放 player.set_playback_speed(0.5f); // 正常速度 player.set_playback_speed(1.0f);
方法二:直接跳转到目标时间点(更高效)
bool seek_to(double target_seconds) { // 暂停播放 bool was_playing = is_playing; pause(); // 清空解码器缓冲区 video_decoder.flush(); audio_decoder.flush(); // 清空渲染器缓冲区 video_renderer.clear(); audio_renderer.clear(); // 重置时钟 sync_clock.reset(); // 调用FFmpeg的seek功能 int64_t target_ts = target_seconds * AV_TIME_BASE; int ret = avformat_seek_file(fmt_ctx, -1, INT64_MIN, target_ts, INT64_MAX, 0); if (ret < 0) { // seek失败 if (was_playing) resume(); return false; } // 恢复播放状态 if (was_playing) resume(); return true; } // 快进到1分钟位置 player.seek_to(60.0); // 快退到30秒位置 player.seek_to(30.0);
3.精确 seek 的实现细节:
int64_t calculate_seek_target(AVFormatContext* fmt_ctx, int stream_index, double target_seconds) { AVStream* stream = fmt_ctx->streams[stream_index]; // 计算目标时间戳 int64_t target_ts = target_seconds * stream->time_base.den / stream->time_base.num; // 找到最接近的关键帧 int64_t keyframe_ts = AV_NOPTS_VALUE; for (int i = 0; i < stream->nb_index_entries; i++) { AVIndexEntry* entry = &stream->index_entries[i]; if (entry->flags & AVINDEX_KEYFRAME) { if (entry->pos >= target_ts) { keyframe_ts = entry->pos; break; } } } // 如果没有找到更后的关键帧,使用最后一个关键帧 if (keyframe_ts == AV_NOPTS_VALUE && stream->nb_index_entries > 0) { keyframe_ts = stream->index_entries[stream->nb_index_entries - 1].pos; } return keyframe_ts != AV_NOPTS_VALUE ? keyframe_ts : target_ts; }
4.进度条更新:
// 获取当前播放时间(秒) double get_current_time() { return sync_clock.get_current_time(); } // 获取总时长(秒) double get_total_duration() { return fmt_ctx->duration / (double)AV_TIME_BASE; } // 获取播放进度(0.0-1.0) float get_progress() { double total = get_total_duration(); if (total <= 0) return 0.0f; return (float)(get_current_time() / total); }
实现注意事项:
- seek 操作应尽量定位到关键帧,提高效率
- 处理 seek 后的音视频同步问题
- 快进时可以跳过部分帧渲染,提高性能
- 音频变速播放可能需要重采样或音调校正
5. 什么是硬件加速渲染?如何利用 GPU 进行视频渲染?
硬件加速渲染是利用 GPU(图形处理器)或专用硬件来加速视频渲染过程,而不是仅使用 CPU。
优势:
- 提高渲染性能,支持更高分辨率和帧率
- 减少 CPU 占用,释放 CPU 资源用于其他任务
- 降低功耗,尤其在移动设备上
- 支持更复杂的视频特效和处理
利用 GPU 进行视频渲染的方式:
1.纹理映射(Texture Mapping):
- 将视频帧数据上传到 GPU 纹理
- 通过渲染纹理到屏幕 quad 实现显示
- 支持 YUV 直接渲染,减少格式转换
// OpenGL纹理渲染流程 void gpu_render_frame(const uint8_t* y_data, const uint8_t* u_data, const uint8_t* v_data, int width, int height) { // 初始化YUV纹理(如果未初始化) static GLuint textures[3] = {0}; if (textures[0] == 0) { glGenTextures(3, textures); // 配置Y纹理 glBindTexture(GL_TEXTURE_2D, textures[0]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, width, height, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr); // 配置U纹理 glBindTexture(GL_TEXTURE_2D, textures[1]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, width/2, height/2, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr); // 配置V纹理 glBindTexture(GL_TEXTURE_2D, textures[2]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, width/2, height/2, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr); } // 更新纹理数据 glBindTexture(GL_TEXTURE_2D, textures[0]); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RED, GL_UNSIGNED_BYTE, y_data); glBindTexture(GL_TEXTURE_2D, textures[1]); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width/2, height/2, GL_RED, GL_UNSIGNED_BYTE, u_data); glBindTexture(GL_TEXTURE_2D, textures[2]); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width/2, height/2, GL_RED, GL_UNSIGNED_BYTE, v_data); // 使用着色器将YUV转换为RGB并渲染 glUseProgram(yuv_shader_program); // 绑定纹理到纹理单元 glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, textures[0]); glUniform1i(glGetUniformLocation(yuv_shader_program, "y_texture"), 0); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, textures[1]); glUniform1i(glGetUniformLocation(yuv_shader_program, "u_texture"), 1); glActiveTexture(GL_TEXTURE2); glBindTexture(GL_TEXTURE_2D, textures[2]); glUniform1i(glGetUniformLocation(yuv_shader_program, "v_texture"), 2); // 绘制全屏 quad draw_fullscreen_quad(); // 交换缓冲区 swap_buffers(); }
2.专用视频渲染 API:
- 利用 GPU 的专用视频处理单元
- 支持硬件解码和渲染的无缝衔接
- Windows: Direct3D Video Acceleration (DXVA)
- Linux: VA-API (Video Acceleration API)
- macOS/iOS: VideoToolbox
- 跨平台: FFmpeg 的 hwaccel 框架
3.YUV 到 RGB 的 GPU 转换:
- 在着色器中实现 YUV 到 RGB 的转换
- 避免 CPU 端的格式转换,提高性能
顶点着色器示例:
#version 330 core layout(location = 0) in vec2 aPos; layout(location = 1) in vec2 aTexCoord; out vec2 TexCoord; void main() { gl_Position = vec4(aPos, 0.0, 1.0); TexCoord = aTexCoord; }
片段着色器示例(YUV 转 RGB):
#version 330 core in vec2 TexCoord; out vec4 FragColor; uniform sampler2D y_texture; uniform sampler2D u_texture; uniform sampler2D v_texture; void main() { // 获取YUV分量 float y = texture(y_texture, TexCoord).r; float u = texture(u_texture, TexCoord).r - 0.5; float v = texture(v_texture, TexCoord).r - 0.5; // YUV转RGB float r = y + 1.402 * v; float g = y - 0.34414 * u - 0.71414 * v; float b = y + 1.772 * u; FragColor = vec4(r, g, b, 1.0); }
4.FFmpeg 硬件加速渲染集成:
// 初始化硬件加速上下文 AVBufferRef* hw_device_ctx = nullptr; av_hwdevice_ctx_create(&hw_device_ctx, AV_HWDEVICE_TYPE_CUDA, nullptr, nullptr, 0); // 创建硬件加速解码器 AVCodec* codec = avcodec_find_decoder_by_name("h264_cuvid"); AVCodecContext* codec_ctx = avcodec_alloc_context3(codec); // 设置解码器参数... // 将硬件设备上下文关联到解码器 codec_ctx->hw_device_ctx = av_buffer_ref(hw_device_ctx); // 解码得到硬件帧 AVFrame* hw_frame = av_frame_alloc(); avcodec_receive_frame(codec_ctx, hw_frame); // 将硬件帧转换为可渲染的纹理 // 平台相关的转换代码...
硬件加速渲染的挑战:
- 平台兼容性:不同平台有不同的硬件加速 API
- 设备驱动:需要正确的 GPU 驱动支持
- 资源管理:GPU 内存管理和纹理资源释放
- 错误处理:硬件加速相关错误的处理和恢复
全部评论
(1) 回帖