当前位置: 首页 > news >正文

9:USB摄像头的最后一战(上):MP4音视频合封!

        经过长途跋涉,我们从开始一无所有,到现在终于集齐了南慕容北乔峰,呸!集齐了视频和音频!但是音频和视频在之前的章节中,都是独立存在的,既然它们集齐了,就让我们开启最终章节--最后一战:音视频合封!

        等等等等,不要误会,最后一战是说USB摄像头的部分的。理论上来说如果电脑挂一个USB摄像头,然后执行我们这一部分的代码,是基本能够实现运动相机的功能的。但是就一个小缺点:有一点点大。。。

序章--MP4搭起舞台

        两大高手集在一起,总要搭起戏台子。什么戏台子最合适呢?那就是MP4!MP4可以让我们的音视频合在一起,唱一出好戏。

1、MP4简介

        MP4(文件扩展名通常是 .mp4)是目前应用最广泛、最通用的数字多媒体容器格式之一。它的核心功能是封装容纳多种不同类型的数据流(主要是视频流、音频流),并辅以元数据(如标题、作者、字幕、章节信息等),将它们组合成一个单一的文件。

        核心概念和特点:

  • 容器格式 (Container Format)​​:

    • 这是 MP4 的本质。它本身不定义视频或音频的编码方式(压缩算法)。
    • 它像一个“盒子”或“包裹”,可以把用不同编码标准(如 H.264, H.265/HEVC, AAC, MP3)压缩的视频和音频轨道、字幕、图片等“装”在一起。
    • 与之相对的是编码格式​(如 H.264, AAC),它们负责具体的音视频数据压缩和解压缩。
  • 基于 ISO 基础媒体文件格式 (ISO Base Media File Format)​​:

    • 这种结构使用“盒子”来组织文件内容。每个 box(或 atom)包含特定类型的数据,并具有明确的长度和类型标识符。
    • 常见的盒子包括:
      • ftyp:文件类型标识(表明这是一个 MP4 文件及其兼容性)。
      • moov:电影元数据盒子(Movie Box)。这是最关键的盒子之一,包含了关于整个文件的结构信息:有多少条轨道(视频、音频、字幕等)、每条轨道的编码格式、时长、分辨率、采样率、时间戳映射关系等。
      • mdat:媒体数据盒子(Media Data Box)。这是文件的主体部分,实际存储着压缩后的视频帧、音频帧等媒体数据。
      • free:空闲空间盒子。
  •  ​MP4 vs .m4a, .m4v, .m4p​:

    这些本质上都是 MP4 容器格式的文件。

    .m4a:通常表示只包含音频(通常是 AAC)的 MP4 文件。

    .m4v:通常表示包含视频(通常是 H.264)的 MP4 文件。Apple 有时用它来区分包含特定功能(如 DRM 或章节)的视频。

    .m4p:Apple iTunes 使用的受 DRM 保护的音频文件(也是 MP4 容器)。

    .mp4:最通用的扩展名,可以包含音视频、纯音频或纯视频(较少见)。

    注:这里提到的DRM是Digital Rights Management​(数字版权管理),而非Linux系统中的Direct Rendering Manager(直接渲染管理器)

2、MP4文件结构

        网络上有很多优秀的解析MP4的文章,我就不在重复造轮子了,推荐这篇文章可以看一下,实在弄不清楚也没关系,反正这些活都是交给FFmpeg去做的,推荐了解但并不会对我们的进程造成影响,因为FFmpeg已经帮我们做好了,比如生成mp4的文件头,使用函数avformat_write_header就能帮我们搞定,其他的部分也类似。

你真的懂 MP4 格式吗? -阿里云开发者社区

        下面是解析mp4文件特别详细的一张图,原来我把这张图贴在了我的卧室,想着天天看,给他背下来。但是现在想想多少有点冒傻气,谁会拿着一本字典天天背呢。没错,这张图就是一本关于mp4的字典,哪里不会看哪里(但是大概率是不会用到的)。

3、将我们之前的内容稍作梳理

        我们在第7章,讨论了如何进行实时录像:

7:实时录像、延时摄影、水印--基于FFmpeg(下)_如何把ffmpeg.c封装成动态库-CSDN博客

        并在第8章讨论了如何从摄像头获取音频流:

8:从USB摄像头把声音拿出来--ALSA大佬登场!-CSDN博客

        这两章内容一个为纯视频,一个为纯音频。笔者打算在第7章的最终代码上进行修改,把第8章的代码加进去,合封到一起。

进入正戏--兵合一处、将打一家

        我们先定个目标:我们通过USB摄像头,使用V4L2将视频获取出来,使用ALSA将音频取出来,并使用FFmpeg将视频编码成h264,将音频编码成aac,并通过FFmpeg将音视频封装进mp4文件。

1、代码结构

        由于这次的代码比较复杂,不能再以单个的文件进行编写和变异了,所以这次将代码分成了以下几个部分:

        

        audio_capture:主要负责的是音频的初始化、采集、清理等工作。需要注意以下几点:

        1)初始化完毕后,就要启动。否则如果超过一定的时间后(本人时长大约500ms),USB摄像头可能会进入休眠的状态,导致无法正常启动。

        2)USB摄像头传输的PCM数据格式为S16的,但是FFmpeg需要的是浮点型,所以需要经过转换,否则音频编码会出现问题,导致声音无法正常编码和播放。

        3)AAC编码器启动等因素,会导致音频出现一个200ms左右的固定延迟,如果音视频同步要求没那么高,也可以不处理,但是本人看着很难受,所以将音频的PTS(稍后讨论)固定提前200ms。

        4)USB摄像头的音频数据周期大小为512,但是FFmpeg编码需要的周期大小为1024,所以需要进行拼包后再放入FFmpeg进行AAC编码。

        video_capture:主要负责的是视频的初始化、采集、清理等工作。视频方面反而比音频注意的地方少,只需要注意初始化完毕后,要及时启动,否则可能摄像头会进入休眠模式,导致启动和捕获失败。

        encoder:主要负责编码器的初始化、视频编码、音频编码以及容器封装的工作。由于我们之前已经有了单独视频和单独音频的编码经验,这里无非就是将AAC和H264两个编码器的工作内容进行了合并。里面最主要的功能之一就是音视频同步的技术,我们会在后面一节专门介绍。

        main:主要负责的是音视频设备的初始化、编码器的初始化、循环处理音视频数据等工作。在大循环中,处理完视频后,再处理音频,实际上这样是不合理的。最合理的方式应该是开启两个独立的线程分别处理。但是因为我的最终目标并非PC端,所以没再深入进行优化,有兴趣的道友可以自行尝试。

        Makefile:由于之前的代码都是单个的,所以直接使用gcc即可完成编译,但是随着文件的增多以及功能越来越负责,只靠手动gcc越来越繁重,所以我们引入Makefile,这个工具可以将我们需要编译的文件、链接的库、编译选项等统统的管理起来。编译的时候只需要执行“make”就可以对工程进行编译。(WOW,又是个新鲜玩意,嵌入式真是个学无止境的领域......)

以下为全部源码。

main.c:

