Basit Anlatım (Herkes İçin)
Sorun Ne?
Müzik dinlerken bazen ses kopuyor veya şarkı başlayamıyor. HLS.js kütüphanesi şifreleme anahtarını (encryption key) yüklemeye çalışırken, aynı anda kalite değiştirmeye de çalışıyor. Bu iki işlem çarpışıyor ve "anahtarımı kaybettim" hatası veriyor.
Neden Oluyor?
Düşünün ki bir kasadaki anahtarı almak için gönderiyor olun. Anahtarı almaya giden kişi yolda iken, siz kasanın kilidini değiştiriyorsunuz. Kişi geri geldiğinde "bu anahtar artık bu kasaya uymuyor" diyor. İşte HLS.js'de de tam olarak bu oluyor: Anahtar indirilirken, kalite değişikliği yapılınca anahtar bilgisi değişiyor ve eski indirme işlemi başarısız oluyor.
Mevcut Durumda Ne Yapıldı?
Şu anda player-core.js'de birçok koruma katmanı var: ABR başlangıçta kilitlenmesi, anahtar ön-yükleme, fatal hata sonrası kaynak yeniden yükleme ve HLS havuz yöneticisi ile kirli instance temizleme. Bunlar sorunu büyük ölçüde azaltıyor ama HLS.js'nin iç yapısındaki sorun devam ediyor.
Bilinen Bir Bug mı?
Sonuç: Resmi bir GitHub Issue YOK, ama bilinen bir tasarım sınırlaması
HLS.js GitHub deposunda (video-dev/hls.js) "after key load, decryptdata unset or changed" hata mesajı ile doğrudan eşleşen bir issue bulunamadı. Ancak bu hata mesajı, HLS.js kaynak kodunda kasıtlı olarak yerleştirilmiş bir güvenlik kontrolüdür.
İlgili GitHub issue'ları:
- #5244 - "Sometimes play AES-128 stream failed" (v1.3.3) - Benzer belirtiler, çözümsüz
- #7022 - Custom loader ile decryptdata etkileşimi sorunları
- #7474 - Multi-key handling mimarisi tartışması (devam ediyor)
- #1836 - Key-loader error handling (eski, key retry sorunları)
Bu sorun, HLS.js'nin key-loader mimarisindeki yapısal bir sınırlamadır. Hata mesajı, race condition tespiti için bilinçli olarak eklenmiş bir guard clause'dur. Resmi olarak bir "bug" olarak raporlanmamış, çünkü HLS.js geliştiricileri bunu bir güvenlik kontrolü olarak görüyor.
Daha Yeni Versiyonda Düzeltildi mi?
Sonuç: HAYIR - Aynı kod v1.4.12'den v1.6.15'e kadar değişmeden duruyor
HLS.js'nin tüm ana versiyonları karşılaştırıldı:
| Versiyon | Key Map Adı | Referans Kontrolü | Bu Hata Var mı? |
|---|---|---|---|
| v1.4.12 | keyUriToKeyInfo | keyInfo !== this.keyUriToKeyInfo[uri] | EVET |
| v1.5.0 | keyUriToKeyInfo | keyInfo !== this.keyUriToKeyInfo[uri] | EVET |
| v1.6.0 | keyUriToKeyInfo | keyInfo !== this.keyUriToKeyInfo[uri] | EVET |
| master (son) | keyIdToKeyInfo | keyInfo !== this.keyIdToKeyInfo[id] | KISMI |
Master Branch'teki Değişiklik
Master branch'te keyUriToKeyInfo yerine keyIdToKeyInfo kullanılıyor ve getKeyId() fonksiyonu ile anahtar indeksleme yapılıyor. Bu, aynı URI'yi farklı IV'lerle kullanan level'lar için daha iyi çalışabilir, çünkü key ID daha spesifik bir tanımlayıcı. Ancak temel referans eşitliği kontrolü (===) hala aynı ve aynı race condition tetiklenebilir.
Neden Düzeltilmedi?
HLS.js geliştiricileri bu kontrolü bir güvenlik mekanizması olarak görüyor. Stale (bayatlamış) key verisi ile şifre çözme yapmak veri bozulmasına yol açabilir. Bu nedenle "hatalı key ile devam etmektense hata vermek" yaklaşımını benimsemişler. Sorun HLS.js'de değil, aynı key URI'sini paylaşan multi-level stream'lerin race condition'a açık yapısında.
key-loader.ts Kaynak Kod Analizi
Hata, src/loader/key-loader.ts dosyasındaki loadKeyHTTP() metodunun onSuccess callback'inde oluşuyor.
Adım 1: Key Yükleme Başlatılır
// key-loader.ts - loadKeyHTTP() metodu (v1.4.12)
private loadKeyHTTP(keyInfo: KeyLoaderInfo, frag: Fragment): Promise<KeyLoadedData> {
const { decryptdata } = keyInfo; // decryptdata referansını al
const uri = decryptdata.uri; // key URI'si
return new Promise((resolve, reject) => {
// ... loader kurulumu ...
this.loader.load(context, loaderConfig, {
onSuccess: (response, stats, ctx, networkDetails) => {
// ---- HATA BURADA OLUŞUYOR ----
Adım 2: Referans Kontrolü (Hatanın Kaynağı)
// onSuccess callback - XHR başarılı dönünce çağrılır
onSuccess: (response, stats, context, networkDetails) => {
const { frag, keyInfo } = context;
// !!!!! SORUNLU KONTROL !!!!!
if (!frag.decryptdata || keyInfo !== this.keyUriToKeyInfo[uri]) {
return reject(
this.createKeyLoadError(
frag,
ErrorDetails.KEY_LOAD_ERROR,
new Error('after key load, decryptdata unset or changed'),
networkDetails,
),
);
}
// Başarılı durumda: key verisini ata
keyInfo.decryptdata.key = new Uint8Array(response.data);
frag.decryptdata.key = keyInfo.decryptdata.key;
resolve({ frag, keyInfo });
}
Hatanın Tetiklenme Akışı
keyUriToKeyInfo["enc.bin?token=X"] = keyInfo_A olarak kaydedilir.
enc.bin?token=X URI'sini kullanıyor (farklı IV ile). Parse sırasında keyUriToKeyInfo["enc.bin?token=X"] = keyInfo_B (yeni nesne!) ile overwrite edilir.
keyInfo_A referansını tutuyor, ama map'te artık keyInfo_B var. keyInfo_A !== keyInfo_B --> HATA!
İkincil Sorun: LevelKey.getDecryptData() Yeni Nesne Oluşturması
// level-key.ts - getDecryptData() metodu
getDecryptData(sn: number | 'initSegment'): LevelKey | null {
// AES-128 için: IV yoksa segment numarasından üretilir
if (method === 'AES-128' && this.uri && !this.iv) {
// IV = segment numarasından oluşturulur
const iv = createInitializationVector(sn);
// !!!!! YENİ NESNE OLUŞTURULUYOR !!!!!
const decryptdata = new LevelKey(
this.method, this.uri, 'identity',
this.keyFormatVersions, iv
);
return decryptdata; // this DEĞİL, yeni nesne!
}
// Diğer durumlar: this döner (aynı referans)
return this;
}
Neden Önemli: AES-128 ile IV'siz playlist'lerde, her frag.decryptdata erişiminde getDecryptData() yeni bir LevelKey nesnesi oluşturur. Fragment sınıfındaki getter bunu cache'ler (_decryptdata), ama fragment nesnesi değiştirilir veya yeniden oluşturulursa, referans eşitliği bozulabilir.
Fragment.decryptdata Getter (Lazy Initialization)
// fragment.ts - decryptdata getter
get decryptdata(): LevelKey | null {
const { levelkeys } = this;
if (!levelkeys || levelkeys.NONE) {
return null; // Şifreleme yok
}
if (levelkeys.identity) {
if (!this._decryptdata) {
// İLK erişimde: getDecryptData() çağrılır → yeni nesne oluşur
this._decryptdata = levelkeys.identity.getDecryptData(this.sn);
}
}
return this._decryptdata; // Cache'lenmiş referans
}
Cache mekanizması: _decryptdata bir kez set edilince aynı referans döner. Ancak fragment nesnesi yeniden oluşturulursa (level switch gibi), _decryptdata = null ile başlar ve getDecryptData() yeni nesne oluşturur. Bu yeni nesne, key-loader'daki map'teki keyInfo'nun decryptdata alanı ile aynı referans olmaz.
Bilinen Çözümler ve Workaround'lar
HLS.js'nin resmi bir fix'i yok. Aşağıdaki yöntemler topluluk ve kendi uygulamamızdan derlendi.
UYGULANDI 1. ABR Startup Lock (En Etkili)
Şarkı başlarken ABR'ı level 0'da kilitle, ilk key yüklenene kadar level switch yapma.
// player-core.js satır ~5058
this.hls.autoLevelCapping = 0; // Level 0'da kilitle
this._abrStartupLocked = true; // Lock bayrağı
// KEY_LOADED sonrası:
this.hls.autoLevelCapping = -1; // ABR serbest
this._abrStartupLocked = false;
Etkinlik: ~%90 azalma. Key cache'e girince level switch güvenli, çünkü loadKey() cache'ten okur, yeni XHR açmaz.
UYGULANDI 2. Key Pre-fetch (Browser Cache Isıtma)
Şarkı yüklenmeden önce key URL'sini fetch ederek browser HTTP cache'ini ısıt. HLS.js'nin XHR'ı cache'ten hızlı döner.
// player-core.js satır ~371
async function prefetchHlsKey(streamUrl) {
var keyUrl = constructKeyUrl(streamUrl);
var resp = await fetch(keyUrl);
if (resp.ok) {
var buf = await resp.arrayBuffer();
if (buf.byteLength === 16) {
_hlsKeyCache[keyUrl] = buf;
}
}
}
Key XHR ~50-100ms'de döner (cache'ten). Race condition penceresi küçülür ama tamamen kapanmaz.
UYGULANDI 3. Fatal keyLoadError Recovery (loadSource + startLoad)
Hata oluşursa, manifest'i yeniden yükle. loadSource() tüm decryptdata state'ini sıfırlar.
// player-core.js satır ~5341
if (data.details === 'keyLoadError' && !self._keyStartLoadRecovery) {
self._keyStartLoadRecovery = true;
self.hls.autoLevelCapping = 0; // ABR tekrar kilitle
self._abrStartupLocked = true;
setTimeout(function() {
self.hls.loadSource(self._lastHlsUrl); // State sıfırla
self.hls.startLoad(); // Yeniden başla
}, 100);
}
UYGULANDI 4. HLS Pool: Her Zaman Destroy + Fresh Instance
HLS instance'ları asla reuse etme. Her release'de destroy et ve yeni instance oluştur. Eski instance'ın stale decryptdata'sı temizlenir.
// player-core.js satır ~595-606
// Pool reuse → stale decryptdata → "decryptdata unset or changed"
// Artık her release'de destroy edip fresh instance oluşturuyoruz
try { rawHls.stopLoad(); } catch (e) {}
try { rawHls.detachMedia(); } catch (e) {}
try { rawHls.removeAllListeners(); } catch (e) {}
try { rawHls.destroy(); } catch (e) {}
// Fresh instance
const fresh = new Hls(config);
UYGULANDI 5. startLevel: 0 (Auto Level Devre Dışı)
startLevel: -1 (auto) kullanılırsa, HLS.js tüm level'ları paralel yükler ve key XHR'lar abort edilerek penalty box'a düşer. startLevel: 0 ile tek key XHR garantilenir.
UYGULANMADI 6. HLS.js Kaynak Kod Patch'i (Potansiyel)
key-loader.ts'deki referans kontrolünü URI bazlı eşitlik kontrolüne değiştirmek. Bu, referans yerine anahtar URI'sini karşılaştırır.
// MEVCUT (sorunlu):
if (!frag.decryptdata || keyInfo !== this.keyUriToKeyInfo[uri]) {
// ÖNERİLEN PATCH:
if (!frag.decryptdata || !this.keyUriToKeyInfo[uri]) {
// veya URI bazlı kontrol:
if (!frag.decryptdata || keyInfo.decryptdata.uri !== this.keyUriToKeyInfo[uri]?.decryptdata?.uri) {
Risk: Bu patch, stale key verisiyle şifre çözmeye izin verebilir. AES-128'de yanlış key ile çözme = bozuk ses verisi. Dikkatli test gerekir. Önerilen yaklaşım: Referans kontrolünü kaldırmak yerine, onSuccess'te map'i güncellemek veya key verisini URI bazlı cache'lemek.
Özet ve Öneriler
Sorunun Özü
- HLS.js key-loader referans eşitliği (===) kontrolü kullanıyor
- ABR level switch sırasında
keyUriToKeyInfomap'i overwrite ediliyor - AES-128 IV'siz stream'lerde
getDecryptData()her seferinde yeni nesne oluşturuyor - Bu sorun v1.4.12'den v1.6.15'e kadar tüm versiyonlarda mevcut
- HLS.js geliştiricileri bunu bug değil, güvenlik kontrolü olarak görüyor
Mevcut Korumalar
- ABR startup lock (level 0) -- key cache'e girene kadar
- Key pre-fetch -- browser cache ısıtma
- Fatal recovery -- loadSource() ile state sıfırlama
- HLS Pool -- destroy + fresh instance
- startLevel: 0 -- paralel key XHR'ları önleme
- Non-fatal keyLoadError sessiz geçme
İleriye Dönük Seçenekler
hls.min.js dosyasında onSuccess referans kontrolünü gevşet. Risk: Stale key ile bozuk ses.keyUriToKeyInfo map'inin overwrite yerine merge davranışı göstermesini öner. Topluluk desteği ile kalıcı çözüm.Kaynak Dosyalar ve Referanslar
public/themes/muzibu/js/player/core/player-core.js
public/assets/libs/hls.js@1.4.12/hls.min.js