HLS keyLoadError Analiz, Düzeltme & Production Rehberi

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

3 Kritik Bug Test'te Düzeltildi Production Bekliyor (30.000 şarkı)

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
  • Test sunucusundaki 66 şarkı düzeltildi
  • Production'daki ~30.000 şarkı henüz düzeltilmedi!

2. Bulunan 3 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 segmentleri storage URL'sine yönlendiriliyor
// 3. High key URI'sine &level=high ekleniyor (race condition önlemi)

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ı, ilk başlangıç
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 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 (3 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/Muzibu/App/Http/Controllers/Api/SongStreamController.php → High filtre kaldırıldı

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)

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)

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ı + query/segment/level=high eklendi
storage/.../hls/*/ 66 şarkı yeniden encode (test) — 30.000 şarkı bekliyor (production)

7. Test Sonuçları (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)

22 Şubat 2026 • Muzibu.com.tr