<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Zephyrwr6k</title>
    <description>The latest articles on DEV Community by Zephyrwr6k (@zephyrwr6k).</description>
    <link>https://dev.to/zephyrwr6k</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4242322%2Fcfdc8baa-7e27-467f-8768-c18c4ba436c4.jpg</url>
      <title>DEV Community: Zephyrwr6k</title>
      <link>https://dev.to/zephyrwr6k</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zephyrwr6k"/>
    <language>en</language>
    <item>
      <title>When a user closes the tab, who settles the live AI session?</title>
      <dc:creator>Zephyrwr6k</dc:creator>
      <pubDate>Fri, 13 Nov 2026 14:18:34 +0000</pubDate>
      <link>https://dev.to/zephyrwr6k/when-a-user-closes-the-tab-who-settles-the-live-ai-session-57en</link>
      <guid>https://dev.to/zephyrwr6k/when-a-user-closes-the-tab-who-settles-the-live-ai-session-57en</guid>
      <description>&lt;p&gt;A user starts a live camera session, runs it for half a minute, and closes the tab. When they come back, the balance can still show the credits set aside at the start. The browser is gone, but the server still has a session to close and an amount to reconcile.&lt;/p&gt;

&lt;p&gt;I encountered this while building LiveFaceSwap, a browser studio that meters live video by the second. A camera preview can stop before the billing record catches up. I wanted the server to settle from the usage recorded for that session and return the rest of the reservation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set a ceiling before opening the stream
&lt;/h2&gt;

&lt;p&gt;At the start, the server reserves up to the amount the user can spend on a single session. It writes the ledger debit and the new session together, with a balance check on the debit. The reservation also sets the budget for the live connection. LiveFaceSwap currently meters at 0.6 credits per second, with a 900-second session cap.&lt;/p&gt;

&lt;p&gt;Here is an example. With 60 credits reserved, the budget covers 100 seconds. If the service reports 32 seconds used, the final charge is 20 credits. That's 19.2 rounded up to a whole credit. The remaining 40 go back to the balance. The numbers illustrate the calculation and are independent of any real account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give the browser a fast path
&lt;/h2&gt;

&lt;p&gt;The normal &lt;strong&gt;Stop&lt;/strong&gt; button closes the live connection and sends an authenticated request to end the session. The server closes the metered connection, reads its usage, and returns the charge and updated balance when settlement succeeds.&lt;/p&gt;

&lt;p&gt;Closing the tab is a different path through the same endpoint. The studio listens for &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/pagehide_event" rel="noopener noreferrer"&gt;&lt;code&gt;pagehide&lt;/code&gt;&lt;/a&gt; and attempts a small POST using &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Request/keepalive" rel="noopener noreferrer"&gt;&lt;code&gt;fetch&lt;/code&gt; with &lt;code&gt;keepalive&lt;/code&gt;&lt;/a&gt;. The browser can keep that request alive after the page unloads. A mobile OS can also kill the browser without delivering the event, so the server has to handle a session it never gets asked to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give the server a way to catch missed requests
&lt;/h2&gt;

&lt;p&gt;A &lt;a href="https://developers.cloudflare.com/workers/configuration/cron-triggers/" rel="noopener noreferrer"&gt;scheduled Cloudflare Worker&lt;/a&gt; looks for sessions past their deadline and calls the same settlement logic. The current schedule runs every five minutes. That interval describes how often the sweep starts. An abandoned session must first become eligible for cleanup, and settlement can take longer if usage lookup fails.&lt;/p&gt;

&lt;p&gt;The server calculates the charge from the metering service's reported usage. It rounds to a whole credit and caps the charge at the reservation. When two requests try to settle the same session, only the one that changes it from active to settled issues the refund. The ledger also uses a unique refund key for the session. Those checks address repeated requests. The state update and refund write are separate operations, so recovery from a failure between them deserves its own test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the calculation and the delivery paths separately
&lt;/h2&gt;

&lt;p&gt;The local unit tests passed for rounding, the reservation cap, and invalid inputs. To check the full flow in a test account, I'd compare the session record and ledger after three cases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Stop&lt;/strong&gt; and confirm that the returned balance matches the charge.&lt;/li&gt;
&lt;li&gt;Close the tab with the end request blocked, then check that the sweep settles the session after its deadline.&lt;/li&gt;
&lt;li&gt;Send the end request twice and confirm there's only one refund in the ledger.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The next time I build a metered browser feature, I'll start with those three cases. The client needs a fast way to say it is done. The server needs enough information to finish the accounting on its own.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI assistance was used to draft and edit this article from the implementation and public documentation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>javascript</category>
    </item>
    <item>
      <title>My AI video worked in the browser. Getting it into OBS was a different problem.</title>
      <dc:creator>Zephyrwr6k</dc:creator>
      <pubDate>Fri, 13 Nov 2026 06:44:07 +0000</pubDate>
      <link>https://dev.to/zephyrwr6k/my-ai-video-worked-in-the-browser-getting-it-into-obs-was-a-different-problem-1g77</link>
      <guid>https://dev.to/zephyrwr6k/my-ai-video-worked-in-the-browser-getting-it-into-obs-was-a-different-problem-1g77</guid>
      <description>&lt;p&gt;I’m building LiveFaceSwap, a web studio that takes a webcam feed and a reference image, then shows the transformed video next to the original. You can see the result in the browser as you move. Open the camera menu in another app, though, and you’ll see your physical webcam. The transformed video is still sitting inside the browser.&lt;/p&gt;

