<?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: Yasin Özmen</title>
    <description>The latest articles on DEV Community by Yasin Özmen (@yasin_zmen_2d39354b410f6).</description>
    <link>https://dev.to/yasin_zmen_2d39354b410f6</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%2F2504934%2Fc8e14954-24c4-40fb-aab1-dc00f88e2c5b.jpg</url>
      <title>DEV Community: Yasin Özmen</title>
      <link>https://dev.to/yasin_zmen_2d39354b410f6</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yasin_zmen_2d39354b410f6"/>
    <language>en</language>
    <item>
      <title>The macOS play/pause key goes to the wrong app, so I built a router for it</title>
      <dc:creator>Yasin Özmen</dc:creator>
      <pubDate>Sun, 13 Sep 2026 09:27:59 +0000</pubDate>
      <link>https://dev.to/yasin_zmen_2d39354b410f6/the-macos-playpause-key-goes-to-the-wrong-app-so-i-built-a-router-for-it-1i0k</link>
      <guid>https://dev.to/yasin_zmen_2d39354b410f6/the-macos-playpause-key-goes-to-the-wrong-app-so-i-built-a-router-for-it-1i0k</guid>
      <description>&lt;p&gt;A YouTube tab is playing in Brave. I press play/pause on the keyboard. The video keeps going and Apple Music opens on top of everything, playing a song I did not ask for.&lt;/p&gt;

&lt;p&gt;I did this for years. Close Music, click the tab, press space. Then one evening I opened Console to find out why, and ended up writing SmartPause, a small menu bar app that sends the key to the app that is actually making sound. Source is on GitHub: &lt;a href="https://github.com/yasinozmeen/smartpause" rel="noopener noreferrer"&gt;github.com/yasinozmeen/smartpause&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is what I learned about media keys on macOS, and the four things that cost me the most time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the key actually goes
&lt;/h2&gt;

&lt;p&gt;macOS keeps one "Now Playing" slot. Whichever app last registered with the MediaRemote framework owns it, and the play/pause key goes there. Browsers register when a tab starts playing, then lose the slot to another app, or keep it after the tab is gone. From the outside you cannot tell which.&lt;/p&gt;

&lt;p&gt;When the slot is empty, macOS falls back to Apple Music. That is the "Music opens by itself" moment. The key is routed by memory, not by sound.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask Core Audio instead
&lt;/h2&gt;

&lt;p&gt;Since macOS 14.2 Core Audio exposes the list of audio processes as public API, with a property that says whether each one is outputting right now. No microphone permission, no screen recording, and nothing runs until you press a key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;runningOutputProcesses&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;AudioProcess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kAudioHardwarePropertyProcessObjectList&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UInt32&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;sys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;AudioObjectID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kAudioObjectSystemObject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="kt"&gt;AudioObjectGetPropertyDataSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;noErr&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;AudioObjectID&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="nv"&gt;repeating&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="kt"&gt;MemoryLayout&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;AudioObjectID&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="kt"&gt;AudioObjectGetPropertyData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;noErr&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compactMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="nf"&gt;guard&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kAudioProcessPropertyIsRunningOutput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;UInt32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kAudioProcessPropertyPID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Int32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="c1"&gt;// bundle id, responsible pid, name ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The whole call takes about 14 ms. That is the core of the app. Everything below is the part that made it actually work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #1: Core Audio keeps saying "playing" after you pause
&lt;/h2&gt;

&lt;p&gt;Pause Spotify and &lt;code&gt;kAudioProcessPropertyIsRunningOutput&lt;/code&gt; stays true for a few seconds, sometimes longer. The audio unit is still open; it just outputs silence. If you trust that flag you will "pause" an app that is already paused, which for Spotify's &lt;code&gt;playpause&lt;/code&gt; command means resuming it.&lt;/p&gt;

&lt;p&gt;So the process list is a hint. The app itself has the answer, and every app exposes it differently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight applescript"&gt;&lt;code&gt;&lt;span class="k"&gt;tell&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;application&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Spotify"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;player&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;playing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as &lt;/span&gt;&lt;span class="nc"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Browsers have no player state, but they do have the DOM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight applescript"&gt;&lt;code&gt;&lt;span class="k"&gt;tell&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;application&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Brave Browser"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="k"&gt;tell&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;active&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;tab&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;front&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;execute&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;javascript&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[...document.querySelectorAll('video,audio')].some(e=&amp;gt;!e.paused)"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;tell&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;tell&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That needs "Allow JavaScript from Apple Events" enabled in the browser's Develop menu, once. SmartPause detects when it is off and shows how to turn it on instead of failing silently.&lt;/p&gt;

&lt;p&gt;I also stopped using toggle commands entirely. &lt;code&gt;playpause&lt;/code&gt; is a coin flip when your state is wrong; &lt;code&gt;play&lt;/code&gt; and &lt;code&gt;pause&lt;/code&gt; are not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #2: the process making sound is not the app
&lt;/h2&gt;

&lt;p&gt;Core Audio reports the process that opened the audio device. For Brave that is a GPU helper, for Safari it is &lt;code&gt;WebKit.GPU&lt;/code&gt;, and neither has a bundle ID you can script. The helper's parent is not the app either.&lt;/p&gt;

&lt;p&gt;There is a libSystem function that TCC itself uses to answer "which app is responsible for this pid":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;typealias&lt;/span&gt; &lt;span class="kt"&gt;Fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;@convention&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid_t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pid_t&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;sym&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dlsym&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;dlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;RTLD_NOW&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"responsibility_get_pid_responsible_for_pid"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;responsible&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;unsafeBitCast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sym&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Fn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;helperPID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is undocumented, so it lives in one file behind a fallback that returns the original pid if the symbol is missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Catching the key
&lt;/h2&gt;

