ytdlp.online
Trang chủ Tampermonkey

Tất cả hướng dẫn sửa lỗi

Mục lục

Sửa lỗi tải Douyu bằng yt-dlp: PhantomJS not found và lỗi ID phòng

Ngày đăng

Tải plugin sửa lỗi douyu_signing.py

Nếu yt-dlp báo PhantomJS not found khi tải video Douyu hoặc Unable to extract room id khi ghi lại buổi phát trực tiếp, bạn có thể nạp plugin tương thích trong bài này. Plugin xử lý cả ba vấn đề: lấy ID phòng từ trang phát trực tiếp mới, thực thi mã ký bằng Deno và ghép URL luồng trực tiếp.

Nếu không muốn cấu hình Deno và thư mục plugin trên máy, bạn có thể dán liên kết Douyu vào trình tải ở trang chủ để sử dụng công cụ trực tuyến.

Khi điều tra lỗi phát trực tiếp và video theo yêu cầu của Douyu, chúng tôi gặp ba vấn đề liên tiếp. Trang mới chuyển sang các khối dữ liệu Next.js khiến biểu thức chính quy lấy ID phòng không còn khớp. Sau khi sửa được ID phòng, mã nguồn upstream vẫn gọi cố định PhantomJS đã lỗi thời để tạo chữ ký. Khi ký thành công và đã liệt kê được các mức chất lượng, URL luồng FLV được ghép lại thiếu thư mục /live/. Phần dưới bắt đầu bằng các bước xử lý, sau đó giải thích từng nguyên nhân.

Triệu chứng và vị trí xảy ra lỗi

Lỗi hoặc triệu chứng Công đoạn gặp vấn đề
Unable to extract room id Trang phòng mới dùng luồng dữ liệu Next.js, nên mẫu $ROOM.room_id cũ không còn khớp
PhantomJS not found douyutv.py upstream gọi cố định PhantomJSwrapper; chỉ cài Deno vào hệ thống sẽ không tự thay thế lời gọi này
Liệt kê được định dạng (-F hoạt động) nhưng tải xuống kết thúc với lỗi rtmp_url do API trả về thiếu dấu gạch chéo cuối, khiến urljoin làm mất thư mục /live/ khi ghép URL
The channel is not currently live Phòng hiện không phát trực tiếp; việc dừng xử lý theo trạng thái phòng là bình thường
The channel is auto-playing VODs Phòng đang phát lặp video theo yêu cầu (videoLoop: 1); plugin dừng trích xuất dữ liệu

Các bước sửa lỗi nhanh

1. Chuẩn bị môi trường và tệp plugin

Đảm bảo terminal có thể chạy yt-dlp, deno và ffmpeg. Môi trường dùng để kiểm tra trong bài là yt-dlp nightly 2026.10.29.232701, Deno 2.9.8 và FFmpeg 7.1.5. Plugin phụ thuộc vào phương thức _search_nextjs_v13_data có sẵn trong yt-dlp; nếu bản đang dùng quá cũ, hãy cập nhật yt-dlp trước.

Tạo các thư mục theo cấu trúc sau trong thư mục làm việc, rồi lưu toàn bộ mã plugin ở cuối bài thành douyu_signing.py:

plugins/
└── douyu/
    └── yt_dlp_plugins/
        └── extractor/
            └── douyu_signing.py

Chạy tất cả các lệnh tiếp theo trong thư mục chứa plugins. Tùy chọn --plugin-dirs ./plugins nạp plugin mà không cần sửa mã nguồn trong thư mục cài đặt yt-dlp.

2. Kiểm tra danh sách định dạng phát trực tiếp

Ví dụ sau dùng một phòng công khai. Khi sử dụng, hãy thay bằng URL phòng cần kiểm tra:

yt-dlp --ignore-config --plugin-dirs ./plugins --no-cache-dir -v -F "https://www.douyu.com/pigff"

Nếu nhật ký có [DouyuTV+DenoSigning], plugin đã có hiệu lực. Nếu phòng đang phát trực tiếp, terminal sẽ yêu cầu API getH5Play rồi hiển thị các định dạng theo chất lượng. Nếu thấy The channel is not currently live, phòng hiện không phát trực tiếp; hãy thử một phòng đang phát.

3. Ghi thử 30 giây để kiểm tra luồng media

Liệt kê được định dạng chỉ cho biết yêu cầu API có chữ ký đã trả về error: 0. Bạn vẫn cần nhận luồng thực tế để xác nhận URL CDN được ghép chính xác:

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"

Lệnh trên dùng -f worst để chọn bitrate thấp nhất, giúp giảm kích thước tệp thử nghiệm. -t 30 giới hạn thời gian ghi ở 30 giây, còn thời gian chờ đọc/ghi mạng được đặt là 15 giây.

4. Tải video theo yêu cầu (VOD) và kiểm tra tệp (tùy chọn)

