HATA ANALİZİ AES-128 HLS.js v1.6.14

HLS.js keyLoadError: "decryptdata unset or changed"

ABR Level Switch Sirasinda Key Loading Race Condition Analizi

Basit Anlatim (Herkes Icin)

Sorun Ne?

Muzik dinlerken bazen ses kopuyor veya sarki baslayamiyor. Bunun sebebi: HLS.js kutuphanesi (ses akisi saglayan yazilim) sifreleme anahtarini yuklemeye calisirken, ayni anda kalite degistirmeye de calisiyor. Bu iki islem carpisiyor ve "anahtarini kaybettim" hatasi veriyor.

Nasil Oluyor?

  1. Sarki basliyor, en dusuk kalitede (ultralow) sifreleme anahtari isteniyor
  2. Anahtar yukleniyor ama AYNI ANDA ABR (otomatik kalite ayari) "internet hizli, daha iyi kaliteye gec" diyor
  3. Kalite degisince eskimis (kullanilmayan) parca bilgisi degisiyor
  4. Anahtar yuklemesi tamamlandiginda, ilk istekte bulunan parca bilgisi artik gecersiz
  5. HLS.js "anahtari yukledim ama parca bilgisi degismis/kaybolmus!" diyor ve hata veriyor

Neden Onemli?

Bu hata kullanicilarin sarki dinleyememesine, sarkinin baslamadan MP3'e dusmesin veya 6+ dakika bekleme sonrasi ancak baslamasina yol aciyor. Ozellikle sifrelenmis HLS akislarinda (AES-128) ve birden fazla kalite seviyesinde (ultralow/low/high) siklikla olusur.

Hatanin Teknik Mekanizmasi

Hata, key-loader.ts dosyasindaki loadKeyHTTP() metodunun onSuccess callback'inde uretilir:

// hls.js/src/loader/key-loader.ts - loadKeyHTTP() icindeki onSuccess callback const { frag, keyInfo } = context; const id = getKeyId(keyInfo.decryptdata); if (!frag.decryptdata || keyInfo !== this.keyIdToKeyInfo[id]) { return reject( this.createKeyLoadError( frag, ErrorDetails.KEY_LOAD_ERROR, new Error('after key load, decryptdata unset or changed'), networkDetails, ), ); }

Iki Kosul (OR) Hataya Sebep Olur:

!frag.decryptdata

Fragment'in decryptdata property'si null/undefined olmus. ABR switch sirasinda fragment degistirildiginde eski fragment'in decryptdata'si temizleniyor.

keyInfo !== this.keyIdToKeyInfo[id]

keyIdToKeyInfo map'indeki referans degismis. Yeni bir fragment ayni key ID icin farkli bir keyInfo objesi olusturmus.

Race Condition Akisi (Adim Adim)

1

startLevel: 0 — Player ultralow kalitede baslar, Fragment A icin key XHR baslatilir

key-loader.ts: loadKeyHTTP(keyInfo_A, frag_A) → XHR basladi
2

ABR Controller — Bant genisligi tahmini yuksek, level switch tetikleniyor

abr-controller.ts: nextAutoLevel = 2 (high) → level switch baslat
3

Stream Controller — Yeni level icin yeni fragment (B) yukleniyor, frag_A'nin decryptdata'si degisebilir veya keyIdToKeyInfo map'i guncellenebilir

stream-controller.ts: immediateLevelSwitch() → flushMainBuffer() → yeni frag_B
4

Key XHR Tamamlandi — Ama frag_A.decryptdata artik null VEYA keyInfo referansi degismis!

onSuccess: !frag.decryptdata || keyInfo !== keyIdToKeyInfo[id] → REJECT!

→ KEY_LOAD_ERROR: "after key load, decryptdata unset or changed"

5

Error ControllergetFragRetryOrSwitchAction() → retry'lar tukenir → SendAlternateToPenaltyBox → 6+ dakika bekleme!

error-controller.ts: MoveAllAlternatesMatchingKey → tum level'lar penalize

ABR Controller Analizi

Kritik Bulgu: Key Loading Kontrolu YOK

ABR controller, fragment/key yukleme durumunu kontrol etmeden level switch karari verir. _abandonRulesCheck() sadece fragment indirme hizina bakar, aktif key XHR'i gormezden gelir.

