<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.9.2">Jekyll</generator><link href="https://jshih.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://jshih.dev/" rel="alternate" type="text/html" /><updated>2023-01-05T23:53:17+00:00</updated><id>https://jshih.dev/feed.xml</id><title type="html">James Shih</title><subtitle>Magic happens.
</subtitle><entry><title type="html">iPhone 安全指南</title><link href="https://jshih.dev/2017/12/05/iphone-security-guide.html" rel="alternate" type="text/html" title="iPhone 安全指南" /><published>2017-12-05T09:20:16+00:00</published><updated>2017-12-05T09:20:16+00:00</updated><id>https://jshih.dev/2017/12/05/iphone-security-guide</id><content type="html" xml:base="https://jshih.dev/2017/12/05/iphone-security-guide.html">&lt;p&gt;所有智能手机用户都面临一个共同的危险：你的手机可能会丢失，后果会很严重。&lt;/p&gt;

&lt;p&gt;今天的智能手机能做的事情多得令人难以置信，你的手机上不仅存放着你的通讯簿（泄漏通讯簿中的联系方式就已经很危险了），还存储了你的邮件、照片、银行账号、社交网络账号、网络浏览历史……通过这些数据几乎可以完整地还原你的身份！所以丢失一只智能手机所带来的数据泄露，其后果往往是远超过手机本身价值的损失。&lt;/p&gt;

&lt;p&gt;这篇文章讲的不是怎样防止你的 iPhone 丢失，而是怎样防止设备丢失带来的数据泄露风险。&lt;/p&gt;

&lt;h2 id=&quot;锁定你的设备&quot;&gt;锁定你的设备&lt;/h2&gt;

&lt;p&gt;密码锁定应该是你保护设备的第一步，也是最重要的一步。现在的 iOS 版本会在设备激活向导中指导你设置密码和 &lt;a href=&quot;https://support.apple.com/zh-cn/HT201371&quot;&gt;Touch ID&lt;/a&gt; / &lt;a href=&quot;https://support.apple.com/zh-cn/HT208109&quot;&gt;Face ID&lt;/a&gt;，如果你还没有设置它们，可以&lt;a href=&quot;https://support.apple.com/zh-cn/HT204060&quot;&gt;参考这里&lt;/a&gt;进行设置。&lt;/p&gt;

&lt;p&gt;这里值得一提的是，你应该至少使用六位数字密码，最好使用自定字母数字密码。在启用 Touch ID / Face ID 的情况下，你很少需要输入这些密码，所以不妨设置得复杂一些。&lt;/p&gt;

&lt;p&gt;“抹掉数据”选项会在十次尝试输入错误密码后自动抹掉设备上的内容，如果你坚持要使用四位数字密码，建议打开这个选项。&lt;/p&gt;

&lt;p&gt;请记住：复杂的字母数字密码比 Touch ID / Face ID 更安全，在紧急情况下，利用 iOS 11 的 &lt;a href=&quot;https://support.apple.com/zh-cn/HT208076&quot;&gt;SOS 紧急联络&lt;/a&gt;功能可以快速禁用 Touch ID / Face ID，临时提高设备的安全防护级别。&lt;/p&gt;

&lt;h2 id=&quot;备份你的设备&quot;&gt;备份你的设备&lt;/h2&gt;

&lt;p&gt;对于重要的数据而言，备份也是安全措施的其中一环。只不过，备份的安全也需要注意，所以尽量选择可靠的备份方式并且加密备份的数据是很重要的。&lt;/p&gt;

&lt;p&gt;利用 &lt;a href=&quot;https://support.apple.com/zh-cn/HT203977#icloud&quot;&gt;iCloud 云备份&lt;/a&gt;来备份信息、设备设置和 App 数据等。iCloud 云备份会在你充电并且接入 Wi-Fi 时自动在后台进行备份。有些 App（例如腾讯视频）因为开发者不负责任，将缓存等没有必要备份的数据也备份到了 iCloud，导致消耗大量 iCloud 存储空间。这时你可以在“设置”&amp;gt;“Apple ID”&amp;gt;“iCloud”&amp;gt;“管理存储空间”中关闭这些 App 的云备份。&lt;/p&gt;

&lt;p&gt;除此之外，你还可以使用 &lt;a href=&quot;https://support.apple.com/zh-cn/HT203977#itunes&quot;&gt;iTunes 备份&lt;/a&gt;。不过，与 iCloud 云备份相比，你需要定期手动备份。iTunes 备份可以作为 iCloud 云备份的补充。&lt;/p&gt;

&lt;p&gt;对于邮件、通讯录、日历、提醒事项和备忘录，你可以选择使用 iCloud、Google、Outlook 或者 Exchange 服务等等，数据会自动同步到云端。如果你在激活设备时选择登录到 iCloud，那么很可能你已经在使用 iCloud 同步这些数据了。&lt;/p&gt;

&lt;p&gt;至于照片和视频，&lt;a href=&quot;https://support.apple.com/zh-cn/HT204264&quot;&gt;iCloud 照片图库&lt;/a&gt;是不错的选择，除了起到备份的作用，还可以把照片（以及你对照片的编辑）同步到电脑。你还可以选择在设备上只存储压缩过的图片以节约存储空间。当然，也有很多类似的服务可供选择，比如 &lt;a href=&quot;https://photos.google.com&quot;&gt;Google 相册&lt;/a&gt;和 &lt;a href=&quot;https://onedrive.live.com/&quot;&gt;OneDrive&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&quot;打开查找我的-iphone&quot;&gt;打开“查找我的 iPhone”&lt;/h2&gt;

&lt;p&gt;很多人认为“查找我的 iPhone”没有什么用处，如果你指的是它不能帮你找回设备，没错，你得需要尽职的警察的帮助才能做到，尽管如此，它仍然可以保护你的数据安全。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://support.apple.com/zh-cn/HT201365&quot;&gt;“查找我的 iPhone”激活锁&lt;/a&gt; 会使你的设备即使被盗甚至被重置以后，也无法激活使用，除非输入你之前激活时设置的 Apple ID 账号。当你打开“查找我的 iPhone”时，激活锁会自动启用。这样一来，保护你的 Apple ID 安全就变得非常重要，下文会讨论。&lt;/p&gt;

&lt;p&gt;另外，一旦设备失窃，你可以&lt;a href=&quot;https://support.apple.com/zh-cn/HT201472&quot;&gt;利用“查找我的 iPhone”锁定或抹掉设备&lt;/a&gt;，以保护你的数据。注意，如果你使用“家人共享”，任何家庭成员都可以帮助定位其他成员丢失的设备。&lt;/p&gt;

&lt;h2 id=&quot;启用-apple-id-双重认证&quot;&gt;启用 Apple ID 双重认证&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://support.apple.com/zh-cn/HT204915&quot;&gt;“双重认证”&lt;/a&gt;可以在 Apple ID 密码泄露的情况下，仍然限制他人对你的账号的访问权的安全机制。当在一部新设备或 Apple 网站上首次登录 Apple ID 时，你的设备上会出现提示，只能在你轻点允许并输入之后显示出的验证码，才能完成登录。&lt;/p&gt;

&lt;p&gt;与之前的&lt;a href=&quot;https://support.apple.com/zh-cn/HT204152&quot;&gt;“两步验证”&lt;/a&gt;不同，“双重认证”是一种改进的安全机制，它使用起来更方便，但条件是你必须拥有一部装有 iOS 9 或 OS X El Capitan 或更高版本的设备。&lt;/p&gt;

&lt;p&gt;使用“双重验证”需要注意的是，受信任的设备和电话号码的设置，你需要注意它们的安全。因为显示验证码时，设备需要解锁，所以更需要注意的是受信任的电话号码，注意不要设置为你手机上装的 SIM 卡的号码，因为如果你打开了短信的通知预览，通过短信接收的验证码无需解锁也能看到。另外，一旦你丢失了手机，你的 SIM 卡也随之丢失，你无法通过短信收到验证码，而窃贼却可以把他装进另一只手机来接收它（关于 SIM 卡的安全，下文会讲到）。一个更好的主意是设置为亲友的电话号码，比如和你的伴侣的手机互相设置对方的号码。&lt;/p&gt;

&lt;h2 id=&quot;防范-apple-id-钓鱼&quot;&gt;防范 Apple ID 钓鱼&lt;/h2&gt;

&lt;p&gt;如果你的设备被盗，通过伪装成 Apple 给你发送找回设备的钓鱼邮件或网站，是窃贼试图取得你的 Apple ID 密码的标准套路。在中国，利用钓鱼网站或邮件骗取密码，只是产业化的销赃渠道中的一环。&lt;/p&gt;

&lt;p&gt;即使你的设备没有丢失，骗子也有可能通过各种手段获取到你用来注册 Apple ID 的电子邮件地址，然后尝试骗取你的密码。一旦得手，它们会利用“查找我的 iPhone”锁定你的设备，并勒索赎金。当你支付赎金后，你就会发现他们并不会解锁你的设备。这进一步说明了保护 Apple ID 的重要性。&lt;/p&gt;

