4 Commits

Author SHA1 Message Date
b1b5fa1717 Add quality selector 2025-10-20 01:24:18 +02:00
7f04c16e3e Add video track handling on web 2025-10-20 00:25:36 +02:00
959c23d6f2 Add audio track support 2025-10-20 00:13:46 +02:00
958778e647 Fix media session for web 2025-10-19 22:00:26 +02:00
9 changed files with 391 additions and 37 deletions

View File

@@ -1,21 +1,24 @@
import { Platform } from 'react-native'; import { Platform } from "react-native";
import { NitroModules } from 'react-native-nitro-modules'; import { NitroModules } from "react-native-nitro-modules";
import type { VideoPlayer as VideoPlayerImpl } from '../spec/nitro/VideoPlayer.nitro'; import type { VideoPlayer as VideoPlayerImpl } from "../spec/nitro/VideoPlayer.nitro";
import type { VideoPlayerSource } from '../spec/nitro/VideoPlayerSource.nitro'; import type { VideoPlayerSource } from "../spec/nitro/VideoPlayerSource.nitro";
import type { IgnoreSilentSwitchMode } from './types/IgnoreSilentSwitchMode'; import type { IgnoreSilentSwitchMode } from "./types/IgnoreSilentSwitchMode";
import type { MixAudioMode } from './types/MixAudioMode'; import type { MixAudioMode } from "./types/MixAudioMode";
import type { TextTrack } from './types/TextTrack'; import type { TextTrack } from "./types/TextTrack";
import type { NoAutocomplete } from './types/Utils'; import type { NoAutocomplete } from "./types/Utils";
import type { VideoConfig, VideoSource } from './types/VideoConfig'; import type { VideoConfig, VideoSource } from "./types/VideoConfig";
import { import {
tryParseNativeVideoError, tryParseNativeVideoError,
VideoRuntimeError, VideoRuntimeError,
} from './types/VideoError'; } from "./types/VideoError";
import type { VideoPlayerBase } from './types/VideoPlayerBase'; import type { VideoPlayerBase } from "./types/VideoPlayerBase";
import type { VideoPlayerStatus } from './types/VideoPlayerStatus'; import type { VideoPlayerStatus } from "./types/VideoPlayerStatus";
import { createPlayer } from './utils/playerFactory'; import { createPlayer } from "./utils/playerFactory";
import { createSource } from './utils/sourceFactory'; import { createSource } from "./utils/sourceFactory";
import { VideoPlayerEvents } from './VideoPlayerEvents'; 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 { class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
protected player: VideoPlayerImpl; protected player: VideoPlayerImpl;
@@ -57,7 +60,7 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
if ( if (
parsedError instanceof VideoRuntimeError && parsedError instanceof VideoRuntimeError &&
this.triggerEvent('onError', parsedError) this.triggerEvent("onError", parsedError)
) { ) {
// We don't throw errors if onError is provided // We don't throw errors if onError is provided
return; return;
@@ -153,9 +156,9 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
} }
set ignoreSilentSwitchMode(value: IgnoreSilentSwitchMode) { set ignoreSilentSwitchMode(value: IgnoreSilentSwitchMode) {
if (__DEV__ && !['ios'].includes(Platform.OS)) { if (__DEV__ && !["ios"].includes(Platform.OS)) {
console.warn( 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 +251,16 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
} }
async replaceSourceAsync( async replaceSourceAsync(
source: VideoSource | VideoConfig | NoAutocomplete<VideoPlayerSource> | null source:
| VideoSource
| VideoConfig
| NoAutocomplete<VideoPlayerSource>
| null,
): Promise<void> { ): Promise<void> {
await this.wrapPromise( await this.wrapPromise(
this.player.replaceSourceAsync( this.player.replaceSourceAsync(
source === null ? null : createSource(source) source === null ? null : createSource(source),
) ),
); );
NitroModules.updateMemorySize(this.player); NitroModules.updateMemorySize(this.player);
@@ -281,6 +288,43 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
get selectedTrack(): TextTrack | undefined { get selectedTrack(): TextTrack | undefined {
return this.player.selectedTrack; return this.player.selectedTrack;
} }
// TODO: implement this
getAvailableAudioTracks(): AudioTrack[] {
return [];
}
selectAudioTrack(_: AudioTrack | null): void {}
get selectedAudioTrack(): AudioTrack | undefined {
return undefined;
}
getAvailableVideoTracks(): VideoTrack[] {
return [];
}
selectVideoTrack(_: VideoTrack | null): void {}
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 }; export { VideoPlayer };

View File

@@ -1,20 +1,28 @@
import videojs from "video.js";
import type { VideoPlayerSource } from "../spec/nitro/VideoPlayerSource.nitro"; import type { VideoPlayerSource } from "../spec/nitro/VideoPlayerSource.nitro";
import type { AudioTrack } from "./types/AudioTrack";
import type { IgnoreSilentSwitchMode } from "./types/IgnoreSilentSwitchMode"; import type { IgnoreSilentSwitchMode } from "./types/IgnoreSilentSwitchMode";
import type { MixAudioMode } from "./types/MixAudioMode"; import type { MixAudioMode } from "./types/MixAudioMode";
import type { TextTrack } from "./types/TextTrack"; import type { TextTrack } from "./types/TextTrack";
import type { NoAutocomplete } from "./types/Utils"; import type { NoAutocomplete } from "./types/Utils";
import type { VideoConfig, VideoSource } from "./types/VideoConfig"; import type {
NativeVideoConfig,
VideoConfig,
VideoSource,
} from "./types/VideoConfig";
import type { VideoPlayerBase } from "./types/VideoPlayerBase"; import type { VideoPlayerBase } from "./types/VideoPlayerBase";
import type { VideoPlayerSourceBase } from "./types/VideoPlayerSourceBase";
import type { VideoPlayerStatus } from "./types/VideoPlayerStatus"; import type { VideoPlayerStatus } from "./types/VideoPlayerStatus";
import { VideoPlayerEvents } from "./VideoPlayerEvents"; import { VideoPlayerEvents } from "./VideoPlayerEvents";
import { MediaSessionHandler } from "./web/MediaSession"; import { MediaSessionHandler } from "./web/MediaSession";
import { WebEventEmiter } from "./web/WebEventEmiter"; import { WebEventEmiter } from "./web/WebEventEmiter";
import videojs from "video.js"; import type { VideoTrack } from "./types/VideoTrack";
import type { QualityLevel } from "./types/QualityLevel";
type VideoJsPlayer = ReturnType<typeof videojs>; type VideoJsPlayer = ReturnType<typeof videojs>;
// declared https://github.com/videojs/video.js/blob/main/src/js/tracks/track-list.js#L58 // declared https://github.com/videojs/video.js/blob/main/src/js/tracks/track-list.js#L58
type VideoJsTextTracks = { export type VideoJsTextTracks = {
length: number; length: number;
[i: number]: { [i: number]: {
// declared: https://github.com/videojs/video.js/blob/main/src/js/tracks/track.js // declared: https://github.com/videojs/video.js/blob/main/src/js/tracks/track.js
@@ -27,14 +35,40 @@ type VideoJsTextTracks = {
}; };
}; };
export type VideoJsTracks = {
length: number;
[i: number]: {
id: string;
label: string;
language: string;
enabled: boolean;
};
};
// 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 { class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
protected video: HTMLVideoElement; protected video: HTMLVideoElement;
public player: VideoJsPlayer; public player: VideoJsPlayer;
private mediaSession: MediaSessionHandler; private mediaSession: MediaSessionHandler;
private _source: NativeVideoConfig | undefined;
constructor(source: VideoSource | VideoConfig | VideoPlayerSource) { constructor(source: VideoSource | VideoConfig | VideoPlayerSource) {
const video = document.createElement("video"); const video = document.createElement("video");
const player = videojs(video); const player = videojs(video, { qualityLevels: true });
super(new WebEventEmiter(player)); super(new WebEventEmiter(player));
@@ -59,12 +93,23 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
} }
// Source // Source
get source(): VideoPlayerSource { get source(): VideoPlayerSourceBase {
// TODO: properly implement this
return { return {
uri: this.player.src(undefined), uri: this._source?.uri!,
config: {}, config: this._source!,
} as any; getAssetInformationAsync: async () => {
return {
bitrate: NaN,
width: this.player.videoWidth(),
height: this.player.videoHeight(),
duration: BigInt(this.duration),
fileSize: BigInt(NaN),
isHDR: false,
isLive: false,
orientation: "landscape",
};
},
};
} }
// Status // Status
@@ -166,8 +211,12 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
} }
set showNotificationControls(value: boolean) { set showNotificationControls(value: boolean) {
if (value) this.mediaSession.enable(); if (!value) {
else this.mediaSession.disable(); this.mediaSession.disable();
return;
}
this.mediaSession.enable();
this.mediaSession.updateMediaSession(this._source?.metadata);
} }
async initialize(): Promise<void> { async initialize(): Promise<void> {
@@ -219,20 +268,24 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
return; return;
} }
if (typeof source === "number") { if (typeof source === "string") {
source = { uri: source };
}
if (typeof source === "number" || typeof source.uri === "number") {
console.error( console.error(
"A source uri must be a string. Numbers are only supported on native.", "A source uri must be a string. Numbers are only supported on native.",
); );
return; return;
} }
if (typeof source === "string") { this._source = source as VideoPlayerSource;
source = { uri: source };
}
// TODO: handle start time // TODO: handle start time
this.player.src({ this.player.src({
src: source.uri, src: source.uri,
type: source.mimeType, type: source.mimeType,
}); });
if (this.mediaSession.enabled)
this.mediaSession.updateMediaSession(source.metadata);
if (source.initializeOnCreation) await this.preload(); if (source.initializeOnCreation) await this.preload();
} }
@@ -264,6 +317,97 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
get selectedTrack(): TextTrack | undefined { get selectedTrack(): TextTrack | undefined {
return this.getAvailableTextTracks().find((x) => x.selected); return this.getAvailableTextTracks().find((x) => x.selected);
} }
// audio tracks
getAvailableAudioTracks(): AudioTrack[] {
// @ts-expect-error they define length & index properties via prototype
const tracks: VideoJsTracks = 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: VideoJsTracks = 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);
}
// 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);
}
// 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 }; export { VideoPlayer };