#include "common.h"
#include "video_capture.h"
#include "audio_capture.h"
#include "encoder.h"
#include <signal.h>// 全局退出标志
static volatile int should_exit = 0;// 信号处理函数
void signal_handler(int sig) {printf("\nReceived signal %d, stopping recording...\n", sig);should_exit = 1;
}// 主函数
int main(int argc, char *argv[]) {struct recording_config config;struct video_capture_ctx video_ctx;struct audio_capture_ctx audio_ctx;struct encoder_ctx encoder_ctx;uint8_t *video_data;size_t video_size;int64_t video_timestamp;int16_t audio_buffer[AUDIO_PERIOD_SIZE];int audio_frames;// 初始化默认配置init_default_config(&config);if (argc > 1) {config.recording_time = atoi(argv[1]);}printf("=== USB Camera Recorder ===\n");printf("Recording time: %d seconds\n", config.recording_time);printf("Output file: %s\n", config.output_file);// 设置信号处理signal(SIGINT, signal_handler);   // Ctrl+Csignal(SIGTERM, signal_handler);  // 终止信号// 初始化视频捕获if (video_capture_init(&video_ctx, VIDEO_DEVICE) < 0) {fprintf(stderr, "Failed to initialize video capture\n");goto cleanup;}// 初始化音频捕获(与原始代码顺序一致,在编码器之前)// 先用临时帧大小初始化,后面会根据编码器要求调整if (audio_capture_init(&audio_ctx, AUDIO_DEVICE, AUDIO_PERIOD_SIZE) < 0) {fprintf(stderr, "Failed to initialize audio capture\n");goto cleanup;}// 初始化编码器(最后初始化)if (encoder_init(&encoder_ctx, &config) < 0) {fprintf(stderr, "Failed to initialize encoder\n");goto cleanup;}// 更新音频帧大小(在编码器初始化后)audio_ctx.frame_size = encoder_ctx.audio_codec_ctx->frame_size;// 重新分配音频累积缓冲区以匹配实际帧大小if (audio_ctx.accumulate_buffer) {free(audio_ctx.accumulate_buffer);}audio_ctx.accumulate_buffer = (int16_t*)malloc(audio_ctx.frame_size * sizeof(int16_t));if (!audio_ctx.accumulate_buffer) {fprintf(stderr, "Failed to reallocate audio accumulation buffer\n");goto cleanup;}printf("Starting recording...\n");printf("Video: %dx%d @ %dfps, NV12 -> H.264\n", config.video_width, config.video_height, config.video_fps);printf("Audio: %dHz, %d samples/period, %d channels, S16 -> AAC\n", config.audio_sample_rate, AUDIO_PERIOD_SIZE, config.audio_channels);printf("Codec frame size: %d samples\n", encoder_ctx.audio_codec_ctx->frame_size);printf("Time base: 1/%d\n", AV_TIME_BASE);// 记录录制开始的精确时间struct timespec recording_start_time;clock_gettime(CLOCK_MONOTONIC, &recording_start_time);// 计算录制结束的目标时间(微秒精度)int64_t target_duration_us = (int64_t)config.recording_time * 1000000LL;// 主录制循环int loop_count = 0;int64_t video_frame_count = 0;while (1) {loop_count++;// 检查是否收到退出信号if (should_exit) {printf("Exit signal received, stopping recording...\n");break;}// 检查是否达到录制时长(使用微秒精度)struct timespec current_time;clock_gettime(CLOCK_MONOTONIC, &current_time);int64_t elapsed_us = (current_time.tv_sec - recording_start_time.tv_sec) * 1000000LL +(current_time.tv_nsec - recording_start_time.tv_nsec) / 1000LL;if (elapsed_us >= target_duration_us) {printf("Recording time reached: %.3fs\n", (double)elapsed_us / 1000000.0);break;}if (loop_count % 25 == 1) {  // 每25次循环显示一次进度printf("Loop %d, elapsed: %.3fs\n", loop_count, (double)elapsed_us / 1000000.0);}// 捕获视频帧if (loop_count % 100 == 1) {  // 每100次循环显示调试信息printf("DEBUG: About to capture video frame (loop %d)\n", loop_count);}int video_ret = video_capture_frame(&video_ctx, &video_data, &video_size, &video_timestamp);if (video_ret > 0) {// 使用已获取的当前时间计算视频时间戳int64_t video_pts = elapsed_us;printf("Video frame: count=%ld, elapsed=%.3fs, pts=%ld\n", video_frame_count, (double)elapsed_us / 1000000.0, video_pts);if (loop_count % 100 == 1) {printf("DEBUG: About to encode video frame\n");}int encode_ret = encoder_encode_video_frame(&encoder_ctx, video_data, video_pts);if (encode_ret < 0) {fprintf(stderr, "Failed to encode video frame\n");break;}video_frame_count++;if (loop_count % 100 == 1) {printf("DEBUG: Video frame encoded successfully\n");}} else if (video_ret < 0) {fprintf(stderr, "Video capture error: %d\n", video_ret);break;} else if (loop_count <= 10) {  // 前10次循环显示详细信息printf("Video capture returned 0 (no data available)\n");}// 捕获音频数据if (loop_count % 200 == 1) {  // 每200次循环显示调试信息printf("DEBUG: About to capture audio data (loop %d)\n", loop_count);}audio_frames = audio_capture_data(&audio_ctx, audio_buffer, AUDIO_PERIOD_SIZE);if (audio_frames > 0) {// 更新实际接收的样本总数(与原始代码一致)audio_ctx.total_samples_received += audio_frames;// 累积音频样本,当达到编码器要求帧大小时自动编码// 关键修复:传递与视频相同的实时时间戳,确保音视频完全同步if (audio_capture_accumulate_and_encode(&audio_ctx, &encoder_ctx, audio_buffer, audio_frames, elapsed_us) < 0) {fprintf(stderr, "Failed to accumulate and encode audio\n");break;}} else if (audio_frames < 0) {fprintf(stderr, "Audio capture error: %d\n", audio_frames);break;} else {// 音频无数据的情况static int no_audio_count = 0;no_audio_count++;if (no_audio_count % 1000 == 0) {  // 每1000次无数据时警告printf("Warning: No audio data for %d loops (%.3fs)\n", no_audio_count, (double)elapsed_us / 1000000.0);}// 如果音频长时间无数据,尝试重新准备设备if (no_audio_count > 5000) {  // 5000次循环后重置printf("Attempting to reset audio device...\n");snd_pcm_prepare(audio_ctx.handle);no_audio_count = 0;}}// 短暂休眠以避免过度占用CPU// 如果前几次都没有数据,给摄像头更多时间启动if (loop_count < 100) {usleep(10000); // 10ms,给USB摄像头更多启动时间} else {usleep(1000);  // 1ms,正常运行时的休眠}if (loop_count <= 10) {printf("End of loop %d\n", loop_count);}}printf("Recording finished\n");// 显示最终统计信息printf("Final statistics:\n");printf("- Video frames: %ld\n", video_frame_count);printf("- Audio samples: %ld (%.3fs)\n", audio_ctx.total_samples_received,(double)audio_ctx.total_samples_received / AUDIO_SAMPLE_RATE);printf("- Expected duration: %d seconds\n", config.recording_time);// 处理剩余的音频数据if (audio_ctx.accumulated_samples > 0) {// 获取最终的实时时间戳struct timespec final_time;clock_gettime(CLOCK_MONOTONIC, &final_time);int64_t final_elapsed_us = (final_time.tv_sec - recording_start_time.tv_sec) * 1000000LL +(final_time.tv_nsec - recording_start_time.tv_nsec) / 1000LL;// 应用相同的AAC编码器延迟补偿int64_t compensated_timestamp = final_elapsed_us - AAC_ENCODER_DELAY_US;if (compensated_timestamp < 0) {compensated_timestamp = 0;}encoder_finalize_remaining_audio(&encoder_ctx, audio_ctx.accumulate_buffer, audio_ctx.accumulated_samples, compensated_timestamp);}cleanup:// 停止视频捕获video_capture_stop(&video_ctx);// 清理所有资源video_capture_cleanup(&video_ctx);audio_capture_cleanup(&audio_ctx);encoder_cleanup(&encoder_ctx);printf("=== Recording Complete ===\n");return 0;
}

