HLS keyLoadError Analiz, Düzeltme & Production Rehberi

22 Şubat 2026 • v3 • Muzibu.com.tr

5 Bug Bulundu & Düzeltildi Sorun Henüz Tam Çözülmedi Production Bekliyor (30.000 şarkı)

Durum (v3): 5 bug bulunup düzeltildi, 66 şarkı yeniden encode edildi, ABA testleri yapıldı. Ancak sorun henüz tam olarak çözülmüş değil — testler devam ediyor. Bu rapor yapılan tüm işlemlerin kaydıdır.

1. Sorun Neydi? (Basit Anlatım)

Dinleyiciler Ne Yaşıyordu?

Müzik dinlerken şarkılar en düşük kalitede çalıyordu. İnternet hızı ne kadar iyi olursa olsun, herkes maksimum 64kbps (telefon kalitesinin bile altında) ses duyuyordu. Orijinal kalite hiç çalmıyordu.

Neden?

Şarkıları korumak için kullandığımız şifreleme sisteminde bir hata vardı. Orijinal kaliteli dosyaların "şifre kombinasyonu" (IV değeri) sıfır olarak yazılmıştı. Bu yüzden orijinal kalite çözülemiyordu ve biz de onu tamamen kapattık. Sonuç: herkes düşük kalitede dinliyordu.

Ne Değişti?

  • Artık 3 kalite seviyesi aktif: düşük (32kbps), orta (64kbps), orijinal (~180-280kbps)
  • İnternet hızına göre otomatik kalite seçimi çalışıyor
  • Player artık direkt orijinal kaliteden başlıyor (ultralow yerine)
  • Test sunucusundaki 66 şarkı düzeltildi
  • Production'daki ~30.000 şarkı henüz düzeltilmedi!

2. Bulunan 5 Bug & Düzeltmeleri

Bug 1: IV=0x00 (Sıfır Initialization Vector)

Dosya: Modules/Muzibu/App/Jobs/ConvertToHLSJob.php:95-98

// ESKİ KOD (HATALI):
$keyInfoContent = $keyUri . "\n" . $keyPath . "\n";
// 3. satır (IV) yazılmamış! FFmpeg IV=0x0000...0000 kullanıyor
// YENİ KOD (DÜZELTİLMİŞ):
$iv = bin2hex(random_bytes(16));
$keyInfoContent = $keyUri . "\n" . $keyPath . "\n" . $iv;
// Artık her şarkı benzersiz IV alıyor

Neden önemli: AES-128'de IV her dosya için benzersiz olmalı. Sıfır IV hem güvenlik açığı hem de HLS.js'te "decryptdata unset or changed" hatasının ana nedeni.

Bug 2: Yanlış Bitrate Hesabı (669kbps Hayalet Değer)

Dosya: app/Services/Muzibu/HLSService.php:509

// ESKİ KOD (HATALI):
$highBitrate = round($totalSize / ($segCount * self::CHUNK_DURATION) * 8);
// CHUNK_DURATION=4sn ama orijinal segmentler 10sn → hesap 2.5x şişik
// 268kbps yerine 669kbps yazıyordu
// YENİ KOD (DÜZELTİLMİŞ):
$playlistContent = file_get_contents($highPlaylist);
preg_match_all('/#EXTINF:([\d.]+),/', $playlistContent, $matches);
foreach ($matches[1] as $dur) { $totalDuration += (float) $dur; }
$highBitrate = round($totalSize * 8 / $totalDuration);
// Playlist'ten gerçek süre okunuyor

Bug 3: Orijinal Kalite Filtrelenmişti

Dosya: Modules/Muzibu/App/Http/Controllers/Api/SongStreamController.php:746-755

// ESKİ KOD: master.m3u8'den orijinal kaliteyi tamamen siliyordu
$content = preg_replace(
    '/^#EXT-X-STREAM-INF:[^\n]*\nplaylist\.m3u8\n?/m',
    '', $content  // HIGH SİLİNİYORDU!
);
// YENİ KOD: Filtre kaldırıldı, yerine:
// 1. High playlist'e query parametreleri ekleniyor
// 2. High key URI'sine &level=high ekleniyor (race condition önlemi)
// 3. Segmentler relative path olarak kalıyor

