Sunday, 8 December 2019

Setting Up a Local Streaming Server

Ingredients

minidlnad
icecast2
ezstream
update-ezstream
ice2chrome

Description

minidlnad is a program to serve media files via DLNA protocols

icecast2 is the actual server that clients connect to to stream music

ezstream is the application which supplies media files to the icecast2 server

update-ezstream is an application which forces ezstream to re-read its source media files

ice2chrome is an application which pushes the icecast2 stream to a chromecast receiver

Method

Minidlnad Configuration

A modified version of minidlna can be found at www.vizier.uk.  This version supports configurable layouts.

[config]
unknown=Unknown
searchdepth=4
strippath=/disk/media/Media
[replace]
genre=language courses=audiobook
genre=podcast=audiobook
genre=speech=audiobook
genre=blues-rock=blues
genre=books + spoken=audiobook
genre=international=pop-rock
genre=pop/rock=pop-rock
artist=Jean Michel Jarre=Jean-Michel Jarre
artist=r -e-m=Rem
artist=r-e-m=Rem
[rule]
comment=Ignore Index Tracks for Audiobooks etc
chainset=none
mediatype=audio
or=genre=vocal,genre=radio,genre=audiobook
and=comment=index
[rule]
chainset=audiobook
mediatype=audio
or=genre=audiobook
[rule]
chainset=radio
mediatype=audio
or=genre=radio
[rule]
chainset=legacyradio
mediatype=audio
or=genre=vocal
[rule]
chainset=musicindex
mediatype=audio
and=comment=index
[rule]
chainset=music
mediatype=audio
[rule]
chainset=video
mediatype=video
[rule]
chainset=image
mediatype=image
[rule]
chainset=catchall
[catchall]
chain=/Catchall/$TITLE
[image]
chain=/Folder/$PATH/$FILENAME
chain=/Photo/Location/$LOCNSEW/ - All Photos - /$TITLE
chain=/Photo/Location/$LOCNSEW/$YEAR/$TITLE
chain=/Photo/Date/$DECADE/ - All Photos - /$TITLE
chain=/Photo/Date/$DECADE/$YEAR/$TITLE
chain=/Photo/ - All Photos - /$TITLE
[video]
chain=/Folder/$PATH/$FILENAME
chain=/Video/Date/$DECADE/ - All Videos - /$FILEBASE
chain=/Video/Date/$DECADE/$YEAR/$FILEBASE
chain=/Video/ - All Videos - /$FILEBASE
[music]
chain=/Folder/$PATH/$FILENAME
chain=/Music/Track/ - All Tracks - /$TITLE
chain=/Music/Track/$ABCTITLE/$TITLE
chain=/Music/Album/ - All Albums - /$ALBUM/$TRACKNUM. $TITLE
chain=/Music/Album/$ABCALBUM/$ALBUM/$TRACKNUM. $TITLE
chain=/Music/Artist/ - All Artists - /$ARTIST/By Album/$SALBUM - $TRACKNUM. $TITLE
chain=/Music/Artist/ - All Artists - /$ARTIST/Shuffle/$RND. $TITLE
chain=/Music/Artist/$ABCARTIST/$ARTIST/By Album/$SALBUM - $TRACKNUM. $TITLE
chain=/Music/Artist/$ABCARTIST/$ARTIST/Shuffle/$RND. $TITLE ($SALBUM)
chain=/Music/Date/$DECADE/$YEAR/$TITLE
chain=/Music/Date/$DECADE/ - All Tracks - /$TITLE
chain=/Genre/$GENRE/Track/ - All Tracks - /$TITLE
chain=/Genre/$GENRE/Track/$ABCTITLE/$TITLE
chain=/Genre/$GENRE/Album/ - All Albums - /$ALBUM/$TRACKNUM. $TITLE
chain=/Genre/$GENRE/Album/$ABCALBUM/$ALBUM/$TRACKNUM. $TITLE
chain=/Genre/$GENRE/Artist/ - All Artists - /$ARTIST/By Album/$SALBUM - $TRACKNUM. $TITLE
chain=/Genre/$GENRE/Artist/ - All Artists - /$ARTIST/Shuffle/$RND. $TITLE
chain=/Genre/$GENRE/Artist/$ABCARTIST/$ARTIST/By Album/$SALBUM - $TRACKNUM. $TITLE
chain=/Genre/$GENRE/Artist/$ABCARTIST/$ARTIST/Shuffle/$RND. $TITLE ($SALBUM)
chain=/Genre/$GENRE/Date/$DECADE/$YEAR/$TITLE
chain=/Genre/$GENRE/Date/$DECADE/ - All Tracks - /$TITLE
chain=/Search/Music/$SEARCHALBUMARTIST/$TITLE
chain=/Search/Music/$SEARCHCOMPOSER/$TITLE
chain=/Search/Music/$SEARCHALBUM/$TITLE
chain=/Search/Music/$SEARCHTITLE/$TITLE
 
