qt+gxdi+ffmpeg远程控制(九)

好久没更新了,在学校比较忙,大家见谅。

本篇介绍根据wasapi捕获音频数据,将捕获到的数据传给ffmpeg编码为aac,与前文捕获屏幕并编码后的h264数据合并存储为mp4文件。

qt+gxdi+ffmpeg远程控制(八)

wasapi

wasapi是windows给出的音频api,详细就不再介绍了,有兴趣可以自行百度。这里注意在使用前要在同线程调用CoInitialize(),但与ffmpeg有些冲突,在初始化h264编码器时必须执行CoUninitialize(),否则avcodec_open2打开编码器失败,具体原因不明。

枚举音频设备

IMMDeviceEnumerator* enumerator = nullptr;
IMMDeviceCollection* device_collection = nullptr;

HRESULT result = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr,
CLSCTX_ALL, IID_PPV_ARGS(&enumerator));

enumerator->EnumAudioEndpoints(eAll, eConsole | eMultimedia, &device_collection);

以上是关键代码,CoCreateInstance创建出枚举对象enumerator,随后调用enumerator对象的EnumAudioEndpoints生成满足指定条件的音频设备对象。

HRESULT EnumAudioEndpoints(
  [in]  EDataFlow           dataFlow,
  [in]  DWORD               dwStateMask,
  [out] IMMDeviceCollection **ppDevices
);

 其中dataFlow表示数据流方向,即输入数据流/输出数据流,对应的也就是扬声器与麦克风。

dwStateMask参数设定与windowsApi给出的不一样,网上查找的结果与obs源码中都是这么写的,采用的是ERole枚举。

eConsole    与计算机的交互    游戏和系统通知    语音命令

eCommunications    与其他人的语音通信    聊天和 VoIP    聊天和 VoIP
eMultimedia    播放或录制音频内容    音乐和电影    旁白和实时音乐录制

 最后,函数将结果存放到pDevices指针中,这里会自动分配内存。

可以用IPropertyStore对象获取音频设备的详细信息,代码中用来获取设备名称。

Microsoft::WRL::ComPtr<IPropertyStore> property_store;
PROPVARIANT name_var;
temp = new AudioCapture();
device_collection->Item(i, &temp->m_device);
temp->m_device->GetId(&device_id);
temp->m_device->OpenPropertyStore(STGM_READ, property_store.GetAddressOf());
PropVariantInit(&name_var);
property_store->GetValue(PKEY_Device_FriendlyName, &name_var);

其中GetValue的第一个参数用来标识获取的属性名称,具体其他属性参考windows api

初始化设备

以扬声器为例,捕获音频需要IAudioCaptureClient对象,获取步骤为:IMMDevice\rightarrowIAudioClient\rightarrowIAudioCaptureClient。

	HRESULT hr = S_OK;
	hr = this->m_device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, NULL, reinterpret_cast<void**>(&this->m_audio_client));

	WAVEFORMATEX* mic_format = nullptr;
	// 获取音频引擎内如共享模式程序的音频流格式
	hr = this->m_audio_client->GetMixFormat(&mic_format);
	PWAVEFORMATEXTENSIBLE pEx = NULL;
	switch (mic_format->wFormatTag) {
	case WAVE_FORMAT_IEEE_FLOAT:
		break;
	case WAVE_FORMAT_EXTENSIBLE:
		pEx = reinterpret_cast<PWAVEFORMATEXTENSIBLE>(mic_format);
		if (IsEqualGUID(KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, pEx->SubFormat)) {
			pEx->SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
			pEx->Samples.wValidBitsPerSample = 16;

			mic_format->wBitsPerSample = 16;
			mic_format->nBlockAlign = mic_format->nChannels * mic_format->wBitsPerSample / 8;
			mic_format->nAvgBytesPerSec = mic_format->nBlockAlign * mic_format->nSamplesPerSec;
		}
		else {
			CoTaskMemFree(mic_format);
			return;
		}
		break;
	default:
		break;
	}
	this->m_sample_rate = mic_format->nSamplesPerSec;
	this->m_sample_size = (mic_format->wBitsPerSample / 8) * mic_format->nChannels;
	/*
	*	AUDCLNT_STREAMFLAGS_EVENTCALLBACK允许设置事件通知回调 SetEventHandle才会有效果
	*/
	hr = this->m_audio_client->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_LOOPBACK, BUFFER_TIME_100NS, 0, mic_format, NULL);
	this->m_audio_samples_ready_event = CreateEvent(NULL, FALSE, FALSE, NULL);

	//设置事件通知对象
	hr = this->m_audio_client->SetEventHandle(this->m_audio_samples_ready_event);

	//生成采集服务
	hr = this->m_audio_client->GetService(__uuidof(IAudioCaptureClient), (void**)&this->m_capture_client);

