Mevcut sistemin eksiksiz incelemesi • v2 yeniden yazım hazırlığı
Müzik çalar (player) sistemimiz çalışıyor ama büyümüş ve karmaşıklaşmış. Bir odayı düşünün: her şey çalışıyor ama kablolar birbirine dolanmış, anahtar nerede bilinmiyor.
Neden donuyor? Player'da bir şarkıdan diğerine geçerken, eski şarkının sesi kapatılıp yenisi açılıyor (crossfade). Bu geçiş sırasında aynı anda çok fazla iş yapılıyor: eski ses kaynağı temizleniyor, yeni HLS bağlantısı kuruluyor, ilerleme çubuğu güncelleniyor, geçmiş kaydediliyor. Bunlardan biri takılırsa — donma başlıyor.
Neden geçişlerde sorun var? Crossfade sistemi 7 saniyelik bir "çapraz geçiş" yapıyor. Ama şarkının bitişini takip eden zamanlayıcılar (timer) bazen senkrondan çıkıyor — özellikle telefon arka plana alındığında tarayıcı bu zamanlayıcıları yavaşlatıyor, sonra geri dönünce yığılmış eventler bir anda patladığında sorunlar çıkıyor.
v2'de ne değişecek? Aynı özellikler kalacak ama kablolar düzgün döşenecek. Her parça (ses motoru, kuyruk, geçiş, arayüz) ayrı modüller halinde olacak, birbirlerini sadece "mesaj" ile haberdar edecek. Böylece bir parça sorun çıkarsa diğerlerini etkilemeyecek.
| Framework | Alpine.js (Livewire ile birlikte) |
| Ses Motoru | HLS.js (HLS) + Howler.js (MP3 fallback) |
| Ana Bileşen | muzibuApp() Alpine data component |
| State Yönetimi | Alpine reaktif + Alpine.store proxy |
| SPA Router | Özel (fetch + DOM swap + Alpine reinit) |
| Sıkıştırma | AES-128 şifreli HLS segmentleri |
| Framework | Laravel 11 + Stancl Tenancy |
| Ses İşleme | FFmpeg (2-pass loudnorm) |
| Cache | Redis (song:24h, playlist:1h, premium:5m) |
| CDN | Cloudflare (MP3 30dk blok expires) |
| URL İmzalama | HMAC-SHA256 + token flag encoding |
| Kuyruk | Horizon (hls queue, 900s timeout) |
| # | Dosya | Satır | Boyut | Sorumluluk | Durum |
|---|---|---|---|---|---|
| 1 | player-core.js | ~9.200 | ~380KB | Ana Alpine bileşeni, tüm player state + mantık | Aşırı büyük |
| 2 | play-helpers.js | 980 | 36KB | Genre/playlist/album/radio/song çalıştırıcılar | Duplikasyon |
| 3 | performance-debug.js | 3.675 | 194KB | Dev debug panel (20+ veri yapısı, 6+ interval) | Prod'da yükleniyor |
| 4 | spa-router.js | 688 | 28KB | SPA navigasyon, prefetch cache, Alpine destroy/init | Karmaşık |
| 5 | spot-player.js | 568 | 21KB | Kurumsal anons sistemi, preload, rotasyon | İyi |
| 6 | speed-tester.js | 395 | 13KB | İnternet hız testi: download + latency + jitter | İyi |
| 7 | device-profiler.js | 356 | 11KB | Cihaz parmak izi + backend kayıt + mz_device cookie | Çakışma |
| 8 | buffer-monitor.js | 294 | 9KB | Buffer/donma izleme, otomatik hız testi tetikleme | Kısmen devre dışı |
| 9 | old-device-checker.js | 244 | 8KB | Eski cihaz tespiti (RAM/CPU/browser) | Çakışma |
| 10 | favorites.js | 231 | 9KB | Favori toggle, optimistic UI, 7 model desteği | İyi |
| 11 | auth.js | 178 | 8KB | Login/register form + validasyon | Boş fonksiyonlar! |
| 12 | api.js | 100 | 4KB | Authenticated fetch wrapper, 401 kontrolü | İyi |
| 13 | session.js | 191 | 7KB | Session sonlandırma, logout, storage temizleme | İyi |
| 14 | safe-storage.js | ~50 | 2KB | localStorage wrapper | İyi |
| Dosya | Sorumluluk | Sorun |
|---|---|---|
SongStreamController.php | Stream URL oluşturma, HLS dosya sunma, key sunma, 3-fazlı play tracking | Çok sorumluluk |
SongController.php | Song CRUD, legacy stream, CDN serve, encryption key (duplicate!) | Stream çakışması |
PlaylistController.php | Playlist CRUD, AI oluşturma, seeded shuffle | N+1 query |
QueueRefillController.php | Context-based sonsuz kuyruk, 9 strateji | Debug query overhead |
HomeController.php | Birleşik anasayfa endpoint'i | N+1 query |
HLSService.php | MP3→HLS dönüşüm, AES-128 şifreleme, FFmpeg | Kod tekrarı |
SignedUrlService.php | HMAC-SHA256 URL imzalama, token flag encoding | file_exists hot path |
MuzibuCacheService.php | Redis cache yönetimi | Redis KEYS komutu |
ConvertToHLSJob.php | Kuyruktaki HLS dönüşüm işi | Kod tekrarı |
AddHlsVariantsCommand.php | Toplu variant ekleme komutu | Kod tekrarı |
Bu sorunlar doğrudan kullanıcı deneyimini etkiliyor. Donma ve geçiş sorunlarının kök nedenleri:
Sorun: Tarayıcılar arka plan tab'larda setInterval ve setTimeout'u 1 saniyeye throttle eder. Crossfade sistemi 100ms hassasiyetli zamanlayıcılar kullanıyor. Kullanıcı tab'ı arka plana alıp geri geldiğinde, yığılmış callback'ler bir anda tetikleniyor.
Etki: Şarkı geçişi sırasında iki şarkı aynı anda çalıyor, veya hiç çalmıyor (sessiz donma).
v2 Çözüm: requestAnimationFrame + visibilitychange API ile zamanlama yönetimi. Tab geri geldiğinde anında senkronizasyon.
Sorun: _playLock ve isTransitioning flag'leri async işlemlerde senkronizasyon sorunları yaşıyor. Hızlı ileri/geri basıldığında iki loadAndPlaySong() çağrısı aynı anda çalışabiliyor. İlk çağrı HLS instance alıp yüklerken, ikinci çağrı da farklı bir instance alıyor.
Etki: İki HLS instance aynı audio element'e bağlanıyor → ses çarpışması veya donma.
v2 Çözüm: AbortController pattern + tek async queue: her yeni çağrı öncekini iptal eder.
Sorun: Alpine.js tüm data object'leri bir Proxy ile sarar. HLS.js'in currentLevel setter'ı Alpine proxy üzerinden çağrıldığında, reactive watcher'lar tetikleniyor ve aynı setter 4x çağrılabiliyor.
Mevcut Fix: var _rawHls = HlsPool.acquire() ile closure variable kullanılıyor. Ama bu kalıp tüm kodda tutarlı değil.
v2 Çözüm: Ses motoru tamamen Alpine scope dışına çıkarılmalı. Ayrı modül, Alpine sadece UI state'i yönetmeli.
Sorun: Crossfade sırasında eski şarkının howl.fade() çağrısı hata alırsa (zaten dispose edilmiş, veya audio context kilitlendiyse), fade tamamlanmadan kalıyor. Yeni şarkı başlamış ama eski şarkı hâlâ sessiz ama aktif şekilde kaynak tüketiyor.
Etki: Bellek sızıntısı + CPU kullanımı artışı. Birkaç geçiş sonrası gözle görülür donma.
v2 Çözüm: Fade'e timeout limiti (max 8sn), hata durumunda zorla destroy. Web Audio API doğrudan kullanarak daha güvenilir gain control.
Sorun: HLS yükleme başarısız olduğunda (ağ hatası, key hatası), MP3 fallback'e geçiliyor. Ama bu geçiş sırasında currentStreamType, isPlaying, isSongLoading flag'leri yarış durumuna giriyor. HLS error handler isPlaying=false yaparken, Howler'ın onplay callback'i isPlaying=true yapıyor — hangisi kazanacak?
v2 Çözüm: Finite State Machine (FSM): idle → loading → playing → paused → error → fallback. Her state geçişi kesin kurallarla.
Sorun: İlerleme çubuğu requestAnimationFrame ile güncelleniyor. Ama eski şarkının rAF loop'u bazen temizlenmeden yeni şarkının loop'u başlıyor. Sonuç: birden fazla rAF loop aktif, sürekli DOM güncellemesi → jank ve CPU spike.
v2 Çözüm: Tek bir global rAF loop, token-based invalidation. Yeni şarkıda token değişir → eski loop otomatik durur.
Sorun: Sonraki şarkı için preload başlatılıyor (HLS veya Howler). Crossfade zamanı geldiğinde preloaded instance kullanılıyor. Ama arada kullanıcı manuel "next" basarsa, preloaded instance yanlış şarkıya ait oluyor — ya da aynı instance iki kez kullanılmaya çalışılıyor.
v2 Çözüm: Preload sistemi crossfade'den bağımsız olmalı. Her ikisi de aynı pool'dan instance almalı ama birbirlerinin instance'larını çalmayacak.
Sorun: İki audio element var: #hlsAudio (ana) ve #hlsAudioNext (crossfade). Ama HLS.js attachMedia() bir audio element'e bağlandığında, önceki bağlantıyı otomatik koparıyor. Crossfade sırasında iki HLS instance iki farklı audio element'e bağlı olmalı — ama bazen ikisi de aynı element'e bağlanıyor.
v2 Çözüm: AudioContext + GainNode tabanlı çıktı yönetimi. Fiziksel audio element değil, programatik gain control ile crossfade.
Tek bir dosyada: ses motoru, kuyruk yönetimi, crossfade, UI state, hata yönetimi, preload, watchdog, keyboard shortcuts, history tracking, MediaSession API... Hepsi iç içe.
~70 state değişkeni aynı Alpine data scope'unda. Herhangi biri değiştiğinde Alpine tüm watcher'ları kontrol ediyor.
v2: 8-10 ayrı modül dosyası, her biri tek sorumluluk.
3.675 satırlık debug paneli, 20+ veri yapısı, 6+ setInterval, 20+ event listener. Conditional loading var ama dosyanın kendisi her kullanıcıya indirilip parse ediliyor.
v2: Lazy load: sadece ?debug=1 query'si veya console komutuyla dinamik import.
session.js, spa-router.js, api.js içinde this.isPlaying, this.isLoading gibi çağrılar var ama bu dosyaların kendi context'inde bu property'ler yok. player-core'un .call(this, ...) ile çağırmasına bağımlı. Kırılgan (fragile) bir kalıp.
v2: Event bus veya shared state modülü. Her dosya kendi scope'unda çalışacak.
Player ile dış dünya arasında 4 farklı yol: (1) doğrudan Alpine method çağrısı, (2) $dispatch custom event, (3) $store.player proxy, (4) window.playPlaylist() global fonksiyon. Hangisinin ne zaman kullanılacağı belirsiz.
v2: Tek bir EventBus pattern: PlayerBus.emit('play', {songId})
handleLogin(), handleRegister(), handleLogout() tamamen boş implementasyon. Kullanıcı bu formlara bir şey girip gönderdiğinde hiçbir şey olmuyor.
keyboard.js dosyası app.blade.php'de yorum satırına alınmış. Klavye kısayolları sayfası (Space, K, N, P, vb.) gösteriliyor ama hiçbiri çalışmıyor.
Tüm state aynı Alpine data'da. Herhangi biri değiştiğinde Alpine tüm DOM binding'leri kontrol ediyor. Crossfade sırasında volume, progress, isPlaying, currentTime... hepsi hızla değişiyor → UI jank.
Progress tracking için rAF, crossfade fade için setInterval, watchdog için setInterval, session polling için setInterval. Toplamda 8-10 aktif zamanlayıcı. Hepsi UI thread'inde.
22+ ayrı JS dosyası sırayla yükleniyor. Bazıları defer, bazıları senkron. Toplam ~550KB JavaScript. Bundle/code-split yapılmamış.
device-profiler.js, old-device-checker.js, performance-debug.js — üçü de aynı UA regex'leri ve RAM/CPU tespitini ayrı ayrı yapıyor.
MIN 15 şarkı dolumu 3 kez, addToQueue switch/case 2 kez, premium check 6 kez kopyalanmış. ~300+ satır tekrar eden kod.
buffer-monitor (firstVisit + connectionTest), spa-router (viewport + hover prefetch), spot-player (polling) — ölü/devre dışı bırakılmış kod hâlâ dosyalarda.
| Çakışma/Duplikasyon | Nerede? | Detay | Ciddiyet |
|---|---|---|---|
| İki Stream Controller | SongController::stream() vs SongStreamController::stream() |
İkisi de stream endpoint'i. Birincisi basit path döndürüyor, ikincisi full auth+imza+şifreleme. Hangisi kullanılacak belirsiz. | KRİTİK |
| İki Key Serve Endpoint | SongController::serveEncryptionKey() vs SongStreamController::serveKey() |
İkisi de AES-128 key sunuyor. Farklı imza doğrulama mantıkları. Biri 24h Redis cache yapıyor, diğeri yapmıyor. | KRİTİK |
| loudnorm 3x Duplikasyon | HLSService, ConvertToHLSJob, AddHlsVariantsCommand |
buildTwoPassLoudnormFilter() ve parseLoudnormJson() 3 ayrı dosyada tamamen aynı implementasyon. |
YÜKSEK |
| seededShuffle 2x Duplikasyon | PlaylistController ve QueueRefillController |
Aynı Fisher-Yates seeded shuffle implementasyonu 2 controller'da kopyalanmış. | ORTA |
| Cihaz Tespiti 3x Duplikasyon | device-profiler.js, old-device-checker.js, performance-debug.js |
Aynı UA regex'leri, RAM kontrolü, CPU core sayısı tespiti 3 ayrı dosyada. | YÜKSEK |
| formatSongs Pattern Tekrarı | Birden fazla controller | Song verisi format dönüşümü (cover URL, artist adı, süre) her controller'da tekrar implementasyon. | ORTA |
| 3 URL İmza Formatı | SongController::serveAudioCdn() |
Geriye uyumluluk için 3 farklı HMAC signature formatı destekleniyor. En eski kalite parametresiz. | ORTA |
| Event İletişim 4 Yol | Alpine method, $dispatch, $store.player proxy, window.playX() | Player'a erişim için 4 farklı mekanizma. Bazen fallback zincirleri: window.playPlaylist ? ... : $store.player.playPlaylist() |
YÜKSEK |
| Dead Route | MuzibuServiceProvider satır 338 |
muzibu.songs.audio-serve rotası SongController::serveAudio()'yu işaret ediyor ama bu method yok. |
YÜKSEK |
v2'de yeniden yazılsa bile korunması gereken başarılı tasarımlar:
HLS instance'ları yeniden kullanarak GC baskısını azaltıyor. Havuz boyutu sınırlı, FIFO mantığıyla çalışıyor.
AES-128 key'leri cache'leyerek aynı key için tekrar XHR yapmıyor. queueMicrotask ile async cache serve.
3 denemeden sonra şarkıyı blacklist'e alıyor. Aynı hatalı şarkıda sonsuz döngüyü engelliyor.
3 anomali algılama: stall (ses durma), duration jump, progress freeze. Otomatik recovery.
userId.s/u/l/m/h formatıyla soft/level bilgisini URL token'ına gömme. Ekstra query parametresi gerektirmiyor.
MP3 URL'lerinde expires'ı 30dk'lık bloklara yuvarlayarak aynı zaman dilimindeki tüm kullanıcılara aynı URL veriyor → Cloudflare cache hit oranı yüksek.
9 farklı bağlam stratejisi (genre, album, playlist, artist, sector, radio, favorites, popular, recent). Bağlam tükendiğinde otomatik geçiş.
track-start (başlangıç) → track-hit (30sn sonra sayım) → track-end (bitiş istatistik). Doğru dinleme verisi.
FFmpeg ile ölçüm + uygulama. -16 LUFS hedefi tüm şarkıları aynı ses seviyesinde standartlaştırıyor.
Oluşturulan tüm Blob URL'leri takip edip temizleme. Memory leak önleme.
MuzibuCacheService — invalidatePopularSongs(), invalidateFeaturedPlaylists(), flushAll(), getCacheStats()
KEYS komutu O(n) karmaşıklığında ve Redis'i bloklar. Production'da binlerce key ile ciddi yavaşlama yapabilir. SCAN kullanılmalı.
1. PlaylistController::index() — Her playlist için songs()->count() ayrı query
2. HomeController::formatPlaylist() — Her playlist için songs()->count() ayrı query
3. HomeController::formatAlbum() — Her albüm için songs()->count() ayrı query
Çözüm: withCount('songs') kullanılmalı veya songs_count kolonu cache'lenmeli.
MuzibuCacheService::getPlaylist() — bir playlist'in TÜM şarkılarını tüm ilişkileriyle (album.artist) Redis'e kaydediyor. 5.000 şarkılık bir playlist'te bu devasa bir Redis bellek tüketimi demek.
SignedUrlService::generateHlsUrl() her stream isteğinde master.m3u8 dosyasının varlığını disk I/O ile kontrol ediyor. Bu, her şarkı çalma isteğinde gereksiz bir disk erişimi.
2-pass loudnorm (ölçüm + encode) × (high + ultralow + low + mid + mp3_128) = 10 FFmpeg process. 30K şarkı batch'inde = 300K FFmpeg çağrısı.
POST /api/muzibu/queue/refill sadece throttle middleware'i var, auth yok. Herkes şarkı listesi çekebilir.
QueueRefillController — debug açıklama metni için her refill isteğinde ekstra DB sorguları yapıyor (Song::count, Playlist::find, Genre::find). Client bu veriyi kullanmıyor bile.
resolveAudioFormat() — corporate hesaplarda her stream isteğinde users.count() sorgusu çalıştırılıyor. Cache'lenmeli.
Mevcut: HLS.js ve Howler.js Alpine proxy'si içinde → setter cascade.
v2: AudioEngine sınıfı tamamen bağımsız. Alpine sadece EventBus.on('stateChange') dinleyerek UI günceller.
Mevcut: 5+ boolean flag (isPlaying, isLoading, isSeeking, isTransitioning, isCrossfading)
v2: Tek state enum: idle, loading, buffering, playing, paused, crossfading, error, recovering. Geçersiz state kombinasyonları imkansız.
Mevcut: Concurrent guard flag — ama async gap'lerde yarış durumu.
v2: Her play() çağrısı yeni bir AbortController oluşturur, öncekini iptal eder. Tüm fetch/timeout'lar abort signal'a bağlı.
Mevcut: Howler volume fade + setInterval → tab throttle sorunu.
v2: AudioContext.createGain() + gainNode.gain.linearRampToValueAtTime(). Tarayıcı seviyesinde zamanlama, throttle'dan etkilenmez.
Mevcut: Her bileşen kendi rAF/setInterval'ını yönetiyor → zombie loop'lar.
v2: Tek Ticker sınıfı tüm periyodik işleri yönetir. Token-based — yeni şarkıda token değişir, eski callback'ler otomatik geçersiz.
Mevcut: Tab arka plana alınca zamanlayıcılar bozuluyor.
v2: visibilitychange event'inde: hidden → tüm timer'ları duraklat, visible → durum senkronize et, crossfade varsa anında tamamla.
v2 geçişi için iş paketleri, öncelik sırasına göre:
| Öncelik | İş Paketi | Etkilediği Sorun | Etki | Efor |
|---|---|---|---|---|
| P0 | AudioEngine modülü — Ses motorunu Alpine dışına çıkar | K3 (Proxy cascade), K5 (State tutarsızlığı), K6 (Zombie rAF) | Donma sorunlarının %60'ını çözer | Büyük |
| P0 | FSM (State Machine) — Boolean flag'ler yerine tek state enum | K2 (Race condition), K5 (State tutarsızlığı) | Geçersiz state kombinasyonlarını imkansız kılar | Orta |
| P0 | Crossfade v2 — Web Audio API GainNode + AbortController | K1 (Tab throttle), K4 (Howler fade hatası), K7 (Preload çakışması), K8 (Audio element) | Geçiş sorunlarının %90'ını çözer | Büyük |
| P1 | EventBus — 4 event mekanizmasını teke indir | M4 (Event karmaşası), M3 (this context) | Kod karmaşıklığı %40 azalır | Orta |
| P1 | player-core.js bölme — 9.200 satırı 8-10 modüle ayır | M1 (God file), P1 (70 reaktif değişken) | Bakım kolaylığı, debug kolaylığı | Büyük |
| P1 | Tek rAF Loop + Visibility API | K1 (Tab throttle), K6 (Zombie rAF), P2 (Çoklu loop) | CPU kullanımı %30 azalır | Küçük |
| P2 | Backend N+1 fix — withCount kullanımı | B2 (N+1 query) | API response süresi %50 iyileşir | Küçük |
| P2 | Redis KEYS → SCAN | B1 (Redis KEYS bloklama) | Redis bloklama riski ortadan kalkar | Küçük |
| P2 | Controller çakışma temizliği — Stream + Key deduplikasyonu | Çakışma #1, #2 | Bakım karmaşıklığı azalır | Orta |
| P2 | performance-debug.js lazy load | M2 (194KB production yükü) | Sayfa yüklenme hızı iyileşir | Küçük |
| P3 | Backend loudnorm/shuffle trait'e taşı | Çakışma #3, #4 | Kod tekrarı ortadan kalkar | Küçük |
| P3 | Device detection birleştirme | P4 (3x cihaz tespiti), Çakışma #5 | 3 dosya → 1 dosya | Küçük |
| P3 | play-helpers.js refactor | P5 (Duplikasyon) | ~300 satır ölü kod temizlenir | Küçük |
| P3 | Keyboard shortcuts yeniden etkinleştir | M6 (Devre dışı) | Kullanıcı deneyimi iyileşir | Küçük |
| P3 | auth.js boş fonksiyonları doldur veya kaldır | M5 (Boş fonksiyonlar) | Hata kaynağı ortadan kalkar | Küçük |
Tümünü bir anda yazmak yerine kademeli geçiş önerilir:
Faz 1: Altyapı
EventBus + FSM + AudioEngine modülünü yaz. Mevcut player-core'un yanında paralel çalışır. Feature flag ile açılır.
Faz 2: Geçiş
player-core'un fonksiyonları tek tek yeni modüllere taşınır. Her taşıma sonrası A/B test.
Faz 3: Temizlik
Eski kod silinir, backend duplikasyonlar temizlenir, performance-debug lazy load yapılır.
| muzibu:songChanged | Şarkı değiştiğinde (player-core → tüm dinleyiciler) |
| muzibu:playerStateChanged | Play/pause değiştiğinde |
| muzibu:queueUpdated | Kuyruk değiştiğinde |
| muzibu:streamTypeChanged | HLS↔MP3 format geçişinde |
| muzibu:crossfadeStart | Crossfade başladığında |
| muzibu:crossfadeEnd | Crossfade bittiğinde |
| muzibu:error | Hata oluştuğunda |
| play-song | Dış bileşenlerden şarkı çalma talebi |
| play-all-songs | Toplu çalma talebi (playlist/album/genre) |