&lt;p&gt;无论你的设备是否丢失，都请注意：Apple 不可能知晓你的设备被盗，更不可能主动联系你帮助你找回设备。&lt;a href=&quot;https://support.apple.com/zh-cn/HT204759&quot;&gt;参见这里&lt;/a&gt;以了解更多防范钓鱼攻击的知识。&lt;/p&gt;

&lt;h2 id=&quot;使用可靠的电子邮件地址注册-apple-id&quot;&gt;使用可靠的电子邮件地址注册 Apple ID&lt;/h2&gt;

&lt;p&gt;如果你的 Apple ID 是用电子邮件地址注册的，毫无疑问，你还需要注意它的安全。首先，不能给你的 Apple ID 和电子邮件账号设置相同的密码；其次，避免使用在安全问题上劣迹斑斑的某些电子邮件服务商；最后，不要泄露这个电子邮件地址或用它注册其他服务。&lt;/p&gt;

&lt;p&gt;如果你的电子邮件服务商提供两步验证的选项，请启用它。&lt;/p&gt;

&lt;h2 id=&quot;保护你的-sim-卡&quot;&gt;保护你的 SIM 卡&lt;/h2&gt;

&lt;p&gt;如果你的 Apple ID 是用手机号码注册的，你需要保护你的 SIM 卡的安全。任何人都能利用你的 SIM 卡接收电话和短信。另外，由于国内对手机短信验证码安全性的过分依赖，很可能你的很多账号，包括银行账号，都倚仗你的 SIM 卡的安全。所以，&lt;a href=&quot;https://support.apple.com/zh-cn/HT201529&quot;&gt;利用 PIN 码锁定 SIM 卡&lt;/a&gt;是非常有必要的。任何人使用该卡开机时都必须输入 PIN 码才能解锁 SIM 卡。&lt;/p&gt;

&lt;p&gt;另外值得注意的是，通知预览可能会使通过短信接收到的验证码即使在设备锁定的情况下也能显示出来，这可以通过“信息”的&lt;a href=&quot;https://support.apple.com/zh-cn/HT201925#settings&quot;&gt;通知设置&lt;/a&gt;来关闭。&lt;/p&gt;

&lt;p&gt;即使有 PIN 码的保护，如果设备失窃，仍然应该第一时间联系运营商挂失。&lt;/p&gt;

&lt;h2 id=&quot;一旦设备丢失&quot;&gt;一旦设备丢失&lt;/h2&gt;

&lt;p&gt;利用下面的清单来执行必要的步骤：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;利用“查找 iPhone”应用或 &lt;a href=&quot;http://icloud.com/find&quot;&gt;icloud.com/find&lt;/a&gt; 定位、远程锁定或抹掉你的设备；即使设备已关机，它也会在一旦上线后执行指令&lt;/li&gt;
  &lt;li&gt;联系挂失手机 SIM 卡&lt;/li&gt;
  &lt;li&gt;修改 Apple ID、电子邮件以及其他账号的密码&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;不要&lt;/em&gt;理会任何要求你输入 Apple ID 密码的钓鱼邮件、网站或电话&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;不要&lt;/em&gt;从你的 Apple ID 中移除丢失的设备，以便保持激活锁&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;努力去做到这些，即使法律不给窃贼们铁窗和镣铐，我们也可以让他们除了一堆 iPhone 零件，什么也得不到。如果你只在 Apple Store 修理 iPhone，你还能让他们的零件销赃也受阻。&lt;/p&gt;</content><author><name>James Shih</name></author><category term="Apple" /><category term="iOS" /><category term="security" /><summary type="html">所有智能手机用户都面临一个共同的危险：你的手机可能会丢失，后果会很严重。</summary></entry><entry><title type="html">为 iOS 制作 HEVC 视频</title><link href="https://jshih.dev/2017/10/20/encoding-ios-playable-hevc-video.html" rel="alternate" type="text/html" title="为 iOS 制作 HEVC 视频" /><published>2017-10-20T13:54:16+00:00</published><updated>2017-10-20T13:54:16+00:00</updated><id>https://jshih.dev/2017/10/20/encoding-ios-playable-hevc-video</id><content type="html" xml:base="https://jshih.dev/2017/10/20/encoding-ios-playable-hevc-video.html">&lt;p&gt;iOS 11 以及 macOS High Sierra 最广为人知的新特性之一就是对 &lt;a href=&quot;https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding&quot;&gt;HEVC&lt;/a&gt; (H.265) 的支持。这种更高效的视频编码格式据称能减少 40% 的文件体积，这对喜欢用手机拍摄视频，尤其是 4K 视频的用户来说是个好消息。但这个新功能对于以前拍摄的视频并没有帮助，因为新的操作系统不会自动帮你重新用 HEVC 编码那些视频。幸运的是，我们仍然可以用 &lt;a href=&quot;https://www.ffmpeg.org/&quot;&gt;FFmpeg&lt;/a&gt; 来做到。&lt;/p&gt;

&lt;h2 id=&quot;安装-ffmpeg&quot;&gt;安装 FFmpeg&lt;/h2&gt;

&lt;p&gt;在 Mac 上可以用 Homebrew 来安装：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;brew &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;ffmpeg &lt;span class=&quot;nt&quot;&gt;--with-x265&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;转码&quot;&gt;转码&lt;/h2&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ffmpeg &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; input.mov &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt;:v libx265 &lt;span class=&quot;nt&quot;&gt;-tag&lt;/span&gt;:v hvc1 &lt;span class=&quot;nt&quot;&gt;-preset&lt;/span&gt; medium &lt;span class=&quot;nt&quot;&gt;-crf&lt;/span&gt; 28 &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt;:a copy output.mp4
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;参数解释：&lt;/p&gt;

&lt;dl&gt;
  &lt;dt&gt;&lt;code&gt;-c:v libx265&lt;/code&gt;&lt;/dt&gt;
  &lt;dd&gt;选择 libx265 作为视频编码器&lt;/dd&gt;
  &lt;dt&gt;&lt;code&gt;-tag:v hvc1&lt;/code&gt;&lt;/dt&gt;
  &lt;dd&gt;将视频轨道标记为 hvc1，而不是默认的 hev1，否则 macOS 和 iOS 不能播放&lt;/dd&gt;
  &lt;dt&gt;&lt;code&gt;-preset medium&lt;/code&gt;&lt;/dt&gt;
  &lt;dd&gt;（可选）编码方法预设，默认是 medium，这个设置主要用于权衡编码速度和文件大小&lt;/dd&gt;
  &lt;dt&gt;&lt;code&gt;-crf 28&lt;/code&gt;&lt;/dt&gt;
  &lt;dd&gt;（可选）恒定码率因数，默认是 28，这个设置决定需要的视频质量&lt;/dd&gt;
  &lt;dt&gt;&lt;code&gt;-c:a copy&lt;/code&gt;&lt;/dt&gt;
  &lt;dd&gt;直接拷贝音频数据，不做修改&lt;/dd&gt;
&lt;/dl&gt;

&lt;p&gt;关于可以自定的设置参数，请见 &lt;a href=&quot;https://trac.ffmpeg.org/wiki/Encode/H.265&quot;&gt;Encode/H.265&lt;/a&gt;。更多 FFmpeg 使用方法，可参考阅读&lt;a href=&quot;/2013/10/30/html5-video-for-everybody/&quot;&gt;《给每个人的 HTML5 视频》&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;经过以上编码过程后，视频的体积会减少很多。我用以上参数压缩了一段用 GoPro 拍摄的，长度为 3 分钟的 1080/30p 视频，大小仅 20M 左右，且没有明显的画质损失。考虑到源文件 500M 左右的大小，十分惊人。如果提高码率，例如设置 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-crf 16&lt;/code&gt;，可能肉眼就很难区分出和源文件的区别了。&lt;/p&gt;</content><author><name>James Shih</name></author><category term="Apple" /><category term="iOS" /><category term="ffmpeg" /><category term="mp4box" /><category term="hevc" /><summary type="html">iOS 11 以及 macOS High Sierra 最广为人知的新特性之一就是对 HEVC (H.265) 的支持。这种更高效的视频编码格式据称能减少 40% 的文件体积，这对喜欢用手机拍摄视频，尤其是 4K 视频的用户来说是个好消息。但这个新功能对于以前拍摄的视频并没有帮助，因为新的操作系统不会自动帮你重新用 HEVC 编码那些视频。幸运的是，我们仍然可以用 FFmpeg 来做到。</summary></entry><entry><title type="html">让旧 iPhone 变成 AirPlay 接收器</title><link href="https://jshih.dev/2017/10/13/using-iphone-as-airplay-receiver.html" rel="alternate" type="text/html" title="让旧 iPhone 变成 AirPlay 接收器" /><published>2017-10-13T08:56:12+00:00</published><updated>2017-10-13T08:56:12+00:00</updated><id>https://jshih.dev/2017/10/13/using-iphone-as-airplay-receiver</id><content type="html" xml:base="https://jshih.dev/2017/10/13/using-iphone-as-airplay-receiver.html">&lt;p&gt;想要 AirPlay 无线音箱？只要手上有一只旧 iPhone 加上任何带 3.5mm AUX 输入的音箱，你就可以自制一个，而且方法非常简单。&lt;/p&gt;