Varsayilan Degerler

abrBandWidthFactor0.95
abrBandWidthUpFactor0.70
abrEwmaDefaultEstimate500000 (500kbps)
startLevel-1 (auto)
progressivefalse

Mevcut Config (player-core.js)

abrBandWidthFactorvarsayilan (0.95)
abrBandWidthUpFactorvarsayilan (0.70)
abrEwmaDefaultEstimate64000 (64kbps)
startLevel0
progressiveayarlanmamis

Error Controller: keyLoadError Sonrasi Akis

// error-controller.ts - KEY_LOAD_ERROR isleniyor case ErrorDetails.KEY_LOAD_ERROR: case ErrorDetails.KEY_LOAD_TIMEOUT: data.errorAction = this.getFragRetryOrSwitchAction(data); return; // getFragRetryOrSwitchAction() akisi: // 1. keyLoadPolicy.errorRetry'a bakar (maxNumRetry: 3) // 2. Retry hakkı varsa: RetryRequest // 3. Retry tukendiyse: getLevelSwitchAction() // → SendAlternateToPenaltyBox + MoveAllAlternatesMatchingKey // 4. TUM ayni key'i kullanan level'lar penalize edilir! // 5. Sonuc: 6+ dakika penalty box beklemesi

Penalty Box Sorunu

3 level (ultralow/low/high) ayni AES-128 anahtarini kullandiginda, MoveAllAlternatesMatchingKey flag'i tum level'lari penalize eder. Oynatici oynatacak level bulamaz ve dakikalarca bekler.

keyLoadPolicy: Varsayilan vs Mevcut

Parametre HLS.js Varsayilan Mevcut Config Onerilen
maxTimeToFirstByteMs 8000 10000 12000
maxLoadTimeMs 20000 20000 20000
timeoutRetry.maxNumRetry 1 3 2
errorRetry.maxNumRetry 8 3 4
errorRetry.retryDelayMs 1000 500 500
errorRetry.backoff linear exponential linear

Not: key-loader.ts icinde maxRetry: 0 ayarlanir. Bu, key-loader'in kendi retry yapmadigi anlamina gelir — retry mantigi tamamen error-controller'a ve stream-controller'a devredilir. keyLoadPolicy ise error-controller'in retry kararini etkiler.

Ilgili GitHub Issues & PR'lar

MERGED PR #7414

Handle EME key status errors before appending segments

v1.6.11'de merge edildi. Key ban sistemi, otomatik level switching, "context changed during KEY_LOADING" hatasini onler. Kullanilan HLS.js surumunde (v1.6.14) bu fix MEVCUT.

MERGED PR #5598

Fix Multivariant Playlist and Key XHR loader retries

XHR loader abort flag'inin retry'da sifirlanmamasi ve fragment/key hatasi sonrasi stream controller'in ERROR state'e girmesi duzeltildi.

MERGED PR #5255

Fix AES-128 key sharing across playlists

Ayni AES-128 anahtarini kullanan farkli kalite seviyelerinde key paylasim sorunu duzeltildi. Farkli playlist'lerdeki fragment'lar ayni key'i dogru sekilde kullanabiliyor.

ISSUE Issue #1836

Key-loader error handling: retry ayni key'i surekli deniyor

Key yuklemesi basarisiz olunca level switch yerine ayni key'i retry ediyor, maxRetry'a kadar bekliyor. Tam olarak Muzibu'daki sorunun temeli.

ISSUE Issue #5244

AES-128 stream bazen basarisiz oluyor

ABR switch sirasinda sifreleme bagi bozuluyor, segment decode edemiyor. Key paylasim sorunundan kaynaklaniyor.

Mevcut Koruma Mekanizmalari (player-core.js)

Uygulanan Fix'ler

startLevel: 0

En dusuk kaliteden basla → tek key XHR → ABR paralel key yukleme sorunu azalir

abrEwmaDefaultEstimate: 64000

Dusuk bant tahmini → ABR ilk segment tamamlanana kadar ultralow'da kalir → key cache'lenir

HlsPool: Her release'de destroy + fresh

Stale decryptdata sorunu icin: eski instance yeniden kullanilmaz

keyLoadError Recovery: Max 2 deneme + MP3 fallback

