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ı
# Bu dosyayı diğer projelerinize kopyalayın ve .npmrc olarak kaydedin
# @alper scope'u için Gitea registry'yi kullan
@alper:registry=https://gitea.yourdomain.com/api/packages/your-username/npm/
# @source scope'u için private registry kullan
@source:registry=https://gits.yourdomain.com/api/packages/your-username/npm/
# 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)
# //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:
#
# Linux/Mac:
# export GITEA_TOKEN=your-token-here
# export GITS_NPM_TOKEN=your-token-here
# # veya ~/.bashrc veya ~/.zshrc dosyasına ekleyin
#
# Windows (PowerShell):
# $env:GITEA_TOKEN="your-token-here"
# $env:GITS_NPM_TOKEN="your-token-here"
# # veya sistem environment variables'a ekleyin
#
# .env dosyası (projede):
# GITEA_TOKEN=your-token-here
# GITS_NPM_TOKEN=your-token-here
# Not: Bu dosyayı .gitignore'a ekleyin!
# 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**
[![npm version](https://img.shields.io/npm/v/@alper/video-player.svg?style=flat-square)](https://www.npmjs.com/package/@alper/video-player)
[![Bundle Size](https://img.shields.io/bundlephobia/minzip/@alper/video-player?style=flat-square)](https://bundlephobia.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/@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/)
[![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ış
`@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 ⚠️ |
| **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 ⚠️ |
| **HLS Desteği** | **Evet** ✅ | Evet ✅ | Evet ✅ | 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ı
`@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
@@ -119,20 +119,20 @@ New-Item -Path .npmrc -ItemType File
`.npmrc` dosyasını aşağıdaki içerikle doldurun:
```ini
# @alper scope'u için Gitea registry'yi kullan
@alper:registry=https://gitea.hibna.com.tr/api/packages/hibna/npm/
# @source scope'u için private registry'yi kullan
@source:registry=https://gits.hibna.com.tr/api/packages/hibna/npm/
# 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
//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!
#### 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**
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
@@ -168,28 +168,28 @@ Daha güvenli bir yöntem için token'ı environment variable olarak saklayabili
**.npmrc dosyası:**
```ini
@alper:registry=https://gitea.hibna.com.tr/api/packages/hibna/npm/
//gitea.hibna.com.tr/api/packages/hibna/npm/:_authToken=${GITEA_TOKEN}
@source:registry=https://gits.hibna.com.tr/api/packages/hibna/npm/
//gits.hibna.com.tr/api/packages/hibna/npm/:_authToken=${GITS_NPM_TOKEN}
```
**Environment variable ayarlama:**
```bash
# 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)
$env:GITEA_TOKEN="your-actual-token-here"
$env:GITS_NPM_TOKEN="your-actual-token-here"
# Windows (Kalıcı - Sistem Environment Variables)
# 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
```
**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
@@ -197,13 +197,13 @@ Repository settings → Secrets → `GITEA_TOKEN` adında bir secret oluşturun.
```bash
# npm ile
npm install @alper/video-player
npm install @source/player
# veya pnpm ile (önerilen - daha hızlı)
pnpm add @alper/video-player
pnpm add @source/player
# veya yarn ile
yarn add @alper/video-player
yarn add @source/player
```
### 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:
```bash
npm list @alper/video-player
npm list @source/player
# veya
pnpm list @alper/video-player
pnpm list @source/player
```
Çıktı şöyle olmalı:
```
your-project@1.0.0
└─┬ @alper/video-player@0.1.5
└─┬ @source/player@0.1.5
```
### Sorun Giderme
@@ -254,11 +254,11 @@ your-project@1.0.0
**Problem: "404 Not Found" hatası**
- 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
- 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:
```bash
rm -rf node_modules package-lock.json
@@ -272,8 +272,8 @@ your-project@1.0.0
### Temel Kullanım
```tsx
import { VideoPlayer } from '@alper/video-player'
import '@alper/video-player/styles.css'
import { VideoPlayer } from '@source/player'
import '@source/player/styles.css'
function App() {
return (
@@ -630,7 +630,7 @@ function detectVideoProtocol(url: string): {
**Kullanım:**
```typescript
import { detectVideoProtocol } from '@alper/video-player'
import { detectVideoProtocol } from '@source/player'
const info = detectVideoProtocol('https://example.com/stream.m3u8')
console.log(info.protocol) // 'hls'
@@ -690,7 +690,7 @@ Merhaba dünya
**Dönüşüm Fonksiyonu:**
```typescript
import { parseSRT, createSubtitleBlobURL } from '@alper/video-player'
import { parseSRT, createSubtitleBlobURL } from '@source/player'
const srtContent = '...' // SRT içeriği
const vttContent = parseSRT(srtContent)
@@ -737,7 +737,7 @@ const blobUrl = createSubtitleBlobURL(vttContent)
**Programmatic Control:**
```tsx
import { usePlayerContext } from '@alper/video-player'
import { usePlayerContext } from '@source/player'
function CustomSubtitleToggle() {
const { settings, setSubtitle } = usePlayerContext()
@@ -789,7 +789,7 @@ function CustomSubtitleToggle() {
**Programmatic Control:**
```tsx
import { usePlayerContext } from '@alper/video-player'
import { usePlayerContext } from '@source/player'
function AudioTrackSelector() {
const { settings, setAudioTrack } = usePlayerContext()
@@ -867,7 +867,7 @@ function AudioTrackSelector() {
**Otomatik Kalite (Adaptive Bitrate):**
```typescript
import { setHlsQualityLevel } from '@alper/video-player'
import { setHlsQualityLevel } from '@source/player'
setHlsQualityLevel(hlsInstance, null) // Auto
```
@@ -888,7 +888,7 @@ Kullanıcı Settings → Quality menüsünden kalite seçer.
**Programmatic Control:**
```tsx
import { usePlayerContext } from '@alper/video-player'
import { usePlayerContext } from '@source/player'
function QualitySelector() {
const { settings, setQuality } = usePlayerContext()
@@ -974,7 +974,7 @@ function QualitySelector() {
**Custom hook ile:**
```tsx
import { useKeyboardShortcuts } from '@alper/video-player'
import { useKeyboardShortcuts } from '@source/player'
function MyComponent() {
const { videoRef, containerRef } = usePlayerContext()
@@ -1039,7 +1039,7 @@ function MyComponent() {
**Custom hook ile:**
```tsx
import { useTouchGestures } from '@alper/video-player'
import { useTouchGestures } from '@source/player'
function MyComponent() {
const { videoRef, containerRef } = usePlayerContext()
@@ -1411,20 +1411,20 @@ interface PlayerContextValue {
```typescript
// Ana bileşen
import { VideoPlayer } from '@alper/video-player'
import { VideoPlayer } from '@source/player'
```
### Exported Hooks
```typescript
// Player context hook
import { usePlayerContext } from '@alper/video-player'
import { usePlayerContext } from '@source/player'
// Klavye kısayolları hook'u
import { useKeyboardShortcuts } from '@alper/video-player'
import { useKeyboardShortcuts } from '@source/player'
// Dokunmatik jest hook'u
import { useTouchGestures } from '@alper/video-player'
import { useTouchGestures } from '@source/player'
```
**usePlayerContext Kullanımı:**
@@ -1458,7 +1458,7 @@ function CustomControl() {
```typescript
// Zaman formatlama
import { formatTime, parseTime } from '@alper/video-player'
import { formatTime, parseTime } from '@source/player'
formatTime(125) // "2:05"
formatTime(3665) // "1:01:05"
@@ -1470,7 +1470,7 @@ import {
parseSRT,
createSubtitleBlobURL,
fetchSubtitle
} from '@alper/video-player'
} from '@source/player'
const srtContent = "1\n00:00:01,000 --> 00:00:04,000\nMerhaba"
const vttContent = parseSRT(srtContent)
@@ -1483,7 +1483,7 @@ import {
getCORSErrorMessage,
isCORSError,
checkVideoCORS
} from '@alper/video-player'
} from '@source/player'
const validation = validateVideoURL(url)
if (!validation.valid) {
@@ -1505,7 +1505,7 @@ import {
getTranslations,
detectBrowserLanguage,
translations
} from '@alper/video-player'
} from '@source/player'
const lang = detectBrowserLanguage() // "tr", "en", vb.
const t = getTranslations('tr')
@@ -1520,8 +1520,8 @@ console.log(translations.tr.quality) // "Kalite"
### Temel MP4 Oynatma
```tsx
import { VideoPlayer } from '@alper/video-player'
import '@alper/video-player/styles.css'
import { VideoPlayer } from '@source/player'
import '@source/player/styles.css'
function App() {
return (
@@ -1603,7 +1603,7 @@ function VideoWithAnalytics() {
### Custom Kontroller
```tsx
import { VideoPlayer, usePlayerContext } from '@alper/video-player'
import { VideoPlayer, usePlayerContext } from '@source/player'
function CustomControls() {
const {
@@ -1660,7 +1660,7 @@ function App() {
```tsx
import { useState } from 'react'
import { VideoPlayer } from '@alper/video-player'
import { VideoPlayer } from '@source/player'
const videos = [
{ id: 1, src: 'video1.mp4', title: 'Video 1' },
@@ -1706,7 +1706,7 @@ function Playlist() {
### CORS Hata Yönetimi
```tsx
import { VideoPlayer, isCORSError, getCORSErrorMessage } from '@alper/video-player'
import { VideoPlayer, isCORSError, getCORSErrorMessage } from '@source/player'
import { useState } from 'react'
function VideoWithCORSHandling() {
@@ -1762,7 +1762,7 @@ import {
hasTouch,
isIOSSafari,
hasVolumeControl
} from '@alper/video-player'
} from '@source/player'
// Safari'de native HLS var mı kontrol et
if (hasNativeHLS()) {
@@ -1795,7 +1795,7 @@ if (hasVolumeControl()) {
### Manual HLS.js Setup
```typescript
import { loadHls, setupHls } from '@alper/video-player'
import { loadHls, setupHls } from '@source/player'
async function customHlsSetup() {
const videoElement = document.querySelector('video')
@@ -1829,7 +1829,7 @@ async function customHlsSetup() {
### Custom Subtitle Processing
```typescript
import { parseSRT, createSubtitleBlobURL, fetchSubtitle } from '@alper/video-player'
import { parseSRT, createSubtitleBlobURL, fetchSubtitle } from '@source/player'
async function loadCustomSubtitle(url: string) {
// SRT dosyasını fetch et
@@ -2077,7 +2077,7 @@ interface Translations {
### Programmatic Access
```typescript
import { getTranslations, detectBrowserLanguage } from '@alper/video-player'
import { getTranslations, detectBrowserLanguage } from '@source/player'
// Tarayıcı dilini tespit et
const browserLang = detectBrowserLanguage() // "tr", "en-US", vb.
@@ -2170,7 +2170,7 @@ export const translations: Record<string, Translations> = {
```tsx
import { useEffect } from 'react'
import { VideoPlayer } from '@alper/video-player'
import { VideoPlayer } from '@source/player'
function MonitoredVideo() {
useEffect(() => {
@@ -2204,7 +2204,7 @@ function MonitoredVideo() {
import { lazy, Suspense } from 'react'
const VideoPlayer = lazy(() =>
import('@alper/video-player').then(module => ({
import('@source/player').then(module => ({
default: module.VideoPlayer
}))
)
@@ -2300,7 +2300,7 @@ Kütüphane, eski tarayıcılar için otomatik polyfill içerir:
**Tespit:**
```typescript
import { isCORSError, getCORSErrorMessage } from '@alper/video-player'
import { isCORSError, getCORSErrorMessage } from '@source/player'
const handleError = (error: Error) => {
if (isCORSError(error)) {
@@ -2437,7 +2437,7 @@ function RobustVideoPlayer() {
```bash
# 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
# Bağımlılıkları yükleyin
@@ -2542,7 +2542,7 @@ Katkılarınızı bekliyoruz! Lütfen şu adımları takip edin:
```bash
# Repository'yi fork edin (Gitea UI'dan)
# 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
```
@@ -2623,9 +2623,9 @@ SOFTWARE.
## 📞 İletişim
- **Repository:** https://gitea.hibna.com.tr/hibna/video-player
- **NPM Registry:** https://gitea.hibna.com.tr/api/packages/hibna/npm/
- **Issues:** https://gitea.hibna.com.tr/hibna/video-player/issues
- **Repository:** https://gits.hibna.com.tr/hibna/video-player
- **NPM Registry:** https://gits.hibna.com.tr/api/packages/hibna/npm/
- **Issues:** https://gits.hibna.com.tr/hibna/video-player/issues
- **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**
*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 ⚠️ |
| **Runtime Dependencies** | **0** ✅ | Many ❌ | Few ⚠️ | Few ⚠️ |
| **React Native** | **Yes** ✅ | Wrapper ⚠️ | **Yes** ✅ | Wrapper ⚠️ |
| **React (Web)** | **Yes** ✅ | Wrapper ⚠️ | **Yes** ✅ | Wrapper ⚠️ |
| **TypeScript Native** | **Yes** ✅ | Types ⚠️ | Partial ⚠️ | Types ⚠️ |
| **HLS Support** | **Yes** ✅ | Yes ✅ | Yes ✅ | 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
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
```bash
# Copy the src folder to your project
cp -r src/components your-project/src/
cp -r src/contexts your-project/src/
cp -r src/hooks your-project/src/
cp -r src/utils your-project/src/
cp -r src/types your-project/src/
cp -r src/icons your-project/src/
cp -r src/styles your-project/src/
### 1. Configure `.npmrc`
Create `.npmrc` in your app root:
```ini
@source:registry=https://gits.hibna.com.tr/api/packages/hibna/npm/
//gits.hibna.com.tr/api/packages/hibna/npm/:_authToken=${GITS_NPM_TOKEN}
```
### 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
# 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 link
# In your other project
# In your consuming app
npm link @source/player
```
+2 -2
View File
@@ -31,7 +31,7 @@ export const ControlsLayer: React.FC<ControlsLayerProps> = ({
audioTracks = [],
qualities = [],
}) => {
const { videoState, uiState, togglePlay, toggleFullscreen, showControls, hideControls } =
const { videoState, uiState, togglePlay, toggleFullscreen, showControls, hideControls, translations } =
usePlayerContext()
const [isPointerOver, setIsPointerOver] = useState(false)
const [lastInteraction, setLastInteraction] = useState<number>(0)
@@ -228,7 +228,7 @@ export const ControlsLayer: React.FC<ControlsLayerProps> = ({
{videoState.isLiveBroadcast && (
<div className="live-indicator">
<span className="live-dot"></span>
<span className="live-text">LIVE</span>
<span className="live-text">{translations.live}</span>
</div>
)}
</div>
+25 -106
View File
@@ -8,6 +8,7 @@ import { setupRtmpInstance } from '../utils/rtmpSetup'
import { setupMpegtsInstance } from '../utils/mpegtsSetup'
import { detectVideoProtocol } from '../utils/videoProtocol'
import { createSubtitleBlobURL } from '../utils/subtitles'
import { logger } from '../utils/logger'
import './VideoElement.css'
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)
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) => ({
...prev,
@@ -127,7 +128,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
if (tracks && processedSubtitles.length > 0) {
const defaultSubtitle = processedSubtitles.find((sub) => sub.default)
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)
setSubtitle(defaultSubtitle)
}
@@ -142,7 +143,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
// Re-check if this is a live broadcast when duration changes
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) => ({
...prev,
@@ -339,25 +340,16 @@ export const VideoElement: React.FC<VideoElementProps> = ({
throw new Error(`Failed to fetch subtitle: ${response.status} ${response.statusText}`)
}
const srtContent = await response.text()
console.log(`SRT content length: ${srtContent.length} chars`)
const blobUrl = createSubtitleBlobURL(srtContent, 'srt')
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 }
}
// VTT files can be used directly
console.log(`Using VTT subtitle "${subtitle.label}": ${subtitle.src}`)
return subtitle
} catch (error) {
console.error(`Failed to process subtitle ${subtitle.label}:`, error)
logger.error(`Failed to process subtitle ${subtitle.label}:`, error)
return subtitle
}
})
@@ -459,10 +451,10 @@ export const VideoElement: React.FC<VideoElementProps> = ({
}
}
console.log('[VideoElement] Source:', src)
console.log('[VideoElement] Detected protocol:', detection.protocol)
console.log('[VideoElement] Is live stream?', detection.isLive)
console.log('[VideoElement] Needs special player?', detection.needsSpecialPlayer)
logger.log('[VideoElement] Source:', src)
logger.log('[VideoElement] Detected protocol:', detection.protocol)
logger.log('[VideoElement] Is live stream?', detection.isLive)
logger.log('[VideoElement] Needs special player?', detection.needsSpecialPlayer)
const setupPlayer = async () => {
try {
@@ -473,12 +465,12 @@ export const VideoElement: React.FC<VideoElementProps> = ({
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
const shouldUseHlsJs = canPlayHLS === '' || !isSafari
console.log('[VideoElement] Native HLS support?', canPlayHLS)
console.log('[VideoElement] Is Safari?', isSafari)
console.log('[VideoElement] Will use HLS.js?', shouldUseHlsJs)
logger.log('[VideoElement] Native HLS support?', canPlayHLS)
logger.log('[VideoElement] Is Safari?', isSafari)
logger.log('[VideoElement] Will use HLS.js?', shouldUseHlsJs)
if (shouldUseHlsJs) {
console.log('[VideoElement] Setting up HLS.js...')
logger.log('[VideoElement] Setting up HLS.js...')
cleanupFn = await setupHlsInstance({
video,
src,
@@ -507,7 +499,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
}
} else {
if (isCancelled) return
console.log('[VideoElement] Using native HLS playback')
logger.log('[VideoElement] Using native HLS playback')
video.src = src
if (autoplay) {
void video.play().catch(() => undefined)
@@ -518,7 +510,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
case 'rtmp': {
// RTMP/FLV streaming setup
console.log('[VideoElement] Setting up RTMP/FLV player...')
logger.log('[VideoElement] Setting up RTMP/FLV player...')
cleanupFn = await setupRtmpInstance({
video,
src,
@@ -536,7 +528,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
case 'mpegts': {
// MPEG-TS/IPTV streaming setup
console.log('[VideoElement] Setting up MPEG-TS player...')
logger.log('[VideoElement] Setting up MPEG-TS player...')
cleanupFn = await setupMpegtsInstance({
video,
src,
@@ -556,7 +548,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
// DASH streaming - not yet implemented
if (isCancelled) return
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 }))
onError?.(error)
break
@@ -566,7 +558,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
default: {
// Native HTML5 video (MP4, WebM, etc.)
if (isCancelled) return
console.log('[VideoElement] Using native video.src')
logger.log('[VideoElement] Using native video.src')
video.src = src
if (autoplay) {
void video.play().catch(() => undefined)
@@ -585,7 +577,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
if (isCancelled) return
console.error('[VideoElement] Setup error:', error)
logger.error('[VideoElement] Setup error:', error)
setVideoState((prev) => ({
...prev,
error,
@@ -728,11 +720,11 @@ export const VideoElement: React.FC<VideoElementProps> = ({
// Wait for track to have cues before showing
if (track.cues && track.cues.length > 0) {
track.mode = 'showing'
console.log(`🔊 Enabled subtitle track: ${track.label} (${track.language})`)
console.log(` - cues available: ${track.cues.length}`)
console.log(` - track.mode: ${track.mode}`)
logger.log(`🔊 Enabled subtitle track: ${track.label} (${track.language})`)
logger.log(` - cues available: ${track.cues.length}`)
logger.log(` - track.mode: ${track.mode}`)
} 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.mode = 'showing'
}
@@ -747,7 +739,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
// Also listen for track load events to retry
const handleTrackChange = () => {
console.log(`🔄 Track changed, re-enabling subtitle`)
logger.log(`🔄 Track changed, re-enabling subtitle`)
enableSubtitle()
}
@@ -762,80 +754,6 @@ export const VideoElement: React.FC<VideoElementProps> = ({
}
}, [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 (
<div className="video-container">
<video
@@ -876,3 +794,4 @@ export const VideoElement: React.FC<VideoElementProps> = ({
</div>
)
}
+3 -3
View File
@@ -4,7 +4,7 @@ import { PlayIcon } from '../../icons'
import './CenterPlayButton.css'
export const CenterPlayButton: React.FC = () => {
const { play } = usePlayerContext()
const { play, translations } = usePlayerContext()
return (
<div className="center-play-overlay">
@@ -12,8 +12,8 @@ export const CenterPlayButton: React.FC = () => {
className="center-play-button"
type="button"
onClick={play}
aria-label="Play"
title="Play"
aria-label={translations.play}
title={translations.play}
>
<PlayIcon size={72} color="var(--player-text)" />
</button>
+6 -3
View File
@@ -4,14 +4,17 @@ import { FullscreenIcon, FullscreenExitIcon } from '../../icons'
import './ControlButton.css'
export const FullscreenButton: React.FC = () => {
const { videoState, toggleFullscreen } = usePlayerContext()
const { videoState, toggleFullscreen, translations } = usePlayerContext()
const actionLabel = videoState.fullscreen
? translations.exitFullscreen
: translations.enterFullscreen
return (
<button
className="control-button fullscreen-button"
onClick={toggleFullscreen}
aria-label={videoState.fullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
title={videoState.fullscreen ? 'Exit fullscreen (F)' : 'Enter fullscreen (F)'}
aria-label={actionLabel}
title={`${actionLabel} (F)`}
>
{videoState.fullscreen ? (
<FullscreenExitIcon size={24} color="var(--player-text)" />
+6 -3
View File
@@ -4,7 +4,10 @@ import { PIPIcon } from '../../icons'
import './ControlButton.css'
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
const isPIPSupported =
@@ -20,8 +23,8 @@ export const PIPButton: React.FC = () => {
<button
className="control-button pip-button"
onClick={togglePictureInPicture}
aria-label={videoState.pictureInPicture ? 'Exit picture-in-picture' : 'Enter picture-in-picture'}
title="Picture-in-picture (P)"
aria-label={actionLabel}
title={`${actionLabel} (P)`}
>
<PIPIcon size={24} color="var(--player-text)" />
</button>
+4 -3
View File
@@ -4,14 +4,15 @@ import { PlayIcon, PauseIcon } from '../../icons'
import './ControlButton.css'
export const PlayPauseButton: React.FC = () => {
const { videoState, togglePlay } = usePlayerContext()
const { videoState, togglePlay, translations } = usePlayerContext()
const actionLabel = videoState.playing ? translations.pause : translations.play
return (
<button
className="control-button play-pause-button"
onClick={togglePlay}
aria-label={videoState.playing ? 'Pause' : 'Play'}
title={videoState.playing ? 'Pause (Space)' : 'Play (Space)'}
aria-label={actionLabel}
title={`${actionLabel} (Space)`}
>
{videoState.playing ? (
<PauseIcon size={24} color="var(--player-text)" />
+2 -2
View File
@@ -3,7 +3,7 @@ import { usePlayerContext } from '../../contexts/PlayerContext'
import './ProgressBar.css'
export const ProgressBar: React.FC = () => {
const { videoState, seek } = usePlayerContext()
const { videoState, seek, translations } = usePlayerContext()
const progressRef = useRef<HTMLDivElement>(null)
const [seeking, setSeeking] = useState(false)
const [hoverTime, setHoverTime] = useState<number | null>(null)
@@ -83,7 +83,7 @@ export const ProgressBar: React.FC = () => {
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseLeave}
role="slider"
aria-label="Video progress"
aria-label={translations.videoProgress}
aria-valuemin={0}
aria-valuemax={videoState.duration}
aria-valuenow={videoState.currentTime}
+3 -3
View File
@@ -4,15 +4,15 @@ import { SettingsIcon } from '../../icons'
import './ControlButton.css'
export const SettingsButton: React.FC = () => {
const { toggleSettings } = usePlayerContext()
const { toggleSettings, translations } = usePlayerContext()
return (
<button
className="control-button settings-button"
onMouseDown={(event) => event.stopPropagation()}
onClick={toggleSettings}
aria-label="Settings"
title="Settings"
aria-label={translations.settings}
title={translations.settings}
>
<SettingsIcon size={24} color="var(--player-text)" />
</button>
+5 -4
View File
@@ -4,7 +4,7 @@ import { VolumeUpIcon, VolumeDownIcon, VolumeMuteIcon } from '../../icons'
import './VolumeControl.css'
export const VolumeControl: React.FC = () => {
const { videoState, setVolume, toggleMute } = usePlayerContext()
const { videoState, setVolume, toggleMute, translations } = usePlayerContext()
const [showSlider, setShowSlider] = useState(false)
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 actionLabel = videoState.muted ? translations.unmute : translations.mute
return (
<div
@@ -40,8 +41,8 @@ export const VolumeControl: React.FC = () => {
<button
className="control-button volume-button"
onClick={toggleMute}
aria-label={videoState.muted ? 'Unmute' : 'Mute'}
title={videoState.muted ? 'Unmute (M)' : 'Mute (M)'}
aria-label={actionLabel}
title={`${actionLabel} (M)`}
>
<VolumeIcon size={24} color="var(--player-text)" />
</button>
@@ -55,7 +56,7 @@ export const VolumeControl: React.FC = () => {
value={videoState.muted ? 0 : videoState.volume}
onChange={handleSliderChange}
className="volume-slider"
aria-label="Volume"
aria-label={translations.volume}
/>
<div
className="volume-slider-fill"
+2 -2
View File
@@ -92,7 +92,7 @@ export const SettingsMenu: React.FC<SettingsMenuProps> = ({
<div className="settings-main-option-content">
<span className="settings-main-option-label">{translations.speed}</span>
<span className="settings-main-option-value">
{videoState.playbackRate === 1 ? 'Normal' : `${videoState.playbackRate}x`}
{videoState.playbackRate === 1 ? translations.normal : `${videoState.playbackRate}x`}
</span>
</div>
<div className="settings-main-option-arrow"></div>
@@ -148,7 +148,7 @@ export const SettingsMenu: React.FC<SettingsMenuProps> = ({
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)" />}
</button>
))}
+33
View File
@@ -14,6 +14,17 @@ export interface Translations {
audioTrack: string;
settings: 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> = {
@@ -29,6 +40,17 @@ export const translations: Record<string, Translations> = {
audioTrack: 'Audio Track',
settings: 'Settings',
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: {
noSubtitlesAvailable: 'Altyazı mevcut değil',
@@ -42,6 +64,17 @@ export const translations: Record<string, Translations> = {
audioTrack: 'Ses',
settings: 'Ayarlar',
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 { getTranslations, detectBrowserLanguage } from '../i18n'
import { logger } from './logger'
// Re-export control functions for backward compatibility
export { setHlsQualityLevel, setHlsAudioTrack } from './hlsControl'
@@ -47,20 +48,20 @@ const loadHlsFromCDN = (): Promise<any> => {
*/
export const loadHls = async (): Promise<any> => {
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
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
} 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 {
// Fallback to CDN
const Hls = await loadHlsFromCDN()
console.log('[HLS Loader] Successfully loaded from CDN')
logger.log('[HLS Loader] Successfully loaded from CDN')
return Hls
} 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.')
}
}
@@ -87,17 +88,17 @@ export const hasNativeHlsSupport = (): boolean => {
export const getHlsAudioTracks = (hls: any): AudioTrack[] => {
try {
if (!hls) {
console.warn('[HLS Loader] getHlsAudioTracks: No HLS instance provided')
logger.warn('[HLS Loader] getHlsAudioTracks: No HLS instance provided')
return []
}
// Check if audioTracks property exists
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 []
}
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 audioTrack = {
@@ -111,10 +112,10 @@ export const getHlsAudioTracks = (hls: any): AudioTrack[] => {
return audioTrack
})
console.log('[HLS Loader] getHlsAudioTracks: Processed tracks:', audioTracks)
logger.log('[HLS Loader] getHlsAudioTracks: Processed tracks:', audioTracks)
return audioTracks
} catch (error) {
console.error('[HLS Loader] getHlsAudioTracks: Error extracting audio tracks:', error)
logger.error('[HLS Loader] getHlsAudioTracks: Error extracting audio tracks:', error)
return []
}
}
@@ -154,16 +155,16 @@ export const getHlsSubtitleTracks = (hls: any): SubtitleTrack[] => {
export const getHlsQualities = (hls: any): VideoQuality[] => {
try {
if (!hls) {
console.warn('[HLS Loader] getHlsQualities: No HLS instance provided')
logger.warn('[HLS Loader] getHlsQualities: No HLS instance provided')
return []
}
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 []
}
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 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)
})
console.log('[HLS Loader] getHlsQualities: Processed qualities:', sortedQualities)
logger.log('[HLS Loader] getHlsQualities: Processed qualities:', sortedQualities)
return sortedQualities
} catch (error) {
console.error('[HLS Loader] getHlsQualities: Error extracting qualities:', error)
logger.error('[HLS Loader] getHlsQualities: Error extracting qualities:', error)
return []
}
}
+12 -10
View File
@@ -3,6 +3,7 @@
*/
import type { AudioTrack, VideoQuality, SubtitleTrack } from '../types'
import { logger } from './logger'
interface HlsSetupOptions {
video: HTMLVideoElement
@@ -30,7 +31,7 @@ export const setupHlsInstance = async ({
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({
enableWorker: true,
@@ -44,10 +45,10 @@ export const setupHlsInstance = async ({
let manifestParsedHandled = false
hls.on(Hls.Events.MANIFEST_PARSED, () => {
console.log('[HLS Setup] MANIFEST_PARSED event fired')
logger.log('[HLS Setup] MANIFEST_PARSED event fired')
if (manifestParsedHandled) {
console.warn('[HLS Setup] MANIFEST_PARSED already handled, skipping')
logger.warn('[HLS Setup] MANIFEST_PARSED already handled, skipping')
return
}
manifestParsedHandled = true
@@ -58,23 +59,23 @@ export const setupHlsInstance = async ({
const qualities = getHlsQualities(hls)
const subtitles = getHlsSubtitleTracks(hls)
console.log('[HLS Setup] Detected tracks:', {
logger.log('[HLS Setup] Detected tracks:', {
audioTracks: tracks.length,
qualities: qualities.length,
subtitles: subtitles.length
})
if (tracks.length > 0) {
console.log('[HLS Setup] Loading audio tracks:', tracks)
logger.log('[HLS Setup] Loading audio tracks:', tracks)
onAudioTracksLoaded?.(tracks)
}
if (subtitles.length > 0) {
console.log('[HLS Setup] Loading subtitle tracks:', subtitles)
logger.log('[HLS Setup] Loading subtitle tracks:', subtitles)
onSubtitleTracksLoaded?.(subtitles)
}
console.log('[HLS Setup] Loading quality levels:', qualities)
logger.log('[HLS Setup] Loading quality levels:', qualities)
onQualityLevelsLoaded?.(qualities)
}
@@ -93,14 +94,14 @@ export const setupHlsInstance = async ({
hls.on(Hls.Events.LEVEL_LOADED, () => {
const qualities = getHlsQualities(hls)
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)
}
})
hls.on(Hls.Events.AUDIO_TRACKS_UPDATED, () => {
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) {
onAudioTracksLoaded?.(tracks)
}
@@ -108,7 +109,7 @@ export const setupHlsInstance = async ({
hls.on(Hls.Events.SUBTITLE_TRACKS_UPDATED, () => {
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) {
onSubtitleTracksLoaded?.(subtitles)
}
@@ -139,3 +140,4 @@ export const setupHlsInstance = async ({
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
* Dynamically loads mpegts.js library
*/
import { logger } from './logger'
export interface MpegtsConfig {
enableWorker?: boolean
@@ -29,26 +30,26 @@ let loadingPromise: Promise<any> | null = null
export const loadMpegts = async (): Promise<any> => {
// Return cached instance if available
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 existing loading promise if already loading
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
}
// Start loading
loadingPromise = (async () => {
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')
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
} 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')
} finally {
loadingPromise = null
@@ -103,5 +104,6 @@ export const getMpegtsInstance = (): any | null => {
export const clearMpegtsCache = (): void => {
mpegtsInstance = 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 { isLiveStream } from './videoProtocol'
import { logger } from './logger'
export interface MpegtsSetupOptions {
video: HTMLVideoElement
@@ -41,7 +42,7 @@ export const setupMpegtsInstance = async ({
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
const isLive = isLiveStream(src)
@@ -72,40 +73,40 @@ export const setupMpegtsInstance = async ({
// Event handlers
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}`)
// Handle specific error types
if (errorType === mpegts.ErrorTypes.NETWORK_ERROR) {
console.error('Network error occurred:', errorDetail)
logger.error('Network error occurred:', errorDetail)
// Attempt recovery for recoverable network errors
if (
errorDetail === mpegts.ErrorDetails.NETWORK_EXCEPTION ||
errorDetail === mpegts.ErrorDetails.NETWORK_STATUS_CODE_INVALID
) {
console.log('Attempting to recover from network error...')
logger.log('Attempting to recover from network error...')
try {
player.unload()
player.load()
return
} 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) {
console.error('Media error occurred:', errorDetail)
logger.error('Media error occurred:', errorDetail)
// Some media errors are recoverable
if (errorDetail === mpegts.ErrorDetails.MEDIA_MSE_ERROR) {
console.log('Attempting to recover from media error...')
logger.log('Attempting to recover from media error...')
try {
player.unload()
player.load()
return
} 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, () => {
console.log('mpegts.js: Loading complete')
logger.log('mpegts.js: Loading complete')
})
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) => {
console.log('mpegts.js: Metadata arrived', metadata)
logger.log('mpegts.js: Metadata arrived', metadata)
// Trigger onLoadedMetadata callback
if (onLoadedMetadata) {
@@ -145,7 +146,7 @@ export const setupMpegtsInstance = async ({
try {
await video.play()
} catch (playError) {
console.warn('Autoplay failed:', playError)
logger.warn('Autoplay failed:', playError)
// Autoplay might be blocked by browser, ignore error
}
}
@@ -153,7 +154,7 @@ export const setupMpegtsInstance = async ({
// Return cleanup function
return () => {
try {
console.log('Cleaning up mpegts.js player...')
logger.log('Cleaning up mpegts.js player...')
// Remove event listeners
player.off(mpegts.Events.ERROR)
@@ -176,11 +177,11 @@ export const setupMpegtsInstance = async ({
delete (video as any).__mpegtsInstance
delete (video as any).__mpegtsStats
} catch (cleanupError) {
console.error('Error during mpegts.js cleanup:', cleanupError)
logger.error('Error during mpegts.js cleanup:', cleanupError)
}
}
} catch (error) {
console.error('Failed to setup mpegts.js player:', error)
logger.error('Failed to setup mpegts.js player:', error)
const setupError =
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 => {
return getMpegtsInstance(video) !== null
}
+5 -3
View File
@@ -3,6 +3,7 @@
* Loads flv.js library with NPM fallback to CDN strategy
* 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'
@@ -48,14 +49,14 @@ export const loadFlvjs = async (): Promise<any> => {
const flvModule = await import('flv.js')
return flvModule.default || flvModule
} 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 {
// Fallback to CDN
const flvjs = await loadFlvjsFromCDN()
return flvjs
} 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(
'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,
}
} catch (error) {
console.warn('Failed to extract flv.js quality info:', error)
logger.warn('Failed to extract flv.js quality info:', error)
return null
}
}
+17 -15
View File
@@ -6,6 +6,7 @@
import { loadFlvjs, isFlvjsSupported, createDefaultFlvConfig } from './rtmpLoader'
import { isLiveStream } from './videoProtocol'
import { logger } from './logger'
export interface RtmpSetupOptions {
video: HTMLVideoElement
@@ -50,7 +51,7 @@ export const setupRtmpInstance = async ({
if (src.startsWith('rtmp://') || src.startsWith('rtmps://')) {
// For RTMP URLs, flv.js expects HTTP-FLV endpoint
// 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.'
)
type = 'flv'
@@ -84,40 +85,40 @@ export const setupRtmpInstance = async ({
// Event handlers
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}`)
// Handle specific error types
if (errorType === flvjs.ErrorTypes.NETWORK_ERROR) {
console.error('Network error occurred:', errorDetail)
logger.error('Network error occurred:', errorDetail)
// Attempt recovery for recoverable network errors
if (
errorDetail === flvjs.ErrorDetails.NETWORK_EXCEPTION ||
errorDetail === flvjs.ErrorDetails.NETWORK_STATUS_CODE_INVALID
) {
console.log('Attempting to recover from network error...')
logger.log('Attempting to recover from network error...')
try {
player.unload()
player.load()
return
} 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) {
console.error('Media error occurred:', errorDetail)
logger.error('Media error occurred:', errorDetail)
// Some media errors are recoverable
if (errorDetail === flvjs.ErrorDetails.MEDIA_MSE_ERROR) {
console.log('Attempting to recover from media error...')
logger.log('Attempting to recover from media error...')
try {
player.unload()
player.load()
return
} 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, () => {
console.log('flv.js: Loading complete')
logger.log('flv.js: Loading complete')
})
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) => {
console.log('flv.js: Metadata arrived', metadata)
logger.log('flv.js: Metadata arrived', metadata)
// Trigger onLoadedMetadata callback
if (onLoadedMetadata) {
@@ -158,7 +159,7 @@ export const setupRtmpInstance = async ({
try {
await video.play()
} catch (playError) {
console.warn('Autoplay failed:', playError)
logger.warn('Autoplay failed:', playError)
// Autoplay might be blocked by browser, ignore error
}
}
@@ -166,7 +167,7 @@ export const setupRtmpInstance = async ({
// Return cleanup function
return () => {
try {
console.log('Cleaning up flv.js player...')
logger.log('Cleaning up flv.js player...')
// Remove event listeners
player.off(flvjs.Events.ERROR)
@@ -189,11 +190,11 @@ export const setupRtmpInstance = async ({
delete (video as any).__rtmpInstance
delete (video as any).__rtmpStats
} catch (cleanupError) {
console.error('Error during flv.js cleanup:', cleanupError)
logger.error('Error during flv.js cleanup:', cleanupError)
}
}
} catch (error) {
console.error('Failed to setup flv.js player:', error)
logger.error('Failed to setup flv.js player:', error)
const setupError =
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 => {
return getRtmpInstance(video) !== null
}