<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>MacOS on Axqora 博客</title>
    <link>https://blog.axqora.com/tags/macos/</link>
    <description>Recent content in MacOS on Axqora 博客</description>
    <image>
      <title>Axqora 博客</title>
      <url>https://blog.axqora.com/icon.png</url>
      <link>https://blog.axqora.com/icon.png</link>
    </image>
    <generator>Hugo</generator>
    <language>zh</language>
    <lastBuildDate>Fri, 08 Jul 2022 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://blog.axqora.com/tags/macos/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>群晖 NAS - frp 使用及实现 Mac 远程访问</title>
      <link>https://blog.axqora.com/posts/frp-skills/</link>
      <pubDate>Fri, 08 Jul 2022 00:00:00 +0000</pubDate>
      <guid>https://blog.axqora.com/posts/frp-skills/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/fatedier/frp&#34;&gt;frp&lt;/a&gt; 是一个专注于内网穿透的高性能的反向代理应用，支持 TCP、UDP、HTTP、HTTPS 等多种协议。可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;一服务端配置&#34;&gt;一、服务端配置&lt;/h2&gt;
&lt;h3 id=&#34;1安装及配置&#34;&gt;1、安装及配置&lt;/h3&gt;
&lt;p&gt;这里以群晖 NAS 为例：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在 Docker 注册表里搜索 frps，安装 latest 版本（也可以安装指定版本）；&lt;/li&gt;
&lt;li&gt;在映像里创建容器，并进入高级设置；&lt;/li&gt;
&lt;li&gt;在高级设置里启用自动重新启动；&lt;/li&gt;
&lt;li&gt;在存储空间添加文件，装载路径为 &lt;code&gt;/etc/frp/frps.ini&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;网络里勾选使用与 Docker Host 相同的网络；&lt;/li&gt;
&lt;li&gt;启动容器。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img alt=&#34;image-20220707211357236&#34; loading=&#34;lazy&#34; src=&#34;https://blog.axqora.com/image/frp-skills/image-20220707211357236.png&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;image-20220707211924033&#34; loading=&#34;lazy&#34; src=&#34;https://blog.axqora.com/image/frp-skills/image-20220707211924033.png&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;image-20220707211655902&#34; loading=&#34;lazy&#34; src=&#34;https://blog.axqora.com/image/frp-skills/image-20220707211655902.png&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;image-20220707211700736&#34; loading=&#34;lazy&#34; src=&#34;https://blog.axqora.com/image/frp-skills/image-20220707211700736.png&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;image-20220707211739436&#34; loading=&#34;lazy&#34; src=&#34;https://blog.axqora.com/image/frp-skills/image-20220707211739436.png&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;2frpsini-配置文件&#34;&gt;2、frps.ini 配置文件&lt;/h3&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;[common]
bind_port = 7000
token = your_token
dashboard_port = 7500
dashboard_user = your_dashboard_user
dashboard_pwd = your_dashboard_pwd
vhost_http_port = 8000
vhost_https_port = 8001
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;二客户端配置&#34;&gt;二、客户端配置&lt;/h2&gt;
&lt;h3 id=&#34;1安装及配置-1&#34;&gt;1、安装及配置&lt;/h3&gt;
&lt;p&gt;同样以群晖 NAS 为例，安装及配置基本跟服务端相同，配置文件改为 frpc.ini 即可。&lt;/p&gt;
&lt;h3 id=&#34;2frpcini-配置文件&#34;&gt;2、frpc.ini 配置文件&lt;/h3&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;[common]
server_addr = your_server_addr
server_port = 7000
tls_enable = true
token = your_token

[ssh]
type = tcp
local_ip = 192.168.100.2
local_port = 22
remote_port = 8022
use_compression = true
use_encryption = true

[chevereto]
type = tcp
local_ip = 192.168.100.2
local_port = 8090
remote_port = 8090
use_compression = true
use_encryption = true

[WebDAV_https]
type = tcp
local_ip = 192.168.100.2
local_port = 5007
remote_port = 8007
use_compression = true
use_encryption = true

[WebDAV_http]
type = tcp
local_ip = 192.168.100.2
local_port = 5005
remote_port = 8005
use_compression = true
use_encryption = true

[dsm_http]
type = tcp
local_ip = 192.168.100.2
local_port = 5000
remote_port = 8000
use_compression = true
use_encryption = true

[http]
type = http
custom_domains = your_custom_domains
local_port = 5000
use_compression = true
use_encryption = true

[https2http]
type = https
custom_domains = your_custom_domains
local_port = 5000
plugin = https2http
plugin_local_addr = 127.0.0.1:5000

plugin_crt_path = /etc/frp/cert/your_custom_domains.pem
plugin_key_path = /etc/frp/cert/your_custom_domains.key
plugin_host_header_rewrite = 127.0.0.1
plugin_header_X-From-Where = frp
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;三mac-远程访问&#34;&gt;三、Mac 远程访问&lt;/h2&gt;
&lt;p&gt;公司因安全原因，内网屏蔽了 TeamViewer、花生壳等远程桌面工具，而我时常需要在家里访问公司的 Mac 电脑。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p><a href="https://github.com/fatedier/frp">frp</a> 是一个专注于内网穿透的高性能的反向代理应用，支持 TCP、UDP、HTTP、HTTPS 等多种协议。可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网。</p>
</blockquote>
<h2 id="一服务端配置">一、服务端配置</h2>
<h3 id="1安装及配置">1、安装及配置</h3>
<p>这里以群晖 NAS 为例：</p>
<ol>
<li>在 Docker 注册表里搜索 frps，安装 latest 版本（也可以安装指定版本）；</li>
<li>在映像里创建容器，并进入高级设置；</li>
<li>在高级设置里启用自动重新启动；</li>
<li>在存储空间添加文件，装载路径为 <code>/etc/frp/frps.ini</code>；</li>
<li>网络里勾选使用与 Docker Host 相同的网络；</li>
<li>启动容器。</li>
</ol>
<p><img alt="image-20220707211357236" loading="lazy" src="/image/frp-skills/image-20220707211357236.png"></p>
<p><img alt="image-20220707211924033" loading="lazy" src="/image/frp-skills/image-20220707211924033.png"></p>
<p><img alt="image-20220707211655902" loading="lazy" src="/image/frp-skills/image-20220707211655902.png"></p>
<p><img alt="image-20220707211700736" loading="lazy" src="/image/frp-skills/image-20220707211700736.png"></p>
<p><img alt="image-20220707211739436" loading="lazy" src="/image/frp-skills/image-20220707211739436.png"></p>
<h3 id="2frpsini-配置文件">2、frps.ini 配置文件</h3>
<pre tabindex="0"><code>[common]
bind_port = 7000
token = your_token
dashboard_port = 7500
dashboard_user = your_dashboard_user
dashboard_pwd = your_dashboard_pwd
vhost_http_port = 8000
vhost_https_port = 8001
</code></pre><h2 id="二客户端配置">二、客户端配置</h2>
<h3 id="1安装及配置-1">1、安装及配置</h3>
<p>同样以群晖 NAS 为例，安装及配置基本跟服务端相同，配置文件改为 frpc.ini 即可。</p>
<h3 id="2frpcini-配置文件">2、frpc.ini 配置文件</h3>
<pre tabindex="0"><code>[common]
server_addr = your_server_addr
server_port = 7000
tls_enable = true
token = your_token