其中device由上一步枚举音频设备时获取,这里关键的是对mic_format的处理,指定捕获音频为pcm,类型为AV_SAMPLE_FMT_S16,写法参考window api

之后用Initialize方法对IAudioClient初始化,其中第二个参数添加AUDCLNT_STREAMFLAGS_EVENTCALLBACK,用来允许设置事件通知对象,该事件通知对象用来判断扬声器是否有声音输出。

音频采集

开始采集

调用上一步获取的IAudioCaptureClient对象中Start方法开始捕获音频,之后用GetBuffer获取捕获的音频。GetBuffer需要主动调用,因此单开线程调用方法,获取音频数据。

/开始采集
this->m_mutex = new std::shared_mutex();
HRESULT hr = this->m_audio_client->Start();
this->m_alive = true;
this->m_capture_thread = new std::thread(&AudioCapture::capture, this);

 采集线程

以下是线程执行的函数,当有事件通知时即扬声器有输出时才继续执行。

    HRESULT hr;
	byte* data = nullptr;
	uint32_t sample_count = 0, packet_length = 0;
	DWORD flag;
	HANDLE wait_array[3];
	wait_array[0] = this->m_audio_samples_ready_event;
	uint64_t ts = 0;
	while (this->m_alive) {
		DWORD waitResult = WaitForMultipleObjects(1, wait_array, FALSE, INFINITE);
		switch (waitResult)
		{
		case WAIT_OBJECT_0 + 0:
			hr = this->m_capture_client->GetBuffer(&data, &sample_count, &flag, NULL, &ts);
			if (flag & AUDCLNT_BUFFERFLAGS_SILENT)
			{
				// printf("AUDCLNT_BUFFERFLAGS_SILENT \n");
			}

			if (flag & AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY)
			{
				//printf("%06d # AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY \n", nCnt);
			}

			if (flag & AUDCLNT_BUFFERFLAGS_TIMESTAMP_ERROR)
			{
				//printf("%06d # AUDCLNT_BUFFERFLAGS_TIMESTAMP_ERROR \n", nCnt);
			}

			if (this->captureCallback != nullptr) {
				this->captureCallback(data, sample_count, this->m_sample_size);
			}

			hr = this->m_capture_client->ReleaseBuffer(sample_count);

			if (hr != S_OK) {
				// printf("release error");
			}
			hr = this->m_capture_client->GetNextPacketSize(&packet_length);
			if (hr != S_OK) {
				// printf("GetNextPacketSize error");
			}
			break;
		default:
			break;
		}
	}

GetBuffer方法用来获取音频数据,api定义如下

HRESULT GetBuffer(
  [out] BYTE   **ppData,
  [out] UINT32 *pNumFramesToRead,
  [out] DWORD  *pdwFlags,
  [out] UINT64 *pu64DevicePosition,
  [out] UINT64 *pu64QPCPosition
);

 ppData用来获取数据地址

pNumFramesToRead获取捕获的帧数,实测在我的电脑上是480,注意这里是帧计数,实际大小需要乘帧长度,即上一步中m_sample_size,以AV_SAMPLE_FMT_S16为例,计算得出m_sample_size为4,也就是说获取的帧数为480,数据大小为480*4=1920。

pdwFlags缓冲区状态,枚举如下

AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY
数据包中的数据与上一个数据包的设备位置不相关;这可能是由于流状态转换或计时故障造成的。
AUDCLNT_BUFFERFLAGS_SILENT
将数据包中的所有数据视为静音,并忽略实际数据值。 有关使用此标志的详细信息,请参阅呈现Stream和捕获Stream。
AUDCLNT_BUFFERFLAGS_TIMESTAMP_ERROR
设备流位置的记录时间不确定。 因此,客户端可能无法准确设置当前数据包的时间戳。

 pu64DevicePosition设备位置表示为从流开始的音频帧数,不需要,设置为NULL。

