├── .gitignore ├── spotify4steamlink.sh ├── icon2.png ├── consolas.ttf ├── spotify3.bmp ├── librespot-org-build └── Readme.md ├── Makefile ├── README.md └── testspriteminimal.c /.gitignore: -------------------------------------------------------------------------------- 1 | librespot-org-build/arm-unknown-linux-gnueabihf -------------------------------------------------------------------------------- /spotify4steamlink.sh: -------------------------------------------------------------------------------- 1 | ./testspriteminimal > /tmp/testspriteminimal.log 2>&1 -------------------------------------------------------------------------------- /icon2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slax57/spotify4steamlink/HEAD/icon2.png -------------------------------------------------------------------------------- /consolas.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slax57/spotify4steamlink/HEAD/consolas.ttf -------------------------------------------------------------------------------- /spotify3.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slax57/spotify4steamlink/HEAD/spotify3.bmp -------------------------------------------------------------------------------- /librespot-org-build/Readme.md: -------------------------------------------------------------------------------- 1 | Paste in the _librespot_ binaries **here**! 2 | 3 | The _librespot_ executable should be in the following location: `./arm-unknown-linux-gnueabihf/release/librespot` 4 | 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Source setenv.sh before running make 2 | # 3 | 4 | CFLAGS := -I$(MARVELL_ROOTFS)/usr/include/SDL2 5 | LDFLAGS := -lSDL2 -lSDL2_ttf 6 | 7 | testspriteminimal: testspriteminimal.o 8 | $(CC) -o $@ $< $(LDFLAGS) 9 | 10 | clean: 11 | $(RM) testspriteminimal.o 12 | 13 | distclean: clean 14 | $(RM) testspriteminimal 15 | $(RM) -r steamlink 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spotify4steamlink 2 | On this repo you will find a basic example showing how you can run [librespot](https://github.com/librespot-org/librespot) on the [steamlink](https://store.steampowered.com/app/353380/Steam_Link/). 3 | 4 | ## Disclaimer 5 | What you will find on this repo is just the result of some experiments I made to get something that fitted my own needs concerning spotify and the steamlink. It might not necessarily fit yours. I am just sharing this code in case some people happen to have the same exact needs as me, or are looking after some technical information about how you can pipe the librespot audio backend to another process. 6 | 7 | As such, note that: 8 | * The code is really not optimised nor modular. You might have to refactor it to fit your needs. 9 | * I may try to improve it in the future, mainly to add some configuration variables, or maybe to add some features like displaying the current track and so... But please note that I only do this on my free time, so this might come late (if ever), and for now I do not intend to offer much support for what I posted. As I said earlier, this is just an experiment I made for myself, and I just thought it might be useful to others if I shared it here :) 10 | * Using _librespot_ to connect to Spotify's API is probably forbidden by them. Use at your own risk. 11 | 12 | ## Prerequisite 13 | * This example is based on the open source unofficial spotify client [librespot](https://github.com/librespot-org/librespot). You will have to get it and build it prior to using this app. Although I give some tips about the cross-compilation parameters I used, if you need support for building you need to address your issues to them directly. 14 | * _librespot_ requires a Premium Spotify account. 15 | * This app needs the [steamlink SDK](https://github.com/ValveSoftware/steamlink-sdk) to build. Download it and get familiar with it first. 16 | 17 | ## Functionalities 18 | This code allows to build an app that you can install and run on your _steamlink_. Once it is installed, you can launch it directly from the _steamlink_ home menu. 19 | This app starts _librespot_ as a background program, and pipes the audio to the first audio device it finds. 20 | You juste have to use _spotify connect_ (from your phone or a computer, using the official Spotify application) to start a song. 21 | Once you're done, you can exit the app by pressing any key on your keyboard or any buttons on your gamepad (should be compatible whith the Steam gamepad). 22 | 23 | **Be aware that this is all the app does for now!** Since the latest version, the librespot logs are displayed, allowing you to know which track is being played. But still, there is no complex GUI with album cover or so, it's basically just a headless player with a very simple interface. 24 | 25 | # Instructions 26 | ## Building 27 | ### librespot 28 | Follow [librespot cross compiling instructions](https://github.com/librespot-org/librespot/wiki/Cross-compiling) provided on their wiki. 29 | The target architecture `arm-unknown-linux-gnueabihf` is compatible with the _steamlink_. 30 | 31 | **Before you use the docker image to build librespot**, you need to apply the patch described [here](https://github.com/librespot-org/librespot/wiki/Compile-librespot-for-kernel-prior-3.9). 32 | This will enable the _discovery_ feature, allowing you to launch librespot without having to provide your Spotify account credentials. 33 | 34 | Once that _mDNS_ is patched, build the _docker_ image as suggested: 35 | ```Shell 36 | docker build -t librespot-cross -f contrib/Dockerfile . 37 | ``` 38 | 39 | Then, use this line to build for the correct architecture: 40 | ```Shell 41 | docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features 42 | ``` 43 | 44 | ### spotify4steamlink 45 | The sources of this app are meant to be placed in the `examples` folder of the **steamlink sdk**. 46 | They contain a `build_steamlink.sh` script that takes care of the cross compilation for you. 47 | 48 | You need to paste the _librespot_ binaries you juste compiled in the `librespot-org-build` folder of this repo. 49 | 50 | Then you can now launch 51 | ```Shell 52 | ./build_steamlink.sh 53 | ``` 54 | 55 | If the build succeeds, it should generate this file in your current directory: 56 | ``` 57 | steamlink/apps/spotify4steamlink.tgz 58 | ``` 59 | 60 | ## Installation 61 | Now we are going to install _spotify4steamlink_ on the _steamlink_. 62 | 63 | Copy the `steamlink` folder that was created during the build step to a USB flash drive. 64 | Insert it into the Steam Link and power cycle the device. 65 | 66 | After the reboot, you should see a nice **Spotify** icon next to your computer(s)! :) 67 | Start the app, and you're ready to play some Spotify tunes directly from your _steamlink_! 68 | 69 | # Troubleshooting 70 | By default the app is configured to redirect _librespot_ logs into this file: 71 | ``` 72 | /tmp/spotify.log 73 | ``` 74 | If you have trouble using _spotify4steamlink_, you should start by having a look at this file ;). 75 | 76 | You might also encounter troubles when exiting the app. For now, the only workaround is to unplug the _steamlink_, and then plug it back again. 77 | Still, during my tests, I noticed the problem seems **not to happen** if you exit **while playing a song**. 78 | Hopefully having this knowledge will prevent you troubles :). 79 | 80 | # Acknowledgements 81 | * [plietar](https://github.com/plietar/) for the original [librespot](https://github.com/plietar/librespot) project 82 | * The [librespot-org](https://github.com/librespot-org/librespot) team, for their great work and maintenance of the current implementation 83 | * The [steamlink sdk](https://github.com/ValveSoftware/steamlink-sdk/) which, although not enough documented for my taste, still offers a good start to enhance you _steamlink_ device 84 | * [This example](https://gist.github.com/armornick/3447121) on how to play audio with SDL 85 | -------------------------------------------------------------------------------- /testspriteminimal.c: -------------------------------------------------------------------------------- 1 | /* 2 | Simple program to help with running librespot and handling audio. 3 | What it does : 4 | - Display a simple background image (containing usage instructions) 5 | - Start librespot program with a pipe to handle audio data 6 | - Feed the audio data to SDL audio component 7 | - Stop librespot program when exiting 8 | 9 | Many thanks to the librespot-org project: https://github.com/librespot-org/librespot 10 | */ 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | #ifdef __EMSCRIPTEN__ 17 | #include 18 | #endif 19 | 20 | #include "SDL.h" 21 | #include "SDL_ttf.h" 22 | 23 | #define INITIAL_WINDOW_WIDTH 1280 24 | #define INITIAL_WINDOW_HEIGHT 720 25 | 26 | #define AUDIO_SAMPLES 4096 27 | 28 | #define LIBRESPOT_START_CMD "/home/apps/spotify4steamlink/librespot-org-build/arm-unknown-linux-gnueabihf/release/librespot --cache /var/cache --disable-audio-cache --name steamlink --bitrate 320 --initial-volume 85 --backend pipe 2>/tmp/spotify.log" 29 | #define LIBRESPOT_KILL_CMD "pidof /home/apps/spotify4steamlink/librespot-org-build/arm-unknown-linux-gnueabihf/release/librespot | xargs kill" 30 | #define LIBRESPOT_LOG_FILE "/tmp/spotify.log" 31 | 32 | #define BACKGROUND_BMP_FILE "spotify3.bmp" 33 | 34 | #define FONT_FILE "consolas.ttf" 35 | #define FONT_SIZE 18 36 | 37 | // Disable this flag when publishing to steamlink 38 | //#define TEST_MODE 39 | 40 | 41 | /* -- Global variables -- */ 42 | 43 | static SDL_Texture *sprite; 44 | static int sprite_w, sprite_h; 45 | SDL_Renderer *renderer; 46 | int done; 47 | static FILE *audio_buf; // global pointer to the audio buffer to be played 48 | TTF_Font *font = NULL; 49 | static FILE *spotify_log_file; 50 | SDL_Color text_color = {255, 255, 255}; // white 51 | SDL_Surface *text_surf = NULL; 52 | static int text_square_pos_x, text_square_pos_y, text_square_pos_w, text_square_pos_h; 53 | 54 | 55 | /* -- Functions -- */ 56 | 57 | /* Call this instead of exit(), so we can clean up SDL: atexit() is evil. */ 58 | static void 59 | quit(int rc) 60 | { 61 | exit(rc); 62 | } 63 | 64 | void compute_text_square_dimensions(SDL_Window* window) 65 | { 66 | int window_w, window_h; 67 | SDL_GetWindowSize(window, &window_w, &window_h); 68 | // Converting dimensions from a 1280*720 screen if necessary 69 | text_square_pos_x = 455 * window_w / 1280; 70 | text_square_pos_y = 84 * window_h / 720; 71 | text_square_pos_w = 700 * window_w / 1280; 72 | text_square_pos_h = 555 * window_h / 720; 73 | } 74 | 75 | int 76 | LoadSprite(char *file, SDL_Renderer *renderer) 77 | { 78 | SDL_Surface *temp; 79 | 80 | /* Load the sprite image */ 81 | temp = SDL_LoadBMP(file); 82 | if (temp == NULL) { 83 | SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't load %s: %s\n", file, SDL_GetError()); 84 | return (-1); 85 | } 86 | sprite_w = temp->w; 87 | sprite_h = temp->h; 88 | 89 | /* Create textures from the image */ 90 | sprite = SDL_CreateTextureFromSurface(renderer, temp); 91 | if (!sprite) { 92 | SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't create texture: %s\n", SDL_GetError()); 93 | SDL_FreeSurface(temp); 94 | return (-1); 95 | } 96 | SDL_FreeSurface(temp); 97 | 98 | /* We're ready to roll. :) */ 99 | return (0); 100 | } 101 | 102 | // Read the tail of the log file 103 | char* tailLogFile() 104 | { 105 | long lSize; 106 | char * buffer; 107 | size_t result; 108 | 109 | // obtain file size: 110 | fseek (spotify_log_file , 0 , SEEK_END); 111 | lSize = ftell (spotify_log_file); 112 | rewind (spotify_log_file); 113 | 114 | // allocate memory to contain the whole file: 115 | buffer = (char*) malloc (sizeof(char)*lSize); 116 | if (buffer == NULL) {quit(2);} 117 | 118 | // copy the file into the buffer: 119 | result = fread (buffer,1,lSize,spotify_log_file); 120 | if (result != lSize) {quit(3);} 121 | 122 | return buffer; 123 | } 124 | 125 | // Count the number of lines in log file 126 | int logLinesCount() 127 | { 128 | rewind (spotify_log_file); 129 | 130 | int count = 0; 131 | char ch; 132 | 133 | while(!feof(spotify_log_file)) 134 | { 135 | ch = fgetc(spotify_log_file); 136 | if(ch == '\n') 137 | { 138 | count++; 139 | } 140 | } 141 | 142 | return count; 143 | } 144 | 145 | // Render the text surface 146 | void 147 | renderText(SDL_Renderer * renderer) 148 | { 149 | // Write our text into the surface 150 | char* buffer = tailLogFile(); 151 | text_surf = TTF_RenderUTF8_Blended_Wrapped(font, buffer, text_color, text_square_pos_w); 152 | free(buffer); 153 | if (text_surf == NULL) { 154 | SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't create text surface from char buffer: %s\n", SDL_GetError()); 155 | return; 156 | } 157 | // Create texture from the surface 158 | SDL_Texture *sprite = SDL_CreateTextureFromSurface(renderer, text_surf); 159 | if (!sprite) { 160 | SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't create texture from text surface: %s\n", SDL_GetError()); 161 | SDL_FreeSurface(text_surf); 162 | quit (-1); 163 | } 164 | // Set the position and size of source and destination rectangles 165 | int log_lines = logLinesCount(); 166 | int actual_w = text_surf->w; 167 | int actual_h = (text_surf->h < text_square_pos_h) ? text_surf->h : text_square_pos_h; 168 | int actual_y = ((text_surf->h - (log_lines * 2)) < text_square_pos_h) ? 0 : ((text_surf->h - (log_lines * 2)) - text_square_pos_h); 169 | SDL_Rect *srcrect = (SDL_Rect *)malloc(sizeof(SDL_Rect)); 170 | srcrect->x = 0; 171 | srcrect->y = actual_y; 172 | srcrect->w = actual_w; 173 | srcrect->h = actual_h; 174 | SDL_Rect *dstrect = (SDL_Rect *)malloc(sizeof(SDL_Rect)); 175 | dstrect->x = text_square_pos_x; 176 | dstrect->y = text_square_pos_y; 177 | dstrect->w = actual_w; 178 | dstrect->h = actual_h; 179 | // Blit the text onto the screen 180 | SDL_RenderCopy(renderer, sprite, srcrect, dstrect); 181 | // Free memory 182 | SDL_FreeSurface(text_surf); 183 | SDL_DestroyTexture(sprite); 184 | free(dstrect); 185 | free(srcrect); 186 | } 187 | 188 | // Render a sprite fullscreen 189 | void 190 | renderBackground(SDL_Renderer * renderer, SDL_Texture * sprite) 191 | { 192 | /* Draw a dark background */ 193 | SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0xFF); 194 | SDL_RenderClear(renderer); 195 | 196 | /* Blit the sprite onto the screen */ 197 | SDL_RenderCopy(renderer, sprite, NULL, NULL); 198 | } 199 | 200 | // audio callback function 201 | // Read fifo generated by spotify and add it to the audio queue 202 | void my_audio_callback(void *userdata, Uint8 *dest_stream, int len) { 203 | Sint16 *stream = (Sint16 *)dest_stream; 204 | int actual_samples_read = fread(stream, sizeof(Uint8), len, audio_buf); 205 | if (actual_samples_read < len) { 206 | int i; 207 | for (i=actual_samples_read; i