[ssh]
type = tcp
local_ip = 192.168.100.2
local_port = 22
remote_port = 8022
use_compression = true
use_encryption = true

[chevereto]
type = tcp
local_ip = 192.168.100.2
local_port = 8090
remote_port = 8090
use_compression = true
use_encryption = true

[WebDAV_https]
type = tcp
local_ip = 192.168.100.2
local_port = 5007
remote_port = 8007
use_compression = true
use_encryption = true

[WebDAV_http]
type = tcp
local_ip = 192.168.100.2
local_port = 5005
remote_port = 8005
use_compression = true
use_encryption = true

[dsm_http]
type = tcp
local_ip = 192.168.100.2
local_port = 5000
remote_port = 8000
use_compression = true
use_encryption = true

[http]
type = http
custom_domains = your_custom_domains
local_port = 5000
use_compression = true
use_encryption = true

[https2http]
type = https
custom_domains = your_custom_domains
local_port = 5000
plugin = https2http
plugin_local_addr = 127.0.0.1:5000

plugin_crt_path = /etc/frp/cert/your_custom_domains.pem
plugin_key_path = /etc/frp/cert/your_custom_domains.key
plugin_host_header_rewrite = 127.0.0.1
plugin_header_X-From-Where = frp
</code></pre><h2 id="三mac-远程访问">三、Mac 远程访问</h2>
<p>公司因安全原因，内网屏蔽了 TeamViewer、花生壳等远程桌面工具，而我时常需要在家里访问公司的 Mac 电脑。</p>
<p>一种方法是连接 VPN，然后通过 公司的 Mac IP 地址这种场景使用 frp 工具就非常好解决了。</p>
<h3 id="1mac-屏幕共享设置">1、Mac 屏幕共享设置</h3>
<p>在 System Preferences -&gt; Sharing -&gt; 勾选 Screen Sharing</p>
<p><img alt="image-20220707210935005" loading="lazy" src="/image/frp-skills/image-20220707210935005.png"></p>
<h3 id="2mac-安装-frpc">2、Mac 安装 frpc</h3>
<p>可以在 <a href="https://github.com/fatedier/frp/releases">frp GitHub</a> 下载 darwin 二进制包，使用 <code>./frpc -c frpc.ini</code> 命令即可启动客户端，但是如果想要自启动，还得配置 launchctl。</p>
<p>我们可以直接使用 Homebrew 进行安装会更加方便，Homebrew 内置了 brew services 服务（Manage background services with macOS&rsquo; launchctl(1) daemon manager.），我们就不用单独配置 launchctl。</p>
<pre tabindex="0"><code>// 安装
brew install frpc
// 编辑配置
vim /usr/local/etc/frp/frpc.ini
// 新版路径
vim /opt/homebrew/etc/frp/frpc.ini
// 启动服务
brew services start frpc
</code></pre><p>frpc.ini 配置</p>
<pre tabindex="0"><code>[common]
server_addr = your_server_addr
server_port = 7000
tls_enable = true
token = your_token

[vnc]
type = tcp
local_ip = 127.0.0.1
local_port = 5900 // 本地 vnc 默认端口
remote_port = 8050 // 远程访问时使用的端口
use_compression = true
use_encryption = true
</code></pre><h3 id="3远程连接">3、远程连接</h3>
<p>可以安装 <strong>VNC Viewer</strong> 进行访问。</p>
<p>如果使用 macOS，</p>
<ol>
<li>打开 Finder -&gt; Go -&gt; Connect to Server，或者使用快捷键 <em>Command + K</em>；</li>
<li>输入 vnc://your_server_addr:8050，然后点击 Connect；</li>
<li>弹出输入框，输入 Mac 用户名及锁屏密码。</li>
</ol>
<p><img alt="image-20220708104154647" loading="lazy" src="/image/frp-skills/image-20220708104154647.png"></p>
]]></content:encoded>
    </item>
    <item>
      <title>Mac 装机必备</title>
      <link>https://blog.axqora.com/posts/mac-tools/</link>
      <pubDate>Mon, 20 Dec 2021 00:00:00 +0000</pubDate>
      <guid>https://blog.axqora.com/posts/mac-tools/</guid>
      <description>&lt;h2 id=&#34;开发工具&#34;&gt;开发工具&lt;/h2&gt;
