Mã chụp màn hình
bool ScreenCapture::captureFrame()
{
DXGI_OUTDUPL_FRAME_INFO frameInfo;
IDXGIResource* desktopResource = nullptr;
HRESULT hr = this->m_duplication->AcquireNextFrame(0, &frameInfo, &desktopResource);
if (hr == DXGI_ERROR_WAIT_TIMEOUT) {
return false;
}
if (this->m_texture != nullptr) {
this->m_texture->Release();
this->m_texture = nullptr;
}
hr = desktopResource->QueryInterface(__uuidof(ID3D11Texture2D), reinterpret_cast<void**>(&this->m_texture));
if (FAILED(hr)) {
desktopResource->Release();
this->m_duplication->ReleaseFrame();
return false;
}
/*if (!this->transferFrameData(this->m_texture, _buffer, _size)) {
desktopResource->Release();
this->m_duplication->ReleaseFrame();
return false;
}*/
desktopResource->Release();
desktopResource = nullptr;
if (FAILED(hr)) {
return false;
}
return true;
}
Đây là quy trình chụp màn hình bằng GXDI, trước đó cần thực hiện một số công việc để nhận được adapter của màn hình, sau đó tạo dxdevice dựa trên adapter.
bool ScreenCapture::transferFrameData(ID3D11Texture2D* _texture, uint8_t* _buffer, size_t _size)
{
D3D11_TEXTURE2D_DESC desc;
_texture->GetDesc(&desc);
ID3D11Texture2D* _destination = nullptr;
D3D11_TEXTURE2D_DESC destDesc;
memcpy_s(&destDesc, sizeof(destDesc), &desc, sizeof(desc));
destDesc.Usage = D3D11_USAGE_STAGING;
destDesc.BindFlags = 0;
destDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
destDesc.MiscFlags = 0;
HRESULT hr = this->m_device->CreateTexture2D(&destDesc, nullptr, &_destination);
if (FAILED(hr)) {
return false;
}
this->m_context->CopyResource(_destination, _texture);
D3D11_MAPPED_SUBRESOURCE mappedResource;
UINT subresource = D3D11CalcSubresource(0, 0, 0);
hr = this->m_context->Map(_destination, subresource, D3D11_MAP_READ, 0, &mappedResource);
if (FAILED(hr)) {
return false;
}
memcpy_s(_buffer, _size, mappedResource.pData, _size);
this->m_context->Unmap(_destination, subresource);
_destination->Release();
this->m_duplication->ReleaseFrame();
return true;
}
Đoạn mã trên sao chép dữ liệu từ texture đã chụp màn hình, đây là cách viết thông thường, có thể tìm thấy nhiều nguồn trên mạng.
Kiểm soát và tính toán số khung hình
double FrameTimer::nextFrame() const {
const long long target = mulDiv64(this->m_lastTime + this->m_interval, this->m_clockFreq->QuadPart);
LARGE_INTEGER currentTime;
QueryPerformanceCounter(¤tTime);
const long long waitTime = (((target - currentTime.QuadPart) * 1000.0) / this->m_clockFreq->QuadPart);
if (currentTime.QuadPart < target) {
if (waitTime > 1) {
Sleep(waitTime - 1);
}
for (;;) {
QueryPerformanceCounter(¤tTime);
if (currentTime.QuadPart >= target) {
break;
}
YieldProcessor();
}
return this->m_fps;
}
return this->m_fps / (1 - waitTime * this->m_fps/1000.0);
}
void FrameTimer::mark()
{
this->m_lastTime = this->getCurrentTime();
}
long long FrameTimer::getCurrentTime() const {
return std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
}
Ở đây tham khảo mã điều khiển số khung hình của OBS, phương thức getCurrentTime() sử dụng thư viện chrono để lấy thời gian, trong hàm điều khiển số khung hình sử dụng QueryPerformanceCounter để lấy thời gian, kết hợp với mã nguồn OBS có thể chạy đúng.
Nếu phương thức getCurrentTime() cũng sử dụng QueryPerformanceCounter để lấy thời gian, kết quả tính toán sẽ không chính xác. Không thực sự hiểu phần last_time của OBS, ở đây xin trình bày.
const uint64_t udiff = os_gettime_ns() - cur_time;
int64_t diff;
memcpy(&diff, &udiff, sizeof(diff));
const uint64_t clamped_diff = (diff > (int64_t)interval_ns)
? (uint64_t)diff
: interval_ns;
count = (int)(clamped_diff / interval_ns);
*p_time = cur_time + interval_ns * count;
uint64_t os_gettime_ns(void)
{
LARGE_INTEGER current_time;
QueryPerformanceCounter(¤t_time);
return util_mul_div64(current_time.QuadPart, 1000000000,
get_clockfreq());
}
Thực tế os_gettime_ns là chuyển đổi thời gian từ QueryPerformanceCounter (sau khi chuyển đổi, số bit sẽ giống với số bit thời gian lấy từ thư viện chrono), trong quá trình tính toán điều khiển số khung hình lại chuyển đổi ngược lại, không thực sự hiểu mục đích của bước này.
Vẽ con trỏ chuột và kết xuất
Chuyển texture thành QImage để vẽ và kết xuất
void MainWindow::drawCursor(QImage* _image)
{
QPainter painter(_image);
CURSORINFO cursorInfo;
memset(&cursorInfo, 0, sizeof(cursorInfo));
cursorInfo.cbSize = sizeof(cursorInfo);
if (GetCursorInfo(&cursorInfo)) {
HICON hIcon = CopyIcon(cursorInfo.hCursor);
if (hIcon) {
ICONINFO iconInfo;
if (GetIconInfo(hIcon, &iconInfo)) {
this->m_cursorImage = QImage::fromHBITMAP(iconInfo.hbmColor);
this->m_cursorPixmap = QPixmap::fromImage(this->m_cursorImage);
this->m_cursorPixmap.setMask(QBitmap::fromImage(QImage::fromHBITMAP(iconInfo.hbmMask)));
DeleteObject(iconInfo.hbmColor);
DeleteObject(iconInfo.hbmMask);
}
painter.drawPixmap(cursorInfo.ptScreenPos.x, cursorInfo.ptScreenPos.y, this->m_cursorPixmap);
}
}
}
void DisplayWindow::showScreen(QImage& image)
{
QPixmap pixmap = QPixmap::fromImage(image.scaled(this->m_imageLabel->width(), this->m_imageLabel->height(), Qt::IgnoreAspectRatio));
this->m_imageLabel->setPixmap(pixmap);
}
Sau khi vẽ con trỏ chuột, chuyển ảnh thành pixmap và đặt vào label, ở đây có một lỗi. Hình dạng con trỏ chuột khi có thể chọn văn bản (giống chữ cái I viết hoa) không được vẽ ra, không chắc có phải do mask hay do cửa sổ ứng dụng quá nhỏ không thấy.
Nhưng đây không phải là điểm chính, đang xem xét không chuyển sang QImage, tham khảo mã nguồn OBS, đang nghiên cứu.
Kiểm tra hiệu năng
Chỉ riêng việc chụp màn hình đã đạt yêu cầu hiệu năng, có thể ổn định 60 khung hình, sau khi thử nghiệm 144 khung hình cũng có thể ổn định cơ bản.
Nếu thêm phần kết xuất vào luồng chụp màn hình, không thể ổn định 60 khung hình.
Kiểm tra hiệu năng phần kết xuất riêng cũng vậy, gần như không thể ổn định 60 khung hình.
Cần xem xét sử dụng trực tiếp thành phần kết xuất dx11 để hiển thị thời gian thực.
Ngoài ra, luồng mã hóa hiệu năng rất kém.
Thiết lập tham số mã hóa:
this->m_encodeCodecCtx = avcodec_alloc_context3(m_encodeCodec);
this->m_encodeCodecCtx->codec_id = AV_CODEC_ID_H264;
// this->m_encodeCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
this->m_encodeCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
this->m_encodeCodecCtx->bit_rate = 25000 * 1000;
this->m_encodeCodecCtx->width = width;
this->m_encodeCodecCtx->height = height;
this->m_encodeCodecCtx->time_base.num = 1;
this->m_encodeCodecCtx->time_base.den = fps;
this->m_encodeCodecCtx->framerate.num = fps;
this->m_encodeCodecCtx->framerate.den = 1;
this->m_encodeCodecCtx->gop_size = 10;
av_dict_set(&outputParam, "preset", "veryfast", 0);
// av_dict_set(&outputParam, "preset", "ultrafast", 0);
av_dict_set(&outputParam, "tune", "zerolatency", 0);
av_dict_set(&outputParam, "profile", "Main", 0);
Ở đây trước tiên thử mã hóa mềm, hiệu năng không thể ổn định 60 khung hình, không rõ phần mềm điều khiển từ xa đạt 144 khung hình như thế nào, có lẽ không cần nén mà truyền trực tiếp?
Trước hết cần giải quyết vấn đề hiệu năng phần kết xuất.