Add video track handling on web

This commit is contained in:
2025-10-20 00:25:36 +02:00
parent 959c23d6f2
commit 7f04c16e3e
5 changed files with 97 additions and 5 deletions

View File

@@ -17,6 +17,7 @@ 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 { AudioTrack } from "./types/AudioTrack";
import type { VideoTrack } from "./types/VideoTrack";
class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
protected player: VideoPlayerImpl; protected player: VideoPlayerImpl;
@@ -297,6 +298,16 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
get selectedAudioTrack(): AudioTrack | undefined { get selectedAudioTrack(): AudioTrack | undefined {
return undefined; return undefined;
} }
getAvailableVideoTracks(): VideoTrack[] {
return [];
}
selectVideoTrack(_: VideoTrack | null): void {}
get selectedVideoTrack(): VideoTrack | undefined {
return undefined;
}
} }
export { VideoPlayer }; export { VideoPlayer };

View File

@@ -16,6 +16,7 @@ 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 type { VideoTrack } from "./types/VideoTrack";
type VideoJsPlayer = ReturnType<typeof videojs>; type VideoJsPlayer = ReturnType<typeof videojs>;
@@ -33,7 +34,7 @@ export type VideoJsTextTracks = {
}; };
}; };
export type VideoJsAudioTracks = { export type VideoJsTracks = {
length: number; length: number;
[i: number]: { [i: number]: {
id: string; id: string;
@@ -301,9 +302,11 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
return this.getAvailableTextTracks().find((x) => x.selected); return this.getAvailableTextTracks().find((x) => x.selected);
} }
// audio tracks
getAvailableAudioTracks(): AudioTrack[] { getAvailableAudioTracks(): AudioTrack[] {
// @ts-expect-error they define length & index properties via prototype // @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) => ({ return [...Array(tracks.length)].map((_, i) => ({
id: tracks[i]!.id, id: tracks[i]!.id,
@@ -315,7 +318,7 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
selectAudioTrack(track: AudioTrack | null): void { selectAudioTrack(track: AudioTrack | null): void {
// @ts-expect-error they define length & index properties via prototype // @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++) { for (let i = 0; i < tracks.length; i++) {
tracks[i]!.enabled = tracks[i]!.id === track?.id; tracks[i]!.enabled = tracks[i]!.id === track?.id;
@@ -325,6 +328,33 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
get selectedAudioTrack(): AudioTrack | undefined { get selectedAudioTrack(): AudioTrack | undefined {
return this.getAvailableAudioTracks().find((x) => x.selected); 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 }; export { VideoPlayer };

View File

@@ -4,6 +4,7 @@ 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 {
/** /**
@@ -101,6 +102,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 {
@@ -273,5 +279,6 @@ export const ALL_PLAYER_EVENTS: (keyof AllPlayerEvents)[] =
'onTextTrackDataChanged', 'onTextTrackDataChanged',
'onTrackChange', 'onTrackChange',
'onVolumeChange', 'onVolumeChange',
'onVideoTrackChange',
'onStatusChange' 'onStatusChange'
); );

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

@@ -20,7 +20,8 @@ import {
} 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 { 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<typeof videojs>; type VideoJsPlayer = ReturnType<typeof videojs>;
@@ -80,6 +81,9 @@ export class WebEventEmiter implements PlayerEvents {
this._onAudioTrackChange = this._onAudioTrackChange.bind(this); this._onAudioTrackChange = this._onAudioTrackChange.bind(this);
this.player.audioTracks().on("change", this._onAudioTrackChange); this.player.audioTracks().on("change", this._onAudioTrackChange);
this._onVideoTrackChange = this._onVideoTrackChange.bind(this);
this.player.videoTracks().on("change", this._onVideoTrackChange);
} }
destroy() { destroy() {
@@ -106,6 +110,8 @@ export class WebEventEmiter implements PlayerEvents {
this.player.off("error", this._onError); this.player.off("error", this._onError);
this.player.audioTracks().off("change", this._onAudioTrackChange); this.player.audioTracks().off("change", this._onAudioTrackChange);
this.player.videoTracks().off("change", this._onVideoTrackChange);
} }
_onTimeUpdate() { _onTimeUpdate() {
@@ -225,7 +231,7 @@ export class WebEventEmiter implements PlayerEvents {
_onAudioTrackChange() { _onAudioTrackChange() {
// @ts-expect-error they define length & index properties via prototype // @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)] const selected = [...Array(tracks.length)]
.map((_, i) => ({ .map((_, i) => ({
id: tracks[i]!.id, id: tracks[i]!.id,
@@ -238,6 +244,21 @@ export class WebEventEmiter implements PlayerEvents {
this.onAudioTrackChange(selected ?? null); 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 = () => {}; NOOP = () => {};
onError: (error: VideoRuntimeError) => void = this.NOOP; onError: (error: VideoRuntimeError) => void = this.NOOP;
@@ -262,4 +283,5 @@ 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;
} }