@nomercy-entertainment/nomercy-video-player 2.1.1 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.1.2] — 2026-08-29
6
+
7
+ ### Fixed
8
+
9
+ - ASS/SSA subtitles rendered in the wrong face. The libass font map was keyed by the attachment's filename, so a release whose attachments are not named after their family resolved nothing and every line fell back to an arbitrary attached font. Keys now come from the font's own name table.
10
+ - The chosen audio and subtitle language is remembered and re-applied on every item, instead of the player trusting a track index and the playlist's `DEFAULT` flag — both of which differ per file, so a picked language was lost on the next episode.
11
+ - A selected track index left over from the previous item no longer ticks in the menu while the stream plays a different track.
12
+
5
13
  ## [2.1.0] — 2026-08-20
6
14
 
7
15
  ### Added
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [![npm](https://img.shields.io/npm/v/@nomercy-entertainment/nomercy-video-player/rc?label=rc)](https://www.npmjs.com/package/@nomercy-entertainment/nomercy-video-player)
1
+ [![npm](https://img.shields.io/npm/v/@nomercy-entertainment/nomercy-video-player/latest?label=latest)](https://www.npmjs.com/package/@nomercy-entertainment/nomercy-video-player)
2
2
  [![license](https://img.shields.io/npm/l/@nomercy-entertainment/nomercy-video-player)](./LICENSE)
3
3
  [![bundlephobia](https://img.shields.io/bundlephobia/minzip/@nomercy-entertainment/nomercy-video-player)](https://bundlephobia.com/package/@nomercy-entertainment/nomercy-video-player)
4
4
 
package/dist/index.d.ts CHANGED
@@ -172,6 +172,12 @@ export declare class NMVideoPlayer<T extends VideoPlaylistItem = VideoPlaylistIt
172
172
  plugins: () => ReadonlyArray<Plugin>;
173
173
  enabledPlugins: () => ReadonlyArray<Plugin>;
174
174
  constructor(id?: string | number);
175
+ private _languageMemoryInstance;
176
+ /**
177
+ * Carries the viewer's chosen audio / subtitle language across items. Built
178
+ * on first use — `options` is not assigned yet when fields initialise.
179
+ */
180
+ private get languageMemory();
175
181
  private _wantedPoster;
176
182
  private _defaultQualityApplied;
177
183
  /**
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ import { readItemImage } from './player/itemImage.js';
11
11
  import { normalizeVideoPlaylistItem } from './player/normalize-item.js';
12
12
  import { VideoPreloadStrategy } from './player/preload.js';
13
13
  import { pickStartItem } from './player/start-selection.js';
14
+ import { SUBTITLES_OFF, TrackLanguageMemory } from './player/track-language-memory.js';
14
15
  import { titleTokenTranslations } from './plugins/desktop-ui/i18n/token-bundle.js';
15
16
  import { containedRect, FullscreenState, PipState, SubtitleState, TheaterState } from './types.js';
16
17
  export { VttChapterSource } from './adapters/chapter-source/vtt-chapters.js';
@@ -111,6 +112,17 @@ export class NMVideoPlayer extends EventEmitter {
111
112
  this._applyDefaultTracks();
112
113
  this._emitVideoRect();
113
114
  });
115
+ // Record the chosen LANGUAGE, not the index — an index means a different
116
+ // language on the next file. Fires for the viewer's own pick and for the
117
+ // selection _applyDefaultTracks restores, which write the same value.
118
+ this.on('audioTrack', ({ id }) => {
119
+ if (id === null)
120
+ return;
121
+ this.languageMemory.rememberAudio(this.audioTracks()[id]?.language);
122
+ });
123
+ this.on('subtitle', ({ track }) => {
124
+ this.languageMemory.rememberSubtitle(track === null ? SUBTITLES_OFF : this.subtitles()[track]?.language);
125
+ });
114
126
  // Re-emit videoRect on signals that change the displayed content rect.
115
127
  this.on('duration', () => { this._emitVideoRect(); });
116
128
  this.on('fullscreen', () => { this._emitVideoRect(); });
@@ -129,6 +141,15 @@ export class NMVideoPlayer extends EventEmitter {
129
141
  catch { /* backend may not be ready yet — ignore */ }
130
142
  });
131
143
  }
144
+ _languageMemoryInstance;
145
+ /**
146
+ * Carries the viewer's chosen audio / subtitle language across items. Built
147
+ * on first use — `options` is not assigned yet when fields initialise.
148
+ */
149
+ get languageMemory() {
150
+ this._languageMemoryInstance ??= new TrackLanguageMemory(this.options?.storage);
151
+ return this._languageMemoryInstance;
152
+ }
132
153
  _wantedPoster = null;
133
154
  _defaultQualityApplied = false;
134
155
  /**
@@ -194,8 +215,12 @@ export class NMVideoPlayer extends EventEmitter {
194
215
  * `'en-US'`). No match → leave selection at off; no warning emitted.
195
216
  */
196
217
  _applyDefaultTracks() {
197
- const subtitleLang = this.options?.defaultSubtitleLanguage;
198
- if (subtitleLang) {
218
+ const rememberedSubtitle = this.languageMemory.subtitleLanguage();
219
+ const subtitleLang = rememberedSubtitle ?? this.options?.defaultSubtitleLanguage;
220
+ if (subtitleLang === SUBTITLES_OFF) {
221
+ this.subtitle(-1);
222
+ }
223
+ else if (subtitleLang) {
199
224
  let tracks = [];
200
225
  try {
201
226
  tracks = this.subtitles();
@@ -206,17 +231,35 @@ export class NMVideoPlayer extends EventEmitter {
206
231
  this.subtitle(matchIdx);
207
232
  }
208
233
  }
209
- const audioLang = this.options?.defaultAudioLanguage;
210
- if (audioLang) {
211
- let tracks = [];
212
- try {
213
- tracks = this.audioTracks();
214
- }
215
- catch { /* not yet available */ }
216
- const matchIdx = _matchLanguage(tracks.map(audioTrack => audioTrack.language), audioLang);
217
- if (matchIdx >= 0) {
234
+ const audioLang = this.languageMemory.audioLanguage() ?? this.options?.defaultAudioLanguage;
235
+ const selectedIdx = this.audioTrack()?.index ?? null;
236
+ // Nothing to honour and no selection that could have gone stale — leave
237
+ // the backend alone rather than reading its track list for no reason.
238
+ if (!audioLang && selectedIdx === null)
239
+ return;
240
+ let audioTracks = [];
241
+ try {
242
+ audioTracks = this.audioTracks();
243
+ }
244
+ catch { /* not yet available */ }
245
+ const matchIdx = audioLang
246
+ ? _matchLanguage(audioTracks.map(audioTrack => audioTrack.language), audioLang)
247
+ : -1;
248
+ // With no language to honour, fall back to whichever rendition the stream
249
+ // marks default. The selected index survives an item change, so a stale
250
+ // one left alone ticks the previous item's track in the menu while the
251
+ // stream plays its own default.
252
+ const targetIdx = matchIdx >= 0
253
+ ? matchIdx
254
+ : audioTracks.findIndex(audioTrack => audioTrack.default === true);
255
+ if (matchIdx >= 0) {
256
+ if (selectedIdx !== matchIdx)
218
257
  this.audioTrack(matchIdx);
219
- }
258
+ }
259
+ // Nothing to honour: only correct a selection that actually disagrees.
260
+ // Re-asserting the track already playing costs a switch for no change.
261
+ else if (targetIdx >= 0 && selectedIdx !== null && selectedIdx !== targetIdx) {
262
+ this.audioTrack(targetIdx);
220
263
  }
221
264
  }
222
265
  /** Test-only: clear the registry. Not part of the public API. */