&lt;figure&gt;
  &lt;img alt=&quot;AirFloat in action&quot; src=&quot;/media/2017/2017-10-13-using-iphone-as-airplay-receiver/AirFloat-1.png&quot; srcset=&quot;2x /media/2017/2017-10-13-using-iphone-as-airplay-receiver/AirFloat-1.png&quot; /&gt;
  &lt;figcaption&gt;AirFloat&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/trenskow/AirFloat&quot;&gt;AirFloat&lt;/a&gt; 是一个 iOS 上的 AirPlay 接收器实现。虽然因为作者是从头实现了 AirPlay audio 标准，难免会有不稳定的情况出现，但就我的测试结果来看，AirFloat 是完全可用的。出于显而易见的原因，它只能在越狱的 iPhone 上安装。不过对于一只&lt;a href=&quot;https://www.cydiageeks.com/odysseusota-downgrade-iphone-ipad-ios-6-1-3-without-shsh.html&quot;&gt;降级到 iOS 6 的 iPhone 4s&lt;/a&gt; 来说，越狱是&lt;a href=&quot;https://canijailbreak.com/&quot;&gt;很容易的事情&lt;/a&gt;。&lt;/p&gt;

&lt;figure&gt;
  &lt;img alt=&quot;AirFloat streaming music&quot; src=&quot;/media/2017/2017-10-13-using-iphone-as-airplay-receiver/AirFloat-2.png&quot; srcset=&quot;2x /media/2017/2017-10-13-using-iphone-as-airplay-receiver/AirFloat-2.png&quot; /&gt;
  &lt;figcaption&gt;AirFloat 接收来自 iPhone 的音乐&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;在 Cydia 上搜索安装 AirFloat，直接运行，同一个 Wi-Fi 下的设备就可以看到 AirPlay 目标设备了。你可以从 iOS 设备或者 Mac/PC 上的 iTunes 连接到 AirFloat。如果你有好几个旧 iPhone 安装了 AirFloat，用 iTunes 甚至可以同时连接到所有设备，同步播放！&lt;/p&gt;</content><author><name>James Shih</name></author><category term="Gadgets" /><category term="AirPlay" /><category term="旧物利用" /><category term="iPhone" /><summary type="html">想要 AirPlay 无线音箱？只要手上有一只旧 iPhone 加上任何带 3.5mm AUX 输入的音箱，你就可以自制一个，而且方法非常简单。</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://jshih.dev/media/2017/2017-10-13-using-iphone-as-airplay-receiver/AirFloat-0.png" /><media:content medium="image" url="https://jshih.dev/media/2017/2017-10-13-using-iphone-as-airplay-receiver/AirFloat-0.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">卡拉 OK CD+G 复活记</title><link href="https://jshih.dev/2016/05/06/how-to-extract-cdg.html" rel="alternate" type="text/html" title="卡拉 OK CD+G 复活记" /><published>2016-05-06T13:03:44+00:00</published><updated>2016-05-06T13:03:44+00:00</updated><id>https://jshih.dev/2016/05/06/how-to-extract-cdg</id><content type="html" xml:base="https://jshih.dev/2016/05/06/how-to-extract-cdg.html">&lt;p&gt;1990 年代，曾经有一个昙花一现的流行，就是卡拉 OK &lt;a href=&quot;https://en.wikipedia.org/wiki/CD%2BG&quot;&gt;CD+G&lt;/a&gt; 光盘。在著名的 CD 规范&lt;a href=&quot;https://en.wikipedia.org/wiki/Rainbow_Books&quot;&gt;彩虹书&lt;/a&gt;里，CD+G 在&lt;a href=&quot;https://en.wikipedia.org/wiki/Red_Book_(CD_standard)&quot;&gt;红皮书&lt;/a&gt;的修订本中。这种光盘除了有和普通 CD 相同的音轨以外，还能储存低分辨率图像信息，支持的播放器可以把这些图像回放到电视上，不支持的播放器也可以把它当作普通的 CD 播放。因为它能储存简单图像，并且出现很早（1985 年发行了第一张 CD+G），它主要被用于卡拉 OK 内容上。当它 90 年代出现在中国以后，没有能力负担昂贵的 &lt;a href=&quot;https://en.wikipedia.org/wiki/LaserDisc&quot;&gt;LD&lt;/a&gt; 的一般家庭也可以在家 K 歌了。不过，因为不能储存真正的视频，它很快就被 &lt;a href=&quot;https://en.wikipedia.org/wiki/Video_CD&quot;&gt;VCD&lt;/a&gt; 以及之后的 &lt;a href=&quot;https://en.wikipedia.org/wiki/DVD&quot;&gt;DVD&lt;/a&gt; 取代了，所以听说过和用过的人并不多。&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/media/2016/2016-05-06-how-to-extract-cdg/cdg-logo.svg&quot; alt=&quot;CD+G Logo&quot; style=&quot;max-width:200px&quot; /&gt;
  &lt;figcaption&gt;CD+G Logo (维基百科)&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;最近，我很幸运地找到了一张压箱底的 CD+G 卡拉 OK 光盘，想到上一次看到这张光盘里的图像的时候，貌似我还没上小学，科技的发展已经不知经历了多少次沧海桑田。于是我就在网上搜索，怎样才能在今天的电脑上，把这老古董里的数据弄出来。&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/media/2016/2016-05-06-how-to-extract-cdg/karaoke-cdg-1.jpg&quot; srcset=&quot;/media/2016/2016-05-06-how-to-extract-cdg/karaoke-cdg-1.jpg 2x&quot; alt=&quot;CD+G Disc&quot; /&gt;
  &lt;img src=&quot;/media/2016/2016-05-06-how-to-extract-cdg/karaoke-cdg-2.jpg&quot; srcset=&quot;/media/2016/2016-05-06-how-to-extract-cdg/karaoke-cdg-2.jpg 2x&quot; alt=&quot;CD+G Disc&quot; /&gt;
  &lt;img src=&quot;/media/2016/2016-05-06-how-to-extract-cdg/karaoke-cdg-3.jpg&quot; srcset=&quot;/media/2016/2016-05-06-how-to-extract-cdg/karaoke-cdg-3.jpg 2x&quot; alt=&quot;CD+G Disc&quot; /&gt;
  &lt;figcaption&gt;CD+G 卡拉 OK 精選金曲&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;令人吃惊的是，虽然 CD+G 光盘多年前就已经在市场上难觅踪迹了，但它竟仍然以 &lt;a href=&quot;https://en.wikipedia.org/wiki/MP3%2BG&quot;&gt;MP3+G&lt;/a&gt; 文件（本质上就是每首歌一个 MP3 加一个包含图像数据的 .cdg 文件）的形式在网上流行着，并且有专门的播放软件。看到这么多内容在网上流传，很自然地想到，一定有从 CD+G 光盘中抓取这些内容的工具。事实上，我还真找到了一个开源的工具，专门做这种事。&lt;/p&gt;

&lt;h2 id=&quot;抓取&quot;&gt;抓取&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;http://www.kibosh.org/cdgtools/index.php&quot;&gt;cdgtools&lt;/a&gt;，一个 Linux 上的用 Python 写成的 CD+G 工具包。它包含的 cdgrip 就是我要用的抓取工具。在使用这个工具以前，要先用 &lt;a href=&quot;http://cdrdao.sourceforge.net/&quot;&gt;cdrdao&lt;/a&gt; 把光盘内容提取成 TOC/BIN 文件：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;cdrdao read-cd &lt;span class=&quot;nt&quot;&gt;--driver&lt;/span&gt; generic-mmc-raw &lt;span class=&quot;nt&quot;&gt;--device&lt;/span&gt; /dev/cdroms/cdrom0 &lt;span class=&quot;nt&quot;&gt;--read-subchan&lt;/span&gt; rw_raw mycd.toc
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;如果你用过 cdrdao，会注意到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--read-subchan&lt;/code&gt; 参数，这会让 cdrdao 读取&lt;a href=&quot;https://en.wikipedia.org/wiki/Compact_Disc_subcode&quot;&gt;子通道&lt;/a&gt;数据，也就是我们要的 CD+G 图像数据所在的地方。&lt;/p&gt;

&lt;p&gt;然后再用 cdgrip 将内容转换成 MP3 和 .cdg 文件：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;python cdgrip.py &lt;span class=&quot;nt&quot;&gt;--with-cddb&lt;/span&gt; mycd.toc
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;加上 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--with-cddb&lt;/code&gt; 参数后，cdgrip 会从 CDDB 上下载专辑和曲名信息。&lt;/p&gt;

&lt;h2 id=&quot;播放&quot;&gt;播放&lt;/h2&gt;

