diff --git a/packages/react-native-video/src/core/VideoPlayer.ts b/packages/react-native-video/src/core/VideoPlayer.ts index 96bcfc33..c0af82d0 100644 --- a/packages/react-native-video/src/core/VideoPlayer.ts +++ b/packages/react-native-video/src/core/VideoPlayer.ts @@ -1,21 +1,22 @@ -import { Platform } from 'react-native'; -import { NitroModules } from 'react-native-nitro-modules'; -import type { VideoPlayer as VideoPlayerImpl } from '../spec/nitro/VideoPlayer.nitro'; -import type { VideoPlayerSource } from '../spec/nitro/VideoPlayerSource.nitro'; -import type { IgnoreSilentSwitchMode } from './types/IgnoreSilentSwitchMode'; -import type { MixAudioMode } from './types/MixAudioMode'; -import type { TextTrack } from './types/TextTrack'; -import type { NoAutocomplete } from './types/Utils'; -import type { VideoConfig, VideoSource } from './types/VideoConfig'; +import { Platform } from "react-native"; +import { NitroModules } from "react-native-nitro-modules"; +import type { VideoPlayer as VideoPlayerImpl } from "../spec/nitro/VideoPlayer.nitro"; +import type { VideoPlayerSource } from "../spec/nitro/VideoPlayerSource.nitro"; +import type { IgnoreSilentSwitchMode } from "./types/IgnoreSilentSwitchMode"; +import type { MixAudioMode } from "./types/MixAudioMode"; +import type { TextTrack } from "./types/TextTrack"; +import type { NoAutocomplete } from "./types/Utils"; +import type { VideoConfig, VideoSource } from "./types/VideoConfig"; import { tryParseNativeVideoError, VideoRuntimeError, -} from './types/VideoError'; -import type { VideoPlayerBase } from './types/VideoPlayerBase'; -import type { VideoPlayerStatus } from './types/VideoPlayerStatus'; -import { createPlayer } from './utils/playerFactory'; -import { createSource } from './utils/sourceFactory'; -import { VideoPlayerEvents } from './VideoPlayerEvents'; +} from "./types/VideoError"; +import type { VideoPlayerBase } from "./types/VideoPlayerBase"; +import type { VideoPlayerStatus } from "./types/VideoPlayerStatus"; +import { createPlayer } from "./utils/playerFactory"; +import { createSource } from "./utils/sourceFactory"; +import { VideoPlayerEvents } from "./VideoPlayerEvents"; +import type { AudioTrack } from "./types/AudioTrack"; class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { protected player: VideoPlayerImpl; @@ -57,7 +58,7 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { if ( parsedError instanceof VideoRuntimeError && - this.triggerEvent('onError', parsedError) + this.triggerEvent("onError", parsedError) ) { // We don't throw errors if onError is provided return; @@ -153,9 +154,9 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { } set ignoreSilentSwitchMode(value: IgnoreSilentSwitchMode) { - if (__DEV__ && !['ios'].includes(Platform.OS)) { + if (__DEV__ && !["ios"].includes(Platform.OS)) { console.warn( - 'ignoreSilentSwitchMode is not supported on this platform, it wont have any effect' + "ignoreSilentSwitchMode is not supported on this platform, it wont have any effect", ); } @@ -248,12 +249,16 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { } async replaceSourceAsync( - source: VideoSource | VideoConfig | NoAutocomplete | null + source: + | VideoSource + | VideoConfig + | NoAutocomplete + | null, ): Promise { await this.wrapPromise( this.player.replaceSourceAsync( - source === null ? null : createSource(source) - ) + source === null ? null : createSource(source), + ), ); NitroModules.updateMemorySize(this.player); @@ -281,6 +286,17 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { get selectedTrack(): TextTrack | undefined { return this.player.selectedTrack; } + + // TODO: implement this + getAvailableAudioTracks(): AudioTrack[] { + return []; + } + + selectAudioTrack(_: AudioTrack | null): void {} + + get selectedAudioTrack(): AudioTrack | 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 da2a2998..4743d3ba 100644 --- a/packages/react-native-video/src/core/VideoPlayer.web.ts +++ b/packages/react-native-video/src/core/VideoPlayer.web.ts @@ -1,5 +1,6 @@ import videojs from "video.js"; import type { VideoPlayerSource } from "../spec/nitro/VideoPlayerSource.nitro"; +import type { AudioTrack } from "./types/AudioTrack"; import type { IgnoreSilentSwitchMode } from "./types/IgnoreSilentSwitchMode"; import type { MixAudioMode } from "./types/MixAudioMode"; import type { TextTrack } from "./types/TextTrack"; @@ -10,16 +11,16 @@ import type { VideoSource, } from "./types/VideoConfig"; import type { VideoPlayerBase } from "./types/VideoPlayerBase"; +import type { VideoPlayerSourceBase } from "./types/VideoPlayerSourceBase"; import type { VideoPlayerStatus } from "./types/VideoPlayerStatus"; import { VideoPlayerEvents } from "./VideoPlayerEvents"; import { MediaSessionHandler } from "./web/MediaSession"; import { WebEventEmiter } from "./web/WebEventEmiter"; -import type { VideoPlayerSourceBase } from "./types/VideoPlayerSourceBase"; type VideoJsPlayer = ReturnType; // declared https://github.com/videojs/video.js/blob/main/src/js/tracks/track-list.js#L58 -type VideoJsTextTracks = { +export type VideoJsTextTracks = { length: number; [i: number]: { // declared: https://github.com/videojs/video.js/blob/main/src/js/tracks/track.js @@ -32,6 +33,16 @@ type VideoJsTextTracks = { }; }; +export type VideoJsAudioTracks = { + length: number; + [i: number]: { + id: string; + label: string; + language: string; + enabled: boolean; + }; +}; + class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { protected video: HTMLVideoElement; public player: VideoJsPlayer; @@ -289,6 +300,31 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { get selectedTrack(): TextTrack | undefined { return this.getAvailableTextTracks().find((x) => x.selected); } + + getAvailableAudioTracks(): AudioTrack[] { + // @ts-expect-error they define length & index properties via prototype + const tracks: VideoJsAudioTracks = this.player.audioTracks(); + + return [...Array(tracks.length)].map((_, i) => ({ + id: tracks[i]!.id, + label: tracks[i]!.label, + language: tracks[i]!.language, + selected: tracks[i]!.enabled, + })); + } + + selectAudioTrack(track: AudioTrack | null): void { + // @ts-expect-error they define length & index properties via prototype + const tracks: VideoJsAudioTracks = this.player.audioTracks(); + + for (let i = 0; i < tracks.length; i++) { + tracks[i]!.enabled = tracks[i]!.id === track?.id; + } + } + + get selectedAudioTrack(): AudioTrack | undefined { + return this.getAvailableAudioTracks().find((x) => x.selected); + } } export { VideoPlayer }; diff --git a/packages/react-native-video/src/core/types/AudioTrack.ts b/packages/react-native-video/src/core/types/AudioTrack.ts new file mode 100644 index 00000000..88d6dfc2 --- /dev/null +++ b/packages/react-native-video/src/core/types/AudioTrack.ts @@ -0,0 +1,22 @@ +export interface AudioTrack { + /** + * Unique identifier for the audio track + */ + id: string; + + /** + * Display label for the audio 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/types/Events.ts b/packages/react-native-video/src/core/types/Events.ts index 040b8a96..4384ff2a 100644 --- a/packages/react-native-video/src/core/types/Events.ts +++ b/packages/react-native-video/src/core/types/Events.ts @@ -1,3 +1,4 @@ +import type { AudioTrack } from './AudioTrack'; import type { TextTrack } from './TextTrack'; import type { VideoRuntimeError } from './VideoError'; import type { VideoOrientation } from './VideoOrientation'; @@ -16,6 +17,11 @@ export interface VideoPlayerEvents { * @platform Android */ onAudioFocusChange: (hasAudioFocus: boolean) => void; + /** + * Called when the audio track changes + * @param track The new audio track + */ + onAudioTrackChange: (track: AudioTrack | null) => void; /** * Called when the bandwidth of the video changes. */ @@ -249,6 +255,7 @@ export const ALL_PLAYER_EVENTS: (keyof AllPlayerEvents)[] = allKeysOf()( 'onAudioBecomingNoisy', 'onAudioFocusChange', + 'onAudioTrackChange', 'onBandwidthUpdate', 'onBuffer', 'onControlsVisibleChange', diff --git a/packages/react-native-video/src/core/web/WebEventEmiter.ts b/packages/react-native-video/src/core/web/WebEventEmiter.ts index 9eeae3f4..3c5a13a5 100644 --- a/packages/react-native-video/src/core/web/WebEventEmiter.ts +++ b/packages/react-native-video/src/core/web/WebEventEmiter.ts @@ -19,6 +19,8 @@ import { type VideoRuntimeError, } from "../types/VideoError"; import type { VideoPlayerStatus } from "../types/VideoPlayerStatus"; +import type { AudioTrack } from "../types/AudioTrack"; +import type { VideoJsAudioTracks } from "../VideoPlayer.web"; type VideoJsPlayer = ReturnType; @@ -75,6 +77,9 @@ export class WebEventEmiter implements PlayerEvents { // on status change this._onError = this._onError.bind(this); this.player.on("error", this._onError); + + this._onAudioTrackChange = this._onAudioTrackChange.bind(this); + this.player.audioTracks().on("change", this._onAudioTrackChange); } destroy() { @@ -99,6 +104,8 @@ export class WebEventEmiter implements PlayerEvents { this.player.off("volumechange", this._onVolumeChange); this.player.off("error", this._onError); + + this.player.audioTracks().off("change", this._onAudioTrackChange); } _onTimeUpdate() { @@ -216,11 +223,27 @@ export class WebEventEmiter implements PlayerEvents { this.onError(new VideoError(codeMap[err.code]!, err.message)); } + _onAudioTrackChange() { + // @ts-expect-error they define length & index properties via prototype + const tracks: VideoJsAudioTracks = this.player.audioTracks(); + 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.onAudioTrackChange(selected ?? null); + } + NOOP = () => {}; onError: (error: VideoRuntimeError) => void = this.NOOP; onAudioBecomingNoisy: () => void = this.NOOP; onAudioFocusChange: (hasAudioFocus: boolean) => void = this.NOOP; + onAudioTrackChange: (track: AudioTrack | null) => void = this.NOOP; onBandwidthUpdate: (data: BandwidthData) => void = this.NOOP; onBuffer: (buffering: boolean) => void = this.NOOP; onControlsVisibleChange: (visible: boolean) => void = this.NOOP;