summaryrefslogtreecommitdiff
path: root/src/libsink
diff options
context:
space:
mode:
Diffstat (limited to 'src/libsink')
-rw-r--r--src/libsink/meson.build2
-rw-r--r--src/libsink/sink.c632
-rw-r--r--src/libsink/sink.h109
3 files changed, 743 insertions, 0 deletions
diff --git a/src/libsink/meson.build b/src/libsink/meson.build
new file mode 100644
index 0000000..5cbe36d
--- /dev/null
+++ b/src/libsink/meson.build
@@ -0,0 +1,2 @@
+libsink_src = ['sink.c']
+libsink = declare_dependency(sources: libsink_src)
diff --git a/src/libsink/sink.c b/src/libsink/sink.c
new file mode 100644
index 0000000..cf8328a
--- /dev/null
+++ b/src/libsink/sink.c
@@ -0,0 +1,632 @@
+#include <al/log.h>
+
+#include "sink.h"
+
+enum {
+ SINK_EMPTY = 0,
+ SINK_PAUSED,
+ SINK_PLAYING
+};
+
+enum {
+ ENTRY_LOADED = 0,
+ ENTRY_DISREGUARDED
+};
+
+enum {
+ BUFFER_INIT = 0,
+ BUFFER_CONFIGURED,
+ BUFFER_SET_OR_BUFFERED,
+ BUFFER_ADDED,
+};
+
+enum {
+ ADD_BUFFER = 0,
+ REMOVE_BUFFER,
+ START,
+ STOP,
+ TOGGLE_PAUSE,
+ SEEK,
+ CLOSE
+};
+
+#define ENTRY_AUDIO_BUFFER_HELD(entry) \
+ (al_atomic_bool_load(&entry->audio.buf.ref, AL_ATOMIC_RELAXED))
+#ifdef CAMU_SINK_NO_VIDEO
+#define ENTRY_BUFFERS_HELD(entry) ENTRY_AUDIO_BUFFER_HELD(entry)
+#else
+#define ENTRY_VIDEO_BUFFER_HELD(entry) \
+ (al_atomic_bool_load(&entry->video.buf.ref, AL_ATOMIC_RELAXED))
+#define ENTRY_BUFFERS_HELD(entry) (ENTRY_AUDIO_BUFFER_HELD(entry) || ENTRY_VIDEO_BUFFER_HELD(entry))
+#endif
+
+static void handle_sink_cmd(struct camu_sink *sink, struct camu_sink_cmd *cmd)
+{
+ switch (cmd->op) {
+ case ADD_BUFFER: {
+ switch (cmd->value.i) {
+ case CAMU_SINK_AUDIO: {
+ struct camu_sink_entry *entry = (struct camu_sink_entry *)cmd->opaque;
+ sink->callback(sink->userdata, CAMU_SINK_ADD_BUFFER, CAMU_SINK_AUDIO, &entry->audio.buf);
+ break;
+ }
+#ifndef CAMU_SINK_NO_VIDEO
+ case CAMU_SINK_VIDEO: {
+ struct camu_sink_entry *entry = (struct camu_sink_entry *)cmd->opaque;
+ sink->callback(sink->userdata, CAMU_SINK_ADD_BUFFER, CAMU_SINK_VIDEO, &entry->video.buf);
+ break;
+ }
+#endif
+ }
+ break;
+ }
+ case REMOVE_BUFFER: {
+ switch (cmd->value.i) {
+ case CAMU_SINK_AUDIO: {
+ struct camu_sink_entry *entry = (struct camu_sink_entry *)cmd->opaque;
+ sink->callback(sink->userdata, CAMU_SINK_REMOVE_BUFFER, CAMU_SINK_AUDIO, &entry->audio.buf);
+ break;
+ }
+#ifndef CAMU_SINK_NO_VIDEO
+ case CAMU_SINK_VIDEO: {
+ struct camu_sink_entry *entry = (struct camu_sink_entry *)cmd->opaque;
+ sink->callback(sink->userdata, CAMU_SINK_REMOVE_BUFFER, CAMU_SINK_VIDEO, &entry->video.buf);
+ break;
+ }
+#endif
+ }
+ break;
+ }
+ case START: {
+ struct camu_sink *sink = (struct camu_sink *)cmd->opaque;
+ switch (cmd->value.i) {
+ case CAMU_SINK_AUDIO:
+ if (sink->audio.state == SINK_PAUSED) {
+ sink->callback(sink->userdata, CAMU_SINK_START, CAMU_SINK_AUDIO, NULL);
+ sink->audio.state = SINK_PLAYING;
+ }
+ break;
+#ifndef CAMU_SINK_NO_VIDEO
+ case CAMU_SINK_VIDEO:
+ if (sink->video.state == SINK_PAUSED) {
+ sink->callback(sink->userdata, CAMU_SINK_START, CAMU_SINK_VIDEO, NULL);
+ sink->video.state = SINK_PLAYING;
+ }
+ break;
+#endif
+ }
+ break;
+ }
+ case STOP: {
+ struct camu_sink *sink = (struct camu_sink *)cmd->opaque;
+ switch (cmd->value.i) {
+ case CAMU_SINK_AUDIO:
+ if (sink->audio.state == SINK_PLAYING) {
+ sink->callback(sink->userdata, CAMU_SINK_STOP, CAMU_SINK_AUDIO, NULL);
+ sink->audio.state = SINK_PAUSED;
+ }
+ break;
+#ifndef CAMU_SINK_NO_VIDEO
+ case CAMU_SINK_VIDEO:
+ if (sink->video.state == SINK_PLAYING) {
+ sink->callback(sink->userdata, CAMU_SINK_STOP, CAMU_SINK_VIDEO, NULL);
+ sink->video.state = SINK_PAUSED;
+ }
+ break;
+#endif
+ }
+ break;
+ }
+ case TOGGLE_PAUSE: {
+ struct camu_sink_entry *entry = (struct camu_sink_entry *)cmd->opaque;
+ if (camu_clock_is_paused(&entry->clock)) {
+ camu_clock_resume(&entry->clock);
+ if (sink->audio.state == SINK_PAUSED) {
+ sink->callback(sink->userdata, CAMU_SINK_START, CAMU_SINK_AUDIO, NULL);
+ sink->audio.state = SINK_PLAYING;
+ }
+#ifndef CAMU_SINK_NO_VIDEO
+ if (sink->video.state == SINK_PAUSED) {
+ sink->callback(sink->userdata, CAMU_SINK_START, CAMU_SINK_VIDEO, NULL);
+ sink->video.state = SINK_PLAYING;
+ }
+#endif
+ } else {
+ camu_clock_pause(&entry->clock);
+#ifndef CAMU_SINK_NO_VIDEO
+ if (sink->video.state == SINK_PLAYING) {
+ sink->callback(sink->userdata, CAMU_SINK_STOP, CAMU_SINK_VIDEO, NULL);
+ sink->video.state = SINK_PAUSED;
+ }
+#endif
+ }
+ break;
+ }
+ case SEEK: {
+ struct camu_sink_entry *entry = (struct camu_sink_entry *)cmd->opaque;
+ bmu_local_seek(&entry->runner, cmd->value.f);
+ break;
+ }
+ case CLOSE: {
+ sink->callback(sink->userdata, CAMU_SINK_EXIT, 0, NULL);
+ break;
+ }
+ }
+}
+
+static void queue_signal_callback(void *userdata)
+{
+ struct camu_sink *sink = (struct camu_sink *)userdata;
+ u32 size;
+ struct camu_sink_cmd cmd;
+ do {
+ camu_queue_size(sink->queue, size);
+ if (!size) break;
+ camu_queue_pop(sink->queue, cmd);
+ handle_sink_cmd(sink, &cmd);
+ } while (1);
+}
+
+static void queue_cmd(struct camu_sink *sink, struct camu_sink_cmd cmd)
+{
+ camu_queue_push(sink->queue, cmd);
+ aki_signal_send(&sink->signal);
+}
+
+static void add_audio_if_set_and_buffered(struct camu_sink_entry *entry)
+{
+ u8 state = entry->audio.state;
+ if (state == BUFFER_SET_OR_BUFFERED) {
+ entry->sink->callback(entry->sink->userdata, CAMU_SINK_ADD_BUFFER, CAMU_SINK_AUDIO, &entry->audio.buf);
+ queue_cmd(entry->sink, (struct camu_sink_cmd){
+ .op = START,
+ .value.i = CAMU_SINK_AUDIO,
+ .opaque = entry->sink
+ });
+#ifndef CAMU_SINK_NO_VIDEO
+ if (entry->video.state == BUFFER_INIT || entry->video.state == BUFFER_ADDED) {
+#endif
+ camu_clock_resume(&entry->clock);
+#ifndef CAMU_SINK_NO_VIDEO
+ }
+#endif
+ state = BUFFER_ADDED;
+ } else if (state == BUFFER_CONFIGURED) {
+ state = BUFFER_SET_OR_BUFFERED;
+ }
+ entry->audio.state = state;
+}
+
+static void audio_buffer_callback(void *userdata, u8 op)
+{
+ struct camu_sink_entry *entry = (struct camu_sink_entry *)userdata;
+ switch (op) {
+ case CAMU_BUFFER_BUFFERED:
+ aki_mutex_lock(&entry->sink->mutex);
+ add_audio_if_set_and_buffered(entry);
+ aki_mutex_unlock(&entry->sink->mutex);
+ break;
+ case CAMU_BUFFER_STOP:
+ bmu_local_stream_stop((struct bmu_local_stream *)entry->audio.stream);
+ break;
+ case CAMU_BUFFER_CONTINUE:
+ bmu_local_stream_continue((struct bmu_local_stream *)entry->audio.stream);
+ break;
+ case CAMU_BUFFER_PAUSED:
+ queue_cmd(entry->sink, (struct camu_sink_cmd){
+ .op = STOP,
+ .value.i = CAMU_SINK_AUDIO,
+ .opaque = entry->sink
+ });
+ break;
+ case CAMU_BUFFER_EOF: {
+ aki_mutex_lock(&entry->sink->mutex);
+ u8 ret = entry->sink->callback(entry->sink->userdata, CAMU_SINK_SWAP_BUFFER, CAMU_SINK_AUDIO, &entry->audio.buf);
+ entry->audio.state = BUFFER_SET_OR_BUFFERED;
+ aki_mutex_unlock(&entry->sink->mutex);
+ if (ret != CAMU_SINK_BUFFERS_SWAPPED) {
+ queue_cmd(entry->sink, (struct camu_sink_cmd){
+ .op = STOP,
+ .value.i = CAMU_SINK_AUDIO,
+ .opaque = entry->sink
+ });
+ }
+ break;
+ }
+ }
+}
+
+#ifndef CAMU_SINK_NO_VIDEO
+static void add_video_if_set_and_buffered(struct camu_sink_entry *entry)
+{
+ u8 state = entry->video.state;
+ if (state == BUFFER_SET_OR_BUFFERED) {
+ entry->sink->callback(entry->sink->userdata, CAMU_SINK_ADD_BUFFER, CAMU_SINK_VIDEO, &entry->video.buf);
+ queue_cmd(entry->sink, (struct camu_sink_cmd){
+ .op = START,
+ .value.i = CAMU_SINK_VIDEO,
+ .opaque = entry->sink
+ });
+ if (entry->audio.state == BUFFER_INIT || entry->audio.state == BUFFER_ADDED) {
+ camu_clock_resume(&entry->clock);
+ }
+ state = BUFFER_ADDED;
+ } else if (state == BUFFER_CONFIGURED) {
+ state = BUFFER_SET_OR_BUFFERED;
+ }
+ entry->video.state = state;
+}
+
+static void video_buffer_callback(void *userdata, u8 op)
+{
+ struct camu_sink_entry *entry = (struct camu_sink_entry *)userdata;
+ switch (op) {
+ case CAMU_BUFFER_BUFFERED:
+ aki_mutex_lock(&entry->sink->mutex);
+ add_video_if_set_and_buffered(entry);
+ aki_mutex_unlock(&entry->sink->mutex);
+ break;
+ case CAMU_BUFFER_STOP:
+ bmu_local_stream_stop((struct bmu_local_stream *)entry->video.stream);
+ break;
+ case CAMU_BUFFER_CONTINUE:
+ bmu_local_stream_continue((struct bmu_local_stream *)entry->video.stream);
+ break;
+ case CAMU_BUFFER_EOF:
+ queue_cmd(entry->sink, (struct camu_sink_cmd){
+ .op = STOP,
+ .value.i = CAMU_SINK_VIDEO,
+ .opaque = entry->sink
+ });
+ break;
+ }
+}
+#endif
+
+static void client_callback(void *userdata, u8 op, struct bmu_client_stream *stream, void *opaque)
+{
+ struct camu_sink_entry *entry = (struct camu_sink_entry *)userdata;
+ switch (op) {
+ case BIMU_CLIENT_CONFIGURE: {
+ switch (stream->type) {
+ case BIMU_STREAM_AUDIO:
+ entry->audio.stream = stream;
+ camu_audio_buffer_configure(&entry->audio.buf, &stream->stream);
+ entry->audio.state = BUFFER_CONFIGURED;
+ break;
+#ifndef CAMU_SINK_NO_VIDEO
+ case BIMU_STREAM_VIDEO:
+ entry->video.stream = stream;
+ camu_video_buffer_configure(&entry->video.buf, &stream->stream);
+ entry->video.state = BUFFER_CONFIGURED;
+ break;
+#endif
+ }
+ break;
+ }
+ case BIMU_CLIENT_DATA: {
+ struct camu_frame *frame = (struct camu_frame *)opaque;
+ switch (stream->type) {
+ case BIMU_STREAM_AUDIO:
+ camu_audio_buffer_push(&entry->audio.buf, frame);
+ break;
+#ifndef CAMU_SINK_NO_VIDEO
+ case BIMU_STREAM_VIDEO:
+ camu_video_buffer_push(&entry->video.buf, frame);
+ break;
+#endif
+ default:
+#ifdef HAVE_FFMPEG
+ if (frame->type == CAMU_FFMPEG_COMPAT) {
+ av_frame_free(&frame->av.frame);
+ }
+#endif
+ al_free(frame);
+ }
+ break;
+ }
+ case BIMU_CLIENT_SEEK: {
+ f64 seek_req = *(f64 *)opaque;
+ aki_mutex_lock(&entry->sink->mutex);
+ if (entry->audio.state == BUFFER_ADDED) {
+ entry->sink->callback(entry->sink->userdata, CAMU_SINK_REMOVE_BUFFER, CAMU_SINK_AUDIO, &entry->audio.buf);
+ entry->audio.state = BUFFER_SET_OR_BUFFERED;
+ }
+#ifndef CAMU_SINK_NO_VIDEO
+ bool single_frame = camu_video_buffer_is_single_frame(&entry->video.buf);
+ if (entry->video.state == BUFFER_ADDED && !single_frame) {
+ entry->sink->callback(entry->sink->userdata, CAMU_SINK_REMOVE_BUFFER, CAMU_SINK_VIDEO, &entry->video.buf);
+ entry->video.state = BUFFER_SET_OR_BUFFERED;
+ }
+#endif
+ aki_mutex_unlock(&entry->sink->mutex);
+#ifndef CAMU_SINK_NO_VIDEO
+ if (single_frame) {
+ while (ENTRY_AUDIO_BUFFER_HELD(entry)) {
+ aki_thread_sleep(AKI_TS_FROM_USEC(200));
+ }
+ } else {
+#endif
+ while (ENTRY_BUFFERS_HELD(entry)) {
+ aki_thread_sleep(AKI_TS_FROM_USEC(200));
+ }
+#ifndef CAMU_SINK_NO_VIDEO
+ }
+#endif
+ camu_clock_seek(&entry->clock, seek_req);
+ camu_audio_buffer_reset(&entry->audio.buf);
+#ifndef CAMU_SINK_NO_VIDEO
+ if (!single_frame) {
+ camu_video_buffer_reset(&entry->video.buf);
+ }
+#endif
+ break;
+ }
+ case BIMU_CLIENT_EOF: {
+ switch (stream->type) {
+ case BIMU_STREAM_AUDIO: {
+ camu_audio_buffer_flush(&entry->audio.buf);
+ break;
+ }
+#ifndef CAMU_SINK_NO_VIDEO
+ case BIMU_STREAM_VIDEO: {
+ bool single_frame = camu_video_buffer_is_single_frame(&entry->video.buf);
+ if (!single_frame) {
+ camu_video_buffer_flush(&entry->video.buf);
+ }
+ break;
+ }
+#endif
+ }
+ break;
+ }
+ case BIMU_CLIENT_CLOSED: {
+ bmu_local_close(&entry->runner);
+ camu_audio_buffer_free(&entry->audio.buf);
+#ifndef CAMU_SINK_NO_VIDEO
+ camu_video_buffer_free(&entry->video.buf);
+#endif
+ cch_entry_free(&entry->entry);
+ al_free(entry);
+ al_log_debug("sink", "Entry closed.");
+ break;
+ }
+ }
+}
+
+bool camu_sink_init(struct camu_sink *sink, struct aki_event_loop *loop,
+ struct camu_mixer *mixer
+#ifndef CAMU_SINK_NO_VIDEO
+ , struct camu_renderer *renderer
+#endif
+ )
+{
+ sink->loop = loop;
+ aki_signal_init(&sink->signal, queue_signal_callback, sink);
+ aki_signal_start(&sink->signal, sink->loop);
+ camu_queue_init(sink->queue);
+ aki_mutex_init(&sink->mutex);
+ sink->current = NULL;
+ al_array_init(sink->entries);
+ sink->audio.mixer = mixer;
+ sink->audio.state = SINK_PAUSED;
+#ifndef CAMU_SINK_NO_VIDEO
+ sink->video.renderer = renderer;
+ sink->video.state = SINK_PAUSED;
+#endif
+ return true;
+}
+
+bool camu_sink_connect(struct camu_sink *sink, str *addr, s32 port)
+{
+ (void)sink;
+ (void)addr;
+ (void)port;
+ return false;
+}
+
+void camu_sink_toggle_pause(struct camu_sink *sink)
+{
+ aki_mutex_lock(&sink->mutex);
+ if (sink->current) {
+ queue_cmd(sink, (struct camu_sink_cmd){
+ .op = TOGGLE_PAUSE,
+ .opaque = sink->current
+ });
+ }
+ aki_mutex_unlock(&sink->mutex);
+}
+
+void camu_sink_seek(struct camu_sink *sink, f64 pos)
+{
+ aki_mutex_lock(&sink->mutex);
+ if (sink->current) {
+ queue_cmd(sink, (struct camu_sink_cmd){
+ .op = SEEK,
+ .value.f = pos,
+ .opaque = sink->current
+ });
+ }
+ aki_mutex_unlock(&sink->mutex);
+}
+
+void camu_sink_skip(struct camu_sink *sink, s32 n)
+{
+ (void)sink;
+ (void)n;
+}
+
+struct camu_sink_entry *camu_sink_get_current(struct camu_sink *sink)
+{
+ aki_mutex_lock(&sink->mutex);
+ return sink->current;
+}
+
+void camu_sink_return_current(struct camu_sink *sink)
+{
+ aki_mutex_unlock(&sink->mutex);
+}
+
+static void remove_entry_buffers(struct camu_sink *sink, struct camu_sink_entry *entry)
+{
+#ifndef CAMU_SINK_NO_VIDEO
+ if (entry->video.state == BUFFER_ADDED) {
+ sink->callback(sink->userdata, CAMU_SINK_REMOVE_BUFFER, CAMU_SINK_VIDEO, &entry->video.buf);
+ entry->video.state = BUFFER_SET_OR_BUFFERED;
+ }
+#endif
+ if (entry->audio.state == BUFFER_ADDED) {
+ sink->callback(sink->userdata, CAMU_SINK_REMOVE_BUFFER, CAMU_SINK_AUDIO, &entry->audio.buf);
+ entry->audio.state = BUFFER_SET_OR_BUFFERED;
+ }
+}
+
+void camu_sink_stop(struct camu_sink *sink)
+{
+ aki_mutex_lock(&sink->mutex);
+ if (sink->current) {
+ remove_entry_buffers(sink, sink->current);
+ sink->current = NULL;
+ }
+ aki_mutex_unlock(&sink->mutex);
+ queue_cmd(sink, (struct camu_sink_cmd){
+ .op = STOP,
+ .value.i = CAMU_SINK_AUDIO,
+ .opaque = sink
+ });
+ queue_cmd(sink, (struct camu_sink_cmd){
+ .op = CLOSE
+ });
+}
+
+void camu_sink_close(struct camu_sink *sink)
+{
+ aki_mutex_lock(&sink->mutex);
+ struct camu_sink_entry *entry;
+ al_array_foreach(sink->entries, i, entry) {
+ bmu_local_stop(&entry->runner);
+ }
+ aki_mutex_unlock(&sink->mutex);
+ al_array_free(sink->entries);
+ aki_signal_stop(&sink->signal);
+ camu_queue_free(sink->queue);
+ aki_mutex_destroy(&sink->mutex);
+}
+
+static struct camu_sink_entry *entry_from_unique_id(struct camu_sink *sink, str *unique_id)
+{
+ struct camu_sink_entry *entry;
+ al_array_foreach(sink->entries, i, entry) {
+ if (al_str_eq(&entry->unique_id, unique_id)) {
+ aki_mutex_unlock(&sink->mutex);
+ return entry;
+ }
+ }
+ return NULL;
+}
+
+bool camu_sink_local_buffer(struct camu_sink *sink, str *unique_id, struct cch_entry *centry)
+{
+ aki_mutex_lock(&sink->mutex);
+
+ bool add_entry = false;
+ struct camu_sink_entry *entry = entry_from_unique_id(sink, unique_id);
+ if (!entry) {
+ entry = al_alloc_object(struct camu_sink_entry);
+ add_entry = true;
+ }
+ entry->state = ENTRY_LOADED;
+ if (!add_entry) {
+ queue_cmd(sink, (struct camu_sink_cmd){
+ .op = SEEK,
+ .value.f = 0,
+ .opaque = entry
+ });
+ return true;
+ }
+
+ entry->entry = centry;
+ if (!bmu_local_init(&entry->runner, centry)) {
+ al_free(entry);
+ aki_mutex_unlock(&sink->mutex);
+ return false;
+ }
+
+ entry->sink = sink;
+ al_str_clone(&entry->unique_id, unique_id);
+
+ camu_clock_init(&entry->clock);
+
+ entry->audio.state = BUFFER_INIT;
+ camu_audio_buffer_init(&entry->audio.buf, &entry->clock, sink->audio.mixer);
+ entry->audio.buf.callback = audio_buffer_callback;
+ entry->audio.buf.userdata = entry;
+
+ struct camu_renderer *renderer = NULL;
+#ifndef CAMU_SINK_NO_VIDEO
+ renderer = sink->video.renderer;
+ entry->video.state = BUFFER_INIT;
+ camu_video_buffer_init(&entry->video.buf, &entry->clock, renderer);
+ entry->video.buf.callback = video_buffer_callback;
+ entry->video.buf.userdata = entry;
+#endif
+
+ entry->runner.callback = client_callback;
+ entry->runner.userdata = entry;
+ bmu_local_prepare_clients(&entry->runner, renderer);
+ bmu_local_run(&entry->runner);
+
+ if (add_entry) al_array_push(sink->entries, entry);
+
+ aki_mutex_unlock(&sink->mutex);
+
+ return true;
+}
+
+static void maybe_cleanup_entries(struct camu_sink *sink)
+{
+ struct camu_sink_entry *entry;
+ al_array_foreach(sink->entries, i, entry) {
+ if (entry->state == ENTRY_DISREGUARDED && !ENTRY_BUFFERS_HELD(entry)) {
+ bmu_local_stop(&entry->runner);
+ al_array_remove_at_iter(sink->entries, i);
+ }
+ }
+}
+
+void camu_sink_local_set(struct camu_sink *sink, str *unique_id)
+{
+ aki_mutex_lock(&sink->mutex);
+ struct camu_sink_entry *entry = entry_from_unique_id(sink, unique_id);
+ if (sink->current) {
+ if (!camu_clock_is_paused(&sink->current->clock)) {
+ camu_clock_pause(&sink->current->clock);
+ }
+ remove_entry_buffers(sink, sink->current);
+ }
+ sink->current = entry;
+ add_audio_if_set_and_buffered(entry);
+#ifndef CAMU_SINK_NO_VIDEO
+ add_video_if_set_and_buffered(entry);
+#endif
+ maybe_cleanup_entries(sink);
+ aki_mutex_unlock(&sink->mutex);
+}
+
+void camu_sink_local_swap(struct camu_sink *sink, str *unique_id)
+{
+ struct camu_sink_entry *entry = entry_from_unique_id(sink, unique_id);
+ sink->current = entry;
+ add_audio_if_set_and_buffered(entry);
+#ifndef CAMU_SINK_NO_VIDEO
+ add_video_if_set_and_buffered(entry);
+#endif
+}
+
+void camu_sink_local_unload(struct camu_sink *sink, str *unique_id)
+{
+ aki_mutex_lock(&sink->mutex);
+ struct camu_sink_entry *entry = entry_from_unique_id(sink, unique_id);
+ if (entry->state == ENTRY_LOADED) entry->state = ENTRY_DISREGUARDED;
+ aki_mutex_unlock(&sink->mutex);
+}
diff --git a/src/libsink/sink.h b/src/libsink/sink.h
new file mode 100644
index 0000000..e105245
--- /dev/null
+++ b/src/libsink/sink.h
@@ -0,0 +1,109 @@
+#pragma once
+
+//#define CAMU_SINK_NO_VIDEO
+
+#include <al/str.h>
+#include <al/array.h>
+#include <aki/signal.h>
+#include <aki/timer.h>
+
+#include "../util/queue.h"
+
+#include "../cache/entry.h"
+
+#include "../buffer/clock.h"
+#include "../buffer/audio.h"
+#ifndef CAMU_SINK_NO_VIDEO
+#include "../buffer/video.h"
+#endif
+
+#include "../bimu/local.h"
+
+enum {
+ CAMU_SINK_AUDIO = 0,
+#ifndef CAMU_SINK_NO_VIDEO
+ CAMU_SINK_VIDEO
+#endif
+};
+
+enum {
+ CAMU_SINK_ADD_BUFFER = 0,
+ CAMU_SINK_REMOVE_BUFFER,
+ CAMU_SINK_SWAP_BUFFER,
+ CAMU_SINK_START,
+ CAMU_SINK_STOP,
+ CAMU_SINK_EXIT
+};
+
+struct camu_sink_entry {
+ u8 state;
+ str unique_id;
+ struct cch_entry *entry;
+ struct bmu_local runner;
+ struct camu_clock clock;
+ struct {
+ u8 state;
+ struct camu_audio_buffer buf;
+ struct bmu_client_stream *stream;
+ } audio;
+#ifndef CAMU_SINK_NO_VIDEO
+ struct {
+ u8 state;
+ struct camu_video_buffer buf;
+ struct bmu_client_stream *stream;
+ } video;
+#endif
+ struct camu_sink *sink;
+};
+
+struct camu_sink_cmd {
+ u8 op;
+ union { s64 i; f64 f; } value;
+ void *opaque;
+};
+
+enum {
+ CAMU_SINK_OK = 0,
+ CAMU_SINK_BUFFERS_SWAPPED = 1
+};
+
+struct camu_sink {
+ struct aki_event_loop *loop;
+ struct aki_signal signal;
+ queue(struct camu_sink_cmd) queue;
+ struct aki_mutex mutex;
+ struct camu_sink_entry *current;
+ array(struct camu_sink_entry *) entries;
+ struct {
+ u8 state;
+ struct camu_mixer *mixer;
+ } audio;
+#ifndef CAMU_SINK_NO_VIDEO
+ struct {
+ u8 state;
+ struct camu_renderer *renderer;
+ } video;
+#endif
+ u8 (*callback)(void *, u8, u8, void *);
+ void *userdata;
+};
+
+bool camu_sink_init(struct camu_sink *sink, struct aki_event_loop *loop,
+ struct camu_mixer *mixer
+#ifndef CAMU_SINK_NO_VIDEO
+ , struct camu_renderer *renderer
+#endif
+ );
+bool camu_sink_connect(struct camu_sink *sink, str *addr, s32 port);
+void camu_sink_toggle_pause(struct camu_sink *sink);
+void camu_sink_seek(struct camu_sink *sink, f64 pos);
+void camu_sink_skip(struct camu_sink *sink, s32 n);
+struct camu_sink_entry *camu_sink_get_current(struct camu_sink *sink);
+void camu_sink_return_current(struct camu_sink *sink);
+void camu_sink_stop(struct camu_sink *sink);
+void camu_sink_close(struct camu_sink *sink);
+
+bool camu_sink_local_buffer(struct camu_sink *sink, str *unique_id, struct cch_entry *entry);
+void camu_sink_local_set(struct camu_sink *sink, str *unique_id);
+void camu_sink_local_swap(struct camu_sink *sink, str *unique_id);
+void camu_sink_local_unload(struct camu_sink *sink, str *unique_id);