&lt;p&gt;上网搜索能找到不少能播放 MP3+G 的卡拉 OK 软件。经过尝试，我选择了 &lt;a href=&quot;http://www.karafun.com/karaokeplayer/&quot;&gt;Karafun Player&lt;/a&gt;，有趣的是这个播放器是 Windows 软件，而刚才的抓取工具却只支持 Linux。打开软件，在左边栏点击 Add a folder，选择刚才提取出的 MP3+G 文件所在的目录即可播放。&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/media/2016/2016-05-06-how-to-extract-cdg/karafun.png&quot; alt=&quot;Karafun Player&quot; /&gt;
  &lt;figcaption&gt;Karafun Player&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;然后，就没有然后了。我就看着这些很有潮流感的图片，听着这些很有潮流感的歌，坐上了去 90 年代的时光机。猜猜，这些都是什么歌？&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/media/2016/2016-05-06-how-to-extract-cdg/karaoke-1.png&quot; alt=&quot;卡拉 OK 1&quot; /&gt;&lt;br /&gt;
  &lt;img src=&quot;/media/2016/2016-05-06-how-to-extract-cdg/karaoke-2.png&quot; alt=&quot;卡拉 OK 2&quot; /&gt;&lt;br /&gt;
  &lt;img src=&quot;/media/2016/2016-05-06-how-to-extract-cdg/karaoke-3.png&quot; alt=&quot;卡拉 OK 3&quot; /&gt;&lt;br /&gt;
  &lt;img src=&quot;/media/2016/2016-05-06-how-to-extract-cdg/karaoke-4.png&quot; alt=&quot;卡拉 OK 4&quot; /&gt;&lt;br /&gt;
  &lt;img src=&quot;/media/2016/2016-05-06-how-to-extract-cdg/karaoke-5.png&quot; alt=&quot;卡拉 OK 5&quot; /&gt;
&lt;/figure&gt;

&lt;p&gt;答案：&lt;/p&gt;

&lt;ul style=&quot;display:inline-block;-webkit-transform:rotate(180deg);transform:rotate(180deg)&quot;&gt;
  &lt;li&gt;明明白白我的心&lt;/li&gt;
  &lt;li&gt;大约在冬季&lt;/li&gt;
  &lt;li&gt;明天你是否依然爱我&lt;/li&gt;
  &lt;li&gt;你看你看月亮的脸&lt;/li&gt;
  &lt;li&gt;来生缘&lt;/li&gt;
&lt;/ul&gt;</content><author><name>James Shih</name></author><category term="娱乐" /><category term="CD+G" /><category term="复古" /><summary type="html">1990 年代，曾经有一个昙花一现的流行，就是卡拉 OK CD+G 光盘。在著名的 CD 规范彩虹书里，CD+G 在红皮书的修订本中。这种光盘除了有和普通 CD 相同的音轨以外，还能储存低分辨率图像信息，支持的播放器可以把这些图像回放到电视上，不支持的播放器也可以把它当作普通的 CD 播放。因为它能储存简单图像，并且出现很早（1985 年发行了第一张 CD+G），它主要被用于卡拉 OK 内容上。当它 90 年代出现在中国以后，没有能力负担昂贵的 LD 的一般家庭也可以在家 K 歌了。不过，因为不能储存真正的视频，它很快就被 VCD 以及之后的 DVD 取代了，所以听说过和用过的人并不多。</summary></entry><entry><title type="html">如何隐藏有问题的 Windows 10 更新</title><link href="https://jshih.dev/2016/03/11/how-to-hide-problematic-windows-10-updates.html" rel="alternate" type="text/html" title="如何隐藏有问题的 Windows 10 更新" /><published>2016-03-11T00:03:44+00:00</published><updated>2016-03-11T00:03:44+00:00</updated><id>https://jshih.dev/2016/03/11/how-to-hide-problematic-windows-10-updates</id><content type="html" xml:base="https://jshih.dev/2016/03/11/how-to-hide-problematic-windows-10-updates.html">&lt;p&gt;微软大力推广的 Windows 10，是一个快速更新的操作系统，把用户当 QA 使。最近更是连续更新出包（&lt;a href=&quot;http://news.softpedia.com/news/windows-10-users-upset-with-microsoft-resetting-default-apps-over-and-over-again-496747.shtml&quot;&gt;其一&lt;/a&gt;、&lt;a href=&quot;http://news.softpedia.com/news/how-to-fix-issues-caused-by-windows-10-cumulative-update-kb3140743-501298.shtml&quot;&gt;其二&lt;/a&gt;、&lt;a href=&quot;http://news.softpedia.com/news/it-might-happen-again-windows-10-cumulative-update-kb3140768-fails-to-install-501522.shtml&quot;&gt;其三&lt;/a&gt;）。再加上 Windows 10 移除了关闭或者隐藏更新的功能，用户只能眼睁睁看着有问题的更新被卸掉了又被自动重新安装。&lt;/p&gt;

&lt;p&gt;真是应了那句话：微软的怪毛病，还得靠微软隐藏的工具解决（&lt;a href=&quot;/2010/09/10/%E2%80%9C%E6%97%A0%E6%B3%95%E6%89%93%E5%BC%80outlook%E7%AA%97%E5%8F%A3%E2%80%9D%E7%9A%84%E9%97%AE%E9%A2%98/&quot;&gt;即视感&lt;/a&gt;）。事实上，微软自己也对更新缺乏自信，所以它&lt;a href=&quot;https://support.microsoft.com/en-us/kb/3073930&quot;&gt;还是提供了一个疑难解答工具（KB3073930）&lt;/a&gt;，专门用来显示或隐藏更新……&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/media/2016/2016-03-11-how-to-hide-problematic-windows-10-updates/wushowhide-diagcab.png&quot; srcset=&quot;/media/2016/2016-03-11-how-to-hide-problematic-windows-10-updates/wushowhide-diagcab.png 2x&quot; alt=&quot;wushowhide-diagcab&quot; /&gt;
  &lt;figcaption&gt;显示或隐藏更新疑难解答&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;选择 &lt;em&gt;隐藏更新&lt;/em&gt;（Hide updates）选项，系统会首先检查目前可以安装的更新，然后只需要在列表中勾选需要隐藏的更新并确认即可。这些更新将暂时不会被安装，也不能在设置 App 中看到。&lt;/p&gt;

&lt;p&gt;显然，没有改好的 Bug 仍然是 Bug，你迟早还是需要更新。当微软终于推出了补丁的补丁可以一起服用的时候，你会想要重新显示这些更新。这时候仍然需要用这个工具，选择 &lt;em&gt;显示隐藏的更新&lt;/em&gt;（Show hidden updates）。&lt;/p&gt;</content><author><name>James Shih</name></author><category term="PC" /><category term="Windows 10" /><summary type="html">微软大力推广的 Windows 10，是一个快速更新的操作系统，把用户当 QA 使。最近更是连续更新出包（其一、其二、其三）。再加上 Windows 10 移除了关闭或者隐藏更新的功能，用户只能眼睁睁看着有问题的更新被卸掉了又被自动重新安装。</summary></entry><entry><title type="html">本地网络禁止 22 端口出站时怎样使用 Git+SSH</title><link href="https://jshih.dev/2016/02/24/git-ssh-with-port-22-outbound-blocked.html" rel="alternate" type="text/html" title="本地网络禁止 22 端口出站时怎样使用 Git+SSH" /><published>2016-02-24T05:12:27+00:00</published><updated>2016-02-24T05:12:27+00:00</updated><id>https://jshih.dev/2016/02/24/git-ssh-with-port-22-outbound-blocked</id><content type="html" xml:base="https://jshih.dev/2016/02/24/git-ssh-with-port-22-outbound-blocked.html">&lt;p&gt;如果你习惯用 SSH 协议而不是 HTTPS 去连接 GitHub，那么你很可能遇到过：当你 push 代码到 GitHub 的时候出现 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;port 22: Connection refused&lt;/code&gt; 的错误。大部分情况下，这是因为你所在的本地网络防火墙禁止 22 端口出站连接。事实上，对于企业网络安全而言，这么做是&lt;a href=&quot;https://serverfault.com/a/25566&quot;&gt;非常有必要的&lt;/a&gt;。但是，突然你就不能 push 代码了，我现在急着提交一个 hotfix，怎么办？&lt;/p&gt;

&lt;h2 id=&quot;使用后备-gitssh-端口&quot;&gt;使用后备 Git+SSH 端口&lt;/h2&gt;

&lt;p&gt;幸运的是，常用的 Git 托管服务大多提供了通过 HTTPS 的 443 端口连接 Git+SSH 服务的功能。只需要修改你的 SSH 配置文件即可启用。&lt;/p&gt;

&lt;p&gt;编辑 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.ssh/config&lt;/code&gt; 文件：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Host github.com
  Hostname ssh.github.com
  Port 443

Host bitbucket.org
  Hostname altssh.bitbucket.org
  Port 443

Host gitlab.com
  Hostname altssh.gitlab.com
  Port 443
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;使用代理服务器&quot;&gt;使用代理服务器&lt;/h2&gt;

