## Problem and outcome / ????? Windows Auto could reject startup because it benchmarked the only remaining VP8 path after hardware encoding failed. Start that ordinary fallback directly; keep comparison only when it can choose between usable encoders. Browser peer construction failures now reach their existing preparation/recovery owners, while constructors release tracks they acquired before throwing. Native event timeout text no longer assumes which encoder event was missing. ## Release notes ### ?? ?????????????????? - ?? Windows ????????????????????????????????????? - ????????????????????????????????????? - ?????????????????????????????? ???????????????????????????????????? ### English This patch improves sharing startup and connection recovery. - Fixes Windows automatic codec selection failing to start when an unnecessary software benchmark fails after hardware encoding is unavailable. - Fixes some browser media-connection initialization failures not reaching the existing failure or recovery flow. - Releases video tracks left behind by failed initialization and clarifies native encoder timeout diagnostics. These fixes passed local validation. Device- or network-specific sharing failures still require evidence from the affected environment. ## Verification / ?? - npm run check: 76 test files / 1,115 tests, TypeScript and production Web build passed. - Windows capture build and deterministic native checks passed, including proved-H264 retention and software-only fallback under a failed/expired optional comparison. The old implementation fails the new fallback check. - Fault-injected hardware-unavailable and exhausted-probe-budget paths both captured a synthetic window and produced decoded 1920?1080 VP8 frames, followed by clean process/port/profile retirement. - Browser regressions cover construction failures, exact connection identity, existing recovery budgets, prepared-candidate replacement and owned-track cleanup. Ordinary Host rebuild keeps the existing Viewer recovery owner. - Documentation and repository hygiene checks passed. npm run check:go-core passed; Darwin cgo remains assigned to the native macOS packaging runner. CI also verifies race checks and all platform packages. ## Impact / ?? Compatible patch release v1.6.5; no wire, storage, room authority, retry-policy or quality-adaptation change. Source diagnostics stay ignored and private. The complete v1.6.3 reporter exports narrow failure stages but do not establish the affected machine's root cause. No interaction or room-voice feature is included. A matching Server restart interrupts process-owned media; routine atomic deployment keeps the pre-cutover binary through bounded postflight.
195 lines
7.8 KiB
C++
195 lines
7.8 KiB
C++
// Exercise the production worker with a synthetic codec at its existing link
|
|
// boundary. WARP supplies only the device-status API; no screen or audio is used.
|
|
#define wmain capture_main
|
|
#include "main.cpp"
|
|
#undef wmain
|
|
|
|
#include <cassert>
|
|
#include <future>
|
|
|
|
namespace piik::capture::windows {
|
|
class AdaptiveEncoder::Impl final {
|
|
public:
|
|
explicit Impl(Factory create, VideoProfile profile) : encoder(create(profile)) {}
|
|
std::unique_ptr<VideoEncoder> encoder;
|
|
};
|
|
|
|
AdaptiveEncoder::AdaptiveEncoder(OutputKind, VideoProfile profile, ID3D11Device*,
|
|
Factory create, std::unique_ptr<VideoEncoder>, int)
|
|
: impl_(std::make_unique<Impl>(std::move(create), profile)) {}
|
|
AdaptiveEncoder::~AdaptiveEncoder() = default;
|
|
|
|
std::optional<AdaptiveAccessUnit> AdaptiveEncoder::Encode(FrameProducer,
|
|
UINT32 width, UINT32 height, UINT64 timestamp, bool key, UINT32 bitrate) {
|
|
impl_->encoder->SetBitrate(bitrate);
|
|
return AdaptiveAccessUnit{impl_->encoder->Encode(nullptr, timestamp, key),
|
|
width, height, 333'333};
|
|
}
|
|
} // namespace piik::capture::windows
|
|
|
|
class FixtureEncoder final : public VideoEncoder {
|
|
public:
|
|
FixtureEncoder(std::promise<void>* entered, std::shared_future<void> released)
|
|
: VideoEncoder(OutputKind::h264, "fixture", "fixture"),
|
|
entered_(entered), released_(std::move(released)) {}
|
|
|
|
EncodedAccessUnit Encode(ID3D11Texture2D*, UINT64 timestamp, bool key,
|
|
EncoderClock::time_point) override {
|
|
if (entered_) {
|
|
entered_->set_value();
|
|
released_.wait();
|
|
throw GateFailure("fixture-encode", "synthetic encoder failed", E_FAIL);
|
|
}
|
|
return {timestamp, key, {1, 2, 3}};
|
|
}
|
|
void SetBitrate(UINT32) override {}
|
|
|
|
private:
|
|
std::promise<void>* entered_;
|
|
std::shared_future<void> released_;
|
|
};
|
|
|
|
void CheckWorkerFailureGeneration(ID3D11Device* device, bool replace) {
|
|
std::promise<void> entered, release, output, failure;
|
|
auto began = entered.get_future();
|
|
auto released = release.get_future().share();
|
|
auto delivered = output.get_future();
|
|
auto failed = failure.get_future();
|
|
std::atomic<unsigned> created = 0;
|
|
HANDLE sink = CreateFileW(L"NUL", GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
|
|
assert(sink != INVALID_HANDLE_VALUE);
|
|
ProtocolWriter writer(sink);
|
|
OutputWorker worker(0, VideoProfile{}, OutputKind::h264, device, writer,
|
|
[&](const VideoProfile&) -> std::unique_ptr<VideoEncoder> {
|
|
return std::make_unique<FixtureEncoder>(created++ == 0 ? &entered : nullptr, released);
|
|
}, nullptr, true, [&] { output.set_value(); },
|
|
[&](std::exception_ptr error) {
|
|
try { std::rethrow_exception(error); }
|
|
catch (const GateFailure& actual) { assert(actual.result() == E_FAIL); }
|
|
failure.set_value();
|
|
});
|
|
auto input = std::make_shared<CaptureInput>();
|
|
input->width = 1280;
|
|
input->height = 720;
|
|
input->timestamp = 333'333;
|
|
worker.Submit(input);
|
|
assert(began.wait_for(std::chrono::seconds(2)) == std::future_status::ready);
|
|
if (replace) {
|
|
worker.SetActive(false);
|
|
assert(worker.SetActive(true));
|
|
worker.Submit(input);
|
|
}
|
|
release.set_value();
|
|
if (replace) {
|
|
assert(delivered.wait_for(std::chrono::seconds(2)) == std::future_status::ready);
|
|
assert(failed.wait_for(std::chrono::milliseconds(0)) == std::future_status::timeout);
|
|
assert(created == 2);
|
|
} else {
|
|
assert(failed.wait_for(std::chrono::seconds(2)) == std::future_status::ready);
|
|
assert(delivered.wait_for(std::chrono::milliseconds(0)) == std::future_status::timeout);
|
|
assert(created == 1);
|
|
assert(!worker.SetActive(false) && !worker.SetActive(true));
|
|
}
|
|
worker.Stop();
|
|
worker.Join();
|
|
CloseHandle(sink);
|
|
}
|
|
|
|
void CheckPrimaryFailureDetails() {
|
|
std::ostringstream output;
|
|
auto previous = std::cerr.rdbuf(output.rdbuf());
|
|
assert(OutputFailureDetail(3, std::make_exception_ptr(
|
|
GateFailure("fixture-output", "original cause", E_ACCESSDENIED))) == "fixture-output");
|
|
std::cerr.rdbuf(previous);
|
|
const auto text = output.str();
|
|
assert(text.find("layer=3") != std::string::npos);
|
|
assert(text.find("original cause") != std::string::npos);
|
|
assert(text.find("hresult=0x80070005") != std::string::npos);
|
|
// With no COM apartment, the real decoder fails at its first API boundary.
|
|
bool rejected = false;
|
|
try { piik::capture::H264Decoder decoder(nullptr); }
|
|
catch (const GateFailure& error) {
|
|
rejected = error.stage() == "h264-decoder-create" && error.result() == CO_E_NOTINITIALIZED;
|
|
}
|
|
assert(rejected);
|
|
}
|
|
|
|
void CheckEncoderCandidates() {
|
|
const std::vector<EncoderCandidate> candidates{{0, 0}, {1, 0}, {1, 1}, {2, 0}};
|
|
const EncoderCandidate preferred{1, 0};
|
|
std::vector<EncoderCandidate> tried;
|
|
const auto deadline = EncoderClock::now() + std::chrono::seconds(1);
|
|
const auto chosen = SelectEncoderCandidate(candidates, preferred, deadline,
|
|
[&](EncoderCandidate candidate) {
|
|
tried.push_back(candidate);
|
|
if (candidate != EncoderCandidate{1, 1})
|
|
throw GateFailure("fixture-activation", "candidate is not usable", E_FAIL);
|
|
});
|
|
assert((chosen == EncoderCandidate{1, 1}));
|
|
assert((tried == std::vector<EncoderCandidate>{{1, 0}, {0, 0}, {1, 1}}));
|
|
tried.clear();
|
|
try {
|
|
SelectEncoderCandidate(candidates, chosen, deadline, [&](EncoderCandidate candidate) {
|
|
tried.push_back(candidate);
|
|
throw GateFailure("fixture-all-failed", "no working candidate", E_FAIL);
|
|
});
|
|
assert(false);
|
|
} catch (const GateFailure& error) {
|
|
assert(error.stage() == "fixture-all-failed");
|
|
}
|
|
assert(tried.size() == candidates.size() && tried.front() == chosen);
|
|
tried.clear();
|
|
try {
|
|
SelectEncoderCandidate(candidates, preferred, EncoderClock::now(),
|
|
[&](EncoderCandidate candidate) { tried.push_back(candidate); });
|
|
assert(false);
|
|
} catch (const GateFailure& error) {
|
|
assert(error.stage() == "codec-probe-timeout");
|
|
}
|
|
assert(tried.empty());
|
|
}
|
|
|
|
void CheckAutoEncoderFallback() {
|
|
const auto deadline = EncoderClock::time_point::max();
|
|
const auto expired = EncoderClock::time_point::min();
|
|
auto hardware = [] { return std::make_unique<FixtureEncoder>(nullptr, std::shared_future<void>{}); };
|
|
for (const auto probe_deadline : {deadline, expired}) {
|
|
auto initial = hardware();
|
|
const auto* proven = initial.get();
|
|
bool probed = false;
|
|
const auto selected = CompareSoftwareEncoder(std::move(initial), .04, probe_deadline, [&]() -> double {
|
|
probed = true;
|
|
// A slow software comparison exhausts its budget after H264 succeeded.
|
|
RequireEncoderTime(expired);
|
|
return 0;
|
|
});
|
|
assert(selected.kind == OutputKind::h264 && selected.initial.get() == proven);
|
|
assert(probed == (probe_deadline == deadline));
|
|
}
|
|
for (const double software_work : {.02, .04, .06}) {
|
|
const auto selected = CompareSoftwareEncoder(hardware(), .04, deadline, [&] { return software_work; });
|
|
assert((selected.kind == OutputKind::vp8) == (software_work <= .04));
|
|
assert(static_cast<bool>(selected.initial) == (software_work > .04));
|
|
}
|
|
for (const auto probe_deadline : {deadline, expired}) {
|
|
bool probed = false;
|
|
const auto software_only = CompareSoftwareEncoder(nullptr, std::nullopt, probe_deadline, [&]() -> double {
|
|
probed = true;
|
|
throw GateFailure("codec-probe-timeout", "Optional comparison is unavailable");
|
|
});
|
|
// With no hardware candidate to compare, use the same VP8 path as manual selection.
|
|
assert(software_only.kind == OutputKind::vp8 && !software_only.initial && !probed);
|
|
}
|
|
}
|
|
|
|
int main() {
|
|
CheckEncoderCandidates();
|
|
CheckAutoEncoderFallback();
|
|
CheckPrimaryFailureDetails();
|
|
ComPtr<ID3D11Device> device;
|
|
Check(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_WARP, nullptr, 0, nullptr, 0,
|
|
D3D11_SDK_VERSION, &device, nullptr, nullptr), "fixture-warp");
|
|
CheckWorkerFailureGeneration(device.Get(), false);
|
|
CheckWorkerFailureGeneration(device.Get(), true);
|
|
}
|