From 622db5644b9cf157ab75f1352630f5cb521927fe Mon Sep 17 00:00:00 2001 From: Anette Gjetnes Date: Fri, 7 Nov 2025 13:56:04 +0100 Subject: [PATCH 1/2] Apply existing patch implementing custom theme TODO Skipped all changes in the lib files since they are on the actual package. Might need some changes here to account for these. --- .../kotlinaudio/event/EventHolder.kt | 3 ++ .../kotlinaudio/event/PlayerEventHolder.kt | 9 ++++ .../kotlinaudio/models/CustomSchemeRequest.kt | 12 +++++ .../kotlinaudio/players/BaseAudioPlayer.kt | 32 ++++++++++- .../players/components/MediaFactory.kt | 51 ++++++++++++++++-- .../trackplayer/module/MusicEvents.kt | 1 + .../trackplayer/module/MusicModule.kt | 14 +++++ .../trackplayer/service/MusicService.kt | 53 +++++++++++++++++-- ios/TrackPlayer.mm | 4 ++ ios/TrackPlayer.swift | 40 +++++++++++++- ios/Utils/EventType.swift | 1 + src/NativeTrackPlayer.ts | 1 + src/constants/Event.ts | 2 + src/interfaces/UpdateOptions.ts | 1 + .../CustomSchemeRequestReceivedEvent.ts | 6 +++ src/interfaces/events/EventPayloadByEvent.ts | 2 + src/interfaces/events/index.ts | 1 + src/trackPlayer.ts | 8 +++ 18 files changed, 232 insertions(+), 9 deletions(-) create mode 100644 android/src/main/java/com/doublesymmetry/kotlinaudio/models/CustomSchemeRequest.kt create mode 100644 src/interfaces/events/CustomSchemeRequestReceivedEvent.ts diff --git a/android/src/main/java/com/doublesymmetry/kotlinaudio/event/EventHolder.kt b/android/src/main/java/com/doublesymmetry/kotlinaudio/event/EventHolder.kt index 637a03f2d..fe173cffe 100644 --- a/android/src/main/java/com/doublesymmetry/kotlinaudio/event/EventHolder.kt +++ b/android/src/main/java/com/doublesymmetry/kotlinaudio/event/EventHolder.kt @@ -1,6 +1,9 @@ package com.doublesymmetry.kotlinaudio.event class EventHolder internal constructor(private val playerEventHolder: PlayerEventHolder) { + val customSchemeRequest + get() = playerEventHolder.customSchemeRequest + val audioItemTransition get() = playerEventHolder.audioItemTransition diff --git a/android/src/main/java/com/doublesymmetry/kotlinaudio/event/PlayerEventHolder.kt b/android/src/main/java/com/doublesymmetry/kotlinaudio/event/PlayerEventHolder.kt index 204f5b0fb..7edc3adb3 100644 --- a/android/src/main/java/com/doublesymmetry/kotlinaudio/event/PlayerEventHolder.kt +++ b/android/src/main/java/com/doublesymmetry/kotlinaudio/event/PlayerEventHolder.kt @@ -13,6 +13,9 @@ import kotlinx.coroutines.launch class PlayerEventHolder { private val coroutineScope = MainScope() + private var _customSchemeRequest = MutableSharedFlow(1) + var customSchemeRequest = _customSchemeRequest.asSharedFlow() + private var _stateChange = MutableSharedFlow(1) var stateChange = _stateChange.asSharedFlow() @@ -61,6 +64,12 @@ class PlayerEventHolder { */ var onPlayerActionTriggeredExternally = _onPlayerActionTriggeredExternally.asSharedFlow() + internal fun updateCustomSchemeRequest(id: String, uri: String) { + coroutineScope.launch { + _customSchemeRequest.emit(CustomSchemeRequest(id, uri)) + } + } + internal fun updateAudioPlayerState(state: AudioPlayerState) { coroutineScope.launch { _stateChange.emit(state) diff --git a/android/src/main/java/com/doublesymmetry/kotlinaudio/models/CustomSchemeRequest.kt b/android/src/main/java/com/doublesymmetry/kotlinaudio/models/CustomSchemeRequest.kt new file mode 100644 index 000000000..2d08b60a9 --- /dev/null +++ b/android/src/main/java/com/doublesymmetry/kotlinaudio/models/CustomSchemeRequest.kt @@ -0,0 +1,12 @@ +package com.doublesymmetry.kotlinaudio.models + +data class CustomSchemeRequest ( + val id: String, + val uri: String +) + +data class CustomSchemeResponse ( + val id: String, + val newUri: String?, + val headerprops: MutableMap? +) diff --git a/android/src/main/java/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer.kt b/android/src/main/java/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer.kt index a0f995c62..7ac96f482 100644 --- a/android/src/main/java/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer.kt +++ b/android/src/main/java/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer.kt @@ -21,11 +21,13 @@ import androidx.media3.common.util.UnstableApi import androidx.media3.datasource.cache.SimpleCache import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.ExoPlaybackException import androidx.media3.session.legacy.RatingCompat import com.doublesymmetry.kotlinaudio.event.PlayerEventHolder import com.doublesymmetry.kotlinaudio.models.AudioItem import com.doublesymmetry.kotlinaudio.models.AudioItemTransitionReason import com.doublesymmetry.kotlinaudio.models.AudioPlayerState +import com.doublesymmetry.kotlinaudio.models.CustomSchemeResponse import com.doublesymmetry.kotlinaudio.models.MediaSessionCallback import com.doublesymmetry.kotlinaudio.models.PlayWhenReadyChangeData import com.doublesymmetry.kotlinaudio.models.PlaybackError @@ -46,6 +48,7 @@ abstract class BaseAudioPlayer internal constructor( ) { val exoPlayer: ExoPlayer + val mediaFactory: MediaFactory val forwardingPlayer: InnerForwardingPlayer val player: Player get() { @@ -69,6 +72,17 @@ abstract class BaseAudioPlayer internal constructor( open val currentItem: AudioItem? get() = exoPlayer.currentMediaItem?.let { AudioItem.fromMediaItem(it) } + internal val customSchemeResponses = mutableListOf () + + // If this is defined AND matches the beginning of an AudioItem's audioUrl, each new connection + // request from exoplayer will emit a CustomSchemeRequest event and then block the connection + // until respondToCustomSchemeRequest() has been called with a matching id (or we time out). + // This mechanism enables users to rewrite the url and provide headers on a per-connection + // basis, for instance for auth purposes. + var customUrlPrefix: String? + get() = mediaFactory.customUrlPrefix + set(v) { mediaFactory.customUrlPrefix = v } + var playbackError: PlaybackError? = null var playerState: AudioPlayerState = AudioPlayerState.IDLE private set(value) { @@ -163,11 +177,13 @@ abstract class BaseAudioPlayer internal constructor( val renderer = DefaultRenderersFactory(context) renderer.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER) + mediaFactory = MediaFactory(context, cache, playerEventHolder, customSchemeResponses) + exoPlayer = ExoPlayer .Builder(context) .setRenderersFactory(renderer) .setHandleAudioBecomingNoisy(options.handleAudioBecomingNoisy) - .setMediaSourceFactory(MediaFactory(context, cache)) + .setMediaSourceFactory(mediaFactory) .setWakeMode(setWakeMode(options.wakeMode)) .apply { setLoadControl(setupBuffer(options.bufferOptions)) @@ -282,6 +298,10 @@ abstract class BaseAudioPlayer internal constructor( exoPlayer.seekTo(positionMs) } + fun respondToCustomSchemeRequest(id: String, newUri: String?, headerprops: MutableMap?) { + customSchemeResponses.add(CustomSchemeResponse(id, newUri, headerprops)) + } + @UnstableApi inner class InnerPlayerListener : Listener { @@ -430,12 +450,20 @@ abstract class BaseAudioPlayer internal constructor( } override fun onPlayerError(error: PlaybackException) { + val detailMessage: String? = if(error is ExoPlaybackException) { + when (error.type){ + ExoPlaybackException.TYPE_SOURCE -> error.sourceException.message + ExoPlaybackException.TYPE_RENDERER -> error.rendererException.message + ExoPlaybackException.TYPE_UNEXPECTED -> error.unexpectedException.message + else -> null + } + } else { null } val _playbackError = PlaybackError( error.errorCodeName .replace("ERROR_CODE_", "") .lowercase(Locale.getDefault()) .replace("_", "-"), - error.message + error.message + " - " + (detailMessage?:"") ) playerEventHolder.updatePlaybackError(_playbackError) playbackError = _playbackError diff --git a/android/src/main/java/com/doublesymmetry/kotlinaudio/players/components/MediaFactory.kt b/android/src/main/java/com/doublesymmetry/kotlinaudio/players/components/MediaFactory.kt index f8b9ca1ba..5f8602e44 100644 --- a/android/src/main/java/com/doublesymmetry/kotlinaudio/players/components/MediaFactory.kt +++ b/android/src/main/java/com/doublesymmetry/kotlinaudio/players/components/MediaFactory.kt @@ -12,6 +12,7 @@ import androidx.media3.datasource.DataSpec import androidx.media3.datasource.DefaultDataSource import androidx.media3.datasource.DefaultHttpDataSource import androidx.media3.datasource.RawResourceDataSource +import androidx.media3.datasource.ResolvingDataSource import androidx.media3.datasource.cache.CacheDataSource import androidx.media3.datasource.cache.SimpleCache import androidx.media3.exoplayer.dash.DashMediaSource @@ -25,19 +26,27 @@ import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.ProgressiveMediaSource import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy import androidx.media3.extractor.DefaultExtractorsFactory +import com.doublesymmetry.kotlinaudio.event.PlayerEventHolder import com.doublesymmetry.kotlinaudio.utils.isUriLocalFile +import com.doublesymmetry.kotlinaudio.models.CustomSchemeResponse +import okio.IOException +import java.util.UUID @OptIn(UnstableApi::class) class MediaFactory ( private val context: Context, - private val cache: SimpleCache? + private val cache: SimpleCache?, + private val playerEventHolder: PlayerEventHolder, + private val customSchemeResponses: MutableList ) : MediaSource.Factory { companion object { private const val DEFAULT_USER_AGENT = "react-native-track-player" } + var customUrlPrefix: String? = null + private val mediaFactory = DefaultMediaSourceFactory(context) override fun setDrmSessionManagerProvider(drmSessionManagerProvider: DrmSessionManagerProvider): MediaSource.Factory { @@ -64,13 +73,14 @@ class MediaFactory ( // HACK: why are these capitalized? val resourceType = mediaItem.mediaMetadata.extras?.getString("type")?.lowercase() val uri = Uri.parse(mediaItem.mediaMetadata.extras?.getString("uri")!!) + val customPrefix = customUrlPrefix val factory: DataSource.Factory = when { resourceId != 0 && resourceId != null -> { val raw = RawResourceDataSource(context) raw.open(DataSpec(uri)) DataSource.Factory { raw } } - isUriLocalFile(uri) -> { + ((customPrefix == null) || !uri.toString().startsWith(customPrefix)) && isUriLocalFile(uri) -> { DefaultDataSource.Factory(context) } else -> { @@ -83,7 +93,42 @@ class MediaFactory ( } } - enableCaching(tempFactory) + val httpFactory = enableCaching(tempFactory) + + // If the uri matches our current custom scheme prefix we'll use ResolvingDataSource so that we can override + // each new connection to the file. Otherwise we'll just use DefaultHttpDataSource directly. + val returnFactory = if ((customPrefix == null) || !uri.toString().startsWith(customPrefix)) httpFactory else { + val resolver = ResolvingDataSource.Resolver { dataSpec -> + val reqId = UUID.randomUUID().toString() + // Emit event about the new request + playerEventHolder.updateCustomSchemeRequest(reqId, dataSpec.uri.toString()) + + var loopCount = 0 + var response: CustomSchemeResponse? = null + + // We'll time out and throw an exception after (waitMs * waitIterations) milliseconds. + val waitMs: Long = 100 + val waitIterations = 600 + + while(response == null && loopCount < waitIterations){ + Thread.sleep(waitMs) + try { + response = customSchemeResponses.first{ it.id == reqId } + customSchemeResponses.remove(response) + } catch (e: NoSuchElementException) { + loopCount++ + } + } + if(response == null) throw IOException("Custom scheme request timed out while waiting for response") + + val headers = if (response.headerprops != null) response.headerprops!! else mapOf() + val url = if (response.newUri != null) response.newUri!! else dataSpec.uri.toString() + dataSpec.withAdditionalHeaders((headers)).withUri(Uri.parse(url)) + } + ResolvingDataSource.Factory(httpFactory, resolver) + } + + returnFactory } } diff --git a/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicEvents.kt b/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicEvents.kt index 2f9f28fde..4b65dd7e9 100644 --- a/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicEvents.kt +++ b/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicEvents.kt @@ -54,6 +54,7 @@ class MusicEvents(private val reactContext: ReactContext) : BroadcastReceiver() // Other const val PLAYER_ERROR = "player-error" + const val CUSTOM_SCHEME_REQUEST_RECEVIED = "custom-scheme-request-received" const val CONNECTOR_CONNECTED = "android-controller-connected" const val CONNECTOR_DISCONNECTED = "android-controller-disconnected" diff --git a/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicModule.kt b/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicModule.kt index 13a6c609f..637a6957d 100644 --- a/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicModule.kt +++ b/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicModule.kt @@ -433,6 +433,20 @@ class MusicModule(reactContext: ReactApplicationContext) : NativeTrackPlayerSpec callback.resolve(null) } + override fun respondToCustomSchemeRequest(data: ReadableMap?, callback: Promise) = launchInScope { + if (verifyServiceBoundOrReject(callback)) return@launchInScope + + val bundle = Arguments.toBundle(data) + val id = bundle?.getString("id") + if(id is String){ + musicService.respondToCustomSchemeRequest(bundle) + callback.resolve(null) + } else { + callback.reject("missing_argument", "Id is required when responding to custom scheme requests") + } + + } + override fun setVolume(volume: Double, callback: Promise) = launchInScope { if (verifyServiceBoundOrReject(callback)) return@launchInScope diff --git a/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt b/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt index b495b5a98..0b7f808f8 100644 --- a/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt +++ b/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt @@ -64,6 +64,7 @@ class MusicService : HeadlessJsMediaService() { private lateinit var fakePlayer: ExoPlayer private lateinit var mediaSession: MediaLibrarySession private var progressUpdateJob: Job? = null + private var previousProgressBundle: Bundle? = null private var sessionCommands: SessionCommands? = null private var playerCommands: Player.Commands? = null private var customLayout: List = listOf() @@ -206,7 +207,10 @@ class MusicService : HeadlessJsMediaService() { handleAudioFocus = playerOptions?.getBoolean(AUTO_HANDLE_INTERRUPTIONS) ?: true, interceptPlayerActionsTriggeredExternally = true, skipSilence = playerOptions?.getBoolean(SKIP_SILENCE) ?: false, - wakeMode = playerOptions?.getInt(WAKE_MODE, 0) ?: 0 + // Hardcode wake mode "Network", just like we did in the KotlinAudio dependency for RNTP 4.x + // TODO: Switch to doing this from the model code once the option is exposed all the way up to TypeScript. + wakeMode = 2 + // wakeMode = playerOptions?.getInt(WAKE_MODE, 0) ?: 0 ) player = QueuedAudioPlayer(this@MusicService, options) fakePlayer.release() @@ -240,6 +244,11 @@ class MusicService : HeadlessJsMediaService() { androidOptions?.getBoolean(PAUSE_ON_INTERRUPTION_KEY) ?: false player.shuffleMode = androidOptions?.getBoolean(SHUFFLE_KEY) ?: false + options.getString("customUrlPrefix")?.let { + // Explicitly setting an empty prefix string disables it + player.customUrlPrefix = if (it.isEmpty()) null else it + } + // setup progress update events if configured progressUpdateJob?.cancel() val updateInterval = @@ -322,8 +331,17 @@ class MusicService : HeadlessJsMediaService() { @MainThread private fun progressUpdateEventFlow(interval: Double) = flow { while (true) { - if (player.isPlaying) { - val bundle = progressUpdateEvent() + val bundle = progressUpdateEvent() + val prevBundle = previousProgressBundle + // In some cases we want progress updates even when audio is not playing. + // Ref: https://gitlab.fusetools.com/suide/suide/-/issues/9201 + val bundleHasChanged = (prevBundle !== null) && ( + // We only care about position and duration. + !(prevBundle.get(POSITION_KEY)?.equals(bundle.get(POSITION_KEY)) ?: false ) || + !(prevBundle.get(DURATION_KEY)?.equals(bundle.get(DURATION_KEY)) ?: false )) + + previousProgressBundle = bundle + if (player.isPlaying || bundleHasChanged) { emit(bundle) } @@ -500,6 +518,24 @@ class MusicService : HeadlessJsMediaService() { updateMetadataForTrack(player.currentIndex, bundle) } + @MainThread + fun respondToCustomSchemeRequest(bundle: Bundle) { + val id = bundle.getString("id") + if(id == null) return + val newUri = bundle.getString("newUri") + val headers = bundle.getBundle("headerprops") + var headerprops: MutableMap? = null + + if(headers != null) { + headerprops = HashMap() + for (h in headers.keySet()) { + headerprops!![h] = headers.getString(h)!! + } + } + + player.respondToCustomSchemeRequest(id, newUri, headerprops) + } + private fun emitPlaybackTrackChangedEvents( previousIndex: Int?, oldPosition: Double @@ -657,6 +693,17 @@ class MusicService : HeadlessJsMediaService() { emit(MusicEvents.PLAYBACK_ERROR, getPlaybackErrorBundle()) } } + + scope.launch { + event.customSchemeRequest.collect { + val bundle = Bundle().apply { + putString("id", it.id) + putString("uri", it.uri) + } + + emit(MusicEvents.CUSTOM_SCHEME_REQUEST_RECEVIED, bundle) + } + } } private fun getPlaybackErrorBundle(): Bundle { diff --git a/ios/TrackPlayer.mm b/ios/TrackPlayer.mm index e690de8da..0067292e2 100644 --- a/ios/TrackPlayer.mm +++ b/ios/TrackPlayer.mm @@ -181,6 +181,10 @@ - (void)updateOptions:(nonnull NSDictionary *)options resolve:(nonnull RCTPromis [nativeTrackPlayer updateOptions:options resolver:resolve rejecter:reject]; } +- (void)respondToCustomSchemeRequest:(nonnull NSDictionary *)response resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { + [nativeTrackPlayer respondToCustomSchemeRequest:response resolver:resolve rejecter:reject]; +} + // event listeners - (void)sendEvent:(NSString *)name body:(id)body { [super sendEventWithName:name body:body]; diff --git a/ios/TrackPlayer.swift b/ios/TrackPlayer.swift index 92f2c54d5..6ba99c64e 100644 --- a/ios/TrackPlayer.swift +++ b/ios/TrackPlayer.swift @@ -42,6 +42,7 @@ public class NativeTrackPlayerImpl: NSObject, AudioSessionControllerDelegate { player.event.currentItem.addListener(self, handleAudioPlayerCurrentItemChange) player.event.secondElapse.addListener(self, handleAudioPlayerSecondElapse) player.event.playWhenReadyChange.addListener(self, handlePlayWhenReadyChange) + player.event.customSchemeRequest.addListener(self, handleCustomSchemeRequest) } deinit { @@ -287,6 +288,11 @@ public class NativeTrackPlayerImpl: NSObject, AudioSessionControllerDelegate { interval: ((options["progressUpdateEventInterval"] as? NSNumber) ?? 0).doubleValue ) + if let customUrlPrefix = options["customUrlPrefix"] as? String { + // Explicitly setting an empty prefix string disables it + player.customUrlPrefix = customUrlPrefix.isEmpty ? nil : customUrlPrefix + } + resolve(NSNull()) } @@ -697,6 +703,10 @@ public class NativeTrackPlayerImpl: NSObject, AudioSessionControllerDelegate { "message": "The track could not be played", "code": "ios_track_unplayable" ] + case .customSchemeRequestFailed(let message) : return [ + "message": message, + "code": "ios_custom_scheme_request_failed" + ] default: return [ "message": "A playback error occurred", "code": "ios_playback_error" @@ -740,7 +750,12 @@ public class NativeTrackPlayerImpl: NSObject, AudioSessionControllerDelegate { } func handleAudioPlayerFailed(error: Error?) { - emit(event: EventType.PlaybackError, body: ["error": error?.localizedDescription]) + // Remi/Fuse: Previously the error events only contained the generic localizedDescription + // (with the key "error") and not the "code" and "message" keys expected by the higher-level + // typescript code. This function has been patched to include those as well. + var errorBody = getPlaybackStateErrorKeyValues() + errorBody["error"] = error?.localizedDescription + emit(event: EventType.PlaybackError, body: errorBody) } func handleAudioPlayerCurrentItemChange( @@ -816,6 +831,29 @@ public class NativeTrackPlayerImpl: NSObject, AudioSessionControllerDelegate { ] ) } + + func handleCustomSchemeRequest(request: CustomSchemeRequest) { + emit(event: EventType.CustomSchemeRequestReceived, body: ["id": request.id, "uri": request.uri]) + } + + + @objc(respondToCustomSchemeRequest:resolver:rejecter:) + public func respondToCustomSchemeRequest(response: [String: Any], resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + if (rejectWhenNotInitialized(reject: reject)) { return } + + let id = response["id"] as? String + if(id == nil){ + reject("invalid_custom_scheme_response", "missing id",nil) + } + + let headerprops = response["headerprops"] as? [String: String] + let newUri = response["newUri"] as? String + + player.respondToCustomSchemeRequest(response: (id: id!, newUri: newUri, headerprops: headerprops)) + + resolve(NSNull()) + } + } diff --git a/ios/Utils/EventType.swift b/ios/Utils/EventType.swift index eb693294c..5e2e42e96 100644 --- a/ios/Utils/EventType.swift +++ b/ios/Utils/EventType.swift @@ -24,6 +24,7 @@ enum EventType: String, CaseIterable { case MetadataChapterReceived = "metadata-chapter-received" case MetadataTimedReceived = "metadata-timed-received" case MetadataCommonReceived = "metadata-common-received" + case CustomSchemeRequestReceived = "custom-scheme-request-received" static func allRawValues() -> [String] { return allCases.map { $0.rawValue } diff --git a/src/NativeTrackPlayer.ts b/src/NativeTrackPlayer.ts index a5dcd7575..8c26d0377 100644 --- a/src/NativeTrackPlayer.ts +++ b/src/NativeTrackPlayer.ts @@ -23,6 +23,7 @@ export interface Spec extends TurboModule { getProgress(): Promise; getPlaybackState(): Promise; retry(): Promise; + respondToCustomSchemeRequest(options: UnsafeObject): Promise; // playlist management add( diff --git a/src/constants/Event.ts b/src/constants/Event.ts index c8a9ad99b..994890e3d 100644 --- a/src/constants/Event.ts +++ b/src/constants/Event.ts @@ -136,4 +136,6 @@ export enum Event { * typical controllers are media notification and Android Auto. **/ AndroidConnectorDisconnected = 'android-controller-disconnected', + + CustomSchemeRequestReceived = 'custom-scheme-request-received', } diff --git a/src/interfaces/UpdateOptions.ts b/src/interfaces/UpdateOptions.ts index 8f4dce84b..9b06b221c 100644 --- a/src/interfaces/UpdateOptions.ts +++ b/src/interfaces/UpdateOptions.ts @@ -8,6 +8,7 @@ export interface UpdateOptions { forwardJumpInterval?: number; backwardJumpInterval?: number; progressUpdateEventInterval?: number; // in seconds + customUrlPrefix?: string; // ios likeOptions?: FeedbackOptions; diff --git a/src/interfaces/events/CustomSchemeRequestReceivedEvent.ts b/src/interfaces/events/CustomSchemeRequestReceivedEvent.ts new file mode 100644 index 000000000..535de86eb --- /dev/null +++ b/src/interfaces/events/CustomSchemeRequestReceivedEvent.ts @@ -0,0 +1,6 @@ +export interface CustomSchemeRequestReceivedEvent { + /** UUID identifying this specific request. */ + id: string + /** The uri, including custom scheme, for which the request is made. */ + uri: string +} diff --git a/src/interfaces/events/EventPayloadByEvent.ts b/src/interfaces/events/EventPayloadByEvent.ts index 8606a7f14..dde2cf8d1 100644 --- a/src/interfaces/events/EventPayloadByEvent.ts +++ b/src/interfaces/events/EventPayloadByEvent.ts @@ -23,6 +23,7 @@ import type { RemotePlaySearchEvent } from './RemotePlaySearchEvent'; import type { RemoteSeekEvent } from './RemoteSeekEvent'; import type { RemoteSetRatingEvent } from './RemoteSetRatingEvent'; import type { RemoteSkipEvent } from './RemoteSkipEvent'; +import type { CustomSchemeRequestReceivedEvent } from './CustomSchemeRequestReceivedEvent'; export type EventPayloadByEvent = { [Event.PlayerError]: PlayerErrorEvent; @@ -55,6 +56,7 @@ export type EventPayloadByEvent = { [Event.MetadataCommonReceived]: AudioCommonMetadataReceivedEvent; [Event.AndroidConnectorConnected]: AndroidControllerConnectedEvent; [Event.AndroidConnectorDisconnected]: AndroidControllerDisconnectedEvent; + [Event.CustomSchemeRequestReceived]: CustomSchemeRequestReceivedEvent; }; type Simplify = { [KeyType in keyof T]: T[KeyType] } & {}; diff --git a/src/interfaces/events/index.ts b/src/interfaces/events/index.ts index 216ef6c7d..8b1c76fb6 100644 --- a/src/interfaces/events/index.ts +++ b/src/interfaces/events/index.ts @@ -13,3 +13,4 @@ export * from './RemotePlaySearchEvent'; export * from './RemoteSeekEvent'; export * from './RemoteSetRatingEvent'; export * from './RemoteSkipEvent'; +export * from './CustomSchemeRequestReceivedEvent'; \ No newline at end of file diff --git a/src/trackPlayer.ts b/src/trackPlayer.ts index cc427703d..8532f57f1 100644 --- a/src/trackPlayer.ts +++ b/src/trackPlayer.ts @@ -454,3 +454,11 @@ export async function validateOnStartCommandIntent(): Promise { if (!isAndroid) return true; return TrackPlayer.validateOnStartCommandIntent(); } + +export async function respondToCustomSchemeRequest(response: { + id: string; + newUri?: string; + headerprops?: { [id: string]: string }; +}) { + return TrackPlayer.respondToCustomSchemeRequest(response); +} From 6c2b8b3696489b25ea397e3a384adb5b2425d905 Mon Sep 17 00:00:00 2001 From: Anette Gjetnes Date: Fri, 7 Nov 2025 15:07:18 +0100 Subject: [PATCH 2/2] tooling: Add mise.toml config file Specify yarn and node versions --- mise.toml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 mise.toml diff --git a/mise.toml b/mise.toml new file mode 100644 index 000000000..9b00c3017 --- /dev/null +++ b/mise.toml @@ -0,0 +1,3 @@ +[tools] +node = "25.0.0" +yarn = "latest"