diff --git a/packages/react-native-video/src/core/VideoPlayer.ts b/packages/react-native-video/src/core/VideoPlayer.ts index c0af82d0..66dd19c8 100644 --- a/packages/react-native-video/src/core/VideoPlayer.ts +++ b/packages/react-native-video/src/core/VideoPlayer.ts @@ -17,6 +17,7 @@ import { createPlayer } from "./utils/playerFactory"; import { createSource } from "./utils/sourceFactory"; import { VideoPlayerEvents } from "./VideoPlayerEvents"; import type { AudioTrack } from "./types/AudioTrack"; +import type { VideoTrack } from "./types/VideoTrack"; class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { protected player: VideoPlayerImpl; @@ -297,6 +298,16 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { get selectedAudioTrack(): AudioTrack | undefined { return undefined; } + + getAvailableVideoTracks(): VideoTrack[] { + return []; + } + + selectVideoTrack(_: VideoTrack | null): void {} + + get selectedVideoTrack(): VideoTrack | undefined { + return undefined; + } } 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 4743d3ba..ca7ce0f3 100644 --- a/packages/react-native-video/src/core/VideoPlayer.web.ts +++ b/packages/react-native-video/src/core/VideoPlayer.web.ts @@ -16,6 +16,7 @@ import type { VideoPlayerStatus } from "./types/VideoPlayerStatus"; import { VideoPlayerEvents } from "./VideoPlayerEvents"; import { MediaSessionHandler } from "./web/MediaSession"; import { WebEventEmiter } from "./web/WebEventEmiter"; +import type { VideoTrack } from "./types/VideoTrack"; type VideoJsPlayer = ReturnType; @@ -33,7 +34,7 @@ export type VideoJsTextTracks = { }; }; -export type VideoJsAudioTracks = { +export type VideoJsTracks = { length: number; [i: number]: { id: string; @@ -301,9 +302,11 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { return this.getAvailableTextTracks().find((x) => x.selected); } + // audio tracks + getAvailableAudioTracks(): AudioTrack[] { // @ts-expect-error they define length & index properties via prototype - const tracks: VideoJsAudioTracks = this.player.audioTracks(); + const tracks: VideoJsTracks = this.player.audioTracks(); return [...Array(tracks.length)].map((_, i) => ({ id: tracks[i]!.id, @@ -315,7 +318,7 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { selectAudioTrack(track: AudioTrack | null): void { // @ts-expect-error they define length & index properties via prototype - const tracks: VideoJsAudioTracks = this.player.audioTracks(); + const tracks: VideoJsTracks = this.player.audioTracks(); for (let i = 0; i < tracks.length; i++) { tracks[i]!.enabled = tracks[i]!.id === track?.id; @@ -325,6 +328,33 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { get selectedAudioTrack(): AudioTrack | undefined { return this.getAvailableAudioTracks().find((x) => x.selected); } + + // video tracks + + getAvailableVideoTracks(): VideoTrack[] { + // @ts-expect-error they define length & index properties via prototype + const tracks: VideoJsTracks = this.player.videoTracks(); + + return [...Array(tracks.length)].map((_, i) => ({ + id: tracks[i]!.id, + label: tracks[i]!.label, + language: tracks[i]!.language, + selected: tracks[i]!.enabled, + })); + } + + selectVideoTrack(track: VideoTrack | null): void { + // @ts-expect-error they define length & index properties via prototype + const tracks: VideoJsTracks = this.player.videoTracks(); + + for (let i = 0; i < tracks.length; i++) { + tracks[i]!.enabled = tracks[i]!.id === track?.id; + } + } + + get selectedVideoTrack(): VideoTrack | undefined { + return this.getAvailableVideoTracks().find((x) => x.selected); + } } 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 4384ff2a..0f8bb2cc 100644 --- a/packages/react-native-video/src/core/types/Events.ts +++ b/packages/react-native-video/src/core/types/Events.ts @@ -4,6 +4,7 @@ import type { VideoRuntimeError } from './VideoError'; import type { VideoOrientation } from './VideoOrientation'; import type { VideoPlayerSourceBase } from './VideoPlayerSourceBase'; import type { VideoPlayerStatus } from './VideoPlayerStatus'; +import type { VideoTrack } from './VideoTrack'; export interface VideoPlayerEvents { /** @@ -101,6 +102,11 @@ export interface VideoPlayerEvents { * Called when the player status changes. */ onStatusChange: (status: VideoPlayerStatus) => void; + /** + * Called when the video track changes + * @param track The new video track + */ + onVideoTrackChange: (track: VideoTrack | null) => void; } export interface AllPlayerEvents extends VideoPlayerEvents { @@ -273,5 +279,6 @@ export const ALL_PLAYER_EVENTS: (keyof AllPlayerEvents)[] = 'onTextTrackDataChanged', 'onTrackChange', 'onVolumeChange', + 'onVideoTrackChange', 'onStatusChange' ); diff --git a/packages/react-native-video/src/core/types/VideoTrack.ts b/packages/react-native-video/src/core/types/VideoTrack.ts new file mode 100644 index 00000000..dd749e0d --- /dev/null +++ b/packages/react-native-video/src/core/types/VideoTrack.ts @@ -0,0 +1,22 @@ +export interface VideoTrack { + /** + * Unique identifier for the video track + */ + id: string; + + /** + * Display label for the video track + */ + label: string; + + /** + * Language code (ISO 639-1 or ISO 639-2) + * @example "en", "es", "fr" + */ + language?: string; + + /** + * Whether this track 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 3c5a13a5..31d21499 100644 --- a/packages/react-native-video/src/core/web/WebEventEmiter.ts +++ b/packages/react-native-video/src/core/web/WebEventEmiter.ts @@ -20,7 +20,8 @@ import { } from "../types/VideoError"; import type { VideoPlayerStatus } from "../types/VideoPlayerStatus"; import type { AudioTrack } from "../types/AudioTrack"; -import type { VideoJsAudioTracks } from "../VideoPlayer.web"; +import type { VideoJsTracks } from "../VideoPlayer.web"; +import type { VideoTrack } from "../types/VideoTrack"; type VideoJsPlayer = ReturnType; @@ -80,6 +81,9 @@ export class WebEventEmiter implements PlayerEvents { this._onAudioTrackChange = this._onAudioTrackChange.bind(this); this.player.audioTracks().on("change", this._onAudioTrackChange); + + this._onVideoTrackChange = this._onVideoTrackChange.bind(this); + this.player.videoTracks().on("change", this._onVideoTrackChange); } destroy() { @@ -106,6 +110,8 @@ export class WebEventEmiter implements PlayerEvents { this.player.off("error", this._onError); this.player.audioTracks().off("change", this._onAudioTrackChange); + + this.player.videoTracks().off("change", this._onVideoTrackChange); } _onTimeUpdate() { @@ -225,7 +231,7 @@ export class WebEventEmiter implements PlayerEvents { _onAudioTrackChange() { // @ts-expect-error they define length & index properties via prototype - const tracks: VideoJsAudioTracks = this.player.audioTracks(); + const tracks: VideoJsTracks = this.player.audioTracks(); const selected = [...Array(tracks.length)] .map((_, i) => ({ id: tracks[i]!.id, @@ -238,6 +244,21 @@ export class WebEventEmiter implements PlayerEvents { this.onAudioTrackChange(selected ?? null); } + _onVideoTrackChange() { + // @ts-expect-error they define length & index properties via prototype + const tracks: VideoJsTracks = this.player.videoTracks(); + const selected = [...Array(tracks.length)] + .map((_, i) => ({ + id: tracks[i]!.id, + label: tracks[i]!.label, + language: tracks[i]!.language, + selected: tracks[i]!.enabled, + })) + .find((x) => x.selected); + + this.onVideoTrackChange(selected ?? null); + } + NOOP = () => {}; onError: (error: VideoRuntimeError) => void = this.NOOP; @@ -262,4 +283,5 @@ export class WebEventEmiter implements PlayerEvents { onTrackChange: (track: TextTrack | null) => void = this.NOOP; onVolumeChange: (data: onVolumeChangeData) => void = this.NOOP; onStatusChange: (status: VideoPlayerStatus) => void = this.NOOP; + onVideoTrackChange: (track: VideoTrack | null) => void = this.NOOP; }