audio_capture.c和.h

#include "audio_capture.h"
#include "encoder.h"  // 需要encoder_ctx结构体定义// 初始化音频捕获设备
int audio_capture_init(struct audio_capture_ctx *ctx, const char *device, int frame_size) {snd_pcm_hw_params_t *hw_params;int err;// 初始化上下文memset(ctx, 0, sizeof(*ctx));ctx->frame_size = frame_size;if ((err = snd_pcm_open(&ctx->handle, device, SND_PCM_STREAM_CAPTURE, 0)) < 0) {fprintf(stderr, "Failed to open audio device %s: %s\n", device, snd_strerror(err));return -1;}// 分配硬件参数结构if ((err = snd_pcm_hw_params_malloc(&hw_params)) < 0) {fprintf(stderr, "Failed to allocate hw params: %s\n", snd_strerror(err));audio_capture_cleanup(ctx);return -1;}// 初始化硬件参数if ((err = snd_pcm_hw_params_any(ctx->handle, hw_params)) < 0) {fprintf(stderr, "Failed to initialize hw params: %s\n", snd_strerror(err));snd_pcm_hw_params_free(hw_params);audio_capture_cleanup(ctx);return -1;}// 设置访问类型if ((err = snd_pcm_hw_params_set_access(ctx->handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {fprintf(stderr, "Failed to set access type: %s\n", snd_strerror(err));snd_pcm_hw_params_free(hw_params);audio_capture_cleanup(ctx);return -1;}// 设置采样格式if ((err = snd_pcm_hw_params_set_format(ctx->handle, hw_params, SND_PCM_FORMAT_S16_LE)) < 0) {fprintf(stderr, "Failed to set audio format: %s\n", snd_strerror(err));snd_pcm_hw_params_free(hw_params);audio_capture_cleanup(ctx);return -1;}// 设置采样率unsigned int sample_rate = AUDIO_SAMPLE_RATE;if ((err = snd_pcm_hw_params_set_rate_near(ctx->handle, hw_params, &sample_rate, 0)) < 0) {fprintf(stderr, "Failed to set sample rate: %s\n", snd_strerror(err));snd_pcm_hw_params_free(hw_params);audio_capture_cleanup(ctx);return -1;}// 设置通道数if ((err = snd_pcm_hw_params_set_channels(ctx->handle, hw_params, AUDIO_CHANNELS)) < 0) {fprintf(stderr, "Failed to set channels: %s\n", snd_strerror(err));snd_pcm_hw_params_free(hw_params);audio_capture_cleanup(ctx);return -1;}// 设置周期大小snd_pcm_uframes_t period_size = AUDIO_PERIOD_SIZE;if ((err = snd_pcm_hw_params_set_period_size_near(ctx->handle, hw_params, &period_size, 0)) < 0) {fprintf(stderr, "Failed to set period size: %s\n", snd_strerror(err));snd_pcm_hw_params_free(hw_params);audio_capture_cleanup(ctx);return -1;}// 设置周期数unsigned int periods = AUDIO_PERIODS;if ((err = snd_pcm_hw_params_set_periods_near(ctx->handle, hw_params, &periods, 0)) < 0) {fprintf(stderr, "Failed to set periods: %s\n", snd_strerror(err));snd_pcm_hw_params_free(hw_params);audio_capture_cleanup(ctx);return -1;}// 应用硬件参数if ((err = snd_pcm_hw_params(ctx->handle, hw_params)) < 0) {fprintf(stderr, "Failed to set hw params: %s\n", snd_strerror(err));snd_pcm_hw_params_free(hw_params);audio_capture_cleanup(ctx);return -1;}snd_pcm_hw_params_free(hw_params);// 准备PCMif ((err = snd_pcm_prepare(ctx->handle)) < 0) {fprintf(stderr, "Failed to prepare audio: %s\n", snd_strerror(err));audio_capture_cleanup(ctx);return -1;}// 分配累积缓冲区ctx->accumulate_buffer = (int16_t*)malloc(frame_size * sizeof(int16_t));if (!ctx->accumulate_buffer) {fprintf(stderr, "Failed to allocate audio accumulation buffer\n");audio_capture_cleanup(ctx);return -1;}ctx->accumulated_samples = 0;ctx->total_samples_received = 0;printf("Audio device initialized successfully\n");return 0;
}// 捕获音频数据
int audio_capture_data(struct audio_capture_ctx *ctx, int16_t *buffer, int buffer_size) {snd_pcm_sframes_t err;// 添加debug输出,与原始代码一致static int debug_count = 0;if (debug_count < 5) {  // 只显示前5次printf("audio_handle: %p\n", ctx->handle);}// 检查PCM状态snd_pcm_state_t state = snd_pcm_state(ctx->handle);if (state != SND_PCM_STATE_RUNNING && state != SND_PCM_STATE_PREPARED) {printf("Audio PCM state: %s, attempting recovery...\n", snd_pcm_state_name(state));if (state == SND_PCM_STATE_XRUN) {// 处理欠载/溢出if (snd_pcm_prepare(ctx->handle) < 0) {fprintf(stderr, "Failed to recover from XRUN\n");return -1;}} else if (state == SND_PCM_STATE_SUSPENDED) {// 处理设备暂停int res;while ((res = snd_pcm_resume(ctx->handle)) == -EAGAIN) {usleep(1000); // 等待1ms}if (res < 0) {if (snd_pcm_prepare(ctx->handle) < 0) {fprintf(stderr, "Failed to recover from SUSPEND\n");return -1;}}}}err = snd_pcm_readi(ctx->handle, buffer, buffer_size);if (debug_count < 5) {  // 只显示前5次printf("err: %ld\n", err);debug_count++;}if (err == -EAGAIN) {// 非阻塞模式下没有数据可用,正常情况return 0;} else if (err == -EPIPE) {// 缓冲区欠载,恢复printf("Audio underrun occurred, recovering...\n");snd_pcm_prepare(ctx->handle);return 0;} else if (err == -ESTRPIPE) {// 设备暂停,恢复printf("Audio device suspended, recovering...\n");int res;while ((res = snd_pcm_resume(ctx->handle)) == -EAGAIN) {usleep(1000);}if (res < 0) {snd_pcm_prepare(ctx->handle);}return 0;} else if (err < 0) {fprintf(stderr, "Failed to read audio: %s\n", snd_strerror(err));return -1;}// 仅每10次采集显示一次,避免输出过多if (ctx->total_samples_received % (AUDIO_PERIOD_SIZE * 10) == 0) {printf("Audio: total=%ld samples (%.3fs)\n", ctx->total_samples_received, (double)ctx->total_samples_received / AUDIO_SAMPLE_RATE);}return err; // 返回实际读取的帧数
}// 累积音频样本并在需要时编码(使用实时时间戳确保与视频同步)
int audio_capture_accumulate_and_encode(struct audio_capture_ctx *ctx, struct encoder_ctx *encoder, int16_t *audio_data, int samples, int64_t current_timestamp_us) {// 计算还需要多少样本才能填满一个编码帧int samples_needed = ctx->frame_size - ctx->accumulated_samples;int samples_to_copy = (samples < samples_needed) ? samples : samples_needed;// 将新样本拷贝到累积缓冲区memcpy(ctx->accumulate_buffer + ctx->accumulated_samples, audio_data, samples_to_copy * sizeof(int16_t));ctx->accumulated_samples += samples_to_copy;// 如果累积够了一个完整帧,进行编码if (ctx->accumulated_samples >= ctx->frame_size) {// 关键修复:补偿AAC编码器的200ms固有延迟// 通过将音频时间戳提前200ms来实现音视频同步int64_t audio_pts = current_timestamp_us - AAC_ENCODER_DELAY_US;// 如果补偿后为负,使用原始时间戳但添加警告if (audio_pts < 0) {static int warning_count = 0;if (warning_count < 3) {  // 只警告前3次printf("WARNING: Audio timestamp compensation resulted in negative value, using 0 (warning %d/3)\n", warning_count + 1);warning_count++;}audio_pts = 0;}if (encoder_encode_audio_frame(encoder, ctx->accumulate_buffer, audio_pts) < 0) {return -1;}ctx->accumulated_samples = 0;// 如果还有剩余样本,递归处理,更新时间戳避免重复int remaining_samples = samples - samples_to_copy;if (remaining_samples > 0) {// 为下一帧计算新的时间戳,基于帧大小推进时间int64_t frame_duration_us = (int64_t)ctx->frame_size * 1000000LL / AUDIO_SAMPLE_RATE;int64_t next_timestamp = current_timestamp_us + frame_duration_us;return audio_capture_accumulate_and_encode(ctx, encoder, audio_data + samples_to_copy, remaining_samples, next_timestamp);}}return 0;
}// 清理音频捕获资源
void audio_capture_cleanup(struct audio_capture_ctx *ctx) {if (ctx->accumulate_buffer) {free(ctx->accumulate_buffer);ctx->accumulate_buffer = NULL;}if (ctx->handle) {snd_pcm_close(ctx->handle);ctx->handle = NULL;}ctx->accumulated_samples = 0;ctx->total_samples_received = 0;
} 
#ifndef AUDIO_CAPTURE_H
#define AUDIO_CAPTURE_H#include "common.h"struct encoder_ctx;// 音频捕获上下文结构
struct audio_capture_ctx {snd_pcm_t *handle;int16_t *accumulate_buffer;int accumulated_samples;int64_t total_samples_received;int frame_size;  // AAC编码器要求的帧大小
};int audio_capture_init(struct audio_capture_ctx *ctx, const char *device, int frame_size);
int audio_capture_data(struct audio_capture_ctx *ctx, int16_t *buffer, int buffer_size);
int audio_capture_accumulate_and_encode(struct audio_capture_ctx *ctx, struct encoder_ctx *encoder, int16_t *audio_data, int samples, int64_t current_timestamp_us);
void audio_capture_cleanup(struct audio_capture_ctx *ctx);#endif // AUDIO_CAPTURE_H 

