Skip to content

Commit

Permalink
Merge pull request #58 from Uscreen-video/support-multi-audio-tracks
Browse files Browse the repository at this point in the history
Support multi audio tracks
  • Loading branch information
timaramazanov authored Feb 3, 2025
2 parents c0a83d1 + 8810f7c commit 47d376f
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 2 deletions.
48 changes: 46 additions & 2 deletions src/components/buttons/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ const icons = {
chevron: unsafeSVG(_chevronIcon),
};

type Menu = "shortcuts" | "rate" | "quality";
type Menu = "shortcuts" | "rate" | "quality" | "audio";

@customElement("video-settings-button")
export class SubtitlesButton extends VideoButton {
@property({
type: Array,
converter: (v) => v.split(",").map((v) => v.trim()),
})
settings: Menu[] = ["shortcuts", "rate", "quality"];
settings: Menu[] = ["shortcuts", "rate", "quality", "audio"];

@property({ type: Object })
translation: Record<string, any> = {};
Expand All @@ -38,6 +38,12 @@ export class SubtitlesButton extends VideoButton {
@connect("qualityLevels")
qualityLevels: Types.State["qualityLevels"];

@connect("audioTracks")
audioTracks: Types.State["audioTracks"];

@connect("activeAudioTrackId")
activeAudioTrackId: Types.State["activeAudioTrackId"];

@state()
activeMenu: Menu;

Expand Down Expand Up @@ -127,6 +133,8 @@ export class SubtitlesButton extends VideoButton {
return this.selectRate(value);
case "quality":
return this.setQuality(value);
case "audio":
return this.selectAudio(value);
default:
return this.selectMenu(value);
}
Expand All @@ -150,6 +158,8 @@ export class SubtitlesButton extends VideoButton {
return this.shortcutsMenuItems;
case "quality":
return this.qualityMenuItems;
case "audio":
return this.audioMenuItems;
default:
return this.mainMenuItems;
}
Expand All @@ -165,13 +175,19 @@ export class SubtitlesButton extends VideoButton {
this.removeMenu();
};

selectAudio = (id: string) => {
this.command(Types.Command.enableAudioTrack, { trackId: id });
this.removeMenu();
}

selectMenu(menu?: Menu) {
this.activeMenu = this.isSingleMenuItem ? this.settings[0] : menu;

// We need to trigger resize event to update the menu position
Promise.resolve().then(() => emit(this, "resize"));
}


get isSingleMenuItem() {
return this.settings.length === 1;
}
Expand Down Expand Up @@ -208,12 +224,22 @@ export class SubtitlesButton extends VideoButton {
iconAfter: `${this.playbackRate}x`,
});

if (this.settings.includes("audio") && this.audioTracks?.length && this.activeAudioTrackId) {
menu.push({
label: "Audio",
value: "audio",
iconAfter: this.audioTracks.find(t => t.id === this.activeAudioTrackId)?.label,
});
}

if (this.settings.includes("quality") && this.qualityLevels?.length)
menu.push({
label: "Quality",
value: "quality",
iconAfter: this.qualityLevel,
});


return menu;
}

Expand Down Expand Up @@ -260,4 +286,22 @@ export class SubtitlesButton extends VideoButton {
...items,
];
}

get audioMenuItems(): any {
const items = this.audioTracks.map((track) => ({
label: track.label,
value: track.id,
iconAfter: this.activeAudioTrackId === track.id ? icons.check : undefined,
isActive: this.activeAudioTrackId === track.id,
}));
if (this.isSingleMenuItem) return items;
return [
{
label: "back",
iconBefore: icons.chevron,
value: "back",
},
...items,
];
}
}
28 changes: 28 additions & 0 deletions src/components/video-container/Video-container.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { when } from "lit/directives/when.js";
import "../buttons/Play";
import { subtitlesController, SubtitlesController } from "./subtitles";
import { sourcesController, SourcesController } from "./sources";
import { audiosController } from "./audios";
import type { AudiosController } from "./audios/types";

const INIT_NATIVE_HLS_RE = /^((?!chrome|android).)*safari/i;

