From e80c382a25a163bb07b81414a0ff5c9b1640aee2 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 3 Aug 2026 18:50:36 +0200 Subject: [PATCH] feat(tracks): add track indexes --- .../src/main/java/dev/zoriya/omni/EventMap.kt | 25 ++++-------- .../main/java/dev/zoriya/omni/OmniPlayer.kt | 12 ++++-- nitrogen/generated/android/c++/JTrack.hpp | 6 ++- .../kotlin/com/margelo/nitro/omni/Track.kt | 9 ++++- nitrogen/generated/shared/c++/Track.hpp | 6 ++- src/events.web.tsx | 39 +++++++++---------- src/player.web.tsx | 3 +- src/types/player.ts | 2 + 8 files changed, 57 insertions(+), 45 deletions(-) diff --git a/android/src/main/java/dev/zoriya/omni/EventMap.kt b/android/src/main/java/dev/zoriya/omni/EventMap.kt index a9ec1e0..3fbac47 100644 --- a/android/src/main/java/dev/zoriya/omni/EventMap.kt +++ b/android/src/main/java/dev/zoriya/omni/EventMap.kt @@ -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() diff --git a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt index 02f82af..6d7d3fe 100644 --- a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt +++ b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt @@ -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 diff --git a/nitrogen/generated/android/c++/JTrack.hpp b/nitrogen/generated/android/c++/JTrack.hpp index 4b211d6..82c8c50 100644 --- a/nitrogen/generated/android/c++/JTrack.hpp +++ b/nitrogen/generated/android/c++/JTrack.hpp @@ -34,6 +34,8 @@ namespace margelo::nitro::omni { static const auto clazz = javaClassStatic(); static const auto fieldId = clazz->getField("id"); jni::local_ref id = this->getFieldValue(fieldId); + static const auto fieldIndex = clazz->getField("index"); + double index = this->getFieldValue(fieldIndex); static const auto fieldLabel = clazz->getField("label"); jni::local_ref label = this->getFieldValue(fieldLabel); static const auto fieldLanguage = clazz->getField("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(selected) @@ -54,12 +57,13 @@ namespace margelo::nitro::omni { */ [[maybe_unused]] static jni::local_ref fromCpp(const Track& value) { - using JSignature = JTrack(jni::alias_ref, jni::alias_ref, jni::alias_ref, jboolean); + using JSignature = JTrack(jni::alias_ref, double, jni::alias_ref, jni::alias_ref, jboolean); static const auto clazz = javaClassStatic(); static const auto create = clazz->getStaticMethod("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 diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/omni/Track.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/omni/Track.kt index cfcaaf4..47b239c 100644 --- a/nitrogen/generated/android/kotlin/com/margelo/nitro/omni/Track.kt +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/omni/Track.kt @@ -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( 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) } } } diff --git a/nitrogen/generated/shared/c++/Track.hpp b/nitrogen/generated/shared/c++/Track.hpp index 8b130cd..dc319e8 100644 --- a/nitrogen/generated/shared/c++/Track.hpp +++ b/nitrogen/generated/shared/c++/Track.hpp @@ -41,13 +41,14 @@ namespace margelo::nitro::omni { struct Track final { public: std::string id SWIFT_PRIVATE; + double index SWIFT_PRIVATE; std::optional label SWIFT_PRIVATE; std::optional language SWIFT_PRIVATE; bool selected SWIFT_PRIVATE; public: Track() = default; - explicit Track(std::string id, std::optional label, std::optional language, bool selected): id(id), label(label), language(language), selected(selected) {} + explicit Track(std::string id, double index, std::optional label, std::optional 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::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "id"))), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "index"))), JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "label"))), JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "language"))), JSIConverter::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::toJSI(runtime, arg.id)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "index"), JSIConverter::toJSI(runtime, arg.index)); obj.setProperty(runtime, PropNameIDCache::get(runtime, "label"), JSIConverter>::toJSI(runtime, arg.label)); obj.setProperty(runtime, PropNameIDCache::get(runtime, "language"), JSIConverter>::toJSI(runtime, arg.language)); obj.setProperty(runtime, PropNameIDCache::get(runtime, "selected"), JSIConverter::toJSI(runtime, arg.selected)); @@ -86,6 +89,7 @@ namespace margelo::nitro { return false; } if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "id")))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "index")))) return false; if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "label")))) return false; if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "language")))) return false; if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "selected")))) return false; diff --git a/src/events.web.tsx b/src/events.web.tsx index 397440d..2993ce0 100644 --- a/src/events.web.tsx +++ b/src/events.web.tsx @@ -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( 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); if (!config) throw new Error(`No mapper for ${key}`); return config.mapper(ret) as OmniPlayerState[Key]; diff --git a/src/player.web.tsx b/src/player.web.tsx index 3e40374..dcc318b 100644 --- a/src/player.web.tsx +++ b/src/player.web.tsx @@ -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: diff --git a/src/types/player.ts b/src/types/player.ts index 259691e..3f7293b 100644 --- a/src/types/player.ts +++ b/src/types/player.ts @@ -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;