feat(tracks): add track indexes

This commit is contained in:
2026-08-03 19:10:45 +02:00
parent 685d3beb2a
commit e80c382a25
8 changed files with 57 additions and 45 deletions
@@ -117,21 +117,13 @@ class EventMap(private val tracks: TrackProvider) : HybridOmniEventMapSpec(), Pl
}
private fun selectedTrack(trackType: Int): Track? {
val groups = player.currentTracks.groups.filter { it.type == trackType }
for (group in groups) {
val mediaGroup = group.mediaTrackGroup
for (i in 0 until group.length) {
if (!group.isTrackSelected(i)) continue
val format = group.getTrackFormat(i)
return Track(
id = format.id ?: mediaGroup.id,
label = format.label,
language = format.language,
selected = true
)
}
val list = when (trackType) {
TRACK_TYPE_VIDEO -> tracks.videos
TRACK_TYPE_AUDIO -> tracks.audios
TRACK_TYPE_TEXT -> tracks.subtitles
else -> return null
}
return null
return list.firstOrNull { it.selected }
}
private fun getCurrentRendition(): Rendition? {
@@ -265,9 +257,8 @@ class EventMap(private val tracks: TrackProvider) : HybridOmniEventMapSpec(), Pl
selectedTrack(TRACK_TYPE_AUDIO)?.let { track ->
onAudioTrackChangeListeners.forEach { it(track) }
}
selectedTrack(TRACK_TYPE_TEXT)?.let { track ->
onSubtitleChangeListeners.forEach { it(track) }
}
val subtitle = selectedTrack(TRACK_TYPE_TEXT)
onSubtitleChangeListeners.forEach { it(subtitle) }
emitTracks()
emitIsAutoQualityChange()
emitRenditionChange()
@@ -451,13 +451,19 @@ class OmniPlayer(
// when casting tracks are stripped of some metadata, recover it
val castTracks = castMediaTracksById()
// external (sideloaded) subs aren't in the container, so their index is -1
val sideloadedIds = source?.subtitles?.map { it.id }?.toHashSet().orEmpty()
var index = 0
return groups.map { group ->
val format = group.getTrackFormat(0)
val castTrackId = group.mediaTrackGroup.id.substringAfterLast("track=", "").toLongOrNull()
val id = group.mediaTrackGroup.id
val castTrackId = id.substringAfterLast("track=", "").toLongOrNull()
val cast = castTracks[castTrackId]
val sideloaded = trackType == C.TRACK_TYPE_TEXT && id in sideloadedIds
Track(
id = group.mediaTrackGroup.id,
id = id,
index = if (sideloaded) -1.0 else (index++).toDouble(),
label = cast?.name ?: format.label,
language = cast?.language ?: format.language,
selected = group.isSelected
+5 -1
View File
@@ -34,6 +34,8 @@ namespace margelo::nitro::omni {
static const auto clazz = javaClassStatic();
static const auto fieldId = clazz->getField<jni::JString>("id");
jni::local_ref<jni::JString> id = this->getFieldValue(fieldId);
static const auto fieldIndex = clazz->getField<double>("index");
double index = this->getFieldValue(fieldIndex);
static const auto fieldLabel = clazz->getField<jni::JString>("label");
jni::local_ref<jni::JString> label = this->getFieldValue(fieldLabel);
static const auto fieldLanguage = clazz->getField<jni::JString>("language");
@@ -42,6 +44,7 @@ namespace margelo::nitro::omni {
jboolean selected = this->getFieldValue(fieldSelected);
return Track(
id->toStdString(),
index,
label != nullptr ? std::make_optional(label->toStdString()) : std::nullopt,
language != nullptr ? std::make_optional(language->toStdString()) : std::nullopt,
static_cast<bool>(selected)
@@ -54,12 +57,13 @@ namespace margelo::nitro::omni {
*/
[[maybe_unused]]
static jni::local_ref<JTrack::javaobject> fromCpp(const Track& value) {
using JSignature = JTrack(jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jboolean);
using JSignature = JTrack(jni::alias_ref<jni::JString>, double, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JString>, jboolean);
static const auto clazz = javaClassStatic();
static const auto create = clazz->getStaticMethod<JSignature>("fromCpp");
return create(
clazz,
jni::make_jstring(value.id),
value.index,
value.label.has_value() ? jni::make_jstring(value.label.value()) : nullptr,
value.language.has_value() ? jni::make_jstring(value.language.value()) : nullptr,
value.selected
@@ -23,6 +23,9 @@ data class Track(
val id: String,
@DoNotStrip
@Keep
val index: Double,
@DoNotStrip
@Keep
val label: String?,
@DoNotStrip
@Keep
@@ -37,6 +40,7 @@ data class Track(
if (this === other) return true
if (other !is Track) return false
return Objects.deepEquals(this.id, other.id)
&& Objects.deepEquals(this.index, other.index)
&& Objects.deepEquals(this.label, other.label)
&& Objects.deepEquals(this.language, other.language)
&& Objects.deepEquals(this.selected, other.selected)
@@ -45,6 +49,7 @@ data class Track(
override fun hashCode(): Int {
return arrayOf<Any?>(
id,
index,
label,
language,
selected
@@ -59,8 +64,8 @@ data class Track(
@Keep
@Suppress("unused")
@JvmStatic
private fun fromCpp(id: String, label: String?, language: String?, selected: Boolean): Track {
return Track(id, label, language, selected)
private fun fromCpp(id: String, index: Double, label: String?, language: String?, selected: Boolean): Track {
return Track(id, index, label, language, selected)
}
}
}
+5 -1
View File
@@ -41,13 +41,14 @@ namespace margelo::nitro::omni {
struct Track final {
public:
std::string id SWIFT_PRIVATE;
double index SWIFT_PRIVATE;
std::optional<std::string> label SWIFT_PRIVATE;
std::optional<std::string> language SWIFT_PRIVATE;
bool selected SWIFT_PRIVATE;
public:
Track() = default;
explicit Track(std::string id, std::optional<std::string> label, std::optional<std::string> language, bool selected): id(id), label(label), language(language), selected(selected) {}
explicit Track(std::string id, double index, std::optional<std::string> label, std::optional<std::string> language, bool selected): id(id), index(index), label(label), language(language), selected(selected) {}
public:
friend bool operator==(const Track& lhs, const Track& rhs) = default;
@@ -64,6 +65,7 @@ namespace margelo::nitro {
jsi::Object obj = arg.asObject(runtime);
return margelo::nitro::omni::Track(
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "id"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "index"))),
JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "label"))),
JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "language"))),
JSIConverter<bool>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "selected")))
@@ -72,6 +74,7 @@ namespace margelo::nitro {
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::omni::Track& arg) {
jsi::Object obj(runtime);
obj.setProperty(runtime, PropNameIDCache::get(runtime, "id"), JSIConverter<std::string>::toJSI(runtime, arg.id));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "index"), JSIConverter<double>::toJSI(runtime, arg.index));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "label"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.label));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "language"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.language));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "selected"), JSIConverter<bool>::toJSI(runtime, arg.selected));
@@ -86,6 +89,7 @@ namespace margelo::nitro {
return false;
}
if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "id")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "index")))) return false;
if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "label")))) return false;
if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "language")))) return false;
if (!JSIConverter<bool>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "selected")))) return false;
+19 -20
View File
@@ -49,32 +49,30 @@ const eventMapper: EventMapperConfig = {
);
}
}),
...createEventMapper("subtitleChange", selectTextTrack, (cb, value, prev) => {
if (value === prev) return;
const tracks = value?.textTrackList;
if (tracks?.length) {
for (let i = 0; i < tracks.length; i++) {
const track = tracks[i];
if (track?.mode === "showing") {
cb({
id: `text-${i}`,
label: track.label,
language: track.language,
selected: true,
});
return;
}
}
...createEventMapper("subtitleChange", selectTextTrack, (cb, value) => {
if (!value) return;
const track = value.textTrackList.find((x) => x.mode === "showing");
if (!track) {
cb(undefined);
return;
}
cb(undefined);
const idx = value.textTrackList.indexOf(track);
cb({
id: track.id ?? `text-${idx}`,
index: idx,
label: track.label,
language: track.language,
selected: true,
});
}),
...createEventMapper("audioTrackChange", selectAudioTrack, (cb, value) => {
if (!value) return;
const track = value.audioTrackList.find((t) => t.enabled);
if (!track) return;
const idx = value.audioTrackList.indexOf(track);
cb({
id: track.id!,
id: track.id ?? `audio-${idx}`,
index: idx,
label: track.label,
language: track.language,
selected: true,
@@ -206,6 +204,7 @@ export const stateMapper = {
if (!audio) return [];
return audio.audioTrackList.map((track, i) => ({
id: track.id ?? i.toString(),
index: i,
label: track.label,
language: track.language,
selected: track.enabled,
@@ -267,7 +266,7 @@ export function usePlayerState<Key extends keyof OmniPlayerState>(
return player.subtitles as OmniPlayerState[Key];
}
default: {
const config = stateMapper[key];
const config = stateMapper[key as keyof typeof stateMapper];
const ret = useStoreSelector(config?.selector as Selector<any, any>);
if (!config) throw new Error(`No mapper for ${key}`);
return config.mapper(ret) as OmniPlayerState[Key];
+2 -1
View File
@@ -218,8 +218,9 @@ export class WebOmniPlayer implements OmniPlayer {
const textTracks = selectTextTrack(this._store.state)?.textTrackList ?? [];
return textTracks
.filter((x) => x.kind === "subtitles" || x.kind === "captions")
.map((track) => ({
.map((track, i) => ({
id: track.id!,
index: i,
label: track.label,
language: track.language,
selected:
+2
View File
@@ -52,6 +52,8 @@ export type CastStatus =
export interface Track {
readonly id: string;
// -1 for external tracks
readonly index: number;
readonly label?: string;
readonly language?: string;
readonly selected: boolean;