直播伴侣歌词助手功能开发

平时我是不太喜欢写技术文档的,做的大部分工作也都是前端领域的搬砖,因为这次伴侣歌词助手的开发经历也实在是坎坷,最终也算把需求搞定了,就把过程简单记录下,供有兴趣了解的同学看一看。

一、需求

首先需求是希望参考抖音直播伴侣开发一个类似功能,当你打开QQ/网易云音乐的歌词功能后,可以在直播软件通过一个歌词助手的功能把桌面上的歌词同步到直播场景中。最初一看这不好办法嘛,用OBS的窗口素材在场景中添加歌词对应的窗口不就好了,当我第一次尝试的时候就发现太天真,有时候在窗口列表不一定出现歌词对应的窗口,有时候列表存在歌词窗口,但是窗口添加完毕就是黑屏,所以我开始意识到这个功能可能不是那么简单的事情。

二、chromium 踩坑

既然OBS的窗口素材展示不出来,我首先想到的就是直接用Web技术。先是发现chrome的窗口选择界面倒是能呈现缩略图,但是一样无法拿到mediaStream在网页中播放出来。然后我就去Electron中通过getSources读取,当然窗口列表中也是包含歌词窗口的,缩略图也能打印出来,但是当我尝试在getSources回调中把歌词对应的窗口资源返回给渲染进程处理(navigator.mediaDevices.getDIsplayMedia)的时候,代码直接在渲染进程抛出异常。然后在网上搜了下发现有价值的信息非常少,所以我就在一个关于Web音视频开发群里咨询了下,但是没人清楚怎么回事。倒是有个热心群友听了我的需求后,说可以用Python或者C++试试,而且需要HOOK(完全不懂是什么)。这两个我是都不会,如果说Python或者C++能拿到,那浏览器跟Chrome没理由拿不到做啊,就算通过其它语言拿到了怎么放到直播伴侣也是个问题,但是也不妨碍去尝试一下。

三、Python 入门到放弃

虽然Python跟C++我都是门都没入,但是很快在AI的帮助下帮我写了第一个脚本,对目标窗口截图,这个过程中了解到Windows开发过程中的一些概念和利用SPY++(电脑之前就装了vs 2022)查看已打开窗口。截屏功能虽然实现了,但是发现截屏的歌词界面背景是黑色而且歌词画面不会同步,还是群里的热心群友帮助下通过一个死循环实现了画面同步,黑色背景也通过一个OpenCV库把黑色像素换成绿色,最后在OBS里添加了这个python创建的窗口,通过OBS素材的滤镜[色键]进行了抠图(去掉绿色背景),虽然歌词文字边缘锯齿明显,但是起码好像是有希望了,然后我接着想优化下这个脚本,比如死循环逻辑,怎么能让锯齿少一点。还有最重要的是打包成exe发现几十MB,如果做到exe里面被调用程序包就太大了,而且这么多妥协后放到直播客户端里用起来体验也很一般,很显然性价比太低,所以这个时候我想到直接用C++ Win32 API重新实现。

四、C++ 入门到放弃

通过之前的摸索虽然效果一般,但是目标清晰了-我要创建一个能对歌词窗口录屏的exe程序,这个程序跟歌词窗口同步界面跟尺寸,很快在AI及搜索引擎的帮助下我实现了跟Python一样的版本,但遗憾的是尝试了各种Windows API,要么拿不到音乐窗口截图是黑色,要么截图是黑色背景,而且还没实现把黑色换成绿色的逻辑,这中间基本陷入了一种难以自拔的困境,我无数次通过跟AI工具沟通希望能截一个透明的图然后绘制在新窗口的绿色背景上,结果还是没做到,最终只是得到一个黑色背景的歌词同步画面。虽然目的没达到,但是Windows以及C++的一些简单知识了解了不少,窗口创建 颜色 文字绘制 消息 窗口句柄 窗口设置 窗口style exstyle DC bitblt alphabend printwind bitmap hbitmap gdi+ settimer … 当然离入门都不够。搞了很久就发现只有PrintWindow能打印,咨询了一个懂C++的朋友有没有更好的截图方法或者如何把图片的黑底去掉,只告诉了我按照现在的方式存成png图片看看,然后我发现一样是黑色背景,基本就想放弃了,不如就按照黑底窗口往直播软件里添加,已经浪费太多时间跟精力了,毕竟只是个Windows开发的门外汉。