&lt;p&gt;如果你使用的 Git 托管服务没有提供后备 Git+SSH 端口，或者你所在的本地网络必须通过代理服务器访问外网，则可以利用 &lt;a href=&quot;http://linux.die.net/man/1/nc&quot;&gt;nc&lt;/a&gt; 来达成。例如：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Host git.somecompany.com
  ProxyCommand nc -x 10.0.146.35:3128 %h %p
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;</content><author><name>James Shih</name></author><category term="Linux" /><category term="Git" /><category term="SSH" /><summary type="html">如果你习惯用 SSH 协议而不是 HTTPS 去连接 GitHub，那么你很可能遇到过：当你 push 代码到 GitHub 的时候出现 port 22: Connection refused 的错误。大部分情况下，这是因为你所在的本地网络防火墙禁止 22 端口出站连接。事实上，对于企业网络安全而言，这么做是非常有必要的。但是，突然你就不能 push 代码了，我现在急着提交一个 hotfix，怎么办？</summary></entry><entry><title type="html">第一次网购到假货的经历</title><link href="https://jshih.dev/2016/02/03/knockoff-product-from-chinese-online-shop.html" rel="alternate" type="text/html" title="第一次网购到假货的经历" /><published>2016-02-03T00:00:00+00:00</published><updated>2016-02-03T00:00:00+00:00</updated><id>https://jshih.dev/2016/02/03/knockoff-product-from-chinese-online-shop</id><content type="html" xml:base="https://jshih.dev/2016/02/03/knockoff-product-from-chinese-online-shop.html">&lt;p&gt;第一次听说淘宝网，应该已经早在 2006 年以前，那时候的中国的电子商务行业，可以说几乎还不存在，以至于淘宝网还&lt;a href=&quot;http://tech.qq.com/a/20060906/000310.htm&quot;&gt;不得不靠流氓软件恶意弹窗来做推广&lt;/a&gt;。而今天这些事几乎没有人记得，因为如今中国的电商已经碾压传统商业形式，&lt;a href=&quot;http://www.stats.gov.cn/tjsj/zxfb/201508/t20150803_1224544.html&quot;&gt;2014 年产生了超过 16 万亿元的销售额&lt;/a&gt;。淘宝网的创始人马云也顺理成章地成为了中国的风云人物，无数人崇拜的对象，连很多不是他讲的心灵鸡汤文也冒署其名，尽管 20 年前他还在奋斗路上被人各种蔑视。&lt;/p&gt;

&lt;p&gt;我第一次网上购物，大约是在 2009 年在淘宝网上。那时候，中国的网上支付还处于十分原始的阶段，远远落后于国外。各大银行糟烂的“网银”系统，各种 ActiveX 控件、U 盾、驱动程序、IE 安全配置……至今令早年尝鲜的人们感到恶心。而今天，中国的网上支付已经变得无比快捷和安全，其先进程度和对日常生活的渗透程度已经世界领先。&lt;/p&gt;

&lt;p&gt;毫无疑问，中国的电子商务发展是极为迅猛的，大大提高了中国人的生活品质。但凡事都有避不过的两面性。假货，简直是中国电商永远逃不掉的阴影。然而，多年来这个问题我竟然没遇到过一次，直到今天。而且我还不是从饱受假货指责的淘宝网上买到了这个假货，而是不久前&lt;a href=&quot;http://tech.qq.com/a/20140722/066387.htm&quot;&gt;老板声称对假货“零容忍”&lt;/a&gt;的京东。&lt;/p&gt;

&lt;h2 id=&quot;chinese-knockoff-chinese-brand&quot;&gt;Chinese Knockoff Chinese Brand&lt;/h2&gt;

&lt;p&gt;我本来买的是一个很便宜的东西：小米手环的彩色腕带，才 20 块钱的东西。&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/media/2016/2016-02-03-fake-product-from-chinese-online-shop/product-page.png&quot; alt=&quot;product-page&quot; /&gt;
  &lt;figcaption&gt;卖家的产品页面&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;如同以往无数价格便宜的小东西一样，我没有仔细看商品的评论就匆匆下了单（甚至没注意到非京东自营，而是第三方入驻卖家）。可是这一次，当三天后商品到我手上时，我傻了眼。&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/media/2016/2016-02-03-fake-product-from-chinese-online-shop/comparison-of-genuine-and-fake.jpg&quot; srcset=&quot;/media/2016/2016-02-03-fake-product-from-chinese-online-shop/comparison-of-genuine-and-fake.jpg 2x&quot; alt=&quot;comparison-of-genuine-and-fake&quot; /&gt;
  &lt;figcaption&gt;我买到的山寨小米手环腕带&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;从包装到实物，没有一处小米品牌标识或者生产厂家，甚至连一个字都没有。腕带本身做工粗糙，材质软趴趴的，还有毛刺，手环机芯嵌进去戴在手上动一动自己就掉出来了。和一同购买的原厂小米手环一对比，区别特别明显。令我不禁唏嘘：山寨了别人一辈子，想不到也有被别人山寨的时候。&lt;/p&gt;

&lt;p&gt;如果是别人，可能也就觉得为个十几二十块钱的东西，懒得再说什么。可是，这是我第一次网购到假货，而且完全没法凑合用。于是我便联系卖家，要求退货。&lt;/p&gt;

&lt;h2 id=&quot;熟悉的伎俩要求退货却被要求出具检测报告&quot;&gt;熟悉的伎俩：要求退货，却被要求出具检测报告&lt;/h2&gt;

&lt;p&gt;如同我很久以前就听说过的很多网购假货事件一样，卖家用十分理直气壮的语气回复我：“到小米售后出具检测报告先”（此为聊天记录原话，未截图）。就因为这句话，让我不得不跟他玩到底。于是，我使用了京东提供的“申请售后”功能。&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/media/2016/2016-02-03-fake-product-from-chinese-online-shop/first-contact.png&quot; alt=&quot;first-contact&quot; /&gt;
  &lt;figcaption&gt;申请退货后卖家的回复&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;这个伎俩可能会难住大部分人，所以也可以认为是卖家应对此类要求的标准回复。因为事实上厂家和售后并没有义务出具什么“检测报告”，我如果去找小米售后只会吃闭门羹。况且大部分人嫌麻烦也就放弃了。&lt;/p&gt;

&lt;p&gt;难道就这样算了？很不幸，这位卖家不仅出言不慎，而且还遇上了我。&lt;/p&gt;

&lt;h2 id=&quot;有力的反驳提请交易纠纷处理便立即补发正品&quot;&gt;有力的反驳：提请交易纠纷处理，便立即补发正品&lt;/h2&gt;

&lt;p&gt;“申请售后”是卖家自己处理，不会引起京东介入，事实上即便是下一步“交易纠纷”也不会立即引起京东介入，除非卖家的第二次处理仍然不能令我满意。这样的设计意图，应该是把大量的类似事件过滤在卖家这一层，以节省客服人力。这可能也从另一个侧面说明此类事件的数量。尽管如此，我还是只能按照规则办事。&lt;/p&gt;

&lt;p&gt;在说明中，我详细解释了为什么我认为我收到的产品是假货，并且提到了卖家要求我到小米售后出具检测报告的不合理性，还提供了照片证据，请京东平台予以公正处理。&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/media/2016/2016-02-03-fake-product-from-chinese-online-shop/second-contact.png&quot; alt=&quot;second-contact&quot; /&gt;
  &lt;figcaption&gt;提请交易纠纷后卖家的回复&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;令我意外的是，卖家立即打电话来，客气地解释“仓库工作人员发错货了，马上给您补发原装腕带”。四天后，我收到了卖家补发的正品小米腕带，交易纠纷宣告解决。&lt;/p&gt;

&lt;h2 id=&quot;原来只是一场误会&quot;&gt;原来只是一场误会？&lt;/h2&gt;

&lt;p&gt;说实话，我十分希望这真的只是一场误会，多年的亲身经验让我认为在网上做生意的绝大部分是诚实的商人。可是当我发现收到的正品包装与之前的假货如此相似，只是多了一个标签写明生产厂家为小米旗下工厂，尤其是当我看到其他消费者的评论后，我陷入了沉思。&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/media/2016/2016-02-03-fake-product-from-chinese-online-shop/comments-from-other-buyers.png&quot; alt=&quot;comments-from-other-buyers&quot; /&gt;
  &lt;figcaption&gt;其他消费者的评论&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h2 id=&quot;一些思考&quot;&gt;一些思考&lt;/h2&gt;

&lt;p&gt;除了多打了几个字，多照了几张照片，这次网购没有让我损失什么，可以说是比较圆满地结束了。但是这不禁令我思考：为什么有人会在网上明目张胆地卖假货？为什么假货已经成为了电商的潜规则？&lt;/p&gt;

&lt;p&gt;我认为，答案应该和“为什么中国的盗版软件如此猖獗？”这类问题有些像。“人之初，性本善”，如果不是大环境的影响，人为什么要做恶？&lt;/p&gt;

