feat: apply phase1 DX cleanup for private registry

This commit is contained in:
hibna
2026-02-12 18:27:50 +03:00
parent 8a32c5c1b3
commit fcd2a14a05
21 changed files with 281 additions and 279 deletions
+7 -7
View File
@@ -1,27 +1,27 @@
# .npmrc Örnek Dosyası # .npmrc Örnek Dosyası
# Bu dosyayı diğer projelerinize kopyalayın ve .npmrc olarak kaydedin # Bu dosyayı diğer projelerinize kopyalayın ve .npmrc olarak kaydedin
# @alper scope'u için Gitea registry'yi kullan # @source scope'u için private registry kullan
@alper:registry=https://gitea.yourdomain.com/api/packages/your-username/npm/ @source:registry=https://gits.yourdomain.com/api/packages/your-username/npm/
# Authentication token (environment variable kullanımı - ÖNERİLEN) # Authentication token (environment variable kullanımı - ÖNERİLEN)
//gitea.yourdomain.com/api/packages/your-username/npm/:_authToken=${GITEA_TOKEN} //gits.yourdomain.com/api/packages/your-username/npm/:_authToken=${GITS_NPM_TOKEN}
# Alternatif: Doğrudan token (GÜVENLİ DEĞİL - sadece local geliştirme için) # Alternatif: Doğrudan token (GÜVENLİ DEĞİL - sadece local geliştirme için)
# //gitea.yourdomain.com/api/packages/your-username/npm/:_authToken=your-gitea-access-token-here # //gits.yourdomain.com/api/packages/your-username/npm/:_authToken=your-registry-token-here
# Environment variable nasıl ayarlanır: # Environment variable nasıl ayarlanır:
# #
# Linux/Mac: # Linux/Mac:
# export GITEA_TOKEN=your-token-here # export GITS_NPM_TOKEN=your-token-here
# # veya ~/.bashrc veya ~/.zshrc dosyasına ekleyin # # veya ~/.bashrc veya ~/.zshrc dosyasına ekleyin
# #
# Windows (PowerShell): # Windows (PowerShell):
# $env:GITEA_TOKEN="your-token-here" # $env:GITS_NPM_TOKEN="your-token-here"
# # veya sistem environment variables'a ekleyin # # veya sistem environment variables'a ekleyin
# #
# .env dosyası (projede): # .env dosyası (projede):
# GITEA_TOKEN=your-token-here # GITS_NPM_TOKEN=your-token-here
# Not: Bu dosyayı .gitignore'a ekleyin! # Not: Bu dosyayı .gitignore'a ekleyin!
# Asla token'ınızı git'e commit etmeyin. # Asla token'ınızı git'e commit etmeyin.
+65 -62
View File
@@ -1,9 +1,9 @@
# 🎬 @alper/video-player - Tam Dökümantasyon # 🎬 @source/player - Tam Dökümantasyon
**Modern, zengin özellikli ve hafif React video oynatıcı kütüphanesi** **Modern, zengin özellikli ve hafif React video oynatıcı kütüphanesi**
[![npm version](https://img.shields.io/npm/v/@alper/video-player.svg?style=flat-square)](https://www.npmjs.com/package/@alper/video-player) [![npm version](https://img.shields.io/npm/v/@source/player.svg?style=flat-square)](https://www.npmjs.com/package/@source/player)
[![Bundle Size](https://img.shields.io/bundlephobia/minzip/@alper/video-player?style=flat-square)](https://bundlephobia.com/package/@alper/video-player) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/@source/player?style=flat-square)](https://bundlephobia.com/package/@source/player)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue?style=flat-square)](https://www.typescriptlang.org/) [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue?style=flat-square)](https://www.typescriptlang.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
@@ -47,15 +47,15 @@
## 🌟 Genel Bakış ## 🌟 Genel Bakış
`@alper/video-player`, React uygulamaları için özel olarak tasarlanmış, modern bir video oynatıcı kütüphanesidir. HLS streaming, RTMP/FLV desteği, çoklu altyazı ve ses parçaları, kalite değiştirme ve daha fazlası gibi gelişmiş özellikleri içerir. `@source/player`, React uygulamaları için özel olarak tasarlanmış, modern bir video oynatıcı kütüphanesidir. HLS streaming, RTMP/FLV desteği, çoklu altyazı ve ses parçaları, kalite değiştirme ve daha fazlası gibi gelişmiş özellikleri içerir.
### Neden @alper/video-player? ### Neden @source/player?
| Özellik | @alper/video-player | video.js | react-player | plyr | | Özellik | @source/player | video.js | react-player | plyr |
|---------|---------------------|----------|--------------|------| |---------|---------------------|----------|--------------|------|
| **Paket Boyutu (gzipped)** | **~15KB** ✅ | ~500KB ❌ | ~50KB ⚠️ | ~30KB ⚠️ | | **Paket Boyutu (gzipped)** | **~15KB** ✅ | ~500KB ❌ | ~50KB ⚠️ | ~30KB ⚠️ |
| **Runtime Bağımlılıkları** | **0** ✅ | Çok ❌ | Az ⚠️ | Az ⚠️ | | **Runtime Bağımlılıkları** | **0** ✅ | Çok ❌ | Az ⚠️ | Az ⚠️ |
| **React Native** | **Evet** ✅ | Wrapper ⚠️ | **Evet** ✅ | Wrapper ⚠️ | | **React (Web)** | **Evet** ✅ | Wrapper ⚠️ | **Evet** ✅ | Wrapper ⚠️ |
| **TypeScript Native** | **Evet** ✅ | Types ⚠️ | Kısmi ⚠️ | Types ⚠️ | | **TypeScript Native** | **Evet** ✅ | Types ⚠️ | Kısmi ⚠️ | Types ⚠️ |
| **HLS Desteği** | **Evet** ✅ | Evet ✅ | Evet ✅ | Hayır ❌ | | **HLS Desteği** | **Evet** ✅ | Evet ✅ | Evet ✅ | Hayır ❌ |
| **RTMP/FLV Desteği** | **Evet** ✅ | Hayır ❌ | Hayır ❌ | Hayır ❌ | | **RTMP/FLV Desteği** | **Evet** ✅ | Hayır ❌ | Hayır ❌ | Hayır ❌ |
@@ -100,7 +100,7 @@ npm install -g pnpm
### 2. .npmrc Yapılandırması ### 2. .npmrc Yapılandırması
`@alper` scope'lu paketlerin Gitea registry'den çekilmesi için projenizin kök dizininde bir `.npmrc` dosyası oluşturmanız gerekir. `@source` scope'lu paketlerin Gitea registry'den çekilmesi için projenizin kök dizininde bir `.npmrc` dosyası oluşturmanız gerekir.
#### Adım 2.1: .npmrc Dosyası Oluşturma #### Adım 2.1: .npmrc Dosyası Oluşturma
@@ -119,20 +119,20 @@ New-Item -Path .npmrc -ItemType File
`.npmrc` dosyasını aşağıdaki içerikle doldurun: `.npmrc` dosyasını aşağıdaki içerikle doldurun:
```ini ```ini
# @alper scope'u için Gitea registry'yi kullan # @source scope'u için private registry'yi kullan
@alper:registry=https://gitea.hibna.com.tr/api/packages/hibna/npm/ @source:registry=https://gits.hibna.com.tr/api/packages/hibna/npm/
# Authentication token # Authentication token
# Token'ınızı almak için: https://gitea.hibna.com.tr/user/settings/applications # Token'ınızı almak için: https://gits.hibna.com.tr/user/settings/applications
# "Generate New Token" butonuna tıklayın ve "read:package" yetkisini seçin # "Generate New Token" butonuna tıklayın ve "read:package" yetkisini seçin
//gitea.hibna.com.tr/api/packages/hibna/npm/:_authToken=YOUR_TOKEN_HERE //gits.hibna.com.tr/api/packages/hibna/npm/:_authToken=YOUR_TOKEN_HERE
``` ```
**ÖNEMLİ:** `YOUR_TOKEN_HERE` kısmını kendi Gitea access token'ınız ile değiştirin! **ÖNEMLİ:** `YOUR_TOKEN_HERE` kısmını kendi Gitea access token'ınız ile değiştirin!
#### Adım 2.3: Gitea Access Token Alma #### Adım 2.3: Gitea Access Token Alma
1. Gitea hesabınıza giriş yapın: https://gitea.hibna.com.tr 1. Gitea hesabınıza giriş yapın: https://gits.hibna.com.tr
2. Sağ üst köşeden profil ikonuna tıklayın → **Settings** 2. Sağ üst köşeden profil ikonuna tıklayın → **Settings**
3. Sol menüden **Applications** seçeneğine tıklayın 3. Sol menüden **Applications** seçeneğine tıklayın
4. **Manage Access Tokens** bölümünde **Generate New Token** butonuna tıklayın 4. **Manage Access Tokens** bölümünde **Generate New Token** butonuna tıklayın
@@ -168,28 +168,28 @@ Daha güvenli bir yöntem için token'ı environment variable olarak saklayabili
**.npmrc dosyası:** **.npmrc dosyası:**
```ini ```ini
@alper:registry=https://gitea.hibna.com.tr/api/packages/hibna/npm/ @source:registry=https://gits.hibna.com.tr/api/packages/hibna/npm/
//gitea.hibna.com.tr/api/packages/hibna/npm/:_authToken=${GITEA_TOKEN} //gits.hibna.com.tr/api/packages/hibna/npm/:_authToken=${GITS_NPM_TOKEN}
``` ```
**Environment variable ayarlama:** **Environment variable ayarlama:**
```bash ```bash
# Linux/Mac (.bashrc veya .zshrc dosyasına ekleyin) # Linux/Mac (.bashrc veya .zshrc dosyasına ekleyin)
export GITEA_TOKEN=your-actual-token-here export GITS_NPM_TOKEN=your-actual-token-here
# Windows (PowerShell) # Windows (PowerShell)
$env:GITEA_TOKEN="your-actual-token-here" $env:GITS_NPM_TOKEN="your-actual-token-here"
# Windows (Kalıcı - Sistem Environment Variables) # Windows (Kalıcı - Sistem Environment Variables)
# Sistem Özellikler → Gelişmiş → Ortam Değişkenleri → Yeni # Sistem Özellikler → Gelişmiş → Ortam Değişkenleri → Yeni
# Değişken adı: GITEA_TOKEN # Değişken adı: GITS_NPM_TOKEN
# Değişken değeri: your-actual-token-here # Değişken değeri: your-actual-token-here
``` ```
**CI/CD için (GitHub Actions, GitLab CI, vb.):** **CI/CD için (GitHub Actions, GitLab CI, vb.):**
Repository settings → Secrets → `GITEA_TOKEN` adında bir secret oluşturun. Repository settings → Secrets → `GITS_NPM_TOKEN` adında bir secret oluşturun.
### 3. Kütüphaneyi Yükleme ### 3. Kütüphaneyi Yükleme
@@ -197,13 +197,13 @@ Repository settings → Secrets → `GITEA_TOKEN` adında bir secret oluşturun.
```bash ```bash
# npm ile # npm ile
npm install @alper/video-player npm install @source/player
# veya pnpm ile (önerilen - daha hızlı) # veya pnpm ile (önerilen - daha hızlı)
pnpm add @alper/video-player pnpm add @source/player
# veya yarn ile # veya yarn ile
yarn add @alper/video-player yarn add @source/player
``` ```
### 4. Peer Dependencies ### 4. Peer Dependencies
@@ -234,15 +234,15 @@ npm install --save-optional hls.js flv.js
Kurulumun başarılı olduğunu doğrulamak için: Kurulumun başarılı olduğunu doğrulamak için:
```bash ```bash
npm list @alper/video-player npm list @source/player
# veya # veya
pnpm list @alper/video-player pnpm list @source/player
``` ```
Çıktı şöyle olmalı: Çıktı şöyle olmalı:
``` ```
your-project@1.0.0 your-project@1.0.0
└─┬ @alper/video-player@0.1.5 └─┬ @source/player@0.1.5
``` ```
### Sorun Giderme ### Sorun Giderme
@@ -254,11 +254,11 @@ your-project@1.0.0
**Problem: "404 Not Found" hatası** **Problem: "404 Not Found" hatası**
- Registry URL'inin doğru olduğundan emin olun - Registry URL'inin doğru olduğundan emin olun
- `@alper:registry=https://gitea.hibna.com.tr/api/packages/hibna/npm/` - `@source:registry=https://gits.hibna.com.tr/api/packages/hibna/npm/`
- İnternet bağlantınızı kontrol edin - İnternet bağlantınızı kontrol edin
- Gitea sunucusunun erişilebilir olduğundan emin olun - Gitea sunucusunun erişilebilir olduğundan emin olun
**Problem: "Cannot find module '@alper/video-player'"** **Problem: "Cannot find module '@source/player'"**
- `node_modules` klasörünü silin ve yeniden yükleyin: - `node_modules` klasörünü silin ve yeniden yükleyin:
```bash ```bash
rm -rf node_modules package-lock.json rm -rf node_modules package-lock.json
@@ -272,8 +272,8 @@ your-project@1.0.0
### Temel Kullanım ### Temel Kullanım
```tsx ```tsx
import { VideoPlayer } from '@alper/video-player' import { VideoPlayer } from '@source/player'
import '@alper/video-player/styles.css' import '@source/player/styles.css'
function App() { function App() {
return ( return (
@@ -630,7 +630,7 @@ function detectVideoProtocol(url: string): {
**Kullanım:** **Kullanım:**
```typescript ```typescript
import { detectVideoProtocol } from '@alper/video-player' import { detectVideoProtocol } from '@source/player'
const info = detectVideoProtocol('https://example.com/stream.m3u8') const info = detectVideoProtocol('https://example.com/stream.m3u8')
console.log(info.protocol) // 'hls' console.log(info.protocol) // 'hls'
@@ -690,7 +690,7 @@ Merhaba dünya
**Dönüşüm Fonksiyonu:** **Dönüşüm Fonksiyonu:**
```typescript ```typescript
import { parseSRT, createSubtitleBlobURL } from '@alper/video-player' import { parseSRT, createSubtitleBlobURL } from '@source/player'
const srtContent = '...' // SRT içeriği const srtContent = '...' // SRT içeriği
const vttContent = parseSRT(srtContent) const vttContent = parseSRT(srtContent)
@@ -737,7 +737,7 @@ const blobUrl = createSubtitleBlobURL(vttContent)
**Programmatic Control:** **Programmatic Control:**
```tsx ```tsx
import { usePlayerContext } from '@alper/video-player' import { usePlayerContext } from '@source/player'
function CustomSubtitleToggle() { function CustomSubtitleToggle() {
const { settings, setSubtitle } = usePlayerContext() const { settings, setSubtitle } = usePlayerContext()
@@ -789,7 +789,7 @@ function CustomSubtitleToggle() {
**Programmatic Control:** **Programmatic Control:**
```tsx ```tsx
import { usePlayerContext } from '@alper/video-player' import { usePlayerContext } from '@source/player'
function AudioTrackSelector() { function AudioTrackSelector() {
const { settings, setAudioTrack } = usePlayerContext() const { settings, setAudioTrack } = usePlayerContext()
@@ -867,7 +867,7 @@ function AudioTrackSelector() {
**Otomatik Kalite (Adaptive Bitrate):** **Otomatik Kalite (Adaptive Bitrate):**
```typescript ```typescript
import { setHlsQualityLevel } from '@alper/video-player' import { setHlsQualityLevel } from '@source/player'
setHlsQualityLevel(hlsInstance, null) // Auto setHlsQualityLevel(hlsInstance, null) // Auto
``` ```
@@ -888,7 +888,7 @@ Kullanıcı Settings → Quality menüsünden kalite seçer.
**Programmatic Control:** **Programmatic Control:**
```tsx ```tsx
import { usePlayerContext } from '@alper/video-player' import { usePlayerContext } from '@source/player'
function QualitySelector() { function QualitySelector() {
const { settings, setQuality } = usePlayerContext() const { settings, setQuality } = usePlayerContext()
@@ -974,7 +974,7 @@ function QualitySelector() {
**Custom hook ile:** **Custom hook ile:**
```tsx ```tsx
import { useKeyboardShortcuts } from '@alper/video-player' import { useKeyboardShortcuts } from '@source/player'
function MyComponent() { function MyComponent() {
const { videoRef, containerRef } = usePlayerContext() const { videoRef, containerRef } = usePlayerContext()
@@ -1039,7 +1039,7 @@ function MyComponent() {
**Custom hook ile:** **Custom hook ile:**
```tsx ```tsx
import { useTouchGestures } from '@alper/video-player' import { useTouchGestures } from '@source/player'
function MyComponent() { function MyComponent() {
const { videoRef, containerRef } = usePlayerContext() const { videoRef, containerRef } = usePlayerContext()
@@ -1411,20 +1411,20 @@ interface PlayerContextValue {
```typescript ```typescript
// Ana bileşen // Ana bileşen
import { VideoPlayer } from '@alper/video-player' import { VideoPlayer } from '@source/player'
``` ```
### Exported Hooks ### Exported Hooks
```typescript ```typescript
// Player context hook // Player context hook
import { usePlayerContext } from '@alper/video-player' import { usePlayerContext } from '@source/player'
// Klavye kısayolları hook'u // Klavye kısayolları hook'u
import { useKeyboardShortcuts } from '@alper/video-player' import { useKeyboardShortcuts } from '@source/player'
// Dokunmatik jest hook'u // Dokunmatik jest hook'u
import { useTouchGestures } from '@alper/video-player' import { useTouchGestures } from '@source/player'
``` ```
**usePlayerContext Kullanımı:** **usePlayerContext Kullanımı:**
@@ -1458,7 +1458,7 @@ function CustomControl() {
```typescript ```typescript
// Zaman formatlama // Zaman formatlama
import { formatTime, parseTime } from '@alper/video-player' import { formatTime, parseTime } from '@source/player'
formatTime(125) // "2:05" formatTime(125) // "2:05"
formatTime(3665) // "1:01:05" formatTime(3665) // "1:01:05"
@@ -1470,7 +1470,7 @@ import {
parseSRT, parseSRT,
createSubtitleBlobURL, createSubtitleBlobURL,
fetchSubtitle fetchSubtitle
} from '@alper/video-player' } from '@source/player'
const srtContent = "1\n00:00:01,000 --> 00:00:04,000\nMerhaba" const srtContent = "1\n00:00:01,000 --> 00:00:04,000\nMerhaba"
const vttContent = parseSRT(srtContent) const vttContent = parseSRT(srtContent)
@@ -1483,7 +1483,7 @@ import {
getCORSErrorMessage, getCORSErrorMessage,
isCORSError, isCORSError,
checkVideoCORS checkVideoCORS
} from '@alper/video-player' } from '@source/player'
const validation = validateVideoURL(url) const validation = validateVideoURL(url)
if (!validation.valid) { if (!validation.valid) {
@@ -1505,7 +1505,7 @@ import {
getTranslations, getTranslations,
detectBrowserLanguage, detectBrowserLanguage,
translations translations
} from '@alper/video-player' } from '@source/player'
const lang = detectBrowserLanguage() // "tr", "en", vb. const lang = detectBrowserLanguage() // "tr", "en", vb.
const t = getTranslations('tr') const t = getTranslations('tr')
@@ -1520,8 +1520,8 @@ console.log(translations.tr.quality) // "Kalite"
### Temel MP4 Oynatma ### Temel MP4 Oynatma
```tsx ```tsx
import { VideoPlayer } from '@alper/video-player' import { VideoPlayer } from '@source/player'
import '@alper/video-player/styles.css' import '@source/player/styles.css'
function App() { function App() {
return ( return (
@@ -1603,7 +1603,7 @@ function VideoWithAnalytics() {
### Custom Kontroller ### Custom Kontroller
```tsx ```tsx
import { VideoPlayer, usePlayerContext } from '@alper/video-player' import { VideoPlayer, usePlayerContext } from '@source/player'
function CustomControls() { function CustomControls() {
const { const {
@@ -1660,7 +1660,7 @@ function App() {
```tsx ```tsx
import { useState } from 'react' import { useState } from 'react'
import { VideoPlayer } from '@alper/video-player' import { VideoPlayer } from '@source/player'
const videos = [ const videos = [
{ id: 1, src: 'video1.mp4', title: 'Video 1' }, { id: 1, src: 'video1.mp4', title: 'Video 1' },
@@ -1706,7 +1706,7 @@ function Playlist() {
### CORS Hata Yönetimi ### CORS Hata Yönetimi
```tsx ```tsx
import { VideoPlayer, isCORSError, getCORSErrorMessage } from '@alper/video-player' import { VideoPlayer, isCORSError, getCORSErrorMessage } from '@source/player'
import { useState } from 'react' import { useState } from 'react'
function VideoWithCORSHandling() { function VideoWithCORSHandling() {
@@ -1762,7 +1762,7 @@ import {
hasTouch, hasTouch,
isIOSSafari, isIOSSafari,
hasVolumeControl hasVolumeControl
} from '@alper/video-player' } from '@source/player'
// Safari'de native HLS var mı kontrol et // Safari'de native HLS var mı kontrol et
if (hasNativeHLS()) { if (hasNativeHLS()) {
@@ -1795,7 +1795,7 @@ if (hasVolumeControl()) {
### Manual HLS.js Setup ### Manual HLS.js Setup
```typescript ```typescript
import { loadHls, setupHls } from '@alper/video-player' import { loadHls, setupHls } from '@source/player'
async function customHlsSetup() { async function customHlsSetup() {
const videoElement = document.querySelector('video') const videoElement = document.querySelector('video')
@@ -1829,7 +1829,7 @@ async function customHlsSetup() {
### Custom Subtitle Processing ### Custom Subtitle Processing
```typescript ```typescript
import { parseSRT, createSubtitleBlobURL, fetchSubtitle } from '@alper/video-player' import { parseSRT, createSubtitleBlobURL, fetchSubtitle } from '@source/player'
async function loadCustomSubtitle(url: string) { async function loadCustomSubtitle(url: string) {
// SRT dosyasını fetch et // SRT dosyasını fetch et
@@ -2077,7 +2077,7 @@ interface Translations {
### Programmatic Access ### Programmatic Access
```typescript ```typescript
import { getTranslations, detectBrowserLanguage } from '@alper/video-player' import { getTranslations, detectBrowserLanguage } from '@source/player'
// Tarayıcı dilini tespit et // Tarayıcı dilini tespit et
const browserLang = detectBrowserLanguage() // "tr", "en-US", vb. const browserLang = detectBrowserLanguage() // "tr", "en-US", vb.
@@ -2170,7 +2170,7 @@ export const translations: Record<string, Translations> = {
```tsx ```tsx
import { useEffect } from 'react' import { useEffect } from 'react'
import { VideoPlayer } from '@alper/video-player' import { VideoPlayer } from '@source/player'
function MonitoredVideo() { function MonitoredVideo() {
useEffect(() => { useEffect(() => {
@@ -2204,7 +2204,7 @@ function MonitoredVideo() {
import { lazy, Suspense } from 'react' import { lazy, Suspense } from 'react'
const VideoPlayer = lazy(() => const VideoPlayer = lazy(() =>
import('@alper/video-player').then(module => ({ import('@source/player').then(module => ({
default: module.VideoPlayer default: module.VideoPlayer
})) }))
) )
@@ -2300,7 +2300,7 @@ Kütüphane, eski tarayıcılar için otomatik polyfill içerir:
**Tespit:** **Tespit:**
```typescript ```typescript
import { isCORSError, getCORSErrorMessage } from '@alper/video-player' import { isCORSError, getCORSErrorMessage } from '@source/player'
const handleError = (error: Error) => { const handleError = (error: Error) => {
if (isCORSError(error)) { if (isCORSError(error)) {
@@ -2437,7 +2437,7 @@ function RobustVideoPlayer() {
```bash ```bash
# Repository'yi klonlayın # Repository'yi klonlayın
git clone https://gitea.hibna.com.tr/hibna/video-player.git git clone https://gits.hibna.com.tr/hibna/video-player.git
cd video-player cd video-player
# Bağımlılıkları yükleyin # Bağımlılıkları yükleyin
@@ -2542,7 +2542,7 @@ Katkılarınızı bekliyoruz! Lütfen şu adımları takip edin:
```bash ```bash
# Repository'yi fork edin (Gitea UI'dan) # Repository'yi fork edin (Gitea UI'dan)
# Fork'unuzu klonlayın # Fork'unuzu klonlayın
git clone https://gitea.hibna.com.tr/YOUR_USERNAME/video-player.git git clone https://gits.hibna.com.tr/YOUR_USERNAME/video-player.git
cd video-player cd video-player
``` ```
@@ -2623,9 +2623,9 @@ SOFTWARE.
## 📞 İletişim ## 📞 İletişim
- **Repository:** https://gitea.hibna.com.tr/hibna/video-player - **Repository:** https://gits.hibna.com.tr/hibna/video-player
- **NPM Registry:** https://gitea.hibna.com.tr/api/packages/hibna/npm/ - **NPM Registry:** https://gits.hibna.com.tr/api/packages/hibna/npm/
- **Issues:** https://gitea.hibna.com.tr/hibna/video-player/issues - **Issues:** https://gits.hibna.com.tr/hibna/video-player/issues
- **Author:** Alper - **Author:** Alper
--- ---
@@ -2645,3 +2645,6 @@ Bu proje, aşağıdaki açık kaynak kütüphanelerden ilham almıştır:
**Built with ❤️ using React, TypeScript, and Vite** **Built with ❤️ using React, TypeScript, and Vite**
*Son güncelleme: 2024* *Son güncelleme: 2024*
+33 -15
View File
@@ -15,7 +15,7 @@ A feature-rich, modern video player library built with React, TypeScript, and Vi
|---------|---------------------|----------|--------------|------| |---------|---------------------|----------|--------------|------|
| **Bundle Size (gzipped)** | **~18KB JS + ~3.5KB CSS** ✅ | ~500KB ❌ | ~50KB ⚠️ | ~30KB ⚠️ | | **Bundle Size (gzipped)** | **~18KB JS + ~3.5KB CSS** ✅ | ~500KB ❌ | ~50KB ⚠️ | ~30KB ⚠️ |
| **Runtime Dependencies** | **0** ✅ | Many ❌ | Few ⚠️ | Few ⚠️ | | **Runtime Dependencies** | **0** ✅ | Many ❌ | Few ⚠️ | Few ⚠️ |
| **React Native** | **Yes** ✅ | Wrapper ⚠️ | **Yes** ✅ | Wrapper ⚠️ | | **React (Web)** | **Yes** ✅ | Wrapper ⚠️ | **Yes** ✅ | Wrapper ⚠️ |
| **TypeScript Native** | **Yes** ✅ | Types ⚠️ | Partial ⚠️ | Types ⚠️ | | **TypeScript Native** | **Yes** ✅ | Types ⚠️ | Partial ⚠️ | Types ⚠️ |
| **HLS Support** | **Yes** ✅ | Yes ✅ | Yes ✅ | No ❌ | | **HLS Support** | **Yes** ✅ | Yes ✅ | Yes ✅ | No ❌ |
| **Quality Switching** | **Yes** ✅ | Yes ✅ | Limited ⚠️ | No ❌ | | **Quality Switching** | **Yes** ✅ | Yes ✅ | Limited ⚠️ | No ❌ |
@@ -84,27 +84,45 @@ A feature-rich, modern video player library built with React, TypeScript, and Vi
## 📦 Installation ## 📦 Installation
This is a local library project. To use it in your projects: This package is distributed through a private registry.
### Option 1: Copy the library ### 1. Configure `.npmrc`
```bash
# Copy the src folder to your project Create `.npmrc` in your app root:
cp -r src/components your-project/src/
cp -r src/contexts your-project/src/ ```ini
cp -r src/hooks your-project/src/ @source:registry=https://gits.hibna.com.tr/api/packages/hibna/npm/
cp -r src/utils your-project/src/ //gits.hibna.com.tr/api/packages/hibna/npm/:_authToken=${GITS_NPM_TOKEN}
cp -r src/types your-project/src/
cp -r src/icons your-project/src/
cp -r src/styles your-project/src/
``` ```
### Option 2: Build as library and link ### 2. Set token
Set your token in environment variables (`GITS_NPM_TOKEN`) and do not commit `.npmrc` with a hardcoded token.
### 3. Install package
```bash ```bash
# In this project npm install @source/player
# or
pnpm add @source/player
# or
yarn add @source/player
```
### 4. Ensure peer dependencies
```bash
npm install react react-dom
```
### Local development (optional)
```bash
# In this repository
npm run build:lib npm run build:lib
npm link npm link
# In your other project # In your consuming app
npm link @source/player npm link @source/player
``` ```
+2 -2
View File
@@ -31,7 +31,7 @@ export const ControlsLayer: React.FC<ControlsLayerProps> = ({
audioTracks = [], audioTracks = [],
qualities = [], qualities = [],
}) => { }) => {
const { videoState, uiState, togglePlay, toggleFullscreen, showControls, hideControls } = const { videoState, uiState, togglePlay, toggleFullscreen, showControls, hideControls, translations } =
usePlayerContext() usePlayerContext()
const [isPointerOver, setIsPointerOver] = useState(false) const [isPointerOver, setIsPointerOver] = useState(false)
const [lastInteraction, setLastInteraction] = useState<number>(0) const [lastInteraction, setLastInteraction] = useState<number>(0)
@@ -228,7 +228,7 @@ export const ControlsLayer: React.FC<ControlsLayerProps> = ({
{videoState.isLiveBroadcast && ( {videoState.isLiveBroadcast && (
<div className="live-indicator"> <div className="live-indicator">
<span className="live-dot"></span> <span className="live-dot"></span>
<span className="live-text">LIVE</span> <span className="live-text">{translations.live}</span>
</div> </div>
)} )}
</div> </div>
+25 -106
View File
@@ -8,6 +8,7 @@ import { setupRtmpInstance } from '../utils/rtmpSetup'
import { setupMpegtsInstance } from '../utils/mpegtsSetup' import { setupMpegtsInstance } from '../utils/mpegtsSetup'
import { detectVideoProtocol } from '../utils/videoProtocol' import { detectVideoProtocol } from '../utils/videoProtocol'
import { createSubtitleBlobURL } from '../utils/subtitles' import { createSubtitleBlobURL } from '../utils/subtitles'
import { logger } from '../utils/logger'
import './VideoElement.css' import './VideoElement.css'
interface VideoElementProps { interface VideoElementProps {
@@ -112,7 +113,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
// Check if this is a live broadcast (duration is Infinity for live streams) // Check if this is a live broadcast (duration is Infinity for live streams)
const isLiveBroadcast = !isFinite(video.duration) || video.duration === 0 const isLiveBroadcast = !isFinite(video.duration) || video.duration === 0
console.log('[VideoElement] Is live broadcast?', isLiveBroadcast, 'duration:', video.duration) logger.log('[VideoElement] Is live broadcast?', isLiveBroadcast, 'duration:', video.duration)
setVideoState((prev) => ({ setVideoState((prev) => ({
...prev, ...prev,
@@ -127,7 +128,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
if (tracks && processedSubtitles.length > 0) { if (tracks && processedSubtitles.length > 0) {
const defaultSubtitle = processedSubtitles.find((sub) => sub.default) const defaultSubtitle = processedSubtitles.find((sub) => sub.default)
if (defaultSubtitle) { if (defaultSubtitle) {
console.log(`🎯 Found default subtitle in metadata: ${defaultSubtitle.label}`) logger.log(`🎯 Found default subtitle in metadata: ${defaultSubtitle.label}`)
// Set subtitle in context (this will trigger the useEffect that enables it) // Set subtitle in context (this will trigger the useEffect that enables it)
setSubtitle(defaultSubtitle) setSubtitle(defaultSubtitle)
} }
@@ -142,7 +143,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
// Re-check if this is a live broadcast when duration changes // Re-check if this is a live broadcast when duration changes
const isLiveBroadcast = !isFinite(video.duration) || video.duration === 0 const isLiveBroadcast = !isFinite(video.duration) || video.duration === 0
console.log('[VideoElement] Duration changed. Is live broadcast?', isLiveBroadcast, 'duration:', video.duration) logger.log('[VideoElement] Duration changed. Is live broadcast?', isLiveBroadcast, 'duration:', video.duration)
setVideoState((prev) => ({ setVideoState((prev) => ({
...prev, ...prev,
@@ -339,25 +340,16 @@ export const VideoElement: React.FC<VideoElementProps> = ({
throw new Error(`Failed to fetch subtitle: ${response.status} ${response.statusText}`) throw new Error(`Failed to fetch subtitle: ${response.status} ${response.statusText}`)
} }
const srtContent = await response.text() const srtContent = await response.text()
console.log(`SRT content length: ${srtContent.length} chars`)
const blobUrl = createSubtitleBlobURL(srtContent, 'srt') const blobUrl = createSubtitleBlobURL(srtContent, 'srt')
subtitleBlobUrlsRef.current.push(blobUrl) subtitleBlobUrlsRef.current.push(blobUrl)
// Debug: fetch the blob URL to verify VTT content
const vttResponse = await fetch(blobUrl)
const vttContent = await vttResponse.text()
console.log(`VTT content preview (first 500 chars):`, vttContent.substring(0, 500))
console.log(`Total VTT length: ${vttContent.length} chars`)
console.log(`Processed SRT subtitle "${subtitle.label}": ${subtitle.src} -> ${blobUrl}`)
return { ...subtitle, src: blobUrl } return { ...subtitle, src: blobUrl }
} }
// VTT files can be used directly // VTT files can be used directly
console.log(`Using VTT subtitle "${subtitle.label}": ${subtitle.src}`)
return subtitle return subtitle
} catch (error) { } catch (error) {
console.error(`Failed to process subtitle ${subtitle.label}:`, error) logger.error(`Failed to process subtitle ${subtitle.label}:`, error)
return subtitle return subtitle
} }
}) })
@@ -459,10 +451,10 @@ export const VideoElement: React.FC<VideoElementProps> = ({
} }
} }
console.log('[VideoElement] Source:', src) logger.log('[VideoElement] Source:', src)
console.log('[VideoElement] Detected protocol:', detection.protocol) logger.log('[VideoElement] Detected protocol:', detection.protocol)
console.log('[VideoElement] Is live stream?', detection.isLive) logger.log('[VideoElement] Is live stream?', detection.isLive)
console.log('[VideoElement] Needs special player?', detection.needsSpecialPlayer) logger.log('[VideoElement] Needs special player?', detection.needsSpecialPlayer)
const setupPlayer = async () => { const setupPlayer = async () => {
try { try {
@@ -473,12 +465,12 @@ export const VideoElement: React.FC<VideoElementProps> = ({
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
const shouldUseHlsJs = canPlayHLS === '' || !isSafari const shouldUseHlsJs = canPlayHLS === '' || !isSafari
console.log('[VideoElement] Native HLS support?', canPlayHLS) logger.log('[VideoElement] Native HLS support?', canPlayHLS)
console.log('[VideoElement] Is Safari?', isSafari) logger.log('[VideoElement] Is Safari?', isSafari)
console.log('[VideoElement] Will use HLS.js?', shouldUseHlsJs) logger.log('[VideoElement] Will use HLS.js?', shouldUseHlsJs)
if (shouldUseHlsJs) { if (shouldUseHlsJs) {
console.log('[VideoElement] Setting up HLS.js...') logger.log('[VideoElement] Setting up HLS.js...')
cleanupFn = await setupHlsInstance({ cleanupFn = await setupHlsInstance({
video, video,
src, src,
@@ -507,7 +499,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
} }
} else { } else {
if (isCancelled) return if (isCancelled) return
console.log('[VideoElement] Using native HLS playback') logger.log('[VideoElement] Using native HLS playback')
video.src = src video.src = src
if (autoplay) { if (autoplay) {
void video.play().catch(() => undefined) void video.play().catch(() => undefined)
@@ -518,7 +510,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
case 'rtmp': { case 'rtmp': {
// RTMP/FLV streaming setup // RTMP/FLV streaming setup
console.log('[VideoElement] Setting up RTMP/FLV player...') logger.log('[VideoElement] Setting up RTMP/FLV player...')
cleanupFn = await setupRtmpInstance({ cleanupFn = await setupRtmpInstance({
video, video,
src, src,
@@ -536,7 +528,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
case 'mpegts': { case 'mpegts': {
// MPEG-TS/IPTV streaming setup // MPEG-TS/IPTV streaming setup
console.log('[VideoElement] Setting up MPEG-TS player...') logger.log('[VideoElement] Setting up MPEG-TS player...')
cleanupFn = await setupMpegtsInstance({ cleanupFn = await setupMpegtsInstance({
video, video,
src, src,
@@ -556,7 +548,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
// DASH streaming - not yet implemented // DASH streaming - not yet implemented
if (isCancelled) return if (isCancelled) return
const error = new Error('DASH streaming is not yet supported') const error = new Error('DASH streaming is not yet supported')
console.error('[VideoElement]', error.message) logger.error('[VideoElement]', error.message)
setVideoState((prev) => ({ ...prev, error, loading: false })) setVideoState((prev) => ({ ...prev, error, loading: false }))
onError?.(error) onError?.(error)
break break
@@ -566,7 +558,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
default: { default: {
// Native HTML5 video (MP4, WebM, etc.) // Native HTML5 video (MP4, WebM, etc.)
if (isCancelled) return if (isCancelled) return
console.log('[VideoElement] Using native video.src') logger.log('[VideoElement] Using native video.src')
video.src = src video.src = src
if (autoplay) { if (autoplay) {
void video.play().catch(() => undefined) void video.play().catch(() => undefined)
@@ -585,7 +577,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
if (isCancelled) return if (isCancelled) return
console.error('[VideoElement] Setup error:', error) logger.error('[VideoElement] Setup error:', error)
setVideoState((prev) => ({ setVideoState((prev) => ({
...prev, ...prev,
error, error,
@@ -728,11 +720,11 @@ export const VideoElement: React.FC<VideoElementProps> = ({
// Wait for track to have cues before showing // Wait for track to have cues before showing
if (track.cues && track.cues.length > 0) { if (track.cues && track.cues.length > 0) {
track.mode = 'showing' track.mode = 'showing'
console.log(`🔊 Enabled subtitle track: ${track.label} (${track.language})`) logger.log(`🔊 Enabled subtitle track: ${track.label} (${track.language})`)
console.log(` - cues available: ${track.cues.length}`) logger.log(` - cues available: ${track.cues.length}`)
console.log(` - track.mode: ${track.mode}`) logger.log(` - track.mode: ${track.mode}`)
} else { } else {
console.warn(`⚠️ Track ${track.label} has no cues yet, waiting...`) logger.warn(`⚠️ Track ${track.label} has no cues yet, waiting...`)
// Track not ready yet, will be handled by load event // Track not ready yet, will be handled by load event
track.mode = 'showing' track.mode = 'showing'
} }
@@ -747,7 +739,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
// Also listen for track load events to retry // Also listen for track load events to retry
const handleTrackChange = () => { const handleTrackChange = () => {
console.log(`🔄 Track changed, re-enabling subtitle`) logger.log(`🔄 Track changed, re-enabling subtitle`)
enableSubtitle() enableSubtitle()
} }
@@ -762,80 +754,6 @@ export const VideoElement: React.FC<VideoElementProps> = ({
} }
}, [settings.subtitle, videoRef]) }, [settings.subtitle, videoRef])
// Debug: Monitor text track loading
useEffect(() => {
const video = videoRef.current
if (!video) return
const handleTrackLoad = (e: Event) => {
const track = e.target as HTMLTrackElement
const textTrack = track.track
console.log(`✅ Track loaded: ${track.label} (${track.srclang})`)
console.log(` - readyState: ${track.readyState}`)
console.log(` - track.mode: ${textTrack.mode}`)
console.log(` - track.cues: ${textTrack.cues?.length || 0}`)
console.log(` - src: ${track.src}`)
// Log first few cues if available
if (textTrack.cues && textTrack.cues.length > 0) {
console.log(` - First cue: ${(textTrack.cues[0] as VTTCue).startTime}s - ${(textTrack.cues[0] as VTTCue).endTime}s: "${(textTrack.cues[0] as VTTCue).text}"`)
} else {
console.warn(` ⚠️ No cues found in track!`)
}
}
const handleTrackError = (e: Event) => {
const track = e.target as HTMLTrackElement
console.error(`❌ Track error: ${track.label} (${track.srclang})`)
console.error(` - src: ${track.src}`)
console.error(` - readyState: ${track.readyState}`)
}
const trackElements = video.querySelectorAll('track')
trackElements.forEach((track) => {
track.addEventListener('load', handleTrackLoad)
track.addEventListener('error', handleTrackError)
})
// Also monitor text tracks
const textTracks = video.textTracks
const handleCueChange = () => {
for (let i = 0; i < textTracks.length; i++) {
const track = textTracks[i]
if (track.mode === 'showing') {
console.log(`🎬 Cuechange: ${track.label}, cues: ${track.cues?.length || 0}, active cues: ${track.activeCues?.length || 0}`)
if (track.activeCues && track.activeCues.length > 0) {
const cue = track.activeCues[0] as VTTCue
console.log(` - Active cue text: "${cue.text}"`)
}
}
}
}
for (let i = 0; i < textTracks.length; i++) {
textTracks[i].addEventListener('cuechange', handleCueChange)
}
// Log all text tracks after a delay to see their state
setTimeout(() => {
console.log(`📊 Text tracks summary (${textTracks.length} total):`)
for (let i = 0; i < textTracks.length; i++) {
const track = textTracks[i]
console.log(` [${i}] ${track.label} (${track.language}): mode=${track.mode}, cues=${track.cues?.length || 0}`)
}
}, 1000)
return () => {
trackElements.forEach((track) => {
track.removeEventListener('load', handleTrackLoad)
track.removeEventListener('error', handleTrackError)
})
for (let i = 0; i < textTracks.length; i++) {
textTracks[i].removeEventListener('cuechange', handleCueChange)
}
}
}, [videoRef, processedSubtitles])
return ( return (
<div className="video-container"> <div className="video-container">
<video <video
@@ -876,3 +794,4 @@ export const VideoElement: React.FC<VideoElementProps> = ({
</div> </div>
) )
} }
+3 -3
View File
@@ -4,7 +4,7 @@ import { PlayIcon } from '../../icons'
import './CenterPlayButton.css' import './CenterPlayButton.css'
export const CenterPlayButton: React.FC = () => { export const CenterPlayButton: React.FC = () => {
const { play } = usePlayerContext() const { play, translations } = usePlayerContext()
return ( return (
<div className="center-play-overlay"> <div className="center-play-overlay">
@@ -12,8 +12,8 @@ export const CenterPlayButton: React.FC = () => {
className="center-play-button" className="center-play-button"
type="button" type="button"
onClick={play} onClick={play}
aria-label="Play" aria-label={translations.play}
title="Play" title={translations.play}
> >
<PlayIcon size={72} color="var(--player-text)" /> <PlayIcon size={72} color="var(--player-text)" />
</button> </button>
+6 -3
View File
@@ -4,14 +4,17 @@ import { FullscreenIcon, FullscreenExitIcon } from '../../icons'
import './ControlButton.css' import './ControlButton.css'
export const FullscreenButton: React.FC = () => { export const FullscreenButton: React.FC = () => {
const { videoState, toggleFullscreen } = usePlayerContext() const { videoState, toggleFullscreen, translations } = usePlayerContext()
const actionLabel = videoState.fullscreen
? translations.exitFullscreen
: translations.enterFullscreen
return ( return (
<button <button
className="control-button fullscreen-button" className="control-button fullscreen-button"
onClick={toggleFullscreen} onClick={toggleFullscreen}
aria-label={videoState.fullscreen ? 'Exit fullscreen' : 'Enter fullscreen'} aria-label={actionLabel}
title={videoState.fullscreen ? 'Exit fullscreen (F)' : 'Enter fullscreen (F)'} title={`${actionLabel} (F)`}
> >
{videoState.fullscreen ? ( {videoState.fullscreen ? (
<FullscreenExitIcon size={24} color="var(--player-text)" /> <FullscreenExitIcon size={24} color="var(--player-text)" />
+6 -3
View File
@@ -4,7 +4,10 @@ import { PIPIcon } from '../../icons'
import './ControlButton.css' import './ControlButton.css'
export const PIPButton: React.FC = () => { export const PIPButton: React.FC = () => {
const { videoState, togglePictureInPicture } = usePlayerContext() const { videoState, togglePictureInPicture, translations } = usePlayerContext()
const actionLabel = videoState.pictureInPicture
? translations.exitPictureInPicture
: translations.enterPictureInPicture
// Check if PIP is supported // Check if PIP is supported
const isPIPSupported = const isPIPSupported =
@@ -20,8 +23,8 @@ export const PIPButton: React.FC = () => {
<button <button
className="control-button pip-button" className="control-button pip-button"
onClick={togglePictureInPicture} onClick={togglePictureInPicture}
aria-label={videoState.pictureInPicture ? 'Exit picture-in-picture' : 'Enter picture-in-picture'} aria-label={actionLabel}
title="Picture-in-picture (P)" title={`${actionLabel} (P)`}
> >
<PIPIcon size={24} color="var(--player-text)" /> <PIPIcon size={24} color="var(--player-text)" />
</button> </button>
+4 -3
View File
@@ -4,14 +4,15 @@ import { PlayIcon, PauseIcon } from '../../icons'
import './ControlButton.css' import './ControlButton.css'
export const PlayPauseButton: React.FC = () => { export const PlayPauseButton: React.FC = () => {
const { videoState, togglePlay } = usePlayerContext() const { videoState, togglePlay, translations } = usePlayerContext()
const actionLabel = videoState.playing ? translations.pause : translations.play
return ( return (
<button <button
className="control-button play-pause-button" className="control-button play-pause-button"
onClick={togglePlay} onClick={togglePlay}
aria-label={videoState.playing ? 'Pause' : 'Play'} aria-label={actionLabel}
title={videoState.playing ? 'Pause (Space)' : 'Play (Space)'} title={`${actionLabel} (Space)`}
> >
{videoState.playing ? ( {videoState.playing ? (
<PauseIcon size={24} color="var(--player-text)" /> <PauseIcon size={24} color="var(--player-text)" />
+2 -2
View File
@@ -3,7 +3,7 @@ import { usePlayerContext } from '../../contexts/PlayerContext'
import './ProgressBar.css' import './ProgressBar.css'
export const ProgressBar: React.FC = () => { export const ProgressBar: React.FC = () => {
const { videoState, seek } = usePlayerContext() const { videoState, seek, translations } = usePlayerContext()
const progressRef = useRef<HTMLDivElement>(null) const progressRef = useRef<HTMLDivElement>(null)
const [seeking, setSeeking] = useState(false) const [seeking, setSeeking] = useState(false)
const [hoverTime, setHoverTime] = useState<number | null>(null) const [hoverTime, setHoverTime] = useState<number | null>(null)
@@ -83,7 +83,7 @@ export const ProgressBar: React.FC = () => {
onMouseUp={handleMouseUp} onMouseUp={handleMouseUp}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
role="slider" role="slider"
aria-label="Video progress" aria-label={translations.videoProgress}
aria-valuemin={0} aria-valuemin={0}
aria-valuemax={videoState.duration} aria-valuemax={videoState.duration}
aria-valuenow={videoState.currentTime} aria-valuenow={videoState.currentTime}
+3 -3
View File
@@ -4,15 +4,15 @@ import { SettingsIcon } from '../../icons'
import './ControlButton.css' import './ControlButton.css'
export const SettingsButton: React.FC = () => { export const SettingsButton: React.FC = () => {
const { toggleSettings } = usePlayerContext() const { toggleSettings, translations } = usePlayerContext()
return ( return (
<button <button
className="control-button settings-button" className="control-button settings-button"
onMouseDown={(event) => event.stopPropagation()} onMouseDown={(event) => event.stopPropagation()}
onClick={toggleSettings} onClick={toggleSettings}
aria-label="Settings" aria-label={translations.settings}
title="Settings" title={translations.settings}
> >
<SettingsIcon size={24} color="var(--player-text)" /> <SettingsIcon size={24} color="var(--player-text)" />
</button> </button>
+5 -4
View File
@@ -4,7 +4,7 @@ import { VolumeUpIcon, VolumeDownIcon, VolumeMuteIcon } from '../../icons'
import './VolumeControl.css' import './VolumeControl.css'
export const VolumeControl: React.FC = () => { export const VolumeControl: React.FC = () => {
const { videoState, setVolume, toggleMute } = usePlayerContext() const { videoState, setVolume, toggleMute, translations } = usePlayerContext()
const [showSlider, setShowSlider] = useState(false) const [showSlider, setShowSlider] = useState(false)
const timeoutRef = useRef<number | undefined>(undefined) const timeoutRef = useRef<number | undefined>(undefined)
@@ -30,6 +30,7 @@ export const VolumeControl: React.FC = () => {
) )
const VolumeIcon = videoState.muted ? VolumeMuteIcon : videoState.volume > 0.5 ? VolumeUpIcon : VolumeDownIcon const VolumeIcon = videoState.muted ? VolumeMuteIcon : videoState.volume > 0.5 ? VolumeUpIcon : VolumeDownIcon
const actionLabel = videoState.muted ? translations.unmute : translations.mute
return ( return (
<div <div
@@ -40,8 +41,8 @@ export const VolumeControl: React.FC = () => {
<button <button
className="control-button volume-button" className="control-button volume-button"
onClick={toggleMute} onClick={toggleMute}
aria-label={videoState.muted ? 'Unmute' : 'Mute'} aria-label={actionLabel}
title={videoState.muted ? 'Unmute (M)' : 'Mute (M)'} title={`${actionLabel} (M)`}
> >
<VolumeIcon size={24} color="var(--player-text)" /> <VolumeIcon size={24} color="var(--player-text)" />
</button> </button>
@@ -55,7 +56,7 @@ export const VolumeControl: React.FC = () => {
value={videoState.muted ? 0 : videoState.volume} value={videoState.muted ? 0 : videoState.volume}
onChange={handleSliderChange} onChange={handleSliderChange}
className="volume-slider" className="volume-slider"
aria-label="Volume" aria-label={translations.volume}
/> />
<div <div
className="volume-slider-fill" className="volume-slider-fill"
+2 -2
View File
@@ -92,7 +92,7 @@ export const SettingsMenu: React.FC<SettingsMenuProps> = ({
<div className="settings-main-option-content"> <div className="settings-main-option-content">
<span className="settings-main-option-label">{translations.speed}</span> <span className="settings-main-option-label">{translations.speed}</span>
<span className="settings-main-option-value"> <span className="settings-main-option-value">
{videoState.playbackRate === 1 ? 'Normal' : `${videoState.playbackRate}x`} {videoState.playbackRate === 1 ? translations.normal : `${videoState.playbackRate}x`}
</span> </span>
</div> </div>
<div className="settings-main-option-arrow"></div> <div className="settings-main-option-arrow"></div>
@@ -148,7 +148,7 @@ export const SettingsMenu: React.FC<SettingsMenuProps> = ({
setTimeout(() => goBack(), 150) setTimeout(() => goBack(), 150)
}} }}
> >
<span>{rate === 1 ? 'Normal' : `${rate}x`}</span> <span>{rate === 1 ? translations.normal : `${rate}x`}</span>
{videoState.playbackRate === rate && <CheckIcon size={16} color="var(--player-primary)" />} {videoState.playbackRate === rate && <CheckIcon size={16} color="var(--player-primary)" />}
</button> </button>
))} ))}
+33
View File
@@ -14,6 +14,17 @@ export interface Translations {
audioTrack: string; audioTrack: string;
settings: string; settings: string;
level: string; level: string;
play: string;
pause: string;
mute: string;
unmute: string;
enterFullscreen: string;
exitFullscreen: string;
enterPictureInPicture: string;
exitPictureInPicture: string;
videoProgress: string;
volume: string;
live: string;
} }
export const translations: Record<string, Translations> = { export const translations: Record<string, Translations> = {
@@ -29,6 +40,17 @@ export const translations: Record<string, Translations> = {
audioTrack: 'Audio Track', audioTrack: 'Audio Track',
settings: 'Settings', settings: 'Settings',
level: "Level", level: "Level",
play: 'Play',
pause: 'Pause',
mute: 'Mute',
unmute: 'Unmute',
enterFullscreen: 'Enter fullscreen',
exitFullscreen: 'Exit fullscreen',
enterPictureInPicture: 'Enter picture-in-picture',
exitPictureInPicture: 'Exit picture-in-picture',
videoProgress: 'Video progress',
volume: 'Volume',
live: 'LIVE',
}, },
tr: { tr: {
noSubtitlesAvailable: 'Altyazı mevcut değil', noSubtitlesAvailable: 'Altyazı mevcut değil',
@@ -42,6 +64,17 @@ export const translations: Record<string, Translations> = {
audioTrack: 'Ses', audioTrack: 'Ses',
settings: 'Ayarlar', settings: 'Ayarlar',
level: "Seviye", level: "Seviye",
play: 'Oynat',
pause: 'Duraklat',
mute: 'Sesi kapat',
unmute: 'Sesi aç',
enterFullscreen: 'Tam ekrana gir',
exitFullscreen: 'Tam ekrandan çık',
enterPictureInPicture: 'Resim içinde resme gir',
exitPictureInPicture: 'Resim içinde resimden çık',
videoProgress: 'Video ilerlemesi',
volume: 'Ses',
live: 'CANLI',
}, },
}; };
+17 -15
View File
@@ -5,6 +5,7 @@
import type { AudioTrack, VideoQuality, SubtitleTrack } from '../types' import type { AudioTrack, VideoQuality, SubtitleTrack } from '../types'
import { getTranslations, detectBrowserLanguage } from '../i18n' import { getTranslations, detectBrowserLanguage } from '../i18n'
import { logger } from './logger'
// Re-export control functions for backward compatibility // Re-export control functions for backward compatibility
export { setHlsQualityLevel, setHlsAudioTrack } from './hlsControl' export { setHlsQualityLevel, setHlsAudioTrack } from './hlsControl'
@@ -47,20 +48,20 @@ const loadHlsFromCDN = (): Promise<any> => {
*/ */
export const loadHls = async (): Promise<any> => { export const loadHls = async (): Promise<any> => {
try { try {
console.log('[HLS Loader] Attempting to load from npm package...') logger.log('[HLS Loader] Attempting to load from npm package...')
// Try loading from npm package first // Try loading from npm package first
const hlsModule = await import('hls.js') const hlsModule = await import('hls.js')
console.log('[HLS Loader] Successfully loaded from npm package') logger.log('[HLS Loader] Successfully loaded from npm package')
return hlsModule.default return hlsModule.default
} catch (npmError) { } catch (npmError) {
console.warn('[HLS Loader] Failed to load from npm, trying CDN...', npmError) logger.warn('[HLS Loader] Failed to load from npm, trying CDN...', npmError)
try { try {
// Fallback to CDN // Fallback to CDN
const Hls = await loadHlsFromCDN() const Hls = await loadHlsFromCDN()
console.log('[HLS Loader] Successfully loaded from CDN') logger.log('[HLS Loader] Successfully loaded from CDN')
return Hls return Hls
} catch (cdnError) { } catch (cdnError) {
console.error('[HLS Loader] Failed to load from CDN:', cdnError) logger.error('[HLS Loader] Failed to load from CDN:', cdnError)
throw new Error('Unable to load HLS.js library. HLS streaming is not available.') throw new Error('Unable to load HLS.js library. HLS streaming is not available.')
} }
} }
@@ -87,17 +88,17 @@ export const hasNativeHlsSupport = (): boolean => {
export const getHlsAudioTracks = (hls: any): AudioTrack[] => { export const getHlsAudioTracks = (hls: any): AudioTrack[] => {
try { try {
if (!hls) { if (!hls) {
console.warn('[HLS Loader] getHlsAudioTracks: No HLS instance provided') logger.warn('[HLS Loader] getHlsAudioTracks: No HLS instance provided')
return [] return []
} }
// Check if audioTracks property exists // Check if audioTracks property exists
if (!hls.audioTracks || !Array.isArray(hls.audioTracks)) { if (!hls.audioTracks || !Array.isArray(hls.audioTracks)) {
console.warn('[HLS Loader] getHlsAudioTracks: No audioTracks array found on HLS instance') logger.warn('[HLS Loader] getHlsAudioTracks: No audioTracks array found on HLS instance')
return [] return []
} }
console.log('[HLS Loader] getHlsAudioTracks: Raw audioTracks from HLS:', hls.audioTracks) logger.log('[HLS Loader] getHlsAudioTracks: Raw audioTracks from HLS:', hls.audioTracks)
const audioTracks: AudioTrack[] = hls.audioTracks.map((track: any, index: number) => { const audioTracks: AudioTrack[] = hls.audioTracks.map((track: any, index: number) => {
const audioTrack = { const audioTrack = {
@@ -111,10 +112,10 @@ export const getHlsAudioTracks = (hls: any): AudioTrack[] => {
return audioTrack return audioTrack
}) })
console.log('[HLS Loader] getHlsAudioTracks: Processed tracks:', audioTracks) logger.log('[HLS Loader] getHlsAudioTracks: Processed tracks:', audioTracks)
return audioTracks return audioTracks
} catch (error) { } catch (error) {
console.error('[HLS Loader] getHlsAudioTracks: Error extracting audio tracks:', error) logger.error('[HLS Loader] getHlsAudioTracks: Error extracting audio tracks:', error)
return [] return []
} }
} }
@@ -154,16 +155,16 @@ export const getHlsSubtitleTracks = (hls: any): SubtitleTrack[] => {
export const getHlsQualities = (hls: any): VideoQuality[] => { export const getHlsQualities = (hls: any): VideoQuality[] => {
try { try {
if (!hls) { if (!hls) {
console.warn('[HLS Loader] getHlsQualities: No HLS instance provided') logger.warn('[HLS Loader] getHlsQualities: No HLS instance provided')
return [] return []
} }
if (!Array.isArray(hls.levels)) { if (!Array.isArray(hls.levels)) {
console.warn('[HLS Loader] getHlsQualities: No levels array found on HLS instance') logger.warn('[HLS Loader] getHlsQualities: No levels array found on HLS instance')
return [] return []
} }
console.log('[HLS Loader] getHlsQualities: Raw levels from HLS:', hls.levels) logger.log('[HLS Loader] getHlsQualities: Raw levels from HLS:', hls.levels)
const qualities: VideoQuality[] = hls.levels.map((level: any, index: number) => { const qualities: VideoQuality[] = hls.levels.map((level: any, index: number) => {
const resolution = typeof level.attrs?.RESOLUTION === 'string' ? level.attrs.RESOLUTION : undefined const resolution = typeof level.attrs?.RESOLUTION === 'string' ? level.attrs.RESOLUTION : undefined
@@ -205,11 +206,12 @@ export const getHlsQualities = (hls: any): VideoQuality[] => {
return (b.bitrate || 0) - (a.bitrate || 0) return (b.bitrate || 0) - (a.bitrate || 0)
}) })
console.log('[HLS Loader] getHlsQualities: Processed qualities:', sortedQualities) logger.log('[HLS Loader] getHlsQualities: Processed qualities:', sortedQualities)
return sortedQualities return sortedQualities
} catch (error) { } catch (error) {
console.error('[HLS Loader] getHlsQualities: Error extracting qualities:', error) logger.error('[HLS Loader] getHlsQualities: Error extracting qualities:', error)
return [] return []
} }
} }
+12 -10
View File
@@ -3,6 +3,7 @@
*/ */
import type { AudioTrack, VideoQuality, SubtitleTrack } from '../types' import type { AudioTrack, VideoQuality, SubtitleTrack } from '../types'
import { logger } from './logger'
interface HlsSetupOptions { interface HlsSetupOptions {
video: HTMLVideoElement video: HTMLVideoElement
@@ -30,7 +31,7 @@ export const setupHlsInstance = async ({
throw new Error('HLS.js is not supported in this browser') throw new Error('HLS.js is not supported in this browser')
} }
console.log('[HLS Setup] Creating HLS instance for:', src) logger.log('[HLS Setup] Creating HLS instance for:', src)
const hls = new Hls({ const hls = new Hls({
enableWorker: true, enableWorker: true,
@@ -44,10 +45,10 @@ export const setupHlsInstance = async ({
let manifestParsedHandled = false let manifestParsedHandled = false
hls.on(Hls.Events.MANIFEST_PARSED, () => { hls.on(Hls.Events.MANIFEST_PARSED, () => {
console.log('[HLS Setup] MANIFEST_PARSED event fired') logger.log('[HLS Setup] MANIFEST_PARSED event fired')
if (manifestParsedHandled) { if (manifestParsedHandled) {
console.warn('[HLS Setup] MANIFEST_PARSED already handled, skipping') logger.warn('[HLS Setup] MANIFEST_PARSED already handled, skipping')
return return
} }
manifestParsedHandled = true manifestParsedHandled = true
@@ -58,23 +59,23 @@ export const setupHlsInstance = async ({
const qualities = getHlsQualities(hls) const qualities = getHlsQualities(hls)
const subtitles = getHlsSubtitleTracks(hls) const subtitles = getHlsSubtitleTracks(hls)
console.log('[HLS Setup] Detected tracks:', { logger.log('[HLS Setup] Detected tracks:', {
audioTracks: tracks.length, audioTracks: tracks.length,
qualities: qualities.length, qualities: qualities.length,
subtitles: subtitles.length subtitles: subtitles.length
}) })
if (tracks.length > 0) { if (tracks.length > 0) {
console.log('[HLS Setup] Loading audio tracks:', tracks) logger.log('[HLS Setup] Loading audio tracks:', tracks)
onAudioTracksLoaded?.(tracks) onAudioTracksLoaded?.(tracks)
} }
if (subtitles.length > 0) { if (subtitles.length > 0) {
console.log('[HLS Setup] Loading subtitle tracks:', subtitles) logger.log('[HLS Setup] Loading subtitle tracks:', subtitles)
onSubtitleTracksLoaded?.(subtitles) onSubtitleTracksLoaded?.(subtitles)
} }
console.log('[HLS Setup] Loading quality levels:', qualities) logger.log('[HLS Setup] Loading quality levels:', qualities)
onQualityLevelsLoaded?.(qualities) onQualityLevelsLoaded?.(qualities)
} }
@@ -93,14 +94,14 @@ export const setupHlsInstance = async ({
hls.on(Hls.Events.LEVEL_LOADED, () => { hls.on(Hls.Events.LEVEL_LOADED, () => {
const qualities = getHlsQualities(hls) const qualities = getHlsQualities(hls)
if (qualities.length > 0) { if (qualities.length > 0) {
console.log('[HLS Setup] LEVEL_LOADED - Qualities available:', qualities.length) logger.log('[HLS Setup] LEVEL_LOADED - Qualities available:', qualities.length)
onQualityLevelsLoaded?.(qualities) onQualityLevelsLoaded?.(qualities)
} }
}) })
hls.on(Hls.Events.AUDIO_TRACKS_UPDATED, () => { hls.on(Hls.Events.AUDIO_TRACKS_UPDATED, () => {
const tracks = getHlsAudioTracks(hls) const tracks = getHlsAudioTracks(hls)
console.log('[HLS Setup] AUDIO_TRACKS_UPDATED event:', tracks.length, 'tracks') logger.log('[HLS Setup] AUDIO_TRACKS_UPDATED event:', tracks.length, 'tracks')
if (tracks.length > 0) { if (tracks.length > 0) {
onAudioTracksLoaded?.(tracks) onAudioTracksLoaded?.(tracks)
} }
@@ -108,7 +109,7 @@ export const setupHlsInstance = async ({
hls.on(Hls.Events.SUBTITLE_TRACKS_UPDATED, () => { hls.on(Hls.Events.SUBTITLE_TRACKS_UPDATED, () => {
const subtitles = getHlsSubtitleTracks(hls) const subtitles = getHlsSubtitleTracks(hls)
console.log('[HLS Setup] SUBTITLE_TRACKS_UPDATED event:', subtitles.length, 'tracks') logger.log('[HLS Setup] SUBTITLE_TRACKS_UPDATED event:', subtitles.length, 'tracks')
if (subtitles.length > 0) { if (subtitles.length > 0) {
onSubtitleTracksLoaded?.(subtitles) onSubtitleTracksLoaded?.(subtitles)
} }
@@ -139,3 +140,4 @@ export const setupHlsInstance = async ({
delete (video as any).__hlsInstance delete (video as any).__hlsInstance
} }
} }
+9
View File
@@ -0,0 +1,9 @@
type LoggerMethod = (..._args: unknown[]) => void
const noop: LoggerMethod = () => undefined
export const logger = {
log: noop,
warn: noop,
error: noop,
}
+8 -6
View File
@@ -2,6 +2,7 @@
* MPEG-TS loader utility * MPEG-TS loader utility
* Dynamically loads mpegts.js library * Dynamically loads mpegts.js library
*/ */
import { logger } from './logger'
export interface MpegtsConfig { export interface MpegtsConfig {
enableWorker?: boolean enableWorker?: boolean
@@ -29,26 +30,26 @@ let loadingPromise: Promise<any> | null = null
export const loadMpegts = async (): Promise<any> => { export const loadMpegts = async (): Promise<any> => {
// Return cached instance if available // Return cached instance if available
if (mpegtsInstance) { if (mpegtsInstance) {
console.log('[MPEG-TS Loader] Using cached mpegts.js instance') logger.log('[MPEG-TS Loader] Using cached mpegts.js instance')
return mpegtsInstance return mpegtsInstance
} }
// Return existing loading promise if already loading // Return existing loading promise if already loading
if (loadingPromise) { if (loadingPromise) {
console.log('[MPEG-TS Loader] Already loading, waiting for existing promise...') logger.log('[MPEG-TS Loader] Already loading, waiting for existing promise...')
return loadingPromise return loadingPromise
} }
// Start loading // Start loading
loadingPromise = (async () => { loadingPromise = (async () => {
try { try {
console.log('[MPEG-TS Loader] Attempting to load from npm package...') logger.log('[MPEG-TS Loader] Attempting to load from npm package...')
const module = await import('mpegts.js') const module = await import('mpegts.js')
mpegtsInstance = module.default || module mpegtsInstance = module.default || module
console.log('[MPEG-TS Loader] Successfully loaded from npm package') logger.log('[MPEG-TS Loader] Successfully loaded from npm package')
return mpegtsInstance return mpegtsInstance
} catch (error) { } catch (error) {
console.error('[MPEG-TS Loader] Failed to load mpegts.js:', error) logger.error('[MPEG-TS Loader] Failed to load mpegts.js:', error)
throw new Error('Failed to load mpegts.js. Make sure it is installed: npm install mpegts.js') throw new Error('Failed to load mpegts.js. Make sure it is installed: npm install mpegts.js')
} finally { } finally {
loadingPromise = null loadingPromise = null
@@ -103,5 +104,6 @@ export const getMpegtsInstance = (): any | null => {
export const clearMpegtsCache = (): void => { export const clearMpegtsCache = (): void => {
mpegtsInstance = null mpegtsInstance = null
loadingPromise = null loadingPromise = null
console.log('[MPEG-TS Loader] Cache cleared') logger.log('[MPEG-TS Loader] Cache cleared')
} }
+17 -15
View File
@@ -5,6 +5,7 @@
import { loadMpegts, isMpegtsSupported, createDefaultMpegtsConfig } from './mpegtsLoader' import { loadMpegts, isMpegtsSupported, createDefaultMpegtsConfig } from './mpegtsLoader'
import { isLiveStream } from './videoProtocol' import { isLiveStream } from './videoProtocol'
import { logger } from './logger'
export interface MpegtsSetupOptions { export interface MpegtsSetupOptions {
video: HTMLVideoElement video: HTMLVideoElement
@@ -41,7 +42,7 @@ export const setupMpegtsInstance = async ({
throw error throw error
} }
console.log('[MPEG-TS Setup] Creating player instance for:', src) logger.log('[MPEG-TS Setup] Creating player instance for:', src)
// Detect if stream is live // Detect if stream is live
const isLive = isLiveStream(src) const isLive = isLiveStream(src)
@@ -72,40 +73,40 @@ export const setupMpegtsInstance = async ({
// Event handlers // Event handlers
player.on(mpegts.Events.ERROR, (errorType: string, errorDetail: string, errorInfo: any) => { player.on(mpegts.Events.ERROR, (errorType: string, errorDetail: string, errorInfo: any) => {
console.error('mpegts.js error:', { errorType, errorDetail, errorInfo }) logger.error('mpegts.js error:', { errorType, errorDetail, errorInfo })
const error = new Error(`MPEG-TS Player Error: ${errorType} - ${errorDetail}`) const error = new Error(`MPEG-TS Player Error: ${errorType} - ${errorDetail}`)
// Handle specific error types // Handle specific error types
if (errorType === mpegts.ErrorTypes.NETWORK_ERROR) { if (errorType === mpegts.ErrorTypes.NETWORK_ERROR) {
console.error('Network error occurred:', errorDetail) logger.error('Network error occurred:', errorDetail)
// Attempt recovery for recoverable network errors // Attempt recovery for recoverable network errors
if ( if (
errorDetail === mpegts.ErrorDetails.NETWORK_EXCEPTION || errorDetail === mpegts.ErrorDetails.NETWORK_EXCEPTION ||
errorDetail === mpegts.ErrorDetails.NETWORK_STATUS_CODE_INVALID errorDetail === mpegts.ErrorDetails.NETWORK_STATUS_CODE_INVALID
) { ) {
console.log('Attempting to recover from network error...') logger.log('Attempting to recover from network error...')
try { try {
player.unload() player.unload()
player.load() player.load()
return return
} catch (recoveryError) { } catch (recoveryError) {
console.error('Failed to recover from network error:', recoveryError) logger.error('Failed to recover from network error:', recoveryError)
} }
} }
} else if (errorType === mpegts.ErrorTypes.MEDIA_ERROR) { } else if (errorType === mpegts.ErrorTypes.MEDIA_ERROR) {
console.error('Media error occurred:', errorDetail) logger.error('Media error occurred:', errorDetail)
// Some media errors are recoverable // Some media errors are recoverable
if (errorDetail === mpegts.ErrorDetails.MEDIA_MSE_ERROR) { if (errorDetail === mpegts.ErrorDetails.MEDIA_MSE_ERROR) {
console.log('Attempting to recover from media error...') logger.log('Attempting to recover from media error...')
try { try {
player.unload() player.unload()
player.load() player.load()
return return
} catch (recoveryError) { } catch (recoveryError) {
console.error('Failed to recover from media error:', recoveryError) logger.error('Failed to recover from media error:', recoveryError)
} }
} }
} }
@@ -117,15 +118,15 @@ export const setupMpegtsInstance = async ({
}) })
player.on(mpegts.Events.LOADING_COMPLETE, () => { player.on(mpegts.Events.LOADING_COMPLETE, () => {
console.log('mpegts.js: Loading complete') logger.log('mpegts.js: Loading complete')
}) })
player.on(mpegts.Events.RECOVERED_EARLY_EOF, () => { player.on(mpegts.Events.RECOVERED_EARLY_EOF, () => {
console.log('mpegts.js: Recovered from early EOF') logger.log('mpegts.js: Recovered from early EOF')
}) })
player.on(mpegts.Events.METADATA_ARRIVED, (metadata: any) => { player.on(mpegts.Events.METADATA_ARRIVED, (metadata: any) => {
console.log('mpegts.js: Metadata arrived', metadata) logger.log('mpegts.js: Metadata arrived', metadata)
// Trigger onLoadedMetadata callback // Trigger onLoadedMetadata callback
if (onLoadedMetadata) { if (onLoadedMetadata) {
@@ -145,7 +146,7 @@ export const setupMpegtsInstance = async ({
try { try {
await video.play() await video.play()
} catch (playError) { } catch (playError) {
console.warn('Autoplay failed:', playError) logger.warn('Autoplay failed:', playError)
// Autoplay might be blocked by browser, ignore error // Autoplay might be blocked by browser, ignore error
} }
} }
@@ -153,7 +154,7 @@ export const setupMpegtsInstance = async ({
// Return cleanup function // Return cleanup function
return () => { return () => {
try { try {
console.log('Cleaning up mpegts.js player...') logger.log('Cleaning up mpegts.js player...')
// Remove event listeners // Remove event listeners
player.off(mpegts.Events.ERROR) player.off(mpegts.Events.ERROR)
@@ -176,11 +177,11 @@ export const setupMpegtsInstance = async ({
delete (video as any).__mpegtsInstance delete (video as any).__mpegtsInstance
delete (video as any).__mpegtsStats delete (video as any).__mpegtsStats
} catch (cleanupError) { } catch (cleanupError) {
console.error('Error during mpegts.js cleanup:', cleanupError) logger.error('Error during mpegts.js cleanup:', cleanupError)
} }
} }
} catch (error) { } catch (error) {
console.error('Failed to setup mpegts.js player:', error) logger.error('Failed to setup mpegts.js player:', error)
const setupError = const setupError =
error instanceof Error ? error : new Error('Failed to setup MPEG-TS player') error instanceof Error ? error : new Error('Failed to setup MPEG-TS player')
@@ -225,3 +226,4 @@ export const getMpegtsStats = (video: HTMLVideoElement | null): any | null => {
export const hasMpegtsInstance = (video: HTMLVideoElement | null): boolean => { export const hasMpegtsInstance = (video: HTMLVideoElement | null): boolean => {
return getMpegtsInstance(video) !== null return getMpegtsInstance(video) !== null
} }
+5 -3
View File
@@ -3,6 +3,7 @@
* Loads flv.js library with NPM fallback to CDN strategy * Loads flv.js library with NPM fallback to CDN strategy
* Mirrors the HLS loader pattern for consistency * Mirrors the HLS loader pattern for consistency
*/ */
import { logger } from './logger'
const FLVJS_CDN_URL = 'https://cdn.jsdelivr.net/npm/flv.js@1.6.2/dist/flv.min.js' const FLVJS_CDN_URL = 'https://cdn.jsdelivr.net/npm/flv.js@1.6.2/dist/flv.min.js'
@@ -48,14 +49,14 @@ export const loadFlvjs = async (): Promise<any> => {
const flvModule = await import('flv.js') const flvModule = await import('flv.js')
return flvModule.default || flvModule return flvModule.default || flvModule
} catch (npmError) { } catch (npmError) {
console.warn('flv.js NPM package not available, loading from CDN...', npmError) logger.warn('flv.js NPM package not available, loading from CDN...', npmError)
try { try {
// Fallback to CDN // Fallback to CDN
const flvjs = await loadFlvjsFromCDN() const flvjs = await loadFlvjsFromCDN()
return flvjs return flvjs
} catch (cdnError) { } catch (cdnError) {
console.error('Failed to load flv.js from both NPM and CDN', cdnError) logger.error('Failed to load flv.js from both NPM and CDN', cdnError)
throw new Error( throw new Error(
'Failed to load flv.js library. Please ensure flv.js is available or check your network connection.' 'Failed to load flv.js library. Please ensure flv.js is available or check your network connection.'
) )
@@ -162,7 +163,8 @@ export const extractFlvQualityInfo = (player: any): {
audioBitrate: stats.audioBitrate, audioBitrate: stats.audioBitrate,
} }
} catch (error) { } catch (error) {
console.warn('Failed to extract flv.js quality info:', error) logger.warn('Failed to extract flv.js quality info:', error)
return null return null
} }
} }
+17 -15
View File
@@ -6,6 +6,7 @@
import { loadFlvjs, isFlvjsSupported, createDefaultFlvConfig } from './rtmpLoader' import { loadFlvjs, isFlvjsSupported, createDefaultFlvConfig } from './rtmpLoader'
import { isLiveStream } from './videoProtocol' import { isLiveStream } from './videoProtocol'
import { logger } from './logger'
export interface RtmpSetupOptions { export interface RtmpSetupOptions {
video: HTMLVideoElement video: HTMLVideoElement
@@ -50,7 +51,7 @@ export const setupRtmpInstance = async ({
if (src.startsWith('rtmp://') || src.startsWith('rtmps://')) { if (src.startsWith('rtmp://') || src.startsWith('rtmps://')) {
// For RTMP URLs, flv.js expects HTTP-FLV endpoint // For RTMP URLs, flv.js expects HTTP-FLV endpoint
// This is a limitation - direct RTMP playback requires server-side conversion // This is a limitation - direct RTMP playback requires server-side conversion
console.warn( logger.warn(
'Direct RTMP playback requires an HTTP-FLV proxy. Please ensure your RTMP stream is available via HTTP-FLV.' 'Direct RTMP playback requires an HTTP-FLV proxy. Please ensure your RTMP stream is available via HTTP-FLV.'
) )
type = 'flv' type = 'flv'
@@ -84,40 +85,40 @@ export const setupRtmpInstance = async ({
// Event handlers // Event handlers
player.on(flvjs.Events.ERROR, (errorType: string, errorDetail: string, errorInfo: any) => { player.on(flvjs.Events.ERROR, (errorType: string, errorDetail: string, errorInfo: any) => {
console.error('flv.js error:', { errorType, errorDetail, errorInfo }) logger.error('flv.js error:', { errorType, errorDetail, errorInfo })
const error = new Error(`FLV Player Error: ${errorType} - ${errorDetail}`) const error = new Error(`FLV Player Error: ${errorType} - ${errorDetail}`)
// Handle specific error types // Handle specific error types
if (errorType === flvjs.ErrorTypes.NETWORK_ERROR) { if (errorType === flvjs.ErrorTypes.NETWORK_ERROR) {
console.error('Network error occurred:', errorDetail) logger.error('Network error occurred:', errorDetail)
// Attempt recovery for recoverable network errors // Attempt recovery for recoverable network errors
if ( if (
errorDetail === flvjs.ErrorDetails.NETWORK_EXCEPTION || errorDetail === flvjs.ErrorDetails.NETWORK_EXCEPTION ||
errorDetail === flvjs.ErrorDetails.NETWORK_STATUS_CODE_INVALID errorDetail === flvjs.ErrorDetails.NETWORK_STATUS_CODE_INVALID
) { ) {
console.log('Attempting to recover from network error...') logger.log('Attempting to recover from network error...')
try { try {
player.unload() player.unload()
player.load() player.load()
return return
} catch (recoveryError) { } catch (recoveryError) {
console.error('Failed to recover from network error:', recoveryError) logger.error('Failed to recover from network error:', recoveryError)
} }
} }
} else if (errorType === flvjs.ErrorTypes.MEDIA_ERROR) { } else if (errorType === flvjs.ErrorTypes.MEDIA_ERROR) {
console.error('Media error occurred:', errorDetail) logger.error('Media error occurred:', errorDetail)
// Some media errors are recoverable // Some media errors are recoverable
if (errorDetail === flvjs.ErrorDetails.MEDIA_MSE_ERROR) { if (errorDetail === flvjs.ErrorDetails.MEDIA_MSE_ERROR) {
console.log('Attempting to recover from media error...') logger.log('Attempting to recover from media error...')
try { try {
player.unload() player.unload()
player.load() player.load()
return return
} catch (recoveryError) { } catch (recoveryError) {
console.error('Failed to recover from media error:', recoveryError) logger.error('Failed to recover from media error:', recoveryError)
} }
} }
} }
@@ -129,15 +130,15 @@ export const setupRtmpInstance = async ({
}) })
player.on(flvjs.Events.LOADING_COMPLETE, () => { player.on(flvjs.Events.LOADING_COMPLETE, () => {
console.log('flv.js: Loading complete') logger.log('flv.js: Loading complete')
}) })
player.on(flvjs.Events.RECOVERED_EARLY_EOF, () => { player.on(flvjs.Events.RECOVERED_EARLY_EOF, () => {
console.log('flv.js: Recovered from early EOF') logger.log('flv.js: Recovered from early EOF')
}) })
player.on(flvjs.Events.METADATA_ARRIVED, (metadata: any) => { player.on(flvjs.Events.METADATA_ARRIVED, (metadata: any) => {
console.log('flv.js: Metadata arrived', metadata) logger.log('flv.js: Metadata arrived', metadata)
// Trigger onLoadedMetadata callback // Trigger onLoadedMetadata callback
if (onLoadedMetadata) { if (onLoadedMetadata) {
@@ -158,7 +159,7 @@ export const setupRtmpInstance = async ({
try { try {
await video.play() await video.play()
} catch (playError) { } catch (playError) {
console.warn('Autoplay failed:', playError) logger.warn('Autoplay failed:', playError)
// Autoplay might be blocked by browser, ignore error // Autoplay might be blocked by browser, ignore error
} }
} }
@@ -166,7 +167,7 @@ export const setupRtmpInstance = async ({
// Return cleanup function // Return cleanup function
return () => { return () => {
try { try {
console.log('Cleaning up flv.js player...') logger.log('Cleaning up flv.js player...')
// Remove event listeners // Remove event listeners
player.off(flvjs.Events.ERROR) player.off(flvjs.Events.ERROR)
@@ -189,11 +190,11 @@ export const setupRtmpInstance = async ({
delete (video as any).__rtmpInstance delete (video as any).__rtmpInstance
delete (video as any).__rtmpStats delete (video as any).__rtmpStats
} catch (cleanupError) { } catch (cleanupError) {
console.error('Error during flv.js cleanup:', cleanupError) logger.error('Error during flv.js cleanup:', cleanupError)
} }
} }
} catch (error) { } catch (error) {
console.error('Failed to setup flv.js player:', error) logger.error('Failed to setup flv.js player:', error)
const setupError = const setupError =
error instanceof Error ? error : new Error('Failed to setup RTMP/FLV player') error instanceof Error ? error : new Error('Failed to setup RTMP/FLV player')
@@ -238,3 +239,4 @@ export const getRtmpStats = (video: HTMLVideoElement | null): any | null => {
export const hasRtmpInstance = (video: HTMLVideoElement | null): boolean => { export const hasRtmpInstance = (video: HTMLVideoElement | null): boolean => {
return getRtmpInstance(video) !== null return getRtmpInstance(video) !== null
} }