mirror of
https://github.com/ivabus/lonelyradio
synced 2024-11-21 23:55:09 +03:00
Compare commits
3 commits
d43dcde7a2
...
bc3a43e870
Author | SHA1 | Date | |
---|---|---|---|
bc3a43e870 | |||
3804653512 | |||
dfb86522fb |
6 changed files with 207 additions and 36 deletions
32
.dockerignore
Normal file
32
.dockerignore
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# Include any files or directories that you don't want to be copied to your
|
||||||
|
# container here (e.g., local build artifacts, temporary files, etc.).
|
||||||
|
#
|
||||||
|
# For more help, visit the .dockerignore file reference guide at
|
||||||
|
# https://docs.docker.com/go/build-context-dockerignore/
|
||||||
|
|
||||||
|
**/.DS_Store
|
||||||
|
**/.classpath
|
||||||
|
**/.dockerignore
|
||||||
|
**/.env
|
||||||
|
**/.git
|
||||||
|
**/.gitignore
|
||||||
|
**/.project
|
||||||
|
**/.settings
|
||||||
|
**/.toolstarget
|
||||||
|
**/.vs
|
||||||
|
**/.vscode
|
||||||
|
**/*.*proj.user
|
||||||
|
**/*.dbmdl
|
||||||
|
**/*.jfm
|
||||||
|
**/charts
|
||||||
|
**/docker-compose*
|
||||||
|
**/compose*
|
||||||
|
**/Dockerfile*
|
||||||
|
**/node_modules
|
||||||
|
**/npm-debug.log
|
||||||
|
**/secrets.dev.yaml
|
||||||
|
**/values.dev.yaml
|
||||||
|
/bin
|
||||||
|
/target
|
||||||
|
LICENSE
|
||||||
|
README.md
|
91
Dockerfile
Normal file
91
Dockerfile
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
# Autogenerated by docker init
|
||||||
|
# https://docs.docker.com/go/dockerfile-reference/
|
||||||
|
|
||||||
|
ARG RUST_VERSION=1.80.1
|
||||||
|
ARG APP_NAME=lonelyradio
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# xx is a helper for cross-compilation.
|
||||||
|
# See https://github.com/tonistiigi/xx/ for more information.
|
||||||
|
FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.3.0 AS xx
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Create a stage for building the application.
|
||||||
|
FROM --platform=$BUILDPLATFORM rust:${RUST_VERSION}-alpine AS build
|
||||||
|
ARG APP_NAME
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy cross compilation utilities from the xx stage.
|
||||||
|
COPY --from=xx / /
|
||||||
|
|
||||||
|
# Install host build dependencies.
|
||||||
|
RUN apk add --no-cache clang lld musl-dev git file cmake make
|
||||||
|
|
||||||
|
# This is the architecture you’re building for, which is passed in by the builder.
|
||||||
|
# Placing it here allows the previous steps to be cached across architectures.
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
|
# Install cross compilation build dependencies.
|
||||||
|
RUN xx-apk add --no-cache musl-dev gcc
|
||||||
|
|
||||||
|
# Build the application.
|
||||||
|
# Leverage a cache mount to /usr/local/cargo/registry/
|
||||||
|
# for downloaded dependencies, a cache mount to /usr/local/cargo/git/db
|
||||||
|
# for git repository dependencies, and a cache mount to /app/target/ for
|
||||||
|
# compiled dependencies which will speed up subsequent builds.
|
||||||
|
# Leverage a bind mount to the src directory to avoid having to copy the
|
||||||
|
# source code into the container. Once built, copy the executable to an
|
||||||
|
# output directory before the cache mounted /app/target is unmounted.
|
||||||
|
RUN --mount=type=bind,source=src,target=src \
|
||||||
|
--mount=type=bind,source=lonelyradio_types,target=lonelyradio_types \
|
||||||
|
--mount=type=bind,source=monoclient,target=monoclient \
|
||||||
|
--mount=type=bind,source=monoclient-s,target=monoclient-s \
|
||||||
|
--mount=type=bind,source=monolib,target=monolib \
|
||||||
|
--mount=type=bind,source=monoloader,target=monoloader \
|
||||||
|
--mount=type=bind,source=Cargo.toml,target=Cargo.toml \
|
||||||
|
--mount=type=bind,source=Cargo.lock,target=Cargo.lock \
|
||||||
|
--mount=type=cache,target=/app/target/,id=rust-cache-${APP_NAME}-${TARGETPLATFORM} \
|
||||||
|
--mount=type=cache,target=/usr/local/cargo/git/db \
|
||||||
|
--mount=type=cache,target=/usr/local/cargo/registry/ \
|
||||||
|
<<EOF
|
||||||
|
set -e
|
||||||
|
xx-cargo build --locked --release --target-dir ./target
|
||||||
|
cp ./target/$(xx-cargo --print-target-triple)/release/$APP_NAME /bin/server
|
||||||
|
xx-verify /bin/server
|
||||||
|
EOF
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Create a new stage for running the application that contains the minimal
|
||||||
|
# runtime dependencies for the application. This often uses a different base
|
||||||
|
# image from the build stage where the necessary files are copied from the build
|
||||||
|
# stage.
|
||||||
|
#
|
||||||
|
# The example below uses the alpine image as the foundation for running the app.
|
||||||
|
# By specifying the "3.18" tag, it will use version 3.18 of alpine. If
|
||||||
|
# reproducability is important, consider using a digest
|
||||||
|
# (e.g., alpine@sha256:664888ac9cfd28068e062c991ebcff4b4c7307dc8dd4df9e728bedde5c449d91).
|
||||||
|
FROM alpine:3.18 AS final
|
||||||
|
|
||||||
|
# Create a non-privileged user that the app will run under.
|
||||||
|
# See https://docs.docker.com/go/dockerfile-user-best-practices/
|
||||||
|
ARG UID=10001
|
||||||
|
RUN adduser \
|
||||||
|
--disabled-password \
|
||||||
|
--gecos "" \
|
||||||
|
--home "/nonexistent" \
|
||||||
|
--shell "/sbin/nologin" \
|
||||||
|
--no-create-home \
|
||||||
|
--uid "${UID}" \
|
||||||
|
appuser
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Copy the executable from the "build" stage.
|
||||||
|
COPY --from=build /bin/server /bin/
|
||||||
|
|
||||||
|
# Expose the port that the application listens on.
|
||||||
|
EXPOSE 5894
|
||||||
|
|
||||||
|
# What the container should run when it is started.
|
||||||
|
CMD ["/bin/server", "/music"]
|
13
README.md
13
README.md
|
@ -22,6 +22,17 @@ All files (recursively) will be shuffled and played back. Log will be displayed
|
||||||
|
|
||||||
Look into `--help` for detailed info
|
Look into `--help` for detailed info
|
||||||
|
|
||||||
|
#### Run in Docker
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run -d \
|
||||||
|
--name lonelyradio \
|
||||||
|
--restart=unless-stopped \
|
||||||
|
-v /path/to/music:/music \
|
||||||
|
-p 5894:5894 \
|
||||||
|
ivabuz/lonelyradio:latest
|
||||||
|
```
|
||||||
|
|
||||||
#### Playlists
|
#### Playlists
|
||||||
|
|
||||||
Specify a directory with playlists with `--playlist-dir`. lonelyradio will scan them on startup and play them on clients’ requests.
|
Specify a directory with playlists with `--playlist-dir`. lonelyradio will scan them on startup and play them on clients’ requests.
|
||||||
|
@ -77,7 +88,7 @@ Look into `--help` for detailed info on usage.
|
||||||
|
|
||||||
[monolib](./monolib) provides a C API compatible with lonelyradio for creating custom clients.
|
[monolib](./monolib) provides a C API compatible with lonelyradio for creating custom clients.
|
||||||
|
|
||||||
The full protocol specification will be available later. If you would like to learn more about it now, please refer to the monolib.
|
[Protocol documentation] shortly describes the protocol used in lonelyradio. Please refer to monolib and verify custom clients with the reference lonelyradio server.
|
||||||
|
|
||||||
#### monolib API stability
|
#### monolib API stability
|
||||||
|
|
||||||
|
|
65
docs/protocol.md
Normal file
65
docs/protocol.md
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
# lonelyradio protocol (0.7)
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
The lonelyradio protocol is designed to be minimal yet functional for music streaming purposes.
|
||||||
|
|
||||||
|
The lonelyradio protocol operates at the application layer, establishing communication between the server and client. In its reference implementation, it runs atop the TCP protocol, but it could also be implemented on top of any other transport protocol, such as UDP, WebSocket, and so on.
|
||||||
|
|
||||||
|
The lonelyradio protocol uses [MessagePack](https://msgpack.org) to encode messages. Structures used in communication are defined in the `lonelyradio_types` crate.
|
||||||
|
|
||||||
|
## Establishing connection
|
||||||
|
|
||||||
|
1. The client sends a «hello» packet («lonelyra», 8 bytes)
|
||||||
|
1. The server checks the hello packet
|
||||||
|
2. The server sends «ServerCapabilities» which informs the client about supported audio encoders (raw pcm s16le must be supported by all server implementations)
|
||||||
|
3. Then the client picks one of the requests:
|
||||||
|
1. Play (p) (see example 1.1)
|
||||||
|
2. ListPlaylist (lpl) (see example 1.2)
|
||||||
|
3. PlayPlayList (ppl) (see example 1.3)
|
||||||
|
4. The server responds with one of RequestResult
|
||||||
|
1. Ok -> The server begins sending PlayMessage’s
|
||||||
|
1. TrackMetadata indicates the start of the new track
|
||||||
|
2. FragmentMetadata indicates the start of a new fragment and defines the number of bytes in it
|
||||||
|
1. FragmentMetadata is always followed by a fragment
|
||||||
|
2. Playlist is only returned on ListPlaylist and shows available playlists
|
||||||
|
3. Error indicates an error
|
||||||
|
|
||||||
|
To get «next track» just reestablish the connection.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Examples show JSON representation of MessagePack
|
||||||
|
|
||||||
|
### 1.1
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"p": { // e and co definicions could be found in lonelyradio_types crate
|
||||||
|
"e": "Pcm16",
|
||||||
|
"co": -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2
|
||||||
|
|
||||||
|
Just string encoded to MessagePack
|
||||||
|
|
||||||
|
```json
|
||||||
|
"lpl"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ppl": [
|
||||||
|
"someplaylist",
|
||||||
|
{
|
||||||
|
"e": "Pcm16",
|
||||||
|
"co": -1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
|
@ -103,7 +103,7 @@ pub enum Encoder {
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
pub struct FragmentMetadata {
|
pub struct FragmentMetadata {
|
||||||
// In bytes or samples, depends on encoder: Pcm* - samples, any compressed - bytes
|
// In bytes
|
||||||
#[serde(rename = "le")]
|
#[serde(rename = "le")]
|
||||||
pub length: u64,
|
pub length: u64,
|
||||||
|
|
||||||
|
|
40
src/main.rs
40
src/main.rs
|
@ -102,46 +102,18 @@ async fn stream_track(
|
||||||
let mut _samples = _samples.concat();
|
let mut _samples = _samples.concat();
|
||||||
|
|
||||||
match md.encoder {
|
match md.encoder {
|
||||||
Encoder::Pcm16 => {
|
Encoder::Pcm16
|
||||||
let _md = PlayMessage::F(FragmentMetadata {
|
| Encoder::PcmFloat
|
||||||
length: _samples.len() as u64 * 2,
|
| Encoder::Flac
|
||||||
magic_cookie: None,
|
| Encoder::Alac
|
||||||
});
|
| Encoder::Vorbis => {
|
||||||
if s.write_all(rmp_serde::to_vec(&_md).unwrap().as_slice()).is_err() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if s.write_all(
|
|
||||||
&encode(Encoder::Pcm16, _samples, md.sample_rate, md.channels).unwrap().0,
|
|
||||||
)
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Encoder::PcmFloat => {
|
|
||||||
let _md = PlayMessage::F(FragmentMetadata {
|
|
||||||
length: _samples.len() as u64 * 4,
|
|
||||||
magic_cookie: None,
|
|
||||||
});
|
|
||||||
if s.write_all(rmp_serde::to_vec(&_md).unwrap().as_slice()).is_err() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if s.write_all(
|
|
||||||
&encode(Encoder::PcmFloat, _samples, md.sample_rate, md.channels).unwrap().0,
|
|
||||||
)
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Encoder::Flac | Encoder::Alac | Encoder::Vorbis => {
|
|
||||||
let (encoded, magic_cookie) =
|
let (encoded, magic_cookie) =
|
||||||
encode(md.encoder, _samples, md.sample_rate, md.channels).unwrap();
|
encode(md.encoder, _samples, md.sample_rate, md.channels).unwrap();
|
||||||
let _md = PlayMessage::F(FragmentMetadata {
|
let _md = PlayMessage::F(FragmentMetadata {
|
||||||
length: encoded.as_slice().len() as u64,
|
length: encoded.as_slice().len() as u64,
|
||||||
magic_cookie,
|
magic_cookie,
|
||||||
});
|
});
|
||||||
if s.write_all(rmp_serde::to_vec(&_md).unwrap().as_slice()).is_err() {
|
if s.write_all(rmp_serde::to_vec_named(&_md).unwrap().as_slice()).is_err() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if s.write_all(encoded.as_slice()).is_err() {
|
if s.write_all(encoded.as_slice()).is_err() {
|
||||||
|
|
Loading…
Reference in a new issue