+
{line}
))}
diff --git a/src/components/VideoPlayer.css b/src/components/VideoPlayer.css
index 9b91670..56cbd10 100644
--- a/src/components/VideoPlayer.css
+++ b/src/components/VideoPlayer.css
@@ -1,4 +1,4 @@
-.video-player {
+.sp-video-player {
position: relative;
display: block;
width: 100%;
@@ -17,41 +17,41 @@
transition: border-radius var(--player-transition-normal) ease;
}
-.video-player *,
-.video-player *::before,
-.video-player *::after {
+.sp-video-player *,
+.sp-video-player *::before,
+.sp-video-player *::after {
box-sizing: border-box;
}
-.video-player::before {
+.sp-video-player::before {
content: '';
display: block;
padding-top: var(--player-aspect-ratio, 56.25%);
}
-.video-player > * {
+.sp-video-player > * {
position: absolute;
inset: 0;
}
-.video-player video {
+.sp-video-player video {
display: block;
width: 100%;
height: 100%;
}
-.video-player:fullscreen,
-.video-player:-webkit-full-screen,
-.video-player:-moz-full-screen,
-.video-player:-ms-fullscreen,
-:fullscreen .video-player,
-:-webkit-full-screen .video-player {
+.sp-video-player:fullscreen,
+.sp-video-player:-webkit-full-screen,
+.sp-video-player:-moz-full-screen,
+.sp-video-player:-ms-fullscreen,
+:fullscreen .sp-video-player,
+:-webkit-full-screen .sp-video-player {
border-radius: 0;
}
-.video-player video::-webkit-media-controls,
-.video-player video::-webkit-media-controls-enclosure,
-.video-player video::-webkit-media-controls-panel {
+.sp-video-player video::-webkit-media-controls,
+.sp-video-player video::-webkit-media-controls-enclosure,
+.sp-video-player video::-webkit-media-controls-panel {
display: none !important;
}
diff --git a/src/components/VideoPlayer.test.tsx b/src/components/VideoPlayer.test.tsx
index 71bcc97..6633ec1 100644
--- a/src/components/VideoPlayer.test.tsx
+++ b/src/components/VideoPlayer.test.tsx
@@ -9,7 +9,7 @@ describe('VideoPlayer', () => {
it('renders video player container', () => {
const { container } = render(
)
- expect(container.querySelector('.video-player')).toBeInTheDocument()
+ expect(container.querySelector('.sp-video-player')).toBeInTheDocument()
})
it('renders video element', () => {
@@ -41,7 +41,7 @@ describe('VideoPlayer', () => {
it('applies custom className', () => {
const className = 'custom-player'
const { container } = render(
)
- expect(container.querySelector('.video-player')).toHaveClass('video-player', className)
+ expect(container.querySelector('.sp-video-player')).toHaveClass('sp-video-player', className)
})
it('calls onPlay callback when play event fires', async () => {
@@ -134,7 +134,7 @@ describe('VideoPlayer', () => {
it('applies custom style', () => {
const style = { width: '800px', height: '450px' }
const { container } = render(
)
- const playerElement = container.querySelector('.video-player') as HTMLElement
+ const playerElement = container.querySelector('.sp-video-player') as HTMLElement
expect(playerElement.style.width).toBe('800px')
expect(playerElement.style.height).toBe('450px')
})
diff --git a/src/components/VideoPlayer.tsx b/src/components/VideoPlayer.tsx
index e39a8d0..240be0a 100644
--- a/src/components/VideoPlayer.tsx
+++ b/src/components/VideoPlayer.tsx
@@ -2,6 +2,7 @@ import React, { useMemo, useState, useCallback, useImperativeHandle, forwardRef
import { PlayerProvider, usePlayerContext } from '../contexts/PlayerContext'
import { VideoElement } from './VideoElement'
import { ControlsLayer } from './ControlsLayer'
+import { PlayerErrorBoundary } from './ErrorBoundary'
import type { VideoPlayerProps, VideoPlayerHandle, AudioTrack, VideoQuality, SubtitleTrack } from '../types'
import { initializePolyfills } from '../utils/polyfills'
import '../styles/variables.css'
@@ -14,7 +15,7 @@ const initializePolyfillsIfNeeded = () => {
if (typeof document === 'undefined') return
// Check if polyfills are needed
- const needsFullscreenPolyfill = !document.fullscreenEnabled && !(document as any).webkitFullscreenEnabled
+ const needsFullscreenPolyfill = !document.fullscreenEnabled && !document.webkitFullscreenEnabled
const needsPIPPolyfill = !('pictureInPictureEnabled' in document)
if (needsFullscreenPolyfill || needsPIPPolyfill) {
@@ -143,7 +144,7 @@ const VideoPlayerContent = forwardRef
(() => {
const cssVariables: Record = {}
@@ -202,7 +203,7 @@ const VideoPlayerContent = forwardRef
@@ -264,7 +265,7 @@ const VideoPlayerContent = forwardRef
)}
{children && (
-
+
)}
@@ -351,68 +352,75 @@ export const VideoPlayer = forwardRef
(
}, [])
return (
-
-
-
+ {
+ onError?.(error)
+ }}
+ >
+
+
+
+
)
}
)
diff --git a/src/components/controls/CenterPlayButton.css b/src/components/controls/CenterPlayButton.css
index b4b6908..c4080af 100644
--- a/src/components/controls/CenterPlayButton.css
+++ b/src/components/controls/CenterPlayButton.css
@@ -1,4 +1,4 @@
-.center-play-overlay {
+.sp-center-play-overlay {
position: absolute;
inset: 0;
display: flex;
@@ -8,7 +8,7 @@
pointer-events: none;
}
-.center-play-button {
+.sp-center-play-button {
width: 72px;
height: 72px;
border-radius: var(--player-radius-full);
@@ -28,35 +28,35 @@
pointer-events: all;
}
-.center-play-button:hover {
+.sp-center-play-button:hover {
background-color: var(--player-primary-hover);
transform: scale(1.08);
box-shadow: 0 16px 32px rgba(0, 0, 0, 0.4), 0 10px 20px rgba(239, 68, 68, 0.25);
}
-.center-play-button:active {
+.sp-center-play-button:active {
background-color: var(--player-primary-active);
transform: scale(0.97);
}
-.center-play-button:focus-visible {
+.sp-center-play-button:focus-visible {
outline: 2px solid var(--player-text);
outline-offset: 4px;
}
-.center-play-button svg {
+.sp-center-play-button svg {
width: 36px;
height: 36px;
margin-left: 3px;
}
@media (max-width: 640px) {
- .center-play-button {
+ .sp-center-play-button {
width: 64px;
height: 64px;
}
- .center-play-button svg {
+ .sp-center-play-button svg {
width: 30px;
height: 30px;
}
diff --git a/src/components/controls/CenterPlayButton.tsx b/src/components/controls/CenterPlayButton.tsx
index 75e2fef..f05fd6b 100644
--- a/src/components/controls/CenterPlayButton.tsx
+++ b/src/components/controls/CenterPlayButton.tsx
@@ -7,9 +7,9 @@ export const CenterPlayButton: React.FC = () => {
const { play, translations } = usePlayerContext()
return (
-
+
{
return (
{
// Check if PIP is supported
const isPIPSupported =
- typeof (document as any).pictureInPictureEnabled === 'boolean' &&
- (document as any).pictureInPictureEnabled &&
+ 'pictureInPictureEnabled' in document &&
+ document.pictureInPictureEnabled &&
typeof HTMLVideoElement.prototype.requestPictureInPicture === 'function'
if (!isPIPSupported) {
@@ -21,7 +21,7 @@ export const PIPButton: React.FC = () => {
return (
{
return (
{
return (
{
aria-valuetext={formatTime(videoState.currentTime)}
>
{/* Background track */}
-
+
{/* Buffered progress */}
-
+
{/* Played progress */}
-
{/* Hover time tooltip */}
{hoverTime !== null && (
{
return (
event.stopPropagation()}
onClick={toggleSettings}
aria-label={translations.settings}
diff --git a/src/components/controls/TimeDisplay.css b/src/components/controls/TimeDisplay.css
index d2627aa..c3ce27f 100644
--- a/src/components/controls/TimeDisplay.css
+++ b/src/components/controls/TimeDisplay.css
@@ -1,4 +1,4 @@
-.time-display {
+.sp-time-display {
display: flex;
align-items: center;
gap: 4px;
@@ -10,13 +10,13 @@
user-select: none;
}
-.time-separator,
-.time-duration {
+.sp-time-separator,
+.sp-time-duration {
color: var(--player-text-secondary);
}
@media (max-width: 640px) {
- .time-display {
+ .sp-time-display {
font-size: 12px;
}
}
diff --git a/src/components/controls/TimeDisplay.tsx b/src/components/controls/TimeDisplay.tsx
index ab5df38..3a9e08b 100644
--- a/src/components/controls/TimeDisplay.tsx
+++ b/src/components/controls/TimeDisplay.tsx
@@ -7,10 +7,10 @@ export const TimeDisplay: React.FC = () => {
const { videoState } = usePlayerContext()
return (
-
-
{formatTime(videoState.currentTime)}
-
/
-
{formatTime(videoState.duration)}
+
+ {formatTime(videoState.currentTime)}
+ /
+ {formatTime(videoState.duration)}
)
}
diff --git a/src/components/controls/VolumeControl.css b/src/components/controls/VolumeControl.css
index 1f54424..12fb85e 100644
--- a/src/components/controls/VolumeControl.css
+++ b/src/components/controls/VolumeControl.css
@@ -1,11 +1,11 @@
-.volume-control {
+.sp-volume-control {
display: flex;
align-items: center;
gap: var(--player-spacing-xs);
position: relative;
}
-.volume-slider-container {
+.sp-volume-slider-container {
position: relative;
width: 0;
height: 4px;
@@ -17,12 +17,12 @@
opacity var(--player-transition-normal) ease;
}
-.volume-slider-container.visible {
+.sp-volume-slider-container.visible {
width: 88px;
opacity: 1;
}
-.volume-slider {
+.sp-volume-slider {
position: absolute;
inset: 0;
width: 100%;
@@ -32,18 +32,18 @@
cursor: pointer;
}
-.volume-slider:focus-visible {
+.sp-volume-slider:focus-visible {
outline: 2px solid var(--player-primary);
outline-offset: 2px;
border-radius: var(--player-radius-sm);
}
-.volume-slider::-webkit-slider-runnable-track {
+.sp-volume-slider::-webkit-slider-runnable-track {
height: 100%;
background: transparent;
}
-.volume-slider::-webkit-slider-thumb {
+.sp-volume-slider::-webkit-slider-thumb {
appearance: none;
width: 12px;
height: 12px;
@@ -56,17 +56,17 @@
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
-.volume-slider:hover::-webkit-slider-thumb {
+.sp-volume-slider:hover::-webkit-slider-thumb {
transform: scale(1.15);
box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.2);
}
-.volume-slider::-moz-range-track {
+.sp-volume-slider::-moz-range-track {
height: 100%;
background: transparent;
}
-.volume-slider::-moz-range-thumb {
+.sp-volume-slider::-moz-range-thumb {
width: 12px;
height: 12px;
border-radius: 50%;
@@ -77,12 +77,12 @@
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
-.volume-slider:hover::-moz-range-thumb {
+.sp-volume-slider:hover::-moz-range-thumb {
transform: scale(1.15);
box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.2);
}
-.volume-slider-fill {
+.sp-volume-slider-fill {
position: absolute;
top: 0;
left: 0;
@@ -95,7 +95,7 @@
}
@media (max-width: 640px) {
- .volume-slider-container.visible {
+ .sp-volume-slider-container.visible {
width: 72px;
}
}
diff --git a/src/components/controls/VolumeControl.tsx b/src/components/controls/VolumeControl.tsx
index 4c3f09b..9098e27 100644
--- a/src/components/controls/VolumeControl.tsx
+++ b/src/components/controls/VolumeControl.tsx
@@ -34,12 +34,12 @@ export const VolumeControl: React.FC = () => {
return (
{
-
+
diff --git a/src/components/menus/SettingsMenu.css b/src/components/menus/SettingsMenu.css
index d395bf1..10fe3e5 100644
--- a/src/components/menus/SettingsMenu.css
+++ b/src/components/menus/SettingsMenu.css
@@ -1,4 +1,4 @@
-.settings-menu {
+.sp-settings-menu {
position: absolute;
bottom: calc(100% + 12px);
right: 0;
@@ -10,10 +10,10 @@
overflow: hidden;
z-index: var(--player-z-menu);
color: var(--player-text);
- animation: fadeIn var(--player-transition-normal) ease;
+ animation: sp-fade-in var(--player-transition-normal) ease;
}
-.settings-menu-header {
+.sp-settings-menu-header {
display: flex;
align-items: center;
gap: var(--player-spacing-sm);
@@ -21,14 +21,14 @@
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
-.settings-menu-header h3 {
+.sp-settings-menu-header h3 {
flex: 1;
margin: 0;
font-size: 14px;
font-weight: 600;
}
-.settings-back-button {
+.sp-settings-back-button {
appearance: none;
background: transparent;
border: none;
@@ -44,17 +44,17 @@
background-color var(--player-transition-fast) ease;
}
-.settings-back-button:hover {
+.sp-settings-back-button:hover {
color: var(--player-primary);
background-color: rgba(255, 255, 255, 0.08);
}
-.settings-main-options {
+.sp-settings-main-options {
display: flex;
flex-direction: column;
}
-.settings-main-option {
+.sp-settings-main-option {
display: flex;
align-items: center;
gap: var(--player-spacing-md);
@@ -69,11 +69,11 @@
color var(--player-transition-fast) ease;
}
-.settings-main-option:hover {
+.sp-settings-main-option:hover {
background-color: rgba(255, 255, 255, 0.06);
}
-.settings-main-option-icon {
+.sp-settings-main-option-icon {
width: 32px;
height: 32px;
display: flex;
@@ -83,36 +83,36 @@
background-color: rgba(239, 68, 68, 0.14);
}
-.settings-main-option-content {
+.sp-settings-main-option-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
}
-.settings-main-option-label {
+.sp-settings-main-option-label {
font-size: 13px;
font-weight: 600;
}
-.settings-main-option-value {
+.sp-settings-main-option-value {
font-size: 12px;
color: var(--player-text-secondary);
}
-.settings-main-option-arrow {
+.sp-settings-main-option-arrow {
font-size: 18px;
color: var(--player-text-secondary);
}
-.settings-options {
+.sp-settings-options {
display: flex;
flex-direction: column;
max-height: 280px;
overflow-y: auto;
}
-.settings-option {
+.sp-settings-option {
display: flex;
align-items: center;
justify-content: space-between;
@@ -128,55 +128,55 @@
color var(--player-transition-fast) ease;
}
-.settings-option:hover {
+.sp-settings-option:hover {
background-color: rgba(255, 255, 255, 0.06);
}
-.settings-option.active {
+.sp-settings-option.active {
color: var(--player-primary);
background-color: rgba(239, 68, 68, 0.14);
}
-.settings-option span {
+.sp-settings-option span {
flex: 1;
}
-.settings-empty-state {
+.sp-settings-empty-state {
padding: var(--player-spacing-xl) var(--player-spacing-lg);
text-align: center;
color: var(--player-text-muted);
font-size: 13px;
}
-.settings-options::-webkit-scrollbar {
+.sp-settings-options::-webkit-scrollbar {
width: 5px;
}
-.settings-options::-webkit-scrollbar-track {
+.sp-settings-options::-webkit-scrollbar-track {
background: transparent;
}
-.settings-options::-webkit-scrollbar-thumb {
+.sp-settings-options::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.18);
border-radius: 3px;
}
-.settings-options::-webkit-scrollbar-thumb:hover {
+.sp-settings-options::-webkit-scrollbar-thumb:hover {
background-color: rgba(255, 255, 255, 0.25);
}
@media (max-width: 640px) {
- .settings-menu {
+ .sp-settings-menu {
min-width: 240px;
max-height: 320px;
}
- .settings-main-option,
- .settings-option {
+ .sp-settings-main-option,
+ .sp-settings-option {
padding: var(--player-spacing-sm) var(--player-spacing-md);
}
- .settings-options {
+ .sp-settings-options {
max-height: 240px;
}
}
diff --git a/src/components/menus/SettingsMenu.tsx b/src/components/menus/SettingsMenu.tsx
index 0c93ce0..176b031 100644
--- a/src/components/menus/SettingsMenu.tsx
+++ b/src/components/menus/SettingsMenu.tsx
@@ -64,67 +64,67 @@ export const SettingsMenu: React.FC
= ({
if (!uiState.settingsOpen) return null
return (
-
+
{/* Main Menu */}
{currentView === 'main' && (
<>
-
+
{translations.settings}
-
+
{qualities.length > 0 && (
-
setCurrentView('quality')}>
-
+
setCurrentView('quality')}>
+
-
-
{translations.quality}
-
+
+ {translations.quality}
+
{settings.quality ? settings.quality.label : translations.auto}
- ›
+ ›
)}
- setCurrentView('speed')}>
-
+
setCurrentView('speed')}>
+
-
-
{translations.speed}
-
+
+ {translations.speed}
+
{videoState.playbackRate === 1 ? translations.normal : `${videoState.playbackRate}x`}
- ›
+ ›
- setCurrentView('subtitles')}>
-
+
setCurrentView('subtitles')}>
+
-
-
{translations.subtitles}
-
+
+ {translations.subtitles}
+
{settings.subtitle ? settings.subtitle.label : translations.off}
- ›
+ ›
{audioTracks.length > 0 && (
- setCurrentView('audio')}>
-
+
setCurrentView('audio')}>
+
-
-
{translations.audioTrack}
-
+
+ {translations.audioTrack}
+
{settings.audioTrack ? settings.audioTrack.name : translations.default}
- ›
+ ›
)}
@@ -134,17 +134,17 @@ export const SettingsMenu: React.FC = ({
{/* Speed Submenu */}
{currentView === 'speed' && (
<>
-
-
+
+
‹
{translations.speed}
-
+
{playbackRates.map((rate) => (
{
setPlaybackRate(rate)
setTimeout(() => goBack(), 150)
@@ -161,15 +161,15 @@ export const SettingsMenu: React.FC = ({
{/* Subtitles Submenu */}
{currentView === 'subtitles' && (
<>
-
-
+
+
‹
{translations.subtitles}
-
+
{
setSubtitle(null)
setTimeout(() => goBack(), 150)
@@ -182,7 +182,7 @@ export const SettingsMenu: React.FC = ({
subtitles.map((subtitle) => (
{
setSubtitle(subtitle)
setTimeout(() => goBack(), 150)
@@ -193,7 +193,7 @@ export const SettingsMenu: React.FC = ({
))
) : (
-
+
{translations.noSubtitlesAvailable}
)}
@@ -204,17 +204,17 @@ export const SettingsMenu: React.FC
= ({
{/* Audio Submenu */}
{currentView === 'audio' && (
<>
-
-
+
+
‹
{translations.audioTrack}
-
+
{audioTracks.map((track) => (
{
setAudioTrack(track)
setTimeout(() => goBack(), 150)
@@ -233,15 +233,15 @@ export const SettingsMenu: React.FC = ({
{/* Quality Submenu */}
{currentView === 'quality' && (
<>
-
-
+
+
‹
{translations.quality}
-
+
{
setQuality(null)
setTimeout(() => goBack(), 150)
@@ -269,7 +269,7 @@ export const SettingsMenu: React.FC = ({
return (
{
setQuality(quality)
setTimeout(() => goBack(), 150)
diff --git a/src/components/overlays/LoadingSpinner.css b/src/components/overlays/LoadingSpinner.css
index 296caab..4fafc23 100644
--- a/src/components/overlays/LoadingSpinner.css
+++ b/src/components/overlays/LoadingSpinner.css
@@ -1,4 +1,4 @@
-.loading-spinner-overlay {
+.sp-loading-spinner-overlay {
position: absolute;
inset: 0;
display: flex;
@@ -10,6 +10,6 @@
pointer-events: none;
}
-.loading-spinner {
- animation: fadeIn var(--player-transition-normal) ease;
+.sp-loading-spinner {
+ animation: sp-fade-in var(--player-transition-normal) ease;
}
diff --git a/src/components/overlays/LoadingSpinner.tsx b/src/components/overlays/LoadingSpinner.tsx
index 780156f..f4f0f9f 100644
--- a/src/components/overlays/LoadingSpinner.tsx
+++ b/src/components/overlays/LoadingSpinner.tsx
@@ -4,8 +4,8 @@ import './LoadingSpinner.css'
export const LoadingSpinner: React.FC = () => {
return (
-
-
+
diff --git a/src/flv.d.ts b/src/flv.d.ts
deleted file mode 100644
index b006620..0000000
--- a/src/flv.d.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * Type declarations for optional flv.js module
- * Since flv.js is an optional dependency that may not be installed,
- * we declare it as a module that can be dynamically imported
- */
-declare module 'flv.js' {
- const content: any
- export default content
-}
diff --git a/src/hooks/useTouchGestures.ts b/src/hooks/useTouchGestures.ts
index cd1ecd9..2da4f54 100644
--- a/src/hooks/useTouchGestures.ts
+++ b/src/hooks/useTouchGestures.ts
@@ -109,7 +109,7 @@ export const useTouchGestures = (containerRef: MutableRefObject
= ({ size = 24, className = '', color = 'currentColor', children }) => (
{React.Children.map(children, child =>
- React.isValidElement(child) ? React.cloneElement(child, { fill: color } as any) : child
+ React.isValidElement<{ fill?: string }>(child) ? React.cloneElement(child, { fill: color }) : child
)}
)
@@ -104,7 +104,7 @@ export const RewindIcon: React.FC = (props) => (
)
export const LoadingIcon: React.FC = ({ size = 24, className = '', color = 'currentColor' }) => (
-
+
diff --git a/src/index.ts b/src/index.ts
index b8a400e..ab8d2e8 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,5 +1,10 @@
// Main component
export { VideoPlayer } from './components/VideoPlayer'
+export { PlayerErrorBoundary } from './components/ErrorBoundary'
+export type {
+ PlayerErrorBoundaryProps,
+ PlayerErrorBoundaryFallbackRender,
+} from './components/ErrorBoundary'
// Context
export { PlayerProvider, usePlayerContext } from './contexts/PlayerContext'
diff --git a/src/styles/variables.css b/src/styles/variables.css
index 88e8b99..41972af 100644
--- a/src/styles/variables.css
+++ b/src/styles/variables.css
@@ -44,13 +44,13 @@
--player-subtitle-bottom-hidden: 36px;
}
-@keyframes spin {
+@keyframes sp-spin {
to {
transform: rotate(360deg);
}
}
-@keyframes fadeIn {
+@keyframes sp-fade-in {
from {
opacity: 0;
}
@@ -59,7 +59,7 @@
}
}
-@keyframes fadeOut {
+@keyframes sp-fade-out {
from {
opacity: 1;
}
@@ -69,9 +69,9 @@
}
@media (prefers-reduced-motion: reduce) {
- .video-player *,
- .video-player *::before,
- .video-player *::after {
+ .sp-video-player *,
+ .sp-video-player *::before,
+ .sp-video-player *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
diff --git a/src/types/vendor.d.ts b/src/types/vendor.d.ts
new file mode 100644
index 0000000..5758aee
--- /dev/null
+++ b/src/types/vendor.d.ts
@@ -0,0 +1,165 @@
+/**
+ * Type augmentations for vendor-prefixed browser APIs
+ * and dynamic player instance properties on HTMLVideoElement.
+ */
+
+interface PlayerInstance {
+ destroy(): void
+}
+
+interface HTMLVideoElement {
+ /** HLS.js player instance attached during HLS setup */
+ __hlsInstance?: HlsInstance | PlayerInstance
+ /** flv.js player instance attached during RTMP/FLV setup */
+ __rtmpInstance?: FlvjsPlayer | PlayerInstance
+ /** mpegts.js player instance attached during MPEG-TS setup */
+ __mpegtsInstance?: MpegtsPlayer | PlayerInstance
+ /** flv.js runtime statistics */
+ __rtmpStats?: Record
+ /** mpegts.js runtime statistics */
+ __mpegtsStats?: Record
+
+ // Vendor-prefixed fullscreen APIs
+ webkitRequestFullscreen?: () => Promise
+ mozRequestFullScreen?: () => Promise
+ msRequestFullscreen?: () => Promise
+}
+
+interface Document {
+ // Vendor-prefixed fullscreen properties
+ webkitFullscreenEnabled?: boolean
+ mozFullScreenEnabled?: boolean
+ msFullscreenEnabled?: boolean
+
+ webkitFullscreenElement?: Element | null
+ mozFullScreenElement?: Element | null
+ msFullscreenElement?: Element | null
+
+ webkitExitFullscreen?: () => Promise
+ mozCancelFullScreen?: () => Promise
+ msExitFullscreen?: () => Promise
+}
+
+interface Element {
+ // Vendor-prefixed fullscreen APIs
+ webkitRequestFullscreen?: () => Promise
+ mozRequestFullScreen?: () => Promise
+ msRequestFullscreen?: () => Promise
+}
+
+// Window augmentation for CDN-loaded libraries
+interface Window {
+ Hls?: HlsConstructor
+ flvjs?: FlvjsStatic
+ mpegts?: MpegtsStatic
+}
+
+// Minimal type definitions for CDN-loaded HLS.js
+interface HlsConstructor {
+ new (config?: Record): HlsInstance
+ isSupported(): boolean
+ Events: Record
+ ErrorTypes: Record
+}
+
+interface HlsInstance {
+ loadSource(src: string): void
+ attachMedia(video: HTMLVideoElement): void
+ destroy(): void
+ on(event: string, callback: (...args: TArgs) => void): void
+ startLoad(): void
+ recoverMediaError(): void
+ levels: HlsLevel[]
+ audioTracks: HlsAudioTrack[]
+ subtitleTracks: HlsSubtitleTrack[]
+ audioTrack: number
+ currentLevel: number
+}
+
+interface HlsLevel {
+ width?: number
+ height?: number
+ bitrate?: number
+ name?: string
+ url?: string
+ uri?: string
+ attrs?: Record
+}
+
+interface HlsAudioTrack {
+ name?: string
+ label?: string
+ lang?: string
+ language?: string
+ url?: string
+ groupId?: string
+ default?: boolean
+ autoselect?: boolean
+}
+
+interface HlsSubtitleTrack {
+ name?: string
+ label?: string
+ lang?: string
+ language?: string
+ url?: string
+ default?: boolean
+}
+
+// Minimal type definitions for CDN-loaded flv.js
+interface FlvjsStatic {
+ createPlayer(mediaDataSource: object, config?: object): FlvjsPlayer
+ isSupported(): boolean
+ getFeatureList(): Record
+ Events: Record
+ ErrorTypes: Record
+ ErrorDetails: Record
+}
+
+interface FlvjsPlayer {
+ attachMediaElement(video: HTMLVideoElement): void
+ load(): void
+ play(): Promise
+ unload(): void
+ detachMediaElement(): void
+ destroy(): void
+ on(event: string, callback: (...args: TArgs) => void): void
+ off(event: string, callback?: (...args: TArgs) => void): void
+ statisticsInfo?: Record
+}
+
+// Minimal type definitions for CDN-loaded mpegts.js
+interface MpegtsStatic {
+ createPlayer(mediaDataSource: object, config?: object): MpegtsPlayer
+ isSupported(): boolean
+ Events: Record
+ ErrorTypes: Record
+ ErrorDetails: Record
+}
+
+interface MpegtsPlayer {
+ attachMediaElement(video: HTMLVideoElement): void
+ load(): void
+ play(): Promise
+ unload(): void
+ detachMediaElement(): void
+ destroy(): void
+ on(event: string, callback: (...args: TArgs) => void): void
+ off(event: string, callback?: (...args: TArgs) => void): void
+}
+
+// Module declarations for optional npm packages
+declare module 'hls.js' {
+ const Hls: HlsConstructor
+ export default Hls
+}
+
+declare module 'flv.js' {
+ const flvjs: FlvjsStatic
+ export default flvjs
+}
+
+declare module 'mpegts.js' {
+ const mpegts: MpegtsStatic
+ export default mpegts
+}
diff --git a/src/utils/hlsControl.ts b/src/utils/hlsControl.ts
index e2e7c86..9d67404 100644
--- a/src/utils/hlsControl.ts
+++ b/src/utils/hlsControl.ts
@@ -3,11 +3,26 @@
* Separated to avoid circular dependencies and enable better tree-shaking
*/
+const hasQualityControls = (
+ hls: HlsInstance | PlayerInstance | null | undefined
+): hls is HlsInstance => {
+ return Boolean(hls && 'levels' in hls && Array.isArray(hls.levels) && 'currentLevel' in hls)
+}
+
+const hasAudioControls = (
+ hls: HlsInstance | PlayerInstance | null | undefined
+): hls is HlsInstance => {
+ return Boolean(hls && 'audioTracks' in hls && Array.isArray(hls.audioTracks) && 'audioTrack' in hls)
+}
+
/**
* Update active quality level in HLS instance. Passing null re-enables auto.
*/
-export const setHlsQualityLevel = (hls: any, levelIndex: number | null | undefined): void => {
- if (!hls || !Array.isArray(hls.levels)) {
+export const setHlsQualityLevel = (
+ hls: HlsInstance | PlayerInstance | null | undefined,
+ levelIndex: number | null | undefined
+): void => {
+ if (!hasQualityControls(hls)) {
return
}
@@ -30,8 +45,11 @@ export const setHlsQualityLevel = (hls: any, levelIndex: number | null | undefin
/**
* Set active audio track in HLS instance
*/
-export const setHlsAudioTrack = (hls: any, audioTrackIndex: number): void => {
- if (!hls || !hls.audioTracks) {
+export const setHlsAudioTrack = (
+ hls: HlsInstance | PlayerInstance | null | undefined,
+ audioTrackIndex: number
+): void => {
+ if (!hasAudioControls(hls)) {
return
}
diff --git a/src/utils/hlsLoader.ts b/src/utils/hlsLoader.ts
index c2fe6c9..f986aab 100644
--- a/src/utils/hlsLoader.ts
+++ b/src/utils/hlsLoader.ts
@@ -10,16 +10,16 @@ import { logger } from './logger'
// Re-export control functions for backward compatibility
export { setHlsQualityLevel, setHlsAudioTrack } from './hlsControl'
-const HLS_CDN_URL = 'https://cdn.jsdelivr.net/npm/hls.js@1.5.13/dist/hls.min.js'
+const HLS_CDN_URL = 'https://cdn.jsdelivr.net/npm/hls.js@1.6.13/dist/hls.min.js'
/**
* Load hls.js from CDN as fallback
*/
-const loadHlsFromCDN = (): Promise => {
+const loadHlsFromCDN = (): Promise => {
return new Promise((resolve, reject) => {
// Check if already loaded globally
- if (typeof (window as any).Hls !== 'undefined') {
- resolve((window as any).Hls)
+ if (window.Hls) {
+ resolve(window.Hls)
return
}
@@ -28,8 +28,8 @@ const loadHlsFromCDN = (): Promise => {
script.async = true
script.onload = () => {
- if (typeof (window as any).Hls !== 'undefined') {
- resolve((window as any).Hls)
+ if (window.Hls) {
+ resolve(window.Hls)
} else {
reject(new Error('HLS.js CDN loaded but Hls global not found'))
}
@@ -46,7 +46,7 @@ const loadHlsFromCDN = (): Promise => {
/**
* Load hls.js with npm fallback to CDN
*/
-export const loadHls = async (): Promise => {
+export const loadHls = async (): Promise => {
try {
logger.log('[HLS Loader] Attempting to load from npm package...')
// Try loading from npm package first
@@ -70,7 +70,7 @@ export const loadHls = async (): Promise => {
/**
* Check if HLS.js is supported in current browser
*/
-export const isHlsSupported = (Hls: any): boolean => {
+export const isHlsSupported = (Hls: HlsConstructor): boolean => {
return Hls && typeof Hls.isSupported === 'function' && Hls.isSupported()
}
@@ -85,7 +85,7 @@ export const hasNativeHlsSupport = (): boolean => {
/**
* Extract audio tracks from HLS instance
*/
-export const getHlsAudioTracks = (hls: any): AudioTrack[] => {
+export const getHlsAudioTracks = (hls: HlsInstance): AudioTrack[] => {
try {
if (!hls) {
logger.warn('[HLS Loader] getHlsAudioTracks: No HLS instance provided')
@@ -100,7 +100,7 @@ export const getHlsAudioTracks = (hls: any): AudioTrack[] => {
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: HlsAudioTrack, index: number) => {
const audioTrack = {
name: track.name || track.label || `Audio ${index + 1}`,
language: track.lang || track.language || 'unknown',
@@ -123,7 +123,7 @@ export const getHlsAudioTracks = (hls: any): AudioTrack[] => {
/**
* Extract subtitle tracks from HLS instance
*/
-export const getHlsSubtitleTracks = (hls: any): SubtitleTrack[] => {
+export const getHlsSubtitleTracks = (hls: HlsInstance): SubtitleTrack[] => {
try {
if (!hls) {
return []
@@ -134,7 +134,7 @@ export const getHlsSubtitleTracks = (hls: any): SubtitleTrack[] => {
return []
}
- const subtitleTracks: SubtitleTrack[] = hls.subtitleTracks.map((track: any, index: number) => {
+ const subtitleTracks: SubtitleTrack[] = hls.subtitleTracks.map((track: HlsSubtitleTrack, index: number) => {
return {
label: track.name || track.label || `Subtitle ${index + 1}`,
lang: track.lang || track.language || 'unknown',
@@ -152,7 +152,7 @@ export const getHlsSubtitleTracks = (hls: any): SubtitleTrack[] => {
/**
* Extract available quality levels from HLS instance
*/
-export const getHlsQualities = (hls: any): VideoQuality[] => {
+export const getHlsQualities = (hls: HlsInstance): VideoQuality[] => {
try {
if (!hls) {
logger.warn('[HLS Loader] getHlsQualities: No HLS instance provided')
@@ -166,7 +166,7 @@ export const getHlsQualities = (hls: any): VideoQuality[] => {
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: HlsLevel, index: number) => {
const resolution = typeof level.attrs?.RESOLUTION === 'string' ? level.attrs.RESOLUTION : undefined
const [widthFromResolution, heightFromResolution] = resolution
? resolution.split('x').map((value: string) => parseInt(value, 10))
@@ -175,7 +175,7 @@ export const getHlsQualities = (hls: any): VideoQuality[] => {
const translations = getTranslations(detectBrowserLanguage());
const width = level.width || widthFromResolution
const height = level.height || heightFromResolution
- const bitrate = typeof level.bitrate === 'number' ? level.bitrate : level.attrs?.BANDWIDTH
+ const bitrate = typeof level.bitrate === 'number' ? level.bitrate : (level.attrs?.BANDWIDTH ? parseInt(level.attrs.BANDWIDTH, 10) : undefined)
let label: string
if (typeof level.name === 'string' && level.name.trim().length > 0) {
@@ -213,5 +213,3 @@ export const getHlsQualities = (hls: any): VideoQuality[] => {
return []
}
}
-
-
diff --git a/src/utils/hlsSetup.ts b/src/utils/hlsSetup.ts
index 4101122..8752bf3 100644
--- a/src/utils/hlsSetup.ts
+++ b/src/utils/hlsSetup.ts
@@ -115,7 +115,7 @@ export const setupHlsInstance = async ({
}
})
- hls.on(Hls.Events.ERROR, (_event: any, data: any) => {
+ hls.on(Hls.Events.ERROR, (_event: unknown, data: { fatal: boolean; type: string }) => {
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
@@ -131,13 +131,13 @@ export const setupHlsInstance = async ({
}
})
- ;(video as any).__hlsInstance = hls
+ video.__hlsInstance = hls as PlayerInstance
return () => {
if (hls) {
hls.destroy()
}
- delete (video as any).__hlsInstance
+ delete video.__hlsInstance
}
}
diff --git a/src/utils/mpegtsLoader.ts b/src/utils/mpegtsLoader.ts
index a5813bd..5195c06 100644
--- a/src/utils/mpegtsLoader.ts
+++ b/src/utils/mpegtsLoader.ts
@@ -1,9 +1,11 @@
/**
* MPEG-TS loader utility
- * Dynamically loads mpegts.js library
+ * Dynamically loads mpegts.js library with CDN fallback
*/
import { logger } from './logger'
+const MPEGTS_CDN_URL = 'https://cdn.jsdelivr.net/npm/mpegts.js@1.7.3/dist/mpegts.js'
+
export interface MpegtsConfig {
enableWorker?: boolean
enableStashBuffer?: boolean
@@ -20,14 +22,45 @@ export interface MpegtsConfig {
headers?: Record
}
-let mpegtsInstance: any = null
-let loadingPromise: Promise | null = null
+let mpegtsInstance: MpegtsStatic | null = null
+let loadingPromise: Promise | null = null
+
+/**
+ * Load mpegts.js from CDN as fallback
+ */
+const loadMpegtsFromCDN = (): Promise => {
+ return new Promise((resolve, reject) => {
+ if (window.mpegts) {
+ resolve(window.mpegts)
+ return
+ }
+
+ const script = document.createElement('script')
+ script.src = MPEGTS_CDN_URL
+ script.async = true
+
+ script.onload = () => {
+ if (window.mpegts) {
+ resolve(window.mpegts)
+ } else {
+ reject(new Error('mpegts.js loaded but not available on window'))
+ }
+ }
+
+ script.onerror = () => {
+ reject(new Error(`Failed to load mpegts.js from CDN: ${MPEGTS_CDN_URL}`))
+ }
+
+ document.head.appendChild(script)
+ })
+}
/**
* Load mpegts.js library dynamically
+ * Tries NPM package first, falls back to CDN if unavailable
* @returns Promise that resolves to mpegts.js module
*/
-export const loadMpegts = async (): Promise => {
+export const loadMpegts = async (): Promise => {
// Return cached instance if available
if (mpegtsInstance) {
logger.log('[MPEG-TS Loader] Using cached mpegts.js instance')
@@ -47,10 +80,20 @@ export const loadMpegts = async (): Promise => {
const module = await import('mpegts.js')
mpegtsInstance = module.default || module
logger.log('[MPEG-TS Loader] Successfully loaded from npm package')
- return mpegtsInstance
- } catch (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')
+ return mpegtsInstance!
+ } catch (npmError) {
+ logger.warn('[MPEG-TS Loader] npm package not available, loading from CDN...', npmError)
+
+ try {
+ mpegtsInstance = await loadMpegtsFromCDN()
+ logger.log('[MPEG-TS Loader] Successfully loaded from CDN')
+ return mpegtsInstance!
+ } catch (cdnError) {
+ logger.error('[MPEG-TS Loader] Failed to load from both npm and CDN', cdnError)
+ throw new Error(
+ 'Failed to load mpegts.js library. Please ensure mpegts.js is available or check your network connection.'
+ )
+ }
} finally {
loadingPromise = null
}
@@ -64,7 +107,7 @@ export const loadMpegts = async (): Promise => {
* @param mpegts - The mpegts.js module
* @returns True if supported
*/
-export const isMpegtsSupported = (mpegts: any): boolean => {
+export const isMpegtsSupported = (mpegts: MpegtsStatic): boolean => {
return mpegts && mpegts.isSupported()
}
@@ -94,7 +137,7 @@ export const createDefaultMpegtsConfig = (isLive: boolean = false): MpegtsConfig
* Get the cached mpegts.js instance
* @returns The mpegts.js module or null
*/
-export const getMpegtsInstance = (): any | null => {
+export const getMpegtsInstance = (): MpegtsStatic | null => {
return mpegtsInstance
}
@@ -106,4 +149,3 @@ export const clearMpegtsCache = (): void => {
loadingPromise = null
logger.log('[MPEG-TS Loader] Cache cleared')
}
-
diff --git a/src/utils/mpegtsSetup.ts b/src/utils/mpegtsSetup.ts
index ba09e5b..8217f07 100644
--- a/src/utils/mpegtsSetup.ts
+++ b/src/utils/mpegtsSetup.ts
@@ -69,10 +69,10 @@ export const setupMpegtsInstance = async ({
player.load()
// Store player instance on video element for later access
- ;(video as any).__mpegtsInstance = player
+ video.__mpegtsInstance = player
// Event handlers
- player.on(mpegts.Events.ERROR, (errorType: string, errorDetail: string, errorInfo: any) => {
+ player.on(mpegts.Events.ERROR, (errorType: string, errorDetail: string, errorInfo: unknown) => {
logger.error('mpegts.js error:', { errorType, errorDetail, errorInfo })
const error = new Error(`MPEG-TS Player Error: ${errorType} - ${errorDetail}`)
@@ -125,7 +125,7 @@ export const setupMpegtsInstance = async ({
logger.log('mpegts.js: Recovered from early EOF')
})
- player.on(mpegts.Events.METADATA_ARRIVED, (metadata: any) => {
+ player.on(mpegts.Events.METADATA_ARRIVED, (metadata: Record) => {
logger.log('mpegts.js: Metadata arrived', metadata)
// Trigger onLoadedMetadata callback
@@ -134,10 +134,10 @@ export const setupMpegtsInstance = async ({
}
})
- player.on(mpegts.Events.STATISTICS_INFO, (stats: any) => {
+ player.on(mpegts.Events.STATISTICS_INFO, (stats: Record) => {
// Statistics info for debugging/monitoring
if (stats) {
- ;(video as any).__mpegtsStats = stats
+ video.__mpegtsStats = stats
}
})
@@ -174,8 +174,8 @@ export const setupMpegtsInstance = async ({
player.destroy()
// Clean up stored references
- delete (video as any).__mpegtsInstance
- delete (video as any).__mpegtsStats
+ delete video.__mpegtsInstance
+ delete video.__mpegtsStats
} catch (cleanupError) {
logger.error('Error during mpegts.js cleanup:', cleanupError)
}
@@ -199,11 +199,11 @@ export const setupMpegtsInstance = async ({
* @param video - The video element
* @returns The mpegts.js player instance or null
*/
-export const getMpegtsInstance = (video: HTMLVideoElement | null): any | null => {
+export const getMpegtsInstance = (video: HTMLVideoElement | null): MpegtsPlayer | PlayerInstance | null => {
if (!video) {
return null
}
- return (video as any).__mpegtsInstance || null
+ return video.__mpegtsInstance ?? null
}
/**
@@ -211,11 +211,11 @@ export const getMpegtsInstance = (video: HTMLVideoElement | null): any | null =>
* @param video - The video element
* @returns The statistics object or null
*/
-export const getMpegtsStats = (video: HTMLVideoElement | null): any | null => {
+export const getMpegtsStats = (video: HTMLVideoElement | null): Record | null => {
if (!video) {
return null
}
- return (video as any).__mpegtsStats || null
+ return video.__mpegtsStats ?? null
}
/**
@@ -226,4 +226,3 @@ export const getMpegtsStats = (video: HTMLVideoElement | null): any | null => {
export const hasMpegtsInstance = (video: HTMLVideoElement | null): boolean => {
return getMpegtsInstance(video) !== null
}
-
diff --git a/src/utils/polyfills.ts b/src/utils/polyfills.ts
index 4bb1baf..5276549 100644
--- a/src/utils/polyfills.ts
+++ b/src/utils/polyfills.ts
@@ -9,21 +9,15 @@
*/
export const setupFullscreenPolyfill = () => {
if (!document.exitFullscreen) {
- // @ts-ignore - Legacy API
- document.exitFullscreen = document.webkitExitFullscreen ||
- // @ts-ignore
+ document.exitFullscreen = (document.webkitExitFullscreen ||
document.mozCancelFullScreen ||
- // @ts-ignore
- document.msExitFullscreen
+ document.msExitFullscreen) as typeof document.exitFullscreen
}
if (!Element.prototype.requestFullscreen) {
- // @ts-ignore - Legacy API
- Element.prototype.requestFullscreen = Element.prototype.webkitRequestFullscreen ||
- // @ts-ignore
+ Element.prototype.requestFullscreen = (Element.prototype.webkitRequestFullscreen ||
Element.prototype.mozRequestFullScreen ||
- // @ts-ignore
- Element.prototype.msRequestFullscreen
+ Element.prototype.msRequestFullscreen) as typeof Element.prototype.requestFullscreen
}
// Fullscreen change event polyfill
@@ -41,11 +35,8 @@ export const setupFullscreenPolyfill = () => {
if (!Object.prototype.hasOwnProperty.call(document, 'fullscreenElement')) {
Object.defineProperty(document, 'fullscreenElement', {
get: function() {
- // @ts-ignore
return this.webkitFullscreenElement ||
- // @ts-ignore
this.mozFullScreenElement ||
- // @ts-ignore
this.msFullscreenElement
}
})
@@ -157,11 +148,8 @@ export const features = {
if (!isBrowser) return false
return !!(
document.fullscreenEnabled ||
- // @ts-ignore
document.webkitFullscreenEnabled ||
- // @ts-ignore
document.mozFullScreenEnabled ||
- // @ts-ignore
document.msFullscreenEnabled
)
},
diff --git a/src/utils/rtmpLoader.ts b/src/utils/rtmpLoader.ts
index e35c0fc..360f698 100644
--- a/src/utils/rtmpLoader.ts
+++ b/src/utils/rtmpLoader.ts
@@ -11,10 +11,10 @@ const FLVJS_CDN_URL = 'https://cdn.jsdelivr.net/npm/flv.js@1.6.2/dist/flv.min.js
* Dynamically loads flv.js from CDN
* @returns Promise that resolves to the flv.js library
*/
-const loadFlvjsFromCDN = async (): Promise => {
+const loadFlvjsFromCDN = async (): Promise => {
return new Promise((resolve, reject) => {
- if ((window as any).flvjs) {
- resolve((window as any).flvjs)
+ if (window.flvjs) {
+ resolve(window.flvjs)
return
}
@@ -23,8 +23,8 @@ const loadFlvjsFromCDN = async (): Promise => {
script.async = true
script.onload = () => {
- if ((window as any).flvjs) {
- resolve((window as any).flvjs)
+ if (window.flvjs) {
+ resolve(window.flvjs)
} else {
reject(new Error('flv.js loaded but not available on window'))
}
@@ -43,7 +43,7 @@ const loadFlvjsFromCDN = async (): Promise => {
* Tries NPM package first, falls back to CDN if unavailable
* @returns Promise that resolves to the flv.js library
*/
-export const loadFlvjs = async (): Promise => {
+export const loadFlvjs = async (): Promise => {
try {
// Try loading from NPM package first
const flvModule = await import('flv.js')
@@ -69,7 +69,7 @@ export const loadFlvjs = async (): Promise => {
* @param flvjs - The flv.js library instance
* @returns True if supported
*/
-export const isFlvjsSupported = (flvjs: any): boolean => {
+export const isFlvjsSupported = (flvjs: FlvjsStatic): boolean => {
if (!flvjs) {
return false
}
@@ -83,7 +83,7 @@ export const isFlvjsSupported = (flvjs: any): boolean => {
* @param flvjs - The flv.js library instance
* @returns Support information object
*/
-export const getFlvjsSupportInfo = (flvjs: any): {
+export const getFlvjsSupportInfo = (flvjs: FlvjsStatic): {
mseSupported: boolean
networkStreamIOSupported: boolean
httpsSupported: boolean
@@ -138,7 +138,7 @@ export const createDefaultFlvConfig = (isLive: boolean = true) => {
* @param player - The flv.js player instance
* @returns Basic quality information
*/
-export const extractFlvQualityInfo = (player: any): {
+export const extractFlvQualityInfo = (player: FlvjsPlayer): {
width?: number
height?: number
videoCodec?: string
@@ -154,17 +154,16 @@ export const extractFlvQualityInfo = (player: any): {
try {
const stats = player.statisticsInfo
return {
- width: stats.videoWidth,
- height: stats.videoHeight,
- videoCodec: stats.videoCodec,
- audioCodec: stats.audioCodec,
- fps: stats.fps,
- videoBitrate: stats.videoBitrate,
- audioBitrate: stats.audioBitrate,
+ width: stats.videoWidth as number | undefined,
+ height: stats.videoHeight as number | undefined,
+ videoCodec: stats.videoCodec as string | undefined,
+ audioCodec: stats.audioCodec as string | undefined,
+ fps: stats.fps as number | undefined,
+ videoBitrate: stats.videoBitrate as number | undefined,
+ audioBitrate: stats.audioBitrate as number | undefined,
}
} catch (error) {
logger.warn('Failed to extract flv.js quality info:', error)
return null
}
}
-
diff --git a/src/utils/rtmpSetup.ts b/src/utils/rtmpSetup.ts
index 6b053bb..2770f42 100644
--- a/src/utils/rtmpSetup.ts
+++ b/src/utils/rtmpSetup.ts
@@ -81,10 +81,10 @@ export const setupRtmpInstance = async ({
player.load()
// Store player instance on video element for later access
- ;(video as any).__rtmpInstance = player
+ video.__rtmpInstance = player
// Event handlers
- player.on(flvjs.Events.ERROR, (errorType: string, errorDetail: string, errorInfo: any) => {
+ player.on(flvjs.Events.ERROR, (errorType: string, errorDetail: string, errorInfo: unknown) => {
logger.error('flv.js error:', { errorType, errorDetail, errorInfo })
const error = new Error(`FLV Player Error: ${errorType} - ${errorDetail}`)
@@ -137,7 +137,7 @@ export const setupRtmpInstance = async ({
logger.log('flv.js: Recovered from early EOF')
})
- player.on(flvjs.Events.METADATA_ARRIVED, (metadata: any) => {
+ player.on(flvjs.Events.METADATA_ARRIVED, (metadata: Record) => {
logger.log('flv.js: Metadata arrived', metadata)
// Trigger onLoadedMetadata callback
@@ -146,11 +146,11 @@ export const setupRtmpInstance = async ({
}
})
- player.on(flvjs.Events.STATISTICS_INFO, (stats: any) => {
+ player.on(flvjs.Events.STATISTICS_INFO, (stats: Record) => {
// Statistics info for debugging/monitoring
// Can be used to display stream quality, bitrate, etc.
if (stats) {
- ;(video as any).__rtmpStats = stats
+ video.__rtmpStats = stats
}
})
@@ -187,8 +187,8 @@ export const setupRtmpInstance = async ({
player.destroy()
// Clean up stored references
- delete (video as any).__rtmpInstance
- delete (video as any).__rtmpStats
+ delete video.__rtmpInstance
+ delete video.__rtmpStats
} catch (cleanupError) {
logger.error('Error during flv.js cleanup:', cleanupError)
}
@@ -212,11 +212,11 @@ export const setupRtmpInstance = async ({
* @param video - The video element
* @returns The flv.js player instance or null
*/
-export const getRtmpInstance = (video: HTMLVideoElement | null): any | null => {
+export const getRtmpInstance = (video: HTMLVideoElement | null): FlvjsPlayer | PlayerInstance | null => {
if (!video) {
return null
}
- return (video as any).__rtmpInstance || null
+ return video.__rtmpInstance ?? null
}
/**
@@ -224,11 +224,11 @@ export const getRtmpInstance = (video: HTMLVideoElement | null): any | null => {
* @param video - The video element
* @returns The statistics object or null
*/
-export const getRtmpStats = (video: HTMLVideoElement | null): any | null => {
+export const getRtmpStats = (video: HTMLVideoElement | null): Record | null => {
if (!video) {
return null
}
- return (video as any).__rtmpStats || null
+ return video.__rtmpStats ?? null
}
/**
@@ -239,4 +239,3 @@ export const getRtmpStats = (video: HTMLVideoElement | null): any | null => {
export const hasRtmpInstance = (video: HTMLVideoElement | null): boolean => {
return getRtmpInstance(video) !== null
}
-
diff --git a/tsconfig.lib.json b/tsconfig.lib.json
index 742426d..f053b76 100644
--- a/tsconfig.lib.json
+++ b/tsconfig.lib.json
@@ -9,7 +9,7 @@
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
- "strict": false,
+ "strict": true,
"skipLibCheck": true
},
"include": ["src"],
diff --git a/vite.config.lib.ts b/vite.config.lib.ts
index f3867bb..3ec251e 100644
--- a/vite.config.lib.ts
+++ b/vite.config.lib.ts
@@ -42,7 +42,7 @@ export default defineConfig({
},
},
rollupOptions: {
- external: ['react', 'react-dom', 'react/jsx-runtime', 'hls.js', 'flv.js'],
+ external: ['react', 'react-dom', 'react/jsx-runtime', 'hls.js', 'flv.js', 'mpegts.js'],
output: {
globals: {
react: 'React',
@@ -50,6 +50,7 @@ export default defineConfig({
'react/jsx-runtime': 'jsxRuntime',
'hls.js': 'Hls',
'flv.js': 'flvjs',
+ 'mpegts.js': 'mpegts',
},
compact: true,
},