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?
- Sarki basliyor, en dusuk kalitede (ultralow) sifreleme anahtari isteniyor
- Anahtar yukleniyor ama AYNI ANDA ABR (otomatik kalite ayari) "internet hizli, daha iyi kaliteye gec" diyor
- Kalite degisince eskimis (kullanilmayan) parca bilgisi degisiyor
- Anahtar yuklemesi tamamlandiginda, ilk istekte bulunan parca bilgisi artik gecersiz
- 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)
startLevel: 0 — Player ultralow kalitede baslar, Fragment A icin key XHR baslatilir
key-loader.ts: loadKeyHTTP(keyInfo_A, frag_A) → XHR basladi
ABR Controller — Bant genisligi tahmini yuksek, level switch tetikleniyor
abr-controller.ts: nextAutoLevel = 2 (high) → level switch baslat
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
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"
Error Controller — getFragRetryOrSwitchAction() → 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
| abrBandWidthFactor | 0.95 |
| abrBandWidthUpFactor | 0.70 |
| abrEwmaDefaultEstimate | 500000 (500kbps) |
| startLevel | -1 (auto) |
| progressive | false |
Mevcut Config (player-core.js)
| abrBandWidthFactor | varsayilan (0.95) |
| abrBandWidthUpFactor | varsayilan (0.70) |
| abrEwmaDefaultEstimate | 64000 (64kbps) |
| startLevel | 0 |
| progressive | ayarlanmamis |
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
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.
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.
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.
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.
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
En dusuk kaliteden basla → tek key XHR → ABR paralel key yukleme sorunu azalir
Dusuk bant tahmini → ABR ilk segment tamamlanana kadar ultralow'da kalir → key cache'lenir
Stale decryptdata sorunu icin: eski instance yeniden kullanilmaz
Penalty box'i bypass edip fresh HLS instance ile recovery; 2 denemede basarisizsa MP3'e dus
Hala Kapanmamis Aciklar
Cozum Onerileri (Oncelik Sirasina Gore)
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
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;
}
});
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)
}
}
},
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...
}
}
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
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
Race condition'in kaynak kodu
error-controller.tsKEY_LOAD_ERROR isleme mantigi
abr-controller.tsABR level switch karari
API.mdConfig dokumantasyonu
PR #7414EME key status error handling (v1.6.11)
PR #5598Key XHR loader retry fix
PR #5255AES-128 key sharing fix
Issue #1836Key-loader error handling tartismasi