Ayusin ang yt-dlp sa Douyu: PhantomJS not found at room ID errors
Petsa ng paglalathala
I-download ang plugin na pang-ayos douyu_signing.py
Kung may PhantomJS not found kapag nagda-download ka ng mga Douyu video gamit ang yt-dlp, o Unable to extract room id kapag nagre-record ng live stream, maaari mong i-load ang compatibility plugin sa artikulong ito. Inaayos nito ang tatlong problema: pagkuha ng room ID mula sa bagong live-room page, paggawa ng signature gamit ang Deno, at pagbuo ng tamang live-stream URL.
Kung ayaw mong mag-configure ng Deno at plugin directory sa iyong computer, i-paste ang Douyu link sa downloader sa homepage at gamitin ang online tool.
Tatlong sunod-sunod na problema ang nakita namin habang sinusuri ang live stream at video-on-demand ng Douyu. Gumagamit na ng mga Next.js data block ang bagong pahina, kaya hindi na gumagana ang lumang regular expression para sa room ID. Matapos iyon ayusin, direkta pa ring tinatawag ng orihinal na extractor ang lipas nang PhantomJS para gumawa ng signature. At nang gumana na ang signature at lumabas ang mga quality format, nawawala naman ang directory na /live/ sa nabuong FLV stream URL. Unahin natin ang mga hakbang sa pag-aayos, saka ang dahilan ng bawat problema.
Mga sintomas at bahaging pumapalya
| Error o sintomas | Saan ang problema |
|---|---|
Unable to extract room id |
Gumagamit ang bagong live-room page ng Next.js data stream, kaya hindi na tumutugma ang lumang pattern na $ROOM.room_id |
PhantomJS not found |
Direktang tinatawag ng orihinal na douyutv.py ang PhantomJSwrapper; hindi mababago iyon sa pag-install lang ng Deno |
Nakalista ang mga format at gumagana ang -F, pero humihinto ang download dahil sa error |
Walang slash sa dulo ng rtmp_url na ibinabalik ng API, kaya inaalis ng urljoin ang directory na /live/ kapag pinagdudugtong ang URL |
The channel is not currently live |
Hindi live ang room; normal ang paghinto batay sa estado ng room |
The channel is auto-playing VODs |
Paulit-ulit na nagpe-play ng mga VOD ang room (videoLoop: 1); ihihinto ng plugin ang extraction |
Mga hakbang para ayusin ang problema
1. Ihanda ang environment at file ng plugin
Tiyaking naka-install ang yt-dlp, deno, at ffmpeg at mapapatakbo sila mula sa terminal. Ginamit namin sa test ang yt-dlp nightly 2026.10.29.232701, Deno 2.9.8, at FFmpeg 7.1.5. Nakadepende ang plugin sa method na _search_nextjs_v13_data ng yt-dlp, kaya i-update muna ang yt-dlp kung masyadong luma ang iyong bersyon.
Gawin ang sumusunod na mga folder sa working directory, at i-save ang kumpletong code ng plugin bilang douyu_signing.py:
plugins/
└── douyu/
└── yt_dlp_plugins/
└── extractor/
└── douyu_signing.py
Patakbuhin ang lahat ng susunod na command sa directory na naglalaman ng folder na plugins. Ilo-load ng --plugin-dirs ./plugins ang plugin nang hindi binabago ang source code sa installation directory ng yt-dlp.
2. Tingnan ang listahan ng mga live-stream format
Pampublikong live room ang ginagamit sa halimbawang ito. Palitan ang URL ng address ng room na gusto mong suriin:
yt-dlp --ignore-config --plugin-dirs ./plugins --no-cache-dir -v -F "https://www.douyu.com/pigff"
Kapag may [DouyuTV+DenoSigning] sa log, aktibo na ang plugin. Kung live ang room, tatawagin ng extractor ang getH5Play API at ipapakita ang mga format para sa iba't ibang kalidad. Kung The channel is not currently live ang lumabas, hindi kasalukuyang nagbo-broadcast ang room. Subukan ang ibang room na live.
3. Mag-record ng 30 segundo para suriin ang media stream
Ang listahan ng mga format ay nagpapatunay lang na nagbalik ng error: 0 ang API na tumatanggap ng signature. Kailangan ding aktuwal na kumuha ng datos mula sa stream para matiyak na tama ang pagkakabuo ng CDN URL:
yt-dlp --ignore-config --plugin-dirs ./plugins --no-cache-dir --downloader ffmpeg --downloader-args "ffmpeg_i:-rw_timeout 15000000" --downloader-args "ffmpeg_o:-t 30" -f worst -o "douyu-live.%(ext)s" "https://www.douyu.com/pigff"
Pinipili ng -f worst ang pinakamababang bitrate para maliit lang ang test file. Nililimitahan ng -t 30 ang recording sa 30 segundo, at 15 segundo ang timeout sa pagbasa at pagsulat sa network.
4. Suriin ang VOD at ang mga file (opsyonal)
Sakop din ng parehong plugin ang video-on-demand extractor ng Douyu na DouyuShowIE. Kapag VOD ang sinusubukan, makikita sa log ang [DouyuShow+DenoSigning]:
# List VOD formats
yt-dlp --ignore-config --plugin-dirs ./plugins --no-cache-dir -F "https://v.douyu.com/show/mPyq7oVNe5Yv1gLY"
# Download the VOD test sample
yt-dlp --ignore-config --plugin-dirs ./plugins --no-cache-dir -f worst -o "douyu-video.%(ext)s" "https://v.douyu.com/show/mPyq7oVNe5Yv1gLY"
Para malaman kung may sirang frame ang na-record na .flv o .mp4 at kung kumpleto ang mga audio at video track, patakbuhin ang ffprobe at i-decode ang buong file:
ffprobe -v error -show_entries stream=codec_name,codec_type,width,height:format=duration,size -of json "douyu-live.flv"
ffmpeg -nostdin -v error -xerror -i "douyu-live.flv" -map 0:v:0 -map "0:a:0?" -f null -
Teknikal na paliwanag sa tatlong problema
Unang problema: hindi na nababasa ng lumang pattern ang room ID sa datos ng Next.js
Dati, kinukuha ng orihinal na extractor ang room ID mula sa assignment sa $ROOM.room_id sa pahina. Nasa issue #16230 ang kaugnay na error. Pero sa bagong mga Douyu live-room page, gamit man ang alias na pigff o ang numeric ID na 593229, nailipat na ang metadata ng room sa mga script data block ng Next.js App Router (__next_f.push).
Kung hahanapin lang ang room_id sa buong pahina gamit ang isang malawak na regular expression, madaling makuha ang ID ng isang inirerekomendang room sa sidebar o ibaba ng pahina. Kaya ginagamit ng plugin ang _search_nextjs_v13_data para i-parse ang data stream at basahin lang ang dictionary na roomInfo.room, na kumakatawan sa kasalukuyang room:
- Tama nitong itinutugma ang alias na
pigffsa aktuwal na room ID na24422. - Sinusuri nito ang estado ng room bago humingi ng signature. Kapag
show_status == 2, agad itong nagsasabi na hindi live ang room, gaya ng nangyari sa room na593229. Kapagroom_biz_all.videoLoop == 1, sinasabi nitong paulit-ulit na nagpe-play ng mga video ang room, para hindi na gumawa ng hindi kailangang signature. - Kung walang Next.js data block at lumang istruktura pa rin ang gamit ng pahina, awtomatikong ipinapasa ang pagproseso sa orihinal na
super()._real_extract(url).
Ikalawang problema: ang direktang tawag sa PhantomJSwrapper
Pagkatapos makuha ang tamang room_id, kailangang tawagin ng extractor ang signing function na ub98484234 mula sa Douyu para kalkulahin ang apat na parameter na v, did, tt, at sign. Kahit naka-install na ang Deno na inirerekomenda ng yt-dlp, direktang gumagawa pa rin ng PhantomJSwrapper ang _calc_sign sa orihinal na douyutv.py. Dahil dito, lumalabas ang PhantomJS not found para sa live stream at VOD.
Pinapalitan ng plugin ang _calc_sign habang pinananatili ang orihinal na paraan ng pagkuha sa signing script at sa kasama nitong CryptoJS MD5. Para sa live stream, room_id ang ipinapasa; para sa VOD, ang video ID na binasa mula sa field na ROOM.point_id sa window.$DATA. Pagkatapos ligtas na i-encode ang mga argument gamit ang json.dumps, ipinapasa ang script sa standard input ng isang Deno subprocess na walang ibinigay na mga pahintulot sa pag-access:
- Tahasang ipinapasa ang
--no-prompt,--no-config, at--no-remote, nang walang pahintulot sa mga file, environment variable, o network. Nililimitahan ng permission model ng Deno ang pag-access sa mga JavaScript API; hindi nito pinapalitan ang sandbox sa antas ng operating system. - May 15 segundong timeout. Tinitiyak munang may hindi bakanteng
signfield ang ibinalik na query string bago ito isumite sa/lapi/live/getH5Play/{room_id}o sa VOD API. Sa test, parehong nagbalik ngerror: 0ang mga API.
Ikatlong problema: inaalis ng urljoin ang directory na /live/, kaya pumapalya ang stream
Matapos ayusin ang room ID at signature, nakapagpapakita na ang -F ng mga format na may iba't ibang bitrate. Pero pumalya pa rin ang unang recording gamit ang FFmpeg, na nagtapos sa exit code 8 (ffmpeg exited with code 8).
Sa pagsusuri ng JSON na ibinalik ng getH5Play, nakita naming hinahati ng API ang stream address sa dalawang bahagi: ang directory prefix na rtmp_url at ang filename na rtmp_live. Nagtatapos ang rtmp_url sa /live na walang slash sa dulo. Kapag direktang pinagdudugtong ng orihinal na code ang dalawa gamit ang urllib.parse.urljoin, itinuturing ang live na huling filename at pinapalitan ito. Nakatala rin sa issue #11998 ang 403 mula sa CDN kapag nawawala ang /live/:
urljoin('https://cdn.example/live', '24422.flv?sign=example')
# Incorrect result: https://cdn.example/24422.flv?sign=example
urljoin('https://cdn.example/live/', '24422.flv?sign=example')
# Correct result: https://cdn.example/live/24422.flv?sign=example
Sa _extract_stream_formats, tinitiyak muna ng plugin na may slash sa dulo ng rtmp_url (data['rtmp_url'].rstrip('/') + '/') bago ipasa ang datos sa orihinal na parser. Hindi binabago ang filename ng stream o ang mga query parameter ng signature. Pagkatapos ng pag-aayos, nakumpleto ang live recording at ang pag-download ng mga VOD segment, at pareho silang pumasa sa decoding check.
Kumpletong code ng plugin
I-save ang sumusunod na code bilang plugins/douyu/yt_dlp_plugins/extractor/douyu_signing.py:
"""Run Douyu signing scripts in Deno without file, environment, or network access."""
import json
import shutil
import subprocess
import time
import urllib.parse
import uuid
from yt_dlp.extractor.douyutv import DouyuShowIE as UpstreamDouyuShowIE, DouyuTVIE as UpstreamDouyuTVIE
from yt_dlp.utils import (
ExtractorError,
UserNotLive,
int_or_none,
str_or_none,
traverse_obj,
unescapeHTML,
url_or_none,
urlencode_postdata,
)
class _DenoSigning:
def _calc_sign(self, sign_func, video_id, room_id):
deno = shutil.which('deno')
if not deno:
raise ExtractorError('Deno is required for Douyu signing', expected=True)
script = ('globalThis.window=globalThis;' + self._get_cryptojs_md5(video_id) + ';' + sign_func
+ ';console.log(ub98484234(' + ','.join(json.dumps(value) for value in
(str(room_id), uuid.uuid4().hex, str(round(time.time())))) + '))')
try:
result = subprocess.run([deno, 'run', '--no-prompt', '--no-config', '--no-remote', '-'],
input=script, text=True, capture_output=True, timeout=15)
except (OSError, subprocess.TimeoutExpired) as exc:
raise ExtractorError('Douyu signing runtime failed', cause=exc) from exc
lines = result.stdout.strip().splitlines()
fields = urllib.parse.parse_qs(lines[-1]) if lines and result.returncode == 0 else {}
if not fields.get('sign'):
raise ExtractorError('Unable to sign Douyu request with Deno')
return {key: values[0] for key, values in fields.items()}
class DouyuShowIE(UpstreamDouyuShowIE, plugin_name='DenoSigning'):
_calc_sign = _DenoSigning._calc_sign
class DouyuTVIE(UpstreamDouyuTVIE, plugin_name='DenoSigning'):
_calc_sign = _DenoSigning._calc_sign
def _extract_stream_formats(self, stream_formats):
normalized = []
for response in stream_formats:
data = response.get('data')
if isinstance(data, dict) and isinstance(data.get('rtmp_url'), str):
# Treat /live as a directory so urljoin preserves it.
response = {**response, 'data': {
**data, 'rtmp_url': data['rtmp_url'].rstrip('/') + '/',
}}
normalized.append(response)
return super()._extract_stream_formats(normalized)
def _real_extract(self, url):
video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id)
room = traverse_obj(
self._search_nextjs_v13_data(webpage, video_id, fatal=False),
(..., 'roomInfo', 'room', {dict}), get_all=False)
if not room:
# Let upstream handle older pages without guessing IDs from recommendations or URLs.
return super()._real_extract(url)
room_id = str_or_none(room.get('room_id'))
if not room_id or not room_id.isascii() or not room_id.isdecimal() or int(room_id) <= 0:
raise ExtractorError('Unable to extract room id')
if traverse_obj(room, ('room_biz_all', 'videoLoop', {int_or_none})) == 1:
raise UserNotLive('The channel is auto-playing VODs', video_id=video_id)
if int_or_none(room.get('show_status')) == 2:
raise UserNotLive(video_id=video_id)
# New pages omit the signing function; reuse upstream script fetching and format parsing.
sign_func = self._get_sign_func(room_id, video_id)
form_data = {'rate': 0, **self._calc_sign(sign_func, video_id, room_id)}
stream_formats = [self._download_json(
f'https://www.douyu.com/lapi/live/getH5Play/{room_id}', video_id,
note='Downloading livestream format', data=urlencode_postdata(form_data))]
for rate_id in traverse_obj(stream_formats[0], ('data', 'multirates', ..., 'rate')):
if rate_id != traverse_obj(stream_formats[0], ('data', 'rate')):
form_data['rate'] = rate_id
stream_formats.append(self._download_json(
f'https://www.douyu.com/lapi/live/getH5Play/{room_id}', video_id,
note=f'Downloading livestream format {rate_id}', data=urlencode_postdata(form_data)))
return {
'id': room_id,
'display_id': video_id,
'formats': self._extract_stream_formats(stream_formats),
'is_live': True,
**traverse_obj(room, {
'title': ('room_name', {str}, {unescapeHTML}),
'description': ('show_details', {str}),
'uploader': ('nickname', {str}),
'thumbnail': ('coverSrc', {url_or_none}),
}),
}