fix(mobile): upgrade audio player to avoid background service crashes

This commit is contained in:
DIYgod
2026-09-02 16:18:34 +08:00
parent b835efa0c4
commit 465b997e89
13 changed files with 551 additions and 584 deletions
+2 -2
View File
@@ -51,6 +51,7 @@
"@react-native-masked-view/masked-view": "0.3.2",
"@react-native-menu/menu": "2.0.0",
"@react-native-picker/picker": "2.11.4",
"@rntp/player": "5.9.2",
"@shikijs/themes": "4.3.1",
"@shopify/flash-list": "2.3.2",
"@tanstack/query-sync-storage-persister": "5.101.2",
@@ -134,7 +135,6 @@
"react-native-screens": "4.25.2",
"react-native-sheet-transitions": "0.1.2",
"react-native-svg": "15.15.5",
"react-native-track-player": "4.1.2",
"react-native-uikit-colors": "0.6.2",
"react-native-volume-manager": "2.0.8",
"react-native-web": "0.21.2",
@@ -190,7 +190,7 @@
"reactNativeDirectoryCheck": {
"enabled": true,
"exclude": [
"react-native-track-player",
"@rntp/player",
"follow-native",
"react-native-color-matrix-image-filters"
],
@@ -1,11 +1,11 @@
import { EventBus } from "@follow/utils/event-bus"
import { Portal } from "@gorhom/portal"
import TrackPlayer from "@rntp/player"
import { useAtom } from "jotai"
import * as React from "react"
import { useCallback, useEffect } from "react"
import { Dimensions, StyleSheet, View } from "react-native"
import { runOnJS, runOnUI } from "react-native-reanimated"
import TrackPlayer from "react-native-track-player"
import { player } from "@/src/lib/player"
@@ -38,7 +38,7 @@ export function EntryContentWebView(props: EntryContentWebViewProps) {
const handleSeekAudio = useCallback(
async (e: AudioSeekEvent) => {
const activeTrack = await TrackPlayer.getActiveTrack()
const activeTrack = TrackPlayer.getActiveMediaItem()
const entryAudio = entryInWebview?.attachments?.find((attachment) =>
attachment.mime_type?.startsWith("audio/"),
)
@@ -46,7 +46,7 @@ export function EntryContentWebView(props: EntryContentWebViewProps) {
console.warn("Failed to seek audio! No audio attachment found")
return
}
if (activeTrack?.url !== entryAudio.url) {
if (activeTrack?.mediaId !== entryAudio.url) {
await player.play({
url: entryAudio.url || "",
title: entryInWebview?.title || "Unknown Title",
+131
View File
@@ -0,0 +1,131 @@
import type { AppStateStatus } from "react-native"
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
const mocks = vi.hoisted(() => {
const listeners = new Set<(state: AppStateStatus) => void>()
return {
listeners,
appState: {
currentState: "background" as AppStateStatus,
addEventListener: vi.fn((_event: string, listener: (state: AppStateStatus) => void) => {
listeners.add(listener)
return { remove: () => listeners.delete(listener) }
}),
},
platform: { OS: "android" },
setupPlayer: vi.fn(),
setCommands: vi.fn(),
}
})
vi.mock("react-native", () => ({
AppState: mocks.appState,
Platform: mocks.platform,
}))
vi.mock("@rntp/player", () => ({
default: {
setupPlayer: mocks.setupPlayer,
setCommands: mocks.setCommands,
},
PlayerCommand: {
PlayPause: "playPause",
Next: "next",
Previous: "previous",
Stop: "stop",
Seek: "seek",
},
}))
function changeAppState(state: AppStateStatus) {
mocks.appState.currentState = state
for (const listener of mocks.listeners) {
listener(state)
}
}
describe("initializePlayer", () => {
beforeEach(() => {
vi.resetModules()
mocks.listeners.clear()
mocks.appState.currentState = "background"
mocks.platform.OS = "android"
mocks.setupPlayer.mockReset()
mocks.setCommands.mockReset()
mocks.appState.addEventListener.mockClear()
})
afterEach(() => {
vi.restoreAllMocks()
})
it("waits for the Android foreground before enabling native remote controls", async () => {
const player = await import("./player")
const initialization = player.initializePlayer()
expect(mocks.setupPlayer).not.toHaveBeenCalled()
expect(player.PlayerRegistered).toBe(false)
changeAppState("inactive")
await Promise.resolve()
expect(mocks.setupPlayer).not.toHaveBeenCalled()
changeAppState("active")
await initialization
expect(mocks.setupPlayer).toHaveBeenCalledOnce()
expect(mocks.setCommands).toHaveBeenCalledWith({
capabilities: ["playPause", "next", "previous", "stop", "seek"],
handling: "native",
})
expect(player.PlayerRegistered).toBe(true)
expect(mocks.listeners.size).toBe(0)
})
it("initializes once when callers are waiting for the same foreground transition", async () => {
const player = await import("./player")
const first = player.initializePlayer()
const second = player.initializePlayer()
changeAppState("active")
await Promise.all([first, second])
changeAppState("background")
await player.initializePlayer()
expect(mocks.setupPlayer).toHaveBeenCalledOnce()
expect(mocks.setCommands).toHaveBeenCalledOnce()
expect(mocks.listeners.size).toBe(0)
})
it("does not require a foreground transition on iOS", async () => {
mocks.platform.OS = "ios"
const player = await import("./player")
await player.initializePlayer()
expect(mocks.setupPlayer).toHaveBeenCalledOnce()
expect(mocks.appState.addEventListener).not.toHaveBeenCalled()
expect(player.PlayerRegistered).toBe(true)
})
it("keeps a failed setup unregistered so a later call can retry", async () => {
mocks.appState.currentState = "active"
const error = new Error("Native setup failed")
mocks.setupPlayer.mockImplementationOnce(() => {
throw error
})
const logError = vi.spyOn(console, "error").mockImplementation(() => {})
const player = await import("./player")
await player.initializePlayer()
expect(player.PlayerRegistered).toBe(false)
expect(mocks.setCommands).not.toHaveBeenCalled()
expect(logError).toHaveBeenCalledWith("Failed to setup player:", error)
await player.initializePlayer()
expect(mocks.setupPlayer).toHaveBeenCalledTimes(2)
expect(player.PlayerRegistered).toBe(true)
})
})
+18 -41
View File
@@ -1,5 +1,5 @@
import TrackPlayer, { PlayerCommand } from "@rntp/player"
import { AppState, Platform } from "react-native"
import TrackPlayer, { Capability, Event } from "react-native-track-player"
export let PlayerRegistered = false
@@ -16,14 +16,7 @@ function waitForForeground(): Promise<void> {
}
export async function initializePlayer() {
TrackPlayer.registerPlaybackService(() => async () => {
TrackPlayer.addEventListener(Event.RemotePlay, () => TrackPlayer.play())
TrackPlayer.addEventListener(Event.RemotePause, () => TrackPlayer.pause())
TrackPlayer.addEventListener(Event.RemoteStop, () => TrackPlayer.stop())
TrackPlayer.addEventListener(Event.RemoteNext, () => TrackPlayer.skipToNext())
TrackPlayer.addEventListener(Event.RemotePrevious, () => TrackPlayer.skipToPrevious())
TrackPlayer.addEventListener(Event.RemoteSeek, ({ position }) => TrackPlayer.seekTo(position))
})
if (PlayerRegistered) return
// On Android, setupPlayer must be called in the foreground to avoid
// ForegroundServiceStartNotAllowedException
@@ -31,38 +24,22 @@ export async function initializePlayer() {
await waitForForeground()
}
const setup = async (retry = 10) => {
if (retry <= 0) {
console.error("Failed to setup player after multiple attempts")
return
}
try {
await TrackPlayer.setupPlayer()
PlayerRegistered = true
} catch (_err) {
const err = _err as Error & { code?: string }
console.error("Failed to setup player:", "Code:", err.code, err.message)
if (PlayerRegistered) return
if (err.code === "android_cannot_setup_player_in_background") {
await waitForForeground()
await setup(retry - 1)
}
}
try {
TrackPlayer.setupPlayer()
TrackPlayer.setCommands({
capabilities: [
PlayerCommand.PlayPause,
PlayerCommand.Next,
PlayerCommand.Previous,
PlayerCommand.Stop,
PlayerCommand.Seek,
],
handling: "native",
})
PlayerRegistered = true
} catch (error) {
console.error("Failed to setup player:", error)
}
await setup()
if (!PlayerRegistered) return
await TrackPlayer.updateOptions({
capabilities: [
Capability.Play,
Capability.Pause,
Capability.SkipToNext,
Capability.SkipToPrevious,
Capability.Stop,
Capability.SeekTo,
],
compactCapabilities: [Capability.Play, Capability.Pause],
})
}
+210
View File
@@ -0,0 +1,210 @@
import type { MediaItem } from "@rntp/player"
import { beforeEach, describe, expect, it, vi } from "vitest"
const mocks = vi.hoisted(() => {
const state = {
registered: true,
activeItem: null as MediaItem | null,
nativePlaying: false,
playbackState: "idle",
}
const sequence: string[] = []
return {
state,
sequence,
native: {
getActiveMediaItem: vi.fn(() => state.activeItem),
getPlaybackState: vi.fn(() => state.playbackState),
isPlaying: vi.fn(() => state.nativePlaying),
setMediaItem: vi.fn((item: MediaItem) => {
state.activeItem = item
sequence.push("load")
}),
play: vi.fn(() => {
sequence.push("play")
}),
pause: vi.fn(() => {
state.nativePlaying = false
}),
clear: vi.fn(() => {
state.activeItem = null
state.nativePlaying = false
}),
setupPlayer: vi.fn(),
destroy: vi.fn(),
},
toastError: vi.fn(),
}
})
vi.mock("@rntp/player", () => ({
default: mocks.native,
PlaybackState: {
Idle: "idle",
Ready: "ready",
Buffering: "buffering",
Ended: "ended",
Error: "error",
},
useActiveMediaItem: vi.fn(),
useIsPlaying: vi.fn(),
usePlaybackState: vi.fn(),
useProgress: vi.fn(),
}))
vi.mock("@follow/utils", async () => {
const { createStore } = await import("jotai/vanilla")
return { jotaiStore: createStore() }
})
vi.mock("../initialize/player", () => ({
get PlayerRegistered() {
return mocks.state.registered
},
}))
vi.mock("../modules/player/tts-stream-controller", () => ({
ttsStreamController: {},
}))
vi.mock("./toast", () => ({
toast: { error: mocks.toastError },
}))
describe("player", () => {
let player: (typeof import("./player"))["player"]
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
mocks.state.registered = true
mocks.state.activeItem = null
mocks.state.nativePlaying = false
mocks.state.playbackState = "idle"
mocks.sequence.length = 0
const playerModule = await import("./player")
player = playerModule.player
})
it("loads a new media item with V5 artwork metadata before starting playback", async () => {
mocks.state.activeItem = { url: "https://example.com/previous.mp3" }
await player.play({
url: "https://example.com/podcast.mp3",
title: "Podcast Episode",
artist: "Podcast Author",
artwork: "https://example.com/cover.jpg",
})
expect(mocks.native.setMediaItem).toHaveBeenCalledWith({
mediaId: "https://example.com/podcast.mp3",
url: "https://example.com/podcast.mp3",
title: "Podcast Episode",
artist: "Podcast Author",
artworkUrl: "https://example.com/cover.jpg",
})
expect(mocks.sequence).toEqual(["load", "play"])
})
it("supplies display defaults when media metadata is missing", async () => {
await player.play({
url: "https://example.com/audio.mp3",
title: null,
artist: null,
artwork: null,
})
expect(mocks.native.setMediaItem).toHaveBeenCalledWith({
mediaId: "https://example.com/audio.mp3",
url: "https://example.com/audio.mp3",
title: "Unknown Title",
artist: "Unknown Artist",
artworkUrl: undefined,
})
})
it("resumes the active URL without replacing its queue item or resetting progress", async () => {
mocks.state.activeItem = {
mediaId: "https://example.com/audio.mp3",
url: "https://example.com/audio.mp3",
}
await player.play({ url: "https://example.com/audio.mp3" })
expect(mocks.native.setMediaItem).not.toHaveBeenCalled()
expect(mocks.sequence).toEqual(["play"])
})
it("resumes a local file when iOS returns its URL as a decoded filesystem path", async () => {
const url = "file:///var/mobile/Library/Caches/tts/entry%201.mp3"
await player.play({ url })
expect(mocks.native.setMediaItem).toHaveBeenCalledWith(
expect.objectContaining({ mediaId: url }),
)
mocks.state.activeItem = {
mediaId: url,
url: "/var/mobile/Library/Caches/tts/entry 1.mp3",
}
mocks.native.setMediaItem.mockClear()
mocks.sequence.length = 0
await player.play({ url })
expect(mocks.native.setMediaItem).not.toHaveBeenCalled()
expect(mocks.sequence).toEqual(["play"])
})
it("pauses and clears media without destroying the initialized player", async () => {
const track = { url: "https://example.com/audio.mp3" }
await player.play(track)
await player.pause()
expect(mocks.native.pause).toHaveBeenCalledOnce()
expect(mocks.state.activeItem?.url).toBe(track.url)
await player.reset()
expect(mocks.native.clear).toHaveBeenCalledOnce()
expect(mocks.state.activeItem).toBeNull()
expect(mocks.native.destroy).not.toHaveBeenCalled()
expect(mocks.native.setupPlayer).not.toHaveBeenCalled()
await player.play(track)
expect(mocks.native.setMediaItem).toHaveBeenCalledTimes(2)
expect(mocks.native.play).toHaveBeenCalledTimes(2)
expect(mocks.toastError).not.toHaveBeenCalled()
})
it("keeps buffering pausable and distinguishes explicit pause from pending playback", async () => {
mocks.state.playbackState = "buffering"
expect(player.isPlaying()).toBe(true)
await player.pause()
expect(player.isPlaying()).toBe(false)
await player.play()
expect(player.isPlaying()).toBe(true)
mocks.state.playbackState = "ready"
mocks.state.nativePlaying = true
expect(player.isPlaying()).toBe(true)
await player.pause()
expect(player.isPlaying()).toBe(false)
})
it("shows an initialization error without calling native APIs before registration", async () => {
mocks.state.registered = false
await player.play({ url: "https://example.com/audio.mp3" })
expect(mocks.toastError).toHaveBeenCalledOnce()
for (const nativeMethod of Object.values(mocks.native)) {
expect(nativeMethod).not.toHaveBeenCalled()
}
})
})
+65 -28
View File
@@ -1,6 +1,12 @@
import { jotaiStore } from "@follow/utils"
import TrackPlayer, {
PlaybackState,
useActiveMediaItem,
useIsPlaying as useNativeIsPlaying,
usePlaybackState,
} from "@rntp/player"
import { atom, useAtom } from "jotai"
import { useCallback, useEffect, useSyncExternalStore } from "react"
import TrackPlayer, { useActiveTrack, useIsPlaying } from "react-native-track-player"
import { PlayerRegistered } from "../initialize/player"
import { ttsStreamController } from "../modules/player/tts-stream-controller"
@@ -8,27 +14,52 @@ import { toast } from "./toast"
export type SimpleMediaState = "playing" | "paused" | "loading"
/**
* Learn more https://rntp.dev/docs/guides/play-button
*/
export function useAudioPlayState(audioUrl?: string): SimpleMediaState {
const playState = useIsPlaying()
const activeTrack = useActiveTrack()
const playingUrl = activeTrack?.url
const pausedByUserAtom = atom(false)
const isCurrentTrack = !audioUrl || playingUrl === audioUrl
if (!playingUrl || !isCurrentTrack) {
function usePlaybackStatus() {
const playing = useNativeIsPlaying()
const playbackState = usePlaybackState()
const [pausedByUser, setPausedByUser] = useAtom(pausedByUserAtom)
useEffect(() => {
if (playing) setPausedByUser(false)
}, [playing, setPausedByUser])
// V5 reports actual audio output, so buffering must remain separately pausable.
const buffering = playbackState === PlaybackState.Buffering && !pausedByUser
return { playing: playing || buffering, buffering }
}
export function useIsPlaying() {
return usePlaybackStatus().playing
}
export function useAudioPlayState(audioUrl?: string): SimpleMediaState {
const { playing, buffering } = usePlaybackStatus()
const activeTrack = useActiveMediaItem()
const playingMediaId = activeTrack?.mediaId
const isCurrentTrack = !audioUrl || playingMediaId === audioUrl
if (!playingMediaId || !isCurrentTrack) {
// By default the audio should be in "paused" state
return "paused"
}
if (playState.bufferingDuringPlay === true) {
if (buffering) {
return "loading"
}
return playState.playing ? "playing" : "paused"
return playing ? "playing" : "paused"
}
class Player {
isPlaying() {
return (
TrackPlayer.isPlaying() ||
(TrackPlayer.getPlaybackState() === PlaybackState.Buffering &&
!jotaiStore.get(pausedByUserAtom))
)
}
async play(newTrack?: {
url: string
title?: string | null
@@ -37,44 +68,50 @@ class Player {
}) {
if (!PlayerRegistered) {
toast.error("Player is not registered. Please wait for the app to initialize.")
return
}
if (newTrack) {
const activeTrack = await TrackPlayer.getActiveTrack()
if (activeTrack?.url !== newTrack.url) {
const activeTrack = TrackPlayer.getActiveMediaItem()
if (activeTrack?.mediaId !== newTrack.url) {
const { url, title, artist, artwork } = newTrack
await TrackPlayer.load({
// Keep identity stable when native playback normalizes local file URLs.
TrackPlayer.setMediaItem({
mediaId: url,
url,
title: title ?? "Unknown Title",
artist: artist ?? "Unknown Artist",
artwork: artwork ?? undefined,
artworkUrl: artwork ?? undefined,
})
}
}
await TrackPlayer.play()
TrackPlayer.play()
jotaiStore.set(pausedByUserAtom, false)
}
async pause() {
await TrackPlayer.pause()
TrackPlayer.pause()
jotaiStore.set(pausedByUserAtom, true)
}
async reset() {
await TrackPlayer.reset()
TrackPlayer.clear()
jotaiStore.set(pausedByUserAtom, true)
}
async seekBy(offset: number) {
await TrackPlayer.seekBy(offset)
TrackPlayer.seekBy(offset)
}
async seekTo(position: number) {
await TrackPlayer.seekTo(position)
TrackPlayer.seekTo(position)
}
}
export const player = new Player()
export { useActiveTrack, useIsPlaying, useProgress } from "react-native-track-player"
export { useProgress } from "@rntp/player"
export interface ActivePlayable {
artwork?: string | null
@@ -89,7 +126,7 @@ export function useTtsStreamPlayback() {
}
export function useActivePlayable(): ActivePlayable | null {
const activeTrack = useActiveTrack()
const activeTrack = useActiveMediaItem()
const ttsStream = useTtsStreamPlayback()
if (ttsStream.entryId) {
@@ -107,7 +144,7 @@ export function useActivePlayable(): ActivePlayable | null {
}
return {
artwork: activeTrack.artwork,
artwork: typeof activeTrack.artworkUrl === "string" ? activeTrack.artworkUrl : undefined,
artist: activeTrack.artist,
entryId: null,
kind: "track-player",
@@ -124,8 +161,8 @@ export function useRate() {
const [rate, setRate] = useAtom(rateAtom)
useEffect(() => {
async function getRate() {
const rate = await TrackPlayer.getRate()
function getRate() {
const rate = TrackPlayer.getPlaybackSpeed()
if (allowedRate.includes(rate)) {
setRate(rate as Rate)
} else {
@@ -137,9 +174,9 @@ export function useRate() {
}, [setRate])
const setRateAndSave = useCallback(
async (rate: Rate) => {
(rate: Rate) => {
if (allowedRate.includes(rate)) {
await TrackPlayer.setRate(rate)
TrackPlayer.setPlaybackSpeed(rate)
setRate(rate)
}
},
+2 -2
View File
@@ -37,7 +37,7 @@ type ControlButtonProps = {
}
export function PlayPauseButton({ size = 24, className, color }: ControlButtonProps) {
const ttsStream = useTtsStreamPlayback()
const { playing } = useIsPlaying()
const playing = useIsPlaying()
const isStreamPlaying = ttsStream.status === "playing"
const isStream = !!ttsStream.entryId
const label = useColor("label")
@@ -194,7 +194,7 @@ const formatSecondsToMinutes = (seconds: number) => {
}
export function ProgressBar() {
const { isBackgroundLight } = usePlayerScreenContext()
const { duration, position } = useProgress(250)
const { duration, position } = useProgress(0.25)
const isSliding = useSharedValue(false)
const progress = useDerivedValue(() => {
return duration > 0 ? position / duration : 0
@@ -0,0 +1,95 @@
import type { MediaItem } from "@rntp/player"
import { beforeEach, describe, expect, it, vi } from "vitest"
const mocks = vi.hoisted(() => ({
activeItem: null as MediaItem | null,
nativePlaying: false,
fileUrl: "file:///var/mobile/Library/Caches/tts/entry-1.mp3",
requestTtsFile: vi.fn(),
play: vi.fn(async () => {}),
pause: vi.fn(async () => {}),
streamPlay: vi.fn(async () => {
throw new Error("TTS streaming player is not ready")
}),
toastFetchError: vi.fn(),
}))
vi.mock("@follow/store/entry/getter", () => ({
getEntry: () => ({ feedId: "feed-1", title: "Article", media: [] }),
}))
vi.mock("@follow/store/feed/getter", () => ({
getFeedById: () => ({ title: "Feed" }),
}))
vi.mock("@rntp/player", () => ({
default: { getActiveMediaItem: () => mocks.activeItem },
}))
vi.mock("@/src/atoms/settings/general", () => ({
getGeneralSettings: () => ({ voice: "en-US-AvaMultilingualNeural" }),
}))
vi.mock("@/src/lib/error-parser", () => ({
toastFetchError: mocks.toastFetchError,
}))
vi.mock("@/src/lib/player", () => ({
player: {
isPlaying: () => mocks.nativePlaying,
play: mocks.play,
pause: mocks.pause,
},
}))
vi.mock("./tts-service", () => ({
getEntryTtsText: () => "Article text",
requestTtsFile: mocks.requestTtsFile,
}))
vi.mock("./tts-stream-controller", () => ({
ttsStreamController: {
canToggleEntry: () => false,
play: mocks.streamPlay,
},
}))
describe("playEntryTts", () => {
beforeEach(() => {
vi.resetModules()
vi.clearAllMocks()
mocks.activeItem = null
mocks.nativePlaying = false
mocks.requestTtsFile.mockResolvedValue(mocks.fileUrl)
})
it("toggles a cached TTS file using its stable ID after iOS normalizes the URL", async () => {
const { playEntryTts } = await import("./entry-tts")
const options = { toastTitle: "Read article" }
await playEntryTts("entry-1", options)
expect(mocks.requestTtsFile).toHaveBeenCalledOnce()
expect(mocks.play).toHaveBeenCalledWith(expect.objectContaining({ url: mocks.fileUrl }))
mocks.activeItem = {
mediaId: mocks.fileUrl,
url: "/var/mobile/Library/Caches/tts/entry-1.mp3",
}
mocks.nativePlaying = true
await playEntryTts("entry-1", options)
expect(mocks.pause).toHaveBeenCalledOnce()
expect(mocks.requestTtsFile).toHaveBeenCalledOnce()
expect(mocks.streamPlay).toHaveBeenCalledOnce()
mocks.nativePlaying = false
await playEntryTts("entry-1", options)
expect(mocks.play).toHaveBeenLastCalledWith()
expect(mocks.play).toHaveBeenCalledTimes(2)
expect(mocks.requestTtsFile).toHaveBeenCalledOnce()
expect(mocks.toastFetchError).not.toHaveBeenCalled()
})
})
+6 -8
View File
@@ -1,6 +1,6 @@
import { getEntry } from "@follow/store/entry/getter"
import { getFeedById } from "@follow/store/feed/getter"
import TrackPlayer, { State } from "react-native-track-player"
import TrackPlayer from "@rntp/player"
import { getGeneralSettings } from "@/src/atoms/settings/general"
import { toastFetchError } from "@/src/lib/error-parser"
@@ -21,8 +21,8 @@ const isSameEntryTtsTrack = async (entryId: string) => {
return false
}
const activeTrack = await TrackPlayer.getActiveTrack()
return activeTrack?.url === activeTtsTrackUrl
const activeTrack = TrackPlayer.getActiveMediaItem()
return activeTrack?.mediaId === activeTtsTrackUrl
}
const toggleCurrentTtsPlayback = async () => {
@@ -31,14 +31,12 @@ const toggleCurrentTtsPlayback = async () => {
return
}
const { state } = await TrackPlayer.getPlaybackState()
if ([State.Playing, State.Buffering, State.Loading].includes(state)) {
await TrackPlayer.pause()
if (player.isPlaying()) {
await player.pause()
return
}
await TrackPlayer.play()
await player.play()
}
export const playEntryTts = async (
+1 -1
View File
@@ -25,7 +25,7 @@ import { usePrefetchImageColors } from "../store/image/hooks"
function CoverArt({ cover }: { cover?: string }) {
const scale = useSharedValue(1)
const ttsStream = useTtsStreamPlayback()
const { playing } = useIsPlaying()
const playing = useIsPlaying()
const isPlaying = ttsStream.entryId ? ttsStream.status === "playing" : playing
useEffect(() => {
cancelAnimation(scale)
@@ -1,474 +0,0 @@
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 b2409a09939164c49c0f7a16bb6d3284e8eab8fb..cde697e28b1a098cfc654ebe5c7825f26bf2397e 100644
--- a/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicModule.kt
+++ b/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicModule.kt
@@ -251,8 +251,8 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun updateOptions(data: ReadableMap?, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun updateOptions(data: ReadableMap?, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
val options = Arguments.toBundle(data)
@@ -264,14 +264,14 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun add(data: ReadableArray?, insertBeforeIndex: Int, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun add(data: ReadableArray?, insertBeforeIndex: Int, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
try {
val tracks = readableArrayToTrackList(data);
if (insertBeforeIndex < -1 || insertBeforeIndex > musicService.tracks.size) {
callback.reject("index_out_of_bounds", "The track index is out of bounds")
- return@launch
+ return@launchInScope
}
val index = if (insertBeforeIndex == -1) musicService.tracks.size else insertBeforeIndex
musicService.add(
@@ -285,11 +285,11 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun load(data: ReadableMap?, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun load(data: ReadableMap?, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
if (data == null) {
callback.resolve(null)
- return@launch
+ return@launchInScope
}
val bundle = Arguments.toBundle(data);
if (bundle is Bundle) {
@@ -301,15 +301,15 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun move(fromIndex: Int, toIndex: Int, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun move(fromIndex: Int, toIndex: Int, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.move(fromIndex, toIndex)
callback.resolve(null)
}
@ReactMethod
- fun remove(data: ReadableArray?, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun remove(data: ReadableArray?, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
val inputIndexes = Arguments.toList(data)
if (inputIndexes != null) {
val size = musicService.tracks.size
@@ -321,7 +321,7 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
"index_out_of_bounds",
"One or more indexes was out of bounds"
)
- return@launch
+ return@launchInScope
}
indexes.add(index)
}
@@ -332,8 +332,8 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
@ReactMethod
fun updateMetadataForTrack(index: Int, map: ReadableMap?, callback: Promise) =
- scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
if (index < 0 || index >= musicService.tracks.size) {
callback.reject("index_out_of_bounds", "The index is out of bounds")
@@ -348,8 +348,8 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun updateNowPlayingMetadata(map: ReadableMap?, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun updateNowPlayingMetadata(map: ReadableMap?, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
if (musicService.tracks.isEmpty())
callback.reject("no_current_item", "There is no current item in the player")
@@ -364,8 +364,8 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun clearNowPlayingMetadata(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun clearNowPlayingMetadata(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
if (musicService.tracks.isEmpty())
callback.reject("no_current_item", "There is no current item in the player")
@@ -375,16 +375,16 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun removeUpcomingTracks(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun removeUpcomingTracks(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.removeUpcomingTracks()
callback.resolve(null)
}
@ReactMethod
- fun skip(index: Int, initialTime: Float, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun skip(index: Int, initialTime: Float, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.skip(index)
@@ -396,8 +396,8 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun skipToNext(initialTime: Float, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun skipToNext(initialTime: Float, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.skipToNext()
@@ -409,8 +409,8 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun skipToPrevious(initialTime: Float, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun skipToPrevious(initialTime: Float, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.skipToPrevious()
@@ -422,8 +422,8 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun reset(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun reset(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.stop()
delay(300) // Allow playback to stop
@@ -433,134 +433,134 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun play(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun play(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.play()
callback.resolve(null)
}
@ReactMethod
- fun pause(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun pause(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.pause()
callback.resolve(null)
}
@ReactMethod
- fun stop(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun stop(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.stop()
callback.resolve(null)
}
@ReactMethod
- fun seekTo(seconds: Float, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun seekTo(seconds: Float, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.seekTo(seconds)
callback.resolve(null)
}
@ReactMethod
- fun seekBy(offset: Float, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun seekBy(offset: Float, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.seekBy(offset)
callback.resolve(null)
}
@ReactMethod
- fun retry(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun retry(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.retry()
callback.resolve(null)
}
@ReactMethod
- fun setVolume(volume: Float, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun setVolume(volume: Float, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.setVolume(volume)
callback.resolve(null)
}
@ReactMethod
- fun getVolume(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun getVolume(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
callback.resolve(musicService.getVolume())
}
@ReactMethod
- fun setRate(rate: Float, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun setRate(rate: Float, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.setRate(rate)
callback.resolve(null)
}
@ReactMethod
- fun getRate(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun getRate(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
callback.resolve(musicService.getRate())
}
@ReactMethod
- fun setRepeatMode(mode: Int, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun setRepeatMode(mode: Int, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.setRepeatMode(RepeatMode.fromOrdinal(mode))
callback.resolve(null)
}
@ReactMethod
- fun getRepeatMode(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun getRepeatMode(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
callback.resolve(musicService.getRepeatMode().ordinal)
}
@ReactMethod
- fun setPlayWhenReady(playWhenReady: Boolean, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun setPlayWhenReady(playWhenReady: Boolean, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
musicService.playWhenReady = playWhenReady
callback.resolve(null)
}
@ReactMethod
- fun getPlayWhenReady(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun getPlayWhenReady(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
callback.resolve(musicService.playWhenReady)
}
@ReactMethod
- fun getTrack(index: Int, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun getTrack(index: Int, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
if (index >= 0 && index < musicService.tracks.size) {
- callback.resolve(Arguments.fromBundle(musicService.tracks[index].originalItem))
+ callback.resolve(musicService.tracks[index].originalItem?.let { Arguments.fromBundle(it) })
} else {
callback.resolve(null)
}
}
@ReactMethod
- fun getQueue(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun getQueue(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
callback.resolve(Arguments.fromList(musicService.tracks.map { it.originalItem }))
}
@ReactMethod
- fun setQueue(data: ReadableArray?, callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun setQueue(data: ReadableArray?, callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
try {
musicService.clear()
@@ -572,48 +572,48 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun getActiveTrackIndex(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun getActiveTrackIndex(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
callback.resolve(
if (musicService.tracks.isEmpty()) null else musicService.getCurrentTrackIndex()
)
}
@ReactMethod
- fun getActiveTrack(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun getActiveTrack(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
callback.resolve(
if (musicService.tracks.isEmpty()) null
- else Arguments.fromBundle(
- musicService.tracks[musicService.getCurrentTrackIndex()].originalItem
- )
+ else musicService.tracks[musicService.getCurrentTrackIndex()].originalItem?.let {
+ Arguments.fromBundle(it)
+ }
)
}
@ReactMethod
- fun getDuration(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun getDuration(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
callback.resolve(musicService.getDurationInSeconds())
}
@ReactMethod
- fun getBufferedPosition(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun getBufferedPosition(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
callback.resolve(musicService.getBufferedPositionInSeconds())
}
@ReactMethod
- fun getPosition(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun getPosition(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
callback.resolve(musicService.getPositionInSeconds())
}
@ReactMethod
- fun getProgress(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun getProgress(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
var bundle = Bundle()
bundle.putDouble("duration", musicService.getDurationInSeconds());
bundle.putDouble("position", musicService.getPositionInSeconds());
@@ -622,8 +622,16 @@ class MusicModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}
@ReactMethod
- fun getPlaybackState(callback: Promise) = scope.launch {
- if (verifyServiceBoundOrReject(callback)) return@launch
+ fun getPlaybackState(callback: Promise) = launchInScope {
+ if (verifyServiceBoundOrReject(callback)) return@launchInScope
callback.resolve(Arguments.fromBundle(musicService.getPlayerStateBundle(musicService.state)))
}
+
+ // Bridgeless interop layer tries to pass the `Job` from `scope.launch` to the JS side
+ // which causes an exception. We can work around this using a wrapper.
+ private fun launchInScope(block: suspend () -> Unit) {
+ scope.launch {
+ block()
+ }
+ }
}
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 9d6d869efcece065618d4f2cefdc8c54831af9ed..37f41fd3fcd8dcb7b5b54c0ad3f207ddeb6d961f 100644
--- a/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt
+++ b/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt
@@ -741,9 +741,7 @@ class MusicService : HeadlessJsTaskService() {
@MainThread
private fun emit(event: String, data: Bundle? = null) {
- reactNativeHost.reactInstanceManager.currentReactContext
- ?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
- ?.emit(event, data?.let { Arguments.fromBundle(it) })
+ reactContext?.emitDeviceEvent(event, data?.let { Arguments.fromBundle(it) })
}
@MainThread
@@ -751,17 +749,16 @@ class MusicService : HeadlessJsTaskService() {
val payload = Arguments.createArray()
data.forEach { payload.pushMap(Arguments.fromBundle(it)) }
- reactNativeHost.reactInstanceManager.currentReactContext
- ?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
- ?.emit(event, payload)
+ reactContext?.emitDeviceEvent(event, payload)
}
override fun getTaskConfig(intent: Intent?): HeadlessJsTaskConfig {
return HeadlessJsTaskConfig(TASK_KEY, Arguments.createMap(), 0, true)
}
+ // https://github.com/doublesymmetry/react-native-track-player/pull/2451
@MainThread
override fun onBind(intent: Intent): IBinder {
return binder
}
diff --git a/ios/RNTrackPlayer/RNTrackPlayerBridge.m b/ios/RNTrackPlayer/RNTrackPlayerBridge.m
index 7741994191921ed86f7577f7ce589b3939d9dd49..776c09ec0ae450daa4dc9031ccd3ff4baba016d3 100644
--- a/ios/RNTrackPlayer/RNTrackPlayerBridge.m
+++ b/ios/RNTrackPlayer/RNTrackPlayerBridge.m
@@ -151,17 +151,4 @@ RCT_EXTERN_METHOD(updateNowPlayingMetadata:(NSDictionary *)metadata
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject);
-RCT_EXTERN_METHOD(getSleepTimerProgress:(RCTPromiseResolveBlock)resolve
- rejecter:(RCTPromiseRejectBlock)reject);
-
-RCT_EXTERN_METHOD(setSleepTimer:(double)time
- resolver:(RCTPromiseResolveBlock)resolve
- rejecter:(RCTPromiseRejectBlock)reject);
-
-RCT_EXTERN_METHOD(sleepWhenActiveTrackReachesEnd:(RCTPromiseResolveBlock)resolve
- rejecter:(RCTPromiseRejectBlock)reject);
-
-RCT_EXTERN_METHOD(clearSleepTimer:(RCTPromiseResolveBlock)resolve
- rejecter:(RCTPromiseRejectBlock)reject);
-
@end
+18 -24
View File
@@ -71,9 +71,6 @@ patchedDependencies:
react-native-sheet-transitions:
hash: 4db5ed53b31f4349d609d7dd2d0e053b6ee4c27e8d6d72c90ee74015ae18235f
path: patches/react-native-sheet-transitions.patch
react-native-track-player@4.1.2:
hash: dc51df1dccb62feafc2369115ba8e02dba9b261d9cf397f4db6c57d6b4acefe5
path: patches/react-native-track-player@4.1.2.patch
workbox-precaching:
hash: 51e57c78af317e292b43fca4b72f57b247b7b1a1faa8b03c8881ddb798c7c52f
path: patches/workbox-precaching.patch
@@ -1148,6 +1145,9 @@ importers:
'@react-native-picker/picker':
specifier: 2.11.4
version: 2.11.4(react-native@0.86.0(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0)(bufferutil@4.1.0))(@types/react@19.2.17)(bufferutil@4.1.0)(react@19.2.7))(react@19.2.7)
'@rntp/player':
specifier: 5.9.2
version: 5.9.2(react-native@0.86.0(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0)(bufferutil@4.1.0))(@types/react@19.2.17)(bufferutil@4.1.0)(react@19.2.7))(react@19.2.7)
'@shikijs/themes':
specifier: 4.3.1
version: 4.3.1
@@ -1397,9 +1397,6 @@ importers:
react-native-svg:
specifier: 15.15.5
version: 15.15.5(react-native@0.86.0(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0)(bufferutil@4.1.0))(@types/react@19.2.17)(bufferutil@4.1.0)(react@19.2.7))(react@19.2.7)
react-native-track-player:
specifier: 4.1.2
version: 4.1.2(patch_hash=dc51df1dccb62feafc2369115ba8e02dba9b261d9cf397f4db6c57d6b4acefe5)(react-native@0.86.0(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0)(bufferutil@4.1.0))(@types/react@19.2.17)(bufferutil@4.1.0)(react@19.2.7))(react@19.2.7)
react-native-uikit-colors:
specifier: 0.6.2
version: 0.6.2(nativewind@4.2.6(fae4c8694d5ef169f465873e7969eb18))(react-native@0.86.0(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0)(bufferutil@4.1.0))(@types/react@19.2.17)(bufferutil@4.1.0)(react@19.2.7))(react@19.2.7)(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.43(@swc/helpers@0.5.18))(@types/node@26.1.1)(typescript@6.0.3)))
@@ -8621,6 +8618,16 @@ packages:
resolution: {integrity: sha512-Tb5wIMvBf/nLejTQ61krK644/CEMB/cpiaIFXqGApfGqO3GwcR3qnI0DbmkFVCl2OyEp8LnLX3EkucoL0+tbFg==}
engines: {node: ^v12.20.0 || ^14.13.0 || >=16.0.0}
'@rntp/player@5.9.2':
resolution: {integrity: sha512-BckAAXu412xR6jWhZAG3aGS8qyFam0ZMmf3bnaeoi9YAUzVoa9gHWjMUEq3SzVs/udbn1wBoVep+/K0NjrTrCg==}
peerDependencies:
react: 19.2.7
react-native: '*'
shaka-player: '>=4.12.0'
peerDependenciesMeta:
shaka-player:
optional: true
'@rolldown/binding-android-arm64@1.1.5':
resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -16959,19 +16966,6 @@ packages:
react: 19.2.7
react-native: '*'
react-native-track-player@4.1.2:
resolution: {integrity: sha512-cIgMlqVJx/95hirUWPRW8CHxiBFj9Rjl/voKHh2jF/2URYMTQyt76t/m2FKvjeYUW2doKv4QSCBIOUmtyDLtJw==}
peerDependencies:
react: 19.2.7
react-native: '>=0.60.0-rc.2'
react-native-windows: '>=0.63.0'
shaka-player: ^4.7.9
peerDependenciesMeta:
react-native-windows:
optional: true
shaka-player:
optional: true
react-native-uikit-colors@0.6.2:
resolution: {integrity: sha512-qcsQOb/0+3eoYPS/pJG8R5Idxnk+o/UKPwTfa4wCxJ/gFJEcShm1q0wsw9D7BgceXo8tK+aMDebeguvNJYt6Og==}
peerDependencies:
@@ -27619,6 +27613,11 @@ snapshots:
'@reteps/dockerfmt@0.3.6': {}
'@rntp/player@5.9.2(react-native@0.86.0(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0)(bufferutil@4.1.0))(@types/react@19.2.17)(bufferutil@4.1.0)(react@19.2.7))(react@19.2.7)':
dependencies:
react: 19.2.7
react-native: 0.86.0(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0)(bufferutil@4.1.0))(@types/react@19.2.17)(bufferutil@4.1.0)(react@19.2.7)
'@rolldown/binding-android-arm64@1.1.5':
optional: true
@@ -37475,11 +37474,6 @@ snapshots:
react-native: 0.86.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7)(bufferutil@4.1.0))(@types/react@19.2.17)(bufferutil@4.1.0)(react@19.2.7)
optional: true
react-native-track-player@4.1.2(patch_hash=dc51df1dccb62feafc2369115ba8e02dba9b261d9cf397f4db6c57d6b4acefe5)(react-native@0.86.0(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0)(bufferutil@4.1.0))(@types/react@19.2.17)(bufferutil@4.1.0)(react@19.2.7))(react@19.2.7):
dependencies:
react: 19.2.7
react-native: 0.86.0(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0)(bufferutil@4.1.0))(@types/react@19.2.17)(bufferutil@4.1.0)(react@19.2.7)
react-native-uikit-colors@0.6.2(nativewind@4.2.6(fae4c8694d5ef169f465873e7969eb18))(react-native@0.86.0(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0)(bufferutil@4.1.0))(@types/react@19.2.17)(bufferutil@4.1.0)(react@19.2.7))(react@19.2.7)(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.43(@swc/helpers@0.5.18))(@types/node@26.1.1)(typescript@6.0.3))):
dependencies:
apple-uikit-colors: 0.6.2
-1
View File
@@ -87,7 +87,6 @@ patchedDependencies:
re-resizable@6.11.2: patches/re-resizable@6.11.2.patch
react-native-ios-utilities@5.2.0: patches/react-native-ios-utilities@5.2.0.patch
react-native-sheet-transitions: patches/react-native-sheet-transitions.patch
react-native-track-player@4.1.2: patches/react-native-track-player@4.1.2.patch
workbox-precaching: patches/workbox-precaching.patch
peerDependencyRules: