Skip to content

Commit

Permalink
Merge pull request #1610 from owntone/streaming_ffmpeg6
Browse files Browse the repository at this point in the history
Refactor mp3 streaming/fix for ffmpeg 6
  • Loading branch information
ejurgensen committed May 12, 2023
2 parents 0d095b3 + f998b1f commit 82fdb7f
Show file tree
Hide file tree
Showing 9 changed files with 561 additions and 325 deletions.
2 changes: 1 addition & 1 deletion src/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ owntone_SOURCES = main.c \
outputs/rtp_common.h outputs/rtp_common.c \
outputs/raop.c outputs/airplay.c $(PAIR_AP_SRC) \
outputs/airplay_events.c outputs/airplay_events.h \
outputs/streaming.c outputs/streaming.h \
outputs/streaming.c \
outputs/dummy.c outputs/fifo.c outputs/rcp.c \
$(ALSA_SRC) $(PULSEAUDIO_SRC) $(CHROMECAST_SRC) \
evrtsp/rtsp.c evrtsp/evrtsp.h evrtsp/rtsp-internal.h evrtsp/log.h \
Expand Down
167 changes: 92 additions & 75 deletions src/httpd_streaming.c
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,25 @@
#include <event2/buffer.h>

#include "httpd_internal.h"
#include "outputs/streaming.h"
#include "player.h"
#include "logger.h"
#include "conffile.h"

#define STREAMING_ICY_METALEN_MAX 4080 // 255*16 incl header/footer (16bytes)
#define STREAMING_ICY_METATITLELEN_MAX 4064 // STREAMING_ICY_METALEN_MAX -16 (not incl header/footer)

struct streaming_session {
struct httpd_request *hreq;

int fd;
struct event *readev;
struct evbuffer *readbuf;
int id;
struct event *audioev;
struct event *metadataev;
struct evbuffer *audiobuf;
size_t bytes_sent;

bool icy_is_requested;
size_t icy_remaining;
char icy_title[STREAMING_ICY_METATITLELEN_MAX];
};

static struct media_quality streaming_default_quality = {
Expand All @@ -54,25 +59,20 @@ static struct media_quality streaming_default_quality = {
.bit_rate = 128000,
};

static void
session_free(struct streaming_session *session);

/* ------------------------------ ICY metadata -------------------------------*/

// To test mp3 and ICY tagm it is good to use:
// mpv --display-tags=* http://localhost:3689/stream.mp3

#define STREAMING_ICY_METALEN_MAX 4080 // 255*16 incl header/footer (16bytes)
#define STREAMING_ICY_METATITLELEN_MAX 4064 // STREAMING_ICY_METALEN_MAX -16 (not incl header/footer)

// As streaming quality goes up, we send more data to the remote client. With a
// smaller ICY_METAINT value we have to splice metadata more frequently - on
// some devices with small input buffers, a higher quality stream and low
// ICY_METAINT can lead to stuttering as observed on a Roku Soundbridge
static unsigned short streaming_icy_metaint = 16384;

static pthread_mutex_t streaming_metadata_lck;
static char streaming_icy_title[STREAMING_ICY_METATITLELEN_MAX];


// We know that the icymeta is limited to 1+255*16 (ie 4081) bytes so caller must
// provide a buf of this size to avoid needless mallocs
//
Expand Down Expand Up @@ -124,7 +124,7 @@ icy_meta_create(uint8_t buf[STREAMING_ICY_METALEN_MAX+1], unsigned *buflen, cons
}

static void
icy_meta_splice(struct evbuffer *out, struct evbuffer *in, size_t *icy_remaining)
icy_meta_splice(struct evbuffer *out, struct evbuffer *in, size_t *icy_remaining, char *title)
{
uint8_t meta[STREAMING_ICY_METALEN_MAX + 1];
unsigned metalen;
Expand All @@ -138,60 +138,14 @@ icy_meta_splice(struct evbuffer *out, struct evbuffer *in, size_t *icy_remaining
*icy_remaining -= consume;
if (*icy_remaining == 0)
{
pthread_mutex_lock(&streaming_metadata_lck);
icy_meta_create(meta, &metalen, streaming_icy_title);
pthread_mutex_unlock(&streaming_metadata_lck);
icy_meta_create(meta, &metalen, title);

evbuffer_add(out, meta, metalen);
*icy_remaining = streaming_icy_metaint;
}
}
}

// Thread: player. TODO Would be nice to avoid the lock. Consider moving all the
// ICY tag stuff to streaming.c and make a STREAMING_FORMAT_MP3_ICY?
static void
icy_metadata_cb(char *metadata)
{
pthread_mutex_lock(&streaming_metadata_lck);
snprintf(streaming_icy_title, sizeof(streaming_icy_title), "%s", metadata);
pthread_mutex_unlock(&streaming_metadata_lck);
}


/* ----------------------------- Session helpers ---------------------------- */

static void
session_free(struct streaming_session *session)
{
if (!session)
return;

if (session->readev)
{
streaming_session_deregister(session->fd);
event_free(session->readev);
}

evbuffer_free(session->readbuf);
free(session);
}

static struct streaming_session *
session_new(struct httpd_request *hreq, bool icy_is_requested)
{
struct streaming_session *session;

CHECK_NULL(L_STREAMING, session = calloc(1, sizeof(struct streaming_session)));
CHECK_NULL(L_STREAMING, session->readbuf = evbuffer_new());

session->hreq = hreq;
session->icy_is_requested = icy_is_requested;
session->icy_remaining = streaming_icy_metaint;

return session;
}


/* ----------------------------- Event callbacks ---------------------------- */

Expand All @@ -204,15 +158,15 @@ conn_close_cb(void *arg)
}