Penalty box'i bypass edip fresh HLS instance ile recovery; 2 denemede basarisizsa MP3'e dus

Hala Kapanmamis Aciklar

ABR switch senkronizasyonu yok — key loading sirasinda level switch engellenmiyor
abrBandWidthUpFactor ayarlanmamis — hizli yukselis race condition riskini artirir
keyLoadPolicy.errorRetry: backoff=exponential — race condition hatalari icin gereksiz bekleme yaratir
Recovery her zaman tam yeni URL + fresh instance — agresif (bazen basit retry yeter)

Cozum Onerileri (Oncelik Sirasina Gore)

YUKSEK ONCELIK

1. ABR Yukselis Hizini Yavaslatarak Race Window'u Daralt

ABR controller'in daha yuksek kaliteye gecis esigini yukselt. Boylece key yuklenirken gereksiz level switch olasiligi azalir.

// HLS_SHARED_CONFIG icine ekle: abrBandWidthUpFactor: 0.5, // Varsayilan: 0.70 → 0.50 // Anlami: Kalite yukseltmek icin bandwidth'in %50'si yeterli olmali // (varsayilnda %70) → daha muhafazakar, daha az erken switch abrBandWidthFactor: 0.8, // Varsayilan: 0.95 → 0.80 // Anlami: Mevcut level'da kalmak icin %80 marj gerek // Daha genis guvenlik marji = daha az gereksiz switch
Etki: ABR daha yavaz yukselir → key XHR tamamlanana kadar level degismez → race condition penceresi daralir. Ses kalitesi baslangicta biraz gec yukselir ama kararliligi artar.
YUKSEK ONCELIK

2. LEVEL_SWITCHING Event'inde Key Loading'i Koruma

HLS.js LEVEL_SWITCHING event'ini dinleyerek, aktif key XHR varsa switch'i iptal et veya geciktir.

// player-core.js - HLS event listener ekle: hls.on(Hls.Events.LEVEL_SWITCHING, function(event, data) { // Eger henuz key yukleniyorsa, switch'i durdur if (hls._keyLoadInProgress) { console.warn('⚠️ LEVEL_SWITCHING blocked: key loading in progress'); // Level'i geri al hls.nextLoadLevel = hls.loadLevel; return; } }); // Key loading baslangic/bitis takibi: hls.on(Hls.Events.KEY_LOADING, function() { hls._keyLoadInProgress = true; }); hls.on(Hls.Events.KEY_LOADED, function() { hls._keyLoadInProgress = false; }); hls.on(Hls.Events.ERROR, function(event, data) { if (data.details === 'keyLoadError' || data.details === 'keyLoadTimeOut') { hls._keyLoadInProgress = false; } });
Not: Bu yaklasim HLS.js'in ic API'sini kullanir (nextLoadLevel). Surum guncellemelerinde uyumluluk kontrolu gerekir. Ayrica LEVEL_SWITCHING event'i bazen switch zaten baslamis oldugunda tetiklenir — %100 garanti degil ama window'u onemli olcude daraltir.
ORTA ONCELIK

3. keyLoadPolicy'yi Race Condition'a Optimize Et

