# 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), 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
| Khi | Wandura |
|---|---|
| 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.

## Xin vé sớm
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
{
  "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": "k3Jd9sQm2vXa7LpT0bWc1e:Zr8uYq2nM4a"
}
```

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
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](https://docs.wandura.ai/vi/developers/reference/behaviour#retries). Trong code:

```ts
/** 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 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 };

/** 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;
}

/** 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);
}

/** 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;
  }
}
```

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](https://docs.wandura.ai/vi/developers/guides/playback-flow#code).

## Giữ người xem tiếp tục xem
- **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ô.**

Code phía app, bằng Swift và Kotlin, làm đúng như vậy: [iOS](https://docs.wandura.ai/vi/developers/player-sdk/ios#code-controller), [Android](https://docs.wandura.ai/vi/developers/player-sdk/android#code-controller).

## Đọc response một cách dễ tính
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á](https://docs.wandura.ai/vi/developers/concepts/versioning#tolerant-reader).

## Danh sách kiểm tra
- [ ] 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.
- [ ] 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.

## Liên quan
- [Rate limit](https://docs.wandura.ai/vi/developers/concepts/rate-limits)
- [Lỗi](https://docs.wandura.ai/vi/developers/concepts/errors)
