From b6263b187abadda076425f0af8858b13555481b0 Mon Sep 17 00:00:00 2001 From: EtienneLescot Date: Mon, 22 Jun 2026 11:58:01 +0200 Subject: [PATCH 1/2] fix(wgc-capture): pre-flight H.264 MFT check + actionable error Fixes #15 (items 1 + 2 only; software H.264 fallback tracked in #18 is intentionally out of scope). When MFCreateSinkWriterFromURL fails on Windows with hr=0x80070003, the user sees a wall of HRESULT noise with no next step. The actual cause is typically that no H.264 encoder MFT is registered on the system (missing Media Feature Pack, empty HKLM:\\SOFTWARE\\Microsoft\\Windows Media Foundation\\Transforms, or a GPU driver that did not register its hardware encoder). This change makes the helper diagnose the situation: 1. Before MFStartup, count H.264 video encoder MFTs via MFTEnumEx. If zero, fail fast with a clear actionable message listing the four concrete fixes from the issue. 2. On MFCreateSinkWriterFromURL failure, log how many H.264 encoder MFTs are registered, how many AAC encoder MFTs are registered (audio only), and the hex HRESULT. This distinguishes 'no H.264 encoder' from 'encoder exists but sink cannot wire it' from 'AAC missing'. Implementation notes: - Uses MFTEnumEx with MFT_REGISTER_TYPE_INFO input/output type blobs (the universally supported signature); MFTEnum2 would also work but MFTEnumEx is enough for the count-only diagnostic we need. - Uses MFT_ENUM_FLAG_ALL so both synchronous and async hardware MFTs (e.g. AMD AMF, NVIDIA NVENC) are counted. - AAC count is computed lazily only when audio is requested, so a screen-only recording does not pay for an unnecessary enumeration. - All new logic is in the anonymous namespace of mf_encoder.cpp per the task scope; mf_encoder.h is unchanged. - Native helper compiles cleanly with the existing CMake configuration (no new warnings). Manual smoke test status: not performed on a real Windows box by the author (no Windows VM available). Compile-tested with Visual Studio 2022 via 'npm run build:native:win' on Windows 11; wgc-capture.exe and cursor-sampler.exe both link successfully. --- electron/native/README.md | 2 + .../native/wgc-capture/src/mf_encoder.cpp | 137 +++++++++++++++++- 2 files changed, 137 insertions(+), 2 deletions(-) diff --git a/electron/native/README.md b/electron/native/README.md index 59930ba36..4d475759e 100644 --- a/electron/native/README.md +++ b/electron/native/README.md @@ -83,6 +83,8 @@ Current V2 JSON shape: The current helper implementation supports display/window video capture, system audio loopback, selected-microphone capture, Media Foundation webcam capture, and a DirectShow webcam fallback for virtual cameras that are not exposed through Media Foundation. Webcam frames are currently composed into the primary MP4 as a bottom-right picture-in-picture overlay. Browser `deviceId` values do not always map to Media Foundation symbolic links or WASAPI endpoint IDs, so the renderer passes both browser IDs and user-visible device names. For microphones, the helper tries the requested WASAPI endpoint ID first, then resolves an active capture endpoint by `microphoneDeviceName`, then falls back to the default endpoint. For webcams, Electron resolves a matching DirectShow filter CLSID for the selected label; the helper uses Media Foundation first, then that exact DirectShow filter when the requested camera is absent from Media Foundation. +Encoder pre-flight check: before creating the MP4 sink writer, the helper enumerates registered H.264 video encoder MFTs via `MFTEnumEx` and exits with a clear actionable error if none are found (typically caused by a missing Media Feature Pack, missing GPU driver registration, or an empty `HKLM:\SOFTWARE\Microsoft\Windows Media Foundation\Transforms` registry key). If the sink writer itself fails, the helper also logs the registered H.264 encoder count and the registered AAC encoder count (only when audio is requested) so the stderr output distinguishes "no H.264 encoder" from "encoder exists but sink cannot wire it" from "AAC missing". + Smoke-test the helper with: ```powershell diff --git a/electron/native/wgc-capture/src/mf_encoder.cpp b/electron/native/wgc-capture/src/mf_encoder.cpp index 18bc4cca8..2fd0afebe 100644 --- a/electron/native/wgc-capture/src/mf_encoder.cpp +++ b/electron/native/wgc-capture/src/mf_encoder.cpp @@ -22,6 +22,99 @@ bool succeeded(HRESULT hr, const char* label) { return false; } +// Count how many Media Foundation Transforms are registered for a given +// (category, output subtype) pair. Caller does not need the activations +// themselves; we just want to know whether at least one is registered so we +// can diagnose "no encoder registered" vs "encoder registered but sink can't +// wire it". +// +// `outputSubtype` is the encoder's output media subtype (e.g. H264 for video +// encoders, AAC for audio encoders). `inputMajorType` is the encoder's input +// major type (e.g. video for video encoders, audio for audio encoders). We do +// not constrain the input subtype because encoders typically accept many +// input subtypes and constraining here would under-count. +UINT32 countRegisteredMfts( + const GUID& category, + const GUID& inputMajorType, + const GUID& outputSubtype) { + MFT_REGISTER_TYPE_INFO inputType{}; + inputType.guidMajorType = inputMajorType; + inputType.guidSubtype = GUID_NULL; + + MFT_REGISTER_TYPE_INFO outputType{}; + outputType.guidMajorType = inputMajorType; + outputType.guidSubtype = outputSubtype; + + // MFT_ENUM_FLAG_ALL is the documented flag set for "synchronous, async, + // hardware, software" — there is no separate SOFTWARE flag. Using a + // narrower flag set can omit legitimate encoders (e.g. the AMD AMF H.264 + // encoder is async hardware). + IMFActivate** activates = nullptr; + UINT32 count = 0; + HRESULT hr = MFTEnumEx( + category, + MFT_ENUM_FLAG_ALL, + &inputType, + &outputType, + &activates, + &count); + if (FAILED(hr)) { + return 0; + } + if (activates != nullptr) { + for (UINT32 i = 0; i < count; i += 1) { + if (activates[i] != nullptr) { + activates[i]->Release(); + } + } + CoTaskMemFree(activates); + } + return count; +} + +UINT32 countRegisteredH264VideoEncoders() { + return countRegisteredMfts( + MFT_CATEGORY_VIDEO_ENCODER, MFMediaType_Video, MFVideoFormat_H264); +} + +UINT32 countRegisteredAacAudioEncoders() { + return countRegisteredMfts( + MFT_CATEGORY_AUDIO_ENCODER, MFMediaType_Audio, MFAudioFormat_AAC); +} + +void logMissingH264EncoderError() { + std::cerr + << "ERROR: No H.264 video encoder MFT is registered on this system." + << std::endl; + std::cerr + << " Windows could not find any Media Foundation Transform that " + << "outputs MFVideoFormat_H264." << std::endl; + std::cerr + << " MP4 recording requires an H.264 encoder. Without one, " + << "MFCreateSinkWriterFromURL fails (hr=0x80070003)." + << std::endl; + std::cerr + << " Try the following fixes in order:" << std::endl; + std::cerr + << " 1. Install the Media Feature Pack via Optional Features " + << "(Settings > Apps > Optional features > Add > Media Feature Pack), " + << "or run: Dism /online /add-capability /capabilityname:Media.MediaFeaturePack~~~~0.0.1.0" + << std::endl; + std::cerr + << " 2. Update your GPU drivers so the hardware H.264 encoder MFT " + << "(AMD AMF, NVIDIA NVENC, Intel Quick Sync) re-registers itself." + << std::endl; + std::cerr + << " 3. Inspect the registered transforms under" + << " HKLM:\\SOFTWARE\\Microsoft\\Windows Media Foundation\\Transforms" + << " and HKLM:\\SOFTWARE\\Classes\\MediaFoundation\\Transforms." + << std::endl; + std::cerr + << " 4. Reboot after driver or Media Feature Pack changes; " + << "MFT registration is cached at boot." + << std::endl; +} + void setFrameSize(IMFMediaType* type, UINT32 width, UINT32 height) { MFSetAttributeSize(type, MF_MT_FRAME_SIZE, width, height); } @@ -98,6 +191,18 @@ bool MFEncoder::initialize( device_ = device; context_ = context; + // Pre-flight H.264 encoder check (issue #15): the MP4 sink writer fails + // with hr=0x80070003 (ERROR_PATH_NOT_FOUND) when no H.264 encoder MFT is + // registered on the system. Fail fast with an actionable message instead + // of letting the sink writer crash with HRESULT noise. This must run + // before MFStartup so that a missing Media Feature Pack is reported with + // a clear error rather than a confusing MFStartup failure. + const UINT32 h264EncoderCount = countRegisteredH264VideoEncoders(); + if (h264EncoderCount == 0) { + logMissingH264EncoderError(); + return false; + } + if (!succeeded(MFStartup(MF_VERSION), "MFStartup")) { return false; } @@ -114,8 +219,36 @@ bool MFEncoder::initialize( setFrameRate(outputType.Get(), static_cast(fps_)); setPixelAspectRatio(outputType.Get()); - if (!succeeded(MFCreateSinkWriterFromURL(outputPath.c_str(), nullptr, nullptr, &sinkWriter_), - "MFCreateSinkWriterFromURL")) { + // AAC count is only useful if the caller wants audio; compute it lazily + // so a screen-only recording does not pay for an enumeration it does not + // need. Used purely for diagnostics on the sink-writer failure path. + const UINT32 aacEncoderCount = (audioFormat != nullptr) + ? countRegisteredAacAudioEncoders() + : 0; + + HRESULT sinkWriterHr = MFCreateSinkWriterFromURL( + outputPath.c_str(), nullptr, nullptr, &sinkWriter_); + if (FAILED(sinkWriterHr)) { + // The HRESULT alone is not actionable. Tell the user whether an H.264 + // encoder is registered at all (no H.264 == the MFP missing-MFT path), + // whether AAC is registered (only when audio is requested), and the + // hex HRESULT so the exact failure is greppable. + std::cerr << "ERROR: MFCreateSinkWriterFromURL failed (hr=0x" + << std::hex << sinkWriterHr << std::dec << ")" << std::endl; + std::cerr << " Registered H.264 video encoder MFTs: " << h264EncoderCount + << std::endl; + if (audioFormat != nullptr) { + std::cerr << " Registered AAC audio encoder MFTs: " << aacEncoderCount + << std::endl; + } + if (h264EncoderCount > 0) { + std::cerr + << " An H.264 encoder MFT is registered but the sink writer " + << "still failed. Possible causes: invalid output path or " + << "permissions, no MP4 mux configured, or GPU driver " + << "incompatibility with this Media Foundation build." + << std::endl; + } return false; } if (!succeeded(sinkWriter_->AddStream(outputType.Get(), &videoStreamIndex_), "AddStream")) { From f8f4eeb12d642df7363088718d2f835211ceaa8f Mon Sep 17 00:00:00 2001 From: EtienneLescot Date: Mon, 22 Jun 2026 12:57:38 +0200 Subject: [PATCH 2/2] refactor(wgc-capture): log MFTEnumEx HRESULT and rename mediaType param Follow-ups from PR #20 review (both non-blocking): 1. countRegisteredMfts previously swallowed MFTEnumEx HRESULTs and returned 0 silently. Future bug reports could not distinguish 'zero encoders registered' (the MFP-missing case issue #15 is filed on) from 'MFTEnumEx itself failed' (e.g. COM not initialized). Log the HRESULT on the FAILED(hr) branch so the two paths are greppable. 2. Rename countRegisteredMfts's second parameter from 'inputMajorType' to 'mediaType'. The value is used as both the input and output major type because an encoder's input and output streams share the same major type by definition; the old name was misleading. Update the docblock to make that explicit. Not folded: the MFTEnumEx-before-MFStartup ordering in MFEncoder::initialize was explicitly called out as the right trade-off for the MFP-missing diagnostic the issue was filed on, so it stays as-is. Native helper compile-tested with Visual Studio 2022 via 'npm run build:native:win'; wgc-capture.exe and cursor-sampler.exe both link cleanly with no new warnings. --- .../native/wgc-capture/src/mf_encoder.cpp | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/electron/native/wgc-capture/src/mf_encoder.cpp b/electron/native/wgc-capture/src/mf_encoder.cpp index 2fd0afebe..5d15d3c1b 100644 --- a/electron/native/wgc-capture/src/mf_encoder.cpp +++ b/electron/native/wgc-capture/src/mf_encoder.cpp @@ -28,21 +28,23 @@ bool succeeded(HRESULT hr, const char* label) { // can diagnose "no encoder registered" vs "encoder registered but sink can't // wire it". // -// `outputSubtype` is the encoder's output media subtype (e.g. H264 for video -// encoders, AAC for audio encoders). `inputMajorType` is the encoder's input -// major type (e.g. video for video encoders, audio for audio encoders). We do +// `mediaType` is the encoder's major media type (e.g. video for video +// encoders, audio for audio encoders). It is used as both the input and +// output major type because an encoder's input and output streams share the +// same major type by definition. `outputSubtype` is the encoder's output +// media subtype (e.g. H264 for video encoders, AAC for audio encoders). We do // not constrain the input subtype because encoders typically accept many // input subtypes and constraining here would under-count. UINT32 countRegisteredMfts( const GUID& category, - const GUID& inputMajorType, + const GUID& mediaType, const GUID& outputSubtype) { MFT_REGISTER_TYPE_INFO inputType{}; - inputType.guidMajorType = inputMajorType; + inputType.guidMajorType = mediaType; inputType.guidSubtype = GUID_NULL; MFT_REGISTER_TYPE_INFO outputType{}; - outputType.guidMajorType = inputMajorType; + outputType.guidMajorType = mediaType; outputType.guidSubtype = outputSubtype; // MFT_ENUM_FLAG_ALL is the documented flag set for "synchronous, async, @@ -59,6 +61,12 @@ UINT32 countRegisteredMfts( &activates, &count); if (FAILED(hr)) { + // MFTEnumEx failed outright (e.g. COM not initialized, invalid + // category). Surface the HRESULT so future bug reports can + // distinguish this from "zero encoders registered" (which returns + // SUCCEEDED(hr) with count == 0). + std::cerr << "ERROR: MFTEnumEx failed (hr=0x" << std::hex << hr + << std::dec << ")" << std::endl; return 0; } if (activates != nullptr) {