keyLoadPolicy: { default: { maxTimeToFirstByteMs: 12000, // 10s→12s (key sunucusu yavaslarsa) maxLoadTimeMs: 20000, timeoutRetry: { maxNumRetry: 2, // 3→2 (hizli fail, recovery'e birak) retryDelayMs: 500, // 1000→500 (daha hizli retry) maxRetryDelayMs: 2000 // 3000→2000 }, errorRetry: { maxNumRetry: 4, // 3→4 (race condition icin 1 ekstra sans) retryDelayMs: 300, // 500→300 (race condition = gecici, hizli retry) maxRetryDelayMs: 2000, backoff: 'linear' // exponential→linear (300,600,900,1200) } } },
Neden linear? "decryptdata unset or changed" hatasi gecici bir durum — ABR switch tamamlaninca decryptdata yeniden olusur. Exponential backoff gereksiz bekleme yaratir (300, 600, 1200, 2400ms). Linear ile sabit artan gecikme (300, 600, 900, 1200ms) race condition window'u gecene kadar yeterli.
ORTA ONCELIK

4. Mevcut Recovery'yi Daha Verimli Yap

Simdiki recovery her keyLoadError'da fresh instance + yeni URL olusturuyor. Bu agresif. Race condition kaynakliysa basit retry yeterli olabilir.

// Mevcut: Her keyLoadError → hemen taint + fresh instance // Onerilen: ilk 2 hatayi HLS.js'in kendi retry'ina birak if (!data.fatal && data.details === 'keyLoadError') { self._keyLoadErrCount++; // Ilk 2 hata: HLS.js errorRetry'a birak (race condition kendini cozer) if (self._keyLoadErrCount <= 2) { console.warn('🔑 keyLoadError #' + self._keyLoadErrCount + ' → HLS.js retry'a birakildi'); return; // Mudahale etme, HLS.js keyLoadPolicy'ye gore retry edecek } // 3+ hata: Race condition degil, gercek sorun → fresh recovery if (self._keyLoadErrCount >= 3 && self._keyTotalRecoveries < 2) { // Mevcut taint + fresh instance recovery mantigi... } }
DUSUK ONCELIK

5. HLS.js Surum Guncelleme (v1.6.15)

v1.6.15'te PlayReady/FairPlay key fix'leri var. AES-128 icin dogrudan ilgili olmasa da, genel key yonetimi iyilestirmeleri faydali olabilir.

npm install hls.js@1.6.15 && npm run prod
DUSUK ONCELIK

6. Tek Kalite Seviyesi ile Test (Tani Amacli)

ABR switch'i tamamen devre disi birakarak hatanin GERCEKTEN ABR kaynakli olup olmadigini dogrula.

// Tani testi: ABR'i kapat, sadece tek level kullan const testConfig = Object.assign({}, HLS_SHARED_CONFIG, { startLevel: 0, autoLevelEnabled: false, // TANI: ABR kapat // VEYA: autoLevelCapping: 0, // TANI: Max level=0 (ultralow'da kilitle) }); // Eger bu config ile keyLoadError HALA olusuyorsa → sorun ABR degil, // pool reuse/stale state veya sunucu tarafli.

Onerilen Tam HLS_SHARED_CONFIG

const HLS_SHARED_CONFIG = { enableWorker: true, lowLatencyMode: false, startLevel: 0, abrEwmaDefaultEstimate: 64000, // --- YENi: ABR Race Condition Korumasi --- abrBandWidthUpFactor: 0.5, // 0.70→0.50: Kalite yukseltme esigi abrBandWidthFactor: 0.8, // 0.95→0.80: Mevcut kalitede kalma marji maxBufferLength: 12, maxMaxBufferLength: 20, maxBufferSize: 12 * 1000 * 1000, maxBufferHole: 2.5, maxFragLookUpTolerance: 0.5, backBufferLength: 0, keyLoadPolicy: { default: { maxTimeToFirstByteMs: 12000, maxLoadTimeMs: 20000, timeoutRetry: { maxNumRetry: 2, retryDelayMs: 500, maxRetryDelayMs: 2000 }, errorRetry: { maxNumRetry: 4, retryDelayMs: 300, maxRetryDelayMs: 2000, backoff: 'linear' } } }, fragLoadPolicy: { default: { maxTimeToFirstByteMs: 10000, maxLoadTimeMs: 30000, timeoutRetry: { maxNumRetry: 4, retryDelayMs: 1000, maxRetryDelayMs: 5000 }, errorRetry: { maxNumRetry: 5, retryDelayMs: 500, maxRetryDelayMs: 3000 } } }, xhrSetup: function(xhr, url) { xhr.withCredentials = false; } };

Duzeltme Oncelik Matrisi

# Duzeltme Etki Risk Zorluk
1 abrBandWidthUpFactor: 0.5, abrBandWidthFactor: 0.8 Yuksek Dusuk Kolay
2 LEVEL_SWITCHING event guard Yuksek Orta Orta
3 keyLoadPolicy: linear backoff, 4 retry Orta Dusuk Kolay
4 Recovery optimizasyonu (ilk 2 hatada mudahale etme) Orta Dusuk Kolay
5 HLS.js v1.6.15 guncelleme Dusuk Dusuk Kolay
6 Tek kalite seviyesi tani testi Tani Yok Kolay

Kaynaklar