[musicindex]
chain=/Folder/$PATH/$FILENAME
chain=/Music/Album/ - All Albums - /$ALBUM/$TRACKNUM. $TITLE
chain=/Music/Album/$ABCALBUM/$ALBUM/$TRACKNUM. $TITLE
chain=/Genre/$GENRE/Album/ - All Albums - /$ALBUM/$TRACKNUM. $TITLE
chain=/Genre/$GENRE/Album/$ABCALBUM/$ALBUM/$TRACKNUM. $TITLE
[audiobook]
chain=/Folder/$PATH/$FILENAME
chain=/Audiobook/Author/ - All Authors - /$ARTIST/$ALBUM/$0TRACKNUM. $TITLE
chain=/Audiobook/Author/$ABCARTIST/$ALBUM/$0TRACKNUM. $TITLE
chain=/Audiobook/Book/ - All Titles - /$ALBUM/$0TRACKNUM. $TITLE
chain=/Audiobook/Book/$ABCALBUM/$ALBUM/$0TRACKNUM. $TITLE
chain=/Search/Audiobook Book Title/$SEARCHALBUM/$ALBUM/$0TRACKNUM. $TITLE
chain=/Search/Audiobook Author/$SEARCHARTIST/$ARTIST/$ALBUM/$0TRACKNUM. $TITLE
chain=/Search/Audiobook Author/$SEARCHCOMPOSER/$0TRACKNUM. $TITLE
chain=/Search/Audiobook Chapter/$SEARCHALBUM/$ALBUM/$0TRACKNUM. $TITLE
[radio]
chain=/Folder/$PATH/$FILENAME
chain=/Radio/ - All Programmes - /$ALBUMARTIST/$ALBUM/$0TRACKNUM. $TITLE
chain=/Radio/$ABCALBUMARTIST/$ALBUMARTIST/ - All Series - /$0TRACKNUM. $TITLE
chain=/Radio/$ABCALBUMARTIST/$ALBUMARTIST/$ALBUM/$0TRACKNUM. $TITLE
chain=/Search/Radio Programme/$SEARCHALBUMARTIST/$ALBUMARTIST/$0TRACKNUM. $TITLE
chain=/Search/Radio Episode/$SEARCHTITLE/$0TRACKNUM. $TITLE

Icecast Configuration

Install standard icecast2, and make the following modifications:

/etc/icecast2/icecast.xml
<authentication>
  <source-password>PASS1234</source-password>
  <relay-password>PASS1234</relay-password>
  <admin-user>admin</admin-user>
  <admin-password>ADMINPASS</admin-password>
  </authentication>
<hostname>SERVERNAME</hostname>
<listen-socket>
  <port>8000</port>
  <shoutcast-mount>/channel1</shoutcast-mount>
  <shoutcast-mount>/channel2</shoutcast-mount>
  <shoutcast-mount>/channel3</shoutcast-mount>
  <shoutcast-mount>/channel4</shoutcast-mount>
  <shoutcast-mount>/channel5</shoutcast-mount>
  <shoutcast-mount>/channel6</shoutcast-mount>
  <shoutcast-mount>/channel7</shoutcast-mount>
  <shoutcast-mount>/channel8</shoutcast-mount>
  <shoutcast-mount>/channel9</shoutcast-mount>
</listen-socket>

Data File Area

Create an area for the media files and configuration.  For this example, it is assumed that these files are installed on a mounted disk in /disk/media

Create the following folders:
bin/
conf.dlna/
ezstream/bin
ezstream/cfg
ezstream/channel
Media/

EZStream Configuration Management

Create a configuration file for each stream you wish to have:

/disk/media/ezstream/cfg/N.cfg
<ezstream>
<url>http://localhost:8000/channelN</url>
<sourcepassword>PASS1234</sourcepassword>
<format>MP3</format>
<filename>/disk/media/ezstream/channel/N.m3u</filename>
<stream_once>0</stream_once>
<metadata_format>@s@</metadata_format>
<svrinfoname>Media Server</svrinfoname>
<svrinfourl>http://www.openserverproject.com</svrinfourl>
<svrinfogenre>Cantopop</svrinfogenre>
<svrinfodescription></svrinfodescription>
<svrinfobitrate>256</svrinfobitrate>
<svrinfochannels>2</svrinfochannels>
<svrinfosamplerate>44100</svrinfosamplerate>
<svrinfopublic>0</svrinfopublic>
</ezstream>