video_capture.c和.h

#include "video_capture.h"// 初始化视频捕获设备
int video_capture_init(struct video_capture_ctx *ctx, const char *device) {struct v4l2_format fmt;struct v4l2_requestbuffers req;struct v4l2_buffer buf;// 初始化上下文memset(ctx, 0, sizeof(*ctx));ctx->fd = -1;// 打开视频设备ctx->fd = open(device, O_RDWR | O_NONBLOCK, 0);if (ctx->fd == -1) {fprintf(stderr, "Failed to open video device %s: %s\n", device, strerror(errno));return -1;}// 设置视频格式memset(&fmt, 0, sizeof(fmt));fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;fmt.fmt.pix.width = VIDEO_WIDTH;fmt.fmt.pix.height = VIDEO_HEIGHT;fmt.fmt.pix.pixelformat = VIDEO_FORMAT;fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;if (ioctl(ctx->fd, VIDIOC_S_FMT, &fmt) == -1) {fprintf(stderr, "Failed to set video format: %s\n", strerror(errno));video_capture_cleanup(ctx);return -1;}// 请求缓冲区memset(&req, 0, sizeof(req));req.count = 4;req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;req.memory = V4L2_MEMORY_MMAP;if (ioctl(ctx->fd, VIDIOC_REQBUFS, &req) == -1) {fprintf(stderr, "Failed to request video buffers: %s\n", strerror(errno));video_capture_cleanup(ctx);return -1;}ctx->buffer_count = req.count;ctx->buffers = calloc(req.count, sizeof(*ctx->buffers));if (!ctx->buffers) {fprintf(stderr, "Out of memory\n");video_capture_cleanup(ctx);return -1;}// 映射缓冲区for (unsigned int i = 0; i < req.count; ++i) {memset(&buf, 0, sizeof(buf));buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;buf.memory = V4L2_MEMORY_MMAP;buf.index = i;if (ioctl(ctx->fd, VIDIOC_QUERYBUF, &buf) == -1) {fprintf(stderr, "Failed to query video buffer: %s\n", strerror(errno));video_capture_cleanup(ctx);return -1;}ctx->buffers[i].length = buf.length;ctx->buffers[i].start = mmap(NULL, buf.length,PROT_READ | PROT_WRITE,MAP_SHARED,ctx->fd, buf.m.offset);if (ctx->buffers[i].start == MAP_FAILED) {fprintf(stderr, "Failed to mmap video buffer: %s\n", strerror(errno));video_capture_cleanup(ctx);return -1;}}printf("Video device initialized successfully\n");// 将缓冲区入队并启动视频流(与原始代码一致)for (unsigned int i = 0; i < ctx->buffer_count; ++i) {memset(&buf, 0, sizeof(buf));buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;buf.memory = V4L2_MEMORY_MMAP;buf.index = i;if (ioctl(ctx->fd, VIDIOC_QBUF, &buf) == -1) {fprintf(stderr, "Failed to queue video buffer: %s\n", strerror(errno));video_capture_cleanup(ctx);return -1;}}// 开始视频捕获(在初始化时就启动,与原始代码一致)enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;if (ioctl(ctx->fd, VIDIOC_STREAMON, &type) == -1) {fprintf(stderr, "Failed to start video stream: %s\n", strerror(errno));video_capture_cleanup(ctx);return -1;}// 记录开始时间clock_gettime(CLOCK_MONOTONIC, &ctx->start_time);ctx->frame_count = 0;return 0;
}// 捕获视频帧
int video_capture_frame(struct video_capture_ctx *ctx, uint8_t **frame_data, size_t *frame_size, int64_t *timestamp) {struct v4l2_buffer buf;memset(&buf, 0, sizeof(buf));buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;buf.memory = V4L2_MEMORY_MMAP;if (ioctl(ctx->fd, VIDIOC_DQBUF, &buf) == -1) {if (errno == EAGAIN) {return 0; // 没有数据可用}fprintf(stderr, "Failed to dequeue video buffer: %s\n", strerror(errno));return -1;}// 首次捕获成功时显示信息if (ctx->frame_count == 0) {printf("First video frame captured! Size: %u bytes\n", buf.bytesused);}*frame_data = (uint8_t*)ctx->buffers[buf.index].start;*frame_size = buf.bytesused;// 不在这里计算时间戳,由调用者计算*timestamp = 0; // 占位,主循环会重新设置ctx->frame_count++;// 重新入队缓冲区if (ioctl(ctx->fd, VIDIOC_QBUF, &buf) == -1) {fprintf(stderr, "Failed to queue video buffer: %s\n", strerror(errno));return -1;}return 1; // 成功捕获帧
}// 停止视频捕获
void video_capture_stop(struct video_capture_ctx *ctx) {if (ctx->fd != -1) {enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;ioctl(ctx->fd, VIDIOC_STREAMOFF, &type);}
}// 清理视频捕获资源
void video_capture_cleanup(struct video_capture_ctx *ctx) {if (ctx->buffers) {for (unsigned int i = 0; i < ctx->buffer_count; ++i) {if (ctx->buffers[i].start != MAP_FAILED && ctx->buffers[i].start != NULL) {munmap(ctx->buffers[i].start, ctx->buffers[i].length);}}free(ctx->buffers);ctx->buffers = NULL;}if (ctx->fd != -1) {close(ctx->fd);ctx->fd = -1;}ctx->buffer_count = 0;
} 
#ifndef VIDEO_CAPTURE_H
#define VIDEO_CAPTURE_H#include "common.h"// 视频捕获上下文结构
struct video_capture_ctx {int fd;struct video_buffer *buffers;unsigned int buffer_count;struct timespec start_time;int64_t frame_count;
};// 函数声明
int video_capture_init(struct video_capture_ctx *ctx, const char *device);
int video_capture_frame(struct video_capture_ctx *ctx, uint8_t **frame_data, size_t *frame_size, int64_t *timestamp);
void video_capture_stop(struct video_capture_ctx *ctx);
void video_capture_cleanup(struct video_capture_ctx *ctx);#endif // VIDEO_CAPTURE_H 

encoder.c和.h

#include "encoder.h"// 内部函数:编码完整的音频帧
static int encode_audio_frame_internal(struct encoder_ctx *ctx, int16_t *input_data, int64_t pts) {int ret;// 重采样音频数据:从S16整数格式转换为FLTP浮点格式// 使用传入的input_data作为输入,避免与输出缓冲区冲突const uint8_t *in[] = {(uint8_t*)input_data};uint8_t **out = ctx->audio_frame->data;ret = swr_convert(ctx->swr_ctx, out, ctx->audio_frame->nb_samples, in, ctx->audio_frame->nb_samples);if (ret < 0) {fprintf(stderr, "Failed to resample audio (S16->FLTP): %s\n", av_err2str(ret));return -1;}ctx->audio_frame->pts = pts;// 编码帧ret = avcodec_send_frame(ctx->audio_codec_ctx, ctx->audio_frame);if (ret < 0) {fprintf(stderr, "Failed to send audio frame: %s\n", av_err2str(ret));return -1;}while (ret >= 0) {ret = avcodec_receive_packet(ctx->audio_codec_ctx, ctx->pkt);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)break;else if (ret < 0) {fprintf(stderr, "Failed to receive audio packet: %s\n", av_err2str(ret));return -1;}av_packet_rescale_ts(ctx->pkt, ctx->audio_codec_ctx->time_base, ctx->audio_stream->time_base);ctx->pkt->stream_index = ctx->audio_stream->index;ret = av_interleaved_write_frame(ctx->fmt_ctx, ctx->pkt);if (ret < 0) {fprintf(stderr, "Failed to write audio packet: %s\n", av_err2str(ret));return -1;}av_packet_unref(ctx->pkt);}return 0;
}// 初始化编码器
int encoder_init(struct encoder_ctx *ctx, const struct recording_config *config) {int ret;// 初始化上下文memset(ctx, 0, sizeof(*ctx));// 分配输出上下文ret = avformat_alloc_output_context2(&ctx->fmt_ctx, NULL, NULL, config->output_file);if (ret < 0) {fprintf(stderr, "Failed to allocate output context: %s\n", av_err2str(ret));return -1;}// 初始化视频编码器const AVCodec *video_codec = avcodec_find_encoder(AV_CODEC_ID_H264);if (!video_codec) {fprintf(stderr, "H.264 encoder not found\n");encoder_cleanup(ctx);return -1;}ctx->video_stream = avformat_new_stream(ctx->fmt_ctx, NULL);if (!ctx->video_stream) {fprintf(stderr, "Failed to create video stream\n");encoder_cleanup(ctx);return -1;}ctx->video_codec_ctx = avcodec_alloc_context3(video_codec);if (!ctx->video_codec_ctx) {fprintf(stderr, "Failed to allocate video codec context\n");encoder_cleanup(ctx);return -1;}ctx->video_codec_ctx->bit_rate = config->video_bitrate;ctx->video_codec_ctx->width = config->video_width;ctx->video_codec_ctx->height = config->video_height;ctx->video_codec_ctx->time_base = (AVRational){1, AV_TIME_BASE};ctx->video_codec_ctx->framerate = (AVRational){config->video_fps, 1};ctx->video_codec_ctx->gop_size = 12;ctx->video_codec_ctx->max_b_frames = 0;  // 去掉B帧,避免编码延迟ctx->video_codec_ctx->pix_fmt = AV_PIX_FMT_NV12;if (ctx->fmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)ctx->video_codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;ret = avcodec_open2(ctx->video_codec_ctx, video_codec, NULL);if (ret < 0) {fprintf(stderr, "Failed to open video codec: %s\n", av_err2str(ret));encoder_cleanup(ctx);return -1;}ret = avcodec_parameters_from_context(ctx->video_stream->codecpar, ctx->video_codec_ctx);if (ret < 0) {fprintf(stderr, "Failed to copy video codec parameters: %s\n", av_err2str(ret));encoder_cleanup(ctx);return -1;}ctx->video_stream->time_base = ctx->video_codec_ctx->time_base;// 初始化音频编码器const AVCodec *audio_codec = avcodec_find_encoder(AV_CODEC_ID_AAC);if (!audio_codec) {fprintf(stderr, "AAC encoder not found\n");encoder_cleanup(ctx);return -1;}ctx->audio_stream = avformat_new_stream(ctx->fmt_ctx, NULL);if (!ctx->audio_stream) {fprintf(stderr, "Failed to create audio stream\n");encoder_cleanup(ctx);return -1;}ctx->audio_codec_ctx = avcodec_alloc_context3(audio_codec);if (!ctx->audio_codec_ctx) {fprintf(stderr, "Failed to allocate audio codec context\n");encoder_cleanup(ctx);return -1;}ctx->audio_codec_ctx->bit_rate = config->audio_bitrate;// AAC编码器要求浮点数格式(FLTP = Float, Planar)// 这能提供更好的音质和动态范围ctx->audio_codec_ctx->sample_fmt = AV_SAMPLE_FMT_FLTP;ctx->audio_codec_ctx->sample_rate = config->audio_sample_rate;ctx->audio_codec_ctx->channels = config->audio_channels;ctx->audio_codec_ctx->channel_layout = AV_CH_LAYOUT_MONO;// 设置音频编码器时间基与AV_TIME_BASE统一ctx->audio_codec_ctx->time_base = (AVRational){1, AV_TIME_BASE};if (ctx->fmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)ctx->audio_codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;ret = avcodec_open2(ctx->audio_codec_ctx, audio_codec, NULL);if (ret < 0) {fprintf(stderr, "Failed to open audio codec: %s\n", av_err2str(ret));encoder_cleanup(ctx);return -1;}ret = avcodec_parameters_from_context(ctx->audio_stream->codecpar, ctx->audio_codec_ctx);if (ret < 0) {fprintf(stderr, "Failed to copy audio codec parameters: %s\n", av_err2str(ret));encoder_cleanup(ctx);return -1;}ctx->audio_stream->time_base = ctx->audio_codec_ctx->time_base;ctx->swr_ctx = swr_alloc_set_opts(NULL,ctx->audio_codec_ctx->channel_layout, ctx->audio_codec_ctx->sample_fmt, ctx->audio_codec_ctx->sample_rate,AV_CH_LAYOUT_MONO, AV_SAMPLE_FMT_S16, AUDIO_SAMPLE_RATE,0, NULL);if (!ctx->swr_ctx) {fprintf(stderr, "Failed to allocate audio resampler\n");encoder_cleanup(ctx);return -1;}ret = swr_init(ctx->swr_ctx);if (ret < 0) {fprintf(stderr, "Failed to initialize audio resampler: %s\n", av_err2str(ret));encoder_cleanup(ctx);return -1;}// 分配帧和包ctx->video_frame = av_frame_alloc();ctx->audio_frame = av_frame_alloc();ctx->pkt = av_packet_alloc();if (!ctx->video_frame || !ctx->audio_frame || !ctx->pkt) {fprintf(stderr, "Failed to allocate frames/packet\n");encoder_cleanup(ctx);return -1;}ctx->video_frame->format = ctx->video_codec_ctx->pix_fmt;ctx->video_frame->width = ctx->video_codec_ctx->width;ctx->video_frame->height = ctx->video_codec_ctx->height;ret = av_frame_get_buffer(ctx->video_frame, 32);if (ret < 0) {fprintf(stderr, "Failed to allocate video frame buffer: %s\n", av_err2str(ret));encoder_cleanup(ctx);return -1;}ctx->audio_frame->format = ctx->audio_codec_ctx->sample_fmt;ctx->audio_frame->channels = ctx->audio_codec_ctx->channels;ctx->audio_frame->channel_layout = ctx->audio_codec_ctx->channel_layout;ctx->audio_frame->sample_rate = ctx->audio_codec_ctx->sample_rate;ctx->audio_frame->nb_samples = ctx->audio_codec_ctx->frame_size;ret = av_frame_get_buffer(ctx->audio_frame, 0);if (ret < 0) {fprintf(stderr, "Failed to allocate audio frame buffer: %s\n", av_err2str(ret));encoder_cleanup(ctx);return -1;}// 打开输出文件if (!(ctx->fmt_ctx->oformat->flags & AVFMT_NOFILE)) {ret = avio_open(&ctx->fmt_ctx->pb, config->output_file, AVIO_FLAG_WRITE);if (ret < 0) {fprintf(stderr, "Failed to open output file: %s\n", av_err2str(ret));encoder_cleanup(ctx);return -1;}}// 写入文件头ret = avformat_write_header(ctx->fmt_ctx, NULL);if (ret < 0) {fprintf(stderr, "Failed to write header: %s\n", av_err2str(ret));encoder_cleanup(ctx);return -1;}printf("FFmpeg encoders initialized successfully\n");printf("Audio frame size: %d samples (accumulating from %d sample periods)\n", ctx->audio_codec_ctx->frame_size, AUDIO_PERIOD_SIZE);return 0;
}// 编码视频帧
int encoder_encode_video_frame(struct encoder_ctx *ctx, uint8_t *frame_data, int64_t timestamp) {int ret;static int frame_count = 0;frame_count++;if (frame_count % 25 == 1) {  // 每25帧显示一次,减少输出printf("Encoding video frame %d, timestamp: %ld\n", frame_count, timestamp);}// NV12格式:Y平面 + UV交错平面memcpy(ctx->video_frame->data[0], frame_data, VIDEO_WIDTH * VIDEO_HEIGHT);memcpy(ctx->video_frame->data[1], frame_data + VIDEO_WIDTH * VIDEO_HEIGHT, VIDEO_WIDTH * VIDEO_HEIGHT / 2);ctx->video_frame->pts = timestamp;// 编码帧ret = avcodec_send_frame(ctx->video_codec_ctx, ctx->video_frame);if (ret < 0) {fprintf(stderr, "Failed to send video frame: %s\n", av_err2str(ret));return -1;}while (ret >= 0) {ret = avcodec_receive_packet(ctx->video_codec_ctx, ctx->pkt);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {break;} else if (ret < 0) {fprintf(stderr, "Failed to receive video packet: %s\n", av_err2str(ret));return -1;}// 直接使用帧的时间戳设置包的时间戳(修复INT64_MIN问题)if (ctx->pkt->pts == AV_NOPTS_VALUE) {ctx->pkt->pts = ctx->video_frame->pts;}if (ctx->pkt->dts == AV_NOPTS_VALUE) {ctx->pkt->dts = ctx->video_frame->pts;}av_packet_rescale_ts(ctx->pkt, ctx->video_codec_ctx->time_base, ctx->video_stream->time_base);ctx->pkt->stream_index = ctx->video_stream->index;// 添加写入前的调试信息if (frame_count % 100 == 1) {printf("DEBUG: About to write video packet (size=%d)\n", ctx->pkt->size);}ret = av_interleaved_write_frame(ctx->fmt_ctx, ctx->pkt);if (ret < 0) {fprintf(stderr, "Failed to write video packet: %s\n", av_err2str(ret));return -1;}if (frame_count % 100 == 1) {printf("DEBUG: Video packet written successfully\n");}// 只有当包有实际数据时才输出调试信息if (frame_count % 25 == 1 && ctx->pkt->size > 0) {printf("Video packet written: size=%d, pts=%ld, dts=%ld\n", ctx->pkt->size, ctx->pkt->pts, ctx->pkt->dts);}av_packet_unref(ctx->pkt);}return 0;
}// 编码音频帧
int encoder_encode_audio_frame(struct encoder_ctx *ctx, int16_t *audio_data, int64_t timestamp) {// 直接使用传入的audio_data,无需拷贝到audio_frame中return encode_audio_frame_internal(ctx, audio_data, timestamp);
}// 处理剩余的音频数据
int encoder_finalize_remaining_audio(struct encoder_ctx *ctx, int16_t *partial_frame, int samples, int64_t timestamp) {if (samples > 0) {printf("Encoding remaining %d audio samples\n", samples);// 创建临时缓冲区,填充剩余部分为静音int16_t *temp_buffer = malloc(ctx->audio_codec_ctx->frame_size * sizeof(int16_t));if (!temp_buffer) {fprintf(stderr, "Failed to allocate temporary audio buffer\n");return -1;}int remaining_samples = ctx->audio_codec_ctx->frame_size - samples;memcpy(temp_buffer, partial_frame, samples * sizeof(int16_t));memset(temp_buffer + samples, 0, remaining_samples * sizeof(int16_t));int ret = encode_audio_frame_internal(ctx, temp_buffer, timestamp);free(temp_buffer);return ret;}return 0;
}// 清理编码器资源
void encoder_cleanup(struct encoder_ctx *ctx) {// 刷新视频编码器 - 发送NULL帧获取所有待编码的帧if (ctx->video_codec_ctx && ctx->pkt) {printf("Flushing video encoder...\n");avcodec_send_frame(ctx->video_codec_ctx, NULL);int ret;int flushed_count = 0;while ((ret = avcodec_receive_packet(ctx->video_codec_ctx, ctx->pkt)) >= 0) {if (ctx->fmt_ctx) {av_packet_rescale_ts(ctx->pkt, ctx->video_codec_ctx->time_base, ctx->video_stream->time_base);ctx->pkt->stream_index = ctx->video_stream->index;av_interleaved_write_frame(ctx->fmt_ctx, ctx->pkt);// 只显示有效包的信息if (ctx->pkt->size > 0) {printf("Flushed video packet: size=%d, pts=%ld\n", ctx->pkt->size, ctx->pkt->pts);flushed_count++;}}av_packet_unref(ctx->pkt);}printf("Video encoder flushed %d packets\n", flushed_count);}// 刷新音频编码器if (ctx->audio_codec_ctx && ctx->pkt) {printf("Flushing audio encoder...\n"); avcodec_send_frame(ctx->audio_codec_ctx, NULL);int ret;int flushed_count = 0;while ((ret = avcodec_receive_packet(ctx->audio_codec_ctx, ctx->pkt)) >= 0) {if (ctx->fmt_ctx) {av_packet_rescale_ts(ctx->pkt, ctx->audio_codec_ctx->time_base, ctx->audio_stream->time_base);ctx->pkt->stream_index = ctx->audio_stream->index;// 添加音频写入调试信息static int audio_packet_count = 0;audio_packet_count++;if (audio_packet_count % 50 == 1) {printf("DEBUG: About to write audio packet (size=%d)\n", ctx->pkt->size);}ret = av_interleaved_write_frame(ctx->fmt_ctx, ctx->pkt);if (ret < 0) {fprintf(stderr, "Failed to write audio packet: %s\n", av_err2str(ret));break;}if (audio_packet_count % 50 == 1) {printf("DEBUG: Audio packet written successfully\n");}// 只显示有效包的信息if (ctx->pkt->size > 0) {printf("Flushed audio packet: size=%d, pts=%ld\n", ctx->pkt->size, ctx->pkt->pts);flushed_count++;}}av_packet_unref(ctx->pkt);}printf("Audio encoder flushed %d packets\n", flushed_count);}// 写入文件尾if (ctx->fmt_ctx) {av_write_trailer(ctx->fmt_ctx);}// 清理FFmpeg资源if (ctx->video_codec_ctx) {avcodec_free_context(&ctx->video_codec_ctx);}if (ctx->audio_codec_ctx) {avcodec_free_context(&ctx->audio_codec_ctx);}if (ctx->fmt_ctx) {if (!(ctx->fmt_ctx->oformat->flags & AVFMT_NOFILE))avio_closep(&ctx->fmt_ctx->pb);avformat_free_context(ctx->fmt_ctx);}if (ctx->swr_ctx) {swr_free(&ctx->swr_ctx);}if (ctx->video_frame) {av_frame_free(&ctx->video_frame);}if (ctx->audio_frame) {av_frame_free(&ctx->audio_frame);}if (ctx->pkt) {av_packet_free(&ctx->pkt);}// 重置所有指针memset(ctx, 0, sizeof(*ctx));
} 
#ifndef ENCODER_H
#define ENCODER_H#include "common.h"// 编码器上下文结构
struct encoder_ctx {// FFmpeg相关变量AVFormatContext *fmt_ctx;AVCodecContext *video_codec_ctx;AVCodecContext *audio_codec_ctx;AVStream *video_stream;AVStream *audio_stream;struct SwsContext *sws_ctx;  // 保留为NULL,因为直接使用NV12SwrContext *swr_ctx;AVFrame *video_frame;AVFrame *audio_frame;AVPacket *pkt;
};// 函数声明
int encoder_init(struct encoder_ctx *ctx, const struct recording_config *config);
int encoder_encode_video_frame(struct encoder_ctx *ctx, uint8_t *frame_data, int64_t timestamp);
int encoder_encode_audio_frame(struct encoder_ctx *ctx, int16_t *audio_data, int64_t timestamp);
int encoder_finalize_remaining_audio(struct encoder_ctx *ctx, int16_t *partial_frame, int samples, int64_t timestamp);
void encoder_cleanup(struct encoder_ctx *ctx);#endif // ENCODER_H 

common.h

#ifndef COMMON_H
#define COMMON_H// 定义功能特性宏
#define _GNU_SOURCE
#define _POSIX_C_SOURCE 200809L// 标准C库
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <time.h>
#include <sys/mman.h>
#include <sys/ioctl.h>// FFmpeg头文件(在其他系统头文件之前包含)
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/opt.h>
#include <libavutil/imgutils.h>
#include <libswresample/swresample.h>// V4L2和ALSA头文件(在FFmpeg之后包含)
#include <linux/videodev2.h>
#include <alsa/asoundlib.h>// 视频参数
#define VIDEO_DEVICE "/dev/video0"
#define VIDEO_WIDTH 640
#define VIDEO_HEIGHT 480
#define VIDEO_FPS 25
#define VIDEO_FORMAT V4L2_PIX_FMT_NV12// 音频参数
#define AUDIO_DEVICE "plughw:1,0"
#define AUDIO_CHANNELS 1
#define AUDIO_SAMPLE_RATE 22050
#define AUDIO_PERIOD_SIZE 256  // 减少到256以降低延迟 (约11.6ms @ 22050Hz)
#define AUDIO_PERIODS 5// 编码参数
#define OUTPUT_FILE "output.mp4"
#define VIDEO_BITRATE 10000000
#define AUDIO_BITRATE 64000// 音视频同步相关配置
#define AAC_ENCODER_DELAY_US 200000  // AAC编码器固有延迟(微秒)
#define SYNC_DEBUG_ENABLED 1         // 启用同步调试输出
#define MAX_TIMESTAMP_DRIFT_US 50000 // 最大允许时间戳漂移(50ms)// 视频缓冲区结构
struct video_buffer {void *start;size_t length;
};// 录制配置结构
struct recording_config {int recording_time;char output_file[256];int video_width;int video_height;int video_fps;int audio_sample_rate;int audio_channels;int video_bitrate;int audio_bitrate;
};// 错误字符串转换函数(仅在未定义时定义)
#ifndef av_err2str
static char error_buffer[AV_ERROR_MAX_STRING_SIZE];
static const char* av_err2str_func(int errnum) {av_strerror(errnum, error_buffer, AV_ERROR_MAX_STRING_SIZE);return error_buffer;
}
#define av_err2str(e) av_err2str_func(e)
#endif// 初始化默认配置
static inline void init_default_config(struct recording_config *config) {config->recording_time = 30;strcpy(config->output_file, OUTPUT_FILE);config->video_width = VIDEO_WIDTH;config->video_height = VIDEO_HEIGHT;config->video_fps = VIDEO_FPS;config->audio_sample_rate = AUDIO_SAMPLE_RATE;config->audio_channels = AUDIO_CHANNELS;config->video_bitrate = VIDEO_BITRATE;config->audio_bitrate = AUDIO_BITRATE;
}#endif // COMMON_H 

Makefile:

# USB Camera Recorder Makefile (Modular Version)# 编译器设置
CC = gcc
CFLAGS = -Wall -Wextra -std=c99 -O2# 程序名称
PROGRAM = usb_camera_recorder# 源文件
SOURCES = main.c video_capture.c audio_capture.c encoder.c# 目标文件
OBJECTS = $(SOURCES:.c=.o)# 头文件
HEADERS = common.h video_capture.h audio_capture.h encoder.h# 库文件链接
LIBS = -lavcodec -lavformat -lavutil -lswresample -lasound -lm# PKG-CONFIG包
PKGS = libavcodec libavformat libavutil libswresample alsa# 使用pkg-config获取编译和链接参数
CFLAGS += $(shell pkg-config --cflags $(PKGS))
LDFLAGS = $(shell pkg-config --libs $(PKGS))# 默认目标
all: $(PROGRAM)# 编译主程序
$(PROGRAM): $(OBJECTS)$(CC) $(OBJECTS) -o $(PROGRAM) $(LDFLAGS) $(LIBS)# 编译源文件(加入头文件依赖)
%.o: %.c $(HEADERS)$(CC) $(CFLAGS) -c $< -o $@# 特定模块的依赖关系
main.o: main.c common.h video_capture.h audio_capture.h encoder.h
video_capture.o: video_capture.c video_capture.h common.h
audio_capture.o: audio_capture.c audio_capture.h common.h
encoder.o: encoder.c encoder.h common.h# 清理编译文件
clean:rm -f $(OBJECTS) $(PROGRAM)# 深度清理(包括备份文件)
distclean: cleanrm -f *~ *.bak *.mp4# 检查依赖
check-deps:@echo "Checking dependencies..."@pkg-config --exists $(PKGS) && echo "All dependencies found" || echo "Missing dependencies"@which $(CC) > /dev/null && echo "Compiler found: $(CC)" || echo "Compiler not found: $(CC)"# 显示项目结构
info:@echo "=== USB Camera Recorder (Modular Version) ==="@echo "Source files:"@for src in $(SOURCES); do echo "  $$src"; done@echo "Header files:"@for hdr in $(HEADERS); do echo "  $$hdr"; done@echo "Program: $(PROGRAM)"@echo# 伪目标声明
.PHONY: all clean distclean check-deps info 
2、音视频同步
1)PTS是什么?

        是英文Presentation Time Stamp首字母,直译为显示时间戳,比如视频的帧率为25fps,平均40ms产生一帧数据。

        以下是针对 ​25fps 视频​(帧间隔=40ms)的前5帧PTS与显示帧数的对照表:

帧序列物理呈现时间
第0帧0ms
第1帧40ms
第2帧80ms
第3帧120ms
第4帧160ms

表1

        当播放器运行到第0秒的时候,开始显示第0帧。运行到第40ms的时候,显示第1帧。以此类推,直到将所有的帧显示完毕。“物理呈现时间”就是显示时间戳,也就是PTS。

2)增加音频后,播放器应该如何显示?

        假如音频的采样率为22050,音频的帧率为25,以下是0~160ms时间轴的事件:

事件发生时间(ms)事件类型事件序号PTS(微秒)备注(实际对应的时间)
0视频帧00第0帧
0音频包00第0个音频包
40视频帧140000第1帧
46.44音频包146440第1个音频包(起始)
80视频帧280000第2帧
92.88音频包292880第2个音频包
120视频帧3120000第3帧
139.32音频包3139320第3个音频包
160视频帧4160000第4帧

表2

        视频的播放这里就不再赘述,音频与视频的也是类似的,在92.88ms时,播放第2个音频包,这个音频包可以持续播放46.44ms,直到139.32ms到来后,播放第3个音频包。

3)音视频的PTS计算

        音频和视频都有独立的PTS,我们记为pts_a和pts_v。依照上面的表格。

        第0帧视频数据产生后,pts_v = 0(us),第1帧产生时,pts_v = 40000(us)。

        第0包音频包产生后,pts_a = 0(us)。第1包产生时,pts_a = 46440(us)。

        以此类推。

4)如果不做音视频同步会怎样?

        依照前面三个小节的描述,pts就使用以下计算方式:

