The frame pump now submits crop-sized BGRA frames; FFmpeg scales them to final tier dimensions. Bundled scaler benchmark: 300 608x1080 to 1080x1920 frames in 0.435s (scaler-only).
Reference: https://ffmpeg.org/ffmpeg-filters.html#scale-1
Add independent AppLog message types and explicit Class 1/2 severity. Wrap and color log rows, pin Class 1 above Class 2, and let acknowledgement return entries to the current-launch timeline without changing file format or rotation.
Ref: https://logtape.org/manual/levels
Add Live Chat and Logs tabs to the non-edit left panel. Expose a bounded process-local AppLog feed so the newest messages appear first without importing earlier startup.log contents or changing file logging.
Ref: https://obsproject.com/forum/resources/understanding-obs-studio-log.473
Model the AlertBox and standalone ticker with shared settings; scene tickers render the in-memory active and queued events without saving history. Keep AlertBox content active-event-only.
Ref: https://obsproject.com/kb/obs-studio-overview
Keep non-owner resolution choices visible but muted. Selecting one preserves the free tier and opens About; external navigation is only the explicit llamacasty.com link. Ref: https://streamlabs.com/desktop
The earlier pass checked wiring only; this is the typography sweep. Raw Foreground/FontSize
labels are gone from the elements panel:
- Opacity, Timer (minutes) and URI are headings above their control (PropertyHeader), with the
control row on the panel's 10px indent — the same shape as Border Thickness/Opacity.
- Scroll speed, scoreboard Opacity/Text size and their value readouts are PropertyFieldLabel.
- The drag hint and the scoreboard sign-in note are PropertyHint.
- Simulate (dev) keeps its orange, gains the field-label typography.
- Edge, Text colour and Background were PropertySubHeader/SubHeader: ONE heading rank now, so
PropertySubHeader and the legacy SubHeader are deleted from Themes/Controls.xaml.
- DeveloperPanel — the last panel with raw typography and an unstyled CheckBox — now uses the
same styles, and PropertiesStyleTests accepts PropertyPillCheck (the guard went stale when
every check box became a pill switch). 18 tests pass; Release 0 warnings.
Their rank is carried by size and colour, not by an indent — so Video, Sound byte and Ticker all
line up on the left edge exactly like each other, and only the controls under them tab in.
The brand exemption for the universal blur put a BrandLogoOverlay above the blur host while the
canvas ItemsControl kept drawing the same element, so two logos sat on top of each other.
- The canvas presenter for a brand logo is collapsed: BrandLogoOverlay is the only render, and it
now carries the element's Opacity so the canvas copy is not missed.
- Hit-testing follows the eye: a click inside the logo's bounds selects the logo, because it is the
topmost layer in the compositor too. Without that you saw the logo on top and grabbed the webcam
underneath it.
- SelectedLayerPreviewHitTestingTests updated to the new contract (collapsed presenter, logo wins
its bounds, list selection still works). 3 tests pass; Release 0 warnings.
The preview draws two background images — BackgroundImage and, above it, ActiveBackgroundImage
(the static art) — while the mini-pane reads the scene snapshot. The default-restore path only
called UpdateBackgroundImage, so the thumbnails updated and the preview did not.
It now calls UpdateActiveBackground as well.
Also adds the generic cure behind the Background bug: OnSelectedElementPropertyChanged invalidates
the scene bake for ANY property change. A static layer's pixels are cached by element identity, so
a property edit without invalidation repaints nothing and the control looks dead — the failure was
never specific to UseDefaultBackground, only first noticed there.
Background + thumbnail tests pass (21); Release 0 warnings.
Browse… writes the chosen picture into BOTH CustomBackgroundPath and AssetId, and the default
stamp bails when AssetId is already set. So flipping "Use default background" back on changed a
bool while the preview kept showing the custom image — it looked like the switch did nothing.
Turning the default on now clears the custom pointer and the asset, re-runs
StampDefaultBackgroundAsset for the scene's shipped art, and only then invalidates the bake,
updates the preview and refreshes thumbnails.
BackgroundTests pass (17); Release 0 warnings.
Review findings from a screenshot of the Stream Alerts panel, each addressed:
- "Reset to built-in" and "Browse…" were YtButton-derived (white, semi-bold, 13px), so a reset
action outranked the section header. Property-panel actions now use PropertyButton: bordered,
11px, normal weight, OnSurface text.
- Explanation text and field labels were the same muted blue-gray. PropertyHint is now
OnSurfaceDisabled, one clear step below a label.
- Groups inside a category ran together. PropertyDivider (1px OutlineVariant) separates Video,
Sound byte and Message ticker.
- The disabled loop switch was near-invisible. Disabled now keeps the label at OnSurfaceVariant
and dims the whole control to 0.55 — readable, obviously inactive.
- Switch "on" was full-strength accent, shouting about static state. The track now sits at 0.6
opacity when on, which also stops it competing with the slider's accent fill.
No new palette entries: every change uses an existing token, so DESIGN.md stays the one colour
source. Release 0 warnings; token and typography tests pass.
Creator directive 2026-11-01: the properties panels had 14 hand-rolled heading styles, 37
hand-rolled labels, four literal grey hex values, and 14 square checkboxes.
- PropertyHeader / PropertySubHeader / PropertyFieldLabel / PropertyHint in Themes/Controls.xaml
now carry the whole hierarchy: category bright and 11px, labels and hints dim, size and weight
doing the level. Colour groups the panel — bright for what you edit, dim for its labels and
help — and no new palette entry was needed; DESIGN.md stays the only colour source.
- Every checkbox in the panel is a pill switch (PropertyPillCheck, 32x16 animated thumb).
IsChecked semantics unchanged, so no binding moved.
- Literal #888/#666/#555 became OnSurfaceVariant / OnSurfaceDisabled.
- 13 headings, 2 sub-headers and 31 in-row labels now reference a style instead of repeating
colour + size + alignment inline.
Controls audited for backing: every Button/Slider/ComboBox/CheckBox/ToggleButton/TextBox in the
panel has a Command or Click, a Value binding, an ItemsSource or Selected*, IsChecked, or Text —
no dead controls. Release 0 warnings; typography and token tests pass.
New `youTubeTrainingVideos.md`: the feature list worth filming, split into "ready now" and
"after the surge lands" so nothing gets filmed before it exists, plus the format decisions
settled on 2026-11-01.
Format: 16:9 long-form is the library (search intent, walkthroughs, conversion); vertical Shorts
are trailers cut from the same pass. A 16:9 video cannot be a Short, so it is a per-video choice.
TikTok is a mirror of the verticals, never a strategy.
The numbers behind it: Shorts need 1,000 subs + 10M views in 90 days for YPP versus 4,000 watch
hours in a year; Shorts RPM is $0.02-0.08 per 1,000 against $3-8 for tutorials; and one $79 sale
nets ~$55.30, which is what 700k-2M Shorts views pay. Shorts are an install engine, not revenue.
Film first: privacy blur, auto-ducking, webcam blur.
Creator ruling 2026-11-01. One SKU, one price, Steam does the selling.
- Pro is a one-time Steam DLC on the free app: $79 introductory, $99 on a stated date, announced
where the price is shown and actually raised.
- The subscription is retired. The reasoning is in decisions.md: no service behind a monthly fee
(no servers, no cloud, no content), ~1-3 subscribers per 1,000 installs against 5-8% churn, and
it carried the most expensive code in the product.
- Polar goes dormant. Its customer-portal design is recorded, not built. The "freemium on Steam,
convert on Polar, Valve gets nothing" proposal is rejected: in-app purchases must use the Steam
Wallet, so it risks the listing to save ~$24 a sale.
- Entitlement collapses to `steamDlcOwned`. No lease, no key, no portal, no device limit; the
greyed tier opens the Steam store overlay, so every purchase stays inside Valve's own surface.
- The site publishes the model and links the store page. It takes no payment and hosts no binaries.
- The $400 lifetime died with the subscription it anchored. Founding 200 died with the coupons and
passes it needed.
Costs: $115 fixed year one; $79 nets $55.30 after Steam, so three sales cover the year.
Mic and desktop gains run -60 to 0 dB and that is the ceiling: 0 dB is full input level, and
nothing past it is offered or drawn. The unity tick is gone with the zone it explained.
The faders run -60 to +6 dB but the ladder tops out at 0 dBFS, so the last 6 dB looked like
dead track. The tick marks unity at 60/66 of the track: what is left of it is boost, and now it
reads that way. Both mic and desktop faders; the lip-sync slider is milliseconds and gets none.
Creator request: a sound-byte browse + label under Built-in video, with an option to loop the
sound while the clip plays. The video itself is always one pass to completion.
- Source gains AlertAudioPath and AlertAudioLoop, persisted (column + migration + INSERT +
SELECT + reader, the whole chain — that chain is where the blur crashed once).
- The sound byte plays through MediaPlayer on the default output device; MediaEnded re-seeks to
zero while looping, and StopClip kills it so a tail cannot bleed into the next alert.
- The clip's OWN soundtrack moved off the mixer and onto the device as well
(Services/Audio/AlertAudioOutput: one WaveOutEvent + buffered float provider).
Why the move: the creator asked that alert audio — the clip's track or the added sound byte —
appear in the desktop sound bar. Only audio that reaches the default device is picked up by the
WASAPI loopback that feeds both the meter and the stream. The old EnqueueAlertAudio route put the
sound in the stream but left the meter flat and the room silent.
Tests: the sound byte and its loop flag round-trip through SQLite, and a fresh alert layer starts
with no sound byte and no loop. Release 0 warnings.
TestAlert called Enqueue, which is the pure test seam: it fills the queue and returns. The
alert layer needs three more things to show anything — its scene list, the playback ticker
running, and the preview refreshed — and all three live in OnMessageReceived, the same entry
the chat feed uses (MainViewModel.Chat.cs).
Now it calls OnMessageReceived(message, Scenes), so the clip advances on the ticker and writes
frames to the box's VideoImageSource, which is what the preview pane displays.
Sits to the right of the Built-in video pill, labelled "Test alert". It enqueues one synthetic
Super Chat through the real AlertOverlayLayer, so what plays is exactly what a viewer triggers —
the custom clip when one is set, the built-in clip otherwise — and it goes through the same
output path (preview and encoder) as a live alert. Local only: nothing reaches YouTube and no
chat row is invented.
MIC's device name was the only header label at full-strength OnSurface (it was brightened
because it shows a choice), and the lip-sync reading was Tertiary — three different values
across four bays in the same row. All four header labels are now Brush.OnSurfaceVariant, the
standard secondary-text token. No other colours touched.
Every bay is now header / ladder / controls. Lip-sync has no ladder, so it holds an empty 20px
row (10px meter + the two margins) that keeps its slider exactly level with the other three,
and MIC's ladder gained the missing 4px bottom margin that had its slider sitting 4px high.
Presets stay under the slider in the narrow lip-sync bay.
Not under it: the offset slider now rides the header line beside the LIP-SYNC badge and the
reading, and the preset row is what stays below. The box is narrow, so a slider on its own line
was the only thing making it two-and-a-bit rows tall.
GenerateBuildStamp read BuildCounter.txt, added 1 and wrote it back on every CoreCompile, so
the number tracked BUILDS: a day of iteration reached #1122 against 563 commits. It now uses
git rev-list count of HEAD, which is what the comment always claimed (and why the file existed
in .gitignore). No counter file, no drift; a missing git still falls back to the start.
Also moves the MIC and lip-sync volume bars up one line: both were pinned to the floor of their
bay by a star row plus VerticalAlignment=Bottom. They now sit directly under their headers,
with the auto rows sized to content.
Capping the three bay meters at 320px and left-aligning them made each ladder look cut off
mid-bay, which is what the creator saw as deleted bars. The caps are gone; the master ladder
keeps its own 420-900px range so it stays the longest without shortening anything else.
The master meter was a fixed 420px, but each sound bay's ladder fills its column — on a wide
window a bay could be longer than the master, which is the opposite of what was asked.
Master now stretches between 420 and 760 px, and the three bay ladders cap at 320 with the same
cap for each, so they stay the same relative size to one another and stay shorter than the
master at every window width.
Creator rulings, 2026-11-01.
- Master strip is 44px (from 36) and its meter is now the largest on screen: 420x14 with 32
segments, centred under the preview, with a 12px MASTER label — bigger than the 9px bay
labels. L/R imbalance is a flag on the left, MONO and the session clock on the right.
- The kbps / fps / dropped counters are gone from that line: the live readouts already sit
above the preview, and the duplicate was noise rather than telemetry.
- TRAX is its own bay (MIC | GAME | TRAX | LIP-SYNC) with its own meter bound to
TraxMeterFraction. It lived inside the game bay with no ladder, which meant whether the bed
was actually playing could not be answered from the dock — and the player already measures
its own output.
- Lip-sync is narrow (0.45 star) since it is one slider and three presets; the three sound bays
are equal width, so their meters keep the same relative size.
AudioDockLayout moves with it (156 = 112 + 44; collapsed 44) and the layout test is updated.
TraxBayTests now asserts the opposite of the old ruling: the bay HAS a meter, and the game bay
no longer carries it.
The blur is universal, so it would swallow the logo along with everything else. The compositor
now defers every visible brand logo: skipped in both element loops, blitted immediately AFTER
the blur group, before the flash/social/ticker overlays. One logo per scene, so it is cheap.
Preview mirrors it: the staged scene's brand logo is drawn above the blur host (StagedBrandLogo),
so what you see in the editor is what the encoder gets.
Test: a blur covering the logo's area leaves the logo's pixels exactly white while the coloured
background beneath it blends.
The creator found the use case that reframes the feature: a moment of privacy while the webcam is
on. A blur that can only cover the desktop cannot do that.
- SceneCompositor applies the blur group AFTER every element is composited (still before the
flash / social bar / ticker / zoom overlays), so it covers webcam, chat, alerts — anything. Its
position in the layer list no longer affects the output, so it is added at the top and the
reorder clamp is gone.
- Supersedes the earlier "blur sits below the brand" rule: a universal blur does obscure the logo
when it covers it. Called out in frame-pipeline.md so it is not re-litigated.
- Preview mirrors it exactly: the blur host moved OUT of CanvasGrid to a sibling above it, so the
VisualBrush samples every layer (and cannot sample itself). It used to sample only the backdrop,
which is why a circle over the webcam would have shown sharp pixels in the editor and blurred
ones in the stream.
Two holes in the blur's property panel, both of which also affected other layers:
- X/Y/W/H were gated on IsWebcamSelected, so a blur (or an image, chat box or alert zone)
had no numeric position or size at all — only dragging. The block is generic: it always
edited SelectedElement's own values, so the gate was the only thing excluding them.
- Opacity showed for a blur, but the compositor's blur reads BlurAmount — the slider would
have moved a number that changes nothing in the output. Hidden for blur layers.
Blur panel is now: Box | Circle, strength, and the shared geometry fields.
The Blur item's Visibility is bound to CanAddBlur, which depends on the staged scene. The scene
setter never raised it, so the binding evaluated once (collapsed) and stayed collapsed for the
session — build 1082: the option simply wasn't there.
StagedScene now notifies CanAddBlur, with a test that pins both halves: Live true, Chat false,
and a notification on every staged-scene change.
Source.IsDraggable listed Image/Web/Chat/Alert only, so a blur had no selection box, no corner
handle, and no hit-test — the rectangle existed in the render but could not be grabbed. Adding
Blur to that list gives it the same selection chrome and resize path every placeable layer has
(drag to move, corner dot to resize, one undo snapshot per gesture).
It is selected on add (AddSource already does that), so the rectangle appears already outlined
and ready to size.