static void
read_cb(evutil_socket_t fd, short event, void *arg)
audio_cb(evutil_socket_t fd, short event, void *arg)
{
struct streaming_session *session = arg;
struct httpd_request *hreq;
int len;

CHECK_NULL(L_STREAMING, hreq = session->hreq);

len = evbuffer_read(session->readbuf, fd, -1);
len = evbuffer_read(session->audiobuf, fd, -1);
if (len < 0 && errno != EAGAIN)
{
DPRINTF(E_INFO, L_STREAMING, "Stopping mp3 streaming to %s:%d\n", session->hreq->peer_address, (int)session->hreq->peer_port);
Expand All @@ -223,22 +177,94 @@ read_cb(evutil_socket_t fd, short event, void *arg)
}

if (session->icy_is_requested)
icy_meta_splice(hreq->out_body, session->readbuf, &session->icy_remaining);
icy_meta_splice(hreq->out_body, session->audiobuf, &session->icy_remaining, session->icy_title);
else
evbuffer_add_buffer(hreq->out_body, session->readbuf);
evbuffer_add_buffer(hreq->out_body, session->audiobuf);

httpd_send_reply_chunk(hreq, NULL, NULL);

session->bytes_sent += len;
}

static void
metadata_cb(evutil_socket_t fd, short event, void *arg)
{
struct streaming_session *session = arg;
struct evbuffer *evbuf;
int len;

CHECK_NULL(L_STREAMING, evbuf = evbuffer_new());

len = evbuffer_read(evbuf, fd, -1);
if (len < 0)
goto out;

len = sizeof(session->icy_title);
evbuffer_remove(evbuf, session->icy_title, len);
session->icy_title[len - 1] = '\0';

out:
evbuffer_free(evbuf);
}


/* ----------------------------- Session helpers ---------------------------- */

static void
session_free(struct streaming_session *session)
{
if (!session)
return;

player_streaming_deregister(session->id);

if (session->audioev)
event_free(session->audioev);
if (session->metadataev)
event_free(session->metadataev);

evbuffer_free(session->audiobuf);
free(session);
}

static struct streaming_session *
session_new(struct httpd_request *hreq, bool icy_is_requested, enum player_format format, struct media_quality quality)
{
struct streaming_session *session;
int audio_fd;
int metadata_fd;

CHECK_NULL(L_STREAMING, session = calloc(1, sizeof(struct streaming_session)));
CHECK_NULL(L_STREAMING, session->audiobuf = evbuffer_new());

session->hreq = hreq;
session->icy_is_requested = icy_is_requested;
session->icy_remaining = streaming_icy_metaint;

// Ask streaming output module for a fd to read mp3 from
session->id = player_streaming_register(&audio_fd, &metadata_fd, format, quality);
if (session->id < 0)
goto error;

CHECK_NULL(L_STREAMING, session->audioev = event_new(hreq->evbase, audio_fd, EV_READ | EV_PERSIST, audio_cb, session));
event_add(session->audioev, NULL);
CHECK_NULL(L_STREAMING, session->metadataev = event_new(hreq->evbase, metadata_fd, EV_READ | EV_PERSIST, metadata_cb, session));
event_add(session->metadataev, NULL);

return session;

error:
session_free(session);
return NULL;
}


