起因

今天突然发现从上周开始(2024/10/23)通过网易云音乐 PC 客户端下载的歌(.ncm)连他自己都播放不了,拿去解密也说文件是损坏的。而且我的客户端版本很久没更新过了(因为新版刚出的时候是没有下载功能的,就退回去了一直没更新),于是猜测可能是下载的接口变了或者是其他原因,就去下载新版客户端,然后再去下载歌曲,结果发现没问题了。

旧版客户端下载的文件

新版客户端下载的文件

那就用新版客户端好了,然后突发奇想看能不能顺便解决一下 OBS 获取歌曲其他信息的问题(现在一搜就能出来的 tuna 只能通过获取窗口标题来获取歌名这样,而且就算用 Spotify,也最多获取到歌曲时长、播放到哪,并且刷新很迷惑,有时候可能连续10s都能正确刷新,但又会卡住几秒)

方案 1:实时获取播放进度

首先声明这个方法只适用于旧版客户端(2.x),新版客户端的日志已加密,然后原文也已经寄了,只能通过缓存窥见一斑

省略一些过程,总之原文通过浏览日志文件发现有一个文件是存播放历史的(..\AppData\Local\NetEase\CloudMusic\webdata\file\history

所以我们可能就能够通过获取这个历史记录的最新记录来获得当前播放的歌曲。

当我们打开文件之后发现文件内容是这样的(已格式化):

[
{
"track": {
"id": 1895395697,
"name": "饿魔少女",
"alias": [
"emo girl"
],
"artists": [
{
"id": 34477557,
"name": "ChiliChill",
"tns": [],
"alias": []
}
],
"album": {
"id": 138401837,
"name": "每到夜里我就很饿",
"picId": "109951166891913184",
"picUrl": "https://p3.music.126.net/Sdp6DpCQF5q-dDIiNOuBjQ==/109951166891913184.jpg",
"alias": [],
"transNames": []
},
"duration": 185334,
"mvId": 14479857,
"recoverMusic": {
"size": 38858501,
"bitrate": 999000,
"dfsId": "5f344376286b5fc50932f31c5db7574e"
},
"realSuffix": "flac",
"commentThreadId": "R_SO_4_1895395697",
"copyrightId": 1400821,
"alg": "",
"mvid": 14479857,
"cd": "01",
"position": 5,
"ringtone": "",
"rtUrl": null,
"status": 0,
"pstatus": 0,
"fee": 8,
"version": 13,
"songType": 0,
"mst": 9,
"popularity": 85,
"ftype": 0,
"rtUrls": [],
"noCopyrightRcmd": null,
"originCoverType": 0,
"mark": 17179877376,
"hMusic": {
"bitrate": 320000,
"dfsId": 0,
"size": 7415685,
"volumeDelta": -61396
},
"mMusic": {
"bitrate": 192000,
"dfsId": 0,
"size": 4449428,
"volumeDelta": -58875
},
"lMusic": {
"bitrate": 128000,
"dfsId": 0,
"size": 2966300,
"volumeDelta": -57372
},
"yunSong": null,
"privilege": {
"id": 1895395697,
"version": "8-1-0-999-999-999-320-7-1-1-0",
"fee": 8,
"payed": 1,
"status": 0,
"maxPlayBr": 999,
"maxDownBr": 999,
"maxSongBr": 999,
"maxFreeBr": 320,
"sharePriv": 7,
"commentPriv": 1,
"subPriv": 1,
"cloudSong": 0,
"toast": false,
"flag": 524292,
"now": 1730354870000
},
"commentCount": 495,
"lrcAbstractEnd": -1,
"lrcAbstractStart": 0,
"indexId": "NL1895395697",
"haslyric": false,
"starred": false,
"starredNum": 0,
"playedNum": 0,
"dayPlays": 0,
"hearTime": 0,
"mp3Url": "http://m2.music.126.net/hmZoNQaqzZALvVp0rE7faA==/0.mp3",
"originSongSimpleData": null,
"songJumpInfo": null,
"score": 100,
"audition": null,
"copyFrom": "",
"disc": "01",
"no": 5,
"bMusic": {
"volumeDelta": -61372,
"playTime": 185334,
"bitrate": 2161000,
"dfsId": 0,
"sr": 44100,
"name": "",
"id": 6954240686,
"size": 50067454,
"extension": "wav"
},
"sqMusic": {
"volumeDelta": -61377,
"playTime": 185334,
"bitrate": 1673892,
"dfsId": 0,
"sr": 44100,
"name": "",
"id": 6954266909,
"size": 38778697,
"extension": "flac"
},
"hrMusic": {
"volumeDelta": -61372,
"playTime": 185334,
"bitrate": 2161000,
"dfsId": 0,
"sr": 44100,
"name": "",
"id": 6954240686,
"size": 50067454,
"extension": "wav"
},
"crbt": null,
"rtype": 0,
"rurl": null,
"recommendReason": "相似歌曲"
},
"id": "offline-1895395697_-2_",
"tid": 1895395697,
"program": null,
"fid": "-2",
"data": "",
"href": "/m/offline/complete/?fromSource=1",
"text": "我下载的音乐",
"nickName": "",
"userId": "",
"fromButton": false,
"ext": "complete",
"aiRcmd": false,
"startlogtime": 1730354879626,
"loaderr": false,
"playedTime": 1.72,
"lastTime": 1.72,
"logDuration": 1.72,
"qid": "offline-1895395697_-2_",
"time": 1730354879635,
"playType": 0,
"playBrt": 999,
"playFile": "D:\\Music\\VipSongsDownload\\ChiliChill\\每到夜里我就很饿\\ChiliChill - 饿魔少女.ncm",
"lrctype": "online",
"lrcid": 1895395697
},
{
"track": {
"album": {
"id": 252091416,
"name": "短期陪伴",
"picId": "109951170088106953",
"picUrl": "https://p4.music.126.net/nrDBWc-tMOcTemEuo7WZ6A==/109951170088106953.jpg",
"alias": [],
"transNames": []
},
"alias": [],
"artists": [
{
"id": 12277194,
"name": "LBI利比",
"tns": [],
"alias": []
}
],
"commentThreadId": "R_SO_4_2640843881",
"copyrightId": 0,
"duration": 219968,
"id": 2640843881,
"alg": "",
"mvid": 0,
"name": "短期陪伴",
"cd": "01",
"position": 1,
"ringtone": "",
"rtUrl": null,
"status": 0,
"pstatus": 0,
"fee": 8,
"version": 6,
"songType": 0,
"mst": 9,
"popularity": 95,
"ftype": 0,
"rtUrls": [],
"noCopyrightRcmd": null,
"originCoverType": 0,
"mark": 17716748288,
"yunSong": null,
"hMusic": {
"bitrate": 320000,
"dfsId": 0,
"size": 8801325,
"volumeDelta": -34398
},
"mMusic": {
"bitrate": 192000,
"dfsId": 0,
"size": 5280813,
"volumeDelta": -31791
},
"lMusic": {
"bitrate": 128000,
"dfsId": 0,
"size": 3520557,
"volumeDelta": -30118
},
"privilege": {
"id": 2640843881,
"version": "8-1-0-1999-1999-1999-320-7-1-1-0",
"fee": 8,
"payed": 1,
"status": 0,
"maxPlayBr": 1999,
"maxDownBr": 1999,
"maxSongBr": 1999,
"maxFreeBr": 320,
"sharePriv": 7,
"commentPriv": 1,
"subPriv": 1,
"cloudSong": 0,
"toast": false,
"flag": 528388,
"now": 1730354870000
},
"commentCount": 44,
"volumeDelta": -7.8456,
"downloadQuality": 0
},
"id": "2640843881_1_421801417",
"tid": 2640843881,
"program": null,
"fid": "1",
"data": "421801417",
"href": "/m/playlist/?id=421801417&rid=A_PL_0_421801417&fromSource=1",
"text": "我喜欢的音乐",
"fromButton": false,
"aiRcmd": false,
"startlogtime": 1730354879513,
"loaderr": true,
"playedTime": 0,
"lastTime": 0,
"logDuration": 0,
"qid": "2640843881_1_421801417",
"time": 1730354879535,
"playType": 0,
"playBrt": 1999,
"playFile": "D:\\Music\\LBI利比\\短期陪伴\\LBI利比 - 短期陪伴.ncm",
"playedTimeFromNative": 0,
"lrctype": "online",
"lrcid": 2640843881
},
// 其他省略
]

这明显是json格式或者说类json格式。

然后我们发现这个列表的第一个字典元素就包含了我们当前播放的歌曲。那么这可能就是最终的解决方案了。

此外通过监控日志,还可以得到JSON中展示的歌曲的相关数据,包括歌曲所属专辑、歌曲名称、演唱者、时长、歌曲链接等信息。

此外还有:除了歌曲相关的信息,这段JSON还包括了一些评论、播放记录、特权和权限等信息。例如,每首歌曲都有一个评论线程ID、播放次数和评论数等;每个歌曲都有一个特权对象,其中包含了歌曲的ID、版本、价格等信息;同时,还有一些播放记录、时间戳、播放方式、音质等信息。

其中想要获得监控播放时间需要注意一下几个属性:

startlogtime、playedTime、lastTime、logDuration、time

这几个属性都与播放时间有关;

为了获取播放时长,需要对每个歌曲的 JSON 数据进行解析。以第一首歌曲"What You Know Bout Love"为例,其对应的 JSON 如下所示:

{ “track”: { …, “duration”: 160000, …, “playedTime”: 30.28, “lastTime”: 30.28, “logDuration”: 30.28, … }, … }

其中,“duration” 字段代表歌曲的总时长,单位为毫秒;“playedTime” 和 “lastTime” 字段代表已播放的时间和剩余的时间,单位为秒;“logDuration” 字段代表播放记录中记录的播放时长,单位为秒。

因此,我们可以通过访问这些字段来获取歌曲的播放时长。

实时读取文件

一个有效的方法是每隔一段时间查看文件的修改日期,如果修改日期改变的话才读取文件,然后再更新储存了当前播放歌曲的文件。

实际上有一个库叫做watchdog可以监听系统事件,其中也包括文件的修改,这样就不用我们重复造轮子了

import sys
import time
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler


class LoggingEventHandler(FileSystemEventHandler):
def on_modified(self, event):
super(LoggingEventHandler, self).on_modified(event)
what = 'directory' if event.is_directory else 'file'
print(f'Modified {what}: {event.src_path}')


def main():
path = sys.argv[1] if len(sys.argv) > 1 else '.'
event_handler = LoggingEventHandler()
observer = Observer()
observer.schedule(event_handler, path, recursive=True)
observer.start()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()


if __name__ == "__main__":
main()

通过上述的代码,我们就可以监听当前目录下或者给定参数目录下的文件修改事件了。

获取history文件所在目录

我们可以通过以下表达式获取history文件所在的目录。

path = os.path.join(os.path.expanduser('~'), r'AppData\Local\Netease\CloudMusic\webdata\file')

解析history文件

我们发现这个history文件一般都有200KB左右,当历史记录更多的时候可能更大。如果每次这个文件更改都解析整个json未免有点低效了,所以我们需要找个办法只解析这个json列表里面的第一个字典。

history文件中第一个字典长这样(history中整个文件都只有一行):

{'track': {'album': {'id': 36634131,
'name': 'けいおん!はいれぞ!「Come with Me!!」セット',
'picId': '109951163048673023',
'picUrl': 'https://p4.music.126.net/e1n_xjLFAm_GY8ZETmka4g==/109951163048673023.jpg',
'alias': [],
'transNames': []},
'alias': [],
'artists': [{'id': 161782, 'name': '放課後ティータイム', 'tns': [], 'alias': []}],
'commentThreadId': 'R_SO_4_514765041',
'copyrightId': 663018,
'duration': 253280,
'id': 514765041,
'mvid': 10850421,
'name': 'NO, Thank You!',
'cd': '1',
'position': 21,
'ringtone': None,
'rtUrl': None,
'status': 0,
'pstatus': 0,
'fee': 0,
'version': 8,
'songType': 0,
'mst': 9,
'popularity': 45,
'ftype': 0,
'rtUrls': [],
'yunSong': {'uid': 40396092,
'nickname': '',
'songName': 'NO, Thank You!',
'album': 'けいおん!はいれぞ!「Come with Me!!」セット',
'artist': '放課後ティータイム',
'coverId': '109951165018150110',
'fileExt': '.mp3',
'bitrate': 326},
'hMusic': {'bitrate': 320000,
'dfsId': 0,
'size': 10133464,
'volumeDelta': -39100},
'mMusic': {'bitrate': 192000,
'dfsId': 0,
'size': 6080096,
'volumeDelta': -36700},
'lMusic': {'bitrate': 128000,
'dfsId': 0,
'size': 4053412,
'volumeDelta': -35500},
'privilege': {'id': 514765041,
'version': '0-0-0-326-326-999-999-7-1-1-1',
'fee': 0,
'payed': 0,
'status': 0,
'maxPlayBr': 326,
'maxDownBr': 326,
'maxSongBr': 999,
'maxFreeBr': 999,
'sharePriv': 7,
'commentPriv': 1,
'subPriv': 1,
'cloudSong': 1,
'toast': False,
'flag': 136,
'now': 1595490687000},
'commentCount': 151},
'id': '514765041_1_33803710_1595480877683',
'tid': 514765041,
'program': None,
'fid': '1',
'data': '33803710',
'href': '/m/playlist/?id=33803710&rid=A_PL_0_33803710&fromSource=1',
'text': '我喜欢的音乐',
'nickName': '辣条ii',
'userId': 40396092,
'fromButton': False,
'specialType': 5,
'startlogtime': 1595555807539,
'loaderr': False,
'playedTime': 0,
'lastTime': 0,
'logDuration': 0,
'isCloudMusic': False,
'lastPlayInfo': {'bitrate': 320,
'retJson': {'id': 514765041,
'url': 'http://m8.music.126.net/20200502162942/dcb351459e414e759c04a4f0e3366c26/ymusic/6e3f/53bf/0cdc/c86b892db7c67f19604d2a77970f181e.mp3',
'br': 320000,
'size': 10133464,
'md5': 'c86b892db7c67f19604d2a77970f181e',
'code': 200,
'expi': 1200,
'type': 'mp3',
'gain': 0,
'fee': 0,
'uf': None,
'payed': 0,
'flag': 0,
'canExtend': False,
'freeTrialInfo': None,
'level': 'exhigh',
'encodeType': 'mp3'}},
'playType': 4,
'playBrt': 320,
'playedTimeFromNative': 0,
'aiRcmd': False,
'qid': '514765041_1_33803710',
'time': 1595555807594}

我们可以发现里面每个字典的长度都是2000上下,如果保险起见那么为了能够解析到一个完整的字典,至少也要读取3000个字符的样子。

插一句:当年的数据里面竟然还带了歌曲的真实 URL

尝试使用正则

经过进一步考虑我们发现其实只需要读取前400个字符左右,然后用正则表达式 r’”name”:”(.*?)”‘来匹配就可以分别得到专辑名,艺术家和歌曲名了。

为了保险起见,我们读取前800个字符。

所以从history解析正在播放的歌曲的代码就是这样了:

pattern = re.compile(r'"name":"(.*?)"')

def get_playing(path):
with open(path, encoding='utf-8') as f:
new_content = f.read(800)
return pattern.findall(new_content)

但是经过一番测试之后发现有些歌会没有专辑名,导致上面的正则表达式只能匹配到一个。

还是决定用json解析

我们测试完了正则表达式之后发现并不是很可靠,于是还是退回到用json解析的方法来。

但是我们又遇到了一个新的问题。

history文件里的json结构大概是这样的:

[{"a":"...", "b":"...", ...}, {"c":"......", "d":"....", ....}, ...]

这个列表里面的每个字典的长度是不定长的,我们需要想办法只把第一个字典的字符串送入json解析器里面,但是这有点复杂了。

经过搜索,我们发现了我们可以使用如下的代码来从字符串中解析第一个出现的完整json,忽略额外的字符串。

这里的raw_decode方法返回元组里第一个元素是解码器找到的第一个完整json,第二个元素是解析了的字符串长度。这个在流式传输的时候还挺有用的,这里我们就把它用作history文件的解析。

def get_playing(path):
track_info = dict()
with open(path, encoding='utf-8') as f:
read_string = f.read(3200)
for _ in range(4):
try:
read_string += f.read(500)
decoded_json = decoder.raw_decode(read_string[1:])
track_info.update(decoded_json[0])
break
except json.JSONDecodeError:
pass

if not track_info:
return None

track_name = track_info['track']['name']
artist_list = [i['name'] for i in track_info['track']['artists']]

return track_name, artist_list

这里我们首先读取3200个字符,然后除去第一个字符送入json解析器,如果产生错误的话就再加500个字符,然后再次解析,尝试四次直到解析出来。

然后我们就可以从解析出来的字典里面获取歌曲名和艺术家列表了。

所以最后我们的程序就是这样了:

import os
import time
import json
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

decoder = json.JSONDecoder()


def get_history_file():
path = os.path.join(os.path.expanduser('~'), r'AppData\Local\Netease\CloudMusic\webdata\file')
if os.path.exists(path):
return path
else:
print('cloudmusic data folder not found')
exit(1)


def get_playing(path):
track_info = dict()
with open(path, encoding='utf-8') as f:
read_string = f.read(3200)
for _ in range(4):
try:
read_string += f.read(500)
decoded_json = decoder.raw_decode(read_string[1:])
track_info.update(decoded_json[0])
break
except json.JSONDecodeError:
pass

if not track_info:
return None

track_name = track_info['track']['name']
artist_list = [i['name'] for i in track_info['track']['artists']]

return track_name, artist_list


class LoggingEventHandler(FileSystemEventHandler):
def __init__(self):
self.file_size = 0

def on_modified(self, event):
super(LoggingEventHandler, self).on_modified(event)
path = event.src_path

if path.endswith(r'webdata\file\history'):
current_size = os.path.getsize(path)
if current_size != self.file_size:
self.file_size = current_size
for _ in range(5):
try:
song, artists = get_playing(path)
with open('playing.txt', 'w', encoding='utf-8') as f:
playing = f'{song} - {" / ".join(artists)}'
print(playing)
f.write(playing)
break
except PermissionError:
time.sleep(1)


def main():
path = get_history_file()
event_handler = LoggingEventHandler()
observer = Observer()
observer.schedule(event_handler, path, recursive=True)
observer.start()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()


if __name__ == "__main__":
main()

方案 2:实时获取歌词

原文:重磅!python获取同步输出的桌面网易云音乐歌词(内存偏移获取)_网易云音乐 dll进程注入-CSDN博客

再次首先声明,里面给出的代码只适用于某个版本,因为这个方法是直接抓内存,版本一变内存地址肯定也会变,不会抓的话等于没用(

首先,任何数据都在内存里,最直观的就是游戏数据,血量,金钱之类的,小时候应该很多人都用过金山游侠修改数据,就是那套原理,那么歌词作为文本,也是数据,为啥我不找找呢,于是搞了个CE打法,先显示英文的歌词,一直查找第一位字母的ASCII码,果然找到了,歌词不是什么敏感数据,一般也不会加密之类的,所以很典型很顺畅的找到了。

然后,网上教程说用OD去找偏移量,其实CE也可以搞定,一顿顺腾摸瓜,最最最重要的偏移量他来了,上图:

可以很清楚看到实时显示歌词的地址是怎么来的,从cloudmusic.dll的基址开始,经过三次偏移得道,当然最后一次是0,可以不用算。
原理知道了,愣着干嘛,一顿操作如虎,搞定了,这里,网易云音乐歌词的规则也被我看出来了,每个字,不管是英文还是中文,都占用两个bytes,中文用的是unicode编码,两个字符高低位反过来,如原来是\x34\x12就变成u1234,就行了,这里网上居然没找到现成的转换方式,网上找点有点的东西是真的费劲。。。于是自己手动写了坨屎山,转换了。英文就是\x00接ascii码,如果遇到连续两个\x00\x00视为词句歌词结束,现在规则全看透了,搞定。

这样就做好了,感觉干了件大事,网上没有相关资料代码,全靠自己摸索哦

#2022-10-15 by jd3096 vx:jd3096
import pymem
import time
import socket
import win32process
from win32con import PROCESS_ALL_ACCESS
import win32api
import ctypes
from win32gui import FindWindow

def GetProcssID(address,bufflength):
pid = ctypes.c_ulong()
kernel32 = ctypes.windll.LoadLibrary("kernel32.dll")
hwnd = FindWindow("DesktopLyrics", u"桌面歌词")#获取窗口句柄
hpid, pid = win32process.GetWindowThreadProcessId(hwnd)#获取窗口ID
hProcess = win32api.OpenProcess(PROCESS_ALL_ACCESS, False, pid)#获取进程句柄
ReadProcessMemory = kernel32.ReadProcessMemory
addr = ctypes.c_ulong()
ReadProcessMemory(int(hProcess), address, ctypes.byref(addr), bufflength, None)#读内存
win32api.CloseHandle(hProcess)#关闭句柄
return addr.value

def Get_moduladdr(dll): #找到dll的内存基址
modules = list(Game.list_modules())
for module in modules:
if module.name == dll:
Moduladdr = module.lpBaseOfDll
return Moduladdr

def get_add(): #从基址加偏移量反复三次得到实际内存地址
Char_Modlue = Get_moduladdr("cloudmusic.dll")
addr = GetProcssID((Char_Modlue + 0xAF7C44),4)
ret = addr + 0xc8
ret2 = GetProcssID(ret, 4)
ret3 = ret2 + 0x14
ret4 = GetProcssID(ret3, 4)
print (hex(ret4))
return ret4


Game = pymem.Pymem("cloudmusic.exe")
lyrics_addr=get_add()#实际内存地址
lyrics_len=200
last_lyrics=b''


def B2Q(uchar):
"""单个字符 半角转全角"""
inside_code = ord(uchar)
if inside_code < 0x0020 or inside_code > 0x7e: # 不是半角字符就返回原来的字符
return uchar
if inside_code == 0x0020: # 除了空格其他的全角半角的公式为: 半角 = 全角 - 0xfee0
inside_code = 0x3000
else:
inside_code += 0xfee0
return chr(inside_code)

def stringB2Q(ustring):
"""把字符串强行全角"""
return "".join([B2Q(uchar) for uchar in ustring])

def b2u(b): #bytes转unicode方法 我感觉应该有现成的函数,就是找不到好气,只能写了坨屎山凑合用
length=len(b)
sr=''
for i in range(0,length,2):
b0=hex(b[i])
b1=hex(b[i+1])
if b0=='0x0': #第一位如果是0说明不是中文,中文占2bytes
s1=chr(b[i+1])
sr+=s1
else:
if len(b0)==4:
s0=str(b0[2:4])
else:
s0='0'+str(b0[3])
if len(b1)==4:
s1=str(b1[2:4])
else:
s1='0'+str(b1[2])
s=s0+s1
result=b'\x5c\x75'
for ss in s:
bb=ss.encode()
result+=bb
sr+=result.decode("unicode_escape")
return sr


def get_lyrics():
global last_lyrics
raw_bytes=Game.read_bytes(lyrics_addr,lyrics_len)
use_bytes=raw_bytes.split(b'\x00\x00')[0]
if len(use_bytes)%2==1:
use_bytes+=b'\x00'
lyrics_bytes=b''
for i in range(0,lyrics_len,2): #构建bytes 这里高低位需要调换一下顺序
b1=use_bytes[i:i+1]
b2=use_bytes[i+1:i+2]
lyrics_bytes+=b2+b1
if last_lyrics!=lyrics_bytes: #检查看词是否变化
last_lyrics=lyrics_bytes
return b2u(lyrics_bytes)
else:
return None

def sendto(lyric):
quan=stringB2Q(lyric)
data=quan.encode('gbk')
return data

# tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# tcp_server.bind(("", 30960))
# tcp_server.listen(5)
# tcp_client, tcp_client_address= tcp_server.accept()

while True:
lyrics=get_lyrics()
if lyrics!=None:
print(lyrics)
data_send=sendto(lyrics)
#tcp_client.send(data_send)
time.sleep(0.1)

这次真的是爽爆了,完全实时同步,随便切歌,拉进度,歌词永远同步。
不过没有彻底完善,比如遇到日文韩文等显示不了,英文强行被我转GBK,很占地方,这个看心情再说吧哈哈哈,懂原理了什么时候解决都不急。

方案 3:大道至简

原视频:【主播必看】OBS捕捉QQ音乐滚动歌词超简单教程!

省流:捕捉播放器窗口,扣掉背景颜色,裁剪一下就行了(