pu64QPCPosition性能计数器值,目前没有用到。

随后采用回调方式送入编码器,这里注意每次成功GetBuffer后要ReleaseBuffer。

ffmpeg编码为aac

初始化编码器

	this->m_audio_encode_codec = const_cast<AVCodec*>(avcodec_find_encoder(AV_CODEC_ID_AAC));
	if (this->m_audio_encode_codec == nullptr) {
		// TODO log

		return false;
	}
	this->m_audio_encode_codec_ctx = avcodec_alloc_context3(this->m_audio_encode_codec);
	this->m_audio_encode_codec_ctx->sample_rate = _sample_rate;
	this->m_audio_encode_codec_ctx->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL;
	this->m_audio_encode_codec_ctx->sample_fmt = AV_SAMPLE_FMT_FLTP;
	this->m_audio_encode_codec_ctx->bit_rate = _bit_rate;
	this->m_audio_encode_codec_ctx->ch_layout = AV_CHANNEL_LAYOUT_STEREO;
	if (avcodec_open2(this->m_audio_encode_codec_ctx, this->m_audio_encode_codec, NULL) != 0) {
		// TODO log

		return false;
	}

其中,AV_CODEC_ID_AAC表示获取aac编码器。

sample_rate表示采样率,这里为了方便就与音频捕获的采样率相同,在我电脑上为48000。

sample_fmt为数据格式,这里aac编码器需要AV_SAMPLE_FMT_FLTP格式,没有测试是否支持其他格式。可以用如下代码测试:

    const AVPixelFormat* pfmt = this->m_audio_encode_codec->pix_fmts;
	while (pfmt != NULL && *pfmt != AV_PIX_FMT_NONE) {
		const char* name = av_get_pix_fmt_name(*pfmt);
		std::cout << pfmt << ": " << name << std::endl;
		pfmt++;
	}

bit_rate对应obs设置中音频比特率。

重点,需要将strict_std_compliance设置为FF_COMPLIANCE_EXPERIMENTAL,因为ffmpeg音频编码器对aac是默认关闭的,设置以方便打开编码器。

初始化frame与packet

    this->m_audio_encode_frame = av_frame_alloc();
	this->m_audio_encode_frame->pts = 0;
	this->m_audio_encode_frame->nb_samples = this->m_audio_encode_codec_ctx->frame_size;
	this->m_audio_encode_frame->format = this->m_audio_encode_codec_ctx->sample_fmt;
	this->m_audio_encode_frame->ch_layout = this->m_audio_encode_codec_ctx->ch_layout;
	this->m_audio_encode_frame->sample_rate = this->m_audio_encode_codec_ctx->sample_rate;
	auto _buff_size = av_samples_get_buffer_size(NULL, this->m_audio_encode_frame->ch_layout.nb_channels, this->m_audio_encode_codec_ctx->frame_size, this->m_audio_encode_codec_ctx->sample_fmt, 1);
	this->m_audio_frame_buf = (uint8_t*)av_malloc(_buff_size);
	int r = avcodec_fill_audio_frame(this->m_audio_encode_frame, this->m_audio_encode_frame->ch_layout.nb_channels, this->m_audio_encode_codec_ctx->sample_fmt, this->m_audio_frame_buf, _buff_size, 1);

	this->m_audio_encode_packet = av_packet_alloc();
	this->m_audio_encode_buffer = new uint8_t[10240];

这里就是简单的,对编码时需要用到的对象初始化,m_audio_encode_buffer用来缓存音频数据,原因后面说。

音频重采样

	if (swr_alloc_set_opts2(&this->m_audio_swr_ctx, &this->m_audio_encode_codec_ctx->ch_layout, this->m_audio_encode_codec_ctx->sample_fmt, this->m_audio_encode_codec_ctx->sample_rate, &this->m_audio_encode_codec_ctx->ch_layout, _input_format, this->m_audio_encode_codec_ctx->sample_rate, 0, nullptr) != 0) {
		// TODO log

		return false;
	}
	swr_init(this->m_audio_swr_ctx);
	this->m_audio_swr_buffer = new uint8_t[10240];