&lt;p&gt;我想到，中国电商的迅猛发展，其支柱不外乎：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;超低的价格&lt;/li&gt;
  &lt;li&gt;便捷的支付方式&lt;/li&gt;
  &lt;li&gt;发达的物流业&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;其中“超低的价格”我放在首位。因为电商天然的直销优势，又能节省高昂的店面维护和消费者的选择成本，自然会产生极具竞争力的价格。可是中国电商为了扩大市场，把这一点做得太过分，像我们熟悉的“双11”“双12”活动，每次都能打出惊人的 5 折、3 折等等，使人们在潜意识中把“互联网”和不合常理的“超低价”联系在一起，就像中国人不愿为使用软件和内容而付费一样。长此以往就会迫使商家以不符合市场规律的价格销售商品，而商家为了保障利润，不得不开始销售假货。一家售假，全都售假，开启劣币驱逐良币的恶性循环。&lt;/p&gt;

&lt;p&gt;我并不会对电商失去信心，相反我认为电商才是商业的未来。但是，“信誉”二字，并不是换一种商业模式就能不讲的，它不仅是商业的基本，更是一国的根本。如果不能造就一个遵守“信誉”二字的环境，那未来能有希望吗？&lt;/p&gt;</content><author><name>James Shih</name></author><category term="线上生活" /><category term="网上购物" /><category term="假货" /><summary type="html">第一次听说淘宝网，应该已经早在 2006 年以前，那时候的中国的电子商务行业，可以说几乎还不存在，以至于淘宝网还不得不靠流氓软件恶意弹窗来做推广。而今天这些事几乎没有人记得，因为如今中国的电商已经碾压传统商业形式，2014 年产生了超过 16 万亿元的销售额。淘宝网的创始人马云也顺理成章地成为了中国的风云人物，无数人崇拜的对象，连很多不是他讲的心灵鸡汤文也冒署其名，尽管 20 年前他还在奋斗路上被人各种蔑视。</summary></entry><entry><title type="html">WebRTC 之点对点连接</title><link href="https://jshih.dev/2015/05/16/webrtc-peer-connection/" rel="alternate" type="text/html" title="WebRTC 之点对点连接" /><published>2015-05-16T00:00:00+00:00</published><updated>2015-05-16T00:00:00+00:00</updated><id>https://jshih.dev/2015/05/16/webrtc-peer-connection</id><content type="html" xml:base="https://jshih.dev/2015/05/16/webrtc-peer-connection/">&lt;h2 id=&quot;webrtc-的精髓点对点连接&quot;&gt;WebRTC 的精髓——点对点连接&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;/2015/04/21/webrtc-video-capture/&quot;&gt;上一篇文章&lt;/a&gt;中，主要讲了浏览器怎样获取用户设备上的视频流，并且显示在 HTML5 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;video&amp;gt;&lt;/code&gt; 标签中。这一篇文章则是让这一切变得有用起来：把视频流发送到另一位用户的浏览器上。WebRTC 特有的点对点连接，可以让服务器不必中转大量的视频数据，让通讯的速度、私密性得到更好的保障。这是 WebRTC 相对于 WebSocket 等技术最大的优势，也就是它存在的根本。&lt;/p&gt;

&lt;h2 id=&quot;怎样建立点对点连接&quot;&gt;怎样建立点对点连接&lt;/h2&gt;

&lt;p&gt;要建立一个点对点连接，并在其上传送视频内容，我们需要两个浏览器互相交换以下信息：&lt;/p&gt;

&lt;p&gt;1.视频流的元数据，包括分辨率和编码格式等
2.各自的网络连接情况，包括用于 NAT 穿透的信息&lt;/p&gt;

&lt;p&gt;WebRTC 用于实现了以上信息交换，提供给浏览器 JavaScript 平台的 API 就是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RTCPeerConnection&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;为了完成以上第 1 种信息的交换，我们用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RTCPeerConnection&lt;/code&gt; 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;createOffer()&lt;/code&gt; 方法生成一个 Offer，它是以 &lt;a href=&quot;https://en.wikipedia.org/wiki/Session_Description_Protocol&quot;&gt;SDP&lt;/a&gt;（Session Description Protocol，会话描述协议）格式传送的。对方收到 Offer 后，应该生成一个 Answer 并发回，这个 Answer 同样是 SDP 格式的。通信的双方通过调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setLocalDescription()&lt;/code&gt; 方法，把自己生成的 SDP 设置成本地描述；通过调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setRemoteDescription()&lt;/code&gt; 方法，把对方发给自己的 SDP 设置成远程描述。以上的这个过程，被统称为 &lt;a href=&quot;https://en.wikipedia.org/wiki/JavaScript_Session_Establishment_Protocol&quot;&gt;JSEP&lt;/a&gt;（JavaScript Session Establishment Protocol，JavaScript 会话建立协议）。&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/media/2015/2015-05-16-webrtc-peer-connection/jsep-architecture.png&quot; alt=&quot;jsep-architecture&quot; /&gt;
  &lt;figcaption&gt;JSEP 结构（via html5rocks.com）&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;对于以上第 2 种信息的交换，则是通过 &lt;a href=&quot;https://en.wikipedia.org/wiki/Interactive_Connectivity_Establishment&quot;&gt;ICE&lt;/a&gt;（Interactive Connectivity Establishment，交互式连接建立）完成的。对于点对点连接最简单的设想是，大家都连接在一个网络中，只要双方都知道对方的 IP 地址，我就可以直接发送数据。但现实永远不会这么简单：如今的网络世界中，绝大部分设备并不是直接连接到互联网上，具有一个公网 IP 地址，而是处在层层的路由器和防火墙的背后，这也就使得直接建立连接变得不可能。不过，如果双方都向一个公网上的服务器发送一个请求，这台服务器可以获取到双方的公网地址，这样就可以让双方知晓怎样和对方进行通讯。这就是 STUN 服务器。&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/media/2015/2015-05-16-webrtc-peer-connection/webrtc-infrastructure.png&quot; alt=&quot;webrtc-infrastructure&quot; /&gt;
  &lt;figcaption&gt;信令交换与 STUN/TURN 服务器（via html5rocks.com）&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;当双方完成了 Offer 和 Answer 的交换后，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RTCPeerConnection&lt;/code&gt; 便利用 STUN 服务器收集 ICE 候选，也就是双方建立连接的多个可能途径，然后在这些候选中挑选最优化的一个，用以建立点对点连接。&lt;/p&gt;

&lt;p&gt;STUN 还有一个扩展，即 TURN 服务器。除了实现 STUN 的全部功能外，当双方由于某种原因（如防火墙）还是没法建立点对点连接时，TURN 服务器可以起到中转的作用，让双方可以绕过防火墙进行通讯（事实上绝大多数防火墙被配置为允许从内部向外主动发起的连接）。&lt;/p&gt;

&lt;h2 id=&quot;实例代码&quot;&gt;实例代码&lt;/h2&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// 建立一个 RTCPeerConnection 实例，这里设置了 STUN 或 TURN 服务器&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;servers&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;iceServers&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;stun:turn.mywebrtc.com&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;turn:turn.mywebrtc.com&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;credential&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;siEFid93lsd1nF129C4o&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;webrtcuser&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;peerConnection&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;RTCPeerConnection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;servers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 交换 ICE 候选，通过 WebSocket 发送&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;peerConnection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;onicecandidate&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;candidate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;ICE candidate&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;candidate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;emit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;roomToken&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;candidate&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;candidate&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 接收到对方添加的视频流时，显示在本地的 &amp;lt;video&amp;gt; 标签中&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;peerConnection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;onaddstream&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;remoteMediaStream&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;remoteVideo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;src&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createObjectURL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;remoteMediaStream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 在这里添加上一篇文章中获取到的本地视频流&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;peerConnection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addStream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;localMediaStream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 包装一个 Offer&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;peerConnection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createOffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;gotLocalDescription&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;handleError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 有了 Offer，通过 WebSocket 发送给对方&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;gotLocalDescription&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;desc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;peerConnection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setLocalDescription&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;desc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;emit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;roomToken&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;sdp&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;desc&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 在 WebSocket 中接收到信息时&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;socket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;socketId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sdp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 接收到 Offer 时，创建 Answer 并发送&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;desc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;RTCSessionDescription&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sdp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;peerConnection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setRemoteDescription&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;desc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;peerConnection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createAnswer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;gotLocalDescription&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;handleError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;handleError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 接收到 ICE 候选时，让 RTCPeerConnection 收集它，稍后它将在这些候选方式中挑选最佳者建立连接&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 注意：RTCPeerConnection 要在 setLocalDescription 后才能开始收集 ICE 候选&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;peerConnection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addIceCandidate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;RTCIceCandidate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;candidate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;下期预告&quot;&gt;下期预告&lt;/h2&gt;

&lt;p&gt;本文简述了点对点连接的建立过程中，双方信息交换的流程。&lt;/p&gt;

