本篇文章是本人音视频技术文章集中的开篇,之后会持续更新创作更多关于音视频的文章。望有大佬可以交流、指点。
文章主要表现形式是以实现为主,扩展知识为辅,所以一般比较长,不过也有更多实现细节以供参考。

技术简介

DXGI(Microsoft DirectX Graphics Infrastructure)是微软提供的一种可以在win8及以上系统使用的图形设备接口。它负责枚举图形适配器、枚举显示模式、选择缓冲区格式、在进程之间(例如,在应用程序和桌面窗口管理器(DWM)之间)共享资源,以及将呈现的帧传给窗口或监视器以供显示。其直接和硬件设备进行交互,具有很高的效率和性能。
Direct3D 10、Direct3D 11和Direct3D 12等都使用DXGI技术。

使用模块(库)

本篇代码中使用到了d3d11和dxgi两个dll库。

主要流程和代码

1、加载d3d11和dxgi库,并初始化d3d设备。

int DuplicationCaptor::init(const DesktopCaptureRect& rect, const int fps)
{int err = ERROR_CODE_OK;if (m_inited) {return err;}do {m_d3d = HELPER::SystemLib::loadSystemLib("d3d11.dll");if (m_d3d == nullptr) {err = ERROR_CODE_D3D_LOAD_LIB_FAILED;break;}m_dxgi = HELPER::SystemLib::loadSystemLib("dxgi.dll");if (m_dxgi == nullptr) {err = ERROR_CODE_DXGI_LOAD_LIB_FAILED;break;}err = initD3d();HCMDR_ERROR_CODE_BREAK(err);m_timebase = { 1, AV_TIME_BASE };m_pixelFmt = AV_PIX_FMT_BGRA;m_rect = rect;m_fps = fps;m_width = rect.right - rect.left;m_height = rect.bottom - rect.top;m_bufferSize = (m_width * 32 + 31) / 32 * m_height * 4;m_buffer = new uint8_t[m_bufferSize];m_inited = true;} while (0);if (err != ERROR_CODE_OK) {LOGGER::Logger::log(LOGGER::LOG_TYPE_ERROR, "[%s] msg: %s, last error: %lu", __FUNCTION__,HCMDR_GET_ERROR_DESC(err), GetLastError());cleanup();}return err;
}

init中d3d设备初始化函数:

int DuplicationCaptor::initD3d()
{int err = ERROR_CODE_OK;do {IDXGIAdapter* adapter = nullptr;err = getAdapter(&adapter);HCMDR_ERROR_CODE_BREAK(err);err = createD3dDevice(adapter, &m_d3dDevice);HCMDR_ERROR_CODE_BREAK(err);} while (0);return err;
}

initD3d中获取适配器函数:

