mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-07 13:02:52 +01:00
fix: restore StreamRanker for cross-episode track matching
Removing StreamRanker reduced track matching to direct index reuse. When a next episode has a different audio/subtitle track layout (common with anime multi-sub releases or mixed sources), the stored index points at the wrong track. StreamRanker scores by codec/language/title/relative index and only applies a match above threshold, otherwise falls back to media defaults. Restores utils/streamRanker.ts and its use in getDefaultPlaySettings so the cleanup PR stays behavior-preserving for playback.
This commit is contained in:
@@ -15,6 +15,11 @@ import type {
|
|||||||
} from "@jellyfin/sdk/lib/generated-client";
|
} from "@jellyfin/sdk/lib/generated-client";
|
||||||
import { BITRATES } from "@/components/BitrateSelector";
|
import { BITRATES } from "@/components/BitrateSelector";
|
||||||
import { type Settings } from "../atoms/settings";
|
import { type Settings } from "../atoms/settings";
|
||||||
|
import {
|
||||||
|
AudioStreamRanker,
|
||||||
|
StreamRanker,
|
||||||
|
SubtitleStreamRanker,
|
||||||
|
} from "../streamRanker";
|
||||||
|
|
||||||
export interface PlaySettings {
|
export interface PlaySettings {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
@@ -49,27 +54,42 @@ export function getDefaultPlaySettings(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mediaSource = item.MediaSources?.[0];
|
const mediaSource = item.MediaSources?.[0];
|
||||||
const _streams = mediaSource?.MediaStreams ?? [];
|
const streams = mediaSource?.MediaStreams ?? [];
|
||||||
|
|
||||||
// Start with media source defaults
|
// Start with media source defaults
|
||||||
let audioIndex = mediaSource?.DefaultAudioStreamIndex;
|
let audioIndex = mediaSource?.DefaultAudioStreamIndex;
|
||||||
let subtitleIndex = mediaSource?.DefaultSubtitleStreamIndex ?? -1;
|
let subtitleIndex = mediaSource?.DefaultSubtitleStreamIndex ?? -1;
|
||||||
|
|
||||||
// Try to match previous selections (sequential play)
|
// Try to match previous selections (sequential play)
|
||||||
// Simplified: just use previous indexes if available
|
if (previous?.indexes && previous?.source && settings) {
|
||||||
if (previous?.indexes && settings) {
|
|
||||||
if (
|
if (
|
||||||
settings.rememberSubtitleSelections &&
|
settings.rememberSubtitleSelections &&
|
||||||
previous.indexes.subtitleIndex !== undefined
|
previous.indexes.subtitleIndex !== undefined
|
||||||
) {
|
) {
|
||||||
subtitleIndex = previous.indexes.subtitleIndex;
|
const ranker = new StreamRanker(new SubtitleStreamRanker());
|
||||||
|
const result = { DefaultSubtitleStreamIndex: subtitleIndex };
|
||||||
|
ranker.rankStream(
|
||||||
|
previous.indexes.subtitleIndex,
|
||||||
|
previous.source,
|
||||||
|
streams,
|
||||||
|
result,
|
||||||
|
);
|
||||||
|
subtitleIndex = result.DefaultSubtitleStreamIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
settings.rememberAudioSelections &&
|
settings.rememberAudioSelections &&
|
||||||
previous.indexes.audioIndex !== undefined
|
previous.indexes.audioIndex !== undefined
|
||||||
) {
|
) {
|
||||||
audioIndex = previous.indexes.audioIndex;
|
const ranker = new StreamRanker(new AudioStreamRanker());
|
||||||
|
const result = { DefaultAudioStreamIndex: audioIndex };
|
||||||
|
ranker.rankStream(
|
||||||
|
previous.indexes.audioIndex,
|
||||||
|
previous.source,
|
||||||
|
streams,
|
||||||
|
result,
|
||||||
|
);
|
||||||
|
audioIndex = result.DefaultAudioStreamIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
159
utils/streamRanker.ts
Normal file
159
utils/streamRanker.ts
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import type {
|
||||||
|
MediaSourceInfo,
|
||||||
|
MediaStream,
|
||||||
|
} from "@jellyfin/sdk/lib/generated-client";
|
||||||
|
|
||||||
|
abstract class StreamRankerStrategy {
|
||||||
|
abstract streamType: string;
|
||||||
|
|
||||||
|
abstract rankStream(
|
||||||
|
prevIndex: number,
|
||||||
|
prevSource: MediaSourceInfo,
|
||||||
|
mediaStreams: MediaStream[],
|
||||||
|
trackOptions: any,
|
||||||
|
): void;
|
||||||
|
|
||||||
|
protected rank(
|
||||||
|
prevIndex: number,
|
||||||
|
prevSource: MediaSourceInfo,
|
||||||
|
mediaStreams: MediaStream[],
|
||||||
|
trackOptions: any,
|
||||||
|
): void {
|
||||||
|
if (prevIndex === -1) {
|
||||||
|
console.debug("AutoSet Subtitle - No Stream Set");
|
||||||
|
trackOptions[`Default${this.streamType}StreamIndex`] = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prevSource.MediaStreams || !mediaStreams) {
|
||||||
|
console.debug(`AutoSet ${this.streamType} - No MediaStreams`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let bestStreamIndex = null;
|
||||||
|
let bestStreamScore = 0;
|
||||||
|
|
||||||
|
const prevStream = prevSource.MediaStreams[prevIndex];
|
||||||
|
|
||||||
|
if (!prevStream) {
|
||||||
|
console.debug(`AutoSet ${this.streamType} - No prevStream`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug(
|
||||||
|
`AutoSet ${this.streamType} - Previous was ${prevStream.Index} - ${prevStream.DisplayTitle}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
let prevRelIndex = 0;
|
||||||
|
for (const stream of prevSource.MediaStreams) {
|
||||||
|
if (stream.Type !== this.streamType) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stream.Index === prevIndex) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
prevRelIndex += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newRelIndex = 0;
|
||||||
|
for (const stream of mediaStreams) {
|
||||||
|
if (stream.Type !== this.streamType) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
if (prevStream.Codec === stream.Codec) {
|
||||||
|
score += 1;
|
||||||
|
}
|
||||||
|
if (prevRelIndex === newRelIndex) {
|
||||||
|
score += 1;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
prevStream.DisplayTitle &&
|
||||||
|
prevStream.DisplayTitle === stream.DisplayTitle
|
||||||
|
) {
|
||||||
|
score += 2;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
prevStream.Language &&
|
||||||
|
prevStream.Language !== "und" &&
|
||||||
|
prevStream.Language === stream.Language
|
||||||
|
) {
|
||||||
|
score += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug(
|
||||||
|
`AutoSet ${this.streamType} - Score ${score} for ${stream.Index} - ${stream.DisplayTitle}`,
|
||||||
|
);
|
||||||
|
if (score > bestStreamScore && score >= 3) {
|
||||||
|
bestStreamScore = score;
|
||||||
|
bestStreamIndex = stream.Index;
|
||||||
|
}
|
||||||
|
|
||||||
|
newRelIndex += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestStreamIndex != null) {
|
||||||
|
console.debug(
|
||||||
|
`AutoSet ${this.streamType} - Using ${bestStreamIndex} score ${bestStreamScore}.`,
|
||||||
|
);
|
||||||
|
trackOptions[`Default${this.streamType}StreamIndex`] = bestStreamIndex;
|
||||||
|
} else {
|
||||||
|
console.debug(
|
||||||
|
`AutoSet ${this.streamType} - Threshold not met. Using default.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubtitleStreamRanker extends StreamRankerStrategy {
|
||||||
|
streamType = "Subtitle";
|
||||||
|
|
||||||
|
rankStream(
|
||||||
|
prevIndex: number,
|
||||||
|
prevSource: MediaSourceInfo,
|
||||||
|
mediaStreams: MediaStream[],
|
||||||
|
trackOptions: any,
|
||||||
|
): void {
|
||||||
|
super.rank(prevIndex, prevSource, mediaStreams, trackOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AudioStreamRanker extends StreamRankerStrategy {
|
||||||
|
streamType = "Audio";
|
||||||
|
|
||||||
|
rankStream(
|
||||||
|
prevIndex: number,
|
||||||
|
prevSource: MediaSourceInfo,
|
||||||
|
mediaStreams: MediaStream[],
|
||||||
|
trackOptions: any,
|
||||||
|
): void {
|
||||||
|
super.rank(prevIndex, prevSource, mediaStreams, trackOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StreamRanker {
|
||||||
|
private strategy: StreamRankerStrategy;
|
||||||
|
|
||||||
|
constructor(strategy: StreamRankerStrategy) {
|
||||||
|
this.strategy = strategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStrategy(strategy: StreamRankerStrategy) {
|
||||||
|
this.strategy = strategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
rankStream(
|
||||||
|
prevIndex: number,
|
||||||
|
prevSource: MediaSourceInfo,
|
||||||
|
mediaStreams: MediaStream[],
|
||||||
|
trackOptions: any,
|
||||||
|
) {
|
||||||
|
this.strategy.rankStream(prevIndex, prevSource, mediaStreams, trackOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { StreamRanker, SubtitleStreamRanker, AudioStreamRanker };
|
||||||
Reference in New Issue
Block a user