View File

@@ -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;
}

View File

@@ -1,8 +1,11 @@
import type { AudioTrack } from './AudioTrack';
import type { QualityLevel } from './QualityLevel';
import type { TextTrack } from './TextTrack'; import type { TextTrack } from './TextTrack';
import type { VideoRuntimeError } from './VideoError'; import type { VideoRuntimeError } from './VideoError';
import type { VideoOrientation } from './VideoOrientation'; import type { VideoOrientation } from './VideoOrientation';
import type { VideoPlayerSourceBase } from './VideoPlayerSourceBase'; import type { VideoPlayerSourceBase } from './VideoPlayerSourceBase';
import type { VideoPlayerStatus } from './VideoPlayerStatus'; import type { VideoPlayerStatus } from './VideoPlayerStatus';
import type { VideoTrack } from './VideoTrack';
export interface VideoPlayerEvents { export interface VideoPlayerEvents {
/** /**
@@ -16,6 +19,11 @@ export interface VideoPlayerEvents {
* @platform Android * @platform Android
*/ */
onAudioFocusChange: (hasAudioFocus: boolean) => void; 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. * Called when the bandwidth of the video changes.
*/ */
@@ -63,6 +71,10 @@ export interface VideoPlayerEvents {
* Called when the player progress changes. * Called when the player progress changes.
*/ */
onProgress: (data: onProgressData) => void; onProgress: (data: onProgressData) => void;
/**
* Called when the player quality changes.
*/
onQualityChange: (quality: QualityLevel) => void;
/** /**
* Called when the video is ready to display. * Called when the video is ready to display.
*/ */
@@ -95,6 +107,11 @@ export interface VideoPlayerEvents {
* Called when the player status changes. * Called when the player status changes.
*/ */
onStatusChange: (status: VideoPlayerStatus) => void; 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 { export interface AllPlayerEvents extends VideoPlayerEvents {
@@ -249,6 +266,7 @@ export const ALL_PLAYER_EVENTS: (keyof AllPlayerEvents)[] =
allKeysOf<AllPlayerEvents>()( allKeysOf<AllPlayerEvents>()(
'onAudioBecomingNoisy', 'onAudioBecomingNoisy',
'onAudioFocusChange', 'onAudioFocusChange',
'onAudioTrackChange',
'onBandwidthUpdate', 'onBandwidthUpdate',
'onBuffer', 'onBuffer',
'onControlsVisibleChange', 'onControlsVisibleChange',
@@ -260,11 +278,13 @@ export const ALL_PLAYER_EVENTS: (keyof AllPlayerEvents)[] =
'onPlaybackStateChange', 'onPlaybackStateChange',
'onPlaybackRateChange', 'onPlaybackRateChange',
'onProgress', 'onProgress',
'onQualityChange',
'onReadyToDisplay', 'onReadyToDisplay',
'onSeek', 'onSeek',
'onTimedMetadata', 'onTimedMetadata',
'onTextTrackDataChanged', 'onTextTrackDataChanged',
'onTrackChange', 'onTrackChange',
'onVolumeChange', 'onVolumeChange',
'onVideoTrackChange',
'onStatusChange' 'onStatusChange'
); );

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -132,7 +132,11 @@ export class MediaSessionHandler {
disable() {} disable() {}
updateMediaSession(metadata: CustomVideoMetadata) { updateMediaSession(metadata: CustomVideoMetadata | undefined) {
if (!metadata) {
mediaSession.metadata = null;
return;
}
mediaSession.metadata = new window.MediaMetadata({ mediaSession.metadata = new window.MediaMetadata({
title: metadata.title, title: metadata.title,
album: metadata.subtitle, album: metadata.subtitle,

View File

@@ -19,6 +19,10 @@ import {
type VideoRuntimeError, type VideoRuntimeError,
} from "../types/VideoError"; } from "../types/VideoError";
import type { VideoPlayerStatus } from "../types/VideoPlayerStatus"; import type { VideoPlayerStatus } from "../types/VideoPlayerStatus";
import type { AudioTrack } from "../types/AudioTrack";
import type { VideoJsTracks, VideoJsQualityArray } from "../VideoPlayer.web";
import type { VideoTrack } from "../types/VideoTrack";
import type { QualityLevel } from "../types/QualityLevel";
type VideoJsPlayer = ReturnType<typeof videojs>; type VideoJsPlayer = ReturnType<typeof videojs>;
@@ -75,6 +79,16 @@ export class WebEventEmiter implements PlayerEvents {
// on status change // on status change
this._onError = this._onError.bind(this); this._onError = this._onError.bind(this);
this.player.on("error", this._onError); this.player.on("error", this._onError);
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);
this._onQualityChange = this._onQualityChange.bind(this);
// @ts-expect-error this isn't typed
this.player.qualityLevels().on("change", this._onQualityChange);
} }
destroy() { destroy() {
@@ -99,6 +113,14 @@ export class WebEventEmiter implements PlayerEvents {
this.player.off("volumechange", this._onVolumeChange); this.player.off("volumechange", this._onVolumeChange);
this.player.off("error", this._onError); this.player.off("error", this._onError);
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() { _onTimeUpdate() {
@@ -216,11 +238,56 @@ export class WebEventEmiter implements PlayerEvents {
this.onError(new VideoError(codeMap[err.code]!, err.message)); this.onError(new VideoError(codeMap[err.code]!, err.message));
} }
_onAudioTrackChange() {
// @ts-expect-error they define length & index properties via prototype
const tracks: VideoJsTracks = 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);
}
_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);
}
_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 = () => {}; NOOP = () => {};
onError: (error: VideoRuntimeError) => void = this.NOOP; onError: (error: VideoRuntimeError) => void = this.NOOP;
onAudioBecomingNoisy: () => void = this.NOOP; onAudioBecomingNoisy: () => void = this.NOOP;
onAudioFocusChange: (hasAudioFocus: boolean) => void = this.NOOP; onAudioFocusChange: (hasAudioFocus: boolean) => void = this.NOOP;
onAudioTrackChange: (track: AudioTrack | null) => void = this.NOOP;
onBandwidthUpdate: (data: BandwidthData) => void = this.NOOP; onBandwidthUpdate: (data: BandwidthData) => void = this.NOOP;
onBuffer: (buffering: boolean) => void = this.NOOP; onBuffer: (buffering: boolean) => void = this.NOOP;
onControlsVisibleChange: (visible: boolean) => void = this.NOOP; onControlsVisibleChange: (visible: boolean) => void = this.NOOP;
@@ -239,4 +306,6 @@ export class WebEventEmiter implements PlayerEvents {
onTrackChange: (track: TextTrack | null) => void = this.NOOP; onTrackChange: (track: TextTrack | null) => void = this.NOOP;
onVolumeChange: (data: onVolumeChangeData) => void = this.NOOP; onVolumeChange: (data: onVolumeChangeData) => void = this.NOOP;
onStatusChange: (status: VideoPlayerStatus) => void = this.NOOP; onStatusChange: (status: VideoPlayerStatus) => void = this.NOOP;
onVideoTrackChange: (track: VideoTrack | null) => void = this.NOOP;
onQualityChange: (quality: QualityLevel) => void = this.NOOP;
} }

View File

@@ -5,6 +5,9 @@ export type { IgnoreSilentSwitchMode } from "./core/types/IgnoreSilentSwitchMode
export type { MixAudioMode } from "./core/types/MixAudioMode"; export type { MixAudioMode } from "./core/types/MixAudioMode";
export type { ResizeMode } from "./core/types/ResizeMode"; export type { ResizeMode } from "./core/types/ResizeMode";
export type { TextTrack } from "./core/types/TextTrack"; 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 { VideoConfig, VideoSource } from "./core/types/VideoConfig";
export type { export type {
LibraryError, LibraryError,