&lt;h3 id=&#34;iterm2&#34;&gt;&lt;a href=&#34;https://iterm2.com/&#34;&gt;iTerm2&lt;/a&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;iTerm2 is a terminal emulator for macOS that does amazing things.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;tabby&#34;&gt;&lt;a href=&#34;https://tabby.sh/&#34;&gt;Tabby&lt;/a&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Tabby is an infinitely customizable cross-platform terminal app for local shells, serial, SSH and Telnet connections.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;fig&#34;&gt;&lt;a href=&#34;https://fig.io/&#34;&gt;Fig&lt;/a&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Fig adds VSCode-style autocomplete to your existing terminal.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;终端自动补全工具，能在终端里有 VSCode 的自动补全体验，支持 &lt;code&gt;iTerm2&lt;/code&gt;。如果你使用的是 &lt;code&gt;zsh&lt;/code&gt;，也可以安装 &lt;a href=&#34;https://github.com/zsh-users/zsh-autosuggestions&#34;&gt;zsh-autosuggestions&lt;/a&gt; 作为补充使用。&lt;/p&gt;
&lt;h3 id=&#34;homebrew&#34;&gt;&lt;a href=&#34;https://brew.sh/&#34;&gt;Homebrew&lt;/a&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;The Missing Package Manager for macOS.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;oh-my-zsh&#34;&gt;&lt;a href=&#34;https://github.com/ohmyzsh/ohmyzsh&#34;&gt;Oh My Zsh&lt;/a&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Oh My Zsh is an open source, community-driven framework for managing your ### &lt;a href=&#34;https://www.zsh.org/&#34;&gt;zsh&lt;/a&gt; configuration.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;xcodes&#34;&gt;&lt;a href=&#34;https://github.com/RobotsAndPencils/XcodesApp&#34;&gt;Xcodes&lt;/a&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;The easiest way to install and switch between multiple versions of Xcode.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="开发工具">开发工具</h2>
<h3 id="iterm2"><a href="https://iterm2.com/">iTerm2</a></h3>
<blockquote>
<p>iTerm2 is a terminal emulator for macOS that does amazing things.</p>
</blockquote>
<h3 id="tabby"><a href="https://tabby.sh/">Tabby</a></h3>
<blockquote>
<p>Tabby is an infinitely customizable cross-platform terminal app for local shells, serial, SSH and Telnet connections.</p>
</blockquote>
<h3 id="fig"><a href="https://fig.io/">Fig</a></h3>
<blockquote>
<p>Fig adds VSCode-style autocomplete to your existing terminal.</p>
</blockquote>
<p>终端自动补全工具，能在终端里有 VSCode 的自动补全体验，支持 <code>iTerm2</code>。如果你使用的是 <code>zsh</code>，也可以安装 <a href="https://github.com/zsh-users/zsh-autosuggestions">zsh-autosuggestions</a> 作为补充使用。</p>
<h3 id="homebrew"><a href="https://brew.sh/">Homebrew</a></h3>
<blockquote>
<p>The Missing Package Manager for macOS.</p>
</blockquote>
<h3 id="oh-my-zsh"><a href="https://github.com/ohmyzsh/ohmyzsh">Oh My Zsh</a></h3>
<blockquote>
<p>Oh My Zsh is an open source, community-driven framework for managing your ### <a href="https://www.zsh.org/">zsh</a> configuration.</p>
</blockquote>
<h3 id="xcodes"><a href="https://github.com/RobotsAndPencils/XcodesApp">Xcodes</a></h3>
<blockquote>
<p>The easiest way to install and switch between multiple versions of Xcode.</p>
</blockquote>
<h3 id="cakebrew"><a href="https://github.com/brunophilipe/Cakebrew">Cakebrew</a></h3>
<blockquote>
<p>The Homebrew GUI App for OS X</p>
</blockquote>
<h3 id="macsymbolicator"><a href="https://github.com/inket/MacSymbolicator">MacSymbolicator</a></h3>
<blockquote>
<p>A simple Mac app for symbolicating macOS/iOS crash reports.</p>
</blockquote>
<h3 id="tinypng4mac"><a href="https://github.com/kyleduo/TinyPNG4Mac">TinyPNG4Mac</a></h3>
<blockquote>
<p>TinyPNG client for Mac.</p>
</blockquote>
<h3 id="fork"><a href="https://fork.dev/">Fork</a></h3>
<blockquote>
<p>A fast and friendly GIT client.</p>
</blockquote>
<h2 id="日常工具">日常工具</h2>
<h3 id="raycast"><a href="https://www.raycast.com/">Raycast</a></h3>
<blockquote>
<p>Raycast is a blazingly fast, totally extendable launcher.</p>
</blockquote>
<p>系统 Spotlight 的替代产品，插件超多，另一个出名的类似产品就是 Alfred，我选择这个的原因是功能设计更符合我的喜好。</p>
<h3 id="typora"><a href="https://typora.io/">Typora</a></h3>
<blockquote>
<p>A truly minimal markdown editor.</p>
</blockquote>
<p>Markdown 编辑器，我用过最好用的 Markdown 编辑器，最近正式版也上线了，我第一时间也付费支持。</p>
<h3 id="snipaste"><a href="https://www.snipaste.com/">Snipaste</a></h3>
<blockquote>
<p>Snip &amp; Paste</p>
</blockquote>
<p>截图工具，功能齐全，性能稳定，缺点是不支持截长图。</p>
<h3 id="maczip"><a href="https://ezip.awehunt.com/">MacZip</a></h3>
<blockquote>
<p>MacZip Mac版是Mac电脑上的一款专为Mac系统设计的压缩解压软件</p>
</blockquote>
<h3 id="picgo"><a href="https://github.com/Molunerfinn/PicGo">PicGo</a></h3>
<blockquote>
<p>一个用于快速上传图片并获取图片 URL 链接的工具</p>
</blockquote>
<h3 id="istat-menus"><a href="https://bjango.com/mac/istatmenus/">iStat Menus</a></h3>
<blockquote>
<p>An advanced Mac system monitor for your menubar.</p>
</blockquote>
<h3 id="腾讯柠檬清理"><a href="https://lemon.qq.com/">腾讯柠檬清理</a></h3>
<blockquote>
<p>腾讯柠檬清理- 全新Mac清理工具，实时了解Mac系统状况</p>
</blockquote>
<h3 id="clashx"><a href="https://github.com/yichengchen/clashX">ClashX</a></h3>
<p>代理客户端</p>
<h3 id="shadowsocksx-ng"><a href="https://github.com/shadowsocks/ShadowsocksX-NG">ShadowsocksX-NG</a></h3>
<p>代理客户端</p>
<h2 id="娱乐相关">娱乐相关</h2>
<h3 id="iina"><a href="https://iina.io/">IINA</a></h3>
<blockquote>
<p>The modern media player for macOS.</p>
</blockquote>
<p>持续更新。。。</p>
]]></content:encoded>
    </item>
    <item>
      <title>macOS 解决 App Nap 导致 Timer 回调变慢问题</title>
      <link>https://blog.axqora.com/posts/macos-timer/</link>
      <pubDate>Mon, 05 Jul 2021 00:00:00 +0000</pubDate>
      <guid>https://blog.axqora.com/posts/macos-timer/</guid>
      <description>&lt;h2 id=&#34;背景&#34;&gt;背景&lt;/h2&gt;
