mirror of
https://github.com/owntone/owntone-server.git
synced 2025-02-28 13:59:18 -05:00
Also introduce default output format and selected device format, should the user want another format. As part of this, change enum player_format in player.h to enum media_format in misc.h so that it is akin to struct media_quality. Modify json API to support this.
706 lines
18 KiB
C
706 lines
18 KiB
C
/*
|
|
* Copyright (C) 2023 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 <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <unistd.h>
|
|
#include <uninorm.h>
|
|
#include <fcntl.h>
|
|
|
|
#include "outputs.h"
|
|
#include "misc.h"
|
|
#include "worker.h"
|
|
#include "player.h"
|
|
#include "transcode.h"
|
|
#include "logger.h"
|
|
#include "db.h"
|
|
|
|
/* About
|
|
*
|
|
* This output takes the writes from the player thread, gives them to a worker
|
|
* thread for mp3 encoding, and then the mp3 is written to a fd for the httpd
|
|
* request handler to read and pass to clients. If there is no writing from the
|
|
* player, but there are clients, it instead writes silence to the fd.
|
|
*/
|
|
|
|
// Seconds between sending a frame of silence when player is idle
|
|
// (to prevent client from hanging up)
|
|
#define STREAMING_SILENCE_INTERVAL 1
|
|
|
|
// The wanted structure represents a particular format and quality that should
|
|
// be produced for one or more sessions. A pipe pair is created for each session
|
|
// for the i/o.
|
|
#define WANTED_PIPES_MAX 8
|
|
|
|
struct pipepair
|
|
{
|
|
int writefd;
|
|
int readfd;
|
|
};
|
|
|
|
struct streaming_wanted
|
|
{
|
|
int num_sessions; // for refcounting
|
|
struct pipepair audio[WANTED_PIPES_MAX];
|
|
struct pipepair metadata[WANTED_PIPES_MAX];
|
|
|
|
enum media_format format;
|
|
struct media_quality quality;
|
|
|
|
struct evbuffer *audio_in;
|
|
struct evbuffer *audio_out;
|
|
struct encode_ctx *xcode_ctx;
|
|
|
|
int nb_samples;
|
|
uint8_t *frame_data;
|
|
size_t frame_size;
|
|
|
|
struct streaming_wanted *next;
|
|
};
|
|
|
|
struct streaming_ctx
|
|
{
|
|
struct streaming_wanted *wanted;
|
|
struct event *silenceev;
|
|
struct timeval silencetv;
|
|
struct media_quality last_quality;
|
|
|
|
char title[4064]; // See STREAMING_ICY_METALEN_MAX in http_streaming.c
|
|
|
|
// seqnum may wrap around so must be unsigned
|
|
unsigned int seqnum;
|
|
unsigned int seqnum_encode_next;
|
|
};
|
|
|
|
struct encode_cmdarg
|
|
{
|
|
struct output_buffer *obuf;
|
|
unsigned int seqnum;
|
|
};
|
|
|
|
static pthread_mutex_t streaming_wanted_lck;
|
|
static pthread_cond_t streaming_sequence_cond;
|
|
|
|
static struct streaming_ctx streaming =
|
|
{
|
|
.silencetv = { STREAMING_SILENCE_INTERVAL, 0 },
|
|
};
|
|
|
|
extern struct event_base *evbase_player;
|
|
|
|
|
|
/* ------------------------------- Helpers ---------------------------------- */
|
|
|
|
static struct encode_ctx *
|
|
encoder_setup(enum media_format format, struct media_quality *quality)
|
|
{
|
|
struct transcode_encode_setup_args encode_args = { .profile = XCODE_MP3, .quality = quality };
|
|
struct encode_ctx *encode_ctx = NULL;
|
|
|
|
if (quality->bits_per_sample == 16)
|
|
encode_args.src_ctx = transcode_decode_setup_raw(XCODE_PCM16, quality);
|
|
else if (quality->bits_per_sample == 24)
|
|
encode_args.src_ctx = transcode_decode_setup_raw(XCODE_PCM24, quality);
|
|
else if (quality->bits_per_sample == 32)
|
|
encode_args.src_ctx = transcode_decode_setup_raw(XCODE_PCM32, quality);
|
|
|
|
if (!encode_args.src_ctx)
|
|
{
|
|
DPRINTF(E_LOG, L_STREAMING, "Error setting up decoder for quality sr %d, bps %d, ch %d, cannot encode\n",
|
|
quality->sample_rate, quality->bits_per_sample, quality->channels);
|
|
goto out;
|
|
}
|
|
|
|
if (format == MEDIA_FORMAT_MP3)
|
|
encode_ctx = transcode_encode_setup(encode_args);
|
|
|
|
if (!encode_ctx)
|
|
{
|
|
DPRINTF(E_LOG, L_STREAMING, "Error setting up encoder for quality sr %d, bps %d, ch %d, cannot encode\n",
|
|
quality->sample_rate, quality->bits_per_sample, quality->channels);
|
|
goto out;
|
|
}
|
|
|
|
out:
|
|
transcode_decode_cleanup(&encode_args.src_ctx);
|
|
return encode_ctx;
|
|
}
|
|
|
|
static int
|
|
pipe_open(struct pipepair *p)
|
|
{
|
|
int fd[2];
|
|
int ret;
|
|
|
|
#ifdef HAVE_PIPE2
|
|
ret = pipe2(fd, O_CLOEXEC | O_NONBLOCK);
|
|
#else
|
|
if ( pipe(fd) < 0 ||
|
|
fcntl(fd[0], F_SETFL, O_CLOEXEC | O_NONBLOCK) < 0 ||
|
|
fcntl(fd[1], F_SETFL, O_CLOEXEC | O_NONBLOCK) < 0 )
|
|
ret = -1;
|
|
else
|
|
ret = 0;
|
|
#endif
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_STREAMING, "Could not create pipe: %s\n", strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
p->writefd = fd[1];
|
|
p->readfd = fd[0];
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
pipe_close(struct pipepair *p)
|
|
{
|
|
if (p->readfd >= 0)
|
|
close(p->readfd);
|
|
if (p->writefd >= 0)
|
|
close(p->writefd);
|
|
|
|
p->writefd = -1;
|
|
p->readfd = -1;
|
|
}
|
|
|
|
static void
|
|
wanted_free(struct streaming_wanted *w)
|
|
{
|
|
if (!w)
|
|
return;
|
|
|
|
for (int i = 0; i < WANTED_PIPES_MAX; i++)
|
|
pipe_close(&w->audio[i]);
|
|
for (int i = 0; i < WANTED_PIPES_MAX; i++)
|
|
pipe_close(&w->metadata[i]);
|
|
|
|
transcode_encode_cleanup(&w->xcode_ctx);
|
|
evbuffer_free(w->audio_in);
|
|
evbuffer_free(w->audio_out);
|
|
free(w->frame_data);
|
|
free(w);
|
|
}
|
|
|
|
static int
|
|
pipe_index_find_byreadfd(struct pipepair *p, int readfd)
|
|
{
|
|
for (int i = 0; i < WANTED_PIPES_MAX; i++, p++)
|
|
{
|
|
if (p->readfd == readfd)
|
|
return i;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
static struct streaming_wanted *
|
|
wanted_new(enum media_format format, struct media_quality quality)
|
|
{
|
|
struct streaming_wanted *w;
|
|
|
|
CHECK_NULL(L_STREAMING, w = calloc(1, sizeof(struct streaming_wanted)));
|
|
CHECK_NULL(L_STREAMING, w->audio_in = evbuffer_new());
|
|
CHECK_NULL(L_STREAMING, w->audio_out = evbuffer_new());
|
|
|
|
w->xcode_ctx = encoder_setup(format, &quality);
|
|
if (!w->xcode_ctx)
|
|
goto error;
|
|
|
|
w->format = format;
|
|
w->quality = quality;
|
|
w->nb_samples = transcode_encode_query(w->xcode_ctx, "samples_per_frame"); // 1152 for mp3
|
|
w->frame_size = STOB(w->nb_samples, quality.bits_per_sample, quality.channels);
|
|
|
|
CHECK_NULL(L_STREAMING, w->frame_data = malloc(w->frame_size));
|
|
|
|
for (int i = 0; i < WANTED_PIPES_MAX; i++)
|
|
{
|
|
w->audio[i].writefd = -1;
|
|
w->audio[i].readfd = -1;
|
|
w->metadata[i].writefd = -1;
|
|
w->metadata[i].readfd = -1;
|
|
}
|
|
|
|
return w;
|
|
|
|
error:
|
|
wanted_free(w);
|
|
return NULL;
|
|
}
|
|
|
|
static void
|
|
wanted_remove(struct streaming_wanted **wanted, struct streaming_wanted *remove)
|
|
{
|
|
struct streaming_wanted *prev = NULL;
|
|
struct streaming_wanted *w;
|
|
|
|
for (w = *wanted; w; w = w->next)
|
|
{
|
|
if (w == remove)
|
|
break;
|
|
|
|
prev = w;
|
|
}
|
|
|
|
if (!w)
|
|
return;
|
|
|
|
if (!prev)
|
|
*wanted = remove->next;
|
|
else
|
|
prev->next = remove->next;
|
|
|
|
wanted_free(remove);
|
|
}
|
|
|
|
static struct streaming_wanted *
|
|
wanted_add(struct streaming_wanted **wanted, enum media_format format, struct media_quality quality)
|
|
{
|
|
struct streaming_wanted *w;
|
|
|
|
w = wanted_new(format, quality);
|
|
w->next = *wanted;
|
|
*wanted = w;
|
|
|
|
return w;
|
|
}
|
|
|
|
static struct streaming_wanted *
|
|
wanted_find_byformat(struct streaming_wanted *wanted, enum media_format format, struct media_quality quality)
|
|
{
|
|
struct streaming_wanted *w;
|
|
|
|
for (w = wanted; w; w = w->next)
|
|
{
|
|
if (w->format == format && quality_is_equal(&w->quality, &quality))
|
|
return w;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static struct streaming_wanted *
|
|
wanted_find_byreadfd(struct streaming_wanted *wanted, int readfd)
|
|
{
|
|
struct streaming_wanted *w;
|
|
int i;
|
|
|
|
for (w = wanted; w; w = w->next)
|
|
{
|
|
i = pipe_index_find_byreadfd(w->audio, readfd);
|
|
if (i != -1)
|
|
return w;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static int
|
|
wanted_session_add(int *audiofd, int *metadatafd, struct streaming_wanted *w)
|
|
{
|
|
int ret;
|
|
int i;
|
|
|
|
for (i = 0; i < WANTED_PIPES_MAX; i++)
|
|
{
|
|
if (w->audio[i].writefd != -1) // In use
|
|
continue;
|
|
|
|
ret = pipe_open(&w->audio[i]);
|
|
if (ret < 0)
|
|
return -1;
|
|
|
|
ret = pipe_open(&w->metadata[i]);
|
|
if (ret < 0)
|
|
return -1;
|
|
|
|
*audiofd = w->audio[i].readfd;
|
|
*metadatafd = w->metadata[i].readfd;
|
|
break;
|
|
}
|
|
|
|
if (i == WANTED_PIPES_MAX)
|
|
{
|
|
DPRINTF(E_LOG, L_STREAMING, "Cannot add streaming session, max pipe limit reached\n");
|
|
return -1;
|
|
}
|
|
|
|
w->num_sessions++;
|
|
DPRINTF(E_DBG, L_STREAMING, "Session register audiofd %d, metadatafd %d, wanted->num_sessions=%d\n", *audiofd, *metadatafd, w->num_sessions);
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
wanted_session_remove(struct streaming_wanted *w, int readfd)
|
|
{
|
|
int i;
|
|
|
|
i = pipe_index_find_byreadfd(w->audio, readfd);
|
|
if (i < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_STREAMING, "Cannot remove streaming session, readfd %d not found\n", readfd);
|
|
return;
|
|
}
|
|
|
|
pipe_close(&w->audio[i]);
|
|
pipe_close(&w->metadata[i]);
|
|
|
|
w->num_sessions--;
|
|
DPRINTF(E_DBG, L_STREAMING, "Session deregister readfd %d, wanted->num_sessions=%d\n", readfd, w->num_sessions);
|
|
}
|
|
|
|
|
|
/* ----------------------------- Thread: Worker ----------------------------- */
|
|
|
|
static int
|
|
encode_buffer(struct streaming_wanted *w, uint8_t *buf, size_t bufsize)
|
|
{
|
|
ssize_t remaining_bytes;
|
|
transcode_frame *frame = NULL;
|
|
int ret;
|
|
|
|
if (buf)
|
|
{
|
|
evbuffer_add(w->audio_in, buf, bufsize);
|
|
}
|
|
else
|
|
{
|
|
// buf being null is either a silence timeout or that we could't find the
|
|
// subscripted quality. In both cases we encode silence.
|
|
memset(w->frame_data, 0, w->frame_size);
|
|
evbuffer_add(w->audio_in, w->frame_data, w->frame_size);
|
|
}
|
|
|
|
remaining_bytes = evbuffer_get_length(w->audio_in);
|
|
|
|
// Read and encode from 'audio_in' in chunks of 'frame_size' bytes
|
|
while (remaining_bytes > w->frame_size)
|
|
{
|
|
ret = evbuffer_remove(w->audio_in, w->frame_data, w->frame_size);
|
|
if (ret != w->frame_size)
|
|
{
|
|
DPRINTF(E_LOG, L_STREAMING, "Bug! Couldn't read a frame of %zu bytes (format %d)\n", w->frame_size, w->format);
|
|
goto error;
|
|
}
|
|
|
|
remaining_bytes -= w->frame_size;
|
|
|
|
frame = transcode_frame_new(w->frame_data, w->frame_size, w->nb_samples, &w->quality);
|
|
if (!frame)
|
|
{
|
|
DPRINTF(E_LOG, L_STREAMING, "Could not convert raw PCM to frame (format %d)\n", w->format);
|
|
goto error;
|
|
}
|
|
|
|
ret = transcode_encode(w->audio_out, w->xcode_ctx, frame, 0);
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_STREAMING, "Encoding error (format %d)\n", w->format);
|
|
goto error;
|
|
}
|
|
|
|
transcode_frame_free(frame);
|
|
}
|
|
|
|
return 0;
|
|
|
|
error:
|
|
transcode_frame_free(frame);
|
|
return -1;
|
|
}
|
|
|
|
static void
|
|
encode_and_write(int *failed_pipe_readfd, struct streaming_wanted *w, struct output_buffer *obuf)
|
|
{
|
|
uint8_t *buf;
|
|
size_t bufsize;
|
|
size_t len;
|
|
int ret;
|
|
int i;
|
|
|
|
for (i = 0, buf = NULL, bufsize = 0; obuf && obuf->data[i].buffer; i++)
|
|
{
|
|
if (!quality_is_equal(&obuf->data[i].quality, &w->quality))
|
|
continue;
|
|
|
|
buf = obuf->data[i].buffer;
|
|
bufsize = obuf->data[i].bufsize;
|
|
}
|
|
|
|
// If encoding fails we should kill the sessions, which for thread safety
|
|
// and to avoid deadlocks has to be done later with player_streaming_deregister()
|
|
ret = encode_buffer(w, buf, bufsize);
|
|
if (ret < 0)
|
|
{
|
|
for (i = 0; i < WANTED_PIPES_MAX; i++)
|
|
{
|
|
if (w->audio[i].writefd != -1)
|
|
*failed_pipe_readfd = w->audio[i].readfd;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
len = evbuffer_get_length(w->audio_out);
|
|
if (len == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
buf = evbuffer_pullup(w->audio_out, -1);
|
|
for (i = 0; i < WANTED_PIPES_MAX; i++)
|
|
{
|
|
if (w->audio[i].writefd == -1)
|
|
continue;
|
|
|
|
ret = write(w->audio[i].writefd, buf, len);
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_STREAMING, "Error writing to stream pipe %d (format %d): %s\n", w->audio[i].writefd, w->format, strerror(errno));
|
|
*failed_pipe_readfd = w->audio[i].readfd;
|
|
}
|
|
}
|
|
|
|
evbuffer_drain(w->audio_out, -1);
|
|
}
|
|
|
|
static void
|
|
encode_data_cb(void *arg)
|
|
{
|
|
struct encode_cmdarg *ctx = arg;
|
|
struct output_buffer *obuf = ctx->obuf;
|
|
struct streaming_wanted *w;
|
|
int failed_pipe_readfd = -1;
|
|
|
|
pthread_mutex_lock(&streaming_wanted_lck);
|
|
|
|
// To make sure we process the frames in order
|
|
while (ctx->seqnum != streaming.seqnum_encode_next)
|
|
pthread_cond_wait(&streaming_sequence_cond, &streaming_wanted_lck);
|
|
|
|
for (w = streaming.wanted; w; w = w->next)
|
|
encode_and_write(&failed_pipe_readfd, w, obuf);
|
|
|
|
streaming.seqnum_encode_next++;
|
|
pthread_cond_broadcast(&streaming_sequence_cond);
|
|
pthread_mutex_unlock(&streaming_wanted_lck);
|
|
|
|
outputs_buffer_free(ctx->obuf);
|
|
|
|
// We have to do this after letting go of the lock or we will deadlock. This
|
|
// unfortunate method means we can only fail one session (pipe) each pass.
|
|
if (failed_pipe_readfd >= 0)
|
|
player_streaming_deregister(failed_pipe_readfd);
|
|
}
|
|
|
|
static void
|
|
metadata_write(struct streaming_wanted *w, int readfd, const char *metadata)
|
|
{
|
|
size_t metadata_size;
|
|
int i;
|
|
int ret;
|
|
|
|
for (i = 0; i < WANTED_PIPES_MAX; i++)
|
|
{
|
|
if (w->metadata[i].writefd == -1)
|
|
continue;
|
|
if (readfd >= 0 && w->metadata[i].readfd != readfd)
|
|
continue;
|
|
|
|
metadata_size = strlen(metadata) + 1;
|
|
ret = write(w->metadata[i].writefd, metadata, metadata_size);
|
|
if (ret < 0)
|
|
DPRINTF(E_WARN, L_STREAMING, "Error writing metadata '%s' to fd %d\n", metadata, w->metadata[i].writefd);
|
|
}
|
|
}
|
|
|
|
static void
|
|
metadata_startup_cb(void *arg)
|
|
{
|
|
int *metadata_fd = arg;
|
|
struct streaming_wanted *w;
|
|
|
|
pthread_mutex_lock(&streaming_wanted_lck);
|
|
for (w = streaming.wanted; w; w = w->next)
|
|
metadata_write(w, *metadata_fd, streaming.title);
|
|
pthread_mutex_unlock(&streaming_wanted_lck);
|
|
}
|
|
|
|
static void *
|
|
streaming_metadata_prepare(struct output_metadata *metadata)
|
|
{
|
|
struct db_queue_item *queue_item;
|
|
struct streaming_wanted *w;
|
|
|
|
queue_item = db_queue_fetch_byitemid(metadata->item_id);
|
|
if (!queue_item)
|
|
{
|
|
DPRINTF(E_LOG, L_STREAMING, "Could not fetch queue item id %d for new metadata\n", metadata->item_id);
|
|
return NULL;
|
|
}
|
|
|
|
pthread_mutex_lock(&streaming_wanted_lck);
|
|
// Save it here, we might need it later if a new session starts up
|
|
snprintf(streaming.title, sizeof(streaming.title), "%s - %s", queue_item->title, queue_item->artist);
|
|
|
|
for (w = streaming.wanted; w; w = w->next)
|
|
metadata_write(w, -1, streaming.title);
|
|
pthread_mutex_unlock(&streaming_wanted_lck);
|
|
|
|
free_queue_item(queue_item, 0);
|
|
return NULL;
|
|
}
|
|
|
|
|
|
/* ----------------------------- Thread: Player ----------------------------- */
|
|
|
|
static void
|
|
streaming_write(struct output_buffer *obuf)
|
|
{
|
|
struct encode_cmdarg ctx;
|
|
|
|
// No lock since this is just an early exit, it doesn't need to be accurate
|
|
if (!streaming.wanted)
|
|
return;
|
|
|
|
// We don't want to block the player, so we can't lock to access
|
|
// streaming.wanted and find which qualities we need. So we just copy it all
|
|
// and pass it to a worker thread that can lock and check what is wanted, and
|
|
// also can encode without holding the player.
|
|
ctx.obuf = outputs_buffer_copy(obuf);
|
|
ctx.seqnum = streaming.seqnum;
|
|
|
|
streaming.seqnum++;
|
|
|
|
worker_execute(encode_data_cb, &ctx, sizeof(struct encode_cmdarg), 0);
|
|
|
|
// In case this is the last player write() we want to start streaming silence
|
|
evtimer_add(streaming.silenceev, &streaming.silencetv);
|
|
}
|
|
|
|
static void
|
|
silenceev_cb(evutil_socket_t fd, short event, void *arg)
|
|
{
|
|
streaming_write(NULL);
|
|
}
|
|
|
|
static void
|
|
streaming_metadata_send(struct output_metadata *metadata)
|
|
{
|
|
// Nothing to do, metadata_prepare() did all we needed in a worker thread
|
|
}
|
|
|
|
// Since this is streaming and there is no actual device, we will be called with
|
|
// a dummy/ad hoc device that's not part in the speaker list. We don't need to
|
|
// make any callback so can ignore callback_id.
|
|
static int
|
|
streaming_start(struct output_device *device, int callback_id)
|
|
{
|
|
struct streaming_wanted *w;
|
|
int ret;
|
|
|
|
pthread_mutex_lock(&streaming_wanted_lck);
|
|
w = wanted_find_byformat(streaming.wanted, device->selected_format, device->quality);
|
|
if (!w)
|
|
w = wanted_add(&streaming.wanted, device->selected_format, device->quality);
|
|
ret = wanted_session_add(&device->audio_fd, &device->metadata_fd, w);
|
|
if (ret < 0)
|
|
goto error;
|
|
pthread_mutex_unlock(&streaming_wanted_lck);
|
|
|
|
worker_execute(metadata_startup_cb, &(device->metadata_fd), sizeof(device->metadata_fd), 0);
|
|
|
|
outputs_quality_subscribe(&device->quality);
|
|
|
|
device->id = device->audio_fd;
|
|
return 0;
|
|
|
|
error:
|
|
if (w->num_sessions == 0)
|
|
wanted_remove(&streaming.wanted, w);
|
|
pthread_mutex_unlock(&streaming_wanted_lck);
|
|
return -1;
|
|
}
|
|
|
|
// Since this is streaming and there is no actual device, we will be called with
|
|
// a dummy/ad hoc device that's not part in the speaker list. We don't need to
|
|
// make any callback so can ignore callback_id.
|
|
static int
|
|
streaming_stop(struct output_device *device, int callback_id)
|
|
{
|
|
struct streaming_wanted *w;
|
|
|
|
pthread_mutex_lock(&streaming_wanted_lck);
|
|
w = wanted_find_byreadfd(streaming.wanted, device->id);
|
|
if (!w)
|
|
goto error;
|
|
device->quality = w->quality;
|
|
wanted_session_remove(w, device->id);
|
|
if (w->num_sessions == 0)
|
|
wanted_remove(&streaming.wanted, w);
|
|
pthread_mutex_unlock(&streaming_wanted_lck);
|
|
|
|
outputs_quality_unsubscribe(&device->quality);
|
|
return 0;
|
|
|
|
error:
|
|
pthread_mutex_unlock(&streaming_wanted_lck);
|
|
return -1;
|
|
}
|
|
|
|
static int
|
|
streaming_init(void)
|
|
{
|
|
CHECK_NULL(L_STREAMING, streaming.silenceev = event_new(evbase_player, -1, 0, silenceev_cb, NULL));
|
|
CHECK_ERR(L_STREAMING, mutex_init(&streaming_wanted_lck));
|
|
CHECK_ERR(L_STREAMING, pthread_cond_init(&streaming_sequence_cond, NULL));
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
streaming_deinit(void)
|
|
{
|
|
event_free(streaming.silenceev);
|
|
}
|
|
|
|
|
|
struct output_definition output_streaming =
|
|
{
|
|
.name = "streaming",
|
|
.type = OUTPUT_TYPE_STREAMING,
|
|
.priority = 0,
|
|
.disabled = 0,
|
|
.init = streaming_init,
|
|
.deinit = streaming_deinit,
|
|
.write = streaming_write,
|
|
.device_start = streaming_start,
|
|
.device_probe = streaming_start,
|
|
.device_stop = streaming_stop,
|
|
.metadata_prepare = streaming_metadata_prepare,
|
|
.metadata_send = streaming_metadata_send,
|
|
};
|