diff --git a/packages/react-native-video/src/core/VideoPlayer.ts b/packages/react-native-video/src/core/VideoPlayer.ts index 66dd19c8..a7fb6bbf 100644 --- a/packages/react-native-video/src/core/VideoPlayer.ts +++ b/packages/react-native-video/src/core/VideoPlayer.ts @@ -18,6 +18,7 @@ import { createSource } from "./utils/sourceFactory"; import { VideoPlayerEvents } from "./VideoPlayerEvents"; import type { AudioTrack } from "./types/AudioTrack"; import type { VideoTrack } from "./types/VideoTrack"; +import type { QualityLevel } from "./types/QualityLevel"; class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { protected player: VideoPlayerImpl; @@ -308,6 +309,22 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { get selectedVideoTrack(): VideoTrack | undefined { return undefined; } + + // quality + + getAvailableQualities(): QualityLevel[] { + return []; + } + + selectQuality(_: QualityLevel | null): void {} + + get currentQuality(): QualityLevel | undefined { + return undefined; + } + + get autoQualityEnabled(): boolean { + return true; + } } export { VideoPlayer }; diff --git a/packages/react-native-video/src/core/VideoPlayer.web.ts b/packages/react-native-video/src/core/VideoPlayer.web.ts index ca7ce0f3..e2c1ddfa 100644 --- a/packages/react-native-video/src/core/VideoPlayer.web.ts +++ b/packages/react-native-video/src/core/VideoPlayer.web.ts @@ -17,6 +17,7 @@ import { VideoPlayerEvents } from "./VideoPlayerEvents"; import { MediaSessionHandler } from "./web/MediaSession"; import { WebEventEmiter } from "./web/WebEventEmiter"; import type { VideoTrack } from "./types/VideoTrack"; +import type { QualityLevel } from "./types/QualityLevel"; type VideoJsPlayer = ReturnType; @@ -44,6 +45,21 @@ export type VideoJsTracks = { }; }; +// declared https://github.com/videojs/videojs-contrib-quality-levels/blob/main/src/quality-level.js#L32 +export type VideoJsQualityArray = { + length: number; + selectedIndex: number; + [i: number]: { + id: string; + label: string; + width: number; + height: number; + bitrate: number; + frameRate: number; + enabled: boolean; + }; +}; + class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { protected video: HTMLVideoElement; public player: VideoJsPlayer; @@ -52,7 +68,7 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { constructor(source: VideoSource | VideoConfig | VideoPlayerSource) { const video = document.createElement("video"); - const player = videojs(video); + const player = videojs(video, { qualityLevels: true }); super(new WebEventEmiter(player)); @@ -355,6 +371,43 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { get selectedVideoTrack(): VideoTrack | undefined { return this.getAvailableVideoTracks().find((x) => x.selected); } + + // quality + + getAvailableQualities(): QualityLevel[] { + // @ts-expect-error this isn't typed + const levels: VideoJsQualityArray = this.player.qualityLevels(); + return [...Array(levels.length)].map((_, i) => ({ + id: levels[i]!.id, + width: levels[i]!.width, + height: levels[i]!.height, + bitrate: levels[i]!.bitrate, + selected: levels.selectedIndex === i, + })); + } + + selectQuality(quality: QualityLevel | null): void { + // @ts-expect-error this isn't typed + const levels: VideoJsQualityArray = this.player.qualityLevels(); + + for (let i = 0; i < levels.length; i++) { + // if quality is null, enable back auto-quality switch (so enable all lvls) + levels[i]!.enabled = !quality || levels[i]!.id === quality.id; + } + } + + get currentQuality(): QualityLevel | undefined { + return this.getAvailableQualities().find((x) => x.selected); + } + get autoQualityEnabled(): boolean { + // @ts-expect-error this isn't typed + const levels: VideoJsQualityArray = this.player.qualityLevels(); + // if we have a quality disabled that means we manually disabled it & disabled auto quality + for (let i = 0; i < levels.length; i++) { + if (!levels[i]!.enabled) return false; + } + return true; + } } export { VideoPlayer }; diff --git a/packages/react-native-video/src/core/types/Events.ts b/packages/react-native-video/src/core/types/Events.ts index 0f8bb2cc..6911ecfe 100644 --- a/packages/react-native-video/src/core/types/Events.ts +++ b/packages/react-native-video/src/core/types/Events.ts @@ -1,4 +1,5 @@ import type { AudioTrack } from './AudioTrack'; +import type { QualityLevel } from './QualityLevel'; import type { TextTrack } from './TextTrack'; import type { VideoRuntimeError } from './VideoError'; import type { VideoOrientation } from './VideoOrientation'; @@ -70,6 +71,10 @@ export interface VideoPlayerEvents { * Called when the player progress changes. */ onProgress: (data: onProgressData) => void; + /** + * Called when the player quality changes. + */ + onQualityChange: (quality: QualityLevel) => void; /** * Called when the video is ready to display. */ @@ -273,6 +278,7 @@ export const ALL_PLAYER_EVENTS: (keyof AllPlayerEvents)[] = 'onPlaybackStateChange', 'onPlaybackRateChange', 'onProgress', + 'onQualityChange', 'onReadyToDisplay', 'onSeek', 'onTimedMetadata', diff --git a/packages/react-native-video/src/core/types/QualityLevel.ts b/packages/react-native-video/src/core/types/QualityLevel.ts new file mode 100644 index 00000000..32e73cd8 --- /dev/null +++ b/packages/react-native-video/src/core/types/QualityLevel.ts @@ -0,0 +1,26 @@ +export interface QualityLevel { + /** + * Unique identifier for the quality + */ + id: string; + + /** + * Width of the quality + */ + width: number; + + /** + * Height of the quality + */ + height: number; + + /** + * Bitrate of the quality + */ + bitrate: number; + + /** + * Whether this quality is currently selected + */ + selected: boolean; +} diff --git a/packages/react-native-video/src/core/web/WebEventEmiter.ts b/packages/react-native-video/src/core/web/WebEventEmiter.ts index 31d21499..1093950a 100644 --- a/packages/react-native-video/src/core/web/WebEventEmiter.ts +++ b/packages/react-native-video/src/core/web/WebEventEmiter.ts @@ -20,8 +20,9 @@ import { } from "../types/VideoError"; import type { VideoPlayerStatus } from "../types/VideoPlayerStatus"; import type { AudioTrack } from "../types/AudioTrack"; -import type { VideoJsTracks } from "../VideoPlayer.web"; +import type { VideoJsTracks, VideoJsQualityArray } from "../VideoPlayer.web"; import type { VideoTrack } from "../types/VideoTrack"; +import type { QualityLevel } from "../types/QualityLevel"; type VideoJsPlayer = ReturnType; @@ -84,6 +85,10 @@ export class WebEventEmiter implements PlayerEvents { this._onVideoTrackChange = this._onVideoTrackChange.bind(this); this.player.videoTracks().on("change", this._onVideoTrackChange); + + this._onQualityChange = this._onQualityChange.bind(this); + // @ts-expect-error this isn't typed + this.player.qualityLevels().on("change", this._onQualityChange); } destroy() { @@ -112,6 +117,10 @@ export class WebEventEmiter implements PlayerEvents { this.player.audioTracks().off("change", this._onAudioTrackChange); this.player.videoTracks().off("change", this._onVideoTrackChange); + + this._onQualityChange = this._onQualityChange.bind(this); + // @ts-expect-error this isn't typed + this.player.qualityLevels().off("change", this._onQualityChange); } _onTimeUpdate() { @@ -259,6 +268,20 @@ export class WebEventEmiter implements PlayerEvents { this.onVideoTrackChange(selected ?? null); } + _onQualityChange() { + // @ts-expect-error this isn't typed + const levels: VideoJsQualityArray = this.player.qualityLevels(); + const quality = levels[levels.selectedIndex]!; + + this.onQualityChange({ + id: quality.id, + width: quality.width, + height: quality.height, + bitrate: quality.bitrate, + selected: true, + }); + } + NOOP = () => {}; onError: (error: VideoRuntimeError) => void = this.NOOP; @@ -284,4 +307,5 @@ export class WebEventEmiter implements PlayerEvents { onVolumeChange: (data: onVolumeChangeData) => void = this.NOOP; onStatusChange: (status: VideoPlayerStatus) => void = this.NOOP; onVideoTrackChange: (track: VideoTrack | null) => void = this.NOOP; + onQualityChange: (quality: QualityLevel) => void = this.NOOP; } diff --git a/packages/react-native-video/src/index.tsx b/packages/react-native-video/src/index.tsx index 1a1dbfab..cf1ec3c9 100644 --- a/packages/react-native-video/src/index.tsx +++ b/packages/react-native-video/src/index.tsx @@ -5,6 +5,9 @@ export type { IgnoreSilentSwitchMode } from "./core/types/IgnoreSilentSwitchMode export type { MixAudioMode } from "./core/types/MixAudioMode"; export type { ResizeMode } from "./core/types/ResizeMode"; export type { TextTrack } from "./core/types/TextTrack"; +export type { AudioTrack } from "./core/types/AudioTrack"; +export type { VideoTrack } from "./core/types/VideoTrack"; +export type { QualityLevel } from "./core/types/QualityLevel"; export type { VideoConfig, VideoSource } from "./core/types/VideoConfig"; export type { LibraryError,