Bug 4: Segment 403 Forbidden (Storage Direkt Erişim)

v3'te eklendi

Dosya: Modules/Muzibu/App/Http/Controllers/Api/SongStreamController.php

// ESKİ KOD (HATALI):
// Segment URL'leri /storage/tenant1001/... şeklinde rewrite ediliyordu
// AMA storage klasörü direkt erişime kapalı → 403 Forbidden
$line = str_replace('segment-', $storageUrl . '/segment-', $line);
// YENİ KOD (DÜZELTİLMİŞ):
// Segment URL'leri relative kalıyor — HLS.js playlist URL'sine göre çözer
// Segmentler /hls/muzibu/songs/{id}/segment-XXX.ts üzerinden sunuluyor
// Storage direkt erişimi 403 döndüğü için rewrite YAPMIYORUZ

Basit Anlatım

Şarkı parçacıkları (segment) doğrudan storage klasöründen istenince sunucu "Erişim Yasak" (403) diyordu. Çözüm: Parçacık URL'lerini olduğu gibi bırakıyoruz, HLS.js kendisi Laravel üzerinden (/hls/ endpoint) erişiyor.

Bug 5: ABR Soğuk Başlangıç (startLevel:0 → startLevel:2)

v3'te eklendi

Dosya: public/themes/muzibu/js/player/core/player-core.js

// ESKİ KOD:
startLevel: 0,                    // En düşük kaliteden başla (ultralow 32kbps)
abrEwmaDefaultEstimate: 64000,    // 64kbps bant tahmini
this.hls.autoLevelCapping = 0;    // Hardcoded ultralow lock
// YENİ KOD:
startLevel: 2,                    // Direkt orijinal (high) kaliteden başla
abrEwmaDefaultEstimate: 250000,   // 250kbps bant tahmini (orijinale uygun)
var _startLvl = this.hls.config.startLevel || 2;
this.hls.autoLevelCapping = _startLvl; // Dinamik start level

Basit Anlatım

Player eskiden en kötü kaliteden başlayıp yavaş yavaş yükseltiyordu. İlk birkaç saniye herkes kötü ses duyuyordu. Şimdi direkt orijinal kaliteden başlıyor — orijinal ~200kbps olduğu için herhangi bir internet bağlantısı kaldırır. Eğer bağlantı çok kötüyse ABR otomatik düşürür.

Güncellenen 6 Nokta

Konum Eski Yeni
HLS_SHARED_CONFIGstartLevel: 0startLevel: 2
HLS_SHARED_CONFIGabrEwmaDefaultEstimate: 64000250000
playHlsStream (ana)autoLevelCapping = 0config.startLevel || 2
CrossfadeautoLevelCapping = 0config.startLevel || 2
RecoveryautoLevelCapping = 0config.startLevel || 2
PreloadautoLevelCapping = 0config.startLevel || 2
ReconnectautoLevelCapping = 0config.startLevel || 2

Not: Düşük profilli cihazlar (eski telefon/yavaş bağlantı) hala startLevel:0 kullanıyor — getAdaptiveHlsConfig() ile override ediliyor.

3. HLS Encode Sistemi — Nasıl Çalışıyor?

3.1 Dosya Yapısı (Her Şarkı İçin)

storage/tenant{ID}/app/public/muzibu/hls/{song_id}/
├── enc.bin                  ← 16 byte AES-128 key (TÜM kaliteler paylaşır)
├── enc.keyinfo              ← FFmpeg keyinfo (key URL + key path + IV)
├── master.m3u8              ← Ana playlist (3 kaliteyi listeler)
├── playlist.m3u8            ← Orijinal kalite playlist
├── segment-000.ts           ← Orijinal kalite segmentleri (6sn)
├── segment-001.ts
├── ...
├── ultralow/
│   ├── enc.keyinfo          ← Farklı IV!
│   ├── playlist.m3u8
│   ├── segment-000.ts       ← 32kbps segmentler (4sn)
│   └── ...
└── low/
    ├── enc.keyinfo          ← Farklı IV!
    ├── playlist.m3u8
    └── segment-000.ts       ← 64kbps segmentler (4sn)