&lt;p&gt;在开发 macOS 上的窗口共享过程中，发现 demo 被其它应用覆盖后，在25s左右，观众收到的窗口画面开始变得卡顿。&lt;/p&gt;
&lt;p&gt;经过排查发现问题出在主播端的采集逻辑处。在我的上一篇文章 &lt;a href=&#34;https://blog.axqora.com/posts/macos-screensharing/&#34;&gt;macOS 屏幕共享开发&lt;/a&gt; 提到，窗口共享的实现跟屏幕共享不一样，窗口共享没有现有的系统接口，需要启动一个 Timer 去定时对窗口截图，再对截图进行处理。&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;188bf0d5672f682c730e8d524608379a&#34; loading=&#34;lazy&#34; src=&#34;https://blog.axqora.com/image/macOS-Timer/2F4FE15A-02D5-4C4E-9598-DF9B424C33FE.png&#34;&gt;&lt;/p&gt;
&lt;p&gt;通过打印日志发现，demo 进入后台后，Timer 在25s左右，回调开始变慢，大约2s才回调一次，问题的原因就出在这里。&lt;/p&gt;
&lt;h2 id=&#34;排查原因&#34;&gt;排查原因&lt;/h2&gt;
&lt;p&gt;为了避免其它逻辑影响了结果，新建一个干净的 demo。&lt;/p&gt;
&lt;p&gt;我在窗口共享中用的是 GCD Timer，一开始猜测是 GCD Timer 的某些参数没有设置，查看文档后没找到相关的参数；然后猜测 GCD Timer 的问题，但是换成 NSTimer 后，发现也有一样的问题，这时候我开始猜测是系统导致的问题了。&lt;/p&gt;
&lt;p&gt;开始查阅资料，发现了 &lt;a href=&#34;https://sspai.com/post/26538&#34;&gt;一篇文章&lt;/a&gt; 里面提到：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;App Nap 是 OS X 10.9 Mavericks 的一项新功能。它能帮你在同时运行多个应用程序时节省电能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这时候有了切入点，查询关键字 &lt;code&gt;App Nap&lt;/code&gt; 就可以发现相关资料。&lt;/p&gt;
&lt;h2 id=&#34;app-nap&#34;&gt;App Nap&lt;/h2&gt;
&lt;p&gt;附上苹果官方说明文档：&lt;a href=&#34;https://developer.apple.com/library/archive/documentation/Performance/Conceptual/power_efficiency_guidelines_osx/AppNap.html&#34;&gt;Extend App Nap&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If an app isn’t performing user-initiated work such as updating content on screen, playing music, or downloading a file, the system may put the app in App Nap. App Nap conserves battery life by regulating the app’s CPU usage and by reducing the frequency with which its timers are fired.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="背景">背景</h2>
<p>在开发 macOS 上的窗口共享过程中，发现 demo 被其它应用覆盖后，在25s左右，观众收到的窗口画面开始变得卡顿。</p>
<p>经过排查发现问题出在主播端的采集逻辑处。在我的上一篇文章 <a href="/posts/macos-screensharing/">macOS 屏幕共享开发</a> 提到，窗口共享的实现跟屏幕共享不一样，窗口共享没有现有的系统接口，需要启动一个 Timer 去定时对窗口截图，再对截图进行处理。</p>
<p><img alt="188bf0d5672f682c730e8d524608379a" loading="lazy" src="/image/macOS-Timer/2F4FE15A-02D5-4C4E-9598-DF9B424C33FE.png"></p>
<p>通过打印日志发现，demo 进入后台后，Timer 在25s左右，回调开始变慢，大约2s才回调一次，问题的原因就出在这里。</p>
<h2 id="排查原因">排查原因</h2>
<p>为了避免其它逻辑影响了结果，新建一个干净的 demo。</p>
<p>我在窗口共享中用的是 GCD Timer，一开始猜测是 GCD Timer 的某些参数没有设置，查看文档后没找到相关的参数；然后猜测 GCD Timer 的问题，但是换成 NSTimer 后，发现也有一样的问题，这时候我开始猜测是系统导致的问题了。</p>
<p>开始查阅资料，发现了 <a href="https://sspai.com/post/26538">一篇文章</a> 里面提到：</p>
<blockquote>
<p>App Nap 是 OS X 10.9 Mavericks 的一项新功能。它能帮你在同时运行多个应用程序时节省电能。</p>
</blockquote>
<p>这时候有了切入点，查询关键字 <code>App Nap</code> 就可以发现相关资料。</p>
<h2 id="app-nap">App Nap</h2>
<p>附上苹果官方说明文档：<a href="https://developer.apple.com/library/archive/documentation/Performance/Conceptual/power_efficiency_guidelines_osx/AppNap.html">Extend App Nap</a></p>
<blockquote>
<p>If an app isn’t performing user-initiated work such as updating content on screen, playing music, or downloading a file, the system may put the app in App Nap. App Nap conserves battery life by regulating the app’s CPU usage and by reducing the frequency with which its timers are fired.</p>
</blockquote>
<p>官方说明文档介绍很清楚了，包括 App Nap 工作的条件、表现等，我们要处理的就是在启动 Timer 时，暂时把 App Nap 功能关掉。</p>
<h2 id="解决方案">解决方案</h2>
<p>在开启 Timer 前加入以下代码：</p>
<pre tabindex="0"><code>// 避免 macOS 的 App Nap 功能导致在后台时 timer 停止运行。
NSActivityOptions option = NSActivityBackground;
self.processActivity = [[NSProcessInfo processInfo] beginActivityWithOptions:option reason:@&#34;process.activity&#34;];
</code></pre><p>关闭 Timer 后把 Activity 停掉：</p>
<pre tabindex="0"><code>if (self.processActivity) {
    [[NSProcessInfo processInfo] endActivity:self.processActivity];
    self.processActivity = nil;
}
</code></pre><p>这时候查看 Timer 的打印，发现25s后的回调都已经正常了。</p>
]]></content:encoded>
    </item>
    <item>
      <title>macOS 屏幕共享开发</title>
      <link>https://blog.axqora.com/posts/macos-screensharing/</link>
      <pubDate>Tue, 18 May 2021 00:00:00 +0000</pubDate>
      <guid>https://blog.axqora.com/posts/macos-screensharing/</guid>
      <description>&lt;h2 id=&#34;一屏幕录制权限&#34;&gt;一、屏幕录制权限&lt;/h2&gt;
