react-native-nitro-player 1.4.4-alpha.0 → 1.5.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NitroPlayer.podspec +6 -0
- package/README.md +115 -0
- package/android/build.gradle +8 -0
- package/android/src/main/AndroidManifest.xml +11 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridCast.kt +79 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ExoPlayerCore.kt +10 -7
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCast.kt +122 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +13 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +7 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/CastSessionController.kt +160 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/NitroCastConfig.kt +23 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/NitroCastOptionsProvider.kt +30 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/NitroMediaItemConverter.kt +53 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/PlaybackService.kt +33 -0
- package/ios/HybridCast.swift +63 -0
- package/ios/core/CastSessionManager.swift +373 -0
- package/ios/core/TrackPlayerCast.swift +109 -0
- package/ios/core/TrackPlayerCore.swift +23 -2
- package/ios/core/TrackPlayerPlayback.swift +32 -0
- package/ios/core/TrackPlayerQueue.swift +8 -1
- package/ios/core/TrackPlayerQueueBuild.swift +21 -0
- package/lib/components/CastButton.d.ts +44 -0
- package/lib/components/CastButton.js +125 -0
- package/lib/hooks/castCallbackManager.d.ts +19 -0
- package/lib/hooks/castCallbackManager.js +43 -0
- package/lib/hooks/index.d.ts +3 -0
- package/lib/hooks/index.js +3 -0
- package/lib/hooks/useCastState.d.ts +27 -0
- package/lib/hooks/useCastState.js +49 -0
- package/lib/hooks/useIsCasting.d.ts +17 -0
- package/lib/hooks/useIsCasting.js +43 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +4 -1
- package/lib/specs/Cast.nitro.d.ts +58 -0
- package/lib/specs/Cast.nitro.js +1 -0
- package/nitro.json +10 -0
- package/nitrogen/generated/android/NitroPlayer+autolinking.cmake +3 -1
- package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +18 -0
- package/nitrogen/generated/android/c++/JCastState.hpp +64 -0
- package/nitrogen/generated/android/c++/JFunc_void_CastState_std__optional_std__variant_nitro__NullType__std__string__.hpp +83 -0
- package/nitrogen/generated/android/c++/JHybridCastSpec.cpp +115 -0
- package/nitrogen/generated/android/c++/JHybridCastSpec.hpp +69 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/CastState.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_CastState_std__optional_std__variant_nitro__NullType__std__string__.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridCastSpec.kt +85 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +38 -13
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +176 -125
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +8 -0
- package/nitrogen/generated/ios/NitroPlayerAutolinking.mm +8 -0
- package/nitrogen/generated/ios/NitroPlayerAutolinking.swift +12 -0
- package/nitrogen/generated/ios/c++/HybridCastSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridCastSpecSwift.hpp +133 -0
- package/nitrogen/generated/ios/swift/CastState.swift +48 -0
- package/nitrogen/generated/ios/swift/Func_void_CastState_std__optional_std__variant_nitro__NullType__std__string__.swift +65 -0
- package/nitrogen/generated/ios/swift/HybridCastSpec.swift +61 -0
- package/nitrogen/generated/ios/swift/HybridCastSpec_cxx.swift +254 -0
- package/nitrogen/generated/shared/c++/CastState.hpp +84 -0
- package/nitrogen/generated/shared/c++/HybridCastSpec.cpp +27 -0
- package/nitrogen/generated/shared/c++/HybridCastSpec.hpp +75 -0
- package/package.json +3 -3
- package/src/components/CastButton.tsx +233 -0
- package/src/hooks/castCallbackManager.ts +55 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/useCastState.ts +67 -0
- package/src/hooks/useIsCasting.ts +47 -0
- package/src/index.ts +9 -0
- package/src/specs/Cast.nitro.ts +72 -0
|
@@ -103,6 +103,14 @@ class TrackPlayerCore: NSObject {
|
|
|
103
103
|
internal let onProgressListeners = ListenerRegistry<(Double, Double, Bool?) -> Void>()
|
|
104
104
|
internal let onTracksNeedUpdateListeners = ListenerRegistry<([TrackItem], Int) -> Void>()
|
|
105
105
|
internal let onTemporaryQueueChangeListeners = ListenerRegistry<([TrackItem], [TrackItem]) -> Void>()
|
|
106
|
+
internal let onCastStateChangeListeners = ListenerRegistry<(CastState, String?) -> Void>()
|
|
107
|
+
|
|
108
|
+
// MARK: - Google Cast
|
|
109
|
+
/// Owns the GoogleCast session; created on the main thread in init. Cast features
|
|
110
|
+
/// are inert no-ops if the GoogleCast SDK is not linked.
|
|
111
|
+
internal var castManager: CastSessionManager?
|
|
112
|
+
/// True while playback is routed to a Cast device (audio plays only on the device).
|
|
113
|
+
internal var isCasting = false
|
|
106
114
|
|
|
107
115
|
// MARK: - Singleton
|
|
108
116
|
static let shared = TrackPlayerCore()
|
|
@@ -116,8 +124,14 @@ class TrackPlayerCore: NSObject {
|
|
|
116
124
|
self?.setupPlayer()
|
|
117
125
|
}
|
|
118
126
|
DispatchQueue.main.async { [weak self] in
|
|
119
|
-
self
|
|
120
|
-
self
|
|
127
|
+
guard let self else { return }
|
|
128
|
+
self.mediaSessionManager = MediaSessionManager()
|
|
129
|
+
self.mediaSessionManager?.setTrackPlayerCore(self)
|
|
130
|
+
// Initialize Cast with the Default Media Receiver. Apps can override the
|
|
131
|
+
// receiver ID early via Cast.configure(id). No-op without the GoogleCast SDK.
|
|
132
|
+
self.castManager = CastSessionManager()
|
|
133
|
+
self.castManager?.core = self
|
|
134
|
+
self.castManager?.configure(receiverApplicationId: nil)
|
|
121
135
|
}
|
|
122
136
|
}
|
|
123
137
|
|
|
@@ -195,6 +209,13 @@ class TrackPlayerCore: NSObject {
|
|
|
195
209
|
onTemporaryQueueChangeListeners.remove(id: id)
|
|
196
210
|
}
|
|
197
211
|
|
|
212
|
+
@discardableResult func addOnCastStateChangeListener(_ cb: @escaping (CastState, String?) -> Void) -> Int64 {
|
|
213
|
+
onCastStateChangeListeners.add(cb)
|
|
214
|
+
}
|
|
215
|
+
@discardableResult func removeOnCastStateChangeListener(id: Int64) -> Bool {
|
|
216
|
+
onCastStateChangeListeners.remove(id: id)
|
|
217
|
+
}
|
|
218
|
+
|
|
198
219
|
// MARK: - Simple accessors
|
|
199
220
|
func getCurrentPlaylistId() -> String? { currentPlaylistId }
|
|
200
221
|
func getPlaylistManager() -> PlaylistManager { playlistManager }
|
|
@@ -12,22 +12,44 @@ import MediaPlayer
|
|
|
12
12
|
extension TrackPlayerCore {
|
|
13
13
|
|
|
14
14
|
func play() async {
|
|
15
|
+
if isCasting {
|
|
16
|
+
intendedToPlay = true
|
|
17
|
+
castManager?.play()
|
|
18
|
+
return
|
|
19
|
+
}
|
|
15
20
|
await withPlayerQueueNoThrow { self.playInternal() }
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
func pause() async {
|
|
24
|
+
if isCasting {
|
|
25
|
+
intendedToPlay = false
|
|
26
|
+
castManager?.pause()
|
|
27
|
+
return
|
|
28
|
+
}
|
|
19
29
|
await withPlayerQueueNoThrow { self.pauseInternal() }
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
func seek(position: Double) async {
|
|
33
|
+
if isCasting {
|
|
34
|
+
castManager?.seek(to: position)
|
|
35
|
+
return
|
|
36
|
+
}
|
|
23
37
|
await withPlayerQueueNoThrow { self.seekInternal(position: position) }
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
func skipToNext() async {
|
|
41
|
+
if isCasting {
|
|
42
|
+
castManager?.skipToNext()
|
|
43
|
+
return
|
|
44
|
+
}
|
|
27
45
|
await withPlayerQueueNoThrow { self.skipToNextInternal() }
|
|
28
46
|
}
|
|
29
47
|
|
|
30
48
|
func skipToPrevious() async {
|
|
49
|
+
if isCasting {
|
|
50
|
+
castManager?.skipToPrevious()
|
|
51
|
+
return
|
|
52
|
+
}
|
|
31
53
|
await withPlayerQueueNoThrow { self.skipToPreviousInternal() }
|
|
32
54
|
}
|
|
33
55
|
|
|
@@ -40,6 +62,11 @@ extension TrackPlayerCore {
|
|
|
40
62
|
}
|
|
41
63
|
|
|
42
64
|
func setVolume(volume: Double) async {
|
|
65
|
+
if isCasting {
|
|
66
|
+
let clamped = max(0.0, min(100.0, volume))
|
|
67
|
+
castManager?.setVolume(Float(clamped / 100.0))
|
|
68
|
+
return
|
|
69
|
+
}
|
|
43
70
|
await withPlayerQueueNoThrow {
|
|
44
71
|
let clamped = max(0.0, min(100.0, volume))
|
|
45
72
|
let normalized = Float(clamped / 100.0)
|
|
@@ -65,6 +92,11 @@ extension TrackPlayerCore {
|
|
|
65
92
|
}
|
|
66
93
|
|
|
67
94
|
func setPlaybackSpeed(_ speed: Double) async {
|
|
95
|
+
if isCasting {
|
|
96
|
+
currentPlaybackSpeed = speed
|
|
97
|
+
castManager?.setPlaybackRate(Float(speed))
|
|
98
|
+
return
|
|
99
|
+
}
|
|
68
100
|
await withPlayerQueueNoThrow {
|
|
69
101
|
self.currentPlaybackSpeed = speed
|
|
70
102
|
// Only update rate if currently playing; pause keeps rate at 0 until play() is called
|
|
@@ -19,7 +19,14 @@ extension TrackPlayerCore {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
func skipToIndex(index: Int) async -> Bool {
|
|
22
|
-
|
|
22
|
+
if isCasting {
|
|
23
|
+
return await withPlayerQueueNoThrow {
|
|
24
|
+
guard index >= 0 && index < self.getActualQueueInternal().count else { return false }
|
|
25
|
+
self.castManager?.jump(toQueueIndex: index)
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return await withPlayerQueueNoThrow { self.skipToIndexInternal(index: index) }
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
func playFromIndex(index: Int) async {
|
|
@@ -12,6 +12,16 @@ import Foundation
|
|
|
12
12
|
extension TrackPlayerCore {
|
|
13
13
|
|
|
14
14
|
func updatePlayerQueue(tracks: [TrackItem]) {
|
|
15
|
+
// While casting, mirror the new queue to the Cast device instead of building
|
|
16
|
+
// the local AVQueuePlayer (which would start local audio).
|
|
17
|
+
if isCasting {
|
|
18
|
+
currentTracks = tracks
|
|
19
|
+
currentTrackIndex = 0
|
|
20
|
+
if let first = tracks.first { emitCastTrackChange(first) }
|
|
21
|
+
loadCastQueue(autoplay: intendedToPlay, position: 0)
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
NitroPlayerLogger.log("TrackPlayerCore", "\n" + String(repeating: "=", count: Constants.separatorLineLength))
|
|
16
26
|
NitroPlayerLogger.log("TrackPlayerCore", "📋 UPDATE PLAYER QUEUE - Received \(tracks.count) tracks")
|
|
17
27
|
NitroPlayerLogger.log("TrackPlayerCore", String(repeating: "=", count: Constants.separatorLineLength))
|
|
@@ -135,6 +145,17 @@ extension TrackPlayerCore {
|
|
|
135
145
|
NitroPlayerLogger.log("TrackPlayerCore", " Total tracks in playlist: \(self.currentTracks.count)")
|
|
136
146
|
NitroPlayerLogger.log("TrackPlayerCore", " Current index: \(self.currentTrackIndex), target index: \(index)")
|
|
137
147
|
|
|
148
|
+
// While casting, jump on the Cast device instead of rebuilding the local queue.
|
|
149
|
+
if isCasting {
|
|
150
|
+
self.playNextStack.removeAll()
|
|
151
|
+
self.upNextQueue.removeAll()
|
|
152
|
+
self.currentTemporaryType = .none
|
|
153
|
+
self.currentTrackIndex = index
|
|
154
|
+
if let track = self.getCurrentTrack() { self.emitCastTrackChange(track) }
|
|
155
|
+
self.loadCastQueue(autoplay: self.intendedToPlay, position: 0)
|
|
156
|
+
return true
|
|
157
|
+
}
|
|
158
|
+
|
|
138
159
|
// Preserve playback state — only resume if already playing.
|
|
139
160
|
let wasPlaying = self.player?.rate ?? 0 > 0
|
|
140
161
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type PressableProps, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import type { CastState } from '../specs/Cast.nitro';
|
|
4
|
+
export interface CastButtonProps extends Omit<PressableProps, 'children'> {
|
|
5
|
+
/** Icon size in points. Defaults to `24`. */
|
|
6
|
+
size?: number;
|
|
7
|
+
/** Glyph color when not connected. Defaults to `#000000`. */
|
|
8
|
+
color?: string;
|
|
9
|
+
/** Glyph color while connecting / connected. Defaults to `#1E88E5`. */
|
|
10
|
+
activeColor?: string;
|
|
11
|
+
/** Style applied to the pressable container. */
|
|
12
|
+
style?: StyleProp<ViewStyle>;
|
|
13
|
+
/**
|
|
14
|
+
* Hide the button entirely when no Cast devices are available on the network,
|
|
15
|
+
* matching standard platform Cast-button behaviour. Defaults to `true`.
|
|
16
|
+
*/
|
|
17
|
+
hideWhenNoDevices?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Render a custom icon instead of the built-in Cast glyph. Receives the live
|
|
20
|
+
* connection state so you can theme it yourself.
|
|
21
|
+
*/
|
|
22
|
+
renderIcon?: (info: {
|
|
23
|
+
state: CastState;
|
|
24
|
+
isCasting: boolean;
|
|
25
|
+
size: number;
|
|
26
|
+
color: string;
|
|
27
|
+
}) => React.ReactNode;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A ready-to-use Google Cast button.
|
|
31
|
+
*
|
|
32
|
+
* Reflects the live connection state, hides itself when no devices are available
|
|
33
|
+
* (configurable), and presents the native Cast dialog on press. For full control
|
|
34
|
+
* over the icon, pass `renderIcon`; for custom press handling, pass `onPress`
|
|
35
|
+
* (the default opens the system Cast picker).
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* import { CastButton } from 'react-native-nitro-player'
|
|
40
|
+
*
|
|
41
|
+
* <CastButton size={28} activeColor="#1DB954" />
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function CastButton({ size, color, activeColor, hideWhenNoDevices, renderIcon, onPress, style, ...rest }: CastButtonProps): React.ReactElement | null;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable, View, } from 'react-native';
|
|
3
|
+
import { Cast } from '../index';
|
|
4
|
+
import { useCastState } from '../hooks/useCastState';
|
|
5
|
+
/**
|
|
6
|
+
* A ready-to-use Google Cast button.
|
|
7
|
+
*
|
|
8
|
+
* Reflects the live connection state, hides itself when no devices are available
|
|
9
|
+
* (configurable), and presents the native Cast dialog on press. For full control
|
|
10
|
+
* over the icon, pass `renderIcon`; for custom press handling, pass `onPress`
|
|
11
|
+
* (the default opens the system Cast picker).
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* import { CastButton } from 'react-native-nitro-player'
|
|
16
|
+
*
|
|
17
|
+
* <CastButton size={28} activeColor="#1DB954" />
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function CastButton({ size = 24, color = '#000000', activeColor = '#1E88E5', hideWhenNoDevices = true, renderIcon, onPress, style, ...rest }) {
|
|
21
|
+
const { state, isCasting } = useCastState();
|
|
22
|
+
if (hideWhenNoDevices && state === 'no_devices_available') {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const active = state === 'connected' || state === 'connecting';
|
|
26
|
+
const glyphColor = active ? activeColor : color;
|
|
27
|
+
const handlePress = (event) => {
|
|
28
|
+
if (onPress) {
|
|
29
|
+
onPress(event);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
Cast.showCastPicker();
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.error('[CastButton] Failed to present cast picker:', error);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
return (React.createElement(Pressable, { accessibilityRole: "button", accessibilityLabel: isCasting ? 'Casting' : 'Cast', accessibilityState: { selected: isCasting }, hitSlop: 8, onPress: handlePress, style: style, ...rest }, renderIcon ? (renderIcon({ state, isCasting, size, color: glyphColor })) : (React.createElement(CastGlyph, { size: size, color: glyphColor }))));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Dependency-free rendition of the standard Cast glyph: a rounded "screen"
|
|
43
|
+
* rectangle (with its bottom-left corner open) and a broadcast signal — a dot
|
|
44
|
+
* and two concentric arcs — radiating from the bottom-left. Drawn purely with
|
|
45
|
+
* `View`s so the library stays dependency-free.
|
|
46
|
+
*/
|
|
47
|
+
function CastGlyph({ size, color, }) {
|
|
48
|
+
const stroke = Math.max(1.5, Math.round(size / 13));
|
|
49
|
+
const inset = Math.round(size * 0.14);
|
|
50
|
+
const dotSize = stroke * 1.5;
|
|
51
|
+
// Origin of the broadcast signal — the screen's bottom-left corner.
|
|
52
|
+
const cx = inset + stroke / 2;
|
|
53
|
+
const cy = size - inset - stroke / 2;
|
|
54
|
+
// Leave the screen's bottom-left corner open so the signal reads clearly.
|
|
55
|
+
const gap = size * 0.46;
|
|
56
|
+
return (React.createElement(View, { style: { width: size, height: size } },
|
|
57
|
+
React.createElement(View, { style: {
|
|
58
|
+
position: 'absolute',
|
|
59
|
+
left: inset,
|
|
60
|
+
top: inset,
|
|
61
|
+
right: inset,
|
|
62
|
+
height: stroke,
|
|
63
|
+
backgroundColor: color,
|
|
64
|
+
borderRadius: stroke,
|
|
65
|
+
} }),
|
|
66
|
+
React.createElement(View, { style: {
|
|
67
|
+
position: 'absolute',
|
|
68
|
+
top: inset,
|
|
69
|
+
bottom: inset,
|
|
70
|
+
right: inset,
|
|
71
|
+
width: stroke,
|
|
72
|
+
backgroundColor: color,
|
|
73
|
+
borderRadius: stroke,
|
|
74
|
+
} }),
|
|
75
|
+
React.createElement(View, { style: {
|
|
76
|
+
position: 'absolute',
|
|
77
|
+
left: inset + gap,
|
|
78
|
+
bottom: inset,
|
|
79
|
+
right: inset,
|
|
80
|
+
height: stroke,
|
|
81
|
+
backgroundColor: color,
|
|
82
|
+
borderRadius: stroke,
|
|
83
|
+
} }),
|
|
84
|
+
React.createElement(View, { style: {
|
|
85
|
+
position: 'absolute',
|
|
86
|
+
left: inset,
|
|
87
|
+
top: inset,
|
|
88
|
+
height: size - 2 * inset - gap,
|
|
89
|
+
width: stroke,
|
|
90
|
+
backgroundColor: color,
|
|
91
|
+
borderRadius: stroke,
|
|
92
|
+
} }),
|
|
93
|
+
React.createElement(RippleArc, { cx: cx, cy: cy, radius: size * 0.2, stroke: stroke, color: color }),
|
|
94
|
+
React.createElement(RippleArc, { cx: cx, cy: cy, radius: size * 0.34, stroke: stroke, color: color }),
|
|
95
|
+
React.createElement(View, { style: {
|
|
96
|
+
position: 'absolute',
|
|
97
|
+
left: cx - dotSize / 2,
|
|
98
|
+
top: cy - dotSize / 2,
|
|
99
|
+
width: dotSize,
|
|
100
|
+
height: dotSize,
|
|
101
|
+
borderRadius: dotSize / 2,
|
|
102
|
+
backgroundColor: color,
|
|
103
|
+
} })));
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* A quarter-circle arc (the up-right quadrant) centered at (cx, cy). Rendered as
|
|
107
|
+
* a circle with only its top border colored, rotated 45° so the visible cap
|
|
108
|
+
* faces up-and-to-the-right — i.e. radiating from the bottom-left dot.
|
|
109
|
+
*/
|
|
110
|
+
function RippleArc({ cx, cy, radius, stroke, color, }) {
|
|
111
|
+
return (React.createElement(View, { style: {
|
|
112
|
+
position: 'absolute',
|
|
113
|
+
left: cx - radius,
|
|
114
|
+
top: cy - radius,
|
|
115
|
+
width: radius * 2,
|
|
116
|
+
height: radius * 2,
|
|
117
|
+
borderRadius: radius,
|
|
118
|
+
borderWidth: stroke,
|
|
119
|
+
borderTopColor: color,
|
|
120
|
+
borderRightColor: 'transparent',
|
|
121
|
+
borderBottomColor: 'transparent',
|
|
122
|
+
borderLeftColor: 'transparent',
|
|
123
|
+
transform: [{ rotate: '45deg' }],
|
|
124
|
+
} }));
|
|
125
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CastState } from '../specs/Cast.nitro';
|
|
2
|
+
type CastStateCallback = (state: CastState, deviceName: string | null) => void;
|
|
3
|
+
/**
|
|
4
|
+
* Internal subscription manager that allows multiple hooks/components to
|
|
5
|
+
* subscribe to the single native `Cast.onCastStateChange` callback without
|
|
6
|
+
* overwriting each other. Mirrors {@link callbackManager}.
|
|
7
|
+
*/
|
|
8
|
+
declare class CastCallbackSubscriptionManager {
|
|
9
|
+
private castStateSubscribers;
|
|
10
|
+
private isCastStateRegistered;
|
|
11
|
+
/**
|
|
12
|
+
* Subscribe to Cast connection state changes.
|
|
13
|
+
* @returns Unsubscribe function
|
|
14
|
+
*/
|
|
15
|
+
subscribeToCastState(callback: CastStateCallback): () => void;
|
|
16
|
+
private ensureCastStateRegistered;
|
|
17
|
+
}
|
|
18
|
+
export declare const castCallbackManager: CastCallbackSubscriptionManager;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Cast } from '../index';
|
|
2
|
+
/**
|
|
3
|
+
* Internal subscription manager that allows multiple hooks/components to
|
|
4
|
+
* subscribe to the single native `Cast.onCastStateChange` callback without
|
|
5
|
+
* overwriting each other. Mirrors {@link callbackManager}.
|
|
6
|
+
*/
|
|
7
|
+
class CastCallbackSubscriptionManager {
|
|
8
|
+
castStateSubscribers = new Set();
|
|
9
|
+
isCastStateRegistered = false;
|
|
10
|
+
/**
|
|
11
|
+
* Subscribe to Cast connection state changes.
|
|
12
|
+
* @returns Unsubscribe function
|
|
13
|
+
*/
|
|
14
|
+
subscribeToCastState(callback) {
|
|
15
|
+
this.castStateSubscribers.add(callback);
|
|
16
|
+
this.ensureCastStateRegistered();
|
|
17
|
+
return () => {
|
|
18
|
+
this.castStateSubscribers.delete(callback);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
ensureCastStateRegistered() {
|
|
22
|
+
if (this.isCastStateRegistered)
|
|
23
|
+
return;
|
|
24
|
+
try {
|
|
25
|
+
Cast.onCastStateChange((state, deviceName) => {
|
|
26
|
+
this.castStateSubscribers.forEach((subscriber) => {
|
|
27
|
+
try {
|
|
28
|
+
subscriber(state, deviceName);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.error('[CastCallbackManager] Error in cast state subscriber:', error);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
this.isCastStateRegistered = true;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
console.error('[CastCallbackManager] Failed to register cast state callback:', error);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Export singleton instance
|
|
43
|
+
export const castCallbackManager = new CastCallbackSubscriptionManager();
|
package/lib/hooks/index.d.ts
CHANGED
|
@@ -11,6 +11,9 @@ export { usePlaylist } from './usePlaylist';
|
|
|
11
11
|
export type { UsePlaylistResult } from './usePlaylist';
|
|
12
12
|
export { useActualQueue } from './useActualQueue';
|
|
13
13
|
export type { UseActualQueueResult } from './useActualQueue';
|
|
14
|
+
export { useIsCasting } from './useIsCasting';
|
|
15
|
+
export { useCastState } from './useCastState';
|
|
16
|
+
export type { UseCastStateResult } from './useCastState';
|
|
14
17
|
export { useDownloadProgress } from './useDownloadProgress';
|
|
15
18
|
export type { UseDownloadProgressOptions, UseDownloadProgressResult, } from './useDownloadProgress';
|
|
16
19
|
export { useDownloadedTracks } from './useDownloadedTracks';
|
package/lib/hooks/index.js
CHANGED
|
@@ -7,6 +7,9 @@ export { useAudioDevices } from './useAudioDevices';
|
|
|
7
7
|
export { useNowPlaying } from './useNowPlaying';
|
|
8
8
|
export { usePlaylist } from './usePlaylist';
|
|
9
9
|
export { useActualQueue } from './useActualQueue';
|
|
10
|
+
// Cast hooks
|
|
11
|
+
export { useIsCasting } from './useIsCasting';
|
|
12
|
+
export { useCastState } from './useCastState';
|
|
10
13
|
// Download hooks
|
|
11
14
|
export { useDownloadProgress } from './useDownloadProgress';
|
|
12
15
|
export { useDownloadedTracks } from './useDownloadedTracks';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CastState } from '../specs/Cast.nitro';
|
|
2
|
+
export interface UseCastStateResult {
|
|
3
|
+
/** The current Cast connection state. */
|
|
4
|
+
state: CastState;
|
|
5
|
+
/** Friendly name of the connected device, or `null` when not connected. */
|
|
6
|
+
deviceName: string | null;
|
|
7
|
+
/** Convenience flag, equivalent to `state === 'connected'`. */
|
|
8
|
+
isCasting: boolean;
|
|
9
|
+
/** Whether the initial state has been read from native. */
|
|
10
|
+
isReady: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Hook to subscribe to the full Google Cast connection state.
|
|
14
|
+
*
|
|
15
|
+
* Provides the connection {@link CastState}, the connected device name, and a
|
|
16
|
+
* convenience `isCasting` flag. For a simple boolean prefer {@link useIsCasting}.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* function CastStatus() {
|
|
21
|
+
* const { state, deviceName, isCasting } = useCastState()
|
|
22
|
+
* if (!isCasting) return null
|
|
23
|
+
* return <Text>Casting to {deviceName} ({state})</Text>
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare function useCastState(): UseCastStateResult;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { Cast } from '../index';
|
|
3
|
+
import { castCallbackManager } from './castCallbackManager';
|
|
4
|
+
/**
|
|
5
|
+
* Hook to subscribe to the full Google Cast connection state.
|
|
6
|
+
*
|
|
7
|
+
* Provides the connection {@link CastState}, the connected device name, and a
|
|
8
|
+
* convenience `isCasting` flag. For a simple boolean prefer {@link useIsCasting}.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* function CastStatus() {
|
|
13
|
+
* const { state, deviceName, isCasting } = useCastState()
|
|
14
|
+
* if (!isCasting) return null
|
|
15
|
+
* return <Text>Casting to {deviceName} ({state})</Text>
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function useCastState() {
|
|
20
|
+
const [state, setState] = useState('not_connected');
|
|
21
|
+
const [deviceName, setDeviceName] = useState(null);
|
|
22
|
+
const [isReady, setIsReady] = useState(false);
|
|
23
|
+
const isMounted = useRef(true);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
isMounted.current = true;
|
|
26
|
+
// Seed from current native state.
|
|
27
|
+
try {
|
|
28
|
+
setState(Cast.getCastState());
|
|
29
|
+
setDeviceName(Cast.getCastDeviceName());
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
console.error('[useCastState] Failed to read initial cast state:', error);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
setIsReady(true);
|
|
36
|
+
}
|
|
37
|
+
const unsubscribe = castCallbackManager.subscribeToCastState((newState, newDeviceName) => {
|
|
38
|
+
if (isMounted.current) {
|
|
39
|
+
setState(newState);
|
|
40
|
+
setDeviceName(newDeviceName);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return () => {
|
|
44
|
+
isMounted.current = false;
|
|
45
|
+
unsubscribe();
|
|
46
|
+
};
|
|
47
|
+
}, []);
|
|
48
|
+
return { state, deviceName, isCasting: state === 'connected', isReady };
|
|
49
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook that returns whether playback is currently routed to a Google Cast device.
|
|
3
|
+
*
|
|
4
|
+
* Updates in real time as Cast sessions start and end. Initializes synchronously
|
|
5
|
+
* from the native player's current state.
|
|
6
|
+
*
|
|
7
|
+
* @returns `true` while connected to a Cast device, otherwise `false`.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* function NowPlaying() {
|
|
12
|
+
* const isCasting = useIsCasting()
|
|
13
|
+
* return <Text>{isCasting ? 'Casting' : 'Playing locally'}</Text>
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare function useIsCasting(): boolean;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { Cast } from '../index';
|
|
3
|
+
import { castCallbackManager } from './castCallbackManager';
|
|
4
|
+
/**
|
|
5
|
+
* Hook that returns whether playback is currently routed to a Google Cast device.
|
|
6
|
+
*
|
|
7
|
+
* Updates in real time as Cast sessions start and end. Initializes synchronously
|
|
8
|
+
* from the native player's current state.
|
|
9
|
+
*
|
|
10
|
+
* @returns `true` while connected to a Cast device, otherwise `false`.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* function NowPlaying() {
|
|
15
|
+
* const isCasting = useIsCasting()
|
|
16
|
+
* return <Text>{isCasting ? 'Casting' : 'Playing locally'}</Text>
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function useIsCasting() {
|
|
21
|
+
const [isCasting, setIsCasting] = useState(() => {
|
|
22
|
+
try {
|
|
23
|
+
return Cast.isCasting();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
const isMounted = useRef(true);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
isMounted.current = true;
|
|
32
|
+
const unsubscribe = castCallbackManager.subscribeToCastState((state) => {
|
|
33
|
+
if (isMounted.current) {
|
|
34
|
+
setIsCasting(state === 'connected');
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return () => {
|
|
38
|
+
isMounted.current = false;
|
|
39
|
+
unsubscribe();
|
|
40
|
+
};
|
|
41
|
+
}, []);
|
|
42
|
+
return isCasting;
|
|
43
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { PlayerQueue as PlayerQueueType, TrackPlayer as TrackPlayerType } f
|
|
|
2
2
|
import type { AndroidAutoMediaLibrary as AndroidAutoMediaLibraryType } from './specs/AndroidAutoMediaLibrary.nitro';
|
|
3
3
|
import type { AudioDevices as AudioDevicesType } from './specs/AudioDevices.nitro';
|
|
4
4
|
import type { AudioRoutePicker as AudioRoutePickerType } from './specs/AudioRoutePicker.nitro';
|
|
5
|
+
import type { Cast as CastType } from './specs/Cast.nitro';
|
|
5
6
|
import type { DownloadManager as DownloadManagerType } from './specs/DownloadManager.nitro';
|
|
6
7
|
import type { Equalizer as EqualizerType } from './specs/Equalizer.nitro';
|
|
7
8
|
export declare const PlayerQueue: PlayerQueueType;
|
|
@@ -9,9 +10,13 @@ export declare const TrackPlayer: TrackPlayerType;
|
|
|
9
10
|
export declare const AndroidAutoMediaLibrary: AndroidAutoMediaLibraryType | null;
|
|
10
11
|
export declare const AudioDevices: AudioDevicesType | null;
|
|
11
12
|
export declare const AudioRoutePicker: AudioRoutePickerType | null;
|
|
13
|
+
export declare const Cast: CastType;
|
|
12
14
|
export declare const DownloadManager: DownloadManagerType;
|
|
13
15
|
export declare const Equalizer: EqualizerType;
|
|
14
16
|
export * from './hooks';
|
|
17
|
+
export { CastButton } from './components/CastButton';
|
|
18
|
+
export type { CastButtonProps } from './components/CastButton';
|
|
19
|
+
export type { CastState } from './specs/Cast.nitro';
|
|
15
20
|
export * from './types/PlayerQueue';
|
|
16
21
|
export * from './types/AndroidAutoMediaLibrary';
|
|
17
22
|
export * from './types/DownloadTypes';
|
package/lib/index.js
CHANGED
|
@@ -15,13 +15,16 @@ export const AudioDevices = Platform.OS === 'android'
|
|
|
15
15
|
export const AudioRoutePicker = Platform.OS === 'ios'
|
|
16
16
|
? NitroModules.createHybridObject('AudioRoutePicker')
|
|
17
17
|
: null;
|
|
18
|
+
// Google Cast (iOS + Android)
|
|
19
|
+
export const Cast = NitroModules.createHybridObject('Cast');
|
|
18
20
|
// Download Manager
|
|
19
21
|
export const DownloadManager = NitroModules.createHybridObject('DownloadManager');
|
|
20
22
|
// Equalizer
|
|
21
23
|
export const Equalizer = NitroModules.createHybridObject('Equalizer');
|
|
22
24
|
// Export hooks
|
|
23
25
|
export * from './hooks';
|
|
24
|
-
// Export
|
|
26
|
+
// Export components
|
|
27
|
+
export { CastButton } from './components/CastButton';
|
|
25
28
|
export * from './types/PlayerQueue';
|
|
26
29
|
export * from './types/AndroidAutoMediaLibrary';
|
|
27
30
|
export * from './types/DownloadTypes';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { HybridObject } from 'react-native-nitro-modules';
|
|
2
|
+
/**
|
|
3
|
+
* The current Google Cast connection state.
|
|
4
|
+
*
|
|
5
|
+
* - `no_devices_available`: No Cast devices are discoverable on the network.
|
|
6
|
+
* The Cast button is typically hidden in this state.
|
|
7
|
+
* - `not_connected`: Cast devices are available but none is connected.
|
|
8
|
+
* - `connecting`: A session is being established with a device.
|
|
9
|
+
* - `connected`: A session is active and playback is routed to the device.
|
|
10
|
+
*/
|
|
11
|
+
export type CastState = 'no_devices_available' | 'not_connected' | 'connecting' | 'connected';
|
|
12
|
+
/**
|
|
13
|
+
* Google Cast control surface.
|
|
14
|
+
*
|
|
15
|
+
* Casting is implemented as an internal playback backend swap inside the native
|
|
16
|
+
* player core: when a session is `connected`, every playback operation (play,
|
|
17
|
+
* pause, seek, skip, queue) is routed to the remote Cast device and local audio
|
|
18
|
+
* is suppressed automatically. The existing {@link TrackPlayer} API keeps
|
|
19
|
+
* working unchanged — this object only exposes the Cast-specific surface.
|
|
20
|
+
*/
|
|
21
|
+
export interface Cast extends HybridObject<{
|
|
22
|
+
android: 'kotlin';
|
|
23
|
+
ios: 'swift';
|
|
24
|
+
}> {
|
|
25
|
+
/**
|
|
26
|
+
* Initialize the Cast framework. Must be called once early in app startup
|
|
27
|
+
* (before the first Cast interaction). Safe to call multiple times.
|
|
28
|
+
*
|
|
29
|
+
* @param receiverApplicationId Optional Web Receiver application ID registered
|
|
30
|
+
* in the Google Cast Developer Console. When omitted, the platform Default
|
|
31
|
+
* Media Receiver (`CC1AD845`) is used, which supports audio playback,
|
|
32
|
+
* metadata and queues out of the box.
|
|
33
|
+
*/
|
|
34
|
+
configure(receiverApplicationId?: string): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Whether playback is currently routed to a Cast device.
|
|
37
|
+
* Equivalent to `getCastState() === 'connected'`.
|
|
38
|
+
*/
|
|
39
|
+
isCasting(): boolean;
|
|
40
|
+
/** The current Cast connection state. */
|
|
41
|
+
getCastState(): CastState;
|
|
42
|
+
/** The friendly name of the connected device, or `null` when not connected. */
|
|
43
|
+
getCastDeviceName(): string | null;
|
|
44
|
+
/**
|
|
45
|
+
* Present the native Cast dialog. Shows the device chooser when not connected,
|
|
46
|
+
* or the cast controls / disconnect dialog when already connected. Mirrors the
|
|
47
|
+
* behaviour of the standard platform Cast button.
|
|
48
|
+
*/
|
|
49
|
+
showCastPicker(): void;
|
|
50
|
+
/** End the current Cast session and resume local playback. No-op when not connected. */
|
|
51
|
+
endCastSession(): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Register a callback fired whenever the Cast connection state changes.
|
|
54
|
+
* The callback receives the new {@link CastState} and the connected device
|
|
55
|
+
* name (or `null`).
|
|
56
|
+
*/
|
|
57
|
+
onCastStateChange(callback: (state: CastState, deviceName: string | null) => void): void;
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|