# Netflix Manifest API — ストリーミングプロファイル・画質指定
Netflix iOS アプリが動画再生に必要なストリーム情報を取得する `/manifest` API の仕様。Frida フックで取得した平文リクエストに基づく。
---
## 1. 概要
`/manifest` は MSL (Message Security Layer) で暗号化された API エンドポイント。クライアントが再生可能なコーデック・画質・音声・字幕のプロファイル一覧をサーバーに送信し、サーバーはそれに基づいて利用可能なストリームと CDN URL を返す。
---
## 2. 通信フロー
```mermaid
%%{init: {'theme': 'dark'}}%%
sequenceDiagram
participant UI as Netflix UI
participant Player as NFPlayerController
participant MSL as IosMslClient
participant Crypto as MslClient C++
participant Server as ios.prod.cloud.netflix.com
Note over UI: ユーザーがコンテンツを選択
UI->>Player: load(viewableId)
rect rgba(180, 130, 50, 0.2)
Note over Player,Server: PRE_FETCH フェーズ (先読み)
Player->>MSL: sendAPIRequest("/manifest", params)
Note over MSL: flavor: "PRE_FETCH"
MSL->>Crypto: aesCbcEncrypt(headerdata)
MSL->>Crypto: signHmacSha256(header)
MSL->>Crypto: aesCbcEncrypt(payload)
MSL->>Crypto: signHmacSha256(payload)
MSL->>Server: POST /msl/playapi/ios/manifest
(MSL 暗号化済み HTTP Body)
Server->>MSL: MSL 暗号化レスポンス
MSL->>Crypto: aesCbcDecrypt(payload)
MSL->>Player: manifest JSON (CDN URL, ストリーム一覧)
end
rect rgba(50, 160, 80, 0.2)
Note over Player,Server: STANDARD フェーズ (再生開始)
Player->>MSL: sendAPIRequest("/manifest", params)
Note over MSL: flavor: "standard"
MSL->>Server: POST /msl/playapi/ios/manifest
Server->>MSL: MSL 暗号化レスポンス
MSL->>Player: manifest JSON
end
rect rgba(60, 100, 180, 0.2)
Note over Player,Server: LICENSE フェーズ (DRM 鍵取得)
Player->>MSL: sendAPIRequest("/license", params)
Note over MSL: challengeBase64 (FairPlay SPC)
MSL->>Server: POST /msl/playapi/ios/license
Server->>MSL: CKC (Content Key Context)
MSL->>Player: FairPlay CKC → コンテンツ鍵展開
end
Note over Player: 再生開始 (CDN URL + コンテンツ鍵)
```
### 全体の流れ
```mermaid
%%{init: {'theme': 'dark'}}%%
flowchart LR
subgraph "1. マニフェスト取得"
A["/manifest
PRE_FETCH"] --> B["/manifest
standard"]
end
subgraph "2. DRM ライセンス"
C["/license
FairPlay SPC→CKC"]
end
subgraph "3. ストリーム再生"
D["CDN からセグメント取得
(DASH + FairPlay暗号化)"]
end
B --> C
C --> D
style A fill:#6b4c1e,stroke:#c8943e,color:#f0d8a8
style B fill:#1e5c2e,stroke:#3ea84e,color:#a8f0b8
style C fill:#1e3a6b,stroke:#3e78c8,color:#a8c8f0
style D fill:#4a1e6b,stroke:#8e3ec8,color:#d0a8f0
```
---
## 3. 暗号化
`/manifest` のリクエスト・レスポンスは **MSL プロトコルで暗号化** されている。
```mermaid
%%{init: {'theme': 'dark'}}%%
flowchart TD
subgraph "リクエスト (クライアント → サーバー)"
R1["平文 JSON
(viewableId, profiles, ...)"]
R2["MSL PayloadChunk
AES-128-CBC 暗号化"]
R3["MSL MessageHeader
AES-128-CBC + HMAC-SHA256"]
R4["HTTP POST Body
(暗号化済みバイナリ)"]
R1 -->|"aesCbcEncrypt()"| R2
R1 -->|"signHmacSha256()"| R3
R2 --> R4
R3 --> R4
end
subgraph "レスポンス (サーバー → クライアント)"
S1["HTTP Response Body
(暗号化済みバイナリ)"]
S2["MSL PayloadChunk
AES-128-CBC 暗号化"]
S3["平文 JSON
(CDN URL, ストリーム一覧)"]
S1 -->|"aesCbcDecrypt()"| S2
S2 -->|"decompress (GZIP)"| S3
end
```
| 項目 | 暗号化 | 状態 |
|---|---|---|
| リクエスト (profiles, viewableId 等) | MSL (AES-128-CBC + HMAC-SHA256) | **平文取得済み** — `sendAPIRequest` フックで暗号化前に捕捉 |
| レスポンス (CDN URL, ストリーム一覧) | MSL (AES-128-CBC + HMAC-SHA256) + GZIP 圧縮 | **未取得** — `sendAPIRequest` コールバックフック実装済みだが manifest レスポンスのログが出ていない (logblob の ACK のみ) |
| HTTP トランスポート | TLS 1.2/1.3 | TLS 上に MSL が重ねられている (二重暗号化) |
| 動画セグメント本体 | FairPlay CBCS (Common encryption) | コンテンツ鍵は `/license` CKC から取得 |
---
## 4. リクエストパラメータ
`IosMslClient.sendAPIRequest("/manifest", params)` で送信される JSON。
### 4.1 全パラメータ一覧
```json
{
"viewableId": 81774276,
"flavor": "PRE_FETCH",
"profiles": [ ... ],
"profileGroups": [ ... ],
"drmType": "fairplay",
"manifestVersion": "v2",
"desiredVmaf": "phone_plus_lts",
"cellularCap": "auto",
"netType": "wifi",
"useHttpsStreams": true,
"supportsWatermark": true,
"supportsUnequalizedDownloadables": true,
"supportsPartialHydration": true,
"supportsAdBreakHydration": true,
"supportsSecureStop": false,
"supportsPreReleasePin": true,
"unletterboxed": false,
"liveMetadataFormat": "HLS",
"contentPlaygraph": ["start"],
"requiresAudioTrackGroups": true,
"preferAssistiveAudio": false,
"prefersClosedCaptions": false,
"hardware": "IPHONE9-1",
"osName": "iOS",
"osVersion": "15.8.3",
"uiPlatform": "ios",
"clientVersion": "15.48.1",
"platform": "2012.4",
"sdk": "2012.4",
"build": "24",
"xid": "7114306196730548973"
}
```
### 4.2 主要パラメータの意味
| パラメータ | 型 | 説明 |
|---|---|---|
| `viewableId` | number | コンテンツ ID。Netflix の各作品/エピソードに割り当てられた一意の番号 |
| `flavor` | string | `"PRE_FETCH"` = UI 表示時に先読み、`"standard"` = 実際の再生開始時 |
| `profiles` | string[] | **クライアントが対応するコーデック・画質プロファイルの一覧** (後述) |
| `profileGroups` | object[] | profiles をグループ分けした構造 (DRM スキーム別) |
| `drmType` | string | DRM 方式。iOS では常に `"fairplay"` |
| `manifestVersion` | string | マニフェストバージョン。`"v2"` |
| `desiredVmaf` | string | **画質ターゲット**。VMAF (Video Multi-method Assessment Fusion) ベースの品質スコア指定。`"phone_plus_lts"` = スマホ向け長期安定画質モード |
| `cellularCap` | string | **セルラー回線時の帯域制限**。`"auto"` = ネットワーク状況に応じて自動判定 |
| `netType` | string | 現在の接続種別。`"wifi"` or `"cellular"` |
| `useHttpsStreams` | boolean | ストリーム URL に HTTPS を使うか |
| `supportsWatermark` | boolean | フォレンジックウォーターマーク対応 |
| `supportsUnequalizedDownloadables` | boolean | 非均一品質のダウンロード可能セグメント対応 |
| `supportsPartialHydration` | boolean | 部分的なマニフェスト補完対応 |
| `unletterboxed` | boolean | レターボックス除去。`false` = そのまま |
| `liveMetadataFormat` | string | ライブコンテンツのメタデータ形式。`"HLS"` |
| `contentPlaygraph` | string[] | 再生グラフの状態。`["start"]` = 再生開始 |
| `xid` | string | リクエスト一意ID (トレーシング用) |
| `hardware` | string | デバイスモデル。例: `"IPHONE9-1"` (iPhone 7) |
---
## 5. プロファイル (profiles) — コーデック・画質指定
`profiles` 配列がサーバーに「このデバイスはこれらのコーデック・画質に対応している」と伝える。サーバーはこの一覧に基づいて利用可能なストリームを返す。
### 5.1 映像プロファイル
#### H.264 (AVC) — `profileGroups.name: "ce3"` および `"live"`
| プロファイル名 | コーデック | Level | 最大解像度目安 | DRM |
|---|---|---|---|---|
| `h264hpl22-dash-playready-live` | H.264 High Profile | 2.2 | ~352x288 | PlayReady (live) |
| `h264hpl30-dash-playready-live` | H.264 High Profile | 3.0 | ~720x480 (SD) | PlayReady (live) |
| `h264hpl31-dash-playready-live` | H.264 High Profile | 3.1 | ~1280x720 (HD) | PlayReady (live) |
| `h264hpl40-dash-playready-live` | H.264 High Profile | 4.0 | ~1920x1080 (FHD) | PlayReady (live) |
| `playready-h264mpl30-dash` | H.264 Main Profile | 3.0 | SD | PlayReady |
| `playready-h264mpl31-dash` | H.264 Main Profile | 3.1 | HD | PlayReady |
| `playready-h264mpl40-dash` | H.264 Main Profile | 4.0 | FHD | PlayReady |
| `playready-h264hpl22-dash` | H.264 High Profile | 2.2 | 低解像度 | PlayReady |
| `playready-h264hpl30-dash` | H.264 High Profile | 3.0 | SD | PlayReady |
| `playready-h264hpl31-dash` | H.264 High Profile | 3.1 | HD | PlayReady |
| `playready-h264hpl40-dash` | H.264 High Profile | 4.0 | FHD | PlayReady |
#### HEVC (H.265) — `profileGroups.name: "ce4"`
| プロファイル名 | コーデック | Level | 最大解像度目安 | 備考 |
|---|---|---|---|---|
| `hevc-main10-L30-dash-cenc-live` | HEVC Main 10 | 3.0 | SD | ライブ |
| `hevc-main10-L31-dash-cenc-live` | HEVC Main 10 | 3.1 | HD | ライブ |
| `hevc-main10-L40-dash-cenc-live` | HEVC Main 10 | 4.0 | FHD | ライブ |
| `hevc-main10-L41-dash-cenc-live` | HEVC Main 10 | 4.1 | FHD+ | ライブ |
| `hevc-main10-L30-dash-cenc-prk` | HEVC Main 10 | 3.0 | SD | PlayReady Key |
| `hevc-main10-L31-dash-cenc-prk` | HEVC Main 10 | 3.1 | HD | PlayReady Key |
| `hevc-main10-L40-dash-cenc-prk` | HEVC Main 10 | 4.0 | FHD | PlayReady Key |
| `hevc-main10-L41-dash-cenc-prk` | HEVC Main 10 | 4.1 | FHD+ | PlayReady Key |
| `hevc-main10-L30-dash-cenc-prk-do` | HEVC Main 10 | 3.0 | SD | PRK + Download Offline |
| `hevc-main10-L31-dash-cenc-prk-do` | HEVC Main 10 | 3.1 | HD | PRK + Download Offline |
| `hevc-main10-L40-dash-cenc-prk-do` | HEVC Main 10 | 4.0 | FHD | PRK + Download Offline |
| `hevc-main10-L41-dash-cenc-prk-do` | HEVC Main 10 | 4.1 | FHD+ | PRK + Download Offline |
> **Note**: `Main 10` は 10bit 色深度 (HDR 対応)。iPhone 7 (IPHONE9-1) は HEVC ハードウェアデコード対応。
### 5.2 音声プロファイル
| プロファイル名 | コーデック | チャンネル | 備考 |
|---|---|---|---|
| `heaac-2-dash` | HE-AAC v1 | 2ch (ステレオ) | 標準音声 |
| `heaac-2hq-dash` | HE-AAC v1 | 2ch (ステレオ) | 高品質 (ビットレート高) |
| `dd-5.1-dash` | Dolby Digital (AC-3) | 5.1ch | サラウンド |
| `ddplus-5.1-dash` | Dolby Digital Plus (E-AC-3) | 5.1ch | サラウンド (効率的) |
| `ddplus-5.1hq-dash` | Dolby Digital Plus (E-AC-3) | 5.1ch | 高品質サラウンド |
| `ddplus-atmos-dash` | Dolby Atmos (E-AC-3 JOC) | オブジェクト | 空間オーディオ |
### 5.3 字幕・その他
| プロファイル名 | 種別 | 説明 |
|---|---|---|
| `webvtt-lssdh-ios13` | 字幕 | WebVTT (iOS 13+, LSSDH 対応) |
| `webvtt-lssdh-ios8` | 字幕 | WebVTT (iOS 8+ レガシー) |
| `BIF240` | サムネイル | BIF (Base Index Frames) 240px — シークバーのプレビュー用 |
| `BIF320` | サムネイル | BIF 320px — シークバーのプレビュー用 |
| `nflx-cmisc` | メタデータ | Netflix 制御メタデータ (チャプター、スキップ情報等) |
> `LSSDH` = Limited Streams for Simple Device Handling
### 5.4 プロファイルグループ (profileGroups)
profiles をDRM/暗号化方式ごとにグループ分けしたもの。サーバー側でストリーム選択時に使用される。
| グループ名 | 対象プロファイル | DRM 方式 |
|---|---|---|
| `live` | `h264hpl*-live`, `hevc-*-live` | ライブ/リアルタイム配信用 |
| `ce3` | `playready-h264*-dash` | PlayReady Content Encryption 3.0 (H.264) |
| `ce4` | `hevc-main10-*-prk`, `*-prk-do` | PlayReady Key (HEVC), Download Offline 含む |
---
## 6. 画質制御パラメータ
| パラメータ | 値 | 画質への影響 |
|---|---|---|
| `desiredVmaf` | `"phone_plus_lts"` | **VMAF 品質ターゲット**。サーバー側でエンコード品質 (ビットレート配分) を決定する基準。`phone_plus_lts` = スマホ画面向け + 長期安定 (Long Term Stable) |
| `cellularCap` | `"auto"` | セルラー回線時の帯域上限。`auto` = アダプティブ |
| `netType` | `"wifi"` / `"cellular"` | ネットワーク種別。wifi 時は帯域制限が緩和される |
| `profiles` | (上記一覧) | **対応コーデック/画質の上限**。ここに無いプロファイルのストリームはサーバーから返されない |
| `unletterboxed` | `false` | `true` にするとレターボックス (黒帯) を除去した映像が返される |
| `hardware` | `"IPHONE9-1"` | デバイスモデル。サーバー側でハードウェアデコード能力を判定 |
### desiredVmaf の値 (推定)
| 値 | 対象デバイス |
|---|---|
| `phone_plus_lts` | スマートフォン (小画面) |
| `tablet_plus_lts` | タブレット (中画面) |
| `tv_plus_lts` | テレビ/大画面 |
> VMAF (Video Multi-method Assessment Fusion) は Netflix が開発した知覚品質メトリクス。同じビットレートでもコンテンツの複雑さに応じてエンコード品質を調整する (Per-Title Encoding / Dynamic Optimizer)。
---
## 7. リクエスト例 (実データ)
```json
{
"viewableId": 81774276,
"flavor": "PRE_FETCH",
"drmType": "fairplay",
"manifestVersion": "v2",
"desiredVmaf": "phone_plus_lts",
"cellularCap": "auto",
"netType": "wifi",
"useHttpsStreams": true,
"supportsWatermark": true,
"supportsUnequalizedDownloadables": true,
"supportsPartialHydration": true,
"supportsAdBreakHydration": true,
"supportsSecureStop": false,
"supportsPreReleasePin": true,
"unletterboxed": false,
"liveMetadataFormat": "HLS",
"contentPlaygraph": ["start"],
"requiresAudioTrackGroups": true,
"preferAssistiveAudio": false,
"prefersClosedCaptions": false,
"hardware": "IPHONE9-1",
"osName": "iOS",
"osVersion": "15.8.3",
"uiPlatform": "ios",
"clientVersion": "15.48.1",
"platform": "2012.4",
"sdk": "2012.4",
"build": "24",
"xid": "7114306196730548973",
"profiles": [
"h264hpl22-dash-playready-live",
"h264hpl30-dash-playready-live",
"h264hpl31-dash-playready-live",
"h264hpl40-dash-playready-live",
"hevc-main10-L30-dash-cenc-live",
"hevc-main10-L31-dash-cenc-live",
"hevc-main10-L40-dash-cenc-live",
"hevc-main10-L41-dash-cenc-live",
"playready-h264mpl30-dash",
"playready-h264mpl31-dash",
"playready-h264mpl40-dash",
"playready-h264hpl22-dash",
"playready-h264hpl30-dash",
"playready-h264hpl31-dash",
"playready-h264hpl40-dash",
"hevc-main10-L30-dash-cenc-prk",
"hevc-main10-L31-dash-cenc-prk",
"hevc-main10-L40-dash-cenc-prk",
"hevc-main10-L41-dash-cenc-prk",
"hevc-main10-L30-dash-cenc-prk-do",
"hevc-main10-L31-dash-cenc-prk-do",
"hevc-main10-L40-dash-cenc-prk-do",
"hevc-main10-L41-dash-cenc-prk-do",
"heaac-2-dash",
"heaac-2hq-dash",
"dd-5.1-dash",
"ddplus-5.1-dash",
"ddplus-5.1hq-dash",
"ddplus-atmos-dash",
"webvtt-lssdh-ios13",
"nflx-cmisc",
"webvtt-lssdh-ios8",
"BIF240",
"BIF320"
],
"profileGroups": [
{
"name": "live",
"profiles": [
"h264hpl22-dash-playready-live",
"h264hpl30-dash-playready-live",
"h264hpl31-dash-playready-live",
"h264hpl40-dash-playready-live",
"hevc-main10-L30-dash-cenc-live",
"hevc-main10-L31-dash-cenc-live",
"hevc-main10-L40-dash-cenc-live",
"hevc-main10-L41-dash-cenc-live"
]
},
{
"name": "ce3",
"profiles": [
"playready-h264mpl30-dash",
"playready-h264mpl31-dash",
"playready-h264mpl40-dash",
"playready-h264hpl22-dash",
"playready-h264hpl30-dash",
"playready-h264hpl31-dash",
"playready-h264hpl40-dash"
]
},
{
"name": "ce4",
"profiles": [
"hevc-main10-L30-dash-cenc-prk",
"hevc-main10-L31-dash-cenc-prk",
"hevc-main10-L40-dash-cenc-prk",
"hevc-main10-L41-dash-cenc-prk",
"hevc-main10-L30-dash-cenc-prk-do",
"hevc-main10-L31-dash-cenc-prk-do",
"hevc-main10-L40-dash-cenc-prk-do",
"hevc-main10-L41-dash-cenc-prk-do"
]
}
]
}
```
---
## 8. レスポンス (推定構造)
manifest レスポンスはまだ捕捉できていないが、プロファイル名とリクエスト構造から以下の構造が推定される。
```json
{
"version": 2,
"playbackContextId": "...",
"drmContextId": "...",
"urls": [
{
"url": "https://ipv4-*.oca.nflxvideo.net/...",
"cdn_id": 12345
}
],
"video_tracks": [
{
"type": "video",
"profile": "hevc-main10-L40-dash-cenc-prk",
"bitrate": 4500000,
"width": 1920,
"height": 1080,
"vmaf": 93.5,
"downloadable_id": "...",
"urls": [{ "url": "...", "cdn_id": 12345 }]
}
],
"audio_tracks": [
{
"type": "audio",
"profile": "ddplus-5.1-dash",
"bitrate": 640000,
"language": "en",
"channels": "5.1",
"downloadable_id": "..."
}
],
"timedtexttracks": [
{
"type": "text",
"profile": "webvtt-lssdh-ios13",
"language": "en",
"isForced": false,
"downloadable_id": "..."
}
],
"drmHeader": {
"keyId": "...",
"initData": "..."
}
}
```
> **Note**: 実際のレスポンスを取得するには `sendAPIRequest` のコールバックフック (`ObjC.Block(args[7])`) で manifest レスポンスを捕捉する必要がある。現在 logblob の ACK レスポンスのみ取得されている状態。
---
## 9. プロファイル名の命名規則
```
{codec}-{profile}-{level}-{container}-{encryption}[-{variant}]
```
```mermaid
%%{init: {'theme': 'dark'}}%%
flowchart LR
subgraph "プロファイル名の構造"
A["hevc"] --- B["main10"] --- C["L40"] --- D["dash"] --- E["cenc"] --- F["prk-do"]
end
A2["コーデック
h264 / hevc"] -.-> A
B2["プロファイル
hpl (High) / mpl (Main)
main10 (10bit)"] -.-> B
C2["レベル
L22〜L41
(解像度/帯域上限)"] -.-> C
D2["コンテナ
dash (MPEG-DASH)"] -.-> D
E2["暗号化
cenc (Common Encryption)
playready"] -.-> E
F2["バリアント
prk (PlayReady Key)
do (Download Offline)
live (ライブ)"] -.-> F
```
| 略称 | 意味 |
|---|---|
| `hpl` | High Profile Level (H.264) |
| `mpl` | Main Profile Level (H.264) |
| `main10` | Main 10 Profile (HEVC, 10bit HDR 対応) |
| `L30`〜`L41` | コーデックレベル (解像度・ビットレート上限を規定) |
| `dash` | MPEG-DASH コンテナ |
| `cenc` | Common Encryption (ISO 23001-7) |
| `playready` | Microsoft PlayReady DRM |
| `prk` | PlayReady Key |
| `do` | Download Offline (オフライン再生用) |
| `live` | ライブ/リアルタイムストリーミング |
| `hq` | High Quality (高ビットレート) |
| `lssdh` | Limited Streams for Simple Device Handling |
---
## 10. 画質を変更するには
profiles 配列の内容を変更することで、サーバーから返されるストリームの種類が変わる。
| やりたいこと | profiles の変更 |
|---|---|
| H.264 のみにする | `hevc-*` プロファイルを全て削除 |
| 720p 以下に制限 | `*L40*`, `*L41*` プロファイルを削除 |
| SD のみにする | `*L31*`, `*L40*`, `*L41*` を削除 |
| Dolby Atmos を無効化 | `ddplus-atmos-dash` を削除 |
| ステレオ音声のみ | `dd-*`, `ddplus-*` を全て削除 |
| ダウンロード用を除外 | `*-do` プロファイルを削除 |
> **注意**: profiles を書き換えるには `sendAPIRequest` フック内で `params` オブジェクトの profiles 配列を操作する必要がある。MSL 暗号化前のアプリ層で介入する。