Bản lưu ngày / Snapshot of 2026-10-17 · Xem bản mới nhất / See the latest version
WanduraDocsBeta
BetaDocs đang ở bản beta và còn thay đổi. Thấy chỗ sai, khó hiểu hoặc còn thiếu? Hãy cho chúng tôi biết.
Bạn đang đọc một bản tài liệu cũ: v1 · Sang bản mới nhất (v2)
Lịch sử thay đổi

Lịch sử: Phát mượt khi tải cao

Mọi lần sửa trang này, mới nhất ở trên. Mở từng mục để xem đã đổi gì.

← Phát mượt khi tải cao

  1. 02/10/2026 · +68 −136 dòng
    Xem thay đổi
      # Phát mượt khi tải cao
      
    - > **TL;DR** — Khi lưu lượng lên đỉnh, người xem chỉ nên thấy vòng xoay chờ một chút, không bao giờ thấy lỗi. Xin vé **sớm** (tập đang xem cộng một hai tập kế trong một request theo lô), **dùng lại** vé khi vé còn hạn, gửi **`viewer_ref` ổn định**, và retry `429` và `503` bằng **exponential backoff, full jitter và `Retry-After`**, trong một **ngân sách retry** nhỏ. Vé prefetch bị từ chối không bao giờ là lỗi, và trục trặc ở tập kế không bao giờ làm dừng tập đang phát.
    - 
    - Mọi client trên trang này là code của bạn: **backend** của bạn nói chuyện với Wandura bằng API key, còn **app** của bạn chỉ nói chuyện với backend của bạn. Lời khuyên áp cho cả hai phía.
    + > **TL;DR** — Khi lưu lượng lên đỉnh, người xem chỉ nên thấy vòng xoay chờ một chút, không bao giờ thấy lỗi. Xin vé **sớm** (tập đang xem cộng một hai tập kế, trong một request), gửi **`viewer_ref` ổn định** để request lặp dùng lại vé, retry `429` và `503` bằng **`Retry-After`, full jitter và ngân sách retry**, và giữ tập hiện tại tiếp tục phát dù tập kế có chuyện gì.
      
      ## Wandura làm gì khi bận {#what-wandura-does}
      
    - Tổ chức của bạn có [rate limit](../concepts/rate-limits.md) cho request xin vé, và server của Wandura có lúc chậm đi một chút. Wandura được thiết kế để xuống cấp nhẹ nhàng:
    - 
    - | Khi | Wandura làm gì |
    + | Khi | Wandura |
      |---|---|
    - | Bạn chạm rate limit playback | Trả `429` `rate_limited` kèm header `Retry-After`. Hạn mức được nạp lại mỗi giây chứ không phải mỗi phút, nên `Retry-After` thường là **1–2 giây**. |
    - | Không cấp kịp một vé | Trả `503` `service_unavailable` kèm `Retry-After` (thường 1–2 giây). Trong một lô, chỉ phần tử bị ảnh hưởng lỗi, kèm `retry_after` riêng. |
    - | Lưu lượng sát rate limit | Từ chối vé **prefetch** trước (`429` kèm `"reason": "prefetch_shed"`), để vé cho tập người xem vừa chạm vẫn được cấp. |
    - | Cùng người xem xin lại cùng tập | Trả lại vé đã cấp khi vé đó còn ít nhất 5 phút, và không đếm lần nữa. Cần `viewer_ref` ổn định. |
    - | Request play đến ngay khi vừa vượt hạn mức | Giữ request tối đa khoảng **0,5 giây** chờ có chỗ, rồi mới trả `429`. |
    - | Database của Wandura chậm | Phục vụ catalog và kiểm giấy phép từ cache có thể cũ tới 5 phút, thay vì báo lỗi. |
    + | Bạn chạm rate limit playback | Trả `429` kèm `Retry-After`, thường 1–2 giây: hạn mức được nạp lại mỗi giây. |
    + | Còn dưới khoảng 20 % hạn mức | Từ chối vé **prefetch** trước (`"reason": "prefetch_shed"`); vé `play` vẫn được cấp bình thường. |
    + | Request `play` đến ngay khi vừa vượt hạn mức | Giữ request tối đa khoảng 0,5 giây rồi mới trả `429`. |
    + | Cùng người xem xin lại cùng tập, trên cùng thiết bị | Trả lại vé đã cấp (còn ≥ 5 phút), không đếm lần nữa. Cần `viewer_ref`. |
    + | Không cấp kịp một vé | `503` `service_unavailable` kèm `Retry-After`; trong một lô, chỉ phần tử đó lỗi. |
    + | Database của Wandura chậm | Phục vụ catalog và kiểm giấy phép từ câu trả lời tốt gần nhất, cũ tối đa 5 phút. |
      
    - Hai hệ quả cho code của bạn: luôn đọc `Retry-After` thay vì tự giả định một con số, và cho request xin vé timeout phía client ít nhất **5 giây**, để một lần giữ ngắn không bị cắt ngang thành lỗi.
    + Hai hệ quả cho code của bạn: luôn đọc `Retry-After` thay vì tự giả định một con số, và cho request xin vé timeout phía client ít nhất 5 giây.
      
      ## Xin vé sớm {#request-early}
      
    - Lúc tốt nhất để xin vé là trước khi người xem cần đến nó.
    - 
    - 1. **Xin theo lô các tập kế.** Khi người xem bắt đầu một tập, gửi một request cho tập đó với `"intent": "play"` kèm `"next": 2`. Bạn nhận tới ba vé trong một lượt gọi; các tập sau được ghi là prefetch, không phải lượt phát. Áp paywall của bạn trước: chỉ xin tập người xem được phép xem, và chỉ dùng `next` khi các tập kế cũng đã mở khoá (nếu không thì tự liệt kê bằng `episodes`).
    - 2. **Dùng lại vé khi còn hạn.** Vé có hạn 30 phút (`expires_at`). Giữ vé ở backend theo người xem + thiết bị + tập (và `intent`), và đưa lại khi người xem mở lại tập đó trên cùng thiết bị. Vé gắn với giá trị thiết bị lúc cấp (`union_info` và `drm_kek`), nên không bao giờ đưa vé đó cho thiết bị khác. Chỉ xin vé mới khi còn dưới khoảng 5 phút.
    - 3. **Gửi `viewer_ref` ổn định.** Dùng cùng một giá trị mờ cho cùng người xem trên cùng thiết bị, ví dụ `<your-user-id>:<device-id>`, không chứa dữ liệu cá nhân. Nhờ đó Wandura nhận ra request lặp (xem bảng trên) và bộ phận hỗ trợ tìm được vé của một người xem.
    + 1. **Xin theo lô các tập kế.** Khi người xem bắt đầu tập N, gửi một request: N với `"intent": "play"`, cộng N+1 và N+2 dưới dạng prefetch nếu người xem được phép xem chúng. Một lượt gọi, tối đa ba vé.
    + 2. **Dùng lại vé cho đúng.** Vé gắn với một thiết bị. Chỉ dùng lại cho cùng người xem, cùng thiết bị, cùng tập và cùng intent, và chỉ tới 5 phút trước `expires_at`. Wandura làm việc này cho bạn khi bạn gửi `viewer_ref` ổn định; cache ở backend theo cùng khoá cũng được. Không bao giờ dùng chung vé giữa các thiết bị.
    + 3. **Gửi `viewer_ref` ổn định**: HMAC của user ID cộng hash của install ID. Không bao giờ theo phiên, không bao giờ chứa dữ liệu cá nhân.
      
      ```json
      {
    -   "film_ref": "the-heiress-returns",
    -   "episode_no": 4,
    -   "next": 2,
    -   "intent": "play",
    +   "episodes": [
    +     { "film_ref": "the-heiress-returns", "episode_no": 4, "intent": "play" },
    +     { "film_ref": "the-heiress-returns", "episode_no": 5, "intent": "prefetch" },
    +     { "film_ref": "the-heiress-returns", "episode_no": 6, "intent": "prefetch" }
    +   ],
        "union_info": "<device ID from the Player SDK>",
        "drm_kek": "<device public key from the Player SDK>",
    -   "viewer_ref": "u_48213:dev_9a7c"
    +   "viewer_ref": "k3Jd9sQm2vXa7LpT0bWc1e:Zr8uYq2nM4a"
      }
      ```
      
    - Một lô tính mỗi tập một vé vào rate limit, và từng vé được xét riêng. Khi gần chạm ngưỡng, phần tử prefetch bị từ chối trước, mỗi phần tử có `429` và `retry_after` riêng, trong khi các phần tử còn lại vẫn nhận vé. Cả request chỉ trả `429` khi không phần tử nào cấp được. Mỗi phần tử trong response có `status` riêng: một phần tử có thể `200` trong khi phần tử khác `404` `not_ready` (quá tập cuối đã công bố), `429`, hoặc `503` kèm `retry_after`. Xem [Tài liệu tham chiếu Playback](../reference/playback.md).
    + Mỗi phần tử trong response có `status` riêng: một phần tử có thể `200` trong khi phần tử khác `404` `not_ready` (quá tập cuối), `429` (một prefetch bị tạm dừng) hoặc `503` kèm `retry_after`. Chỉ retry những phần tử hỏng mà bạn cần.
      
      ## Retry `429` và `503` cho đúng {#retry}
      
    - Hàng nghìn app cùng retry một lúc sẽ biến một nhịp chậm một giây thành sự cố. Hãy rải các lần retry ra:
    - 
    - - **Chỉ retry `429`, `503` và lỗi mạng.** Mọi `4xx` khác sẽ lại hỏng: sửa request thay vì retry. `5xx` lạ: xử như `503`.
    - - **Tôn trọng `Retry-After`.** Không bao giờ retry sớm hơn con số đó. Cộng thêm ngẫu nhiên 0–1 giây, để server của bạn không cùng quay lại trong một giây.
    - - **Không có `Retry-After` thì dùng exponential backoff với full jitter:** chờ một khoảng ngẫu nhiên từ 0 đến `min(cap, base × 2^attempt)`, ví dụ base 0,5 giây và cap 8 giây.
    - - **Giữ ngân sách retry.** Tối đa 3 lần retry cho một request, và số retry nên dưới khoảng 10 % số request trong phút vừa qua. Hết ngân sách thì báo lỗi nhanh thay vì dồn thêm tải.
    - - **Trong một lô, chỉ retry phần tử hỏng**, mỗi phần tử sau `retry_after` của chính nó.
    - 
    - ### TypeScript (backend của bạn) {#typescript}
    + Các luật (status nào, `Retry-After`, full jitter, mức trần, timeout, ngân sách, số request song song) nằm ở [chính sách retry chuẩn](../reference/behaviour.md#retries). Trong code:
      
      ```ts
    - type Ticket = { vid: string; play_auth_token: string; expires_at: string; protection: string };
    - // A ticket is bound to the device values it was issued for (union_info / drm_kek):
    - // key the cache by viewer + device + episode + intent, never share a ticket across devices.
    - const cache = new Map<string, Ticket>();
    - const cacheKey = (b: Record<string, unknown>) => JSON.stringify([
    -   b.viewer_ref, b.union_info, b.drm_kek, b.episode_id ?? `${b.film_ref}#${b.episode_no}`, b.intent ?? 'play',
    - ]);
    - let recent = { sent: 0, retries: 0, since: Date.now() };
    - 
    - const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
    - const budgetLeft = () => {
    -   if (Date.now() - recent.since > 60_000) recent = { sent: 0, retries: 0, since: Date.now() };
    -   return recent.retries < Math.max(10, recent.sent * 0.1);
    + /** How hard to try. INTERACTIVE = a viewer is waiting; BACKGROUND = a sync job. */
    + export type RetryPolicy = {
    +   maxRetries: number; // retries after the first attempt
    +   baseMs: number; // first backoff step
    +   capMs: number; // largest backoff step
    +   timeoutMs: number; // per attempt
    +   deadlineMs: number; // the whole call, retries included
      };
    - 
    - export async function getTicket(body: Record<string, unknown>): Promise<Ticket> {
    -   const key = cacheKey(body);
    -   const hit = cache.get(key);
    -   if (hit && Date.parse(hit.expires_at) - Date.now() > 5 * 60_000) return hit; // reuse
    - 
    -   for (let attempt = 0; ; attempt++) {
    -     recent.sent++;
    -     let res: Response | null = null;
    -     try {
    -       res = await fetch('https://www.wandura.ai/api/delivery/v1/playback', {
    -         method: 'POST',
    -         headers: { Authorization: `Bearer ${process.env.WANDURA_PLAYBACK_KEY}`, 'Content-Type': 'application/json' },
    -         body: JSON.stringify(body),
    -         signal: AbortSignal.timeout(5_000),
    -       });
    -     } catch { /* network error or timeout: retryable */ }
    - 
    -     if (res?.ok) {
    -       const ticket = (await res.json()) as Ticket; // ignore fields you do not know
    -       cache.set(key, ticket);
    -       return ticket;
    -     }
    -     const retryable = !res || res.status === 429 || res.status >= 500;
    -     if (!retryable || attempt >= 3 || !budgetLeft()) throw new Error(`ticket failed: ${res?.status ?? 'network'}`);
    + export const INTERACTIVE: RetryPolicy = { maxRetries: 3, baseMs: 500, capMs: 8_000, timeoutMs: 8_000, deadlineMs: 15_000 };
    + export const BACKGROUND: RetryPolicy = { maxRetries: 5, baseMs: 1_000, capMs: 30_000, timeoutMs: 15_000, deadlineMs: 120_000 };
      
    -     recent.retries++;
    -     const retryAfter = Number(res?.headers.get('Retry-After'));
    -     const wait = retryAfter > 0
    -       ? retryAfter * 1000 + Math.random() * 1000             // honour Retry-After, plus jitter
    -       : Math.random() * Math.min(8_000, 500 * 2 ** attempt); // full jitter
    -     await sleep(wait);
    -   }
    + /** Retry only what can succeed later: network errors, 429, 500, 502 and 503 (except 503 `not_ready`). */
    + export function isRetryable(status: number | null, errorCode?: string): boolean {
    +   if (status === null) return true;
    +   if (status === 503) return errorCode !== 'not_ready';
    +   return status === 429 || status === 500 || status === 502;
      }
    - ```
      
    - ## Giữ người xem tiếp tục xem {#in-the-app}
    - 
    - Thứ người xem nhìn thấy quan trọng hơn bất kỳ request đơn lẻ nào.
    - 
    - - **Hiện vòng xoay chờ, không hiện lỗi.** Trong lúc backend đang retry, giữ trạng thái đang tải. Chỉ hiện thông báo kèm nút **Thử lại** khi đã hết ngân sách retry, ví dụ sau khoảng 10 giây.
    - - **Tiếp tục phát tập hiện tại.** Request cho tập kế bị hỏng không bao giờ được làm dừng hay tải lại tập đang phát. Thử lại một lúc sau, hoặc khi người xem xem tới cuối tập.
    - - **Prefetch bị từ chối không phải lỗi.** Nếu vé prefetch bị từ chối (`429`, `503`, hoặc thiếu phần tử), không làm gì người xem thấy được. Khi người xem chạm vào tập đó, xin lúc đó với `"intent": "play"`.
    - - **Không bao giờ hiện mã HTTP thô** cho người xem.
    - 
    - ### Kotlin (app Android) {#kotlin}
    - 
    - ```kotlin
    - // Your backend does the Wandura call and the retries; the app only shows a spinner.
    - suspend fun prefetchNext(episode: Episode) {
    -     runCatching { backend.ticket(episode, intent = "prefetch") }
    -         .onSuccess { prefetched[episode.id] = it }
    -         // Refused or failed: not an error for the viewer. Ask again on tap.
    + /** Wait before retry number `attempt` (0-based): Retry-After (+ up to 1 s jitter) wins, else full jitter under the cap. */
    + export function backoffMs(attempt: number, retryAfterS: number | null, p: RetryPolicy, random = Math.random): number {
    +   if (retryAfterS !== null && retryAfterS > 0) return retryAfterS * 1_000 + random() * 1_000;
    +   return random() * Math.min(p.capMs, p.baseMs * 2 ** attempt);
      }
      
    - suspend fun play(episode: Episode) {
    -     ui.showSpinner()
    -     val ticket = prefetched.remove(episode.id)?.takeIf { it.validForMinutes() > 5 }
    -         ?: withTimeoutOrNull(10_000) { backend.ticket(episode, intent = "play") }
    -     if (ticket == null) ui.showRetry("Taking longer than usual") else player.start(ticket)
    + /** Retries may be at most 10 % of the requests of the last minute (never fewer than 10 a minute). */
    + export class RetryBudget {
    +   private sent = 0;
    +   private retries = 0;
    +   private since = Date.now();
    +   private roll() {
    +     if (Date.now() - this.since > 60_000) { this.sent = 0; this.retries = 0; this.since = Date.now(); }
    +   }
    +   noteRequest() { this.roll(); this.sent++; }
    +   tryRetry(): boolean {
    +     this.roll();
    +     if (this.retries >= Math.max(10, this.sent * 0.1)) return false;
    +     this.retries++;
    +     return true;
    +   }
      }
      ```
      
    - ### Swift (app iOS) {#swift}
    - 
    - ```swift
    - // Your backend does the Wandura call and the retries; the app only shows a spinner.
    - func prefetchNext(_ episode: Episode) async {
    -     // Refused or failed: not an error for the viewer. Ask again on tap.
    -     if let ticket = try? await backend.ticket(episode, intent: .prefetch) {
    -         prefetched[episode.id] = ticket
    -     }
    - }
    + Endpoint backend dùng nó, với request theo lô và cách xử lý từng phần tử: [Luồng playback và paywall](playback-flow.md#code).
      
    - func play(_ episode: Episode) async {
    -     ui.showSpinner()
    -     if let t = prefetched.removeValue(forKey: episode.id), t.minutesLeft > 5 {
    -         return player.start(t)
    -     }
    -     do { player.start(try await backend.ticket(episode, intent: .play, timeout: 10)) }
    -     catch { ui.showRetry("Taking longer than usual") }
    - }
    - ```
    + ## Giữ người xem tiếp tục xem {#in-the-app}
      
    - ## Đọc response một cách dễ tính {#tolerant-reader}
    + - **Vòng xoay chờ, không phải lỗi.** Trong lúc backend đang retry, giữ trạng thái đang tải. Chỉ hiện **Retry** khi đã hết ngân sách (khoảng 15 giây).
    + - **Tập hiện tại tiếp tục phát.** Request cho tập kế bị hỏng không bao giờ làm dừng hay tải lại tập đang trên màn hình.
    + - **Prefetch bị từ chối không phải lỗi.** Không làm gì người xem thấy được; xin với `"intent": "play"` khi người xem tới tập đó.
    + - **Không bao giờ hiện mã status thô.**
      
    - API tiếp tục thêm trường, giá trị và mã lỗi. Tích hợp của bạn vẫn chạy nếu nó:
    + Code phía app, bằng Swift và Kotlin, làm đúng như vậy: [iOS](../player-sdk/ios.md#code-controller), [Android](../player-sdk/android.md#code-controller).
      
    - - bỏ qua trường không nhận ra, ví dụ một trường mới cho biết vé được dùng lại;
    - - coi giá trị lạ là trường hợp chung;
    - - xử `error_code` lạ theo nhóm HTTP status: `429` chờ `Retry-After`, `4xx` khác không retry, `5xx` retry có backoff.
    + ## Đọc response một cách dễ tính {#tolerant-reader}
      
    - Luật đầy đủ ở [API tiến hoá](../concepts/api-evolution.md#tolerant-reader).
    + Bỏ qua trường lạ, coi giá trị lạ là trường hợp mặc định, và xử mã lỗi lạ theo nhóm status. Xem [Versioning và API tiến hoá](../concepts/versioning.md#tolerant-reader).
      
      ## Danh sách kiểm tra {#checklist}
      
    - - [ ] Bắt đầu một tập gửi một request theo lô: `"intent": "play"` kèm `next` (hoặc `episodes`) cho các tập kế đã mở khoá.
    - - [ ] Vé được dùng lại theo người xem + thiết bị + tập, không bao giờ dùng chéo thiết bị, tới khoảng 5 phút trước `expires_at`.
    - - [ ] `viewer_ref` ổn định theo người xem + thiết bị, không chứa dữ liệu cá nhân.
    - - [ ] `429` và `503` được retry sau `Retry-After` cộng jitter; không có header thì exponential backoff với full jitter.
    - - [ ] Tối đa 3 lần retry mỗi request, và ngân sách khoảng 10 % retry mỗi phút.
    + - [ ] Bắt đầu một tập gửi một request: `play` cho tập đó, `prefetch` cho các tập kế đã mở khoá.
    + - [ ] Vé chỉ được dùng lại theo người xem, thiết bị, tập và intent, không bao giờ dùng chéo thiết bị, tới 5 phút trước `expires_at`.
    + - [ ] `viewer_ref` ổn định theo người xem và thiết bị, và không chứa dữ liệu cá nhân.
    + - [ ] `429` và `503` được retry sau `Retry-After` cộng jitter; nếu không có thì exponential backoff với full jitter.
    + - [ ] Tối đa 3 lần retry cho mỗi request tương tác, và ngân sách retry 10 % mỗi phút.
      - [ ] Request xin vé có timeout phía client ít nhất 5 giây.
    - - [ ] Người xem thấy vòng xoay chờ khi đang retry, và chỉ thấy nút **Thử lại** sau khoảng 10 giây.
      - [ ] Prefetch bị từ chối không có tác động nhìn thấy được, và không bao giờ làm dừng tập hiện tại.
    - - [ ] Trường, giá trị và mã lỗi lạ không làm hỏng tích hợp.
      
    - ## Liên quan
    + ## Liên quan {#related}
      
      - [Rate limit](../concepts/rate-limits.md)
      - [Lỗi](../concepts/errors.md)
    - - [Playback](../concepts/playback.md)
    - - [Hướng dẫn tích hợp](../integration-guide.md#prefetch)
  2. 02/10/2026 · +22 −17 dòng
    Xem thay đổi
    ⋯
      ## Wandura làm gì khi bận {#what-wandura-does}
      
    - Tổ chức của bạn có [rate limit](../concepts/rate-limits.md) cho request xin vé, và server của Wandura có lúc chậm đi một chút. Wandura được thiết kế để xuống cấp nhẹ nhàng. Hành vi ghi **Đang triển khai** đang được đưa lên; tích hợp theo đúng trang này thì chạy được cả trước lẫn sau.
    + Tổ chức của bạn có [rate limit](../concepts/rate-limits.md) cho request xin vé, và server của Wandura có lúc chậm đi một chút. Wandura được thiết kế để xuống cấp nhẹ nhàng:
      
    - | Khi | Wandura làm gì | Trạng thái |
    - |---|---|---|
    - | Bạn chạm rate limit playback | Trả `429` `rate_limited` kèm header `Retry-After`. | Đang chạy |
    - | Không cấp kịp một vé | Trả `503` `service_unavailable` kèm `Retry-After` (thường 1–2 giây). Trong một lô, chỉ phần tử bị ảnh hưởng lỗi, kèm `retry_after` riêng. | Đang chạy |
    - | Lưu lượng sát rate limit | Từ chối vé **prefetch** trước, để vé cho tập người xem vừa chạm vẫn được cấp. | **Đang triển khai** |
    - | Bạn chạm rate limit playback | Nạp lại hạn mức mỗi giây thay vì mỗi phút, nên `Retry-After` khoảng **1–2 giây** thay vì tới 60. | **Đang triển khai** |
    - | Cùng người xem xin lại cùng tập | Trả lại vé đã cấp khi vé đó còn hạn, và không đếm lần nữa. Cần `viewer_ref` ổn định. | **Đang triển khai** |
    - | Request play đến ngay khi vừa vượt hạn mức | Giữ request tối đa khoảng **0,5 giây** chờ có chỗ, rồi mới trả `429`. | **Đang triển khai** |
    - | Database của Wandura chậm | Phục vụ catalog và kiểm giấy phép từ cache có thể cũ vài phút, thay vì báo lỗi. | **Đang triển khai** |
    + | Khi | Wandura làm gì |
    + |---|---|
    + | Bạn chạm rate limit playback | Trả `429` `rate_limited` kèm header `Retry-After`. Hạn mức được nạp lại mỗi giây chứ không phải mỗi phút, nên `Retry-After` thường là **1–2 giây**. |
    + | Không cấp kịp một vé | Trả `503` `service_unavailable` kèm `Retry-After` (thường 1–2 giây). Trong một lô, chỉ phần tử bị ảnh hưởng lỗi, kèm `retry_after` riêng. |
    + | Lưu lượng sát rate limit | Từ chối vé **prefetch** trước (`429` kèm `"reason": "prefetch_shed"`), để vé cho tập người xem vừa chạm vẫn được cấp. |
    + | Cùng người xem xin lại cùng tập | Trả lại vé đã cấp khi vé đó còn ít nhất 5 phút, và không đếm lần nữa. Cần `viewer_ref` ổn định. |
    + | Request play đến ngay khi vừa vượt hạn mức | Giữ request tối đa khoảng **0,5 giây** chờ có chỗ, rồi mới trả `429`. |
    + | Database của Wandura chậm | Phục vụ catalog và kiểm giấy phép từ cache có thể cũ tới 5 phút, thay vì báo lỗi. |
      
      Hai hệ quả cho code của bạn: luôn đọc `Retry-After` thay vì tự giả định một con số, và cho request xin vé timeout phía client ít nhất **5 giây**, để một lần giữ ngắn không bị cắt ngang thành lỗi.
    ⋯
      
      1. **Xin theo lô các tập kế.** Khi người xem bắt đầu một tập, gửi một request cho tập đó với `"intent": "play"` kèm `"next": 2`. Bạn nhận tới ba vé trong một lượt gọi; các tập sau được ghi là prefetch, không phải lượt phát. Áp paywall của bạn trước: chỉ xin tập người xem được phép xem, và chỉ dùng `next` khi các tập kế cũng đã mở khoá (nếu không thì tự liệt kê bằng `episodes`).
    - 2. **Dùng lại vé khi còn hạn.** Vé có hạn 30 phút (`expires_at`). Giữ vé ở backend theo người xem + tập, và đưa lại khi người xem mở lại tập đó. Chỉ xin vé mới khi còn dưới khoảng 5 phút.
    - 3. **Gửi `viewer_ref` ổn định.** Dùng cùng một giá trị mờ cho cùng người xem trên cùng thiết bị, ví dụ `<your-user-id>:<device-id>`, không chứa dữ liệu cá nhân. Nhờ đó Wandura nhận ra request lặp (**Đang triển khai**, xem bảng trên) và bộ phận hỗ trợ tìm được vé của một người xem.
    + 2. **Dùng lại vé khi còn hạn.** Vé có hạn 30 phút (`expires_at`). Giữ vé ở backend theo người xem + thiết bị + tập (và `intent`), và đưa lại khi người xem mở lại tập đó trên cùng thiết bị. Vé gắn với giá trị thiết bị lúc cấp (`union_info` và `drm_kek`), nên không bao giờ đưa vé đó cho thiết bị khác. Chỉ xin vé mới khi còn dưới khoảng 5 phút.
    + 3. **Gửi `viewer_ref` ổn định.** Dùng cùng một giá trị mờ cho cùng người xem trên cùng thiết bị, ví dụ `<your-user-id>:<device-id>`, không chứa dữ liệu cá nhân. Nhờ đó Wandura nhận ra request lặp (xem bảng trên) và bộ phận hỗ trợ tìm được vé của một người xem.
      
      ```json
    ⋯
      ```
      
    - Một lô tính mỗi tập một vé vào rate limit, và được nhận hoặc từ chối nguyên lô. Mỗi phần tử trong response có `status` riêng: một phần tử có thể `200` trong khi phần tử khác `404` `not_ready` (quá tập cuối đã công bố) hoặc `503` kèm `retry_after`. Xem [Tài liệu tham chiếu Playback](../reference/playback.md).
    + Một lô tính mỗi tập một vé vào rate limit, và từng vé được xét riêng. Khi gần chạm ngưỡng, phần tử prefetch bị từ chối trước, mỗi phần tử có `429` và `retry_after` riêng, trong khi các phần tử còn lại vẫn nhận vé. Cả request chỉ trả `429` khi không phần tử nào cấp được. Mỗi phần tử trong response có `status` riêng: một phần tử có thể `200` trong khi phần tử khác `404` `not_ready` (quá tập cuối đã công bố), `429`, hoặc `503` kèm `retry_after`. Xem [Tài liệu tham chiếu Playback](../reference/playback.md).
      
      ## Retry `429` và `503` cho đúng {#retry}
    ⋯
      ```ts
      type Ticket = { vid: string; play_auth_token: string; expires_at: string; protection: string };
    - const cache = new Map<string, Ticket>(); // key: viewerRef + episode
    + // A ticket is bound to the device values it was issued for (union_info / drm_kek):
    + // key the cache by viewer + device + episode + intent, never share a ticket across devices.
    + const cache = new Map<string, Ticket>();
    + const cacheKey = (b: Record<string, unknown>) => JSON.stringify([
    +   b.viewer_ref, b.union_info, b.drm_kek, b.episode_id ?? `${b.film_ref}#${b.episode_no}`, b.intent ?? 'play',
    + ]);
      let recent = { sent: 0, retries: 0, since: Date.now() };
      
    ⋯
      };
      
    - export async function getTicket(body: Record<string, unknown>, key: string): Promise<Ticket> {
    + export async function getTicket(body: Record<string, unknown>): Promise<Ticket> {
    +   const key = cacheKey(body);
        const hit = cache.get(key);
        if (hit && Date.parse(hit.expires_at) - Date.now() > 5 * 60_000) return hit; // reuse
    ⋯
      ## Đọc response một cách dễ tính {#tolerant-reader}
      
    - API tiếp tục thêm trường, giá trị và mã lỗi, kể cả trong đợt triển khai này. Tích hợp của bạn vẫn chạy nếu nó:
    + API tiếp tục thêm trường, giá trị và mã lỗi. Tích hợp của bạn vẫn chạy nếu nó:
      
      - bỏ qua trường không nhận ra, ví dụ một trường mới cho biết vé được dùng lại;
    ⋯
      
      - [ ] Bắt đầu một tập gửi một request theo lô: `"intent": "play"` kèm `next` (hoặc `episodes`) cho các tập kế đã mở khoá.
    - - [ ] Vé được dùng lại theo người xem + tập tới khoảng 5 phút trước `expires_at`.
    + - [ ] Vé được dùng lại theo người xem + thiết bị + tập, không bao giờ dùng chéo thiết bị, tới khoảng 5 phút trước `expires_at`.
      - [ ] `viewer_ref` ổn định theo người xem + thiết bị, không chứa dữ liệu cá nhân.
      - [ ] `429` và `503` được retry sau `Retry-After` cộng jitter; không có header thì exponential backoff với full jitter.
    ⋯
  3. 02/10/2026 · Tạo trang
↑↓ chọnEnter mởEsc đóng

Gửi góp ý

Góp ý được gửi thẳng tới đội Wandura, kèm địa chỉ trang này.

0 / 4000