这里主要是改变音频数据的采样率与格式,也就是sample_rate与sample_fmt。

m_audio_swr_buffer用来接收重采样之后的数据。

接下来是采集线程的回调方法,首先需要重采样音频数据。

	// 转换
	size_t size = _sample_count * _sample_size;
	uint8_t* in[2] = { _data, nullptr };
	uint8_t* out[2] = { this->m_audio_swr_buffer, this->m_audio_swr_buffer + size };
	int count = swr_convert(this->m_audio_swr_ctx, out, _sample_count, const_cast<const uint8_t**>(in), _sample_count);
	if (this->m_filter != nullptr) {
		this->m_filter->afterAudioSwr(out[0], _sample_count, this);
	}
	// 缓存保证传入编码器数据为1024 * 4  aac规定
	memcpy_s(this->m_audio_encode_buffer + this->m_audio_encode_buffer_size, size, out[0], size);
	this->m_audio_encode_buffer_size += size;

由于aac编码器规定传入的帧长度为1024,因此需要m_audio_encode_buffer缓存重采样之后的数据。其中SENDFRAMESIZE=1024*4,原因与音频采集部分相同,帧长度*单个帧所占字节数,这里直接写死了,因为aac传入格式为AV_SAMPLE_FMT_FLTP。

	while (this->m_audio_encode_buffer_size >= SENDFRAMESIZE) {
		memcpy_s(m_audio_frame_buf, SENDFRAMESIZE, this->m_audio_encode_buffer, SENDFRAMESIZE);
		memcpy_s(m_audio_frame_buf + SENDFRAMESIZE, SENDFRAMESIZE, this->m_audio_encode_buffer, SENDFRAMESIZE);
		avcodec_send_frame(this->m_audio_encode_codec_ctx, this->m_audio_encode_frame);
		memcpy_s(this->m_audio_encode_buffer, SENDFRAMESIZE, this->m_audio_encode_buffer + SENDFRAMESIZE, SENDFRAMESIZE);
		this->m_audio_encode_buffer_size -= SENDFRAMESIZE;
	}
	while (avcodec_receive_packet(this->m_audio_encode_codec_ctx, this->m_audio_encode_packet) >= 0) {
		if (this->m_filter != nullptr) {
			this->m_filter->afterAudioEncode(this->m_audio_encode_packet);
		}
		av_packet_unref(this->m_audio_encode_packet);
	}

这里贴一下仿照obs写的实时获取音频输入的音量,传入_data为重采样之后的frame.data:

void MemCodecAcc::calculateVolum(uint8_t** _data, size_t _sample_count)
{
	float r[2] = { 0.0 };
	for (int i = 0; i < 2; ++i) {
		float* samples = reinterpret_cast<float*>(_data[i]);
		__m128 peek = _mm_loadu_ps(samples);
		for (size_t i = 0; (i + 3) < _sample_count; i += 4) {
			__m128 new_work = _mm_load_ps(&samples[i]);
			peek = _mm_max_ps(peek, _mm_andnot_ps(_mm_set1_ps(-0.f), new_work));
		}
		float x4_mem[4];
		float mul = 1.0f;
		_mm_storeu_ps(x4_mem, peek);
		r[i] = x4_mem[0];
		r[i] = fmaxf(r[i], x4_mem[1]);
		r[i] = fmaxf(r[i], x4_mem[2]);
		r[i] = fmaxf(r[i], x4_mem[3]);
		r[i] = 20.0f * log10f(r[i] * mul);
	}
	std::cout << "\r";
	std::cout << "L: " << std::setprecision(4) << r[0] << ", R: " << std::setprecision(4) << r[1];
}

将h264与aac合成为mp4

这里写成了一个filter,具体可以去gitee翻一下源码,在Encoder里。