pts_v = 帧数 * (1 / 帧率)
pts_a = (包数 * 包周期) * (1 / 采样率)

        这样的计算方式有问题吗?

        回答:在短时间内是没什么问题的(比如几十秒之内),但是录制时间久了会出现问题(比如一个小时)。

        为什么录制时间久了会出现音视频不同步?

        回答:第一、音频和视频的时钟都是独立的,又固定和波动偏差。第二、音频的计算有很多是除不尽的,导致数学计算后必须四舍五入。因为这两点原因,录制时间久了之后,会产生一个让人头大的问题:累积误差

        举个例子:假定录制一个小时,音频由于系统性误差为+10us,再加上除不尽的问题,那么1个小时后,累积误差将达到+0.8s左右,也就是音频比画面慢了将近1秒!基本上这个视频就没办法看了。(这个举例是很客气了,实际上音视频的累计误差比这个糟糕多了)

        如果不做音视频同步会怎样?

        回答:由于累计误差的问题,录制时间久了,会出现严重的声音画面不同步!

5)解决方案

        其实核心思想很简单,就是加入一个第三方时钟,本次代码中采用的是单调递增时钟CLOCK_MONOTONIC作为时间基。每次产生一帧图像数据或者一包音频数据,将CLOCK_MONOTONIC生成的时间戳作为PTS写入即可。

        示意图如下:

        另外还有将音频时钟作为时间基,这种方式更加适合长时间的录制,此方式大家自己研究。

