mirror of
https://github.com/zoriya/react-native-video.git
synced 2025-12-06 07:16:12 +00:00
Compare commits
4 Commits
c49b2f17f2
...
b1b5fa1717
| Author | SHA1 | Date | |
|---|---|---|---|
| b1b5fa1717 | |||
| 7f04c16e3e | |||
| 959c23d6f2 | |||
| 958778e647 |
@@ -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 };
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
22
packages/react-native-video/src/core/types/AudioTrack.ts
Normal file
22
packages/react-native-video/src/core/types/AudioTrack.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
);
|
);
|
||||||
|
|||||||
26
packages/react-native-video/src/core/types/QualityLevel.ts
Normal file
26
packages/react-native-video/src/core/types/QualityLevel.ts
Normal 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;
|
||||||
|
}
|
||||||
22
packages/react-native-video/src/core/types/VideoTrack.ts
Normal file
22
packages/react-native-video/src/core/types/VideoTrack.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user