十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

B站直播源接入实战:PHP代理解决防盗链与时效性

B站直播源接入实战:PHP代理解决防盗链与时效性 做B站直播源接入这块我前前后后折腾了不少时间。从最初在浏览器里拼命按F12抓接口到后来老老实实写PHP代理服务器弯路没少走坑也没少踩。这篇文章不打算讲什么高深理论就把我实际操作的完整流程、翻车记录和最终可落地的方案一次说清楚。如果你也想把B站某个直播间的流地址接出来放到自己的播放器或者网页里看这篇应该能帮你省下不少时间。先说结论B站直播源的核心是它的playUrl接口但这个接口返回的流地址有防盗链、时效性和跨域三重限制直接用根本不行。所以需要一个中转层来补齐请求头、刷新过期地址并把数据安全地转发给下游。这个中转层用PHP实现部署简单、不依赖额外服务拿到一个虚拟主机就能跑。下面我会把每一步的细节和为什么这么做讲透。1. 内容整体设计与思路拆解1.1 直播源的核心链路从直播间到播放器的数据流B站的直播源不像普通的网页视频那样一条地址能看半天它整个链路是分层级的。以我实际操作为例你想要拿到一个直播间正在推流的播放地址浏览器里的流程大概是这样第一步浏览器请求直播间页面拿房间号。第二步页面里的JavaScript脚本拿到房间号之后去请求一个接口这个接口会返回一个母列表playlist母列表里包含了不同协议HLS、HTTP-FLV、不同清晰度、不同编码方式的子流信息。第三步播放器根据这些子流信息拼出一个真正的流地址开始拉流播放。这里有一个关键点接口返回的流地址不是固定的。它包含了一串参数这些参数里有时效戳和签名信息过期之后地址就失效了需要重新请求接口刷新。也就是说哪怕你把地址放进播放器里能用过个十几分钟它一样罢工。所以你在做直播源抓取时本质上要做的工作是把“直播间房间号”映射到一个“当前有效的流地址”。这个映射不是一次性的而是要不断刷新、不断维护。整个链路的依赖关系是这样的房间号是稳定不变的用户输入、页面绑定都用它。playUrl接口返回的数据是动态的里面有当前可用的流地址。流地址本身也是动态的受时效控制过期就得重新请求。直接暴露流地址给播放器会有防盗链问题因为B站CDN会校验请求头里的Referer和User-Agent。想清楚这个链路之后设计思路就清晰了入口是房间号出口是稳定的可播放地址中间所有动态、校验、时效性的问题全部收敛到代理层处理。1.2 为什么需要PHP代理服务器而不是直接用真实地址这个问题我一开始也没想明白。当时从接口里拿到了一个看起来非常完整的FLV地址直接复制到VLC里结果一打开就报错。后来我换了HLS格式的地址VLC倒是能播了但换了台电脑、清了缓存之后又不行了。折腾半天才明白直接使用真实地址会踩三堵墙第一堵墙是防盗链。B站直播CDN会检查请求头里的Referer必须来自live.bilibili.comUser-Agent必须像一个浏览器。普通播放器发出的请求不会带这些头CDN直接拒绝连接。第二堵墙是跨域限制。如果你是在浏览器里用JavaScript去请求直播流或者接口浏览器会拦截跨域响应。就算你用了一些方式强行拉流也很容易遇到CORS错误什么都做不了。第三堵墙是时效性。真实地址里的参数有效期很短过期之后如果播放器重连就再也连不上了。你需要一个定时刷新地址的机制否则直播源就是个一次性用品。用一个PHP脚本做代理服务器本质上就是在你的播放器和B站CDN之间加了一道中转。所有需要校验的请求头由PHP统一加上所有过期失效率由PHP定时刷新所有跨域问题因为PHP是服务端发起的请求所以根本不涉及CORS。这也是我最后选择PHP而不是Node或者Python的原因。论性能PHP不是这块的标杆但它在虚拟主机上的部署成本最低几乎人手一个PHP环境不用单独维护常驻进程。一个php文件就能跑完所有逻辑丢到任何支持PHP的站点上就能用。这个方案的实用性对于个人项目来说比性能更关键。2. API分析找到直播源的关键突破口2.1 抓包定位接口浏览器开发者工具实战要分析B站直播源第一步是弄清楚浏览器在观看直播时到底调用了哪些接口。这个不用瞎猜直接用开发者工具抓包就能看到完整链路。我当时的操作步骤给你参考打开Chrome浏览器按F12进开发者工具切到Network面板。在过滤框里输入xhr只显示异步请求。然后在地址栏打开一个B站直播间随便进一个正在直播的房间。这时候Network面板里会刷出一堆请求进度条一直在转。等直播画面出来之后刷新一下页面在Network里找到一条名字叫getRoomPlayInfo的请求这就是我要找的playUrl接口。如果你找不到这个接口可以试试直接在筛选框里输入playUrl或者live大多数情况下都能定位到。点开这条请求看它的Headers。URL的Query参数里有一串参数比较关键的是room_id、protocol、format、codec、qn这几个。其中room_id就是房间号protocol指定了返回的流协议类型format指定格式codec指定编码qn指定清晰度。再看响应内容。在Preview标签页里可以看到完整的JSON结构。这个结构虽然层级很多但信息量很足整个响应里最重要的几个节点是data.playurl_info.playurl.stream这是一个数组包含了一组流。每个流有protocol_name字段标识是HLS还是HTTP_FLV。每个流里有format数组里面是不同封装格式的信息。每个format里有codec数组代表不同编码H.264、H.265、AV1。每个codec里有url_info数组里面才是真正的服务端地址、当前请求的host以及需要拼接的extra参数。这一层一层剥下来你能看到B站直播源的设计非常模块化它把所有线路线路、清晰度、编码方式全部解耦了后端想切哪个就切哪个。对于咱们要抓源的场景来说只要从中挑选一条合适的线路把host、baseUrl、extra拼接起来就可以得到完整地址。2.2 接口响应结构解读从JSON里挖出真实流地址如果说抓包是找到入口那么读懂响应就是关键的第二步。我第一次拿到这个JSON的时候说实话有点懵字段嵌套非常多而且每个字段的命名都不太直观。这里我把最核心的读取逻辑给你拆开讲。在stream数组里每一个流对象结构都差不多。先用protocol_name字段区分HLS还是FLV。一般建议优先选HLS格式因为HLS是基于HTTP的切片流播放器支持度好天然适合互联网传输而且B站的HLS切片会不断滚动更新容错性比FLV好一些。选中HLS之后进入format数组。这里会看到不同的封装格式名称比如ts、fmp4。从兼容性来说选ts格式最稳。接下来是codec数组主流是avcH.264。在codec对象内部你会看到baseUrl字段这个字段不是一个完整URL只是一个路径类似/live-bvc/xxxx/playlist.m3u8。另外还有一个url_info数组里面通常会有多个备选节点每个节点都有host字段和extra字段。组合方式就是host baseUrl extra三段拼接在一起才是当前这条线路的完整流地址。举个例子假设你拿到的数据是这样的host: https://xy124x.d1c02b04.live-play.acgvideo.combaseUrl: /live-bvc/123456/abcdef.m3u8extra: ?sniabcdefexpires1699999999tokenxxxxx那完整地址就是https://xy124x.d1c02b04.live-play.acgvideo.com/live-bvc/123456/abcdef.m3u8?sniabcdefexpires1699999999tokenxxxxx注意expires参数它就是时效性的来源。我测过几次这个参数的有效期通常在15分钟到1小时之间过期之后即使地址结构完全正确CDN也会返回403或者直接断开连接。所以读取响应里的地址只是第一步真正的难点在于如何处理它的动态刷新。这个问题我们放到PHP代理部分细说。2.3 接口调用时的隐藏要求Referer、UA与时效性很多人在这一步容易翻车明明接口地址抓对了参数也带了但用代码请求的时候就是返回错误。我自己第一次用cURL模拟这个接口时就收到了一个400错误一查代码发现是请求头里没有带Referer和User-Agent。B站直播接口的防盗链机制分两个层面第一层面是接口本身的校验。这个校验主要在HTTP请求头上。如果你不带浏览器会带的那两个头接口可能会返回错误码或者干脆拒绝响应。我测试下来至少需要带上Referer: https://live.bilibili.com/User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36第二层面是CDN的校验。即使你拿到了流地址在进行拉流时CDN也会对请求头发起校验。校验规则和接口类似Referer和User-Agent缺一不可。这也是为什么普通播放器直接用流地址会失败的根本原因。接口时效性方面有一点容易忽略B站的playUrl接口对同一IP的请求频率有一定限制。我做过一个简单测试如果用循环每秒请求一次几分钟之后接口就返回了风控错误。但如果你按照“拉流失败才刷新成功缓存不重复请求”的策略来设计就不会触发这个限制。所以调用这个接口的原则是能少请求就少请求能缓存就缓存千万别做成无脑轮询。3. PHP代理服务器搭建全流程3.1 基础架构设计一个文件解决的事情别写复杂了既然要做PHP代理首先要确定架构。我见过有人把这个问题搞得很复杂又是数据库又是队列其实完全没必要。我们的核心需求就三个接收房间号、获取并缓存流地址、把流数据转发给下游播放器。所以我最终的设计非常简单整个项目就一个proxy.php文件对外暴露两个能力第一接收room_id参数返回一个可用的流地址。这个地址不是B站原始地址而是指向我们自己的proxy.php?actionstreamroom_idxxx。为什么这么做因为如果返回的是B站原始地址播放器在请求真实流时还是会因为缺少Referer而失败。所以必须将地址也代理到我们自己的服务上。第二接收stream请求实时从B站CDN拉流并透传给播放器。这一步绕开了防盗链因为请求是由PHP发起的请求头都在PHP里设置好了CDN会认为这是一个正常的浏览器在观看。这两个能力合在一起就构成了一个完整的透明代理。播放器只需要面对我们的PHP服务PHP服务负责面对B站。这样就实现了请求头、时效性、跨域三个问题的统一化解。你可能会问为什么不直接把流地址返回给播放器让播放器自己直连CDN呢因为播放器没法自定义请求头。VLC虽然有些版本能设置HTTP头但过程非常麻烦而且不是所有播放器都支持。做成全代理之后任何播放器都只需要面对一个普通HTTP地址不需要做任何额外设置。3.2 核心代码实现cURL转发与流透传下面这段代码是我实际在用的核心逻辑我把它精简成了最关键的流程注释也加得比较详细你可以直接参考。?php $action isset($_GET[action]) ? $_GET[action] : play; $roomId isset($_GET[room_id]) ? intval($_GET[room_id]) : 0; if (!$roomId) { http_response_code(400); exit(missing room_id); } $cacheFile __DIR__ . /cache_ . $roomId . .json; // 从缓存里读取流地址避免频繁请求B站接口 $streamUrl ; if (file_exists($cacheFile)) { $cache json_decode(file_get_contents($cacheFile), true); if ($cache[expire_at] time()) { $streamUrl $cache[base_url]; } } // 缓存过期时重新请求playUrl接口 if (!$streamUrl) { $apiUrl https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo; $params [ room_id $roomId, protocol 0,1, format 0,1,2, codec 0,1, qn 10000, platform web ]; $ch curl_init(); curl_setopt_array($ch, [ CURLOPT_URL $apiUrl . ? . http_build_query($params), CURLOPT_RETURNTRANSFER true, CURLOPT_REFERER https://live.bilibili.com/, CURLOPT_USERAGENT Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, CURLOPT_TIMEOUT 10, ]); $response curl_exec($ch); $httpCode curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode ! 200) { http_response_code(502); exit(failed to fetch playurl); } $data json_decode($response, true); // 这里需要按照接口响应结构逐层取字段 // 以HLS、TS、AVC为优先取第一个可用的线路 $stream $data[data][playurl_info][playurl][stream][0] ?? null; $format $stream[format][0] ?? null; $codec $format[codec][0] ?? null; $urlInfo $codec[url_info][0] ?? null; if (!$codec || !$urlInfo) { http_response_code(502); exit(no available stream); } $streamUrl $urlInfo[host] . $codec[baseUrl] . $urlInfo[extra]; // 写入缓存过期时间设为10分钟留一定余量 file_put_contents($cacheFile, json_encode([ base_url $streamUrl, expire_at time() 600, ])); } // 如果请求的是播放列表直接返回流地址 if ($action play) { header(Content-Type: application/json); echo json_encode([url $streamUrl]); exit; } // 如果请求的是流数据则用cURL代理拉取并输出 if ($action stream) { $ch curl_init(); curl_setopt_array($ch, [ CURLOPT_URL $streamUrl, CURLOPT_HTTPHEADER [ Referer: https://live.bilibili.com/, User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, ], CURLOPT_FOLLOWLOCATION true, CURLOPT_HEADER false, ]); curl_exec($ch); curl_close($ch); }这段代码里有两个地方值得细说。第一是缓存设计。我把接口返回的base_url存到一个JSON文件里过期时间设置为600秒也就是10分钟。之所以不直接读原接口过期时间长度的原因是因为我在测试中发现原地址的有效期虽然标注很长但实际在稳定播放时可能因为网络切换、CDN节点调度等原因提前失效。留10分钟的缓存既有一定复用率又不会因为过期时间设得太长导致播放中断后无法快速恢复。第二是透传逻辑。当actionstream时PHP直接用cURL去请求B站CDN拉流。注意我没有设置CURLOPT_RETURNTRANSFER也就是说curl_exec返回的是CDN流数据会直接输出到客户端这种模式就是透传。对于直播流这种大流量的场景透传模式效率较高不会因为先存入PHP内存再输出导致内存暴涨。3.3 缓存与稳定性配置别让代理被B站接口拉黑代理服务器能不能稳定跑关键在缓存策略和失败处理上。如果缓存没做好每次播放器重连都会触发一次playUrl接口调用高并发场景下很容易被封IP。我这边的做法是三级并发控制第一级是文件缓存。如果当前房间的流地址还在有效期直接复用不再请求B站接口。这个逻辑上面代码里已经写了。第二级是过期刷新加锁。这里有个小坑如果多个播放器同时请求同一个房间而缓存刚好过期可能会同时触发多个playUrl接口请求。解决方式是在请求接口前先写一个锁文件。$lockFile __DIR__ . /lock_ . $roomId . .lock; while (file_exists($lockFile)) { usleep(200000); // 等待200毫秒 } file_put_contents($lockFile, time()); // 请求接口、写缓存... unlink($lockFile);第三级是接口失败降级。如果playUrl接口返回失败不要直接退出而是尝试继续使用旧的缓存地址播放。因为有时候旧地址虽然标记过期但短时间内通过CDN的宽容校验还能拉流。这个策略在直播断流重连时非常有用能明显减少播放器报错。稳定性配置方面还有几个细节PHP的max_execution_time要设置大一些直播流是长时间连接的默认的30秒会切断连接。可以在脚本开头加set_time_limit(0)来解除这个限制。另外如果跑的服务器内存比较小要留意PHP运行内存透传模式下虽然内存占用不大但多次并发连接时仍会累积建议在Nginx或Apache层面做好连接数限制。4. 实操过程与核心环节实现4.1 完整接入步骤从房间号到可播放地址为了让这套流程更清晰我再说一下从拿到一个直播间到最终能在播放器里打开到底要经过哪几步。假设你现在要看B站某个直播间地址是https://live.bilibili.com/123456那么123456就是房间号。操作流程把proxy.php文件放到你PHP服务器的web目录里确保你有访问权限。然后用浏览器或者HTTP工具访问http://你的服务器地址/proxy.php?room_id123456actionplay正常的话返回的JSON里会有一个url字段这个url指向你自己的代理地址比如http://你的服务器地址/proxy.php?room_id123456actionstream把这段地址直接复制到VLC、PotPlayer或者其他播放器打开网络串流就可以开始播放了。如果你用的是支持m3u8的客户端也可以把这个地址作为一条m3u8链接添加到IPTV观看工具里。对于不理解m3u8格式的播放器可以直接用这个地址因为它本身就是一个HLS播放列表地址播放器会按照HLS协议自动解析。整个过程里你唯一需要提供给用户的信息就是代理地址。这个地址对外是完全中立的不包含B站特有的参数也不包含时效戳对所有下游播放器透明。4.2 生成m3u8地址让VLC和IPTV播放器直接用说到m3u8我额外多说一点。B站直播的HLS流返回的m3u8播放列表里有多个分片地址这些分片地址默认是指向B站CDN的播放器在请求这些分片时同样会遇到防盗链问题。要让播放器真正能流畅播放最省事的方法其实就是我们上面说的全代理方式把整个流请求都指向PHP代理代理层去处理CDN的请求头。如果你坚持要生成一个m3u8文件供本地播放器使用那就要做一步改写解析B站返回的m3u8内容把里面所有分片地址的host部分替换成我们自己的代理地址同时给每个分片地址带上代理参数。这样做的好处是不需要PHP持续长连接转发流对服务器的资源占用会小一些。坏处是每次分片切换都要经过PHP延迟会高一点。我个人实测下来如果只是自己看全代理方式就够用了不用折腾m3u8改写。但如果是要给一堆朋友分享直播源分发m3u8链接会更方便因为这时候源地址是静态的不怕连接数一多把PHP打挂。改写m3u8的核心逻辑是正则替换你可以把B站返回的ts分片地址批量替换成代理地址。替换时注意保留原分片地址作为参数PHP接到请求后再转发到B站CDN。4.3 多线路与画质切换protocol/codec参数怎么选我的代码在请求playUrl接口时protocol参数写的0,1format写的0,1,2codec写的0,1。这几个数字代表的意思是告诉B站后端我能支持这些格式和编码你优先返回我需要的类型。实际返回的数据里stream数组会按B站后端的优先级排列。以我的经验前端展示的是最优线路越靠后的线路越可能是备胎。如果你发现第一条线路在某些时段的延迟较高可以尝试取stream数组里的第二个甚至第三个元素作为播放源。画质方面qn参数控制清晰度。10000代表原画相当于直播间的最高清晰度。如果你只想取高清流可以改成400蓝光、250超清、150高清等等。这里有个细节如果你的账号没有登录部分直播间的原画可能无法播放这时候qn可以设置小一点比如400。编码方面H.265hevc的压缩率高同样画质下码率更低但老一些的播放器不支持解码。H.264avc兼容性最好。我这边的折中方案是优先取avc如果播放器支持hevc再切换。为了让优先顺序可控请求接口时codec参数可以只写0avc这样后端就只会返回avc编码的流。当然如果你的播放器解码能力强也可以把codec写成0,1然后自行迭代选择。5. 常见问题与排查技巧实录5.1 典型报错速查表我在搭建和使用过程中遇到过不少报错有些问题真是找半天才找到原因。下面这个表格是我整理的典型问题和解决办法你在排查时可以直接对照。现象可能原因解决方案请求playUrl接口返回400缺少Referer或User-Agent在cURL请求头里带上B站直播间来源接口返回数据里没有stream字段房间号不存在或未开播确认房间号正确检查直播间是否正在直播播放器打开代理地址后报403代理转发时请求头缺失在透传流数据时也要设置Referer和UA播几分钟后卡住或断流缓存地址过期播放器重连调短缓存时间让过期后能及时刷新多个用户同时看代理服务器卡顿并发连接过多PHP内存或连接数打满开启连接数限制或者改用静态m3u8分发接口请求频繁之后返回风控同一IP请求次数过多加大缓存时间增加锁机制避免并发重复请求代理地址在部分播放器报无法解析播放器不支持HLS或TS封装换用支持m3u8的播放器或改用FLV协议流5.2 实战排查案例403、超时与缓存失效挑两个我在实际使用中最典型的案例出来讲讲这些坑属于网上文档几乎不会写但踩一次能折腾你半天的类型。第一个案例是403问题。我最初实现透传时只在请求playUrl接口时带了Referer和UA但在透传流数据时没带。结果就是流地址获取成功播放器连接代理地址也正常但视频画面死活不出来日志里全是403。排查了很久才意识到B站CDN对拉流请求的校验比对接口请求的校验还要严格所以透传时也必须带上同样的请求头。这是一个非常容易遗漏的细节。第二个案例是缓存失效导致黑屏。我之前把过期时间设定为30分钟想着能省点接口请求。实际运行时发现有些直播间的CDN节点在10分钟左右就会做节点切换导致旧地址中的host无法访问。播放器一直请求这个过期的host自然黑屏。后来统一改成10分钟缓存并在播放失败时主动清理缓存让下一次请求重新拉取新地址问题就解决了。5.3 避坑心得与长期维护建议最后一个部分说说我积累下来的一些心得这些经验适用于任何准备长期维护直播源接入的人。不要过度依赖固定线路。B站的CDN节点调度是动态的今天能用的host可能明天就延迟暴涨。我的建议是每次缓存刷新时可以顺手把stream数组里的前两条线路都记下来播放时如果第一条线路卡顿能快速切到备用线路。不要忽略日志。PHP代理虽然简单但出问题时没有日志会特别痛苦。我在代码里加了简单文件日志每次请求接口、生成缓存、转发流都记录一行。这样出问题时翻一下日志就能判断是缓存问题、CDN问题还是请求头问题不用瞎猜。注意安全与合规。代理地址一旦公开可能会被很多人同时使用。为了不影响B站正常服务也为了自己的服务器不被打爆建议在代理层做简单的访问限制。可以限制只允许特定IP段访问也可以给代理地址加一个访问token只有带正确token的请求才会被转发。这样既能保证自己的使用体验又不会给源站带来不必要的压力。长期维护方面建议三个月检查一次接口结构。B站前端接口偶有调整字段名和返回结构可能会变化。你只需要定期抓包看看最新返回的JSON和你的解析代码是否对得上发现字段变了就更新一下解析逻辑其他基本不用动。这套PHP代理方案我目前已经连续用了大半年整体稳定性我还是比较满意的。如果你只是自用部署一个文件就够如果想分享给更多人再加点访问控制就行。希望这篇避坑指南能让你少走弯路顺利把B站直播源接入自己的播放器里。
返回列表