3.2 enc.keyinfo Dosya Formatı (3 Satır)

Satır 1: Key URI  → https://domain.com/storage/tenant{ID}/muzibu/hls/{song_id}/enc.bin
Satır 2: Key Path → /var/www/.../storage/tenant{ID}/app/public/muzibu/hls/{song_id}/enc.bin
Satır 3: IV       → a1b2c3d4e5f6789012345678abcdef01  (32 hex char = 16 byte)

Bug: Satır 3 yazılmamıştı! FFmpeg boş satırı görünce IV=0x0000...0000 kullanıyordu.

3.3 İki Ayrı Encode Sistemi Vardı (Sorunun Kaynağı)

Sistem Dosya Ne Üretiyor IV Segment
ConvertToHLSJob Modules/.../Jobs/ConvertToHLSJob.php Orijinal (high) Eksikti! → 0x00 10sn → 6sn
HLSService app/Services/Muzibu/HLSService.php Ultralow + Low random_bytes(16) ✓ 4sn ✓

ConvertToHLSJob Horizon queue üzerinden çalışıyor (ilk upload). HLSService ise variant'ları sonradan ekliyor. İkisi farklı davranıyordu.

3.4 Kalite Seviyeleri

Kalite Bitrate Sample Rate Kanal Segment Kullanım
ultralow 32 kbps 22050 Hz Mono 4 sn Çok kötü bağlantı
low 64 kbps 22050 Hz Mono 4 sn Orta bağlantı
high (orijinal) ~180-280 kbps (MP3'e göre) 48000 Hz Stereo 6 sn Normal/iyi bağlantı

3.5 Segment Erişim Yolu

Player → master.m3u8 ister     → /hls/muzibu/songs/{id}/master.m3u8    → SongStreamController
Player → playlist.m3u8 ister   → /hls/muzibu/songs/{id}/playlist.m3u8  → SongStreamController
Player → segment-000.ts ister  → /hls/muzibu/songs/{id}/segment-000.ts → SongStreamController
Player → enc.bin key ister     → /hls-key/muzibu/songs/{id}?...        → SongController

Kritik: Tüm dosyalar Laravel üzerinden sunuluyor. /storage/ klasörüne direkt erişim 403 döner — bu yüzden segment URL'leri relative kalmalı!

3.6 FFmpeg Encode Komutu (Orijinal Kalite)

ffmpeg -i {MP3_PATH} \
    -map 0:a \
    -c:a aac \
    -b:a {TARGET_BITRATE} \
    -profile:a aac_low \
    -ar 48000 \
    -ac 2 \
    -vn \
    -hls_key_info_file {HLS_DIR}/enc.keyinfo \
    -start_number 0 \
    -hls_time 6 \
    -hls_list_size 0 \
    -hls_segment_filename {HLS_DIR}/segment-%03d.ts \
    -f hls \
    {HLS_DIR}/playlist.m3u8 -y

Bitrate Mapping (Orijinal MP3 → AAC Hedef)

MP3 ≤ 128kbps → AAC 128k | MP3 ≤ 160kbps → AAC 160k | MP3 ≤ 192kbps → AAC 192k
MP3 ≤ 256kbps → AAC 256k | MP3 ≤ 320kbps → AAC 320k | MP3 > 320kbps → orijinal

4. Production Re-Encode Rehberi (~30.000 Şarkı)

Ana sunucuda (muzibu.com) uygulanacak adımlar. Test sunucusunda (mztest) 66 şarkıda başarıyla test edildi.

1 Kod Değişikliklerini Uygula (4 Dosya)

Önce kodu düzelt ki yeni encode edilen şarkılar doğru olsun:

Modules/Muzibu/App/Jobs/ConvertToHLSJob.php → IV eklendi + hls_time 10→6
app/Services/Muzibu/HLSService.php → Bitrate hesabı düzeltildi
Modules/.../SongStreamController.php → High filtre kaldırıldı + segment rewrite kaldırıldı
public/themes/muzibu/js/player/core/player-core.js → startLevel:2 + dinamik autoLevelCapping

2 Şarkı Listesini Hazırla

Tinker ile HLS'i olan tüm şarkıları dosyaya yaz:

php artisan tinker --execute="
\$tenant = App\Models\Tenant::find(1001);
tenancy()->initialize(\$tenant);
\$songs = DB::connection('tenant')->table('muzibu_songs')
    ->whereNotNull('hls_path')->get(['song_id','file_path']);
foreach(\$songs as \$s) {
    echo \$s->song_id . '|' . \$s->file_path . PHP_EOL;
}
" > /tmp/hls-songs.txt

3 Re-Encode Script'i (Sadece Orijinal Kalite)

Not: Bu script sadece orijinal (high) kaliteyi yeniden encode eder. Ultralow ve low'a dokunmaz. Zaten doğru IV'si olan şarkıları atlar.

#!/bin/bash
# re-encode-high.sh — Orijinal kaliteyi IV + 6sn segment ile yeniden encode et

HLS_BASE="/path/to/storage/tenant{ID}/app/public/muzibu/hls"
SONGS_BASE="/path/to/storage/tenant{ID}/app/public/muzibu/songs"
DOMAIN="muzibu.com"
TENANT_ID="1001"
SONG_LIST="/tmp/hls-songs.txt"

SUCCESS=0; FAIL=0; SKIP=0; TOTAL=0

for dir in "$HLS_BASE"/*/; do
    SONG_ID=$(basename "$dir")
    PL="$dir/playlist.m3u8"

    # Sadece IV=0x00 olanları işle (zaten doğru olanları atla)
    [ ! -f "$PL" ] && continue
    grep -q "IV=0x00000000" "$PL" || { SKIP=$((SKIP+1)); continue; }

    TOTAL=$((TOTAL + 1))

    # MP3 dosyasını bul
    MP3_FILE=$(grep "^${SONG_ID}|" "$SONG_LIST" | cut -d'|' -f2)
    MP3_PATH="$SONGS_BASE/$MP3_FILE"
    [ -z "$MP3_FILE" ] || [ ! -f "$MP3_PATH" ] && { FAIL=$((FAIL+1)); continue; }

    KEY_PATH="$dir/enc.bin"
    [ ! -f "$KEY_PATH" ] && { FAIL=$((FAIL+1)); continue; }

    # Eski orijinal segmentleri sil
    rm -f "$dir"/segment-*.ts "$dir/playlist.m3u8"

    # Yeni IV oluştur ve enc.keyinfo'yu güncelle
    NEW_IV=$(openssl rand -hex 16)
    KEY_URI="https://$DOMAIN/storage/tenant$TENANT_ID/muzibu/hls/$SONG_ID/enc.bin"
    printf '%s\n%s\n%s' "$KEY_URI" "$KEY_PATH" "$NEW_IV" > "$dir/enc.keyinfo"

    # Orijinal bitrate'i tespit et
    BR=$(/usr/bin/ffprobe -v quiet -show_entries format=bit_rate \
         -of csv=p=0 "$MP3_PATH" 2>/dev/null)
    BRK=$((BR / 1000))

    # Bitrate mapping
    if [ "$BRK" -le 128 ]; then T="128k"
    elif [ "$BRK" -le 160 ]; then T="160k"
    elif [ "$BRK" -le 192 ]; then T="192k"
    elif [ "$BRK" -le 256 ]; then T="256k"
    elif [ "$BRK" -le 320 ]; then T="320k"
    else T="${BRK}k"; fi

    # FFmpeg ile yeniden encode (6sn segment, IV ile)
    /usr/bin/ffmpeg -i "$MP3_PATH" \
        -map 0:a -c:a aac -b:a "$T" -profile:a aac_low \
        -ar 48000 -ac 2 -vn \
        -hls_key_info_file "$dir/enc.keyinfo" \
        -start_number 0 -hls_time 6 -hls_list_size 0 \
        -hls_segment_filename "$dir/segment-%03d.ts" \
        -f hls "$dir/playlist.m3u8" -y 2>/dev/null

    [ $? -ne 0 ] && { FAIL=$((FAIL+1)); continue; }

    # Master playlist'i yeniden oluştur (doğru bitrate ile)
    TD=$(grep '#EXTINF:' "$dir/playlist.m3u8" | \
         sed 's/#EXTINF://' | sed 's/,$//' | \
         python3 -c "import sys; print(sum(float(l) for l in sys.stdin))")
    TS=$(du -cb "$dir"/segment-*.ts 2>/dev/null | tail -1 | cut -f1)
    HBW=$(python3 -c "print(int(round($TS * 8 / $TD)))")

    M="$dir/master.m3u8"
    echo "#EXTM3U" > "$M"
    [ -f "$dir/ultralow/playlist.m3u8" ] && \
        printf '#EXT-X-STREAM-INF:BANDWIDTH=32000,CODECS="mp4a.40.2",NAME="ultralow"\nultralow/playlist.m3u8\n' >> "$M"
    [ -f "$dir/low/playlist.m3u8" ] && \
        printf '#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.2",NAME="low"\nlow/playlist.m3u8\n' >> "$M"
    [ -f "$dir/mid/playlist.m3u8" ] && \
        printf '#EXT-X-STREAM-INF:BANDWIDTH=128000,CODECS="mp4a.40.2",NAME="mid"\nmid/playlist.m3u8\n' >> "$M"
    printf "#EXT-X-STREAM-INF:BANDWIDTH=${HBW},CODECS=\"mp4a.40.2\",NAME=\"high\"\nplaylist.m3u8\n" >> "$M"

    SC=$(ls "$dir"/segment-*.ts 2>/dev/null | wc -l)
    echo "[$TOTAL] $SONG_ID — $T ${SC}seg BW=${HBW}"
    SUCCESS=$((SUCCESS + 1))
done

echo ""
echo "Başarılı: $SUCCESS | Atlandı: $SKIP | Başarısız: $FAIL | Toplam: $TOTAL"

4 Çalıştırma ve Süre Tahmini

# Arka planda çalıştır (nohup ile)
nohup bash re-encode-high.sh > /tmp/re-encode.log 2>&1 &

# İlerlemeyi takip et
tail -f /tmp/re-encode.log

# Tamamlanınca doğrula (sıfır IV kalmış mı?)
for dir in $HLS_BASE/*/; do
    PL="$dir/playlist.m3u8"
    [ -f "$PL" ] && grep -q "IV=0x00000000" "$PL" && echo "BOZUK: $(basename $dir)"
done
~5-8 sn
Şarkı başına encode süresi
~40-65 saat
30.000 şarkı toplam (tek çekirdek)

Paralel çalıştırma: GNU parallel veya xargs ile 4 çekirdek kullanılırsa ~10-16 saat'e düşer. Ama CPU yükünü izlemek lazım (canlı sunucu).

5 Doğrulama Kontrolleri

# 1. Sıfır IV kalmış mı?
BROKEN=0; OK=0
for dir in $HLS_BASE/*/; do
    PL="$dir/playlist.m3u8"; [ ! -f "$PL" ] && continue
    grep -q "IV=0x00000000" "$PL" && BROKEN=$((BROKEN+1)) || OK=$((OK+1))
done
echo "OK: $OK | Bozuk: $BROKEN"

# 2. master.m3u8'de high variant var mı?
grep -l "NAME=\"high\"" $HLS_BASE/*/master.m3u8 | wc -l

# 3. Rastgele bir şarkıyı kontrol et
SAMPLE=$(ls -d $HLS_BASE/*/ | shuf -n 1)
echo "=== $(basename $SAMPLE) ==="
grep "EXT-X-KEY" "$SAMPLE/playlist.m3u8"          # IV sıfır değil mi?
cat "$SAMPLE/master.m3u8"                           # 3 kalite var mı?
ls "$SAMPLE"/segment-*.ts | wc -l                   # Segment sayısı

5. Öncesi / Sonrası

Öncesi

  • Ultralow: 32kbps
  • Low: 64kbps
  • Orijinal: KAPALI
  • Max kalite: 64kbps
  • IV: sıfır (güvenlik açığı)
  • Bitrate raporu: 669kbps (yanlış)
  • Segment süresi: 10sn (orijinal)
  • Başlangıç: ultralow (32kbps)
  • Segment erişimi: storage → 403

Sonrası

  • Ultralow: 32kbps
  • Low: 64kbps
  • Orijinal: ~180-280kbps
  • Max kalite: orijinal MP3 kalitesi
  • IV: benzersiz (güvenli)
  • Bitrate raporu: gerçekçi
  • Segment süresi: 6sn (orijinal)
  • Başlangıç: orijinal (high)
  • Segment erişimi: Laravel /hls/ endpoint

6. Değişen Dosyalar

DosyaDeğişiklik
Modules/Muzibu/App/Jobs/ConvertToHLSJob.php IV eklendi (random_bytes) + hls_time 10→6
app/Services/Muzibu/HLSService.php generateMasterPlaylist() bitrate hesabı: EXTINF sürelerinden
Modules/.../SongStreamController.php High filtre kaldırıldı + segment storage rewrite kaldırıldı
public/.../player-core.js startLevel:2 + dinamik autoLevelCapping (7 nokta güncellendi)
storage/.../hls/*/ 66 şarkı yeniden encode (test) — 30.000 şarkı bekliyor (production)

7. Test Sonuçları

7.1 Re-Encode Testi (mztest — 66 Şarkı)

66
Toplam
66
Başarılı
0
Başarısız

Encode sonrası doğrulama: IV doğru: 66 | IV sıfır: 0

Bitrate aralığı: 192k-256k (MP3 orijinaline bağlı)

Gerçek bandwidth: 210-285 kbps (master.m3u8'de doğru raporlanıyor)

7.2 ABA Testleri (Tarayıcı)

BAŞARISIZ ABA Test 1
  • Segment'ler 403 Forbidden dönüyordu (storage direkt erişim)
  • keyLoadError devam ediyordu
  • High kaliteye ulaşılamıyordu

→ Bug 4 (segment 403) bu testte keşfedildi

BAŞARILI ABA Test 2
  • 3/3 kriter karşılandı
  • 403 hatası yok
  • keyLoadError yok
  • High kaliteye ulaşıldı

→ Bug 4 düzeltildikten sonra

BEKLİYOR ABA Test 3 (startLevel:2 testi)
  • startLevel:2 değişikliği yapıldı, henüz test edilmedi
  • Test kriteri: İlk segment orijinal kaliteden mi geliyor?
  • Düşük profilli cihaz desteği doğru çalışıyor mu?

8. Yapılan İşlemler Zaman Çizgisi

v3'te eklendi
Adım 1: Araştırma — Song 34455 analiz edildi, IV=0x00 bulundu, iki encode sistemi keşfedildi
Adım 2: Bug 1-2-3 düzeltildi — IV eklendi, bitrate hesabı düzeltildi, high filtre kaldırıldı
Adım 3: 66 şarkı re-encode — Tüm test şarkıları yeni IV + 6sn segment ile yeniden encode edildi
Adım 4: ABA Test 1 → BAŞARISIZ — Segment 403, keyLoadError devam ediyor
Adım 5: Bug 4 düzeltildi — Segment storage URL rewrite kaldırıldı, relative path'e geçildi
Adım 6: ABA Test 2 → BAŞARILI — 3/3 kriter karşılandı
Adım 7: Bug 5 düzeltildi — startLevel:0→2, orijinal kaliteden başlama, 7 noktada autoLevelCapping güncellendi
Adım 8: Sorun henüz tam çözülmedi — testler devam ediyor
22 Şubat 2026 • Muzibu.com.tr