&lt;p&gt;&lt;img alt=&#34;e33c5801ba456666fadd2aa2602bd730&#34; loading=&#34;lazy&#34; src=&#34;https://blog.axqora.com/image/macOS-ScreenSharing/AE71518A-D357-473C-AD35-4C4FFB62AD34.png&#34;&gt;&lt;/p&gt;
&lt;p&gt;应用首次创建屏幕截图或者窗口截图时（以下任一接口都可以），就会显示系统的权限申请弹窗，通过选中&lt;code&gt;Screen Recording&lt;/code&gt;里面的相关应用，重启应用后即可开启权限。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-objc&#34; data-lang=&#34;objc&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;CGDisplayCreateImage()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;CGDisplayCreateImageForRect()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;CGWindowListCreateImage()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;CGWindowListCreateImageFromArray()
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;二获取screenid&#34;&gt;二、获取screenId&lt;/h2&gt;
&lt;p&gt;通过AppKit的&lt;code&gt;NSScreen.screens&lt;/code&gt;或者通过CoreGraphics的&lt;code&gt;CGGetActiveDisplayList()&lt;/code&gt;都可以获取当前的所有显示器信息，下面用&lt;code&gt;NSScreen.screens&lt;/code&gt;举例子：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-objc&#34; data-lang=&#34;objc&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[NSScreen.screens enumerateObjectsUsingBlock:&lt;span style=&#34;color:#f92672&#34;&gt;^&lt;/span&gt;(NSScreen &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; _Nonnull screen, NSUInteger idx, &lt;span style=&#34;color:#66d9ef&#34;&gt;BOOL&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; _Nonnull stop) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    NSNumber &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;screenNumber &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; screen.deviceDescription[&lt;span style=&#34;color:#e6db74&#34;&gt;@&amp;#34;NSScreenNumber&amp;#34;&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (screenNumber) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        CGDirectDisplayID displayId &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; screenNumber.unsignedIntValue;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        NSLog(&lt;span style=&#34;color:#e6db74&#34;&gt;@&amp;#34;displayId: %d&amp;#34;&lt;/span&gt;, displayId);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}];
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;注意：如果是镜像显示，则只会打印一个显示器信息。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;三获取windowid&#34;&gt;三、获取windowId&lt;/h2&gt;
&lt;h4 id=&#34;1获取windowid列表&#34;&gt;1、获取windowId列表&lt;/h4&gt;
&lt;p&gt;通过接口&lt;code&gt;CGWindowListCopyWindowInfo()&lt;/code&gt;可以获取windowId列表。&lt;/p&gt;
&lt;p&gt;其中&lt;code&gt;kCGWindowListExcludeDesktopElements&lt;/code&gt;参数表示从列表中排除所有属于桌面元素的窗口。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;CGWindowListCopyWindowInfo()&lt;/code&gt;返回的是一个&lt;code&gt;CFArrayRef&lt;/code&gt;，使用完后需要主动release。
这里有一个技巧，利用&lt;code&gt;CFBridgingRelease()&lt;/code&gt;函数，可以把&lt;code&gt;CFArrayRef&lt;/code&gt;桥接到&lt;code&gt;NSArray&lt;/code&gt;，并且会把内存管理转移到ARC，就是说我们不需要再去主动release了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;NSArray *windowDictArr = CFBridgingRelease(CGWindowListCopyWindowInfo(kCGWindowListOptionAll | kCGWindowListExcludeDesktopElements, 0));
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id=&#34;2过滤无用windowid&#34;&gt;2、过滤无用windowId&lt;/h4&gt;
&lt;p&gt;通过以上接口获取到的windowId列表可能会包含很多我们不关心的windowId：&lt;/p&gt;
&lt;h5 id=&#34;1-没显示出来的window&#34;&gt;1. 没显示出来的window&lt;/h5&gt;
&lt;p&gt;通过&lt;code&gt;kCGWindowLayer&lt;/code&gt;跟&lt;code&gt;kCGWindowAlpha&lt;/code&gt;两个key，可以获取到当前window是否有layer以及是否透明，我们需要过滤掉不可见的window。&lt;/p&gt;
&lt;h5 id=&#34;2-过滤信息不全的window&#34;&gt;2. 过滤信息不全的window&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;kCGWindowNumber&lt;/code&gt;key可以从字典里获取到windowId，需要过滤没有windowId的字典信息。&lt;/p&gt;
&lt;h5 id=&#34;3-过滤无法获取截图的window&#34;&gt;3. 过滤无法获取截图的window&lt;/h5&gt;
&lt;p&gt;通过&lt;code&gt;CGWindowListCreateImage()&lt;/code&gt;接口，传入对应windowId，可获取到window的截图，如果无法获取到截图，可能没有录屏权限、不是window主体、window不可见。&lt;/p&gt;
&lt;h4 id=&#34;3获取window-frame&#34;&gt;3、获取window frame&lt;/h4&gt;
&lt;p&gt;通过&lt;code&gt;CGRectMakeWithDictionaryRepresentation()&lt;/code&gt;可获取窗口的frame，某些业务可能需要用到frame。&lt;/p&gt;
&lt;h2 id=&#34;四屏幕共享&#34;&gt;四、屏幕共享&lt;/h2&gt;
&lt;p&gt;屏幕共享的视频输入总体来说跟摄像头的视频输入相当类似，可以理解成视频流从摄像头改成了屏幕输入（&lt;code&gt;AVCaptureDeviceInput&lt;/code&gt;-&amp;gt;&lt;code&gt;AVCaptureScreenInput&lt;/code&gt;），视频输出的处理则跟摄像头的输出处理完全一样。&lt;/p&gt;
&lt;h4 id=&#34;1关键代码&#34;&gt;1、关键代码：&lt;/h4&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-objc&#34; data-lang=&#34;objc&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- (&lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt;)&lt;span style=&#34;color:#a6e22e&#34;&gt;startCapture&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    self.captureSession &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [[AVCaptureSession alloc] init];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    self.captureSession.sessionPreset &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; AVCaptureSessionPresetMedium;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// input
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    AVCaptureScreenInput &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;input &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [[AVCaptureScreenInput alloc] initWithDisplayID:kCGDirectMainDisplay];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    [self.captureSession addInput:input];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// output
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    [self.captureSession beginConfiguration];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    dispatch_queue_t queue &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; dispatch_queue_create(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;smaple_buffer&amp;#34;&lt;/span&gt;, DISPATCH_QUEUE_SERIAL);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    AVCaptureVideoDataOutput &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;videoDataOutput &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [[AVCaptureVideoDataOutput alloc] init];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    videoDataOutput.videoSettings &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;@{&lt;/span&gt;(NSString &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;)kCVPixelBufferPixelFormatTypeKey: &lt;span style=&#34;color:#ae81ff&#34;&gt;@(&lt;/span&gt;kCVPixelFormatType_420YpCbCr8BiPlanarFullRange&lt;span style=&#34;color:#ae81ff&#34;&gt;)}&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    [videoDataOutput setSampleBufferDelegate:self queue:queue];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    AVCaptureConnection &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;videoConnection &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [videoDataOutput connectionWithMediaType:AVMediaTypeVideo];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    videoConnection.videoOrientation &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; AVCaptureVideoOrientationPortrait;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    [self.captureSession addOutput:videoDataOutput];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    [self.captureSession commitConfiguration];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    [self.captureSession startRunning];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id=&#34;2视频输出分辨率设置&#34;&gt;2、视频输出分辨率设置&lt;/h4&gt;
&lt;p&gt;我们的编码分辨率最大为720p，一个是避免捕获到的图片过大造成性能浪费，一个是避免难以适配太多的屏幕分辨率，我们需要指定输出的分辨率。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="一屏幕录制权限">一、屏幕录制权限</h2>
<p><img alt="e33c5801ba456666fadd2aa2602bd730" loading="lazy" src="/image/macOS-ScreenSharing/AE71518A-D357-473C-AD35-4C4FFB62AD34.png"></p>
<p>应用首次创建屏幕截图或者窗口截图时（以下任一接口都可以），就会显示系统的权限申请弹窗，通过选中<code>Screen Recording</code>里面的相关应用，重启应用后即可开启权限。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-objc" data-lang="objc"><span style="display:flex;"><span>CGDisplayCreateImage()
</span></span><span style="display:flex;"><span>CGDisplayCreateImageForRect()
</span></span><span style="display:flex;"><span>CGWindowListCreateImage()
</span></span><span style="display:flex;"><span>CGWindowListCreateImageFromArray()
</span></span></code></pre></div><h2 id="二获取screenid">二、获取screenId</h2>
<p>通过AppKit的<code>NSScreen.screens</code>或者通过CoreGraphics的<code>CGGetActiveDisplayList()</code>都可以获取当前的所有显示器信息，下面用<code>NSScreen.screens</code>举例子：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-objc" data-lang="objc"><span style="display:flex;"><span>[NSScreen.screens enumerateObjectsUsingBlock:<span style="color:#f92672">^</span>(NSScreen <span style="color:#f92672">*</span> _Nonnull screen, NSUInteger idx, <span style="color:#66d9ef">BOOL</span> <span style="color:#f92672">*</span> _Nonnull stop) {
</span></span><span style="display:flex;"><span>    NSNumber <span style="color:#f92672">*</span>screenNumber <span style="color:#f92672">=</span> screen.deviceDescription[<span style="color:#e6db74">@&#34;NSScreenNumber&#34;</span>];
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (screenNumber) {
</span></span><span style="display:flex;"><span>        CGDirectDisplayID displayId <span style="color:#f92672">=</span> screenNumber.unsignedIntValue;
</span></span><span style="display:flex;"><span>        NSLog(<span style="color:#e6db74">@&#34;displayId: %d&#34;</span>, displayId);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}];
</span></span></code></pre></div><p><strong>注意：如果是镜像显示，则只会打印一个显示器信息。</strong></p>
<h2 id="三获取windowid">三、获取windowId</h2>
<h4 id="1获取windowid列表">1、获取windowId列表</h4>
<p>通过接口<code>CGWindowListCopyWindowInfo()</code>可以获取windowId列表。</p>
<p>其中<code>kCGWindowListExcludeDesktopElements</code>参数表示从列表中排除所有属于桌面元素的窗口。</p>
<blockquote>
<p><code>CGWindowListCopyWindowInfo()</code>返回的是一个<code>CFArrayRef</code>，使用完后需要主动release。
这里有一个技巧，利用<code>CFBridgingRelease()</code>函数，可以把<code>CFArrayRef</code>桥接到<code>NSArray</code>，并且会把内存管理转移到ARC，就是说我们不需要再去主动release了。</p>
</blockquote>
<pre tabindex="0"><code>NSArray *windowDictArr = CFBridgingRelease(CGWindowListCopyWindowInfo(kCGWindowListOptionAll | kCGWindowListExcludeDesktopElements, 0));
</code></pre><h4 id="2过滤无用windowid">2、过滤无用windowId</h4>
<p>通过以上接口获取到的windowId列表可能会包含很多我们不关心的windowId：</p>
<h5 id="1-没显示出来的window">1. 没显示出来的window</h5>
<p>通过<code>kCGWindowLayer</code>跟<code>kCGWindowAlpha</code>两个key，可以获取到当前window是否有layer以及是否透明，我们需要过滤掉不可见的window。</p>
<h5 id="2-过滤信息不全的window">2. 过滤信息不全的window</h5>
<p><code>kCGWindowNumber</code>key可以从字典里获取到windowId，需要过滤没有windowId的字典信息。</p>
<h5 id="3-过滤无法获取截图的window">3. 过滤无法获取截图的window</h5>
<p>通过<code>CGWindowListCreateImage()</code>接口，传入对应windowId，可获取到window的截图，如果无法获取到截图，可能没有录屏权限、不是window主体、window不可见。</p>
<h4 id="3获取window-frame">3、获取window frame</h4>
<p>通过<code>CGRectMakeWithDictionaryRepresentation()</code>可获取窗口的frame，某些业务可能需要用到frame。</p>
<h2 id="四屏幕共享">四、屏幕共享</h2>
<p>屏幕共享的视频输入总体来说跟摄像头的视频输入相当类似，可以理解成视频流从摄像头改成了屏幕输入（<code>AVCaptureDeviceInput</code>-&gt;<code>AVCaptureScreenInput</code>），视频输出的处理则跟摄像头的输出处理完全一样。</p>
<h4 id="1关键代码">1、关键代码：</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-objc" data-lang="objc"><span style="display:flex;"><span>- (<span style="color:#66d9ef">void</span>)<span style="color:#a6e22e">startCapture</span> {
</span></span><span style="display:flex;"><span>    self.captureSession <span style="color:#f92672">=</span> [[AVCaptureSession alloc] init];
</span></span><span style="display:flex;"><span>    self.captureSession.sessionPreset <span style="color:#f92672">=</span> AVCaptureSessionPresetMedium;
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// input
</span></span></span><span style="display:flex;"><span>    AVCaptureScreenInput <span style="color:#f92672">*</span>input <span style="color:#f92672">=</span> [[AVCaptureScreenInput alloc] initWithDisplayID:kCGDirectMainDisplay];
</span></span><span style="display:flex;"><span>    [self.captureSession addInput:input];
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// output
</span></span></span><span style="display:flex;"><span>    [self.captureSession beginConfiguration];
</span></span><span style="display:flex;"><span>    dispatch_queue_t queue <span style="color:#f92672">=</span> dispatch_queue_create(<span style="color:#e6db74">&#34;smaple_buffer&#34;</span>, DISPATCH_QUEUE_SERIAL);
</span></span><span style="display:flex;"><span>    AVCaptureVideoDataOutput <span style="color:#f92672">*</span>videoDataOutput <span style="color:#f92672">=</span> [[AVCaptureVideoDataOutput alloc] init];
</span></span><span style="display:flex;"><span>    videoDataOutput.videoSettings <span style="color:#f92672">=</span> <span style="color:#ae81ff">@{</span>(NSString <span style="color:#f92672">*</span>)kCVPixelBufferPixelFormatTypeKey: <span style="color:#ae81ff">@(</span>kCVPixelFormatType_420YpCbCr8BiPlanarFullRange<span style="color:#ae81ff">)}</span>;
</span></span><span style="display:flex;"><span>    [videoDataOutput setSampleBufferDelegate:self queue:queue];
</span></span><span style="display:flex;"><span>    AVCaptureConnection <span style="color:#f92672">*</span>videoConnection <span style="color:#f92672">=</span> [videoDataOutput connectionWithMediaType:AVMediaTypeVideo];
</span></span><span style="display:flex;"><span>    videoConnection.videoOrientation <span style="color:#f92672">=</span> AVCaptureVideoOrientationPortrait;
</span></span><span style="display:flex;"><span>    [self.captureSession addOutput:videoDataOutput];
</span></span><span style="display:flex;"><span>    [self.captureSession commitConfiguration];
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    [self.captureSession startRunning];
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h4 id="2视频输出分辨率设置">2、视频输出分辨率设置</h4>
<p>我们的编码分辨率最大为720p，一个是避免捕获到的图片过大造成性能浪费，一个是避免难以适配太多的屏幕分辨率，我们需要指定输出的分辨率。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-objc" data-lang="objc"><span style="display:flex;"><span>captureSession.sessionPreset <span style="color:#f92672">=</span> AVCaptureSessionPreset1280x720;
</span></span><span style="display:flex;"><span><span style="color:#75715e">// 如果下面代码不设置不会生效
</span></span></span><span style="display:flex;"><span>AVCaptureVideoDataOutput <span style="color:#f92672">*</span>output <span style="color:#f92672">=</span> captureSession.outputs[<span style="color:#ae81ff">0</span>];
</span></span><span style="display:flex;"><span>[output setVideoSettings:<span style="color:#ae81ff">@{</span>(<span style="color:#66d9ef">id</span>)kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange],
</span></span><span style="display:flex;"><span>                           (<span style="color:#66d9ef">id</span>)kCVPixelBufferWidthKey:<span style="color:#ae81ff">@1280</span>,
</span></span><span style="display:flex;"><span>                           (<span style="color:#66d9ef">id</span>)kCVPixelBufferHeightKey:<span style="color:#ae81ff">@720</span><span style="color:#ae81ff">}</span>];
</span></span></code></pre></div><h2 id="五窗口共享">五、窗口共享</h2>
<p>窗口共享没有现有的系统接口，跟屏幕共享差异较大，总体来说，有以下几步</p>
<ol>
<li>启动定时器；</li>
<li>定时获取窗口的截图；</li>
<li>对截图进行处理（添加光标、裁剪）；</li>
<li>把<code>CGImageRef</code>转换为yuv数据；</li>
<li>调整分辨率。</li>
</ol>
<h4 id="1启动定时器">1、启动定时器</h4>
<p>定时器的间隔与设置的帧率相关，<code>dispatch_source_set_timer()</code>要注意的是<code>interval</code>参数的单位是纳秒。</p>
<h4 id="2获取窗口截图">2、获取窗口截图</h4>
<p>通过<code>CGWindowListCreateImage()</code>可以获取指定windowId的窗口截图，我们需要原始分辨率，而且截图中不需要包含窗口的装饰（如阴影），所以其中<code>imageOption</code>需要指定<code>(kCGWindowImageNominalResolution | kCGWindowImageBoundsIgnoreFraming)</code>。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-objc" data-lang="objc"><span style="display:flex;"><span>CGImageRef windowImage <span style="color:#f92672">=</span> CGWindowListCreateImage(CGRectNull, kCGWindowListOptionIncludingWindow, windowId, (kCGWindowImageNominalResolution <span style="color:#f92672">|</span> kCGWindowImageBoundsIgnoreFraming));
</span></span></code></pre></div><h4 id="3追加光标">3、追加光标</h4>
<p>通过<code>CGWindowListCreateImage()</code>获取的窗口截图不会包含光标，所以需要额外追加光标。</p>
<ol>
<li>获取光标的坐标，<code>[NSEvent mouseLocation]</code></li>
<li>获取光标的图片，<code>[[NSCursor currentSystemCursor] image]</code></li>
<li>创建上下文，<code>CGBitmapContextCreate()</code></li>
<li>画背景、画光标，<code>CGContextDrawImage()</code></li>
</ol>
<h4 id="4裁剪">4、裁剪</h4>
<p>这里主要是为了需要指定区域捕获视频，接口<code>CGImageCreateWithImageInRect()</code></p>
<h4 id="5转换为yuv数据">5、转换为yuv数据</h4>
<ol>
<li>把<code>CGImageRef</code>转换为rgbaBuffer，<code>CVPixelBufferCreateWithBytes()</code></li>
<li>创建yuvBuffer的内存空间，<code>CVPixelBufferCreate()</code></li>
<li>利用libyuv库的<code>ARGBToI420()</code>函数把rgbaBuffer的信息写到yuvBuffer里</li>
</ol>
<h4 id="6分辨率处理">6、分辨率处理</h4>
<p>窗口的分辨率相对不可控，可能会被用户拉伸成一个特别大的窗口，我们在初始化渲染画布时就不好确定长宽。</p>
<ol>
<li>设置最大分辨率为1280*720，把窗口截图的长宽等比缩放为小于最大分辨率的长宽，关键代码：</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-objc" data-lang="objc"><span style="display:flex;"><span>- (CGSize)<span style="color:#a6e22e">resize:</span>(CGSize)size <span style="color:#a6e22e">maxSize:</span>(CGSize)maxSize {
</span></span><span style="display:flex;"><span>    CGSize outSize <span style="color:#f92672">=</span> size;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (size.width <span style="color:#f92672">&gt;</span> maxSize.width <span style="color:#f92672">||</span> size.height <span style="color:#f92672">&gt;</span> maxSize.height) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> ((size.width <span style="color:#f92672">/</span> size.height) <span style="color:#f92672">&gt;</span> (maxSize.width <span style="color:#f92672">/</span> maxSize.height)) {
</span></span><span style="display:flex;"><span>            outSize.width <span style="color:#f92672">=</span> maxSize.width;
</span></span><span style="display:flex;"><span>            outSize.height <span style="color:#f92672">=</span> ceil(size.height <span style="color:#f92672">*</span> maxSize.width <span style="color:#f92672">/</span> size.width);
</span></span><span style="display:flex;"><span>        } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>            outSize.height <span style="color:#f92672">=</span> maxSize.height;
</span></span><span style="display:flex;"><span>            outSize.width <span style="color:#f92672">=</span> ceil(size.width <span style="color:#f92672">*</span> maxSize.height <span style="color:#f92672">/</span> size.height);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> outSize;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><ol start="2">
<li>通过libyuv库的<code>I420Scale()</code>把上面的yuvBuffer缩放为scaledYuvBuffer</li>
</ol>
<h2 id="六捕获指定区域视频">六、捕获指定区域视频</h2>
<h4 id="1计算rect">1、计算Rect</h4>
<ol>
<li>判断是否需要裁剪，若传入的是<code>CGRectNull</code>或<code>CGRectZero</code>，则不需要裁剪；</li>
<li>取交集，可通过<code>CGRectIntersection()</code>比较方便获取两个Rect的交集，如果无交集则认为传入的Rect无效，不做裁剪；另外，如果计算出来的Rect的长宽小于8，编码时可能崩溃，这里加上了长宽大于等于10的判断；</li>
<li>转换坐标系，参数传入时的坐标系是左上角为原点，屏幕共享时设置捕获区域的接口的坐标系是左下角为原点，<code>targetRect.origin.y = CGRectGetMaxY(screenBound) - CGRectGetMaxY(intersection);</code></li>
</ol>
<h4 id="2屏幕共享">2、屏幕共享</h4>
<p>直接设置<code>AVCaptureScreenInput</code>的<code>cropRect</code>属性即可，<code>self.screenInput.cropRect = targetRect;</code></p>
<h4 id="3窗口共享">3、窗口共享</h4>
<p>获取到窗口截图后做裁剪即可，<code>CGImageRef croppedImage = CGImageCreateWithImageInRect(showImage, cropRect);</code></p>
<h2 id="七设置捕获帧率">七、设置捕获帧率</h2>
<h4 id="1屏幕共享">1、屏幕共享</h4>
<p>设置<code>AVCaptureScreenInput</code>的<code>minFrameDuration</code>属性即可。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-objc" data-lang="objc"><span style="display:flex;"><span>AVCaptureScreenInput <span style="color:#f92672">*</span>screenInput <span style="color:#f92672">=</span> (AVCaptureScreenInput <span style="color:#f92672">*</span>)input;
</span></span><span style="display:flex;"><span>screenInput.minFrameDuration <span style="color:#f92672">=</span> CMTimeMake(<span style="color:#ae81ff">1</span>, videoFrameRate);
</span></span></code></pre></div><h4 id="2窗口共享">2、窗口共享</h4>
<p>设置窗口截图定时器的时间间隔即可。</p>
<h2 id="八其它">八、其它</h2>
<h4 id="1窗口最小化">1、窗口最小化</h4>
<p>窗口最小化后，下面接口都返回了空，注意要做防崩溃处理</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-objc" data-lang="objc"><span style="display:flex;"><span>CGWindowListCopyWindowInfo()
</span></span><span style="display:flex;"><span>CGWindowListCreateImage()
</span></span></code></pre></div><h4 id="2屏幕的不同显示模式">2、屏幕的不同显示模式</h4>
<ol>
<li>Mirror Displays，如果选中了镜像模式，则<code>NSScreen.screens</code>只返回一个显示器的信息；</li>
<li>Scaled，如果选中了缩放，则输出的分辨率会变化，注意要做处理；</li>
<li>Rotation，同上；</li>
<li>不同分辨率的显示器，同上。</li>
</ol>
]]></content:encoded>
    </item>
  </channel>
</rss>