&lt;p&gt;That gap turned into a separate engineering problem. The model had already done its job. I still needed to get the right frames to the software people use to record, stream, or join a call.&lt;/p&gt;

&lt;h2&gt;
  
  
  OBS gave me a useful first version
&lt;/h2&gt;

&lt;p&gt;For recording or streaming, OBS can capture a window. Point it at the browser studio, crop the scene to the processed output, and select your microphone in OBS. That gives someone a way to use the result without installing a separate LiveFaceSwap desktop app. If they need the composed scene in a call, OBS Virtual Camera can expose that scene as a camera source.&lt;/p&gt;

&lt;p&gt;I kept that route because it uses the studio that’s already running. It also gives people control over the rest of their scene. The tradeoff is that you’re capturing part of a window. Change the layout or resize the window and you may need to adjust the crop. If you want the output to show up as a named camera that another app can pick directly, you need an operating system integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  One studio, with a second place for the frames to go
&lt;/h2&gt;

&lt;p&gt;I kept the web studio inside an Electron shell. It uses the same live processing and account as the website. The extra piece is a small bridge that only exists inside the desktop app. During a live session, that bridge reads frames from the processed output and passes them to a native helper. A Windows Media Foundation camera source can then present those frames to another app as &lt;strong&gt;LiveFaceSwap Camera&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The split is useful for debugging. The studio decides what the video looks like. The bridge moves those pixels. The Windows camera source makes them available as a device. If the preview looks fine but OBS can’t find the camera, I can start with registration and device selection instead of changing the model or its prompts.&lt;/p&gt;

&lt;p&gt;There’s a simple way to catch one common setup mistake. Put an obviously different effect on the processed feed, then compare the two panes in the studio with the OBS preview. If OBS matches the raw pane, check which camera source you selected before spending time on AI settings. The microphone stays a separate input, so you can check it independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small bridge can still move a lot of data
&lt;/h2&gt;

&lt;p&gt;The current bridge targets 1280 × 720 at 30 frames per second. At four bytes per pixel, that’s roughly &lt;strong&gt;111 MB per second of raw pixel data&lt;/strong&gt; at the target cadence, before copies or other overhead. That’s the nominal size of the local handoff, not an internet speed requirement or a measured frame rate.&lt;/p&gt;

&lt;p&gt;That calculation shaped a choice in the implementation. The renderer allows one frame submission at a time. If a submission is still outstanding when the next tick comes around, it skips that tick. The native pipe also tells the sender when to wait for it to drain. In live video, an old frame loses value quickly. A queue that faithfully delivers every frame can make a camera feel further and further behind.&lt;/p&gt;

&lt;p&gt;There’s also a difference between a helper process starting and the helper being ready to accept frames. The controller waits for an explicit ready message before sending video. I can test that handshake and the binary frame format with a fake child process. Those tests catch errors at the boundary between JavaScript and native code. They don’t tell me whether a particular Windows machine will list the camera in OBS.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I can verify separately
&lt;/h2&gt;

&lt;p&gt;I think about the check in three steps now:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does the original webcam preview move?&lt;/li&gt;
&lt;li&gt;Does the processed preview move independently, with the effect applied?&lt;/li&gt;
&lt;li&gt;Does the receiving app show that processed feed, with the expected microphone selected?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each step narrows down where to look. A black browser preview can point to permissions or a busy webcam. A moving original preview with no processed video sends me to the live session. A healthy processed preview with no camera in the receiving app sends me to the capture or device path.&lt;/p&gt;

&lt;p&gt;The browser capture route is documented, and the native route is available as an &lt;strong&gt;unsigned preview for Windows 11 22H2 or later on x64&lt;/strong&gt;. I’m treating device discovery and live output in each receiving app as separate compatibility checks. That’s one of the more useful lessons from this build: I can make the demo work without having tested the place someone actually wants to use it.&lt;/p&gt;

&lt;p&gt;OBS documents both the &lt;a href="https://obsproject.com/kb/window-capture-sources" rel="noopener noreferrer"&gt;window capture source&lt;/a&gt; and &lt;a href="https://obsproject.com/kb/virtual-camera-guide" rel="noopener noreferrer"&gt;OBS Virtual Camera&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you’ve shipped a virtual camera, what check caught the most real-world bugs for you: clean-install device discovery, repeated start/stop, or letting the video run long enough to see whether it falls behind?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: This article was drafted by an AI agent from the project's source code and development notes. The project owner reviewed the draft and approved publication.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>electron</category>
      <category>video</category>
      <category>windows</category>
    </item>
  </channel>
</rss>
