This release adds microphone processing controls and local chat controls, expands platform packages, and synchronizes the website and guides. It also fixes native H.264 input/format handling, signaling recovery, fresh STUN discovery, and UDP ownership during overlapping connections. This is **v1.8.0**. Browser/Server v23, Native control v9 and capture v7 remain unchanged, with the existing 1440p ceiling. Additional resolutions, capability negotiation and Magicsock are separate follow-up work. ## Validation - Full local Web check: 79 files / 1,317 tests, TypeScript and production build. - Full Go checks, platform builds and Windows native capture regressions/probe. - Native H.264 Host lifecycle, Browser-to-App fanout and App-exit recovery, and Native H.264 through embedded SFU: all passed with owned process/port/profile cleanup. - Website build, documentation and repository hygiene passed. Existing UI acceptance covers narrow screens, both languages/themes, architecture downloads and all 23 film checks; final packaging and Go race checks run on the merged revision in CI. - Physical macOS/Linux capture, representative public networks and unmatched field reports retain their documented evidence limits. No claim that every blur/disconnection report is resolved. ## Release notes ### 中文 这是一次功能与兼容性更新:麦克风和聊天增加了实用的小选项,App 与 Server 支持更多设备,同时修复了原生分享和连接恢复中的若干问题。现有 1.x 协议保持不变。 #### 新增与改进 - 浏览器麦克风新增「降噪与回声消除」开关,默认开启回声消除、降噪和自动增益;分享音乐或使用虚拟音频输入时,可按需关闭。 - 可调整弹幕字号和透明度,并将当前保留的聊天记录导出为 TXT。当前会话最多保留 1,000 条文字消息,刷新页面后清空;需要保存时请先导出。 - 新增 Intel Mac、Linux ARM64 App,以及 Linux ARM64 Server 和容器镜像。官网下载区可直接选择对应架构,并显示最新版本和发布时间。 - 更新官网演示、README 截图和中英文教程,明确浏览器分享也可选择窗口或屏幕;开始分享前显示房间号占位,保持底栏控件位置稳定。 - 官网新增直达问题排查的入口,补充浏览器 WebRTC 限制、校园网和 Windows 显卡相关的排查步骤。 - App 模式选择页显示免费开源说明、作者与贡献者署名,以及官方网站和源码链接。 #### 修复 - 修正部分 H.264 原生分享的格式兼容性校验,以及 Windows 编码器输入长度和输出缓冲区对齐问题。 - 清理已失效的重连任务和 Native 接收信令,避免旧连接继续影响当前会话。 - 修复 App 重连时可能复用过期公网地址的问题;重新探测使用当前地址,并处理探测丢包和过期响应,保留其他正常连接。 - 修复 App 新旧媒体连接重叠时共用端口造成的冲突,避免准备或撤销新连接打断仍在使用的画面;关闭连接时及时取消未完成的端口映射请求。 - 无站点密码时,连接恢复不再误引导用户输入站点密码。 - 房间失效或会话终止时,开播前后的原因提示统一显示在电视状态图标中,避免额外提示条。 - 修正 Linux 原生采集的时间戳与关键帧时间基准。感谢 Pedro Antonio 在 [#449](https://github.com/TNTcraftHIM/Piik/pull/449) 的贡献。 #### 使用说明 下载对应平台的完整压缩包,退出旧 App 后解压使用。Windows 继续提供 x64;macOS 提供 Apple 芯片与 Intel 版本,原生采集需要 macOS 13 或更新版本。Linux 提供 x64 与 ARM64,原生采集还需安装[系统组件](https://github.com/TNTcraftHIM/Piik/blob/main/native/capture/linux/README.md)。macOS 和 Linux 的实机覆盖仍有限,欢迎反馈。 ### English This update adds useful microphone and chat controls, brings Piik to more devices, and fixes several native-sharing and connection-recovery issues. The existing 1.x protocols remain unchanged. #### New and improved - Browser microphones now have a “Noise & echo reduction” toggle. Echo cancellation, noise suppression and automatic gain control are on by default; turn them off when appropriate for music or virtual audio inputs. - Adjust chat-overlay text size and opacity, and export retained messages as TXT. The current session keeps up to 1,000 text messages and clears on page refresh; export first if you want to keep them. - Added Intel Mac and Linux ARM64 App packages, plus Linux ARM64 Server and container images. The website provides architecture selectors and shows the latest release and publication date. - Updated the website demonstration, README screenshots and bilingual guides, clarifying that browser capture can also select windows or screens. A room-code placeholder keeps the bottom bar stable before sharing starts. - Added direct website links to troubleshooting, with steps for browser WebRTC restrictions, campus networks and Windows graphics issues. - The App mode-selection page now shows its free and open-source status, author and contributor credit, and links to the official website and source. #### Fixes - Corrected H.264 format compatibility checks and Windows encoder input-length and output-buffer alignment handling. - Retired stale reconnect tasks and Native receive signals so they cannot continue affecting the current session. - Fixed App reconnection reusing an expired public address. Fresh discovery handles lost probes and stale responses while preserving other working connections. - Fixed UDP socket conflicts between overlapping App media connections so preparing or canceling a replacement preserves the active picture. Closing a connection now cancels unfinished gateway mapping requests. - Connection recovery on passwordless sites no longer incorrectly asks for a site passphrase. - Expired-room and session-termination reasons now use the same TV status indicator before and during sharing, without an extra notice strip. - Corrected Linux native capture timestamps and keyframe time bases. Thanks to Pedro Antonio for [#449](https://github.com/TNTcraftHIM/Piik/pull/449). #### Updating Download the complete archive for your platform, quit the old App, and extract the new package. Windows remains x64-only. macOS packages support Apple silicon and Intel; native capture requires macOS 13 or newer. Linux supports x64 and ARM64 and requires additional [system components](https://github.com/TNTcraftHIM/Piik/blob/main/native/capture/linux/README.md) for native capture. Physical-device coverage on macOS and Linux remains limited; feedback is welcome. ## Attribution Co-authored-by: Pedro Antonio <wrenchwastaken@gmail.com>
754 lines
27 KiB
C++
754 lines
27 KiB
C++
#include "h264_encoder.h"
|
|
|
|
#include "capture_output.h"
|
|
#include "capture_color.h"
|
|
|
|
#include <d3d10_1.h>
|
|
#include <evr.h>
|
|
#include <mferror.h>
|
|
#include <propvarutil.h>
|
|
|
|
#include <algorithm>
|
|
#include <charconv>
|
|
|
|
namespace piik::capture::windows {
|
|
|
|
namespace {
|
|
constexpr DWORD kMaxEncodedSampleBytes = 4 * 1024 * 1024;
|
|
}
|
|
|
|
bool VideoProfile::accepts_h264_profile_level_id(const std::string& value) const {
|
|
if (value.size() != 6) return false;
|
|
UINT32 parsed = 0;
|
|
const auto end = value.data() + value.size();
|
|
const auto result = std::from_chars(value.data(), end, parsed, 16);
|
|
if (result.ec != std::errc{} || result.ptr != end || (parsed & 0xff) != h264_level()) return false;
|
|
const auto idc = parsed >> 16;
|
|
const auto constraints = (parsed >> 8) & 0xff;
|
|
// RFC 6184 Table 5: these are the same Constrained Baseline tools, not
|
|
// permission to emit Main/High. Extra constraint flags need not be identical.
|
|
return (idc == 0x42 && (constraints & 0x4f) == 0x40) ||
|
|
(idc == 0x4d && (constraints & 0x8f) == 0x80) ||
|
|
(idc == 0x58 && (constraints & 0xcf) == 0xc0);
|
|
}
|
|
|
|
std::string NarrowAscii(const std::wstring& value) {
|
|
std::string result;
|
|
result.reserve(value.size());
|
|
for (wchar_t character : value) {
|
|
result.push_back(character >= 32 && character <= 126
|
|
? static_cast<char>(character)
|
|
: '?');
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::string GuidString(const GUID& value) {
|
|
wchar_t buffer[40] = {};
|
|
if (StringFromGUID2(value, buffer, static_cast<int>(std::size(buffer))) == 0) {
|
|
return "unavailable";
|
|
}
|
|
return NarrowAscii(buffer);
|
|
}
|
|
|
|
std::string HexByte(UINT8 value) {
|
|
std::ostringstream output;
|
|
output << std::hex << std::setfill('0') << std::setw(2)
|
|
<< static_cast<UINT32>(value);
|
|
return output.str();
|
|
}
|
|
|
|
std::string ProfileLevelId(UINT8 profile, UINT8 constraints, UINT8 level) {
|
|
return HexByte(profile) + HexByte(constraints) + HexByte(level);
|
|
}
|
|
|
|
std::vector<Adapter> EnumerateAdapters() {
|
|
ComPtr<IDXGIFactory1> factory;
|
|
Check(CreateDXGIFactory1(IID_PPV_ARGS(&factory)), "dxgi-factory");
|
|
|
|
std::vector<Adapter> adapters;
|
|
for (UINT index = 0;; ++index) {
|
|
ComPtr<IDXGIAdapter1> adapter;
|
|
HRESULT result = factory->EnumAdapters1(index, &adapter);
|
|
if (result == DXGI_ERROR_NOT_FOUND) {
|
|
break;
|
|
}
|
|
Check(result, "dxgi-enumerate-adapter");
|
|
|
|
Adapter current;
|
|
current.index = index;
|
|
current.value = adapter;
|
|
Check(adapter->GetDesc1(¤t.description), "dxgi-adapter-description");
|
|
if ((current.description.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) == 0) {
|
|
adapters.push_back(std::move(current));
|
|
}
|
|
}
|
|
return adapters;
|
|
}
|
|
|
|
const Adapter& SelectAdapter(const std::vector<Adapter>& adapters, UINT index) {
|
|
auto found = std::find_if(adapters.begin(), adapters.end(),
|
|
[index](const Adapter& adapter) {
|
|
return adapter.index == index;
|
|
});
|
|
if (found == adapters.end()) {
|
|
Fail("adapter-select", "selected DXGI adapter index is unavailable");
|
|
}
|
|
return *found;
|
|
}
|
|
|
|
DeviceContext CreateDevice(const Adapter& adapter) {
|
|
DeviceContext result;
|
|
constexpr D3D_FEATURE_LEVEL levels[] = {
|
|
D3D_FEATURE_LEVEL_12_1, D3D_FEATURE_LEVEL_12_0,
|
|
D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0,
|
|
};
|
|
Check(D3D11CreateDevice(adapter.value.Get(), D3D_DRIVER_TYPE_UNKNOWN, nullptr,
|
|
D3D11_CREATE_DEVICE_VIDEO_SUPPORT |
|
|
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
|
|
levels, static_cast<UINT>(std::size(levels)),
|
|
D3D11_SDK_VERSION, &result.device,
|
|
&result.feature_level, &result.context),
|
|
"d3d11-create-device");
|
|
|
|
ComPtr<ID3D10Multithread> multithread;
|
|
Check(result.device.As(&multithread), "d3d11-multithread-interface");
|
|
multithread->SetMultithreadProtected(TRUE);
|
|
if (!multithread->GetMultithreadProtected()) {
|
|
Fail("d3d11-multithread-protection",
|
|
"D3D11 device did not enable multithread protection");
|
|
}
|
|
|
|
UINT reset_token = 0;
|
|
Check(MFCreateDXGIDeviceManager(&reset_token, &result.manager),
|
|
"dxgi-device-manager-create");
|
|
Check(result.manager->ResetDevice(result.device.Get(), reset_token),
|
|
"dxgi-device-manager-reset");
|
|
return result;
|
|
}
|
|
|
|
std::wstring GetOptionalString(IMFAttributes* attributes, const GUID& key) {
|
|
wchar_t* raw = nullptr;
|
|
UINT32 length = 0;
|
|
HRESULT result = attributes->GetAllocatedString(key, &raw, &length);
|
|
if (result == MF_E_ATTRIBUTENOTFOUND) {
|
|
return L"";
|
|
}
|
|
Check(result, "mft-activation-string");
|
|
std::wstring value(raw, length);
|
|
CoTaskMemFree(raw);
|
|
return value;
|
|
}
|
|
|
|
ActivationList EnumerateHardwareEncoders(const Adapter& adapter,
|
|
bool require_candidate) {
|
|
ComPtr<IMFAttributes> attributes;
|
|
Check(MFCreateAttributes(&attributes, 1), "mft-enumeration-attributes");
|
|
Check(attributes->SetBlob(MFT_ENUM_ADAPTER_LUID,
|
|
reinterpret_cast<const UINT8*>(
|
|
&adapter.description.AdapterLuid),
|
|
sizeof(LUID)),
|
|
"mft-enumeration-adapter-luid");
|
|
|
|
MFT_REGISTER_TYPE_INFO input = {MFMediaType_Video, MFVideoFormat_NV12};
|
|
MFT_REGISTER_TYPE_INFO output = {MFMediaType_Video, MFVideoFormat_H264};
|
|
ActivationList list;
|
|
Check(MFTEnum2(MFT_CATEGORY_VIDEO_ENCODER,
|
|
MFT_ENUM_FLAG_HARDWARE | MFT_ENUM_FLAG_SORTANDFILTER,
|
|
&input, &output, attributes.Get(), &list.values, &list.count),
|
|
"mft-hardware-enumeration");
|
|
if (require_candidate && list.count == 0) {
|
|
Fail("mft-hardware-enumeration",
|
|
"selected adapter exposed no hardware NV12-to-H264 MFT");
|
|
}
|
|
return list;
|
|
}
|
|
|
|
SelectedTransform ActivateTransform(const ActivationList& list, UINT index,
|
|
IMFDXGIDeviceManager* manager) {
|
|
if (index >= list.count) {
|
|
Fail("mft-select", "selected hardware MFT index is unavailable");
|
|
}
|
|
|
|
IMFActivate* activation = list.values[index];
|
|
std::wstring friendly = GetOptionalString(activation, MFT_FRIENDLY_NAME_Attribute);
|
|
if (friendly.empty()) {
|
|
Fail("mft-identity", "hardware MFT has no friendly-name attribute");
|
|
}
|
|
|
|
GUID clsid = {};
|
|
Check(activation->GetGUID(MFT_TRANSFORM_CLSID_Attribute, &clsid),
|
|
"mft-clsid");
|
|
|
|
PROPVARIANT hardware_url;
|
|
PropVariantInit(&hardware_url);
|
|
HRESULT hardware_result = activation->GetItem(
|
|
MFT_ENUM_HARDWARE_URL_Attribute, &hardware_url);
|
|
PropVariantClear(&hardware_url);
|
|
if (FAILED(hardware_result)) {
|
|
Fail("mft-hardware-identity",
|
|
"hardware-only activation omitted its hardware URL attribute");
|
|
}
|
|
|
|
SelectedTransform selected;
|
|
selected.activation = activation;
|
|
selected.name = NarrowAscii(friendly);
|
|
selected.clsid = GuidString(clsid);
|
|
Check(activation->ActivateObject(IID_PPV_ARGS(&selected.transform)),
|
|
"mft-activate");
|
|
|
|
ComPtr<IMFAttributes> transform_attributes;
|
|
Check(selected.transform->GetAttributes(&transform_attributes),
|
|
"mft-global-attributes");
|
|
UINT32 is_async = 0;
|
|
Check(transform_attributes->GetUINT32(MF_TRANSFORM_ASYNC, &is_async),
|
|
"mft-async-readback");
|
|
if (is_async != TRUE) {
|
|
Fail("mft-async-readback", "hardware MFT is not asynchronous");
|
|
}
|
|
Check(transform_attributes->SetUINT32(MF_TRANSFORM_ASYNC_UNLOCK, TRUE),
|
|
"mft-async-unlock");
|
|
|
|
UINT32 d3d11_aware = 0;
|
|
Check(transform_attributes->GetUINT32(MF_SA_D3D11_AWARE, &d3d11_aware),
|
|
"mft-d3d11-aware-readback");
|
|
if (d3d11_aware != TRUE) {
|
|
Fail("mft-d3d11-aware-readback", "hardware MFT is not D3D11-aware");
|
|
}
|
|
|
|
Check(selected.transform->ProcessMessage(
|
|
MFT_MESSAGE_SET_D3D_MANAGER,
|
|
reinterpret_cast<ULONG_PTR>(manager)),
|
|
"mft-set-d3d-manager");
|
|
Check(selected.transform.As(&selected.codec), "mft-codec-api");
|
|
Check(selected.transform.As(&selected.events), "mft-event-generator");
|
|
return selected;
|
|
}
|
|
|
|
void RequireProperty(ICodecAPI* codec, const GUID& key,
|
|
const std::string& name) {
|
|
HRESULT supported = codec->IsSupported(&key);
|
|
if (supported != S_OK) {
|
|
if (FAILED(supported) && supported != E_NOTIMPL) {
|
|
Check(supported, name + "-supported-query");
|
|
}
|
|
Fail(name + "-unsupported", "required codec property is unsupported");
|
|
}
|
|
HRESULT modifiable = codec->IsModifiable(&key);
|
|
if (modifiable == S_FALSE) {
|
|
Fail(name + "-readonly", "required codec property is read-only");
|
|
}
|
|
// Some hardware MFTs do not implement this advisory query. SetValue=S_OK
|
|
// followed by exact GetValue readback remains the authoritative gate.
|
|
if (modifiable != S_OK && modifiable != E_NOTIMPL) {
|
|
Check(modifiable, name + "-modifiable-query");
|
|
}
|
|
}
|
|
|
|
UINT32 ReadU32(ICodecAPI* codec, const GUID& key, const std::string& name) {
|
|
VARIANT value;
|
|
VariantInit(&value);
|
|
Check(codec->GetValue(&key, &value), name + "-readback");
|
|
if (value.vt != VT_UI4) {
|
|
VariantClear(&value);
|
|
Fail(name + "-readback-type", "codec property did not return VT_UI4");
|
|
}
|
|
UINT32 result = value.ulVal;
|
|
VariantClear(&value);
|
|
return result;
|
|
}
|
|
|
|
bool ReadBool(ICodecAPI* codec, const GUID& key, const std::string& name) {
|
|
VARIANT value;
|
|
VariantInit(&value);
|
|
Check(codec->GetValue(&key, &value), name + "-readback");
|
|
if (value.vt != VT_BOOL) {
|
|
VariantClear(&value);
|
|
Fail(name + "-readback-type", "codec property did not return VT_BOOL");
|
|
}
|
|
bool result = value.boolVal != VARIANT_FALSE;
|
|
VariantClear(&value);
|
|
return result;
|
|
}
|
|
|
|
void SetU32(ICodecAPI* codec, const GUID& key, UINT32 expected,
|
|
const std::string& name) {
|
|
RequireProperty(codec, key, name);
|
|
VARIANT value;
|
|
VariantInit(&value);
|
|
value.vt = VT_UI4;
|
|
value.ulVal = expected;
|
|
HRESULT set_result = codec->SetValue(&key, &value);
|
|
VariantClear(&value);
|
|
if (set_result != S_OK) {
|
|
if (FAILED(set_result)) {
|
|
Check(set_result, name + "-set");
|
|
}
|
|
Fail(name + "-readonly", "codec property rejected the requested value");
|
|
}
|
|
if (ReadU32(codec, key, name) != expected) {
|
|
Fail(name + "-weakened", "codec property readback changed the requested value");
|
|
}
|
|
}
|
|
|
|
void SetBool(ICodecAPI* codec, const GUID& key, bool expected,
|
|
const std::string& name) {
|
|
RequireProperty(codec, key, name);
|
|
VARIANT value;
|
|
VariantInit(&value);
|
|
value.vt = VT_BOOL;
|
|
value.boolVal = expected ? VARIANT_TRUE : VARIANT_FALSE;
|
|
HRESULT set_result = codec->SetValue(&key, &value);
|
|
VariantClear(&value);
|
|
if (set_result != S_OK) {
|
|
if (FAILED(set_result)) {
|
|
Check(set_result, name + "-set");
|
|
}
|
|
Fail(name + "-readonly", "codec property rejected the requested value");
|
|
}
|
|
if (ReadBool(codec, key, name) != expected) {
|
|
Fail(name + "-weakened", "codec property readback changed the requested value");
|
|
}
|
|
}
|
|
|
|
void SetOptionalU32(ICodecAPI* codec, const GUID& key, UINT32 value) {
|
|
if (codec->IsSupported(&key) != S_OK) return;
|
|
VARIANT requested;
|
|
VariantInit(&requested);
|
|
requested.vt = VT_UI4;
|
|
requested.ulVal = value;
|
|
(void)codec->SetValue(&key, &requested);
|
|
VariantClear(&requested);
|
|
}
|
|
|
|
void ConfigureCodec(
|
|
ICodecAPI* codec,
|
|
const VideoProfile& profile) {
|
|
SetU32(codec, CODECAPI_AVEncCommonRateControlMode,
|
|
eAVEncCommonRateControlMode_CBR, "codec-cbr");
|
|
SetBool(codec, CODECAPI_AVLowLatencyMode, true, "codec-low-latency");
|
|
SetU32(codec, CODECAPI_AVEncCommonMeanBitRate, profile.bit_rate,
|
|
"codec-mean-bitrate");
|
|
SetU32(codec, CODECAPI_AVEncMPVGOPSize, profile.gop_frames(), "codec-gop");
|
|
SetOptionalU32(codec, CODECAPI_AVEncCommonQualityVsSpeed,
|
|
profile.quality_vs_speed());
|
|
RequireProperty(codec, CODECAPI_AVEncVideoForceKeyFrame,
|
|
"codec-force-keyframe");
|
|
}
|
|
|
|
ComPtr<IMFMediaType> CreateOutputType(
|
|
const VideoProfile& profile) {
|
|
ComPtr<IMFMediaType> type;
|
|
Check(MFCreateMediaType(&type), "output-type-create");
|
|
Check(type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video),
|
|
"output-type-major");
|
|
Check(type->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_H264),
|
|
"output-type-subtype");
|
|
SetSdrVideoColor(type.Get());
|
|
Check(type->SetUINT32(MF_MT_AVG_BITRATE, profile.bit_rate),
|
|
"output-type-bitrate");
|
|
Check(MFSetAttributeSize(type.Get(), MF_MT_FRAME_SIZE, profile.width,
|
|
profile.height),
|
|
"output-type-size");
|
|
Check(MFSetAttributeRatio(type.Get(), MF_MT_FRAME_RATE, profile.frame_rate, 1),
|
|
"output-type-framerate");
|
|
Check(MFSetAttributeRatio(type.Get(), MF_MT_PIXEL_ASPECT_RATIO, 1, 1),
|
|
"output-type-pixel-aspect");
|
|
Check(type->SetUINT32(MF_MT_INTERLACE_MODE,
|
|
MFVideoInterlace_Progressive),
|
|
"output-type-interlace");
|
|
Check(type->SetUINT32(MF_MT_MPEG2_PROFILE, eAVEncH264VProfile_Base),
|
|
"output-type-profile");
|
|
Check(type->SetUINT32(MF_MT_MPEG2_LEVEL, profile.h264_level()),
|
|
"output-type-level");
|
|
return type;
|
|
}
|
|
|
|
ComPtr<IMFMediaType> CreateInputType(
|
|
const VideoProfile& profile) {
|
|
ComPtr<IMFMediaType> type;
|
|
Check(MFCreateMediaType(&type), "input-type-create");
|
|
Check(type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video),
|
|
"input-type-major");
|
|
Check(type->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_NV12),
|
|
"input-type-subtype");
|
|
SetSdrVideoColor(type.Get());
|
|
Check(MFSetAttributeSize(type.Get(), MF_MT_FRAME_SIZE, profile.width,
|
|
profile.height),
|
|
"input-type-size");
|
|
Check(MFSetAttributeRatio(type.Get(), MF_MT_FRAME_RATE, profile.frame_rate, 1),
|
|
"input-type-framerate");
|
|
Check(MFSetAttributeRatio(type.Get(), MF_MT_PIXEL_ASPECT_RATIO, 1, 1),
|
|
"input-type-pixel-aspect");
|
|
Check(type->SetUINT32(MF_MT_INTERLACE_MODE,
|
|
MFVideoInterlace_Progressive),
|
|
"input-type-interlace");
|
|
Check(type->SetUINT32(MF_MT_DEFAULT_STRIDE, profile.width),
|
|
"input-type-stride");
|
|
return type;
|
|
}
|
|
|
|
void RequireTypeU32(IMFAttributes* type, const GUID& key, UINT32 expected,
|
|
const std::string& stage) {
|
|
UINT32 actual = 0;
|
|
Check(type->GetUINT32(key, &actual), stage + "-readback");
|
|
if (actual != expected) {
|
|
Fail(stage + "-weakened", "media type changed a required value");
|
|
}
|
|
}
|
|
|
|
void ValidateMediaTypes(
|
|
IMFTransform* transform,
|
|
const VideoProfile& profile) {
|
|
ComPtr<IMFMediaType> output;
|
|
Check(transform->GetOutputCurrentType(0, &output),
|
|
"output-type-current");
|
|
GUID subtype = {};
|
|
Check(output->GetGUID(MF_MT_SUBTYPE, &subtype), "output-type-subtype-readback");
|
|
if (subtype != MFVideoFormat_H264) {
|
|
Fail("output-type-subtype-weakened", "output is not H264");
|
|
}
|
|
RequireTypeU32(output.Get(), MF_MT_AVG_BITRATE, profile.bit_rate,
|
|
"output-type-bitrate");
|
|
RequireTypeU32(output.Get(), MF_MT_MPEG2_PROFILE,
|
|
eAVEncH264VProfile_Base, "output-type-profile");
|
|
RequireTypeU32(output.Get(), MF_MT_MPEG2_LEVEL, profile.h264_level(),
|
|
"output-type-level");
|
|
|
|
UINT32 width = 0;
|
|
UINT32 height = 0;
|
|
Check(MFGetAttributeSize(output.Get(), MF_MT_FRAME_SIZE, &width, &height),
|
|
"output-type-size-readback");
|
|
if (width != profile.width || height != profile.height) {
|
|
Fail("output-type-size-weakened", "output dimensions changed");
|
|
}
|
|
UINT32 numerator = 0;
|
|
UINT32 denominator = 0;
|
|
Check(MFGetAttributeRatio(output.Get(), MF_MT_FRAME_RATE, &numerator,
|
|
&denominator),
|
|
"output-type-framerate-readback");
|
|
if (numerator != profile.frame_rate || denominator != 1) {
|
|
Fail("output-type-framerate-weakened", "output frame rate changed");
|
|
}
|
|
}
|
|
|
|
void ValidateCodecReadback(
|
|
ICodecAPI* codec,
|
|
const VideoProfile& profile) {
|
|
if (ReadU32(codec, CODECAPI_AVEncCommonRateControlMode, "codec-cbr") !=
|
|
eAVEncCommonRateControlMode_CBR ||
|
|
!ReadBool(codec, CODECAPI_AVLowLatencyMode, "codec-low-latency") ||
|
|
ReadU32(codec, CODECAPI_AVEncCommonMeanBitRate,
|
|
"codec-mean-bitrate") != profile.bit_rate ||
|
|
ReadU32(codec, CODECAPI_AVEncMPVGOPSize, "codec-gop") !=
|
|
profile.gop_frames()) {
|
|
Fail("codec-final-readback", "one or more codec properties were weakened");
|
|
}
|
|
}
|
|
|
|
std::optional<std::pair<size_t, size_t>> FindStartCode(
|
|
const std::vector<UINT8>& bytes, size_t from) {
|
|
for (size_t index = from; index + 3 <= bytes.size(); ++index) {
|
|
if (bytes[index] != 0 || bytes[index + 1] != 0) {
|
|
continue;
|
|
}
|
|
if (bytes[index + 2] == 1) {
|
|
return std::pair<size_t, size_t>{index, 3};
|
|
}
|
|
if (index + 4 <= bytes.size() && bytes[index + 2] == 0 &&
|
|
bytes[index + 3] == 1) {
|
|
return std::pair<size_t, size_t>{index, 4};
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
NalSummary InspectAnnexB(const std::vector<UINT8>& bytes) {
|
|
NalSummary summary;
|
|
std::optional<std::pair<size_t, size_t>> current = FindStartCode(bytes, 0);
|
|
if (!current || current->first != 0) {
|
|
return summary;
|
|
}
|
|
summary.annex_b = true;
|
|
while (current) {
|
|
size_t nal_start = current->first + current->second;
|
|
std::optional<std::pair<size_t, size_t>> next = FindStartCode(bytes, nal_start);
|
|
size_t nal_end = next ? next->first : bytes.size();
|
|
if (nal_start < nal_end) {
|
|
UINT8 nal_type = bytes[nal_start] & 0x1f;
|
|
summary.sps = summary.sps || nal_type == 7;
|
|
summary.pps = summary.pps || nal_type == 8;
|
|
summary.idr = summary.idr || nal_type == 5;
|
|
if (nal_type == 7 && nal_end - nal_start >= 4) {
|
|
std::string value = ProfileLevelId(bytes[nal_start + 1],
|
|
bytes[nal_start + 2],
|
|
bytes[nal_start + 3]);
|
|
if (summary.profile_level_id && *summary.profile_level_id != value) {
|
|
Fail("bitstream-sps-consistency",
|
|
"one access unit contained conflicting SPS profile bytes");
|
|
}
|
|
summary.profile_level_id = value;
|
|
}
|
|
}
|
|
current = next;
|
|
}
|
|
return summary;
|
|
}
|
|
|
|
std::vector<UINT8> ReadSample(IMFSample* sample) {
|
|
DWORD total_length = 0;
|
|
Check(sample->GetTotalLength(&total_length), "output-total-length");
|
|
if (total_length == 0 || total_length > kMaxEncodedSampleBytes) {
|
|
Fail("output-buffer-bounds", "encoded output size is empty or unbounded");
|
|
}
|
|
|
|
ComPtr<IMFMediaBuffer> buffer;
|
|
Check(sample->ConvertToContiguousBuffer(&buffer), "output-contiguous-buffer");
|
|
DWORD length = 0;
|
|
Check(buffer->GetCurrentLength(&length), "output-buffer-length");
|
|
if (length != total_length) {
|
|
Fail("output-buffer-length", "contiguous output length changed unexpectedly");
|
|
}
|
|
std::vector<UINT8> bytes(length);
|
|
UINT8* raw = nullptr;
|
|
DWORD maximum = 0;
|
|
DWORD current = 0;
|
|
Check(buffer->Lock(&raw, &maximum, ¤t), "output-buffer-lock");
|
|
if (raw == nullptr || current != length || current > kMaxEncodedSampleBytes) {
|
|
buffer->Unlock();
|
|
Fail("output-buffer-lock-bounds",
|
|
"locked output length is inconsistent or unbounded");
|
|
}
|
|
std::copy(raw, raw + current, bytes.begin());
|
|
Check(buffer->Unlock(), "output-buffer-unlock");
|
|
return bytes;
|
|
}
|
|
|
|
void ForceKeyFrame(ICodecAPI* codec) {
|
|
VARIANT value;
|
|
VariantInit(&value);
|
|
value.vt = VT_UI4;
|
|
value.ulVal = 1;
|
|
HRESULT set_result =
|
|
codec->SetValue(&CODECAPI_AVEncVideoForceKeyFrame, &value);
|
|
VariantClear(&value);
|
|
if (set_result != S_OK) {
|
|
if (FAILED(set_result)) {
|
|
Check(set_result, "codec-force-keyframe-set");
|
|
}
|
|
Fail("codec-force-keyframe-readonly",
|
|
"force-key-frame command was not accepted");
|
|
}
|
|
}
|
|
|
|
ComPtr<IMFSample> CreateCallerOutputSample(const MFT_OUTPUT_STREAM_INFO& info) {
|
|
if (info.cbSize > kMaxEncodedSampleBytes) {
|
|
Fail("output-buffer-contract",
|
|
"hardware MFT requested an output buffer larger than four MiB");
|
|
}
|
|
ComPtr<IMFSample> sample;
|
|
Check(MFCreateSample(&sample), "output-sample-create");
|
|
ComPtr<IMFMediaBuffer> buffer;
|
|
// Stream info uses bytes; the allocator takes an alignment mask (bytes - 1).
|
|
Check(MFCreateAlignedMemoryBuffer(kMaxEncodedSampleBytes,
|
|
info.cbAlignment ? info.cbAlignment - 1 : 0,
|
|
&buffer),
|
|
"output-memory-buffer");
|
|
Check(sample->AddBuffer(buffer.Get()), "output-sample-buffer");
|
|
return sample;
|
|
}
|
|
|
|
ComPtr<IMFSample> PullOutput(IMFTransform* transform,
|
|
const MFT_OUTPUT_STREAM_INFO& info) {
|
|
bool transform_provides =
|
|
(info.dwFlags & (MFT_OUTPUT_STREAM_PROVIDES_SAMPLES |
|
|
MFT_OUTPUT_STREAM_CAN_PROVIDE_SAMPLES)) != 0;
|
|
ComPtr<IMFSample> caller_sample;
|
|
if (!transform_provides) {
|
|
caller_sample = CreateCallerOutputSample(info);
|
|
}
|
|
|
|
MFT_OUTPUT_DATA_BUFFER output = {};
|
|
output.dwStreamID = 0;
|
|
output.pSample = caller_sample.Get();
|
|
DWORD status = 0;
|
|
HRESULT result = transform->ProcessOutput(0, 1, &output, &status);
|
|
|
|
ComPtr<IMFCollection> events;
|
|
if (output.pEvents != nullptr) {
|
|
events.Attach(output.pEvents);
|
|
}
|
|
Check(result, "mft-process-output");
|
|
if ((output.dwStatus & MFT_OUTPUT_DATA_BUFFER_FORMAT_CHANGE) != 0 ||
|
|
(status & MFT_PROCESS_OUTPUT_STATUS_NEW_STREAMS) != 0) {
|
|
Fail("mft-output-format-change",
|
|
"hardware MFT changed format during the fixed run");
|
|
}
|
|
|
|
if (caller_sample) {
|
|
if (output.pSample != caller_sample.Get()) {
|
|
if (output.pSample != nullptr) {
|
|
output.pSample->Release();
|
|
}
|
|
Fail("mft-output-allocation",
|
|
"hardware MFT replaced the caller-owned output sample");
|
|
}
|
|
return caller_sample;
|
|
}
|
|
if (output.pSample == nullptr) {
|
|
Fail("mft-output-sample", "hardware MFT produced no output sample");
|
|
}
|
|
ComPtr<IMFSample> produced;
|
|
produced.Attach(output.pSample);
|
|
return produced;
|
|
}
|
|
|
|
ComPtr<IMFSample> CreateSurfaceSample(ID3D11Texture2D* texture,
|
|
LONGLONG timestamp100ns,
|
|
LONGLONG duration100ns) {
|
|
ComPtr<IMFMediaBuffer> buffer;
|
|
Check(MFCreateDXGISurfaceBuffer(IID_ID3D11Texture2D, texture, 0, FALSE,
|
|
&buffer),
|
|
"input-dxgi-buffer");
|
|
// Surface storage exists, but the wrapper initially reports zero valid bytes.
|
|
// MFTs may honor that length instead of inferring it from the texture.
|
|
DWORD length = 0;
|
|
Check(buffer->GetMaxLength(&length), "input-buffer-capacity");
|
|
Check(buffer->SetCurrentLength(length), "input-buffer-length");
|
|
ComPtr<IMFSample> sample;
|
|
Check(MFCreateVideoSampleFromSurface(nullptr, &sample),
|
|
"input-video-sample");
|
|
Check(sample->AddBuffer(buffer.Get()), "input-sample-buffer");
|
|
Check(sample->SetSampleTime(timestamp100ns), "input-sample-time");
|
|
Check(sample->SetSampleDuration(duration100ns), "input-sample-duration");
|
|
return sample;
|
|
}
|
|
|
|
void RequireEncoderTime(EncoderClock::time_point deadline) {
|
|
if (EncoderClock::now() >= deadline) {
|
|
Fail("codec-probe-timeout", "native codec selection exceeded its time budget");
|
|
}
|
|
}
|
|
|
|
LiveEncoder::LiveEncoder(
|
|
SelectedTransform selected,
|
|
VideoProfile profile)
|
|
: VideoEncoder(OutputKind::h264, selected.name, selected.clsid),
|
|
selected_(std::move(selected)), events_(selected_.events.Get()), profile_(profile) {
|
|
ConfigureCodec(selected_.codec.Get(), profile_);
|
|
ComPtr<IMFMediaType> output_type = CreateOutputType(profile_);
|
|
Check(selected_.transform->SetOutputType(0, output_type.Get(), 0),
|
|
"mft-set-output-type");
|
|
ComPtr<IMFMediaType> input_type = CreateInputType(profile_);
|
|
Check(selected_.transform->SetInputType(0, input_type.Get(), 0),
|
|
"mft-set-input-type");
|
|
ValidateMediaTypes(selected_.transform.Get(), profile_);
|
|
ValidateCodecReadback(selected_.codec.Get(), profile_);
|
|
Check(selected_.transform->GetOutputStreamInfo(0, &output_info_),
|
|
"mft-output-stream-info");
|
|
Check(selected_.transform->ProcessMessage(
|
|
MFT_MESSAGE_NOTIFY_BEGIN_STREAMING, 0),
|
|
"mft-begin-streaming");
|
|
Check(selected_.transform->ProcessMessage(
|
|
MFT_MESSAGE_NOTIFY_START_OF_STREAM, 0),
|
|
"mft-start-stream");
|
|
started_ = true;
|
|
}
|
|
|
|
LiveEncoder::~LiveEncoder() {
|
|
if (started_) {
|
|
selected_.transform->ProcessMessage(MFT_MESSAGE_COMMAND_FLUSH, 0);
|
|
selected_.transform->ProcessMessage(MFT_MESSAGE_NOTIFY_END_STREAMING, 0);
|
|
}
|
|
}
|
|
|
|
void LiveEncoder::SetBitrate(UINT32 bitrate) {
|
|
SetU32(selected_.codec.Get(), CODECAPI_AVEncCommonMeanBitRate, bitrate,
|
|
"codec-live-bitrate");
|
|
profile_.bit_rate = bitrate;
|
|
}
|
|
|
|
EncodedAccessUnit LiveEncoder::Encode(
|
|
ID3D11Texture2D* texture, UINT64 timestamp100ns, bool force_key_frame,
|
|
EncoderClock::time_point probe_deadline) {
|
|
WaitForInput(probe_deadline);
|
|
if (force_key_frame) ForceKeyFrame(selected_.codec.Get());
|
|
ComPtr<IMFSample> sample = CreateSurfaceSample(
|
|
texture, static_cast<LONGLONG>(timestamp100ns),
|
|
profile_.frame_duration_100ns());
|
|
Check(selected_.transform->ProcessInput(0, sample.Get(), 0),
|
|
"mft-process-input");
|
|
--input_requests_;
|
|
|
|
const auto deadline = std::min(probe_deadline,
|
|
EncoderClock::now() + std::chrono::seconds(2));
|
|
while (std::chrono::steady_clock::now() < deadline) {
|
|
MediaEventType type = NextEvent(deadline, "mft-output-timeout");
|
|
if (type == METransformNeedInput) {
|
|
++input_requests_;
|
|
continue;
|
|
}
|
|
if (type != METransformHaveOutput) continue;
|
|
|
|
ComPtr<IMFSample> output = PullOutput(selected_.transform.Get(),
|
|
output_info_);
|
|
LONGLONG output_time = -1;
|
|
Check(output->GetSampleTime(&output_time), "output-sample-time");
|
|
if (output_time != static_cast<LONGLONG>(timestamp100ns)) {
|
|
Fail("output-order", "hardware MFT changed or reordered a live timestamp");
|
|
}
|
|
std::vector<UINT8> bytes = ReadSample(output.Get());
|
|
NalSummary nal = InspectAnnexB(bytes);
|
|
if (!nal.annex_b) {
|
|
Fail("bitstream-annexb", "live output is not Annex-B H264");
|
|
}
|
|
if (nal.profile_level_id) {
|
|
if (!profile_.accepts_h264_profile_level_id(*nal.profile_level_id)) {
|
|
Fail("bitstream-profile", "hardware MFT changed the requested H.264 profile level");
|
|
}
|
|
if (profile_level_id_ && *profile_level_id_ != *nal.profile_level_id) {
|
|
Fail("bitstream-profile-change", "hardware MFT changed SPS profile");
|
|
}
|
|
profile_level_id_ = nal.profile_level_id;
|
|
}
|
|
if (force_key_frame && !(nal.sps && nal.pps && nal.idr)) {
|
|
Fail("bitstream-recovery-unit",
|
|
"requested live recovery unit lacks SPS, PPS, or IDR");
|
|
}
|
|
if (!profile_level_id_) {
|
|
Fail("bitstream-sps", "first live access unit did not carry an SPS");
|
|
}
|
|
RequireEncoderTime(probe_deadline);
|
|
return EncodedAccessUnit{timestamp100ns, nal.idr && nal.sps && nal.pps,
|
|
std::move(bytes)};
|
|
}
|
|
Fail("mft-output-timeout", "hardware MFT did not produce live output in time");
|
|
}
|
|
|
|
MediaEventType LiveEncoder::NextEvent(
|
|
EncoderClock::time_point deadline, const char* timeout_stage) {
|
|
ComPtr<IMFMediaEvent> event = events_.Read(deadline, timeout_stage);
|
|
HRESULT status = S_OK;
|
|
Check(event->GetStatus(&status), "mft-event-status-read");
|
|
Check(status, "mft-event-status");
|
|
MediaEventType type = MEUnknown;
|
|
Check(event->GetType(&type), "mft-event-type");
|
|
if (type == MEError) Fail("mft-error-event", "hardware MFT emitted MEError");
|
|
return type;
|
|
}
|
|
|
|
void LiveEncoder::WaitForInput(EncoderClock::time_point probe_deadline) {
|
|
const auto deadline = std::min(probe_deadline,
|
|
EncoderClock::now() + std::chrono::seconds(2));
|
|
while (input_requests_ == 0) {
|
|
MediaEventType type = NextEvent(deadline, "mft-input-timeout");
|
|
if (type == METransformNeedInput) {
|
|
++input_requests_;
|
|
} else if (type == METransformHaveOutput) {
|
|
Fail("mft-unexpected-output", "hardware MFT produced output without live input");
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace piik::capture::windows
|