Refactor transcode.c so it can actually transcode + use new capability to support mp3 streaming.

Also includes the skeleton for perhaps supporting video in the future. Adds more fine-grained
ffmpeg/libav compability checks. Dependency on libavresample/libswresample exchanged with
dependency on libavfilter, which seems more versatile.
This commit is contained in:
ejurgensen 2015-10-09 23:58:27 +02:00
parent da3d32902e
commit 2a610812a5
16 changed files with 2180 additions and 810 deletions

View File

@ -4,8 +4,8 @@ forked-daapd is a Linux/FreeBSD DAAP (iTunes), MPD (Music Player Daemon) and
RSP (Roku) media server.
It has support for AirPlay devices/speakers, Apple Remote (and compatibles),
MPD clients, internet radio, Spotify and LastFM. It does not support AirPlay
video.
MPD clients, network streaming, internet radio, Spotify and LastFM. It does not
support AirPlay video.
DAAP stands for Digital Audio Access Protocol, and is the protocol used
by iTunes and friends to share/stream media libraries over the network.
@ -31,6 +31,7 @@ forked-daapd is a complete rewrite of mt-daapd (Firefly Media Server).
- [Using Remote](#using-remote)
- [AirPlay devices/speakers](#airplay-devicesspeakers)
- [Local audio output](#local-audio-output)
- [MP3 network streaming (streaming to iOS)](#MP3-network-streaming-(streaming-to-iOS))
- [Supported formats](#supported-formats)
- [Streaming MPEG4](#streaming-mpeg4)
- [Playlists and internet radio](#playlists-and-internet-radio)
@ -62,8 +63,9 @@ forked-daapd supports these kinds of clients:
- DAAP clients, like iTunes or Rhythmbox
- Remote clients, like Apple Remote or compatibles for Android/Windows Phone
- AirPlay devices, like AirPort Express, Shairport and various AirPlay speakers
- RSP clients, like Roku Soundbridge
- MPD clients, like mpc (see [mpd-clients](#mpd-clients))
- MP3 network stream clients, like VLC and almost any other music player
- RSP clients, like Roku Soundbridge
Like iTunes, you can control forked-daapd with Remote and stream your music
to AirPlay devices.
@ -76,14 +78,14 @@ probably obsolete when you read it :-)
| Client | Developer | Type | Platform | Working (vers.) |
| ------------------------ | ---------- | ------ | ------------- | --------------- |
| iTunes | Apple | DAAP | Win, OSX | Yes (12.1) |
| iTunes | Apple | DAAP | Win, OSX | Yes (12.3) |
| Rhythmbox | Gnome | DAAP | Linux | Yes |
| WinAmp DAAPClient | WardFamily | DAAP | WinAmp | Yes |
| Amarok w/DAAP plugin | KDE | DAAP | Linux/Win | Yes (2.8.0) |
| Banshee | | DAAP | Linux/Win/OSX | No (2.6.2) |
| jtunes4 | | DAAP | Java | No |
| Firefly Client | | (DAAP) | Java | No |
| Remote | Apple | Remote | iOS | Yes (4.2.1) |
| Remote | Apple | Remote | iOS | Yes (4.2.2) |
| Retune | SquallyDoc | Remote | Android | Yes (3.5.23) |
| TunesRemote+ | Melloware | Remote | Android | Yes (2.5.3) |
| Remote for iTunes | Hyperfine | Remote | Android | Yes |
@ -146,7 +148,7 @@ local path. See [Libraries on network mounts](#libraries-on-network-mounts).
#### You did not enter the correct name or pairing code
You will see an error in the log about pairing failure with a HTTP response code
that is not 0.
that is *not* 0.
Solution: Copy-paste the name to be sure to get specials characters right. You
can also try the pairinghelper script located in the scripts-folder of the
source.
@ -158,8 +160,10 @@ If you see an error in the log with either:
it means that forked-daapd could not establish a connection to Remote. This
might be a network issue.
Solution: Sometimes it resolves the issue if you force Remote to quit, restart
it and do the pairing proces again. Otherwise try using avahi-browse for
troubleshooting:
it and do the pairing proces again. Another trick is to establish some other
connection (eg SSH) from the iPod/iPhone/iPad to the host.
Otherwise try using avahi-browse for troubleshooting:
- in a terminal, run `avahi-browse -r -k _touch-remote._tcp`
- start Remote, goto Settings, Add Library
- after a couple seconds at most, you should get something similar to this:
@ -214,10 +218,29 @@ audio device:
OSS4.
## MP3 network streaming (streaming to iOS)
You can listen to audio being played by forked-daapd by opening this network
stream address in pretty much any music player:
http://[your hostname/ip address]:3689/stream.mp3
This is currently the only way of listening to your audio on iOS devices, since
Apple does not allow AirPlay receiver apps, and because Apple Home Sharing
cannot be supported by forked-daapd. So what you can do instead is install a
music player app like VLC, connect to the stream and control playback with
Remote. You can also use MPoD in "On the go"-mode, where control and playback is
integrated in one app (see (#mpd-clients)).
Note that MP3 encoding must be supported by ffmpeg/libav for this to work. If
it is not available you will see a message in the log file. In Debian/Ubuntu you
get MP3 encoding support by installing the package "libavcodec-extra".
## Supported formats
forked-daapd should support pretty much all media formats. It relies on libav
(ffmpeg) to extract metadata and decode the files on the fly when the client
(or ffmpeg) to extract metadata and decode the files on the fly when the client
doesn't support the format.
Formats are attributed a code, so any new format will need to be explicitely
@ -338,6 +361,9 @@ configuration file a rescan is required before the new setting will take effect.
Currently, this will not be done automatically, so you need to trigger the
rescan as described below.
### Symlinks and pipes
Symlinks are supported and dereferenced. This does interact in tricky ways
with the above monitoring and rescanning, so you've been warned. Changes to
symlinks themselves won't be taken into account, or not the way you'd expect.
@ -395,15 +421,15 @@ not necessary during normal operation.
forked-daapd is meant to be used with the clients mentioned above, so it does
not have a command line interface nor does it have a web interface. You can,
however, to some extent control forked-daapd with [MPD clients](#mpd-clients) or
from the command line by issuing DAAP/DACP commands with a program like curl. Here
is an example of how to do that.
from the command line by issuing DAAP/DACP commands with a program like curl.
Here is an example of how to do that.
Say you have a playlist with a radio station, and you want to make a script that
starts playback of that station:
1. Run 'sqlite3 [your forked-daapd db]'. Use 'select id,title from files' to get
the id of the radio station, and use 'select id,title from playlists' to get the
id of the playlist.
the id of the radio station, and use 'select id,title from playlists' to get
the id of the playlist.
2. Convert the two ids to hex.
3. Put the following lines in the script with the relevant ids inserted (also
observe that you must use a session-id < 100, and that you must login and
@ -420,9 +446,9 @@ curl "http://localhost:3689/logout?session-id=50"
## Spotify
forked-daapd has *some* support for Spotify. It must be compiled with the
`--enable-spotify option` (see [INSTALL](INSTALL)). You must have also have libspotify
installed, otherwise the Spotify integration will not be available. You can
get libspotify here:
`--enable-spotify option` (see [INSTALL](INSTALL)). You must have also have
libspotify installed, otherwise the Spotify integration will not be available.
You can get libspotify here:
- Original (binary) tar.gz, see https://developer.spotify.com
- Debian package (libspotify-dev), see https://apt.mopidy.com

View File

@ -83,25 +83,32 @@ AC_RUN_IFELSE(
AC_LANG_POP([C])
LIBS="$save_LIBS"
PKG_CHECK_EXISTS([ libavcodec >= 54.35 ],
[PKG_CHECK_EXISTS([ libswresample ],
libxxresample=libswresample;
,
libxxresample=libavresample;
)]
)
if test x$libxxresample = xlibswresample; then
AC_DEFINE(HAVE_LIBSWRESAMPLE, 1, [Define to 1 if you have ffmpeg's libswresample])
elif test x$libxxresample = xlibavresample; then
AC_DEFINE(HAVE_LIBAVRESAMPLE, 1, [Define to 1 if you have libav's libavresample])
fi
PKG_CHECK_MODULES(LIBAV, [ libavformat libavcodec libswscale libavutil $libxxresample ])
PKG_CHECK_MODULES(LIBAV, [ libavformat libavcodec libswscale libavutil libavfilter ])
dnl Checks for misc libav and ffmpeg API differences
save_LIBS="$LIBS"
AC_CHECK_LIB([avcodec], [avcodec_find_best_pix_fmt_of_list],
AC_DEFINE(HAVE_FFMPEG, 1, [Define to 1 if you have ffmpeg (and not libav).]))
AC_DEFINE(HAVE_FFMPEG, 1, [Define to 1 if you have ffmpeg/libav with avcodec_find_best_pix_fmt_of_list]))
AC_CHECK_LIB([avfilter], [av_buffersrc_add_frame_flags],
AC_DEFINE(HAVE_LIBAV_BUFFERSRC_ADD_FRAME_FLAGS, 1, [Define to 1 if you have ffmpeg/libav with av_buffersrc_add_frame_flags]))
AC_CHECK_LIB([avfilter], [av_buffersink_get_frame],
AC_DEFINE(HAVE_LIBAV_BUFFERSINK_GET_FRAME, 1, [Define to 1 if you have ffmpeg/libav with av_buffersink_get_frame]))
AC_CHECK_LIB([avfilter], [avfilter_graph_parse_ptr],
AC_DEFINE(HAVE_LIBAV_GRAPH_PARSE_PTR, 1, [Define to 1 if you have ffmpeg/libav with avfilter_graph_parse_ptr]))
AC_CHECK_LIB([avcodec], [av_copy_packet],
AC_DEFINE(HAVE_LIBAV_COPY_PACKET, 1, [Define to 1 if you have ffmpeg/libav with av_copy_packet]))
AC_CHECK_LIB([avcodec], [av_packet_rescale_ts],
AC_DEFINE(HAVE_LIBAV_PACKET_RESCALE_TS, 1, [Define to 1 if you have ffmpeg/libav with av_packet_rescale_ts]))
AC_CHECK_LIB([avformat], [avformat_alloc_output_context2],
AC_DEFINE(HAVE_LIBAV_ALLOC_OUTPUT_CONTEXT2, 1, [Define to 1 if you have ffmpeg/libav with avformat_alloc_output_context2]))
AC_CHECK_LIB([avutil], [av_frame_alloc],
AC_DEFINE(HAVE_LIBAV_FRAME_ALLOC, 1, [Define to 1 if you have ffmpeg/libav with av_frame_alloc]))
AC_CHECK_LIB([avutil], [av_frame_get_best_effort_timestamp],
AC_DEFINE(HAVE_LIBAV_BEST_EFFORT_TIMESTAMP, 1, [Define to 1 if you have ffmpeg/libav with av_frame_get_best_effort_timestamp]))
AC_CHECK_HEADERS([libavutil/channel_layout.h])
AC_CHECK_HEADERS([libavutil/mathematics.h])
LIBS="$save_LIBS"
PKG_CHECK_MODULES(MINIXML, [ mxml ])

View File

@ -128,11 +128,18 @@ library {
# Should iTunes metadata override ours?
# itunes_overrides = false
# Decoding options for DAAP clients
# Since iTunes has native support for mpeg, mp4a, mp4v, alac and wav,
# such files will be sent as they are. Any other formats will be decoded
# to raw wav. If forked-daapd detects a non-iTunes DAAP client, it is
# assumed to only support mpeg and wav, other formats will be decoded.
# Here you can change when to decode. Note that these settings have no
# effect on AirPlay.
# Formats: mp4a, mp4v, mpeg, alac, flac, mpc, ogg, wma, wmal, wmav, aif, wav
# Formats that should never be transcoded
# no_transcode = { "alac", "mp4a" }
# Formats that should always be transcoded
# force_transcode = { "ogg", "flac" }
# Formats that should never be decoded
# no_decode = { "format", "format" }
# Formats that should always be decoded
# force_decode = { "format", "format" }
}
# Local audio output

View File

@ -92,6 +92,7 @@ forked_daapd_SOURCES = main.c \
httpd_rsp.c httpd_rsp.h \
httpd_daap.c httpd_daap.h \
httpd_dacp.c httpd_dacp.h \
httpd_streaming.c httpd_streaming.h \
http.c http.h \
dmap_common.c dmap_common.h \
transcode.c transcode.h \

View File

@ -83,8 +83,8 @@ static cfg_opt_t sec_library[] =
CFG_STR_LIST("filepath_ignore", NULL, CFGF_NONE),
CFG_BOOL("filescan_disable", cfg_false, CFGF_NONE),
CFG_BOOL("itunes_overrides", cfg_false, CFGF_NONE),
CFG_STR_LIST("no_transcode", NULL, CFGF_NONE),
CFG_STR_LIST("force_transcode", NULL, CFGF_NONE),
CFG_STR_LIST("no_decode", NULL, CFGF_NONE),
CFG_STR_LIST("force_decode", NULL, CFGF_NONE),
CFG_END()
};

93
src/ffmpeg-compat.c Normal file
View File

@ -0,0 +1,93 @@
#ifdef HAVE_LIBAVUTIL_CHANNEL_LAYOUT_H
# include <libavutil/channel_layout.h>
#endif
#ifdef HAVE_LIBAVUTIL_MATHEMATICS_H
# include <libavutil/mathematics.h>
#endif
#ifndef HAVE_FFMPEG
# define avcodec_find_best_pix_fmt_of_list(a, b, c, d) avcodec_find_best_pix_fmt2((enum AVPixelFormat *)(a), (b), (c), (d))
#endif
#ifndef HAVE_LIBAV_COPY_PACKET
# define av_copy_packet(dst, src) memcpy(dst, src, sizeof(AVPacket))
#endif
#ifndef HAVE_LIBAV_FRAME_ALLOC
# define av_frame_alloc() avcodec_alloc_frame()
# define av_frame_free(x) avcodec_free_frame((x))
#endif
#ifndef HAVE_LIBAV_BEST_EFFORT_TIMESTAMP
# define av_frame_get_best_effort_timestamp(x) (x)->pts
#endif
#ifndef HAVE_LIBAV_PACKET_RESCALE_TS
void av_packet_rescale_ts(AVPacket *pkt, AVRational src_tb, AVRational dst_tb)
{
if (pkt->pts != AV_NOPTS_VALUE)
pkt->pts = av_rescale_q(pkt->pts, src_tb, dst_tb);
if (pkt->dts != AV_NOPTS_VALUE)
pkt->dts = av_rescale_q(pkt->dts, src_tb, dst_tb);
if (pkt->duration > 0)
pkt->duration = av_rescale_q(pkt->duration, src_tb, dst_tb);
if (pkt->convergence_duration > 0)
pkt->convergence_duration = av_rescale_q(pkt->convergence_duration, src_tb, dst_tb);
}
#endif
#ifndef HAVE_LIBAV_ALLOC_OUTPUT_CONTEXT2
int avformat_alloc_output_context2(AVFormatContext **avctx, AVOutputFormat *oformat,
const char *format, const char *filename)
{
AVFormatContext *s = avformat_alloc_context();
int ret = 0;
*avctx = NULL;
if (!s)
goto nomem;
if (!oformat) {
if (format) {
oformat = av_guess_format(format, NULL, NULL);
if (!oformat) {
av_log(s, AV_LOG_ERROR, "Requested output format '%s' is not a suitable output format\n", format);
ret = AVERROR(EINVAL);
goto error;
}
} else {
oformat = av_guess_format(NULL, filename, NULL);
if (!oformat) {
ret = AVERROR(EINVAL);
av_log(s, AV_LOG_ERROR, "Unable to find a suitable output format for '%s'\n",
filename);
goto error;
}
}
}
s->oformat = oformat;
if (s->oformat->priv_data_size > 0) {
s->priv_data = av_mallocz(s->oformat->priv_data_size);
if (!s->priv_data)
goto nomem;
if (s->oformat->priv_class) {
*(const AVClass**)s->priv_data= s->oformat->priv_class;
av_opt_set_defaults(s->priv_data);
}
} else
s->priv_data = NULL;
if (filename)
snprintf(s->filename, sizeof(s->filename), "%s", filename);
*avctx = s;
return 0;
nomem:
av_log(s, AV_LOG_ERROR, "Out of memory\n");
ret = AVERROR(ENOMEM);
error:
avformat_free_context(s);
return ret;
}
#endif

View File

@ -51,6 +51,7 @@
#include "httpd_rsp.h"
#include "httpd_daap.h"
#include "httpd_dacp.h"
#include "httpd_streaming.h"
#include "transcode.h"
/*
@ -126,6 +127,7 @@ static pthread_t tid_httpd;
struct stream_ctx *g_st;
#endif
static void
stream_end(struct stream_ctx *st, int failed)
{
@ -447,8 +449,8 @@ httpd_stream_file(struct evhttp_request *req, int id)
stream_cb = stream_chunk_xcode_cb;
ret = transcode_setup(&st->xcode, mfi, &st->size, 1);
if (ret < 0)
st->xcode = transcode_setup(mfi, XCODE_PCM16_HEADER, &st->size);
if (!st->xcode)
{
DPRINTF(E_WARN, L_HTTPD, "Transcoding setup failed, aborting streaming\n");
@ -1034,6 +1036,12 @@ httpd_gen_cb(struct evhttp_request *req, void *arg)
{
dacp_request(req);
goto out;
}
else if (streaming_is_request(req, uri))
{
streaming_request(req);
goto out;
}
@ -1311,6 +1319,8 @@ httpd_init(void)
goto dacp_fail;
}
streaming_init();
#ifdef USE_EVENTFD
exit_efd = eventfd(0, EFD_CLOEXEC);
if (exit_efd < 0)
@ -1387,6 +1397,7 @@ httpd_init(void)
close(exit_pipe[1]);
#endif
pipe_fail:
streaming_deinit();
dacp_deinit();
dacp_fail:
daap_deinit();
@ -1432,6 +1443,7 @@ httpd_deinit(void)
return;
}
streaming_deinit();
rsp_deinit();
dacp_deinit();
daap_deinit();

417
src/httpd_streaming.c Normal file
View File

@ -0,0 +1,417 @@
/*
* Copyright (C) 2015 Espen Jürgensen <espenjurgensen@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <uninorm.h>
#include <unistd.h>
#include <event2/event.h>
#include <event2/buffer.h>
#include <event2/http.h>
#include "logger.h"
#include "conffile.h"
#include "transcode.h"
#include "player.h"
#include "listener.h"
#include "httpd.h"
#include "httpd_streaming.h"
/* httpd event base, from httpd.c */
extern struct event_base *evbase_httpd;
// Seconds between sending silence when player is idle
// (to prevent client from hanging up)
#define STREAMING_SILENCE_INTERVAL 1
// Buffer size for transmitting from player to httpd thread
#define STREAMING_RAWBUF_SIZE (STOB(AIRTUNES_V2_PACKET_SAMPLES))
// Should prevent that we keep transcoding to dead connections
#define STREAMING_CONNECTION_TIMEOUT 60
// Linked list of Icecast requests
struct streaming_session {
struct evhttp_request *req;
struct streaming_session *next;
};
static struct streaming_session *streaming_sessions;
static int streaming_initialized;
// Buffers and interval for sending silence when playback is paused
static uint8_t *streaming_silence_data;
static size_t streaming_silence_size;
static struct timeval streaming_silence_tv = { STREAMING_SILENCE_INTERVAL, 0 };
// Input buffer, output buffer and encoding ctx for transcode
static uint8_t streaming_rawbuf[STREAMING_RAWBUF_SIZE];
static struct encode_ctx *streaming_encode_ctx;
static struct evbuffer *streaming_encoded_data;
// Used for pushing events and data from the player
static struct event *streamingev;
static struct player_status streaming_player_status;
static int streaming_player_changed;
static int streaming_pipe[2];
static void
streaming_fail_cb(struct evhttp_connection *evcon, void *arg)
{
struct streaming_session *this;
struct streaming_session *session;
struct streaming_session *prev;
this = (struct streaming_session *)arg;
DPRINTF(E_WARN, L_STREAMING, "Connection failed; stopping mp3 streaming to client\n");
prev = NULL;
for (session = streaming_sessions; session; session = session->next)
{
if (session->req == this->req)
break;
prev = session;
}
if (!prev)
streaming_sessions = session->next;
else
prev->next = session->next;
free(session);
if (!streaming_sessions)
{
DPRINTF(E_INFO, L_STREAMING, "No more clients, will stop streaming\n");
event_del(streamingev);
player_streaming_stop();
}
}
static void
streaming_send_cb(evutil_socket_t fd, short event, void *arg)
{
struct streaming_session *session;
struct evbuffer *evbuf;
struct decoded_frame *decoded;
uint8_t *buf;
int len;
int ret;
if (!streaming_sessions)
return;
// Callback from player (EV_READ)
if (event & EV_READ)
{
ret = read(streaming_pipe[0], &streaming_rawbuf, STREAMING_RAWBUF_SIZE);
if (ret < 0)
return;
decoded = transcode_raw2frame(streaming_rawbuf, STREAMING_RAWBUF_SIZE);
if (!decoded)
{
DPRINTF(E_LOG, L_STREAMING, "Could not convert raw PCM to frame\n");
return;
}
ret = transcode_encode(streaming_encoded_data, decoded, streaming_encode_ctx);
transcode_decoded_free(decoded);
if (ret < 0)
return;
}
// Event timed out, let's see what the player is doing and send silence if it is paused
else
{
if (streaming_player_changed)
{
streaming_player_changed = 0;
player_get_status(&streaming_player_status);
}
if (streaming_player_status.status != PLAY_PAUSED)
return;
evbuffer_add(streaming_encoded_data, streaming_silence_data, streaming_silence_size);
}
len = evbuffer_get_length(streaming_encoded_data);
// Send data
evbuf = evbuffer_new();
for (session = streaming_sessions; session; session = session->next)
{
if (session->next)
{
buf = evbuffer_pullup(streaming_encoded_data, -1);
evbuffer_add(evbuf, buf, len);
evhttp_send_reply_chunk(session->req, evbuf);
}
else
evhttp_send_reply_chunk(session->req, streaming_encoded_data);
}
evbuffer_free(evbuf);
}
// Thread: player
static int
streaming_cb(uint8_t *rawbuf, size_t size)
{
if (size != STREAMING_RAWBUF_SIZE)
{
DPRINTF(E_LOG, L_STREAMING, "Bug! Buffer size in streaming_cb does not equal input from player\n");
return -1;
}
if (write(streaming_pipe[1], rawbuf, size) < 0)
return -1;
return 0;
}
// Thread: player (not fully thread safe, but hey...)
static void
player_change_cb(enum listener_event_type type)
{
streaming_player_changed = 1;
}
int
streaming_is_request(struct evhttp_request *req, char *uri)
{
char *ptr;
ptr = strrchr(uri, '/');
if (!ptr || (strcasecmp(ptr, "/stream.mp3") != 0))
return 0;
return 1;
}
int
streaming_request(struct evhttp_request *req)
{
struct streaming_session *session;
struct evhttp_connection *evcon;
struct evkeyvalq *output_headers;
cfg_t *lib;
const char *name;
char *address;
ev_uint16_t port;
if (!streaming_initialized)
{
DPRINTF(E_LOG, L_STREAMING, "Got mp3 streaming request, but cannot encode to mp3\n");
evhttp_send_error(req, HTTP_NOTFOUND, "Not Found");
return -1;
}
evcon = evhttp_request_get_connection(req);
evhttp_connection_get_peer(evcon, &address, &port);
DPRINTF(E_INFO, L_STREAMING, "Beginning mp3 streaming to %s:%d\n", address, (int)port);
lib = cfg_getsec(cfg, "library");
name = cfg_getstr(lib, "name");
output_headers = evhttp_request_get_output_headers(req);
evhttp_add_header(output_headers, "Content-Type", "audio/mpeg");
evhttp_add_header(output_headers, "Server", "forked-daapd/" VERSION);
evhttp_add_header(output_headers, "Cache-Control", "no-cache");
evhttp_add_header(output_headers, "Pragma", "no-cache");
evhttp_add_header(output_headers, "Expires", "Mon, 31 Aug 2015 06:00:00 GMT");
evhttp_add_header(output_headers, "icy-name", name);
// TODO ICY metaint
evhttp_send_reply_start(req, HTTP_OK, "OK");
session = malloc(sizeof(struct streaming_session));
if (!session)
{
DPRINTF(E_LOG, L_STREAMING, "Out of memory for streaming request\n");
evhttp_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error");
return -1;
}
if (!streaming_sessions)
event_add(streamingev, &streaming_silence_tv);
session->req = req;
session->next = streaming_sessions;
streaming_sessions = session;
evhttp_connection_set_timeout(evcon, STREAMING_CONNECTION_TIMEOUT);
evhttp_connection_set_closecb(evcon, streaming_fail_cb, session);
player_streaming_start(streaming_cb);
return 0;
}
int
streaming_init(void)
{
struct decode_ctx *decode_ctx;
struct decoded_frame *decoded;
int remaining;
int ret;
decode_ctx = transcode_decode_setup_raw();
if (!decode_ctx)
{
DPRINTF(E_LOG, L_STREAMING, "Could not create decoding context\n");
return -1;
}
streaming_encode_ctx = transcode_encode_setup(decode_ctx, XCODE_MP3, NULL);
transcode_decode_cleanup(decode_ctx);
if (!streaming_encode_ctx)
{
DPRINTF(E_LOG, L_STREAMING, "Will not be able to stream mp3, libav does not support mp3 encoding\n");
return -1;
}
// Non-blocking because otherwise httpd and player thread may deadlock
ret = pipe2(streaming_pipe, O_CLOEXEC | O_NONBLOCK);
if (ret < 0)
{
DPRINTF(E_FATAL, L_STREAMING, "Could not create pipe: %s\n", strerror(errno));
goto pipe_fail;
}
// Listen to playback changes so we don't have to poll to check for pausing
ret = listener_add(player_change_cb, LISTENER_PLAYER);
if (ret < 0)
{
DPRINTF(E_FATAL, L_STREAMING, "Could not add listener\n");
goto listener_fail;
}
// Initialize buffer for encoded mp3 audio and event for pipe reading
streaming_encoded_data = evbuffer_new();
streamingev = event_new(evbase_httpd, streaming_pipe[0], EV_TIMEOUT | EV_READ | EV_PERSIST, streaming_send_cb, NULL);
if (!streaming_encoded_data || !streamingev)
{
DPRINTF(E_LOG, L_STREAMING, "Out of memory for encoded_data or event\n");
goto event_fail;
}
// Encode some silence which will be used for playback pause and put in a permanent buffer
remaining = STREAMING_SILENCE_INTERVAL * STOB(44100);
while (remaining > STREAMING_RAWBUF_SIZE)
{
decoded = transcode_raw2frame(streaming_rawbuf, STREAMING_RAWBUF_SIZE);
if (!decoded)
{
DPRINTF(E_LOG, L_STREAMING, "Could not convert raw PCM to frame\n");
goto silence_fail;
}
ret = transcode_encode(streaming_encoded_data, decoded, streaming_encode_ctx);
transcode_decoded_free(decoded);
if (ret < 0)
{
DPRINTF(E_LOG, L_STREAMING, "Could not encode silence buffer\n");
goto silence_fail;
}
remaining -= STREAMING_RAWBUF_SIZE;
}
streaming_silence_size = evbuffer_get_length(streaming_encoded_data);
if (streaming_silence_size == 0)
{
DPRINTF(E_LOG, L_STREAMING, "The encoder didn't encode any silence\n");
goto silence_fail;
}
streaming_silence_data = malloc(streaming_silence_size);
if (!streaming_silence_data)
{
DPRINTF(E_LOG, L_STREAMING, "Out of memory for streaming_silence_data\n");
goto silence_fail;
}
ret = evbuffer_remove(streaming_encoded_data, streaming_silence_data, streaming_silence_size);
if (ret != streaming_silence_size)
{
DPRINTF(E_LOG, L_STREAMING, "Unknown error while copying silence buffer\n");
free(streaming_silence_data);
goto silence_fail;
}
// All done
streaming_initialized = 1;
return 0;
silence_fail:
event_free(streamingev);
evbuffer_free(streaming_encoded_data);
event_fail:
listener_remove(player_change_cb);
listener_fail:
close(streaming_pipe[0]);
close(streaming_pipe[1]);
pipe_fail:
transcode_encode_cleanup(streaming_encode_ctx);
return -1;
}
void
streaming_deinit(void)
{
struct streaming_session *session;
struct streaming_session *next;
if (!streaming_initialized)
return;
player_streaming_stop();
event_free(streamingev);
next = NULL;
for (session = streaming_sessions; session; session = next)
{
evhttp_send_reply_end(session->req);
next = session->next;
free(session);
}
listener_remove(player_change_cb);
close(streaming_pipe[0]);
close(streaming_pipe[1]);
transcode_encode_cleanup(streaming_encode_ctx);
evbuffer_free(streaming_encoded_data);
free(streaming_silence_data);
}

25
src/httpd_streaming.h Normal file
View File

@ -0,0 +1,25 @@
#ifndef __HTTPD_STREAMING_H__
#define __HTTPD_STREAMING_H__
#include <event2/http.h>
/* httpd_streaming takes care of incoming requests to /stream.mp3
* It will receive decoded audio from the player, and encode it, and
* stream it to one or more clients. It will not be available
* if a suitable ffmpeg/libav encoder is not present at runtime.
*/
int
streaming_is_request(struct evhttp_request *req, char *uri);
int
streaming_request(struct evhttp_request *req);
int
streaming_init(void);
void
streaming_deinit(void);
#endif /* !__HTTPD_STREAMING_H__ */

View File

@ -43,7 +43,7 @@ static int threshold;
static int console;
static char *logfilename;
static FILE *logfile;
static char *labels[] = { "config", "daap", "db", "httpd", "http", "main", "mdns", "misc", "rsp", "scan", "xcode", "event", "remote", "dacp", "ffmpeg", "artwork", "player", "raop", "laudio", "dmap", "dbperf", "spotify", "lastfm", "cache", "mpd" };
static char *labels[] = { "config", "daap", "db", "httpd", "http", "main", "mdns", "misc", "rsp", "scan", "xcode", "event", "remote", "dacp", "ffmpeg", "artwork", "player", "raop", "laudio", "dmap", "dbperf", "spotify", "lastfm", "cache", "mpd", "stream" };
static char *severities[] = { "FATAL", "LOG", "WARN", "INFO", "DEBUG", "SPAM" };

View File

@ -5,34 +5,35 @@
#include <stdarg.h>
/* Log domains */
#define L_CONF 0
#define L_DAAP 1
#define L_DB 2
#define L_HTTPD 3
#define L_HTTP 4
#define L_MAIN 5
#define L_MDNS 6
#define L_MISC 7
#define L_RSP 8
#define L_SCAN 9
#define L_XCODE 10
#define L_CONF 0
#define L_DAAP 1
#define L_DB 2
#define L_HTTPD 3
#define L_HTTP 4
#define L_MAIN 5
#define L_MDNS 6
#define L_MISC 7
#define L_RSP 8
#define L_SCAN 9
#define L_XCODE 10
/* libevent logging */
#define L_EVENT 11
#define L_REMOTE 12
#define L_DACP 13
#define L_FFMPEG 14
#define L_ART 15
#define L_PLAYER 16
#define L_RAOP 17
#define L_LAUDIO 18
#define L_DMAP 19
#define L_DBPERF 20
#define L_SPOTIFY 21
#define L_LASTFM 22
#define L_CACHE 23
#define L_MPD 24
#define L_EVENT 11
#define L_REMOTE 12
#define L_DACP 13
#define L_FFMPEG 14
#define L_ART 15
#define L_PLAYER 16
#define L_RAOP 17
#define L_LAUDIO 18
#define L_DMAP 19
#define L_DBPERF 20
#define L_SPOTIFY 21
#define L_LASTFM 22
#define L_CACHE 23
#define L_MPD 24
#define L_STREAMING 25
#define N_LOGDOMAINS 25
#define N_LOGDOMAINS 26
/* Severities */
#define E_FATAL 0

View File

@ -50,6 +50,7 @@
#include <event.h>
#include <libavutil/log.h>
#include <libavformat/avformat.h>
#include <libavfilter/avfilter.h>
#include <gcrypt.h>
GCRY_THREAD_OPTION_PTHREAD_IMPL;
@ -585,6 +586,7 @@ main(int argc, char **argv)
}
av_register_all();
avfilter_register_all();
#if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 13)
avformat_network_init();
#endif

View File

@ -116,7 +116,7 @@ struct player_source
end of file, until then it is 0. */
uint64_t end;
struct transcode_ctx *ctx;
struct transcode_ctx *xcode;
int setup_done;
struct player_source *play_next;
@ -267,12 +267,15 @@ static uint64_t last_rtptime;
static int dev_autoselect; //TODO [player] Is this still necessary?
static struct raop_device *dev_list;
/* Device status */
/* Output status */
static enum laudio_state laudio_status;
static int laudio_selected;
static int laudio_volume;
static int laudio_relvol;
static int raop_sessions;
static int streaming_selected;
static player_streaming_cb streaming_write;
/* Commands */
static struct player_command *cur_cmd;
@ -755,7 +758,7 @@ metadata_check_icy(void)
struct http_icy_metadata *metadata;
int changed;
transcode_metadata(cur_streaming->ctx, &metadata, &changed);
metadata = transcode_metadata(cur_streaming->xcode, &changed);
if (!metadata)
return;
@ -847,7 +850,8 @@ stream_setup(struct player_source *ps, struct media_file_info *mfi)
switch (ps->data_kind)
{
case DATA_KIND_FILE:
ret = transcode_setup(&ps->ctx, mfi, NULL, 0);
ps->xcode = transcode_setup(mfi, XCODE_PCM16_NOHEADER, NULL);
ret = ps->xcode ? 0 : -1;
break;
case DATA_KIND_HTTP:
@ -858,7 +862,8 @@ stream_setup(struct player_source *ps, struct media_file_info *mfi)
free(mfi->path);
mfi->path = url;
ret = transcode_setup(&ps->ctx, mfi, NULL, 0);
ps->xcode = transcode_setup(mfi, XCODE_PCM16_NOHEADER, NULL);
ret = ps->xcode ? 0 : -1;
break;
case DATA_KIND_SPOTIFY:
@ -960,14 +965,14 @@ stream_read(struct player_source *ps, int len)
switch (ps->data_kind)
{
case DATA_KIND_HTTP:
ret = transcode(ps->ctx, audio_buf, len, &icy_timer);
ret = transcode(ps->xcode, audio_buf, len, &icy_timer);
if (icy_timer)
metadata_check_icy();
break;
case DATA_KIND_FILE:
ret = transcode(ps->ctx, audio_buf, len, &icy_timer);
ret = transcode(ps->xcode, audio_buf, len, &icy_timer);
break;
#ifdef HAVE_SPOTIFY_H
@ -1060,7 +1065,7 @@ stream_seek(struct player_source *ps, int seek_ms)
break;
case DATA_KIND_FILE:
ret = transcode_seek(ps->ctx, seek_ms);
ret = transcode_seek(ps->xcode, seek_ms);
break;
#ifdef HAVE_SPOTIFY_H
@ -1102,10 +1107,10 @@ stream_stop(struct player_source *ps)
{
case DATA_KIND_FILE:
case DATA_KIND_HTTP:
if (ps->ctx)
if (ps->xcode)
{
transcode_cleanup(ps->ctx);
ps->ctx = NULL;
transcode_cleanup(ps->xcode);
ps->xcode = NULL;
}
break;
@ -1584,6 +1589,9 @@ playback_write(void)
return;
}
if (streaming_selected)
streaming_write(rawbuf, sizeof(rawbuf));
if (laudio_status & LAUDIO_F_STARTED)
laudio_write(rawbuf, last_rtptime);
@ -2289,10 +2297,10 @@ artwork_url_get(struct player_command *cmd)
return -1;
/* Check that we are playing a viable stream, and that it has the requested id */
if (!ps->ctx || ps->data_kind != DATA_KIND_HTTP || ps->id != cmd->arg.icy.id)
if (!ps->xcode || ps->data_kind != DATA_KIND_HTTP || ps->id != cmd->arg.icy.id)
return -1;
transcode_metadata_artwork_url(ps->ctx, &cmd->arg.icy.artwork_url);
cmd->arg.icy.artwork_url = transcode_metadata_artwork_url(ps->xcode);
return 0;
}
@ -3904,6 +3912,20 @@ player_playback_prev(void)
return ret;
}
void
player_streaming_start(player_streaming_cb cb)
{
streaming_write = cb;
streaming_selected = 1;
}
void
player_streaming_stop(void)
{
streaming_selected = 0;
}
void
player_speaker_enumerate(spk_enum_cb cb, void *arg)
{

View File

@ -70,6 +70,7 @@ struct player_status {
};
typedef void (*spk_enum_cb)(uint64_t id, const char *name, int relvol, struct spk_flags flags, void *arg);
typedef int (*player_streaming_cb)(uint8_t *rawbuf, size_t size);
struct player_history
{
@ -130,6 +131,12 @@ player_playback_next(void);
int
player_playback_prev(void);
void
player_streaming_start(player_streaming_cb cb);
void
player_streaming_stop(void);
int
player_volume_set(int vol);

File diff suppressed because it is too large Load Diff

View File

@ -2,34 +2,81 @@
#ifndef __TRANSCODE_H__
#define __TRANSCODE_H__
#ifdef HAVE_LIBEVENT2
# include <event2/buffer.h>
#else
# include <event.h>
#endif
#include <event2/buffer.h>
#include "db.h"
#include "http.h"
#define XCODE_WAVHEADER (1 << 14)
#define XCODE_HAS_VIDEO (1 << 15)
enum transcode_profile
{
// Transcodes the best available audio stream into PCM16 (does not add wav header)
XCODE_PCM16_NOHEADER = 1,
// Transcodes the best available audio stream into PCM16 (with wav header)
XCODE_PCM16_HEADER = XCODE_WAVHEADER | 2,
// Transcodes the best available audio stream into MP3
XCODE_MP3 = 3,
// Transcodes video + audio + subtitle streams (not tested - for future use)
XCODE_H264_AAC = XCODE_HAS_VIDEO | 4,
};
struct decode_ctx;
struct encode_ctx;
struct transcode_ctx;
struct decoded_frame;
int
transcode(struct transcode_ctx *ctx, struct evbuffer *evbuf, int wanted, int *icy_timer);
// Setting up
struct decode_ctx *
transcode_decode_setup(struct media_file_info *mfi, int decode_video);
int
transcode_seek(struct transcode_ctx *ctx, int ms);
struct encode_ctx *
transcode_encode_setup(struct decode_ctx *src_ctx, enum transcode_profile profile, off_t *est_size);
int
transcode_setup(struct transcode_ctx **nctx, struct media_file_info *mfi, off_t *est_size, int wavhdr);
struct transcode_ctx *
transcode_setup(struct media_file_info *mfi, enum transcode_profile profile, off_t *est_size);
void
transcode_cleanup(struct transcode_ctx *ctx);
struct decode_ctx *
transcode_decode_setup_raw(void);
int
transcode_needed(const char *user_agent, const char *client_codecs, char *file_codectype);
// Cleaning up
void
transcode_metadata(struct transcode_ctx *ctx, struct http_icy_metadata **metadata, int *changed);
transcode_decode_cleanup(struct decode_ctx *ctx);
void
transcode_metadata_artwork_url(struct transcode_ctx *ctx, char **artwork_url);
transcode_encode_cleanup(struct encode_ctx *ctx);
void
transcode_cleanup(struct transcode_ctx *ctx);
void
transcode_decoded_free(struct decoded_frame *decoded);
// Transcoding
struct decoded_frame *
transcode_decode(struct decode_ctx *ctx);
int
transcode_encode(struct evbuffer *evbuf, struct decoded_frame *decoded, struct encode_ctx *ctx);
int
transcode(struct transcode_ctx *ctx, struct evbuffer *evbuf, int wanted, int *icy_timer);
struct decoded_frame *
transcode_raw2frame(uint8_t *data, size_t size);
// Seeking
int
transcode_seek(struct transcode_ctx *ctx, int ms);
// Metadata
struct http_icy_metadata *
transcode_metadata(struct transcode_ctx *ctx, int *changed);
char *
transcode_metadata_artwork_url(struct transcode_ctx *ctx);
#endif /* !__TRANSCODE_H__ */