Packages

Official Erlang SDK for Rocksky — a Rustler NIF over the shared Rust core (rocksky-sdk): AppView reads, record writes (scrobble fan-out, like, follow, shout), and the identity hashes shared across every Rocksky SDK.

Current section

Files

Jump to
rocksky_erl src rocksky_remote_controller.erl
Raw

src/rocksky_remote_controller.erl

%% RemoteController — build a Rocksky remote UI over the remote-control WebSocket
%% (see remote-ws/PROTOCOL.md). The other half of rocksky_remote_player: connect/
%% 2,3 registers in the background; you observe the user's players by polling
%% next_event/1 in a loop (device list, now-playing, status, queue) and drive them
%% by sending commands (set_primary/command/seek/queue_jump/queue_remove/enqueue).
%% Heartbeat, reconnect, and the register handshake are handled by the native core.
%%
%% Every command takes a `Target` device id; pass <<>> (or undefined) to
%% broadcast to all the user's devices.
%%
%% Ctl = rocksky_remote_controller:connect(Token, <<"My Controller">>),
%% rocksky_remote_controller:spawn_listener(Ctl, fun handle_event/1),
%% rocksky_remote_controller:set_primary(Ctl, DeviceId),
%% rocksky_remote_controller:pause(Ctl, DeviceId),
%% rocksky_remote_controller:seek(Ctl, DeviceId, 42000),
%% ...
%% rocksky_remote_controller:disconnect(Ctl).
%%
%% `Controller` is an opaque NIF resource from connect/2,3 (GC'd). Commands
%% return {ok, Value} | {error, Message}.
-module(rocksky_remote_controller).
-export([connect/2, connect/3, next_event/1, set_primary/2, command/3,
play/1, play/2, pause/1, pause/2, next/1, next/2,
previous/1, previous/2, seek/3, queue_jump/3, queue_remove/3,
enqueue/6, disconnect/1, spawn_listener/2]).
%% Connect and register a controller. `Name` is a registration label (controllers
%% are hidden from device lists); connect/3 overrides the WebSocket URL (empty =
%% public endpoint). Returns the opaque controller resource.
connect(Token, Name) -> connect(Token, Name, <<>>).
connect(Token, Name, Url) ->
rocksky_nif:remote_controller_connect(b(Token), b(Name), b(Url)).
%% Block until the next update, returned as a decoded event map with a
%% <<"type">> key (devices | device_registered | device_unregistered |
%% primary_changed | now_playing | status | queue). Returns `undefined` once the
%% controller is disconnected.
next_event(Controller) ->
case unwrap(rocksky_nif:remote_controller_next_event(Controller)) of
{ok, null} -> undefined;
{ok, Event} -> Event;
{error, _} = Err -> Err
end.
%% Choose the primary (scrobble/profile) device.
set_primary(Controller, DeviceId) ->
unwrap(rocksky_nif:remote_controller_set_primary(Controller, b(DeviceId))).
%% Send a simple command to a device: <<"play">> | <<"pause">> | <<"next">> |
%% <<"previous">>. `Target` is a device id, or <<>>/undefined to broadcast.
command(Controller, Action, Target) ->
unwrap(rocksky_nif:remote_controller_command(Controller, b(Action), b(Target))).
%% Convenience wrappers over command/3 (arity /1 broadcasts to all devices).
play(Controller) -> play(Controller, <<>>).
play(Controller, Target) -> command(Controller, <<"play">>, Target).
pause(Controller) -> pause(Controller, <<>>).
pause(Controller, Target) -> command(Controller, <<"pause">>, Target).
next(Controller) -> next(Controller, <<>>).
next(Controller, Target) -> command(Controller, <<"next">>, Target).
previous(Controller) -> previous(Controller, <<>>).
previous(Controller, Target) -> command(Controller, <<"previous">>, Target).
%% Seek the target device to `PositionMs` (<<>>/undefined target = broadcast).
seek(Controller, Target, PositionMs) ->
unwrap(rocksky_nif:remote_controller_seek(Controller, b(Target), PositionMs)).
%% Jump to `Index` in the target device's queue (<<>>/undefined = broadcast).
queue_jump(Controller, Target, Index) ->
unwrap(rocksky_nif:remote_controller_queue_jump(Controller, b(Target), Index)).
%% Remove queue item `Index` on the target device (<<>>/undefined = broadcast).
queue_remove(Controller, Target, Index) ->
unwrap(rocksky_nif:remote_controller_queue_remove(Controller, b(Target), Index)).
%% Enqueue `Tracks` (a list of queue-item maps, camelCase binary keys) on the
%% target device. `Mode` is <<"now">> | <<"next">> | <<"last">>; `Shuffle` is a
%% boolean; `StartIndex` is 0-based. <<>>/undefined target = broadcast.
enqueue(Controller, Target, Tracks, Mode, Shuffle, StartIndex) when is_list(Tracks) ->
unwrap(rocksky_nif:remote_controller_enqueue(
Controller, b(Target), iolist_to_binary(json:encode(Tracks)),
b(Mode), Shuffle, StartIndex)).
%% Disconnect and stop the background task.
disconnect(Controller) ->
unwrap(rocksky_nif:remote_controller_disconnect(Controller)).
%% Spawn a process that polls next_event/1 in a loop and hands each decoded event
%% to `Handler` — a fun/1 (called with the event map) or a pid (which receives
%% `{rocksky_event, Event}`). The loop ends when the controller disconnects
%% (next_event returns `undefined`). Returns the listener Pid.
spawn_listener(Controller, Handler) ->
spawn(fun() -> listen_loop(Controller, Handler) end).
listen_loop(Controller, Handler) ->
case next_event(Controller) of
undefined -> ok;
{error, _} -> ok;
Event ->
dispatch(Handler, Event),
listen_loop(Controller, Handler)
end.
dispatch(Handler, Event) when is_function(Handler, 1) -> Handler(Event);
dispatch(Handler, Event) when is_pid(Handler) -> Handler ! {rocksky_event, Event}.
%% Decode a NIF JSON-envelope binary into {ok, Value} | {error, Message}.
unwrap(Bin) ->
case json:decode(Bin) of
#{<<"error">> := Msg} -> {error, Msg};
#{<<"ok">> := Value} -> {ok, Value};
Other -> {ok, Other}
end.
b(V) when is_binary(V) -> V;
b(V) when is_list(V) -> list_to_binary(V);
b(undefined) -> <<>>.