Baiki yt-dlp Douyu: PhantomJS not found dan ralat ID bilik
Tarikh penerbitan
Muat turun pemalam pembaikan douyu_signing.py
Jika yt-dlp memaparkan PhantomJS not found semasa memuat turun video Douyu, atau Unable to extract room id semasa merakam siaran langsung, muatkan pemalam keserasian dalam artikel ini. Pemalam membaiki pengekstrakan ID bilik pada halaman baharu, menjalankan pengiraan tandatangan dengan Deno dan membetulkan cantuman URL aliran langsung.
Jika anda tidak mahu menyediakan Deno dan direktori pemalam secara setempat, tampalkan pautan Douyu ke pemuat turun di halaman utama untuk menggunakan alat dalam talian.
Semasa menyiasat kegagalan siaran langsung dan video atas permintaan Douyu, kami menemui tiga masalah berturut-turut. Halaman baharu menggunakan blok data Next.js, menyebabkan ungkapan nalar ID bilik lama gagal. Selepas ID bilik berjaya diperoleh, kod tandatangan asal masih memanggil PhantomJS yang telah ditamatkan penggunaannya. Setelah tandatangan berfungsi dan senarai kualiti dipaparkan, URL aliran FLV yang dibina pula kehilangan direktori /live/. Ikuti langkah pembaikan dahulu, kemudian lihat punca setiap masalah.
Gejala dan peringkat kegagalan
| Ralat atau gejala | Peringkat kegagalan |
|---|---|
Unable to extract room id |
Halaman siaran langsung baharu menggunakan aliran data Next.js, jadi padanan $ROOM.room_id lama gagal |
PhantomJS not found |
douyutv.py asal memanggil PhantomJSwrapper secara terus; memasang Deno sahaja tidak menggantikan panggilan itu |
Senarai format dipaparkan (-F berfungsi), tetapi muat turun terhenti dengan ralat |
rtmp_url daripada API tidak mempunyai garis miring di hujungnya, menyebabkan urljoin membuang direktori /live/ |
The channel is not currently live |
Bilik tidak bersiaran; pengekstrakan dihentikan mengikut status bilik |
The channel is auto-playing VODs |
Bilik sedang mengulang video atas permintaan (videoLoop: 1); pemalam menghentikan pengekstrakan |
Langkah pembaikan pantas
1. Sediakan persekitaran dan fail pemalam
Pastikan yt-dlp, deno dan ffmpeg tersedia dalam terminal. Persekitaran ujian kami menggunakan yt-dlp nightly 2026.10.29.232701, Deno 2.9.8 dan FFmpeg 7.1.5. Pemalam bergantung pada kaedah terbina dalam yt-dlp, _search_nextjs_v13_data. Kemas kini yt-dlp dahulu jika versi anda terlalu lama.
Cipta struktur folder berikut dalam direktori kerja dan simpan kod pemalam lengkap di hujung artikel sebagai douyu_signing.py:
plugins/
└── douyu/
└── yt_dlp_plugins/
└── extractor/
└── douyu_signing.py
Jalankan arahan seterusnya dari direktori yang mengandungi folder plugins. Pilihan --plugin-dirs ./plugins memuatkan pemalam tanpa mengubah kod sumber dalam direktori pemasangan yt-dlp.
2. Semak senarai format siaran langsung
Contoh ini menggunakan bilik siaran langsung awam. Gantikan URL dengan bilik yang mahu anda uji:
yt-dlp --ignore-config --plugin-dirs ./plugins --no-cache-dir -v -F "https://www.douyu.com/pigff"
Label [DouyuTV+DenoSigning] dalam log mengesahkan bahawa pemalam sedang digunakan. Jika bilik sedang bersiaran, yt-dlp akan meminta API getH5Play dan menyenaraikan format bagi setiap kualiti. Jika The channel is not currently live dipaparkan, cuba bilik yang sedang bersiaran.
3. Rakam 30 saat untuk menguji aliran media
Senarai format hanya menunjukkan bahawa permintaan bertandatangan mengembalikan error: 0. Aliran media perlu dibaca untuk memastikan URL CDN dicantum dengan betul:
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"
Pilihan -f worst memilih kadar bit terendah untuk mengecilkan fail ujian. -t 30 mengehadkan rakaman kepada 30 saat, manakala tamat masa baca/tulis rangkaian ditetapkan kepada 15 saat.
4. Uji video atas permintaan (VOD) dan semak fail (pilihan)
Pemalam yang sama turut meliputi pengekstrak VOD Douyu, DouyuShowIE. Semasa menguji video atas permintaan, log akan memaparkan [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 sample.
yt-dlp --ignore-config --plugin-dirs ./plugins --no-cache-dir -f worst -o "douyu-video.%(ext)s" "https://v.douyu.com/show/mPyq7oVNe5Yv1gLY"
Untuk memeriksa sama ada rakaman .flv atau .mp4 mempunyai bingkai rosak atau trek audio dan video yang tidak lengkap, gunakan ffprobe dan semakan penyahkodan penuh:
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 -
Penjelasan teknikal bagi tiga kegagalan
1. Data Next.js menggantikan penetapan ID bilik
Pengekstrak asal membaca ID bilik daripada pernyataan penetapan $ROOM.room_id dalam halaman. Kegagalan berkaitan dilaporkan dalam isu #16230. Pada halaman siaran langsung Douyu baharu, termasuk alias pigff dan URL berangka 593229, metadata bilik telah dipindahkan ke blok data skrip Next.js App Router (__next_f.push).
Ungkapan nalar yang mencari room_id di seluruh halaman boleh tersalah mengambil ID bilik cadangan daripada bar sisi atau bahagian bawah halaman. Pemalam menggunakan _search_nextjs_v13_data untuk menghuraikan aliran data dan membaca hanya kamus roomInfo.room yang mewakili bilik semasa:
- Alias
pigffdikenal pasti sebagai ID bilik sebenar,24422. - Status bilik diperiksa sebelum meminta tandatangan. Jika
show_status == 2, pemalam memaparkan mesej bahawa bilik tidak bersiaran, seperti bilik593229dalam ujian kami. Jikaroom_biz_all.videoLoop == 1, ia memaklumkan bahawa bilik sedang mengulang video, sekali gus mengelakkan pengiraan tandatangan yang tidak diperlukan. - Jika halaman tidak mengandungi blok data Next.js, pemalam menyerahkan pemprosesan struktur halaman lama kepada
super()._real_extract(url)asal.
2. Kod asal memanggil PhantomJSwrapper secara terus
Selepas memperoleh room_id sebenar, pengekstrak perlu memanggil fungsi tandatangan ub98484234 daripada Douyu untuk mengira v, did, tt dan sign. Walaupun Deno yang disyorkan oleh yt-dlp telah dipasang, _calc_sign dalam douyutv.py asal masih mencipta PhantomJSwrapper secara terus. Akibatnya, ralat PhantomJS not found berlaku pada siaran langsung dan VOD.
Pemalam menggantikan _calc_sign sambil mengekalkan logik asal untuk mendapatkan skrip tandatangan dan kod CryptoJS MD5 terbina dalam. Bagi siaran langsung, ia menggunakan room_id; bagi VOD, ia menggunakan ID video yang dibaca daripada medan ROOM.point_id dalam window.$DATA. Python mengekodkan argumen dengan selamat menggunakan json.dumps, kemudian menghantar skrip melalui input standard kepada subproses Deno tanpa kebenaran akses:
- Arahan menetapkan
--no-prompt,--no-configdan--no-remotesecara jelas, tanpa memberi akses kepada fail, pemboleh ubah persekitaran atau rangkaian. Model kebenaran Deno mengehadkan akses API JavaScript; ia tidak menggantikan kotak pasir pada peringkat sistem pengendalian. - Subproses mempunyai tamat masa 15 saat. Pemalam memastikan rentetan pertanyaan yang diterima mengandungi medan
signyang tidak kosong sebelum menghantarnya ke/lapi/live/getH5Play/{room_id}atau API VOD. Kedua-dua API mengembalikanerror: 0dalam ujian kami.
3. urljoin membuang direktori /live/
Selepas pengekstrakan ID bilik dan tandatangan dibaiki, -F sudah dapat menyenaraikan format pada beberapa kadar bit. Namun, cubaan rakaman FFmpeg pertama masih gagal dengan ffmpeg exited with code 8.
Respons JSON getH5Play memisahkan URL aliran kepada awalan direktori rtmp_url dan nama fail rtmp_live. Dalam ujian kami, rtmp_url berakhir dengan /live tanpa garis miring di hujungnya. Apabila kod asal mencantumkannya menggunakan urllib.parse.urljoin, live dianggap sebagai nama fail terakhir lalu digantikan. Isu #11998 juga melaporkan ralat CDN 403 akibat laluan /live/ yang hilang:
urljoin('https://cdn.example/live', '24422.flv?sign=example')
# Incorrect: https://cdn.example/24422.flv?sign=example
urljoin('https://cdn.example/live/', '24422.flv?sign=example')
# Correct: https://cdn.example/live/24422.flv?sign=example
Dalam _extract_stream_formats, pemalam menambah garis miring di hujung rtmp_url dengan data['rtmp_url'].rstrip('/') + '/' sebelum menyerahkan data kepada penghurai asal. Nama fail aliran dan parameter pertanyaan tandatangan dikekalkan. Selepas pembaikan, rakaman siaran langsung dan muat turun segmen VOD selesai, dan fail yang terhasil lulus semakan penyahkodan.
Kod pemalam lengkap
Simpan kod berikut sebagai 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}),
}),
}