五、Windows.Graphics.Capture

中间在探索 Windows 截屏方式的时候,就发现了一个谈到 Win32 PrintWindow等方式窗口截图黑屏的问题 issue,在下面回复中有大佬丢了个Window Capture的 Example Code,作者建议使用新版的Capture API(支持Windows 10+),我把代码Clone到本地运行之后,发现这个程序在罗列桌面窗口列表的时候压根儿没有歌词窗口,所以我就翻了翻窗口列表的创建逻辑,发现程序在用EnumWindows函数枚举所有桌面窗口逻辑中,有一个函数检测是否要被添加到界面窗口列表中,在这个函数中我找到了一点代码:

auto exStyle = GetWindowLongW(window.WindowHandle, GWL_EXSTYLE);
if (exStyle & WS_EX_TOOLWINDOW) // No tooltips
{
  return false;
}

是这个逻辑把歌词窗口过滤掉了,于是我删除了判断,窗口列表中有了桌面歌词,但是选择了窗口后发现直接抛出了异常,原来过滤是有原因的,搜了一圈也没发现EX_TOOLWINDOW为什么不能被录制或者截屏。实在没有什么头绪就去处理其它业务问题了。

六、柳暗花明

后面感觉自己去用C++实现可能实在是能力有限,所以计划直接去找别人写好的截图代码/库。找了几个都不太行,不是截不到就是黑底,实现原理也都是那几个函数Bitblt、PrintWindow,也有一些关于D3D截图的例子,代码太麻烦也被劝退了。

最终我想看看有没有别人写好的桌面程序或者OBS插件,在搜索的时候看到有人在视频里讲怎么用一个桌面音乐歌词,视频中的效果跟抖音一样,所以想着干脆找个软件丢在客户端里得了,然后就找到2个程序(一个是lyrics.run,另外一个不记得了,界面基本一样),打开后功能很简单,界面上一个锁定,一个置顶,还有个OBS支持,前两个功能音乐软件本来就有,OBS支持选中以后,我去OBS里面看确实能添加歌词窗口然后设置 Windows 10采集就能看到非常完美的窗口采集效果了,这正是我想要的。

看来还是可以实现完美窗口录制的,而且走的是 WGC 接口,但是搞不懂他这个桌面歌词软件中的的 ”OBS 支持“ 背后到底做了什么操作。我想既然置顶跟锁屏都能实现(很好猜到应该是修改了窗口的属性),大概率就是通过API修改了窗口的扩展style,结合之前那个Example过滤了Ex_ToolWindow,我想着试试是不是这个也能修改别的程序的窗口style,如果没有这个style是不是就能被WCP API识别并录制,然后就在之前写的黑屏版本加上下面的代码尝试解除桌面歌词的扩展style:

// 通过Spy++工具查找发现QQ音音跟网易云音乐的歌词窗口都是叫 桌面歌词
// 下面代码假设窗口一定在,没有判断
HWND hWndLRC = FindWindow(NULL, L"桌面歌词");

// 获取窗口的扩展样式设置
LONG exStyles = GetWindowLong(hWndLRC, GWL_EXSTYLE); 

// 清除窗口的WS_EX_TOOLWINDOW属性
exStyles &= ~(WS_EX_TOOLWINDOW);

// 重新设置窗口exStyles 
SetWindowLong(hWndLRC, GWL_EXSTYLE, exStyles);

关掉了别人写的歌词助手,重新打开音乐软件显示歌词,再打开OBS添加窗口素材,窗口设置列表能看到桌面歌词窗口了,设置Windows 10录制模式,场景果然显示了完美的歌词界面录制,我已经知道下面怎么办了。只需要用C++写一个解除歌词窗口的扩展属性的函数然后封装成NAPI在Electron中调用即可,于是赶紧在之前项目的原生模块加了一个函数,代码如下:

