Skip to content

Device hot-plug, extended-length paths, VB-CABLE detection (Phase 7) - #15

Merged
revtex merged 2 commits into
mainfrom
feat/phase-7-devices-paths
Aug 14, 2026
Merged

Device hot-plug, extended-length paths, VB-CABLE detection (Phase 7)#15
revtex merged 2 commits into
mainfrom
feat/phase-7-devices-paths

Conversation

@revtex

Copy link
Copy Markdown
Owner

Completes Phase 7's remaining three items: device hot-plug handling, extended-length path support, and VB-CABLE detection.

Extended-length paths

Offstream.Core.Naming.LongPath.Extended prefixes \\?\ (or \\?\UNC\) once a path reaches 260 characters. The prefix is used rather than a longPathAware manifest because Offstream does not write the file — ffmpeg does, in its own process with its own manifest. The prefix travels with the path through ArgumentList into whatever ffmpeg hands to CreateFile. Verified rather than assumed: ffmpeg 8.1 wrote an MP3 to a 298-character destination through a prefixed path on a machine with LongPathsEnabledoff.

The prefix disables path normalisation, which is the trap — Windows stops resolving ./.., stops converting /, and stops trimming trailing dots and spaces. So it applies only to fully-qualified paths, normalised first, and only when long enough to need it.

OutputPaths now budgets against LongPath.MaxLength (32767) instead of 260, with the per-level allowance clamped to 255. Extended paths raise the total length, never the NTFS component length; without the clamp the wider budget produces folder names the filesystem refuses outright — trading "truncated at 260" for "cannot be written at all".

One existing test asserted a deep output root was too long. It is not any more, which is the feature; it now asserts the opposite, with a companion case proving the check still fires on a root with genuinely no room left.

Audio endpoint hot-plug

AudioEndpointWatcher wraps IMMNotificationClient and reports arrivals, departures and default changes. LoopbackAudioCapture takes one optionally and ends the capture when the endpoint it was reading goes away.

It reports and stops; it does not re-route. Moving a running capture to another endpoint sounds helpful and is not — the replacement can have a different sample rate and channel count, so the file in progress would gain a seam, or a tail its header does not describe. Losing the endpoint ends that recording cleanly and says why.

The two ways of losing an endpoint look different in the notifications, and AudioEndpointRelevance.EndsTheCapture is the piece with the rules in it (separated out so it needs no audio hardware to test): a pinned device is lost when that exact id goes away; a capture following the default is lost when the default changes — the old device may still exist and still enumerate, but Windows has moved playback elsewhere. That is the more confusing failure, because nothing was unplugged and watching for removals would never catch it.

Without any of this the loss is silent: WASAPI simply stops delivering and the session keeps believing it is recording. Callbacks arrive on a system thread from inside the audio stack, so the watcher raises an event and returns; OnDefaultDeviceChanged is filtered to render + multimedia, since Windows raises it per role.

VB-CABLE detection

Offstream.Core.Audio.VirtualCable matches the driver's product name among the render endpoints and exposes the vendor's download page when it is absent. Detection only, and that is a licence decision rather than an unfinished one — the package is donationware whose readme forbids integrating it into another installation procedure without the author's agreement. No vendor binaries in this repo. Open question 9 must be answered before any other form ships.

Surfacing the absence in the Settings UI is still to do, and is noted as such in the plan.

Testing

969 green (801 Core + 168 UI), dotnet format --verify-no-changes clean.

🤖 Generated with Claude Code

revtexand others added 2 commits August 13, 2026 21:06
Long paths. Offstream does not write the output file — ffmpeg does, in a
separate process with its own manifest — so a longPathAware opt-in of
ours would not reach it. The \\?\ prefix does: it travels with the path
through ArgumentList into whatever ffmpeg hands to CreateFile. Verified
rather than assumed, since the feature rests on it: ffmpeg 8.1 wrote an
MP3 to a 298-character destination through a prefixed path, on a machine
with the LongPathsEnabled registry switch off.
The prefix turns off path normalisation, which is the trap. Windows stops
resolving . and .., stops converting / to \, and stops trimming trailing
dots and spaces, so a merely untidy path becomes one the filesystem
rejects. It is applied only to fully-qualified paths, normalised first,
and only when the path is long enough to need it.
Extended paths raise the total length and not the component length, which
is the part that catches people out. The 260 budget divided across
template levels became a 32767 budget dividing into per-level allowances
of thousands of characters, which renders folder names NTFS refuses
outright — trading "truncated at 260" for "cannot be written at all". The
per-level allowance is clamped to 255.
One existing test asserted that a deep output root was too long. It is no
longer, which is the whole point, so it now asserts the opposite; a
companion case keeps the check honest by proving it still fires on a root
with genuinely no room left.
VB-CABLE detection only, and that is a licence decision rather than an
unfinished one. The package is donationware whose readme forbids
integrating it into another installation procedure without the author's
agreement. The predecessor ships the vendor's setup executables in its
own tree, launches them elevated, and displays neither the origin nor the
donationware notice the licence asks for. Offstream carries no vendor
binaries: it detects the cable by the driver's product name, as the
reference does, and sends the user to vb-audio.com when it is absent.
Plan open question 9 has to be answered before any other form ships.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Losing the endpoint mid-recording was silent. WASAPI stops delivering
buffers, the session keeps believing it is capturing, and the file that
lands is however many seconds arrived before the headphones came out.
It reports and stops rather than re-routing. Moving a running capture to
another endpoint sounds like the helpful thing and is not: the
replacement can have a different sample rate and channel count, so the
recording in progress would gain a seam mid-file or a tail encoded from a
format its header does not describe. Losing the endpoint now ends that
recording cleanly and says why; choosing a different device is the user's
call, and the next recording picks it up.
The two ways of losing an endpoint look different in the notifications,
which is the part worth getting right. A pinned device is lost when that
exact id goes away. A capture following the default is lost when the
default changes — the old device may still exist and still enumerate, but
Windows has moved playback elsewhere. That is the more confusing failure
because nothing was unplugged, and watching for removals would never
catch it.
The decision is separated from the notification plumbing: the rules need
no audio hardware to exercise, and the plumbing cannot be exercised
without it. Callbacks arrive on a system thread from inside the audio
stack, so the watcher raises an event and returns — blocking one is a
documented way to wedge the endpoint enumerator for the whole process.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@revtex
revtex merged commit 579b9a6 into mainAug 14, 2026
1 check passed
@revtex
revtex deleted the feat/phase-7-devices-paths branch August 14, 2026 01:19
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@revtex