int DuplicationCaptor::getAdapter(IDXGIAdapter** adapter)
{int err = ERROR_CODE_OK;do {std::list<IDXGIAdapter*> adapters;err = getAdapters(adapters);HCMDR_ERROR_CODE_BREAK(err);if (adapters.empty()) {err = ERROR_CODE_DXGI_FOUND_ADAPTER_FAILED;break;}for (std::list<IDXGIAdapter*>::iterator it = adapters.begin(); it != adapters.end(); it++) {IDXGIOutput* adapterOutput = nullptr;DXGI_ADAPTER_DESC adapterDesc = { 0 };(*it)->GetDesc(&adapterDesc);for (UINT i = 0; (*it)->EnumOutputs(i, &adapterOutput) != DXGI_ERROR_NOT_FOUND; i++) {DXGI_OUTPUT_DESC outputDesc = { 0 };RECT outputRect = { 0 };HRESULT hr = adapterOutput->GetDesc(&outputDesc);if (FAILED(hr)) {continue;}outputRect = outputDesc.DesktopCoordinates;// The target area is within the selected areaif (m_rect.left >= outputRect.left && m_rect.top >= outputRect.top &&m_rect.right <= outputRect.right && m_rect.bottom <= outputRect.bottom) {break;}}if (err == ERROR_CODE_OK) {*adapter = *it;break;}}} while (0);return err;
}

getAdapter中获取适配器列表函数:

int DuplicationCaptor::getAdapters(std::list<IDXGIAdapter*>& adapters)
{int err = ERROR_CODE_OK;do {DXGI_CREATE_FACTORY1 createFactory1 = (DXGI_CREATE_FACTORY1)GetProcAddress(m_dxgi, "CreateDXGIFactory1");if (createFactory1 == nullptr) {err = ERROR_CODE_DXGI_GET_PROC_FAILED;break;}IDXGIFactory1* factory1 = nullptr;HRESULT hr = createFactory1(__uuidof(IDXGIFactory1), &factory1);if (FAILED(hr)) {err = ERROR_CODE_DXGI_GET_FACTORY_FAILED;break;}IDXGIAdapter* adapter = nullptr;adapters.clear();for (UINT i = 0; factory1->EnumAdapters(i, &adapter) != DXGI_ERROR_NOT_FOUND; i++) {if (adapter != nullptr) {adapters.push_back(adapter);}}factory1->Release();} while (0);return err;
}

initD3d中创建d3d设备函数:

int DuplicationCaptor::createD3dDevice(const IDXGIAdapter* adapter, ID3D11Device** device)
{int err = ERROR_CODE_OK;do {PFN_D3D11_CREATE_DEVICE createDevice = (PFN_D3D11_CREATE_DEVICE)GetProcAddress(m_d3d, "D3D11CreateDevice");if (createDevice == nullptr) {err = ERROR_CODE_D3D_GET_PROC_FAILED;break;}D3D_DRIVER_TYPE driverTypes[] ={D3D_DRIVER_TYPE_UNKNOWN,D3D_DRIVER_TYPE_HARDWARE,D3D_DRIVER_TYPE_WARP,D3D_DRIVER_TYPE_REFERENCE,};UINT driverTypeSize = ARRAYSIZE(driverTypes);D3D_FEATURE_LEVEL featureLevels[] ={D3D_FEATURE_LEVEL_11_0,D3D_FEATURE_LEVEL_10_1,D3D_FEATURE_LEVEL_10_0,D3D_FEATURE_LEVEL_9_1};UINT featureLevelSize = ARRAYSIZE(featureLevels);HRESULT hr = S_OK;D3D_FEATURE_LEVEL featureLevel;for (UINT i = 0; i < driverTypeSize; ++i) {hr = createDevice((IDXGIAdapter*)adapter, driverTypes[i], nullptr, 0, featureLevels, featureLevelSize,D3D11_SDK_VERSION, device, &featureLevel, &m_d3dCtx);if (SUCCEEDED(hr)) {break;}}if (FAILED(hr)) {err = ERROR_CODE_D3D_CREATE_DEVICE_FAILED;break;}} while (0);return err;
}

2、采集桌面截图,开启采集线程。

int DuplicationCaptor::start()
{int err = ERROR_CODE_OK;if (m_running) {LOGGER::Logger::log(LOGGER::LOG_TYPE_WARN, "[%s] duplication captor already running", __FUNCTION__);return err;}if (!m_inited) {err = ERROR_CODE_UNINITIALIZED;LOGGER::Logger::log(LOGGER::LOG_TYPE_ERROR, "[%s] duplication captor not yet initialized: %s",__FUNCTION__, HCMDR_GET_ERROR_DESC(err));return err;}m_running = true;m_thread = std::thread(std::bind(&DuplicationCaptor::captureProcess, this));return err;
}

桌面采集线程函数:

void DuplicationCaptor::captureProcess()
{int err = ERROR_CODE_OK;if (!attatchedDesktop()) {err = ERROR_CODE_DXGI_ATTATCH_DESKTOP_FAILED;LOGGER::Logger::log(LOGGER::LOG_TYPE_ERROR, "[%s] msg: %s, last error: %lu", __FUNCTION__,HCMDR_GET_ERROR_DESC(err), GetLastError());if (m_onVideoCaptureError != nullptr) {m_onVideoCaptureError(err);}return;}err = initDuplication();if (err != ERROR_CODE_OK) {LOGGER::Logger::log(LOGGER::LOG_TYPE_ERROR, "[%s] msg: %s", __FUNCTION__, HCMDR_GET_ERROR_DESC(err));if (m_onVideoCaptureError != nullptr) {m_onVideoCaptureError(err);}return;}AVFrame* frame = av_frame_alloc();DXGI_OUTDUPL_FRAME_INFO frameInfo = { 0 };UINT interval = AV_TIME_BASE / m_fps, preTs = 0;while (m_running) {err = getDuplicatedFrame(&frameInfo);if (err == ERROR_CODE_DXGI_DESKTOP_NO_CHANGE) {continue;}if (err != ERROR_CODE_OK) {while (m_running) {av_usleep(300 * 1000);cleanDuplication();err = initDuplication();if (err != ERROR_CODE_OK) {if (m_onVideoCaptureError != nullptr) {m_onVideoCaptureError(err);}}else {break;}}// Get new duplication, then goto duplicate frame.continue;}err = getCursor(&frameInfo);if (err == ERROR_CODE_OK) {drawCursor();}err = releaseDuplicatedFrame();frame->pts = frame->pkt_dts = av_gettime_relative();frame->width = m_width;frame->height = m_height;frame->format = m_pixelFmt;frame->pict_type = AV_PICTURE_TYPE_I;frame->pkt_size = frame->width*frame->height * 4;av_image_fill_arrays(frame->data, frame->linesize, m_buffer, m_pixelFmt, frame->width, frame->height, 1);#ifdef DUPLICATION_CAPTOR_SAVEsaveBitmap();
#endif // DUPLICATION_CAPTOR_SAVEif (m_onVideoCaptureData != nullptr) {m_onVideoCaptureData(frame);}// Control frameratesleep(frame->pts, preTs, interval);preTs = frame->pts;}av_frame_free(&frame);
}

captureProcess中附加桌面函数:

bool DuplicationCaptor::attatchedDesktop()
{HDESK desktop = OpenInputDesktop(0, FALSE, GENERIC_ALL);if (desktop == nullptr) {return false;}// Attach desktop to this threadBOOL battached = SetThreadDesktop(desktop);CloseDesktop(desktop);if (battached == FALSE) {return false;}return true;
}

captureProcess中初始化桌面输出设备的函数:

int DuplicationCaptor::initDuplication()
{int err = ERROR_CODE_OK;do {IDXGIDevice* device = nullptr;HRESULT hr = m_d3dDevice->QueryInterface(__uuidof(IDXGIDevice), reinterpret_cast<void**>(&device));if (FAILED(hr)) {err = ERROR_CODE_D3D_QUERY_INTERFACE_FAILED;break;}IDXGIAdapter* adapter = nullptr;hr = device->GetParent(__uuidof(IDXGIAdapter), reinterpret_cast<void**>(&adapter));device->Release();device = nullptr;if (FAILED(hr)) {err = ERROR_CODE_DXGI_GET_PARENT_FAILED;break;}IDXGIOutput* output = nullptr;hr = adapter->EnumOutputs(m_outputIndex, &output);adapter->Release();adapter = nullptr;if (FAILED(hr)) {err = ERROR_CODE_DXGI_ENUM_OUTPUTS_FAILED;break;}output->GetDesc(&m_outputDesc);IDXGIOutput1* output1 = nullptr;hr = output->QueryInterface(__uuidof(IDXGIOutput1), reinterpret_cast<void**>(&output1));output->Release();output = nullptr;if (FAILED(hr)) {err = ERROR_CODE_DXGI_QUERY_INTERFACE_FAILED;break;}// Create desktop duplicationhr = output1->DuplicateOutput(m_d3dDevice, &m_duplication);output1->Release();output1 = nullptr;if (FAILED(hr)) {err = ERROR_CODE_DXGI_DUPLICATE_FAILED;if (hr == DXGI_ERROR_NOT_CURRENTLY_AVAILABLE) {err = ERROR_CODE_DXGI_DUPLICATE_NOT_AVALIABLE;}LOGGER::Logger::log(LOGGER::LOG_TYPE_ERROR, "[%s] duplicate output failed %ld", __FUNCTION__, hr);break;}} while (0);return err;
}

captureProcess中采集一帧桌面帧函数:

int DuplicationCaptor::getDuplicatedFrame(DXGI_OUTDUPL_FRAME_INFO* frameInfo)
{int err = ERROR_CODE_OK;IDXGIResource* resource = nullptr;HRESULT hr = m_duplication->AcquireNextFrame(500, frameInfo, &resource);// Timeout will return when desktop has no changeif (hr == DXGI_ERROR_WAIT_TIMEOUT) {err = ERROR_CODE_DXGI_DESKTOP_NO_CHANGE;return err;}if (FAILED(hr)) {err = ERROR_CODE_DXGI_ACQUIRE_FRAME_FAILED;return err;}hr = resource->QueryInterface(__uuidof(ID3D11Texture2D), reinterpret_cast<void**>(&m_image));resource->Release();resource = nullptr;if (FAILED(hr)) {err = ERROR_CODE_DXGI_DUPLICATE_FAILED;return err;}// Copy old descriptionD3D11_TEXTURE2D_DESC frameDesc = { 0 };m_image->GetDesc(&frameDesc);// Create a new staging buffer for fill frame imageID3D11Texture2D* newImage = nullptr;frameDesc.Usage = D3D11_USAGE_STAGING;frameDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ | D3D11_CPU_ACCESS_WRITE;frameDesc.BindFlags = 0;frameDesc.MiscFlags = 0;frameDesc.MipLevels = 1;frameDesc.ArraySize = 1;frameDesc.SampleDesc.Count = 1;frameDesc.SampleDesc.Quality = 0;hr = m_d3dDevice->CreateTexture2D(&frameDesc, nullptr, &newImage);if (FAILED(hr)) {err = ERROR_CODE_DXGI_CREATE_TEXTURE_FAILED;return err;}// Copy next staging buffer to new staging bufferm_d3dCtx->CopyResource(newImage, m_image);// Should calculate the row pitch, and compare target row pitch with frame row pitch// Create staging buffer for map bitsIDXGISurface* surface = nullptr;hr = newImage->QueryInterface(__uuidof(IDXGISurface), reinterpret_cast<void**>(&surface));if (FAILED(hr)) {err = ERROR_CODE_DXGI_QUERY_INTERFACE_FAILED;return err;}// Map buffer to mapped rect structureDXGI_MAPPED_RECT mappedRect = { 0 };hr = surface->Map(&mappedRect, DXGI_MAP_READ);if (FAILED(hr)) {err = ERROR_CODE_DXGI_MAP_FAILED;return err;}int dstRowPitch = frameDesc.Width * 4;for (int h = 0; h < frameDesc.Height; h++) {memcpy_s(m_buffer + h*dstRowPitch, dstRowPitch, mappedRect.pBits + h*mappedRect.Pitch, min(mappedRect.Pitch, dstRowPitch));}surface->Unmap();surface->Release();surface = nullptr;return err;
}

captureProcess中获取鼠标信息的函数:

int DuplicationCaptor::getCursor(const DXGI_OUTDUPL_FRAME_INFO* frameInfo)
{int err = ERROR_CODE_OK;// Mouse no changeif (frameInfo->LastMouseUpdateTime.QuadPart == 0) {return err;}bool updated = true;if (frameInfo->PointerPosition.Visible == FALSE && m_cursorInfo.outputIndex != m_outputIndex) {updated = false;}if (frameInfo->PointerPosition.Visible == TRUE && m_cursorInfo.visible &&m_cursorInfo.outputIndex != m_outputIndex &&m_cursorInfo.lastTimestamp.QuadPart > frameInfo->LastMouseUpdateTime.QuadPart) {updated = false;}if (updated) {m_cursorInfo.outputIndex = m_outputIndex;m_cursorInfo.position.x = m_outputDesc.DesktopCoordinates.left + frameInfo->PointerPosition.Position.x;m_cursorInfo.position.y = m_outputDesc.DesktopCoordinates.top + frameInfo->PointerPosition.Position.y;m_cursorInfo.lastTimestamp = frameInfo->LastMouseUpdateTime;m_cursorInfo.visible = frameInfo->PointerPosition.Visible == TRUE;}// No new mouse shape, only update cursor positions & visible stateif (frameInfo->PointerShapeBufferSize == 0) {return err;}// Old buffer is too smallif (frameInfo->PointerShapeBufferSize > m_cursorInfo.size) {if (m_cursorInfo.buffer != nullptr) {delete[] m_cursorInfo.buffer;}m_cursorInfo.buffer = new BYTE[frameInfo->PointerShapeBufferSize];if (m_cursorInfo.buffer == nullptr) {m_cursorInfo.size = 0;err = ERROR_CODE_ALLOCATE_FAILED;return err;}m_cursorInfo.size = frameInfo->PointerShapeBufferSize;}// Get cursor shapeUINT requiredBufferSize;HRESULT hr = m_duplication->GetFramePointerShape(m_cursorInfo.size, reinterpret_cast<void*>(m_cursorInfo.buffer),&requiredBufferSize, &m_cursorInfo.shape);if (FAILED(hr)) {delete[] m_cursorInfo.buffer;m_cursorInfo.buffer = nullptr;m_cursorInfo.size = 0;err = ERROR_CODE_DXGI_GET_CURSOR_SHAPE_FAILED;return err;}return err;
}

createProcess中画鼠标函数:

void DuplicationCaptor::drawCursor()
{if (!m_cursorInfo.visible) {return;}int cursorWidth = m_cursorInfo.shape.Width;int cursorHeight = m_cursorInfo.shape.Height;int relativeLeft = abs(m_cursorInfo.position.x - m_rect.left);int relativeTop = abs(m_cursorInfo.position.y - m_rect.top);// Notice thisif (m_cursorInfo.shape.Type == DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME) {cursorHeight /= 2;}// Skip invisible pixelcursorWidth = min(m_width - relativeLeft, cursorWidth);cursorHeight = min(m_height - relativeTop, cursorHeight);switch (m_cursorInfo.shape.Type) {case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME: {UINT* screenBuffer = reinterpret_cast<UINT*>(m_buffer);for (int row = 0; row < cursorHeight; row++) {BYTE mask = 0x80;for (int col = 0; col < cursorWidth; col++) {// Get masks using apropriate offsets.BYTE andMask = m_cursorInfo.buffer[row * m_cursorInfo.shape.Pitch + col / 8] & mask;BYTE xorMask = m_cursorInfo.buffer[(row + cursorHeight) * m_cursorInfo.shape.Pitch + col / 8] & mask;UINT andMaskVal = (andMask > 0) ? 0xFFFFFFFF : 0xFF000000;UINT xorMaskVal = (xorMask > 0) ? 0x00FFFFFF : 0x00000000;UINT& screenVal = screenBuffer[(relativeTop + row) * m_width + relativeLeft + col];screenVal = (screenVal & andMaskVal) ^ xorMaskVal;// Adjust maskmask = (mask == 0x01) ? 0x80 : (mask >> 1);}}break;}case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR: {UINT* screenBuffer = reinterpret_cast<UINT*>(m_buffer);UINT* cursorBuffer = reinterpret_cast<UINT*>(m_cursorInfo.buffer);for (int row = 0; row < cursorHeight; row++) {for (int col = 0; col < cursorWidth; col++) {UINT cursorVal = cursorBuffer[row*(m_cursorInfo.shape.Pitch / sizeof(UINT)) + col];// Skip black or empty valueif (cursorVal == 0x00000000) {continue;}screenBuffer[(relativeTop + row)*m_width + relativeLeft + col] = cursorVal;}}break;}case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR: {UINT* screenBuffer = reinterpret_cast<UINT*>(m_buffer);UINT* cursorBuffer = reinterpret_cast<UINT*>(m_cursorInfo.buffer);for (int row = 0; row < cursorHeight; row++) {for (int col = 0; col < cursorWidth; col++) {UINT cursorVal = cursorBuffer[row*(m_cursorInfo.shape.Pitch / sizeof(UINT)) + col];UINT& screenVal = screenBuffer[(relativeTop + row)*m_width + relativeLeft + col];UINT maskVal = cursorVal & 0xFF000000;if (maskVal == 0xFF) {screenVal = (screenVal^cursorVal) | 0xFF000000;}else if (maskVal == 0x00) {screenVal = cursorVal | 0xFF000000;}}}break;}default:break;}
}

captureProcess中保存桌面bitmap图片的函数:

void DuplicationCaptor::saveBitmap()
{BITMAPFILEHEADER bf = { 0 };bf.bfType = 0x4d42;bf.bfReserved1 = 0;bf.bfReserved2 = 0;bf.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);bf.bfSize = bf.bfOffBits + m_width * m_height * 4;BITMAPINFOHEADER bi = { 0 };bi.biSize = sizeof(BITMAPINFOHEADER);bi.biWidth = m_width;bi.biHeight = m_height * (-1);bi.biPlanes = 1;bi.biBitCount = 32; // should get from system color bitsbi.biCompression = BI_RGB;bi.biSizeImage = 0;bi.biXPelsPerMeter = 0;bi.biYPelsPerMeter = 0;bi.biClrUsed = 0;bi.biClrImportant = 0;FILE* save = nullptr;fopen_s(&save, "DuplicationCaptor.bmp", "wb");if (save != nullptr) {fwrite(&bf, 1, sizeof(bf), save);fwrite(&bi, 1, sizeof(bi), save);fwrite(m_buffer, 1, m_bufferSize, save);fflush(save);fclose(save);}
}

3、停止采集线程。

int DuplicationCaptor::stop()
{int err = ERROR_CODE_OK;if (!m_running) {return err;}m_running = false;if (m_thread.joinable()) {m_thread.join();}return err;
}

至此,就可以成功保存桌面图片了。

【音视频】WIN8|WIN10的桌面采集技术-DXGI(一)相关推荐

  1. 音视频开发的前景如何?技术深度够深吗?

    音视频行业优势 电信行业的变革: 从1G语音.2G短信.3G图片语音.4G视频到5G未来可期的新时代,见证了音视频行业的磅礴发展. 技术更新慢且门槛高 技术更新慢,技术门槛高,大部分技术沿用至今,依然 ...

  2. 音视频SDK开发包涉及的技术要求

    音视频SDK开发包涉及的技术要求 音视频软件开发,也叫音视频即时通信开发.随着互联网的发展,每天都有相当多的人在使用各种网络交流工具,如MSN,腾讯QQ,ICQ,新浪微博. 然而目前大部分网络交流工具 ...

  3. (二)音视频:MediaCodec编码桌面信息 完整Demo 进一步理解H264

    (一)音视频:解码H264文件流程 渲染和拿到解码后源数据YUV 完整Demo] (二)音视频:MediaCodec编码桌面信息 完整Demo 进一步理解H264 (三)音视频:解析H264 SPS ...

  4. 音视频开发系列(16)技术解码 | SRT和RIST协议综述

    概要 近些年来,互联网行业出现了几波和音视频相关的热潮:VR.短视频.直播等.除了VR因技术成熟度问题,还在蓄势待发,短视频和直播持续热度不减,以各种方式进入新的行业应用领域.视频直播方向,RTMP仍 ...

  5. 音视频开发总结之三网络直播技术

    一. 直播流程总览 目前主流的直播架构中主要有两种方案,即流媒体转发.P2P.流媒体转发,是一种在视频直播中以流的方式将连续的音.视频数据经编码压缩后传输到流媒体服务器,用户实时从服务器获取流媒体资源 ...

  6. 即构互动白板音视频同步、多端协作技术实践

    8月27日晚,即构联合技术社区LiveVideoStack在线上举办了互动白板的技术分享直播活动,吸引了大量在线教育及音视频相关的开发者参与.即构科技互动白板研发负责人陈晓聪在活动上分享了我们在互动白 ...

  7. 音视频系列3:编解码技术

    1. 基础知识 FOURCC是一个4个字节32位的标识符,通常用来标示视频数据流的格式,播放软件可以通过查询FOURCC代码并寻找对于解码器来播放特定视频流,取值通常由各个格式标准自行定义,如DIV3 ...

  8. 音视频---速搭建语音聊天室技术分析

    语音聊天室孵化 一起KTV.众人大合唱.语音开黑.狼人杀.剧本杀.多人配音.观影.语音电台.相亲联谊社交等,一般都是在语音聊天室中进行,那么语音聊天室产品如此火热的原因有哪些呢? 一对一社交适用于朋友 ...

  9. 音视频学习笔记(雷神)—技术解析

    音视频技术解析 封装技术+视频压缩编解码+音频压缩编解码 这是技术层 流媒体传输协议 这是网络层 视频播放器解析 解协议 从视频播放器的角度做解析,拿到传输而来的视频数据后,首先要解协议(传输协议) ...

  10. 音视频云系列 - 谈谈XR关键技术及VR/AR/MR/XR关系

    作者,李琳,毕蕾,灯塔 一.先别被VR/AR/MR/XR搞晕,说说区别 虚拟现实(Virtual Reality,VR).增强现实(Augmented Reality,AR)等业务以其三维化.自然交互 ...

最新文章

  1. 建房子 最安全图纸_农村建什么样的房子合适?分享15套图纸,总有一套适合你...
  2. HTML网页制作:[12]使用框架结构之frameset
  3. Lambda架构概述
  4. hexo部署至FTP-COS
  5. centos常用网络管理命令
  6. 谷歌开源 Kotlin 版本 gRPC
  7. 【C语言】筛选法求素数
  8. VMware esxi6.7虚拟机安装教程
  9. 练习-Java循环综合练习四之日历打印
  10. 一个运行成功的hibernate例子(解决一直报hibernate mapping exception的错误)
  11. python——列表定义及方法
  12. windows下使用vscode编写运行以及调试C/C++
  13. 我把csdn版权标志隐藏了.
  14. docker daemon.json肯多多
  15. 小米、维沃等知名企业启用“.CN”“.中国”域名
  16. 江苏限额申报|2022年省级企业工程技术研究中心项目申报
  17. 酷壳陈皓:如何学好C语言
  18. Deep Knowledge Tracing with Transformers论文阅读
  19. 如何用python爬视频_科学网—利用python爬取一个小视频 - 李鸿斌的博文
  20. (Tekla Structures二次开发)创建布置图

热门文章

  1. HTTP长连接与短链接以及推送技术原理
  2. MATLAB画柱状图(包括普通柱状图,多组柱状图,三维柱状图)
  3. 自定义 QTreeView
  4. 云南等保2.0介绍,等保合规二级、三级整改所需设备清单和具体解决方案
  5. biopython安装_python下如何安装biopython
  6. Oracle全局临时表和私有临时表
  7. stp网页浏览器2.0版本
  8. linux查看rss内存,linux rss 内存
  9. 《图书管理系统》-用例图、活动图与时序图简单绘制-startUML
  10. 计算机操作系统笔记总结:Part1 计算机系统概述