napi_value activeLyricsWindow(napi_env env, napi_callback_info info) {
  LPCSTR titleA = "\u684c\u9762\u6b4c\u8bcd";
  LPCSTR titleB = "\u684c\u9762\u6b4c\u8bcd - \u9177\u72d7\u97f3\u4e50";
  int musicApp = 0;
  HWND hWndLrc = FindWindow(NULL, titleA);
  if (hWndLrc != NULL && IsWindowVisible(hWndLrc)) {
    musicApp = 1; // QQ && 163
  } else {
    hWndLrc = FindWindow(NULL, titleB);
    if (hWndLrc != NULL && IsWindowVisible(hWndLrc)) {
      musicApp = 2; // Kugou
    }
  }
  BOOL isAvailable = hWndLrc != NULL && IsWindowVisible(hWndLrc);
  if (isAvailable) {
    LONG exStyle = GetWindowLong(hWndLrc, GWL_EXSTYLE);
    if (exStyle & WS_EX_TOOLWINDOW) {
      exStyle &= ~(WS_EX_TOOLWINDOW);
      SetWindowLong(hWndLrc, GWL_EXSTYLE, exStyle);
    }
  }
  return toValue(env, isAvailable ? musicApp : 0);
}

编译成.node之后复制到项目中,我赶紧启动程序在console手动调用了对应的函数,果然在添加素材的时候能看到桌面歌词窗口了,设置好采集方式(Windows 10),窗口就完美展示了歌词,至此苦苦挣扎几天的歌词助手终于画上了句号。

附:

Read more

那些年玩过的归零山寨币

这两天币安因为调整合约策略导致的不少meme币腰斩再腰斩,作为一个老韭菜看到这样的场景仍然是触目惊心。虽然我早已经不碰这些玩意儿,但是不禁让我想起来这些年经手的归零山寨币还真不少。2017年的夏天我的朋友带我走进了数字货币的世界,不知不觉在币圈熬了8年,最终的结局是没有暴富,也没有了期待,下面就盘点下从入圈到现在归零的山寨币。 归零定义:基本被所有交易所下架/项目方解散或者是无人关注/价格下跌95%以上。以下为2017年到2025年持仓时间跟数量相对比较多的归零或者接近归零的山寨币。 SNT (https://status.im) 这个是我接触的最早的一个以太坊ICO项目,做的去中心化IM。虽然没有参与众筹,但是在二级市场接盘在高点,当时恰逢2017年牛市,作为币圈小白完全不清楚熊市的威力,后面亏的抬不头。项目现在还在做,但是基本没人关注了,币价也可以忽略不计了。 EOS 老韭菜都懂,当时参与了ICO,最多的时候有9000多个,2017年牛市没来就跑光了。从最高差不多20美金跌到现在的0.8美金,最近热度又起来了,搞不懂。 DEW 李笑来那一伙搞的什么去中心化交易所

By Tee

Electron窗口打开速度1s到0.2s

最近项目进行到尾声,就开始对客户端做一些性能方面的优化。首先发现的问题就是其中一类子窗口的打开速度实在不尽人意,但是这些窗口内容并不复杂,就考虑系统性优化一遍,找到影响速度的地方。 首先要说明的是我们客户端一共有三种类型的窗口: * 软件主界面 * 子窗口A(Modal,Parent是主界面,进程常驻,关闭只隐藏,所以打开是秒开) * 子窗口类型B (独立窗口,需要就创建,关闭就销毁) 所以子窗口A只会存在1个,而子窗口B是可以同时创建多个的。现在问题就出在窗口类型B。当然如果只是图快,完全可以采用窗口池的概念,预先创建好窗口,需要的时候显示窗口并渲染UI组件即可,但是我们客户端本身就是内存消耗大户,实在没法这么来了,所以还是本着能优化就优化的原则,把问题消灭掉才行,因为这种慢还会影响到软件启动到UI呈现的时间,下面会说。 最初软件的工程师设计的时候采用的是单文件实现的全部功能,所以每个窗口都是载入的同一个html(也是同样的js脚本),所以各种功能都打包在一个js文件中,这个文件没压缩的时候是18MB+,所以这次优化的思路主要是围绕js脚本加载这一个方向去改进。 加载时间分析

By Tee