feat: apply phase1 DX cleanup for private registry
This commit is contained in:
+7
-7
@@ -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
@@ -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**
|
||||||
|
|
||||||
[](https://www.npmjs.com/package/@alper/video-player)
|
[](https://www.npmjs.com/package/@source/player)
|
||||||
[](https://bundlephobia.com/package/@alper/video-player)
|
[](https://bundlephobia.com/package/@source/player)
|
||||||
[](https://www.typescriptlang.org/)
|
[](https://www.typescriptlang.org/)
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](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*
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)" />
|
||||||
|
|||||||
@@ -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,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)" />
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
type LoggerMethod = (..._args: unknown[]) => void
|
||||||
|
|
||||||
|
const noop: LoggerMethod = () => undefined
|
||||||
|
|
||||||
|
export const logger = {
|
||||||
|
log: noop,
|
||||||
|
warn: noop,
|
||||||
|
error: noop,
|
||||||
|
}
|
||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user