&lt;p&gt;事实上，在 WebRTC 的技术规范中并没有规定这些信息交换要通过什么途径进行，而是把选择的自由留给留给上层的应用程序。在开发 Web App 时，我们可能最先想到的是 WebSocket，但是也可以采用 SIP 或者 Jingle，或者 XMPP 信息服务比如 OpenFire 之类，等等。在本文中，我们使用了 WebSocket 实现信令交换。&lt;/p&gt;

&lt;p&gt;信令服务的任务并不止于连接的建立过程。我们还需要把诸如有人加入聊天、有人挂断这样的信息通知给所有客户端。这些信息既可以用与建立连接时相同的机制进行交换，也可以用 WebRTC RTCDataChannel，这是 WebRTC 的数据传送通道，但这个通道只能在点对点连接建立好以后才能使用，也就是说它不能代替 WebSocket 等，但可以在连接建立后把信令交换的任务接管过来。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;javascript:&quot;&gt;WebRTC 之服务器&lt;/a&gt; 将会介绍如何使用 node.js 和 socket.io 建立一个 WebSocket 服务器，以提供信令服务；以及如何搭建 STUN/TURN 服务器。&lt;/p&gt;</content><author><name>James Shih</name></author><category term="Web 开发" /><category term="WebRTC" /><category term="HTML5" /><category term="JavaScript" /><summary type="html">WebRTC 的精髓——点对点连接</summary></entry><entry><title type="html">WebRTC 之视频捕获</title><link href="https://jshih.dev/2015/04/21/webrtc-video-capture/" rel="alternate" type="text/html" title="WebRTC 之视频捕获" /><published>2015-04-21T00:00:00+00:00</published><updated>2015-04-21T00:00:00+00:00</updated><id>https://jshih.dev/2015/04/21/webrtc-video-capture</id><content type="html" xml:base="https://jshih.dev/2015/04/21/webrtc-video-capture/">&lt;h2 id=&quot;什么是-webrtc&quot;&gt;什么是 WebRTC&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/WebRTC&quot;&gt;WebRTC&lt;/a&gt;（Web Real-Time Communication）是实现浏览器之间点对点实时通讯的一套技术规范（现在也支持 iOS 和 Android 应用）。2010 年 5 月，Google 收购了 VoIP 开发商 Global IP Solutions，在其技术基础上开发了 WebRTC，并于一年后开源。目前，WebRTC 1.0 是 &lt;a href=&quot;http://www.w3.org/TR/webrtc/&quot;&gt;W3C 的标准草案&lt;/a&gt;，Chrome 30+ 和 Firefox 36+ 实现了&lt;a href=&quot;http://caniuse.com/#feat=rtcpeerconnection&quot;&gt;对 WebRTC 的支持&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;以往，浏览器间的通讯绝大部分还必须要通过服务器中转来间接实现，这需要浏览器和服务器间进行全双工通讯。尽管我们有老的技术（如轮询、&lt;a href=&quot;https://en.wikipedia.org/wiki/Comet_(programming)&quot;&gt;Comet&lt;/a&gt;），也有新技术（如 &lt;a href=&quot;https://en.wikipedia.org/wiki/WebSocket&quot;&gt;WebSocket&lt;/a&gt;）来实现，但它们都不是为浏览器间直接通讯而设计的。这些通过服务器中转的方式不可避免地带来了明显的开销，通讯的时延和速度不理想，数据的私密性也难以保证。而 WebRTC 就是专门解决这个问题的。&lt;/p&gt;

&lt;p&gt;利用 WebRTC 可以在客户端之间传送任意数据，但它目前最吸引人的应用还是实时视频通讯。多年来，这样的应用都是通过浏览器插件（如 Flash 和 Silverlight）来实现，但 HTML5 的崛起改变了一切，新的 JavaScript API，极大增强了前端代码对硬件的控制能力：GPS、加速度计、陀螺仪、电池、GPU、视频和音频……等等。为了实现视频通讯，我们需要用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getUserMedia&lt;/code&gt;，也就是 Stream API，来获取设备的视频和音频设备，具体来讲，就是摄像头和麦克风。目前，只要支持 WebRTC 的浏览器都&lt;a href=&quot;http://caniuse.com/#feat=stream&quot;&gt;支持 Stream API&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;WebRTC 系列文章将一步步讲述一个实时通讯应用是怎样实现的，而本文的内容就是第一步：捕获视频。在本例中，我们将获取摄像头的视频数据，然后显示在页面上一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;video&amp;gt;&lt;/code&gt; 标签中。&lt;/p&gt;

&lt;h2 id=&quot;简单的开始&quot;&gt;简单的开始&lt;/h2&gt;

&lt;p&gt;首先，我们在页面上放一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;video&amp;gt;&lt;/code&gt; 标签：&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;video&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;local&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;autoplay&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/video&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;然后，调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getUserMedia&lt;/code&gt;，它的三个参数如下：&lt;/p&gt;

&lt;dl&gt;
  &lt;dt&gt;&lt;code&gt;constraints&lt;/code&gt;&lt;/dt&gt;
  &lt;dd&gt;指定媒体规格（如视频分辨率、帧速率等），Chrome 和 Firefox 上有所不同，见后文&lt;/dd&gt;
  &lt;dt&gt;&lt;code&gt;successCallback&lt;/code&gt;&lt;/dt&gt;
  &lt;dd&gt;成功后的回调函数，返回一个 `MediaStream` 媒体流对象作为参数&lt;/dd&gt;
  &lt;dt&gt;&lt;code&gt;errorCallback&lt;/code&gt;&lt;/dt&gt;
  &lt;dd&gt;失败后的回调函数&lt;/dd&gt;
&lt;/dl&gt;

&lt;p&gt;获取到媒体流对象后，该怎样把它交给 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;video&amp;gt;&lt;/code&gt; 标签呢？答案是&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL&quot;&gt;对象 URL&lt;/a&gt;，对象 URL 可以代表某一个 File 对象或 Blob 对象，而我们拿到的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MediaStream&lt;/code&gt; 对象就是一个 Blob 对象。利用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;URL.createObjectURL&lt;/code&gt; 方法来创建 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MediaStream&lt;/code&gt; 对象的 URL，并填入 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;video&amp;gt;&lt;/code&gt; 标签的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src&lt;/code&gt; 属性就行了。&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;localVideo&lt;/span&gt;        &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;video.local&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;localMediaStream&lt;/span&gt;  &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;localMediaURL&lt;/span&gt;     &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getUserMedia&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;callback&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;navigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getUserMedia&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;undefined&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;navigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getUserMedia&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;navigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;webkitGetUserMedia&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;navigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;mozGetUserMedia&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;navigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getUserMedia&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;undefined&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;getUserMedia not supported.&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;navigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getUserMedia&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;video&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;audio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;callback&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;getUserMedia failed: &lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;getUserMedia&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;localMediaStream&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;localVideo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;src&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;localMediaURL&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createObjectURL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;localMediaStream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;在有摄像头的电脑上，用 Chrome 或 Firefox 打开这个页面。浏览器会询问你是否允许页面访问你的摄像头，点击允许后，页面上就会显示出视频画面。&lt;/p&gt;

&lt;h2 id=&quot;媒体规格&quot;&gt;媒体规格&lt;/h2&gt;

&lt;p&gt;以上代码中，我们只是简单地指定媒体规格为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{ video: true, audio: true }&lt;/code&gt;，要求浏览器返回视频和音频，而对具体规格没有要求。事实上，如果你摄像头能输出 720p 甚至 1080p 的视频，用以上代码也只能获取到 VGA 规格。所以我们需要对规格做进一步设置。&lt;/p&gt;

&lt;p&gt;不幸的是，目前 Chrome 上的实现 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;webkitGetUserMedia&lt;/code&gt; 支持的规格格式遵循一个&lt;a href=&quot;http://tools.ietf.org/html/draft-alvestrand-constraints-resolution-00&quot;&gt;早期的规范&lt;/a&gt;，而不是&lt;a href=&quot;http://w3c.github.io/mediacapture-main/getusermedia.html#mediastreamconstraints&quot;&gt;最新的规范&lt;/a&gt;。所以，我们得先判断浏览器类型，再予以指定：&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getMediaConstraints&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;navigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;webkitGetUserMedia&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Chrome&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;video&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;mandatory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;minWidth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;640&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;minHeight&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;480&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;optional&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;minWidth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1280&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;minHeight&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;720&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;facingMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}]&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;audio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// Firefox&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;video&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;640&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;ideal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1280&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;480&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;ideal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;720&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;facingMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;audio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;使用以上的规格，就可以在摄像头支持时使用 720p 格式，若不支持则回落到 VGA 格式。如果是在手机等移动设备上，则优先选择自拍摄像头。Firefox 38 以前的版本支持也不完全符合标准，不支持 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ideal&lt;/code&gt; 参数，所以只会返回 VGA 格式。使用这些规格的时候需要谨慎，因为如果设备连你指定的最低标准都达不到，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getUserMedia&lt;/code&gt; 会失败。&lt;/p&gt;

&lt;h2 id=&quot;一些润色&quot;&gt;一些润色&lt;/h2&gt;

