“2026年还到处求B站直播源m3u地址的兄弟我劝你趁早死心。B站直播流的真实地址是动态签名的短的一两分钟、长的五分钟就过期市面上所有所谓“稳定B站直播源”要么背后挂着一套自动解析脚本要么就是用来引流的过期假源。今天我把整套方案完整拆开从B站直播API的分析、房间号解析到用PHP搭一个属于自己的代理服务器一步步讲清楚顺便把这两年踩过的坑全部记录下来。这篇东西适合三类人看——自建IPTV源的老哥、想研究B站接口的爬虫新手、以及被各种失效源折腾到头秃的播放器折腾党。”1. 从需求到方案B站直播源抓取的完整思路1.1 想清楚抓直播源到底是在抓什么先说一个核心概念很多人搞混了B站直播源不是一条固定的流地址而是一个“需要实时解析出来的动态URL”。它的构成通常是“CDN节点地址 一段带签名的query参数”签名里有时间戳、过期时间、加密校验位过期之后就算你把地址复制下来也播放不了。所以网上下载的“B站直播源2026最新配置”这类文件本质上是一份“解析接口列表”而不是真正的流地址。发布者跑了一个定时脚本每过几十秒就去B站API重新解析一次再把解析出来的新地址写进文件里然后通过HTTP协议让你播放器去拉取。一旦发布者停止维护、接口被风控、或者解析频率过高被B站拉黑你的源就“失效”了。理解了这个逻辑你就会明白最靠谱的做法不是等别人发源而是自己搭一条解析链路拿到直播间号请求B站API拼出临时流地址返回给播放器。整套链路的核心就两件事——API参数别写错代理脚本别被风控。1.2 一条请求链路的完整画像B站网页端在浏览器里播放直播时走的也是同样的链路只不过浏览器帮它处理了签名、Cookie、请求头这些事情。我自己整理过一条完整的请求时序按这个顺序理解就不会乱输入房间号可能是短号比如主播ID是1024先调用room_init接口拿到真实房间号room_id和直播状态live_status。如果live_status等于1说明正在直播接着调用播放流接口getRoomPlayInfo带上清晰度参数、协议类型参数、编码参数。接口返回的JSON里嵌套了三层结构stream流协议→format封装格式→codec编码方式最里层才是真正的CDN地址和签名参数。把host base_url extra拼成一个完整URL交给播放器。播放器拿到URL后请求CDN拉流开始播放。整个流程看起来不复杂但实际每一步都有坑。比如短号不转真实房间号直接请求会报错协议参数不写全返回的流不完整拼URL的时候漏了extra导致403……这些我在后面章节会逐个点名。1.3 为什么中间非要加一层PHP代理可能有人问我直接用浏览器开发者工具复制一个流地址再用播放器打开不就行了可以但地址几分钟就过期你每次都得手动复制给自己找罪受。而写一个解析脚本挂在服务器上把“解析”这件事自动化才是长期稳定的方案。至于为什么选PHP而不是Python、Node或者Go我的理由很实际PHP部署门槛低只要是台能跑Web服务的机器装个Nginx/Apache就能跑虚拟主机甚至服务器面板都自带PHP环境而且PHP的curl扩展默认开启率极高写个几百行的脚本就能完成所有功能。Python当然也能做但要常驻进程、配调度器对只想自用的朋友来说维护成本偏高。代理脚本在这一套方案里承担三个作用第一统一在服务端带上User-Agent、Referer、Cookie等请求头解决播放器发起的请求因为缺头被B站拒绝的问题第二把复杂的JSON解析逻辑封装成一个简单接口播放器端只需要记住一个地址第三顺带解决跨域问题方便你在自己网页里嵌入直播。理解了这三点你就能明白为什么“代理服务器”是整套直播源方案的枢纽。2. 直播API深度拆解每个参数都是坑2.1 第一步房间号转换短号与真实房间号B站直播间有两种房号一种是短号比如主播自己设置的1024、77777这种好记的号码另一种是系统分配的真实房间号通常是8到10位的数字。网页端地址栏里显示的是短号但大部分API接口只认真实房间号所以第一步永远是先做转换。转换接口是curl https://api.live.bilibili.com/room/v1/Room/room_init?id1024 \ -H User-Agent: Mozilla/5.0 \ -H Referer: https://live.bilibili.com/返回的JSON长这样{ code: 0, msg: ok, data: { room_id: 21452505, short_id: 1024, uid: 123456, live_status: 1 } }code等于0才代表请求正常data.room_id是真实房间号后续所有请求都用它data.short_id就是短号live_status是直播状态1是直播中0是未开播2是轮播中。这个字段建议脚本里必须判断否则你在主播没开播的时候去请求播放流拿到的多半是空数据或者报错数据。有人会问未开播能不能拿轮播流可以但我个人不建议把轮播流写进自建源里因为轮播内容是循环播放的录像而且地址有效期更短容易让播放器反复重连体验很差。我一般只返回live_status为1的直播流。2.2 第二步播放流接口getRoomPlayInfo参数详解拿到真实房间号后主推的接口是这个curl https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo?room_id21452505protocol0,1format0,1,2codec0,1qn10000platformweb \ -H User-Agent: Mozilla/5.0 \ -H Referer: https://live.bilibili.com/这个接口的几个参数值得好好讲因为它们直接影响你能不能拿到理想清晰度的流room_id真实房间号用第一步转换后的值。protocol请求的协议类型0代表http_stream也就是FLV流1代表http_hls也就是HLS/m3u8流。这里写0,1就是两种都要让接口自己按可用性返回。format封装格式0是FLV1是TS2是FMP4。不同协议支持不同格式HLS一般返回TS或FMP4FLV协议返回FLV。codec视频编码0是AVCH.2641是HEVCH.265。H.265压缩率高但很多播放器支持不好我建议让它同时返回播放器自己选。qn清晰度等级10000是原画400是蓝光250是超清150是高清80是流畅。这里有个细节接口返回的accept_qn列表里才是这个直播间实际支持的清晰度如果你请求了不支持的清晰度接口会自动降级到最接近的档位。所以代理脚本最好解析一下accept_qn再回传实际生效的current_qn方便前端显示。2.3 响应结构解析与URL拼接规则这个接口返回的JSON嵌套层级比较深很多人在解析这步翻车。我先把关键结构列出来{ code: 0, data: { room_id: 21452505, playurl_info: { playurl: { stream: [ { protocol_name: http_hls, format: [ { format_name: fmp4, codec: [ { codec_name: avc, current_qn: 10000, accept_qn: [10000, 400, 250, 150], base_url: /live-bvc/12345/live_12345.m3u8, url_info: [ { host: https://xxx.play.bilibili.com, extra: ?expire1710000000signabc123, stream_ttl: 120 } ] } ] } ] } ] } } } }完整的流地址拼接规则是完整地址 url_info[0].host base_url url_info[0].extra很多人的第一个坑就是只拼了host base_url把extra漏掉了。extra里带着签名和过期时间不拼上播放器直接403。第二个坑是url_info有可能是数组里面有多个备选CDN节点比如节点A解析失败可以换节点B。第三个坑是base_url开头可能没有斜杠也可能有拼接时要自己兼容处理一下。解析的时候我建议先按协议偏好排序优先取http_hls拿不到再用http_stream然后按编码偏好排序优先avc再hevc最后按清晰度排序优先qn10000原画接口不支持再降级。这样写出来的脚本无论面对什么直播间都能稳定返回一条能播的地址。2.4 请求头与Cookie最容易被忽略的保命项B站直播API对请求头是有要求的。我实测下来User-Agent和Referer缺一不可尤其是Referer必须写成https://live.bilibili.com/或者对应的直播间页面地址否则很容易触发403或者风控。Cookie这块要单独说。第一次请求时B站可能会要求你带上buvid3这类浏览器指纹Cookie没有的话部分接口会返回-352风控错误码。解决办法很简单用一个浏览器正常打开一次B站直播间从开发者工具里复制Cookie存到脚本里备用或者让PHP脚本先请求一次B站首页把返回的Set-Cookie存起来后续请求带上。我自己的策略是Cookie存成一个配置文件每天早上自动更新一次避免时间长了失效。请求频率方面同一个直播间两次解析间隔控制在30秒以上一台服务器一天总请求量控制在几百次以内基本不会触发风控。这个频率对个人自用完全够因为流地址有效期普遍有120秒你完全不需要频繁刷新。2.5 备用老接口playUrl有时候getRoomPlayInfo会因为平台调整接口而抽风这时候可以切到老接口做备用curl https://api.live.bilibili.com/room/v1/Room/playUrl?cid21452505quality4platformandroid \ -H User-Agent: Mozilla/5.0 \ -H Referer: https://live.bilibili.com/这个接口返回的是data.durl数组每个元素有url和length字段地址已经拼好直接用就行。quality取值是另一套体系4是原画3是蓝光2是超清1是流畅。老接口的优点是解析简单、响应快缺点是偶尔会返回空durl所以我只把它当备用通道主通道还是用getRoomPlayInfo。脚本里做一个自动降级主接口拿不到流自动切备用接口。3. PHP代理服务器搭建完整实录3.1 环境准备最少需要什么我本地用的是一台1核1G的云服务器装的是DebianNginxPHP 7.4实际跑下来CPU和内存完全够。如果你手头没有服务器用一台能跑PHP的电脑也行——Windows装个PHPStudyMac用自带PHPLinux就装php-fpm本地调试完再部署到线上。PHP这边确保curl扩展和json扩展是开启的。检查命令php -m | grep curl php -m | grep json只要有输出就说明扩展正常。如果是虚拟主机环境一般也都默认带curl实在不行可以改用file_get_contents加stream_context的方式发HTTP请求但curl更稳定我强烈建议优先用curl。部署结构上我习惯把所有脚本放在/var/www/live-proxy/目录下只留一个入口文件live.php播放器只认这一个地址。这样出问题时排查路径最短。3.2 第一步完整代码框架下面这套代码是我目前线上在跑的版本减掉了注释和一些容错分支核心逻辑完整保留。你直接存成live.php就能用。?php /** * B站直播源解析代理 * 用法 * /live.php?room1024formatjson * /live.php?room1024formatm3u8 * /live.php?room1024formatm3u */ header(Content-Type: application/json; charsetutf-8); header(Access-Control-Allow-Origin: *); $room isset($_GET[room]) ? intval($_GET[room]) : 0; $format isset($_GET[format]) ? $_GET[format] : json; if ($room 0) { die(json_encode([code -1, msg 缺少room参数])); } $headers [ 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, Referer: https://live.bilibili.com/, Cookie: buvid3你的buvid3 ]; function httpRequest($url, $headers) { $ch curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); $result curl_exec($ch); $err curl_error($ch); curl_close($ch); if ($err) { return null; } return $result; } // 第一步房间号转换 开播状态判断 $initJson httpRequest( https://api.live.bilibili.com/room/v1/Room/room_init?id{$room}, $headers ); $initData json_decode($initJson, true); if (!$initData || $initData[code] ! 0) { die(json_encode([code -2, msg 房间号解析失败])); } $realRoomId $initData[data][room_id]; $liveStatus $initData[data][live_status]; if ($liveStatus ! 1) { die(json_encode([code -3, msg 当前未开播, room $realRoomId])); } // 第二步获取播放流 $playApi https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo . ?room_id{$realRoomId} . protocol0,1 . format0,1,2 . codec0,1 . qn10000 . platformweb; $playJson httpRequest($playApi, $headers); $playData json_decode($playJson, true); if (!$playData || $playData[code] ! 0) { die(json_encode([code -4, msg 播放流获取失败可尝试备用接口])); } // 第三步从嵌套JSON里提取最合适的流地址 $streams $playData[data][playurl_info][playurl][stream] ?? []; if (empty($streams)) { die(json_encode([code -5, msg 直播流为空])); } $playUrl ; $currentQn 0; $protocolName ; foreach ($streams as $stream) { foreach ($stream[format] as $formatItem) { foreach ($formatItem[codec] as $codecItem) { if ($codecItem[codec_name] ! avc) { continue; } if (empty($codecItem[url_info])) { continue; } $firstNode $codecItem[url_info][0]; $playUrl $firstNode[host] . $codecItem[base_url] . $firstNode[extra]; $currentQn $codecItem[current_qn]; $protocolName $stream[protocol_name]; break 3; } } } if (empty($playUrl)) { die(json_encode([code -6, msg 没有找到可用流地址])); }上方代码里有一行Cookie: buvid3你的buvid3我需要特别解释一下这个值从浏览器开发者工具里复制具体位置是网页请求的Cookie字段中buvid3xxx这一段。如果拿不到也可以让脚本先请求一次B站首页获取但我为了代码简洁直接在配置里写死了。有人会问这么写会不会失效实测一个buvid3用一周问题不大失效了重新换一个就行。3.3 第二步URL拼接逻辑与质量参数上面代码里有一段很关键的循环——从三层嵌套结构里找到AVC编码的流地址。我为什么坚持优先AVC而不是HEVC因为H.265编码在PC播放器上还好但在电视盒子、老设备上的兼容性很差经常出现有声音没画面。自建直播源就是为了在不同设备上都能看兼容性优先级必须排在画质前面。还有一点是清晰度策略。我请求参数里写的是qn10000但主播没开原画时接口会返回current_qn400或者其他档位。这个值建议通过formatjson返回给前端方便你自己判断当前是什么清晰度。如果你想让某个直播间强制固定清晰度可以在入口参数里加一个qn把它直接透传到API请求参数里。3.4 第三步输出m3u格式直播列表如果你是用PotPlayer、VLC或者安卓上的IPTV播放器最方便的做法是让代理脚本直接生成m3u格式的播出列表。我加了这样一个分支// 第四步按format参数输出不同格式 switch ($format) { case m3u8: header(Location: . $playUrl); break; case m3u: header(Content-Type: audio/x-mpegurl; charsetutf-8); echo #EXTM3U\n; echo #EXTINF:-1,B站直播间-{$room}\n; echo $playUrl . \n; break; case json: default: header(Content-Type: application/json; charsetutf-8); echo json_encode([ code 0, room $realRoomId, qn $currentQn, protocol $protocolName, url $playUrl ], JSON_UNESCAPED_SLASHES); break; }这里有个细节m3u8分支我用了301跳转而不是直接输出内容好处是播放器请求/live.php?room1024formatm3u8时会被引导到真实的CDN地址上PHP进程立刻释放不占额外带宽。但坏处是部分播放器跳转时不带Referer而CDN那个地址恰好不校验Referer实测是可以播放的如果遇到校验严格的线路就需要用下面第5章讲的中转方式。至于m3u分支适合把多个直播间一起管理。你可以再写一个playlist.php遍历频道列表把每个直播间都输出成一行m3u条目这样一台电视上就能看到你订阅的所有主播谁开播了就直接点进去没开播的播放器会自动跳过或者报错比手动切直播间方便太多。3.5 第四步部署测试本地调试时直接进到脚本目录启动PHP内置服务器cd /var/www/live-proxy php -S 0.0.0.0:8080 live.php然后浏览器访问http://localhost:8080/live.php?room1024formatjson看到返回JSON且url字段不为空就说明解析成功。把这个URL复制到VLC的网络串流里能出画面说明整条链路通了。线上部署我更推荐NginxPHP-FPM。Nginx配置写一个站点指向项目目录server { listen 80; server_name your-domain.com; root /var/www/live-proxy; index live.php; location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php7.4-fpm.sock; } }部署完记得测试一下HTTPS。如果你的服务器有SSL证书建议把整个站点跑在HTTPS下因为有些播放器对HTTP流和HTTPS流混合内容有拦截统一HTTPS能少踩很多坑。3.6 给接口加一个简单缓存直播流地址有效期虽然短但同一时间你可能有多个设备在看同一个直播间。每台设备都请求一次B站API的话既浪费请求次数也可能触发频率风控。我加了一个简单的文件缓存$cacheFile __DIR__ . /cache_ . $realRoomId . .json; $cacheTtl 30; if (file_exists($cacheFile) (time() - filemtime($cacheFile)) $cacheTtl) { $cached json_decode(file_get_contents($cacheFile), true); if ($cached $cached[url]) { $playUrl $cached[url]; $currentQn $cached[qn]; } } // ... 解析成功后写缓存 file_put_contents($cacheFile, json_encode([ url $playUrl, qn $currentQn, time time() ]));缓存时间我设的是30秒。流地址有效期120秒30秒的缓存既不会让你拿到过期地址又能显著降低API请求频率。如果你只有一个设备这个缓存加不加无所谓但如果家里有电视、手机、电脑同时看这个设计能让体验稳定很多。4. 避坑全景图常见问题与排查速查表4.1 403 ForbiddenReferer与UA校验这是我第一次搭建时遇到的头号问题。播放器拿到live.php返回的地址后去请求CDN直接被B站返回403。排查到最后发现是两个原因一是User-Agent被播放器改成了自己的标识B站CDN不认二是Referer为空。如果是VLC、PotPlayer这类PC播放器可以在播放器设置里手动给网络流添加User-Agent和Referer请求头。VLC的路径是“工具-偏好设置-全部-输入/编解码器-HTTP头”把User-Agent和Referer填上PotPlayer是右键-选项-播放-网络-自定义HTTP用户代理。如果播放器不支持自定义请求头那就只能走中转方案了见4.5。4.2 -352风控Cookie与buvid3code:-352这个错误码是B站风控返回的意思是请求被判定为异常流量。触发条件通常是请求频率过高、缺少Cookie、IP被标记。解决办法按优先级排序加上buvid3Cookie这是最有效的。降低请求频率同一直播间两次请求至少间隔30秒。检查服务器IP有没有被B站标记如果是共享IP换个IP试试。确保Referer完整别用localhost之类的地址。我的脚本里已经内置了Cookie正常自用基本不会触发风控。如果真触发了建议停一分钟再试不要反复请求否则会加重风控。4.3 流地址很快失效缓存与主动刷新播放过程中画面突然断了重新打开播放器又能看这种体验我遇到过很多次。原因是播放器拉取的地址过期了而播放器没有自动重新请求直播源。解决思路有三个层面第一代理脚本缓存时间不要超过60秒保证播放器每次“重连”拿到的是新鲜地址。第二在播放器设置里开启自动重连VLC和PotPlayer都有这个选项开播后就算断流也会自动去请求live.php重新拉流。第三如果你用的播放器支持“失败后重新加载”逻辑把重试间隔设成5到10秒体验会平滑很多。4.4 FLV还是HLS延迟与兼容性取舍getRoomPlayInfo同时返回http_stream和http_hls两种协议我默认解析代码里是先找HLS的。为什么HLSm3u8的兼容性明显更好几乎所有的IPTV播放器、电视盒子、安卓播放器都支持而且HLS在弱网下的卡顿率低于FLV因为它有分片缓冲机制。缺点就是延迟会高一些实测大约比FLV高5到15秒。FLV走的是http_stream协议延迟能做到两三秒但有些播放器不支持需要转封装或者装插件。我的建议是手机、电脑上追求实时互动用FLV电视、盒子上追求稳定使用HLS。不用纠结谁好谁坏让代理脚本两种都解析你在使用端选对应的format参数就行。4.5 播放器不认自定义请求头怎么办中转模式这是自建源的终极难题。电视上的IPTV播放器比如tivi、Televizo大多数不支持修改HTTP请求头直接拉CDN地址大概率被403拒掉。解法是让PHP脚本去请求CDN流再把数据流原样转发给播放器这样播放器只跟你的服务器通信不直接面对B站CDN。case relay: $ch curl_init(); curl_setopt($ch, CURLOPT_URL, $playUrl); curl_setopt($ch, CURLOPT_HTTPHEADER, [ User-Agent: Mozilla/5.0, Referer: https://live.bilibili.com/ ]); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, false); curl_setopt($ch, CURLOPT_TIMEOUT, 0); curl_exec($ch); curl_close($ch); break;这个方法有个致命缺点所有流量都经过你的服务器1万码率的直播流跑起来一个月轻松烧掉几个TB流量。所以我的立场很明确PC上能用播放器自定义请求头解决就用直接模式只有电视播放器实在搞不定才开中转模式而且必须限制中转线路数量防止服务器流量爆掉。4.6 常见问题排查速查表现象可能原因解决办法接口返回code不为0参数错误或接口变更检查room_id、协议参数换备用接口返回-352触发风控加Cookie、降低请求频率播放器403缺Referer或UA播放器设置请求头或开中转模式能播但没过多久断流流地址过期播放器开自动重连缩短PHP缓存时间m3u文件批量导入失败m3u格式问题或未开播检查EXTINF行格式过滤未开播直播间服务器流量飙升中转模式流量过大尽量用直接模式限制中转直播间数量直播源无法在电视上播放电视播放器不支持FLV返回HLS地址或换用支持的播放器HTTPS页面请求HTTP流被拦混合内容拦截给代理站点配SSL证书全站HTTPS这张表我打印了一份贴在工位上服务器出问题先看症状再猜原因比瞎改代码快得多。5. 稳定运行的进阶建议5.1 多直播间管理与在线状态监控当你不再满足于单个直播间的解析而是想维护一份常备直播列表时需要引入“订阅管理”的概念。我自己的做法是在项目目录放一个channels.json存着所有你想关注的主播格式如下[ {name: 主播A, room: 1024}, {name: 主播B, room: 77777} ]再写一个cron任务每5分钟调用一次room_init接口更新每个主播的live_status生成一份live_on.json。播放列表playlist.php只从live_on.json里取数据这样维护起来非常清晰——想加主播改一个文件不想看哪个了删一行播放器端永远只面对一份干净又不过期的列表。5.2 多线路容错与备用接口B站CDN节点偶尔会抽风表现为某个host解析出来但连不上超时。我的解析脚本里做了一个小改进遍历url_info数组如果第一个节点不可达就自动尝试第二个节点。这个逻辑放在解析阶段的意义比中转阶段更大因为解析阶段换节点几乎零成本只需要换host重新拼一次URL。备用接口的逻辑前面提过getRoomPlayInfo拿不到流时自动切到playUrl老接口。两个接口都失败正常返回错误码而不是输出一个空地址这样播放器能明确知道“源坏了”而不会一直卡在加载状态。5.3 几条重要的合规边界玩了两年B站直播源解析我发现这东西确实方便但也有几条红线必须遵守。这套技术方案请只用于个人学习、自用观看不要拿去做二次分发、商业贩卖、公开平台聚合更不要用来搭建绕过会员付费的渠道。解析接口的请求频率务必保持克制不加任何优化地高频抓取会给平台服务器造成压力这不是一个技术玩家该干的事。如果某个直播间明确显示了版权保护标识请尊重版权方的要求不要强行解析和录制。我见过一些卖“付费直播源”的群本质上就是把这种解析脚本包装一下再卖买家既不知道原理也不知道哪天就失效。与其花那个冤枉钱不如自己抽一个下午把这套东西搭起来稳定的同时还能学到API调用、JSON解析、HTTP请求头这些基础知识怎么算都不亏。最后分享一个小技巧如果你只是偶尔用手机看直播又嫌官方App广告多完全可以把自己搭的live.php地址存到手机浏览器书签里配合VLC移动版打开体验相当于一个无广告纯净版直播间。我自己就是这么干的省心得很。
