synxed-native-sdk
v0.1.2
Published
Lightweight licensed music playback SDK for Expo and React Native applications
Downloads
16
Maintainers
Readme
Synxed Native SDK
Lightweight licensed-music playback SDK for Expo and React Native applications. Choose between a drop-in ready music player UI or build your own custom controls — both backed by the same powerful playback engine.
- Bundle size: ~35 KB (ESM), ~6 KB gzipped
- Runtime deps: 1 (
socket.io-client) - 60 fps record spinner driven entirely on the native thread
- Zero-config playlists — queue management, track changes, and skip logic handled by the server
Two ways to integrate
| Approach | When to use | Lines of code | | ---------------------------------------- | ------------------------------------------------------------------ | ------------- | | Drop-in UI | You want a ready-made player — just pass your API key and playlist | ~5 | | Build your own | You need custom controls, layout, or a non-React environment | ~30 |
Drop-in UI
The SDK ships with a production-ready MusicPlayer component. Three display modes, automatic lifecycle handling, and full playlist support — no configuration needed beyond your credentials.
5-second integration
npm install synxed-native-sdk expo-audio react-native-svgimport { MusicPlayer } from "synxed-native-sdk";
export default function App() {
return (
<View style={{ flex: 1 }}>
{/* Your app content */}
<MusicPlayer
apiKey="sk-..."
serverUrl="https://api.synxed.com"
playlistCode="chill-vibes"
defaultImage={require("./fallback-art.png")}
/>
</View>
);
}That's it. The player auto-connects, auto-plays the playlist, handles app backgrounding, and shows track info — all out of the box.
Display modes
// Mini — compact bar pinned to an edge (default)
<MusicPlayer
apiKey="sk-..."
serverUrl="https://api.synxed.com"
playlistCode="chill-vibes"
displayMode="mini"
position="bottom"
/>
// Full — large RecordSpinner + progress bar + prev/play/skip controls
<MusicPlayer
apiKey="sk-..."
serverUrl="https://api.synxed.com"
playlistCode="chill-vibes"
displayMode="full"
/>
// Icon — floating circular play/pause button
<MusicPlayer
apiKey="sk-..."
serverUrl="https://api.synxed.com"
playlistCode="chill-vibes"
displayMode="icon"
/>| Mode | Description |
| -------------------- | ------------------------------------------------------------------------------------ |
| "mini" (default) | Compact horizontal bar with artwork, title, progress, and play/pause |
| "full" | Large RecordSpinner + track info + scrub-able progress bar + prev/play/skip controls |
| "icon" | Floating circular play/pause button — smallest footprint |
MusicPlayer props
| Prop | Type | Default | Description |
| -------------------- | ------------------------------------ | ------------ | ------------------------------------------- |
| apiKey | string | required | Your Synxed API key |
| serverUrl | string | required | Synxed server URL |
| playlistCode | string | required | The playlist to stream |
| displayMode | "full" \| "mini" \| "icon" | "mini" | Visual presentation mode |
| position | "bottom" \| "top" | "bottom" | Edge to pin the player to |
| defaultImage | ImageSourcePropType | — | Fallback when no album art from the server |
| defaultArtwork | ImageSourcePropType | — | Alias for defaultImage (takes precedence) |
| autoPlay | boolean | true | Auto-start the playlist on mount |
| initialVolume | number | 0.3 | Volume on mount (0–1) |
| showProgress | boolean | true | Show scrub-able progress bar |
| backgroundColor | string | "#111" | Container background color |
| accentColor | string | "#14772F" | Play button and progress fill color |
| textColor | string | "#FFFFFF" | Track title color |
| textSecondaryColor | string | "#999999" | Artist name and time label color |
| edgeOffset | number | 0 | Distance from the pinned edge (pt) |
| horizontalInset | number | 12 | Horizontal inset from screen edges (pt) |
| style | StyleProp<ViewStyle> | — | Additional container styles |
| onTrackChange | (track: TrackInfo \| null) => void | — | Fires when the current track changes |
| onConnected | () => void | — | Fires when WebSocket connects |
| onError | (error: Error) => void | — | Fires on playback errors |
RecordSpinner
A standalone vinyl-record spinner you can use anywhere — with or without MusicPlayer.
import { RecordSpinner } from "synxed-native-sdk";
<RecordSpinner
imageSource={{ uri: currentTrack?.albumArt }}
size={280}
spinning={isPlaying}
onPress={() => (isPlaying ? pause() : resume())}
/>;| Prop | Type | Default | Description |
| ------------- | ---------------------- | ------------ | ------------------------------------ |
| imageSource | ImageSourcePropType | required | Album art or avatar — center label |
| size | number | 300 | Diameter in points |
| spinning | boolean | false | Drives rotation (tie to isPlaying) |
| visible | boolean | true | Show/hide the component |
| onPress | () => void | — | Tap handler |
| style | StyleProp<ViewStyle> | — | Additional container styles |
The rotation runs on the native thread via useNativeDriver: true — 60 fps, zero JS thread impact.
ProgressBar
A scrub-able progress bar with time labels. Works with any currentTime / duration pair.
import { ProgressBar } from "synxed-native-sdk";
<ProgressBar
currentTime={currentTime}
duration={duration}
onSeek={(ms) => seek(ms)}
accentColor="#1DB954"
labelColor="#999"
/>;| Prop | Type | Default | Description |
| ------------- | ---------------------- | ------------ | ------------------------------ |
| currentTime | number | required | Current playback position (ms) |
| duration | number | required | Total track duration (ms) |
| onSeek | (ms: number) => void | required | Called when the user scrubs |
| accentColor | string | "#1DB954" | Fill and thumb color |
| labelColor | string | "#999" | Time label color |
| barHeight | number | 4 | Track height in points |
| style | StyleProp<ViewStyle> | — | Additional container styles |
Icon components
Standalone SVG icons rendered via react-native-svg. Use them to build your own controls.
import { PlayIcon, PauseIcon, SkipNextIcon, SkipPreviousIcon, MusicNoteIcon } from "synxed-native-sdk";
<PlayIcon size={24} color="#1DB954" />
<PauseIcon size={24} color="#ffffff" />
<SkipNextIcon size={20} color="#999" />
<SkipPreviousIcon size={20} color="#999" />
<MusicNoteIcon size={40} color="#14772F" />| Component | Source | Description |
| ------------------ | -------------------------- | ------------------------------- |
| PlayIcon | public/play-*-icon.svg | Right-pointing play triangle |
| PauseIcon | public/puase-*-icons.svg | Two vertical pause bars |
| SkipNextIcon | Custom SVG | Bar + triangle — skip forward |
| SkipPreviousIcon | Custom SVG | Triangle + bar — skip backward |
| MusicNoteIcon | Custom SVG | Musical note — fallback artwork |
All accept size (number) and color (hex string) props. Default size varies per icon, default color is "#ffffff".
Build your own UI
If you want full control over the player UI, use the useSynxedPlayer hook (React) or the SynxedPlayer class (any environment).
React hook
import { useSynxedPlayer } from "synxed-native-sdk";
function CustomPlayer() {
const {
player, // raw SynxedPlayer instance
state, // PlayerState { status, currentTrack, currentTime, duration, volume }
isPlaying, // boolean
isConnected, // boolean — WebSocket state
currentTrack, // TrackInfo | null
currentTime, // ms
duration, // ms
playSong, // (options: PlaySongOptions) => Promise
playPlaylist, // (options: PlayPlaylistOptions) => Promise
pause, // () => Promise
resume, // () => Promise
stop, // () => Promise
skip, // () => Promise
previous, // () => Promise
skipTo, // (index: number) => void
seek, // (ms: number) => Promise
setVolume, // (volume: 0-1) => Promise
connect, // () => Promise
} = useSynxedPlayer({
apiKey: "sk-...",
serverUrl: "https://api.synxed.com",
autoConnect: true, // default: true
});
// Build your own UI using these values
return (
<View>
<Text>{currentTrack?.title ?? "Nothing playing"}</Text>
<Text>{currentTrack?.artist}</Text>
<RecordSpinner
imageSource={{ uri: currentTrack?.albumArt }}
spinning={isPlaying}
/>
<ProgressBar
currentTime={currentTime}
duration={duration}
onSeek={seek}
/>
<View style={{ flexDirection: "row", gap: 16 }}>
<Button title="⏮" onPress={previous} />
<Button
title={isPlaying ? "⏸" : "▶"}
onPress={() => (isPlaying ? pause() : resume())}
/>
<Button title="⏭" onPress={skip} />
</View>
<Button
title="Load Playlist"
onPress={() => playPlaylist({ playlistCode: "chill-vibes" })}
/>
</View>
);
}The hook manages the player lifecycle automatically — creates on mount, destroys on unmount. All playback methods are useCallback-wrapped for stable references.
Bare player (no React)
import { SynxedPlayer } from "synxed-native-sdk";
const player = new SynxedPlayer({
apiKey: "sk-...",
serverUrl: "https://api.synxed.com",
});
// Subscribe to events
player.on("stateChange", (state) => {
/* ... */
});
player.on("timeUpdate", ({ currentTime, duration }) => {
/* ... */
});
player.on("trackChange", (track) => {
/* ... */
});
player.on("queueUpdated", (tracks) => {
/* ... */
});
player.on("connected", () => {
/* ... */
});
player.on("disconnected", (reason) => {
/* ... */
});
player.on("error", (err) => {
/* ... */
});
// Playback
await player.playPlaylist({ playlistCode: "chill-vibes" });
await player.playSong({ catalogTrackId: "track-abc-123" });
await player.pause();
await player.resume();
await player.skip();
await player.previous();
await player.seek(30_000);
await player.setVolume(0.5);
// Cleanup
await player.destroy();Playlist streaming
The SDK handles the full playlist lifecycle — queue population, track advancement, skip/previous logic, and server synchronization.
Starting a playlist
// Hook
await playPlaylist({ playlistCode: "chill-vibes" });
// Bare player
await player.playPlaylist({ playlistCode: "chill-vibes" });What happens under the hood:
- SDK connects to the Synxed server via WebSocket
- Sends a playlist init request with your
playlistCode - Server responds with the playback URL and a content summary (full track queue as JSON)
- SDK populates the internal
PlaylistManagerwith all tracks and begins playback
Skip and previous
For multi-track playlists, skip and previous are server-authoritative — the server sends back the updated queue and playback URL. For single-track sessions, the SDK falls back to client-side logic.
await player.skip(); // Server-side for playlists
await player.previous(); // Server-side for playlists
player.skipTo(3); // Client-side — jump to index 3Auto-advance
When a track ends, the SDK automatically advances to the next track. When the last track finishes, the player goes to "idle" status.
Queue events
player.on("queueUpdated", (tracks: TrackInfo[]) => {
// Full track list — render a queue browser
tracks.forEach((t, i) => console.log(`${i + 1}. ${t.title}`));
});
player.on("trackChange", (track: TrackInfo) => {
// Fires on skip, previous, auto-advance, or initial play
updateNowPlaying(track);
});With the hook, currentTrack stays reactive automatically.
Checking what's next
const { player } = useSynxedPlayer({ ... });
const canSkip = player.playlist?.hasNext ?? false;
const canGoBack = player.playlist?.hasPrevious ?? false;App lifecycle
Handle backgrounding/foregrounding to pause and resume correctly:
import { AppState } from "react-native";
function App() {
const { player } = useSynxedPlayer({ apiKey: "...", serverUrl: "..." });
useEffect(() => {
const sub = AppState.addEventListener("change", (state) => {
if (state === "active") player.handleAppForeground();
else player.handleAppBackground();
});
return () => sub.remove();
}, [player]);
}MusicPlayer handles this automatically — you only need this if building your own UI.
API reference
Core types
interface SynxedConfig {
apiKey: string; // Your Synxed API key
serverUrl: string; // Synxed server URL
autoConnect?: boolean; // Auto-connect on construction (default: true)
}
interface PlaySongOptions {
catalogTrackId?: string;
internalTrackId?: string;
listenerId?: string;
}
interface PlayPlaylistOptions {
playlistCode: string;
listenerId?: string;
}
interface TrackInfo {
id: string;
kind: "catalog" | "internal";
title?: string;
artist?: string;
duration?: number; // ms
albumArt?: string; // URL
metadata?: TrackMetadata;
}
interface PlayerState {
status: "idle" | "loading" | "playing" | "paused" | "error";
currentTrack: TrackInfo | null;
currentTime: number; // ms
duration: number; // ms
volume: number; // 0–1
}
interface TimeUpdate {
currentTime: number; // ms
duration: number; // ms
}
interface SynxedEvents {
stateChange: (state: PlayerState) => void;
timeUpdate: (time: TimeUpdate) => void;
trackChange: (track: TrackInfo) => void;
queueUpdated: (tracks: TrackInfo[]) => void;
error: (error: Error) => void;
connected: () => void;
disconnected: (reason: string) => void;
}SynxedPlayer methods
| Method | Description |
| ----------------------- | ------------------------------------------ |
| connect() | Open WebSocket connection |
| disconnect() | Close WebSocket connection |
| playSong(opts) | Play a single track |
| playPlaylist(opts) | Play a playlist |
| pause() | Pause playback |
| resume() | Resume playback |
| stop() | Stop and reset to idle |
| skip() | Next track (server-side for playlists) |
| previous() | Previous track (server-side for playlists) |
| skipTo(index) | Jump to queue index |
| seek(ms) | Seek to position in milliseconds |
| setVolume(v) | Set volume (clamped 0–1) |
| destroy() | Full teardown |
| handleAppBackground() | Pause-on-background handler |
| handleAppForeground() | Auto-resume handler |
Getters
| Getter | Type | Description |
| --------------------- | --------------------------------------------------------- | -------------------------- |
| player.status | "idle" \| "loading" \| "playing" \| "paused" \| "error" | Current playback status |
| player.volume | number | Current volume |
| player.isConnected | boolean | WebSocket connection state |
| player.currentTrack | TrackInfo \| null | Currently active track |
Error types
import {
SynxedError, // Base error
SynxedConnectionError, // Socket / network / timeout failure
SynxedPlaybackError, // Audio engine failure
SynxedProtocolError, // Protobuf decode failure
} from "synxed-native-sdk";
player.on("error", (err) => {
if (err instanceof SynxedConnectionError) {
showToast("Connection lost. Retrying...");
} else if (err instanceof SynxedPlaybackError) {
showToast("Playback failed.");
} else {
showToast(err.message);
}
});Advanced exports
import {
EventEmitter, // Typed event system
AudioEngine, // expo-audio wrapper
PlaylistManager, // Queue manager
TransportManager, // WebSocket transport
encodeClientEnvelope, // Protobuf encoder
decodeServerEnvelope, // Protobuf decoder
} from "synxed-native-sdk";Requirements
| Dependency | Version | Notes |
| ------------------ | -------- | ---------------------------------------------------------------------------- |
| react | ≥ 18.0.0 | Optional — only needed for useSynxedPlayer, RecordSpinner, MusicPlayer |
| react-native | ≥ 0.70.0 | Optional — Animated, View, Image; Hermes TextEncoder support |
| expo-audio | ≥ 0.1.0 | Audio playback engine (replaces deprecated expo-av) |
| react-native-svg | ≥ 15.0.0 | Optional — SVG icon rendering for MusicPlayer and icon components |
| socket.io-client | ^4.7.0 | Bundled runtime dependency |
Troubleshooting
expo-audio not found
npx expo install expo-audioreact-native-svg not found
npx expo install react-native-svgsocket.io-client transport errors
The SDK uses WebSocket only. Ensure your dev server, proxy, or firewall allows ws:// or wss:// connections.
Hermes engine — TextEncoder not found
The SDK uses TextEncoder/TextDecoder for the protobuf layer. Hermes has supported these since React Native 0.70. Upgrade if needed:
npm install react-native@latestRecordSpinner doesn't spin
- Verify
spinning={true}is passed - Ensure the component is mounted (
visible={true}) - Rotation runs on the native thread — JS thread congestion won't affect it
Playlist queue not populating
- Confirm your
playlistCodeis valid - Listen to
queueUpdatedto verify:
player.on("queueUpdated", (tracks) => {
if (tracks.length === 0) console.warn("Empty queue");
});Skip/previous not advancing
- Multi-track playlists use server-authoritative skip/previous — the server must handle the control messages
- Single-track sessions use client-side fallback — ensure a
PlaylistManagerhas been populated
License
MIT