&lt;p&gt;你也许在尝试以上例子的时候，会感到有些不对劲，跟平时使用其他聊天软件有点不同。事实上，大部分聊天软件里，你看到自己的画面是左右翻转的，就像镜子一样。因为人们大都习惯照镜子，所以当你看到没有翻转的视频时会觉得别扭，想捋一捋左边的头发却伸出了右手。&lt;/p&gt;

&lt;p&gt;其实这个翻转很容易实现，用 CSS3 Transform 就行了：&lt;/p&gt;

&lt;div class=&quot;language-scss highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;video&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;.local&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;rotateY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;180deg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;你也可以用 CSS3 Filter 给视频添加滤镜效果：&lt;/p&gt;

&lt;div class=&quot;language-scss highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;video&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;.local&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;rotateY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;180deg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;.grayscale&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;grayscale&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;.sepia&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;sepia&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;.blur&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;blur&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;5px&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;.invert&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;invert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;.8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;.inkwell&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;grayscale&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;brightness&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;.45&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;contrast&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;.05&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;完整例子&quot;&gt;完整例子&lt;/h2&gt;

&lt;script src=&quot;https://gist.github.com/hyjk2000/10e0bffa19181fc3f600.js&quot;&gt; &lt;/script&gt;

&lt;h2 id=&quot;下期预告&quot;&gt;下期预告&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;/2015/05/16/webrtc-peer-connection/&quot;&gt;WebRTC 之点对点连接&lt;/a&gt;将会介绍如何在浏览器之间建立点对点连接。&lt;/p&gt;</content><author><name>James Shih</name></author><category term="Web 开发" /><category term="WebRTC" /><category term="HTML5" /><category term="JavaScript" /><summary type="html">什么是 WebRTC</summary></entry><entry><title type="html">为 RESTful API 配置 CORS 实现跨域请求</title><link href="https://jshih.dev/2015/04/02/cors-for-restful-api/" rel="alternate" type="text/html" title="为 RESTful API 配置 CORS 实现跨域请求" /><published>2015-04-02T00:00:00+00:00</published><updated>2015-04-02T00:00:00+00:00</updated><id>https://jshih.dev/2015/04/02/cors-for-restful-api</id><content type="html" xml:base="https://jshih.dev/2015/04/02/cors-for-restful-api/">&lt;p&gt;利用 Ruby on Rails 可以很方便地实现 &lt;a href=&quot;https://en.wikipedia.org/wiki/Restful&quot;&gt;RESTful&lt;/a&gt; API，但如果我们需要通过 AJAX 跨域调用的话，怎么办？&lt;/p&gt;

&lt;p&gt;说到 AJAX 跨域，很多人最先想到的是 &lt;a href=&quot;https://en.wikipedia.org/wiki/JSONP&quot;&gt;JSONP&lt;/a&gt;。的确，JSONP 我们已经十分熟悉，也使用了多年，从本质上讲，JSONP 的原理是给页面注入一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;script&amp;gt;&lt;/code&gt;，把远程 JavaScript 放在页面上执行。这种做法会带来一个显而易见的问题：如果调用的来源被攻击或篡改，那什么东西都可以注入到页面里，造成 &lt;a href=&quot;https://en.wikipedia.org/wiki/Cross-Site_scripting&quot;&gt;XSS&lt;/a&gt; 漏洞。另外，JSONP 本质上已经不是 &lt;a href=&quot;https://en.wikipedia.org/wiki/XMLHttpRequest&quot;&gt;XMLHttpRequest&lt;/a&gt;，所以在错误处理上也没有什么选择。而且 JSONP 只支持 GET 请求，所以 RESTful API 就没办法了。&lt;/p&gt;

&lt;p&gt;这也就是为什么我们需要 &lt;a href=&quot;https://en.wikipedia.org/wiki/Cross-origin_resource_sharing&quot;&gt;CORS&lt;/a&gt;。CORS 是 Cross Origin Resource Sharing 的缩写，定义了浏览器和服务器间共享内容的新方式，通过它浏览器和服务器可以安全地进行跨域访问，它是 JSONP 的现代继任者。服务器上的 CORS 配置可以精细地指定允许跨域访问的条件：来源域、HTTP 方法、请求头、内容类型……等等。并且，CORS 让 XMLHttpRequest 也可以跨域，我们可以像往常一样编写 AJAX 调用代码。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;http://caniuse.com/#feat=cors&quot;&gt;所有现代浏览器都支持 CORS&lt;/a&gt;，所以你应该可以放心地使用它，只有在需要兼容老旧浏览器的场合，才用 JSONP 做 fallback。&lt;/p&gt;

&lt;p&gt;支持 CORS 的浏览器在尝试进行跨域 XMLHttpRequest 时，会先发出一个“事前检查”，就是一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OPTIONS&lt;/code&gt; 请求，其中会包括一些有用的请求头：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Access-Controll-Request-Headers: accept, content-type
Access-Controll-Request-Method: POST
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;接着服务器会做出响应：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-Requested-With, Content-Type, Accept
Access-Control-Max-Age: 1728000
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;最后浏览器会根据服务器的响应头，判断请求是否在服务器规定的范围内。比如来源是否在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Access-Control-Allow-Origin&lt;/code&gt; 里，HTTP 方法是不是在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Access-Control-Allow-Methods&lt;/code&gt; 里面，有没有不在 Allow-Headers 里面的请求头。如果以上条件都符合，那么浏览器就会放行这次请求，并且在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Access-Control-Max-Age&lt;/code&gt; 指定的时间内（单位是秒，以上设置的是 20 天）不需要再进行这种“事前检查”。&lt;/p&gt;

&lt;p&gt;在以上服务器响应头中，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Access-Control-Allow-Headers&lt;/code&gt; 不可以使用通配符。所以如果你要允许所有请求头，不妨把浏览器发来的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Access-Control-Request-Headers&lt;/code&gt; 直接返回。&lt;/p&gt;

&lt;p&gt;事实上，如果跨域请求是“简单请求”，也就是 HTTP 方法为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GET&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HEAD&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST&lt;/code&gt;，请求体的 MIME Type 是以下其中一种：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;application/x-www-form-urlencoded&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;multipart/form-data&lt;/code&gt; 或者 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;text/plain&lt;/code&gt;，并且没有自定义的请求头。这时浏览器只根据请求头中的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Origin&lt;/code&gt; 和服务器返回的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Access-Control-Allow-Origin&lt;/code&gt; 就可以判断了。但我们是 RESTful API，请求体是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;application/json&lt;/code&gt;，所以只能用上面那种“事前检查”的方式。&lt;/p&gt;

&lt;p&gt;另外，利用 CORS 还可以在跨域请求中发送 Cookie，这个特性是很有用的。只需要为 XMLHttpRequest 对象设置 withCredentials 属性：&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;xhr&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;XMLHttpRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;xhr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;withCredentials&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;但这种情况下，就不能指定 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Access-Control-Allow-Origin: *&lt;/code&gt;，而是必须指定一个来源，比如 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;http://mydomain.com&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;下面来说说在服务器端怎么配置，以 Rails 框架为例。注意：这只是一个很简单的例子，为了展示其原理，建议只在安全要求不高的项目上使用这种方法。Rails 有专门的 &lt;a href=&quot;https://github.com/cyu/rack-cors&quot;&gt;Rack CORS&lt;/a&gt; 中间件可以处理这个问题。&lt;/p&gt;

&lt;p&gt;在一个 Controller（或者 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;application_controller.rb&lt;/code&gt; ）中添加 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;before_filter&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;after_filter&lt;/code&gt;。前者用来回应浏览器的“事前检查”，如果浏览器发来了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OPTIONS&lt;/code&gt; 请求，则返回一些响应头，并结束处理；后者则用来给响应添加 CORS 的响应头：&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# some_controller.rb&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;before_filter&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:cors_preflight_check&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;after_filter&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:cors_set_headers&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;cors_preflight_check&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;method&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'OPTIONS'&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'Access-Controll-Allow-Origin'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'*'&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'Access-Controll-Allow-Methods'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'POST, GET, OPTIONS'&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'Access-Controll-Allow-Headers'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'X-Requested-With, Content-Type, Accept'&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'Access-Controll-Max-Age'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'1728000'&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:text&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:content&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'text/plain'&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;cors_set_headers&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'Access-Controll-Allow-Origin'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'*'&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'Access-Controll-Allow-Methods'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'POST, GET, OPTIONS'&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'Access-Controll-Max-Age'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'1728000'&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;最后，别忘了在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;routes.rb&lt;/code&gt; 中允许 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OPTIONS&lt;/code&gt; 请求：&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;match&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'controller'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;to: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'controller#action'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;via: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:options&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;＃&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;添加此行&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;resources&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:controller&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;</content><author><name>James Shih</name></author><category term="Web 开发" /><category term="Ruby on Rails" /><category term="RESTful" /><category term="CORS" /><summary type="html">利用 Ruby on Rails 可以很方便地实现 RESTful API，但如果我们需要通过 AJAX 跨域调用的话，怎么办？</summary></entry></feed>