6)代码说明

        该工程的代码是借助于前几章的代码合并而成的,无论是音频编码还是视频编码,都是较为简单的,无非就是初始化、采集数据、编码、写文件。但是如果要将音视频合并在一起,就需要实现“音视频同步”,该代码中主要针对音频和视频的同步做了修改,采用CLOCK_MONOTONIC作为了PTS的时钟源。

        这里面有两个概念有可能需要补充说明一下,那就是AV_TIME_BASE和CLOCK_MONOTONIC:

  • AV_TIME_BASE: 只是“单位标尺”(1秒=1,000,000),表示“用微秒记时”的时间基,不是时钟。

  • CLOCK_MONOTONIC: 是“时钟源”,返回单调递增的当前时间。

        二者关系:没有直接绑定。工程里常用“用CLOCK_MONOTONIC取时间”,再“用AV_TIME_BASE把它表达为微秒”,最后按各流的time_base做换算。

总结

        音频和视频编码在之前的章节实现过,本章节将其合二为一,但是并非简单的合并,而是需要考虑“音视频同步”,否则会造成画面和声音不同步的现象。

        本章节使用AV_TIME_BASE作为时间基,将CLOCK_MONOTONIC作为时钟源,并且对音频做了一个固定时间偏执。笔者亲测该代码运行一个小时,音视频依然保持同步。

        下一章节我们进入最终章的下半部分--音视频直播。