初始化

	if (avformat_alloc_output_context2(&this->m_save_ctx, nullptr, nullptr, _file_name) < 0) {
		// TODO log
		return false;
	}
	if (avio_open(&this->m_save_ctx->pb, _file_name, AVIO_FLAG_WRITE | AVIO_FLAG_READ) < 0) {
		// TODO log

		avformat_free_context(this->m_save_ctx);
		this->m_save_ctx = nullptr;
		return false;
	}
	
	if (_video_encoder_codec_ctx != nullptr) {
		AVCodecContext* video_encoder_codec_ctx = reinterpret_cast<AVCodecContext*>(_video_encoder_codec_ctx);
		this->m_save_video_stream = avformat_new_stream(this->m_save_ctx, nullptr);
		avcodec_parameters_from_context(this->m_save_video_stream->codecpar, video_encoder_codec_ctx);
		this->m_video_time_base = new AVRational;
		this->m_video_time_base->den = video_encoder_codec_ctx->time_base.den;
		this->m_video_time_base->num = video_encoder_codec_ctx->time_base.num;
		this->m_video_pts = 0;
	}

	if (_audio_encoder_codec_ctx != nullptr) {
		AVCodecContext* audio_encoder_codec_ctx = reinterpret_cast<AVCodecContext*>(_audio_encoder_codec_ctx);
		this->m_save_audio_stream = avformat_new_stream(this->m_save_ctx, nullptr);
		avcodec_parameters_from_context(this->m_save_audio_stream->codecpar, audio_encoder_codec_ctx);
		this->m_audio_pts = 0;
	}

	// av_dump_format(this->m_saveCtx, 0, _save_file_name, 1); TODO dump to log
	avformat_write_header(this->m_save_ctx, nullptr);
	this->m_ready = true;
	return true;

avformat_alloc_output_context2是ffmpeg的方法,根据文件名,打开一个AVFormatContext。

avio_open打开一个文件,将相应信息传给AVFormatContext.pb,可以类比为fopen_s(),第三个参数表示可读可写。

接下来创建输出流avformat_new_stream,随后通过avcodec_parameters_from_context给流赋值,其实就是指定流的编码格式,保存到文件后方便解码时获取。

合成

void SaveFileFilter::afterVideoEncode(AVPacket* _packet)
{
	if (this->m_ready) {
		_packet->stream_index = this->m_save_video_stream->index;
		_packet->dts = _packet->pts = this->m_video_pts;
		av_packet_rescale_ts(_packet, *this->m_video_time_base, this->m_save_video_stream->time_base);
		this->m_video_pts += 1;
		this->m_mutex->lock();
		av_interleaved_write_frame(this->m_save_ctx, _packet);
		this->m_mutex->unlock();
	}
	EncoderFilter::afterVideoEncode(_packet);
}

void SaveFileFilter::afterAudioEncode(AVPacket* _packet)
{
	if (this->m_ready) {
		_packet->stream_index = this->m_save_audio_stream->index;
		_packet->dts = _packet->pts = this->m_audio_pts;
		av_packet_rescale_ts(_packet, this->m_save_audio_stream->time_base, this->m_save_audio_stream->time_base);
		this->m_audio_pts += 1024;
		this->m_mutex->lock();
		av_interleaved_write_frame(this->m_save_ctx, _packet);
		this->m_mutex->unlock();
	}
	EncoderFilter::afterAudioEncode(_packet);
}

以上是获取音频/视频编码后回调,具体调用方式可以翻下源码。

其中关键的是两个步骤

1. av_packet_rescale_ts,用来进行时间基的转换,对pts与dts进行修改。pts表示解码时帧什么时候显示,dts表示帧什么时候开始解码,其实可以理解为帧的索引或者说刻度。用video举例,video的time_base是1/60,也就是60fps的视频,每秒60帧。而stream的time_base为1/90000,也就是每秒90000帧。因此,如果不进行时间基转换,以stream的时间基为基准,则传入90000张图片组成90000帧,可以构成一秒的视频。pts=60的帧会在60/60秒时显示,而在stream的时间基中,会在60/90000秒显示。因此,video时间基中pts=1的帧,在转换后pts=1500 = 1 *(90000/60)。

2. 在初始化时,pts要分别设置为0,这样在存入文件解码时,相应的帧才会在正确时间被解码展示与播放。

结尾

有时间会结合写好的网络组件完成远程控制,目前可以录制。

源码放在gitee

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值