Create an m3u file in the channel folder for each stream.  Note that this is the file which you can update / change and then call update-ezstream.  Ensure the channel folder is writeable by the web server user.

/disk/media/ezstream/channel/N.m3u
/disk/media/Media/full/path/to/mp3/file1.mp3
/disk/media/Media/full/path/to/mp3/file2.mp3
Create a program to update the ezstream configuration based on configuration files in /disk/media/ezstream/cfg, and set its setuid/setgid bits such that other users can re-initialise the stream (most importantly, the web server user).

/disk/media/ezstream/bin/update-ezstream (.c)
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <linux/limits.h>
#include <libgen.h>
#include <signal.h>
#include <string.h>
int getprocesspid(char *procname, char *filter) ;
int main(int argc, char *argv[])
{
  int pid ;

  if (argc!=2 || argv[1][0]<'1' || argv[1][0]>'9') {
    printf("%s 1-9\n", argv[0]) ;
    exit(1) ;
  }

  // Find PID of running ezstream app
  char cfg[16] ;
  snprintf(cfg, sizeof(cfg), "/%c.xml", argv[1][0]) ;
  pid = getprocesspid("ezstream", cfg) ;
  // Change UID for ezstream user
  printf("Launched with uid: %d, ", getuid()) ;
  setreuid(geteuid(),geteuid()) ;
  printf("running with uid: %d\n", getuid()) ;
  // Send HUP signal, or launch server
  if (pid>0) {
    kill(pid, SIGHUP) ;
    return 0 ;
 
  } else {
 
    if (fork()==0) {
      char *thispath = dirname(argv[0]) ;
      char path[PATH_MAX+1] ;
      char *newargs[3] ;
   
      snprintf(path, sizeof(path), "%s/../cfg/%c.xml",
       thispath, argv[1][0]) ;
   
      newargs[0] = "/usr/local/bin/ezstream" ;
      newargs[1] = "-c" ;
      newargs[2] = path ;
      newargs[3] = NULL ;
   
      execv(newargs[0], newargs) ;
      printf("ERROR: Invalid program: %s %s %s\n",
          newargs[0], newargs[1], newargs[2]) ;
      return 1 ;
    }
    close(0) ;
    close(1) ;
    close(2) ;
    return 0 ;
  }
}
int getprocesspid(char *procname, char *filter)
{
  int pid=-1 ;
  char *args[5] ;
  args[0]="/bin/ps" ;
  args[1]="-fC" ;
  args[2]=procname ;
  args[3]=NULL ;
  int pipefd[2] ;
  pipe(pipefd) ;
  if (fork() == 0) {
    close(pipefd[0]) ;
    dup2(pipefd[1],1) ;
    close(pipefd[1]) ;
    char *args[5] ;
    args[0]="/bin/ps" ;
    args[1]="-fC" ;
    args[2]=procname ;
    args[3]=NULL ;
    execv(args[0], args) ;
    fprintf(stderr, "ERROR: Invalid program: %s %s %s\n",
          args[0], args[1], args[2]) ;
    exit(1) ;
  } else {
    char line[1024] ;
    int l=0 ;
    char ch ;
    int r ;
    close(pipefd[1]) ;
    do {
      if (r=read(pipefd[0], &ch, 1)) {
line[l++]=ch ;
line[l]='\0' ;
if (ch=='\n' || ch=='\r') {
  printf("line=%s\n", line) ;
  if (strstr(line, filter) != NULL) {
            sscanf(&line[9], "%d", &pid) ;
          }
          l=0 ;
}
      }
    } while (pid<0 && r==1 && l<sizeof(line)-1) ;
 
printf("\n pid=%d\n", pid) ;
    return pid ;
  }
}

Boot Configuration

Add a script to initialise the stream sources:

/disk/media/bin/init-stream-sources.sh
#!/bin/sh
/disk/media/ezstream/bin/update-ezstream 1
/disk/media/ezstream/bin/update-ezstream 2
/disk/media/ezstream/bin/update-ezstream 3
/disk/media/ezstream/bin/update-ezstream 4
/disk/media/ezstream/bin/update-ezstream 5
/disk/media/ezstream/bin/update-ezstream 6
/disk/media/ezstream/bin/update-ezstream 7
/disk/media/ezstream/bin/update-ezstream 8
/disk/media/ezstream/bin/update-ezstream 9
Make the following modifications to ensure the dlna media server and ezstream/shoutcast server starts up:

/etc/rc.local
/usr/local/sbin/minidlnad -f /disk/media/conf.dlna/minidlna.conf
/disk/media/bin/init-stream-sources.sh > /tmp/mediastreams.log 2>&1