react-native-nitro-player 1.4.3 → 1.5.0-alpha.0
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 +113 -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 +367 -0
- package/ios/core/TrackPlayerCast.swift +109 -0
- package/ios/core/TrackPlayerCore.swift +28 -2
- package/ios/core/TrackPlayerPlayback.swift +32 -0
- package/ios/core/TrackPlayerQueue.swift +8 -1
- package/ios/core/TrackPlayerQueueBuild.swift +37 -2
- package/ios/core/TrackPlayerRedirectResolver.swift +150 -0
- package/lib/components/CastButton.d.ts +44 -0
- package/lib/components/CastButton.js +100 -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 +196 -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
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
///
|
|
2
|
+
/// CastState.hpp
|
|
3
|
+
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
|
|
4
|
+
/// https://github.com/mrousavy/nitro
|
|
5
|
+
/// Copyright © Marc Rousavy @ Margelo
|
|
6
|
+
///
|
|
7
|
+
|
|
8
|
+
#pragma once
|
|
9
|
+
|
|
10
|
+
#if __has_include(<NitroModules/NitroHash.hpp>)
|
|
11
|
+
#include <NitroModules/NitroHash.hpp>
|
|
12
|
+
#else
|
|
13
|
+
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
|
|
14
|
+
#endif
|
|
15
|
+
#if __has_include(<NitroModules/JSIConverter.hpp>)
|
|
16
|
+
#include <NitroModules/JSIConverter.hpp>
|
|
17
|
+
#else
|
|
18
|
+
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
|
|
19
|
+
#endif
|
|
20
|
+
#if __has_include(<NitroModules/NitroDefines.hpp>)
|
|
21
|
+
#include <NitroModules/NitroDefines.hpp>
|
|
22
|
+
#else
|
|
23
|
+
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
|
|
24
|
+
#endif
|
|
25
|
+
|
|
26
|
+
namespace margelo::nitro::nitroplayer {
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* An enum which can be represented as a JavaScript union (CastState).
|
|
30
|
+
*/
|
|
31
|
+
enum class CastState {
|
|
32
|
+
NO_DEVICES_AVAILABLE SWIFT_NAME(noDevicesAvailable) = 0,
|
|
33
|
+
NOT_CONNECTED SWIFT_NAME(notConnected) = 1,
|
|
34
|
+
CONNECTING SWIFT_NAME(connecting) = 2,
|
|
35
|
+
CONNECTED SWIFT_NAME(connected) = 3,
|
|
36
|
+
} CLOSED_ENUM;
|
|
37
|
+
|
|
38
|
+
} // namespace margelo::nitro::nitroplayer
|
|
39
|
+
|
|
40
|
+
namespace margelo::nitro {
|
|
41
|
+
|
|
42
|
+
// C++ CastState <> JS CastState (union)
|
|
43
|
+
template <>
|
|
44
|
+
struct JSIConverter<margelo::nitro::nitroplayer::CastState> final {
|
|
45
|
+
static inline margelo::nitro::nitroplayer::CastState fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
|
|
46
|
+
std::string unionValue = JSIConverter<std::string>::fromJSI(runtime, arg);
|
|
47
|
+
switch (hashString(unionValue.c_str(), unionValue.size())) {
|
|
48
|
+
case hashString("no_devices_available"): return margelo::nitro::nitroplayer::CastState::NO_DEVICES_AVAILABLE;
|
|
49
|
+
case hashString("not_connected"): return margelo::nitro::nitroplayer::CastState::NOT_CONNECTED;
|
|
50
|
+
case hashString("connecting"): return margelo::nitro::nitroplayer::CastState::CONNECTING;
|
|
51
|
+
case hashString("connected"): return margelo::nitro::nitroplayer::CastState::CONNECTED;
|
|
52
|
+
default: [[unlikely]]
|
|
53
|
+
throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum CastState - invalid value!");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::nitroplayer::CastState arg) {
|
|
57
|
+
switch (arg) {
|
|
58
|
+
case margelo::nitro::nitroplayer::CastState::NO_DEVICES_AVAILABLE: return JSIConverter<std::string>::toJSI(runtime, "no_devices_available");
|
|
59
|
+
case margelo::nitro::nitroplayer::CastState::NOT_CONNECTED: return JSIConverter<std::string>::toJSI(runtime, "not_connected");
|
|
60
|
+
case margelo::nitro::nitroplayer::CastState::CONNECTING: return JSIConverter<std::string>::toJSI(runtime, "connecting");
|
|
61
|
+
case margelo::nitro::nitroplayer::CastState::CONNECTED: return JSIConverter<std::string>::toJSI(runtime, "connected");
|
|
62
|
+
default: [[unlikely]]
|
|
63
|
+
throw std::invalid_argument("Cannot convert CastState to JS - invalid value: "
|
|
64
|
+
+ std::to_string(static_cast<int>(arg)) + "!");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
|
|
68
|
+
if (!value.isString()) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
std::string unionValue = JSIConverter<std::string>::fromJSI(runtime, value);
|
|
72
|
+
switch (hashString(unionValue.c_str(), unionValue.size())) {
|
|
73
|
+
case hashString("no_devices_available"):
|
|
74
|
+
case hashString("not_connected"):
|
|
75
|
+
case hashString("connecting"):
|
|
76
|
+
case hashString("connected"):
|
|
77
|
+
return true;
|
|
78
|
+
default:
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
} // namespace margelo::nitro
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
///
|
|
2
|
+
/// HybridCastSpec.cpp
|
|
3
|
+
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
|
|
4
|
+
/// https://github.com/mrousavy/nitro
|
|
5
|
+
/// Copyright © Marc Rousavy @ Margelo
|
|
6
|
+
///
|
|
7
|
+
|
|
8
|
+
#include "HybridCastSpec.hpp"
|
|
9
|
+
|
|
10
|
+
namespace margelo::nitro::nitroplayer {
|
|
11
|
+
|
|
12
|
+
void HybridCastSpec::loadHybridMethods() {
|
|
13
|
+
// load base methods/properties
|
|
14
|
+
HybridObject::loadHybridMethods();
|
|
15
|
+
// load custom methods/properties
|
|
16
|
+
registerHybrids(this, [](Prototype& prototype) {
|
|
17
|
+
prototype.registerHybridMethod("configure", &HybridCastSpec::configure);
|
|
18
|
+
prototype.registerHybridMethod("isCasting", &HybridCastSpec::isCasting);
|
|
19
|
+
prototype.registerHybridMethod("getCastState", &HybridCastSpec::getCastState);
|
|
20
|
+
prototype.registerHybridMethod("getCastDeviceName", &HybridCastSpec::getCastDeviceName);
|
|
21
|
+
prototype.registerHybridMethod("showCastPicker", &HybridCastSpec::showCastPicker);
|
|
22
|
+
prototype.registerHybridMethod("endCastSession", &HybridCastSpec::endCastSession);
|
|
23
|
+
prototype.registerHybridMethod("onCastStateChange", &HybridCastSpec::onCastStateChange);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
} // namespace margelo::nitro::nitroplayer
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
///
|
|
2
|
+
/// HybridCastSpec.hpp
|
|
3
|
+
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
|
|
4
|
+
/// https://github.com/mrousavy/nitro
|
|
5
|
+
/// Copyright © Marc Rousavy @ Margelo
|
|
6
|
+
///
|
|
7
|
+
|
|
8
|
+
#pragma once
|
|
9
|
+
|
|
10
|
+
#if __has_include(<NitroModules/HybridObject.hpp>)
|
|
11
|
+
#include <NitroModules/HybridObject.hpp>
|
|
12
|
+
#else
|
|
13
|
+
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
|
|
14
|
+
#endif
|
|
15
|
+
|
|
16
|
+
// Forward declaration of `CastState` to properly resolve imports.
|
|
17
|
+
namespace margelo::nitro::nitroplayer { enum class CastState; }
|
|
18
|
+
|
|
19
|
+
#include <NitroModules/Promise.hpp>
|
|
20
|
+
#include <string>
|
|
21
|
+
#include <optional>
|
|
22
|
+
#include "CastState.hpp"
|
|
23
|
+
#include <NitroModules/Null.hpp>
|
|
24
|
+
#include <variant>
|
|
25
|
+
#include <functional>
|
|
26
|
+
|
|
27
|
+
namespace margelo::nitro::nitroplayer {
|
|
28
|
+
|
|
29
|
+
using namespace margelo::nitro;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* An abstract base class for `Cast`
|
|
33
|
+
* Inherit this class to create instances of `HybridCastSpec` in C++.
|
|
34
|
+
* You must explicitly call `HybridObject`'s constructor yourself, because it is virtual.
|
|
35
|
+
* @example
|
|
36
|
+
* ```cpp
|
|
37
|
+
* class HybridCast: public HybridCastSpec {
|
|
38
|
+
* public:
|
|
39
|
+
* HybridCast(...): HybridObject(TAG) { ... }
|
|
40
|
+
* // ...
|
|
41
|
+
* };
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
class HybridCastSpec: public virtual HybridObject {
|
|
45
|
+
public:
|
|
46
|
+
// Constructor
|
|
47
|
+
explicit HybridCastSpec(): HybridObject(TAG) { }
|
|
48
|
+
|
|
49
|
+
// Destructor
|
|
50
|
+
~HybridCastSpec() override = default;
|
|
51
|
+
|
|
52
|
+
public:
|
|
53
|
+
// Properties
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
public:
|
|
57
|
+
// Methods
|
|
58
|
+
virtual std::shared_ptr<Promise<void>> configure(const std::optional<std::string>& receiverApplicationId) = 0;
|
|
59
|
+
virtual bool isCasting() = 0;
|
|
60
|
+
virtual CastState getCastState() = 0;
|
|
61
|
+
virtual std::variant<nitro::NullType, std::string> getCastDeviceName() = 0;
|
|
62
|
+
virtual void showCastPicker() = 0;
|
|
63
|
+
virtual std::shared_ptr<Promise<void>> endCastSession() = 0;
|
|
64
|
+
virtual void onCastStateChange(const std::function<void(CastState /* state */, const std::optional<std::variant<nitro::NullType, std::string>>& /* deviceName */)>& callback) = 0;
|
|
65
|
+
|
|
66
|
+
protected:
|
|
67
|
+
// Hybrid Setup
|
|
68
|
+
void loadHybridMethods() override;
|
|
69
|
+
|
|
70
|
+
protected:
|
|
71
|
+
// Tag for logging
|
|
72
|
+
static constexpr auto TAG = "Cast";
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
} // namespace margelo::nitro::nitroplayer
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-nitro-player",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0-alpha.0",
|
|
4
4
|
"description": "A powerful audio player library for React Native with playlist management, playback controls, and support for Android Auto and CarPlay",
|
|
5
5
|
"main": "lib/index",
|
|
6
6
|
"module": "lib/index",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"registry": "https://registry.npmjs.org/"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"@react-native/eslint-config": "0.
|
|
60
|
+
"@react-native/eslint-config": "0.86.0",
|
|
61
61
|
"@release-it-plugins/workspaces": "^5.0.3",
|
|
62
62
|
"@release-it/bumper": "^7.0.5",
|
|
63
63
|
"@release-it/conventional-changelog": "^10.0.1",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"nitrogen": "^0.35.2",
|
|
69
69
|
"prettier": "^3.3.3",
|
|
70
70
|
"react": "19.2.3",
|
|
71
|
-
"react-native": "0.
|
|
71
|
+
"react-native": "0.86.0",
|
|
72
72
|
"react-native-nitro-modules": "^0.35.2",
|
|
73
73
|
"release-it": "^19.0.4",
|
|
74
74
|
"typescript": "^5.8.3"
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Pressable,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
View,
|
|
6
|
+
type PressableProps,
|
|
7
|
+
type StyleProp,
|
|
8
|
+
type ViewStyle,
|
|
9
|
+
} from 'react-native'
|
|
10
|
+
import { Cast } from '../index'
|
|
11
|
+
import { useCastState } from '../hooks/useCastState'
|
|
12
|
+
import type { CastState } from '../specs/Cast.nitro'
|
|
13
|
+
|
|
14
|
+
export interface CastButtonProps extends Omit<PressableProps, 'children'> {
|
|
15
|
+
/** Icon size in points. Defaults to `24`. */
|
|
16
|
+
size?: number
|
|
17
|
+
/** Glyph color when not connected. Defaults to `#000000`. */
|
|
18
|
+
color?: string
|
|
19
|
+
/** Glyph color while connecting / connected. Defaults to `#1E88E5`. */
|
|
20
|
+
activeColor?: string
|
|
21
|
+
/** Style applied to the pressable container. */
|
|
22
|
+
style?: StyleProp<ViewStyle>
|
|
23
|
+
/**
|
|
24
|
+
* Hide the button entirely when no Cast devices are available on the network,
|
|
25
|
+
* matching standard platform Cast-button behaviour. Defaults to `true`.
|
|
26
|
+
*/
|
|
27
|
+
hideWhenNoDevices?: boolean
|
|
28
|
+
/**
|
|
29
|
+
* Render a custom icon instead of the built-in Cast glyph. Receives the live
|
|
30
|
+
* connection state so you can theme it yourself.
|
|
31
|
+
*/
|
|
32
|
+
renderIcon?: (info: {
|
|
33
|
+
state: CastState
|
|
34
|
+
isCasting: boolean
|
|
35
|
+
size: number
|
|
36
|
+
color: string
|
|
37
|
+
}) => React.ReactNode
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A ready-to-use Google Cast button.
|
|
42
|
+
*
|
|
43
|
+
* Reflects the live connection state, hides itself when no devices are available
|
|
44
|
+
* (configurable), and presents the native Cast dialog on press. For full control
|
|
45
|
+
* over the icon, pass `renderIcon`; for custom press handling, pass `onPress`
|
|
46
|
+
* (the default opens the system Cast picker).
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```tsx
|
|
50
|
+
* import { CastButton } from 'react-native-nitro-player'
|
|
51
|
+
*
|
|
52
|
+
* <CastButton size={28} activeColor="#1DB954" />
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function CastButton({
|
|
56
|
+
size = 24,
|
|
57
|
+
color = '#000000',
|
|
58
|
+
activeColor = '#1E88E5',
|
|
59
|
+
hideWhenNoDevices = true,
|
|
60
|
+
renderIcon,
|
|
61
|
+
onPress,
|
|
62
|
+
style,
|
|
63
|
+
...rest
|
|
64
|
+
}: CastButtonProps): React.ReactElement | null {
|
|
65
|
+
const { state, isCasting } = useCastState()
|
|
66
|
+
|
|
67
|
+
if (hideWhenNoDevices && state === 'no_devices_available') {
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const active = state === 'connected' || state === 'connecting'
|
|
72
|
+
const glyphColor = active ? activeColor : color
|
|
73
|
+
|
|
74
|
+
const handlePress: PressableProps['onPress'] = (event) => {
|
|
75
|
+
if (onPress) {
|
|
76
|
+
onPress(event)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
Cast.showCastPicker()
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('[CastButton] Failed to present cast picker:', error)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<Pressable
|
|
88
|
+
accessibilityRole="button"
|
|
89
|
+
accessibilityLabel={isCasting ? 'Casting' : 'Cast'}
|
|
90
|
+
accessibilityState={{ selected: isCasting }}
|
|
91
|
+
hitSlop={8}
|
|
92
|
+
onPress={handlePress}
|
|
93
|
+
style={style}
|
|
94
|
+
{...rest}
|
|
95
|
+
>
|
|
96
|
+
{renderIcon ? (
|
|
97
|
+
renderIcon({ state, isCasting, size, color: glyphColor })
|
|
98
|
+
) : (
|
|
99
|
+
<CastGlyph size={size} color={glyphColor} />
|
|
100
|
+
)}
|
|
101
|
+
</Pressable>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Dependency-free rendition of the standard Cast glyph: a rounded "screen"
|
|
107
|
+
* rectangle with a broadcast signal (dot + two arcs) in the bottom-left corner.
|
|
108
|
+
* Drawn purely with `View`s so the library stays dependency-free.
|
|
109
|
+
*/
|
|
110
|
+
function CastGlyph({
|
|
111
|
+
size,
|
|
112
|
+
color,
|
|
113
|
+
}: {
|
|
114
|
+
size: number
|
|
115
|
+
color: string
|
|
116
|
+
}): React.ReactElement {
|
|
117
|
+
const stroke = Math.max(1.5, Math.round(size * 0.083))
|
|
118
|
+
const inset = stroke
|
|
119
|
+
const dotSize = stroke * 1.7
|
|
120
|
+
const r1 = size * 0.28 // inner arc radius
|
|
121
|
+
const r2 = size * 0.46 // outer arc radius
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<View style={{ width: size, height: size }}>
|
|
125
|
+
{/* Screen outline */}
|
|
126
|
+
<View
|
|
127
|
+
style={[
|
|
128
|
+
StyleSheet.absoluteFill,
|
|
129
|
+
{
|
|
130
|
+
borderWidth: stroke,
|
|
131
|
+
borderColor: color,
|
|
132
|
+
borderRadius: Math.max(2, size * 0.12),
|
|
133
|
+
},
|
|
134
|
+
]}
|
|
135
|
+
/>
|
|
136
|
+
{/* Broadcast dot */}
|
|
137
|
+
<View
|
|
138
|
+
style={{
|
|
139
|
+
position: 'absolute',
|
|
140
|
+
left: inset,
|
|
141
|
+
bottom: inset,
|
|
142
|
+
width: dotSize,
|
|
143
|
+
height: dotSize,
|
|
144
|
+
borderRadius: dotSize / 2,
|
|
145
|
+
backgroundColor: color,
|
|
146
|
+
}}
|
|
147
|
+
/>
|
|
148
|
+
{/* Two concentric quarter arcs radiating from the dot */}
|
|
149
|
+
<Arc origin={inset + dotSize / 2} radius={r1} stroke={stroke} color={color} />
|
|
150
|
+
<Arc origin={inset + dotSize / 2} radius={r2} stroke={stroke} color={color} />
|
|
151
|
+
</View>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* A quarter-circle arc (the top-right quadrant) centered at the bottom-left
|
|
157
|
+
* `origin` point. Produced by clipping a full bordered circle to one quadrant.
|
|
158
|
+
*/
|
|
159
|
+
function Arc({
|
|
160
|
+
origin,
|
|
161
|
+
radius,
|
|
162
|
+
stroke,
|
|
163
|
+
color,
|
|
164
|
+
}: {
|
|
165
|
+
origin: number
|
|
166
|
+
radius: number
|
|
167
|
+
stroke: number
|
|
168
|
+
color: string
|
|
169
|
+
}): React.ReactElement {
|
|
170
|
+
return (
|
|
171
|
+
<View
|
|
172
|
+
// Clip box reveals only the quadrant facing up-and-right from the origin.
|
|
173
|
+
style={{
|
|
174
|
+
position: 'absolute',
|
|
175
|
+
left: origin,
|
|
176
|
+
bottom: origin - radius,
|
|
177
|
+
width: radius,
|
|
178
|
+
height: radius,
|
|
179
|
+
overflow: 'hidden',
|
|
180
|
+
}}
|
|
181
|
+
>
|
|
182
|
+
<View
|
|
183
|
+
style={{
|
|
184
|
+
position: 'absolute',
|
|
185
|
+
left: -radius,
|
|
186
|
+
top: 0,
|
|
187
|
+
width: radius * 2,
|
|
188
|
+
height: radius * 2,
|
|
189
|
+
borderRadius: radius,
|
|
190
|
+
borderWidth: stroke,
|
|
191
|
+
borderColor: color,
|
|
192
|
+
}}
|
|
193
|
+
/>
|
|
194
|
+
</View>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Cast } from '../index'
|
|
2
|
+
import type { CastState } from '../specs/Cast.nitro'
|
|
3
|
+
|
|
4
|
+
type CastStateCallback = (state: CastState, deviceName: string | null) => void
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal subscription manager that allows multiple hooks/components to
|
|
8
|
+
* subscribe to the single native `Cast.onCastStateChange` callback without
|
|
9
|
+
* overwriting each other. Mirrors {@link callbackManager}.
|
|
10
|
+
*/
|
|
11
|
+
class CastCallbackSubscriptionManager {
|
|
12
|
+
private castStateSubscribers = new Set<CastStateCallback>()
|
|
13
|
+
private isCastStateRegistered = false
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Subscribe to Cast connection state changes.
|
|
17
|
+
* @returns Unsubscribe function
|
|
18
|
+
*/
|
|
19
|
+
subscribeToCastState(callback: CastStateCallback): () => void {
|
|
20
|
+
this.castStateSubscribers.add(callback)
|
|
21
|
+
this.ensureCastStateRegistered()
|
|
22
|
+
|
|
23
|
+
return () => {
|
|
24
|
+
this.castStateSubscribers.delete(callback)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private ensureCastStateRegistered(): void {
|
|
29
|
+
if (this.isCastStateRegistered) return
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
Cast.onCastStateChange((state, deviceName) => {
|
|
33
|
+
this.castStateSubscribers.forEach((subscriber) => {
|
|
34
|
+
try {
|
|
35
|
+
subscriber(state, deviceName)
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error(
|
|
38
|
+
'[CastCallbackManager] Error in cast state subscriber:',
|
|
39
|
+
error
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
this.isCastStateRegistered = true
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error(
|
|
47
|
+
'[CastCallbackManager] Failed to register cast state callback:',
|
|
48
|
+
error
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Export singleton instance
|
|
55
|
+
export const castCallbackManager = new CastCallbackSubscriptionManager()
|
package/src/hooks/index.ts
CHANGED
|
@@ -12,6 +12,11 @@ export type { UsePlaylistResult } from './usePlaylist'
|
|
|
12
12
|
export { useActualQueue } from './useActualQueue'
|
|
13
13
|
export type { UseActualQueueResult } from './useActualQueue'
|
|
14
14
|
|
|
15
|
+
// Cast hooks
|
|
16
|
+
export { useIsCasting } from './useIsCasting'
|
|
17
|
+
export { useCastState } from './useCastState'
|
|
18
|
+
export type { UseCastStateResult } from './useCastState'
|
|
19
|
+
|
|
15
20
|
// Download hooks
|
|
16
21
|
export { useDownloadProgress } from './useDownloadProgress'
|
|
17
22
|
export type {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { Cast } from '../index'
|
|
3
|
+
import { castCallbackManager } from './castCallbackManager'
|
|
4
|
+
import type { CastState } from '../specs/Cast.nitro'
|
|
5
|
+
|
|
6
|
+
export interface UseCastStateResult {
|
|
7
|
+
/** The current Cast connection state. */
|
|
8
|
+
state: CastState
|
|
9
|
+
/** Friendly name of the connected device, or `null` when not connected. */
|
|
10
|
+
deviceName: string | null
|
|
11
|
+
/** Convenience flag, equivalent to `state === 'connected'`. */
|
|
12
|
+
isCasting: boolean
|
|
13
|
+
/** Whether the initial state has been read from native. */
|
|
14
|
+
isReady: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Hook to subscribe to the full Google Cast connection state.
|
|
19
|
+
*
|
|
20
|
+
* Provides the connection {@link CastState}, the connected device name, and a
|
|
21
|
+
* convenience `isCasting` flag. For a simple boolean prefer {@link useIsCasting}.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* function CastStatus() {
|
|
26
|
+
* const { state, deviceName, isCasting } = useCastState()
|
|
27
|
+
* if (!isCasting) return null
|
|
28
|
+
* return <Text>Casting to {deviceName} ({state})</Text>
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function useCastState(): UseCastStateResult {
|
|
33
|
+
const [state, setState] = useState<CastState>('not_connected')
|
|
34
|
+
const [deviceName, setDeviceName] = useState<string | null>(null)
|
|
35
|
+
const [isReady, setIsReady] = useState(false)
|
|
36
|
+
const isMounted = useRef(true)
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
isMounted.current = true
|
|
40
|
+
|
|
41
|
+
// Seed from current native state.
|
|
42
|
+
try {
|
|
43
|
+
setState(Cast.getCastState())
|
|
44
|
+
setDeviceName(Cast.getCastDeviceName())
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('[useCastState] Failed to read initial cast state:', error)
|
|
47
|
+
} finally {
|
|
48
|
+
setIsReady(true)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const unsubscribe = castCallbackManager.subscribeToCastState(
|
|
52
|
+
(newState, newDeviceName) => {
|
|
53
|
+
if (isMounted.current) {
|
|
54
|
+
setState(newState)
|
|
55
|
+
setDeviceName(newDeviceName)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return () => {
|
|
61
|
+
isMounted.current = false
|
|
62
|
+
unsubscribe()
|
|
63
|
+
}
|
|
64
|
+
}, [])
|
|
65
|
+
|
|
66
|
+
return { state, deviceName, isCasting: state === 'connected', isReady }
|
|
67
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { Cast } from '../index'
|
|
3
|
+
import { castCallbackManager } from './castCallbackManager'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hook that returns whether playback is currently routed to a Google Cast device.
|
|
7
|
+
*
|
|
8
|
+
* Updates in real time as Cast sessions start and end. Initializes synchronously
|
|
9
|
+
* from the native player's current state.
|
|
10
|
+
*
|
|
11
|
+
* @returns `true` while connected to a Cast device, otherwise `false`.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* function NowPlaying() {
|
|
16
|
+
* const isCasting = useIsCasting()
|
|
17
|
+
* return <Text>{isCasting ? 'Casting' : 'Playing locally'}</Text>
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function useIsCasting(): boolean {
|
|
22
|
+
const [isCasting, setIsCasting] = useState<boolean>(() => {
|
|
23
|
+
try {
|
|
24
|
+
return Cast.isCasting()
|
|
25
|
+
} catch {
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
const isMounted = useRef(true)
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
isMounted.current = true
|
|
33
|
+
|
|
34
|
+
const unsubscribe = castCallbackManager.subscribeToCastState((state) => {
|
|
35
|
+
if (isMounted.current) {
|
|
36
|
+
setIsCasting(state === 'connected')
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
return () => {
|
|
41
|
+
isMounted.current = false
|
|
42
|
+
unsubscribe()
|
|
43
|
+
}
|
|
44
|
+
}, [])
|
|
45
|
+
|
|
46
|
+
return isCasting
|
|
47
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
import type { AndroidAutoMediaLibrary as AndroidAutoMediaLibraryType } from './specs/AndroidAutoMediaLibrary.nitro'
|
|
10
10
|
import type { AudioDevices as AudioDevicesType } from './specs/AudioDevices.nitro'
|
|
11
11
|
import type { AudioRoutePicker as AudioRoutePickerType } from './specs/AudioRoutePicker.nitro'
|
|
12
|
+
import type { Cast as CastType } from './specs/Cast.nitro'
|
|
12
13
|
import type { DownloadManager as DownloadManagerType } from './specs/DownloadManager.nitro'
|
|
13
14
|
import type { Equalizer as EqualizerType } from './specs/Equalizer.nitro'
|
|
14
15
|
|
|
@@ -37,6 +38,9 @@ export const AudioRoutePicker =
|
|
|
37
38
|
? NitroModules.createHybridObject<AudioRoutePickerType>('AudioRoutePicker')
|
|
38
39
|
: null
|
|
39
40
|
|
|
41
|
+
// Google Cast (iOS + Android)
|
|
42
|
+
export const Cast = NitroModules.createHybridObject<CastType>('Cast')
|
|
43
|
+
|
|
40
44
|
// Download Manager
|
|
41
45
|
export const DownloadManager =
|
|
42
46
|
NitroModules.createHybridObject<DownloadManagerType>('DownloadManager')
|
|
@@ -48,7 +52,12 @@ export const Equalizer =
|
|
|
48
52
|
// Export hooks
|
|
49
53
|
export * from './hooks'
|
|
50
54
|
|
|
55
|
+
// Export components
|
|
56
|
+
export { CastButton } from './components/CastButton'
|
|
57
|
+
export type { CastButtonProps } from './components/CastButton'
|
|
58
|
+
|
|
51
59
|
// Export types
|
|
60
|
+
export type { CastState } from './specs/Cast.nitro'
|
|
52
61
|
export * from './types/PlayerQueue'
|
|
53
62
|
export * from './types/AndroidAutoMediaLibrary'
|
|
54
63
|
export * from './types/DownloadTypes'
|