unified-video-framework
Version:
Cross-platform video player framework supporting iOS, Android, Web, Smart TVs (Samsung/LG), Roku, and more
238 lines (199 loc) • 6.32 kB
text/typescript
import { EventEmitter } from 'events';
import {
VideoPlayerConfig,
VideoSource,
PlayerState,
PlayerEvent,
EventHandler,
Quality,
SubtitleTrack,
AudioTrack,
PlayerMetrics,
PlayerError,
DRMConfig
} from './interfaces';
export abstract class VideoPlayer {
protected config: VideoPlayerConfig;
protected eventEmitter: EventEmitter;
protected state: PlayerState;
protected currentSource?: VideoSource;
protected metrics: PlayerMetrics;
protected errors: PlayerError[] = [];
constructor(config: VideoPlayerConfig = {}) {
this.config = {
autoPlay: false,
muted: false,
controls: true,
loop: false,
preload: 'metadata',
playsInline: true,
...config
};
this.eventEmitter = new EventEmitter();
this.state = PlayerState.IDLE;
this.metrics = this.initializeMetrics();
}
// Core playback methods
abstract load(source: VideoSource): Promise<void>;
abstract play(): Promise<void>;
abstract pause(): void;
abstract stop(): void;
abstract seek(position: number): void;
abstract setVolume(volume: number): void;
abstract setPlaybackRate(rate: number): void;
abstract getCurrentTime(): number;
abstract getDuration(): number;
abstract getVolume(): number;
abstract getPlaybackRate(): number;
abstract isMuted(): boolean;
abstract setMuted(muted: boolean): void;
// Quality management
abstract getAvailableQualities(): Quality[];
abstract getCurrentQuality(): Quality | null;
abstract setQuality(quality: Quality): void;
abstract enableAutoQuality(enabled: boolean): void;
// Subtitle/Audio tracks
abstract getSubtitleTracks(): SubtitleTrack[];
abstract getCurrentSubtitleTrack(): SubtitleTrack | null;
abstract setSubtitleTrack(track: SubtitleTrack | null): void;
abstract getAudioTracks(): AudioTrack[];
abstract getCurrentAudioTrack(): AudioTrack | null;
abstract setAudioTrack(track: AudioTrack): void;
// Platform-specific features
abstract enterFullscreen(): void;
abstract exitFullscreen(): void;
abstract isFullscreen(): boolean;
abstract enterPictureInPicture(): void;
abstract exitPictureInPicture(): void;
abstract isPictureInPicture(): boolean;
// Events
on(event: PlayerEvent, handler: EventHandler): void {
this.eventEmitter.on(event, handler);
}
off(event: PlayerEvent, handler: EventHandler): void {
this.eventEmitter.off(event, handler);
}
once(event: PlayerEvent, handler: EventHandler): void {
this.eventEmitter.once(event, handler);
}
removeAllListeners(event?: PlayerEvent): void {
if (event) {
this.eventEmitter.removeAllListeners(event);
} else {
this.eventEmitter.removeAllListeners();
}
}
protected emit(event: PlayerEvent, data?: any): void {
this.eventEmitter.emit(event, data);
// Track analytics events
if (this.config.analytics?.enabled) {
this.trackAnalytics(event, data);
}
}
// State management
getState(): PlayerState {
return this.state;
}
protected setState(newState: PlayerState): void {
const oldState = this.state;
this.state = newState;
if (oldState !== newState) {
this.emit('statechange' as PlayerEvent, { oldState, newState });
}
}
// Configuration
getConfig(): VideoPlayerConfig {
return { ...this.config };
}
updateConfig(config: Partial<VideoPlayerConfig>): void {
this.config = { ...this.config, ...config };
this.applyConfig();
}
protected abstract applyConfig(): void;
// Metrics
getMetrics(): PlayerMetrics {
return {
...this.metrics,
errors: [...this.errors]
};
}
protected initializeMetrics(): PlayerMetrics {
return {
sessionId: this.generateSessionId(),
totalPlayTime: 0,
bufferingCount: 0,
bufferingDuration: 0,
averageBitrate: 0,
qualityChanges: 0,
errors: []
};
}
protected generateSessionId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
// Error handling
protected handleError(error: PlayerError): void {
this.errors.push(error);
this.emit('onError', error);
if (error.fatal) {
this.setState(PlayerState.ERROR);
}
}
// Analytics
protected trackAnalytics(event: string, data?: any): void {
if (!this.config.analytics?.providers) return;
const analyticsData = {
event,
timestamp: Date.now(),
sessionId: this.metrics.sessionId,
currentTime: this.getCurrentTime(),
duration: this.getDuration(),
state: this.state,
...data
};
this.config.analytics.providers.forEach(provider => {
try {
provider.track(event, analyticsData);
} catch (error) {
console.error(`Analytics provider ${provider.name} failed:`, error);
}
});
}
// DRM
protected abstract configureDRM(drmConfig: DRMConfig): Promise<void>;
// Cleanup
abstract destroy(): void;
protected cleanup(): void {
this.removeAllListeners();
this.state = PlayerState.IDLE;
this.currentSource = undefined;
}
// Utility methods
protected formatTime(seconds: number): string {
if (!isFinite(seconds)) return '00:00';
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
if (hours > 0) {
return `${hours}:${this.pad(minutes)}:${this.pad(secs)}`;
}
return `${minutes}:${this.pad(secs)}`;
}
private pad(num: number): string {
return num.toString().padStart(2, '0');
}
// Buffer management
abstract getBufferedRanges(): TimeRanges;
abstract getSeekableRanges(): TimeRanges;
protected isBuffering(): boolean {
return this.state === PlayerState.BUFFERING;
}
// Network
abstract getBandwidth(): number;
abstract getNetworkState(): number;
// Video properties
abstract getVideoWidth(): number;
abstract getVideoHeight(): number;
abstract getDroppedFrames(): number;
abstract getDecodedFrames(): number;
}