Expand All @@ -42,6 +44,7 @@ export class VideoContainer extends LitElement {
public command = createCommand(this);
private subtitles: SubtitlesController;
private sources: SourcesController;
private audios: AudiosController;

hls: Hls;
initTime: number;
Expand Down Expand Up @@ -201,6 +204,11 @@ export class VideoContainer extends LitElement {
this.subtitles.enableTextTrack(trackId);
}

@listen(Types.Command.enableAudioTrack)
enableAudioTrack({ trackId }: { trackId: string }) {
this.audios.enableAudioTrack(trackId);
}

@listen(Types.Command.setPlaybackRate, { canPlay: true })
setPlaybackRate({ playbackRate }: { playbackRate: number }) {
this.videos[0].playbackRate = this.videos[0].defaultPlaybackRate =
Expand Down Expand Up @@ -342,6 +350,15 @@ export class VideoContainer extends LitElement {
this.hls,
this._storageProvider.get().activeTextTrackId,
);

window.requestAnimationFrame(() => {
this.audios = audiosController(
this,
this.videos[0],
this.hls,
this._storageProvider.get().activeAudioTrackId,
);
})
},
);

Expand Down Expand Up @@ -437,6 +454,7 @@ export class VideoContainer extends LitElement {
@listen(Types.Command.toggleMuted)
@listen(Types.Command.setPlaybackRate)
@listen(Types.Command.enableTextTrack)
@listen(Types.Command.enableAudioTrack)
_syncStateWithStorage(params: CommandParams, _: any, command: Types.Command) {
if (!this.storageKey) return;

Expand Down Expand Up @@ -466,6 +484,10 @@ export class VideoContainer extends LitElement {
key = "activeTextTrackId";
value = params.trackId;
break;
case Types.Command.enableAudioTrack:
key = "activeAudioTrackId";
value = params.trackId;
break;
}
const currentVal = this._storageProvider.get();
this._storageProvider.set({ ...currentVal, [key]: value });
Expand Down Expand Up @@ -496,6 +518,12 @@ export class VideoContainer extends LitElement {
this.hls,
savedSettings.activeTextTrackId,
);
this.audios = audiosController(
this,
this.videos[0],
this.hls,
savedSettings.activeAudioTrackId
);
}

const [
Expand Down
46 changes: 46 additions & 0 deletions src/components/video-container/audios/hls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { VideoContainer } from "../Video-container.component";
import type Hls from "hls.js";
import type { MediaPlaylist } from "hls.js";
import { dispatch, Types } from "../../../state";
import type { AudiosController } from "./types";
import _debug from "debug";

const audiosDebug = _debug("player:audios");

const buildHlsAudioTrackId = (track: MediaPlaylist) => `${track.name}-${track.lang}`

export const hlsController = (host: VideoContainer, hls: Hls, activeAudioTrackId?: string): AudiosController => {
const enableAudioTrack = (id: string) => {
const newHlsTrackId = hls.audioTracks.find(t => buildHlsAudioTrackId(t) === id)?.id
if (typeof newHlsTrackId === 'number') {
hls.audioTrack = newHlsTrackId
dispatch(host, Types.Action.update, {
activeAudioTrackId: id
})
audiosDebug('AUDIO TRACK ENABLED', id)
}
}

if (hls.audioTracks.length > 0) {
dispatch(host, Types.Action.update, {
audioTracks: hls.audioTracks.map((track) => ({
label: track.name,
lang: track.lang,
id: buildHlsAudioTrackId(track)
})),
activeAudioTrackId: buildHlsAudioTrackId(hls.audioTracks.find(t => t.id === hls.audioTrack) || hls.audioTracks[0])
})

hls.audioTracks.forEach(t => {
audiosDebug('AUDIO TRACK ADDED', t.id, t.name, t.lang)
})

if (activeAudioTrackId) {
enableAudioTrack(activeAudioTrackId)
}
}

return {
enableAudioTrack
}
}
13 changes: 13 additions & 0 deletions src/components/video-container/audios/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { nativeController } from "./native";
import { hlsController } from "./hls";
import type { AudiosController, VideoElementWithAudioTracks } from "./types";
import type { VideoContainer } from "../Video-container.component";
import type Hls from "hls.js";

export const audiosController = (host: VideoContainer, video: HTMLVideoElement, hls?: Hls, activeAudioTrackId?: string): AudiosController => {
if (hls) {
return hlsController(host, hls, activeAudioTrackId)
} else {
return nativeController(host, video as VideoElementWithAudioTracks, activeAudioTrackId)
}
}
58 changes: 58 additions & 0 deletions src/components/video-container/audios/native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { VideoContainer } from "../Video-container.component";
import debounce from "../../../helpers/debounce"
import { dispatch, Types } from "../../../state"
import type { AudiosController, AudioTrack, VideoElementWithAudioTracks } from "./types"
import _debug from "debug";

const audiosDebug = _debug("player:audios");

const buildNativeAudioTrackId = (track: AudioTrack) => `${track.label}-${track.language}`

export const nativeController = (host: VideoContainer, video: VideoElementWithAudioTracks, activeAudioTrackId?: string): AudiosController => {
const enableAudioTrack = (id: string) => {
const newActiveTrack = Array.from(video.audioTracks).find(t => buildNativeAudioTrackId(t) === id)

if (newActiveTrack) {
for (let i = 0; i < video.audioTracks.length; i++) {
video.audioTracks[i].enabled = buildNativeAudioTrackId(video.audioTracks[i]) === id
}
audiosDebug('AUDIO TRACK ENABLED', id)
}

}

const onTrackAdded = debounce(() => {
const audioTracks = Array.from(video.audioTracks)

dispatch(host, Types.Action.update, {
audioTracks: audioTracks.map((track) => ({
label: track.label,
lang: track.language,
id: buildNativeAudioTrackId(track)
})),
activeAudioTrackId: buildNativeAudioTrackId(audioTracks.find(t => t.enabled) || audioTracks[0])
})

audioTracks.forEach(t => {
audiosDebug('AUDIO TRACK ADDED', t.label, t.language, t.enabled)
})

if (activeAudioTrackId) {
enableAudioTrack(activeAudioTrackId)
}
}, 100)

const onTracksChanged = debounce(() => {
const audioTracks = Array.from(video.audioTracks)
dispatch(host, Types.Action.update, {
activeAudioTrackId: buildNativeAudioTrackId(audioTracks.find(t => t.enabled) || audioTracks[0])
})
}, 100)

video.audioTracks.addEventListener('addtrack', onTrackAdded)
video.audioTracks.addEventListener('change', onTracksChanged)

return {
enableAudioTrack
}
}
12 changes: 12 additions & 0 deletions src/components/video-container/audios/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export type AudiosController = {
enableAudioTrack: (id: string) => void;
}

export type AudioTrack = {
id: string;
label: string;
language: string;
enabled: boolean;
}

export type VideoElementWithAudioTracks = HTMLVideoElement & { audioTracks: AudioTrack[] & { addEventListener: (event: string, listener: () => void) => void } }
1 change: 1 addition & 0 deletions src/helpers/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type StorageValue = Partial<
| "activeQualityLevel"
| "activeTextTrackId"
| "playbackRate"
| "activeAudioTrackId"
>
>;

Expand Down
9 changes: 9 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export enum Command {
backward = "backward",
/** Enable a text track */
enableTextTrack = "enableTextTrack",
/** Enable an audio track */
enableAudioTrack = "enableAudioTrack",
/** Sets video quality */
setQualityLevel = "setQualityLevel",
/** Initialize the custom HLS player */
Expand Down Expand Up @@ -73,6 +75,7 @@ export enum Action {
interacted = "interacted",
idle = "idle",
selectTextTrack = "selectTextTrack",
selectAudioTrack = "selectAudioTrack",
cues = "cues",
setPlaybackRate = "setPlaybackRate",
setQualityLevel = "setQualityLevel",
Expand Down Expand Up @@ -117,6 +120,7 @@ export type State = Partial<
isSourceSupported: boolean;
isFullscreen: boolean;
activeTextTrackId: string;
activeAudioTrackId: string;
activeQualityLevel: number;
playbackRate: number;
customHLS: boolean;
Expand All @@ -134,6 +138,11 @@ export type State = Partial<
lang: string;
id: string;
}[];
audioTracks: {
label: string,
lang: string,
id: string
}[];
qualityLevels: {
name: string;
}[];
Expand Down

0 comments on commit 47d376f

Please sign in to comment.