http://www.dtcms.com/a/321531.html

相关文章:

  • 《MySQL索引底层原理:B+树、覆盖索引与最左前缀法则》
  • TF 上架全流程实战,从构建到 TestFlight 分发
  • iOS 签名证书全流程详解,申请、管理与上架实战
  • 飞算JavaAI深度剖析:开启Java开发智能新时代
  • 路由器不能上网的解决过程
  • 综合实验作业
  • Web Worker 性能革命:让浏览器多线程为您的应用加速
  • OpenAI 开源 GPT-OSS:1200亿参数推理模型上线,完全免费、商用可用,全民可控智能体时代正式开启!
  • 异步改变Promise状态与then调用顺序
  • 零基础深度学习规划路线:从数学公式到AI大模型的系统进阶指南
  • 【完整源码+数据集+部署教程】植物病害检测系统源码和数据集:改进yolo11-MultiSEAMHead
  • SpringBoot的profile加载
  • Cesium 模型3dtiles 开挖 挖洞 压平
  • 单层 PDF 与双层 PDF:一字之差,功能大不同
  • 如何高效使用Cursor?要节省者用?
  • 【代码随想录day 14】 力扣 104.二叉树的最大深度
  • 机器学习及其KNN算法
  • 静态路由主备切换
  • 力扣-189.轮转数组
  • MetaBit基金会加码投资图灵协议,深化去中心化金融与元宇宙生态合作
  • mysql复制连接下的所有表+一次性拷贝到自己的库
  • 本地开发penpot源码支持AI原型设计(一)
  • node.js 学习笔记2 进程/线程、fs
  • PCB焊盘脱落的补救办法与猎板制造优势解析
  • 活到老学到老之使用jenv管理多个java版本
  • 微型导轨在半导体制造中有哪些高精密应用场景?
  • 【AI工具】解放双手,操控浏览器的工具对比,来了
  • 基于深度学习的nlp
  • ctfshow_萌新web9-web13-----rce
  • Java面试初中级:线程池的主要参数有哪些?