&lt;p&gt;Media keys arrive as &lt;code&gt;NX_SYSDEFINED&lt;/code&gt; events (type 14) with subtype 8. A session-level &lt;code&gt;CGEventTap&lt;/code&gt; sees them before macOS does; returning &lt;code&gt;nil&lt;/code&gt; from the callback swallows the key. This needs the Accessibility permission, the only one the app asks for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="n"&gt;ns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subtype&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rawValue&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;Unmanaged&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;passUnretained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;keyCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;ns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xFFFF0000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// 16 = play/pause&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;keyDown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;ns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xFF00&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mh"&gt;0x0A&lt;/span&gt;
&lt;span class="nf"&gt;guard&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;Unmanaged&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;passUnretained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reinject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;keyCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;keyCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the app decides not to handle the key (nothing is playing, or the app has no adapter) it posts the same event back into the HID stream with a marker in &lt;code&gt;eventSourceUserData&lt;/code&gt;, and the tap lets marked events through. Nothing is ever lost: worst case the key does exactly what it did before SmartPause existed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #3: a slow callback kills your tap, silently
&lt;/h2&gt;

&lt;p&gt;The first version made the AppleScript calls on the main thread, inside the tap callback. It worked for a week. Then one day media keys stopped doing anything at all, no crash, no log.&lt;/p&gt;

&lt;p&gt;A browser tab that is busy (or a browser that is showing a modal dialog) can hold an Apple Event for 120 seconds. macOS gives an event tap roughly a second to return. Miss that and you get &lt;code&gt;kCGEventTapDisabledByTimeout&lt;/code&gt; and every key goes back to the system until you re-enable the tap.&lt;/p&gt;

&lt;p&gt;Three changes fixed it for good:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The tap runs on its own thread with its own &lt;code&gt;CFRunLoop&lt;/code&gt;, so nothing on the main thread can stall it.&lt;/li&gt;
&lt;li&gt;The callback only enqueues; decisions happen on a serial &lt;code&gt;DispatchQueue&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Every AppleScript call is wrapped in a timeout.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;wrapped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"with timeout of 4 seconds&lt;/span&gt;&lt;span class="se"&gt;\n\(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="se"&gt;)\n&lt;/span&gt;&lt;span class="s"&gt;end timeout"&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;NSAppleScript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;wrapped&lt;/span&gt;&lt;span class="p"&gt;)?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executeAndReturnError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tap also re-enables itself when it sees the disabled-by-timeout event, as a last line of defence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #4: ad-hoc signatures lose the Accessibility grant
&lt;/h2&gt;

&lt;p&gt;Every &lt;code&gt;swift build&lt;/code&gt; produces a new ad-hoc signature, and macOS quietly ties the Accessibility grant to the old one. The app shows as enabled in System Settings, &lt;code&gt;AXIsProcessTrusted()&lt;/code&gt; returns true, and the tap gets nothing. Toggle the checkbox off and on and it works again.&lt;/p&gt;

&lt;p&gt;I lost an afternoon to this before finding it. If you ship without a Developer ID (I do, for now), tell users what to do when keys stop working after an update.&lt;/p&gt;

&lt;h2&gt;
  
  
  What one key should do with two apps
&lt;/h2&gt;

&lt;p&gt;The part I did not expect to spend time on. If Spotify and a YouTube tab are both playing, what should a single play/pause do?&lt;/p&gt;

&lt;p&gt;What I settled on: a single press switches. The playing app pauses, the other one resumes and moves to the top of the list. A double press plays or pauses the selected app. A small widget at the top right shows what happened and what the next press will do, slides in from the screen edge, and moves down if a macOS notification banner is in the way.&lt;/p&gt;

&lt;p&gt;When nothing is playing and nothing was paused recently, the key goes back to macOS, which starts whatever it remembers. Predicting that would need the private MediaRemote API, which is locked down since 15.4. Instead the app looks again 0.7 seconds later, sees which app started outputting, shows it in the widget and remembers it. The next press controls that app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Arc and VLC adapters exist but are untested. I do not use either.&lt;/li&gt;
&lt;li&gt;Not notarized. Right-click, Open on first launch. The Buy Me a Coffee link on the repo is for the Developer ID fee.&lt;/li&gt;
&lt;li&gt;Browsers need the "JavaScript from Apple Events" setting. Without it the key is handed back to macOS for that browser, so nothing breaks, it just is not smarter.&lt;/li&gt;
&lt;li&gt;Electron apps that play audio without the Media Session API are seen as "unknown" and passed through.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;macOS 14.2 or newer, Apple Silicon and Intel, about 2,300 lines of Swift, MIT, no analytics.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--cask&lt;/span&gt; yasinozmeen/smartpause/smartpause
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Site: &lt;a href="https://smartpause.yasinozmeen.me" rel="noopener noreferrer"&gt;smartpause.yasinozmeen.me&lt;/a&gt;. Source: &lt;a href="https://github.com/yasinozmeen/smartpause" rel="noopener noreferrer"&gt;github.com/yasinozmeen/smartpause&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you have Arc or VLC and try it, &lt;code&gt;~/Library/Logs/SmartPause.log&lt;/code&gt; in an issue is the most useful thing you can send me. Happy to answer questions about any of the above in the comments.&lt;/p&gt;

</description>
      <category>macos</category>
      <category>swift</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