/* -------------------------- Module implementation ------------------------- */

static int
streaming_mp3_handler(struct httpd_request *hreq)
{
struct streaming_session *session;
struct streaming_session *session = NULL;
const char *name = cfg_getstr(cfg_getsec(cfg, "library"), "name");
const char *param;
bool icy_is_requested;
Expand All @@ -253,15 +279,9 @@ streaming_mp3_handler(struct httpd_request *hreq)
httpd_header_add(hreq->out_headers, "icy-metaint", buf);
}

session = session_new(hreq, icy_is_requested);
session = session_new(hreq, icy_is_requested, PLAYER_FORMAT_MP3, streaming_default_quality);
if (!session)
return -1;

// Ask streaming output module for a fd to read mp3 from
session->fd = streaming_session_register(STREAMING_FORMAT_MP3, streaming_default_quality);

CHECK_NULL(L_STREAMING, session->readev = event_new(hreq->evbase, session->fd, EV_READ | EV_PERSIST, read_cb, session));
event_add(session->readev, NULL);
return -1; // Error sent by caller

httpd_request_close_cb_set(hreq, conn_close_cb, session);

Expand Down Expand Up @@ -345,9 +365,6 @@ streaming_init(void)
else
DPRINTF(E_INFO, L_STREAMING, "Unsupported icy_metaint=%d, supported range: 4096..131072, defaulting to %d\n", val, streaming_icy_metaint);

CHECK_ERR(L_STREAMING, mutex_init(&streaming_metadata_lck));
streaming_metadatacb_register(icy_metadata_cb);

return 0;
}

Expand Down
49 changes: 49 additions & 0 deletions src/outputs.c
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,43 @@ buffer_drain(struct output_buffer *obuf)
}
}

static struct output_buffer *
buffer_copy(struct output_buffer *obuf)
{
struct output_buffer *copy;
int i;

if (!obuf)
return NULL;

CHECK_NULL(L_PLAYER, copy = malloc(sizeof(struct output_buffer)));

memcpy(copy, obuf, sizeof(struct output_buffer));

for (i = 0; obuf->data[i].buffer; i++)
{
CHECK_NULL(L_PLAYER, copy->data[i].evbuf = evbuffer_new());
evbuffer_add(copy->data[i].evbuf, obuf->data[i].buffer, obuf->data[i].bufsize);
copy->data[i].buffer = evbuffer_pullup(copy->data[i].evbuf, -1);
}

return copy;
}

static void
buffer_free(struct output_buffer *obuf)
{
int i;

if (!obuf)
return;

for (i = 0; obuf->data[i].buffer; i++)
evbuffer_free(obuf->data[i].evbuf);

free(obuf);
}

static void
device_list_sort(void)
{
Expand Down Expand Up @@ -705,6 +742,18 @@ outputs_metadata_free(struct output_metadata *metadata)
metadata_free(metadata);
}

struct output_buffer *
outputs_buffer_copy(struct output_buffer *buffer)
{
return buffer_copy(buffer);
}

void
outputs_buffer_free(struct output_buffer *buffer)
{
buffer_free(buffer);
}

/* ---------------------------- Called by player ---------------------------- */

struct output_device *
Expand Down
11 changes: 11 additions & 0 deletions src/outputs.h
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,18 @@ struct output_device

// Quality of audio output
struct media_quality quality;
int format;

// Address
char *v4_address;
char *v6_address;
short v4_port;
short v6_port;

// Only used for streaming
int audio_fd;
int metadata_fd;

struct event *stop_timer;

// Opaque pointers to device and session data
Expand Down Expand Up @@ -287,6 +292,12 @@ outputs_cb(int callback_id, uint64_t device_id, enum output_device_state);
void
outputs_metadata_free(struct output_metadata *metadata);

struct output_buffer *
outputs_buffer_copy(struct output_buffer *buffer);

void
outputs_buffer_free(struct output_buffer *buffer);

/* ---------------------------- Called by player ---------------------------- */

// Ownership of *add is transferred, so don't address after calling. Instead you
Expand Down
Loading

0 comments on commit 82fdb7f

Please sign in to comment.