Cùng plugin này cũng áp dụng cho trình trích xuất VOD của Douyu, DouyuShowIE. Khi thử với VOD, nhật ký sẽ hiển thị [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"

Để kiểm tra thêm xem tệp .flv hoặc .mp4 đã ghi có khung hình hỏng hay thiếu luồng hình ảnh, âm thanh hay không, hãy chạy ffprobe và giải mã toàn bộ tệp:

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 -

Phân tích kỹ thuật của ba lỗi

1. Trang chuyển sang dữ liệu Next.js, biểu thức chính quy cũ không lấy được ID phòng

Trình trích xuất upstream trước đây lấy ID phòng từ câu lệnh gán $ROOM.room_id trong trang. Lỗi liên quan được báo cáo tại issue #16230. Nhưng trên các trang phòng Douyu mới, chẳng hạn bí danh tùy chỉnh pigff hoặc số phòng thông thường 593229, siêu dữ liệu của phòng đã chuyển vào khối dữ liệu script của Next.js App Router (__next_f.push).

Nếu dùng một biểu thức chính quy quá rộng để tìm room_id trên toàn trang, bạn dễ lấy nhầm ID của phòng được đề xuất ở thanh bên hoặc cuối trang. Vì vậy, plugin dùng _search_nextjs_v13_data để phân tích luồng dữ liệu và chỉ đọc dictionary roomInfo.room đại diện cho phòng hiện tại:

  • Phân giải chính xác bí danh pigff thành ID phòng thực 24422.
  • Kiểm tra trạng thái phòng trước khi yêu cầu ký. Nếu show_status == 2, trả ngay thông báo phòng chưa phát trực tiếp, như phòng 593229 ở thời điểm thử. Nếu room_biz_all.videoLoop == 1, thông báo phòng đang phát lặp video để tránh tính chữ ký không cần thiết.
  • Nếu trang không có khối dữ liệu Next.js, tức vẫn dùng cấu trúc cũ, tự động chuyển lại cho super()._real_extract(url) của upstream xử lý.

2. Upstream gọi cố định PhantomJSwrapper

Sau khi có room_id thực, trình trích xuất phải gọi hàm ký ub98484234 do Douyu cung cấp để tính bốn tham số v, did, tt và sign. Ngay cả khi đã cài Deno theo khuyến nghị của yt-dlp, _calc_sign trong douyutv.py upstream vẫn khởi tạo trực tiếp PhantomJSwrapper, gây lỗi PhantomJS not found. Cả phát trực tiếp và VOD đều bị ảnh hưởng.

Plugin ghi đè _calc_sign nhưng giữ nguyên logic lấy script ký và CryptoJS MD5 tích hợp từ upstream. Với phát trực tiếp, nó truyền room_id; với VOD, nó truyền ID video đọc từ trường ROOM.point_id trong window.$DATA. Các đối số được mã hóa an toàn bằng json.dumps, rồi chuyển qua đầu vào chuẩn cho một tiến trình con Deno không được cấp quyền truy cập để thực thi:

  • Truyền rõ --no-prompt, --no-config và --no-remote, không cấp quyền truy cập tệp, biến môi trường hay mạng. Mô hình quyền của Deno giới hạn truy cập API JavaScript; nó không thay thế sandbox ở cấp hệ điều hành.
  • Đặt thời gian chờ là 15 giây. Sau khi kiểm tra chuỗi truy vấn trả về có trường sign không rỗng, plugin gửi dữ liệu tới /lapi/live/getH5Play/{room_id} hoặc API VOD. Trong thử nghiệm thực tế, cả hai API đều trả về error: 0.

3. urljoin làm mất thư mục /live/, khiến việc nhận luồng thất bại

Sau khi sửa ID phòng và chữ ký, -F đã liệt kê được nhiều định dạng bitrate. Tuy nhiên, lần ghi đầu tiên bằng FFmpeg vẫn thất bại với mã thoát 8 (ffmpeg exited with code 8).

Kiểm tra JSON trả về từ getH5Play cho thấy API chia địa chỉ luồng thành tiền tố thư mục rtmp_url và tên tệp rtmp_live. rtmp_url kết thúc bằng /live, không có dấu gạch chéo cuối. Khi upstream ghép trực tiếp bằng urllib.parse.urljoin, live bị coi là tên tệp cuối đường dẫn và bị thay thế. Issue #11998 của upstream cũng ghi nhận trường hợp CDN trả về 403 do thiếu /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

Trong _extract_stream_formats, plugin chuẩn hóa dấu gạch chéo cuối của rtmp_url bằng data['rtmp_url'].rstrip('/') + '/' trước khi chuyển cho upstream phân tích. Tên tệp luồng và các tham số chữ ký trong truy vấn vẫn giữ nguyên. Sau khi sửa, việc ghi luồng trực tiếp và tải các phân đoạn VOD đều hoàn tất, đồng thời vượt qua kiểm tra giải mã.


Mã plugin đầy đủ

Lưu mã dưới đây thành 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}),
            }),
        }
Công cụ yt-dlp
Xem tất cả các công cụ yt-dlp
Trợ giúp & Tài liệu yt-dlp