diff options
Diffstat (limited to 'src')
150 files changed, 10888 insertions, 0 deletions
diff --git a/src/bimu/client.c b/src/bimu/client.c new file mode 100644 index 0000000..f679c0d --- /dev/null +++ b/src/bimu/client.c @@ -0,0 +1 @@ +#include "client.h" diff --git a/src/bimu/client.h b/src/bimu/client.h new file mode 100644 index 0000000..ec48275 --- /dev/null +++ b/src/bimu/client.h @@ -0,0 +1,11 @@ +#pragma once + +#include <al/types.h> + +#include "handlers/codec.h" + +struct bmu_client_stream { + u8 type; + s32 index; + struct camu_stream stream; +}; diff --git a/src/bimu/common.h b/src/bimu/common.h new file mode 100644 index 0000000..1c4897a --- /dev/null +++ b/src/bimu/common.h @@ -0,0 +1,7 @@ +#pragma once + +enum { + BIMU_STREAM_AUDIO = 0, + BIMU_STREAM_VIDEO, + BIMU_STREAM_UNKNOWN, +}; diff --git a/src/bimu/handler.h b/src/bimu/handler.h new file mode 100644 index 0000000..e51bc2d --- /dev/null +++ b/src/bimu/handler.h @@ -0,0 +1,35 @@ +#pragma once + +#include <aki/packet_pool.h> + +#include "../codec/codec.h" +#include "../cache/handle.h" + +struct bmu_server_handler { + bool (*init)(struct bmu_server_handler *, struct cch_handle *, struct aki_packet_pool *); + void (*write_info)(struct bmu_server_handler *, struct aki_packet *); + s64 (*get_duration)(struct bmu_server_handler *); + bool (*seek)(struct bmu_server_handler *, s64); + bool (*step)(struct bmu_server_handler *); + void (*free)(struct bmu_server_handler **); +}; + +enum { + BIMU_CLIENT_CONFIGURE = 0, + BIMU_CLIENT_DATA, + BIMU_CLIENT_SEEK, + BIMU_CLIENT_EOF, + BIMU_CLIENT_CLOSED +}; + +struct bmu_client_stream; +struct bmu_client_handler { + bool (*init)(struct bmu_client_handler *, struct camu_renderer *, struct bmu_client_stream *); + void (*handle_data_packet)(struct bmu_client_handler *, struct aki_packet *); + void (*handle_eof)(struct bmu_client_handler *); + void (*flush)(struct bmu_client_handler *); + void (*free)(struct bmu_client_handler **); + struct bmu_client_stream *stream; + void (*callback)(void *, u8, struct bmu_client_stream *stream, void *); + void *userdata; +}; diff --git a/src/bimu/handlers/codec.h b/src/bimu/handlers/codec.h new file mode 100644 index 0000000..a8031b1 --- /dev/null +++ b/src/bimu/handlers/codec.h @@ -0,0 +1,20 @@ +#pragma once + +#include "../handler.h" + +#include "../../codec/codec.h" + +struct bmu_codec_server { + struct bmu_server_handler handler; + struct camu_demuxer *demux; + struct camu_packet packet; + struct aki_packet_pool *pool; +}; + +struct bmu_codec_client { + struct bmu_client_handler handler; + struct camu_decoder *dec; +}; + +struct bmu_server_handler *bmu_codec_server_create(void); +struct bmu_client_handler *bmu_codec_client_create(void); diff --git a/src/bimu/handlers/codec_client.c b/src/bimu/handlers/codec_client.c new file mode 100644 index 0000000..ffe8c16 --- /dev/null +++ b/src/bimu/handlers/codec_client.c @@ -0,0 +1,109 @@ +#include "../../codec/libav/decoder.h" +#include "../../codec/libav/packet_ext.h" +#include "../../codec/stb_image/decoder.h" +#include "../../codec/wuffs/decoder.h" +#include "../../codec/spng/decoder.h" + +#include "../../bimu/local.h" + +#include "codec.h" + +static void data_callback(void *userdata, struct camu_frame *frame) +{ + struct bmu_codec_client *codec = (struct bmu_codec_client *)userdata; + codec->handler.callback(codec->handler.userdata, BIMU_CLIENT_DATA, codec->handler.stream, frame); +} + +static bool codec_client_init(struct bmu_client_handler *handler, struct camu_renderer *renderer, + struct bmu_client_stream *stream) +{ + struct bmu_codec_client *codec = (struct bmu_codec_client *)handler; + codec->dec = camu_lav_decoder_create(); + //codec->dec = camu_spng_decoder_create(); + //codec->dec = camu_stbi_decoder_create(); + //codec->dec = camu_wuffs_decoder_create(); + codec->handler.stream = stream; + if (!codec->dec->init(codec->dec, renderer, &stream->stream)) { + return false; + } + codec->dec->stream = &stream->stream; + codec->handler.callback(codec->handler.userdata, BIMU_CLIENT_CONFIGURE, codec->handler.stream, NULL); + codec->dec->set_callback(codec->dec, data_callback, codec); + return true; +} + +#ifdef HAVE_FFMPEG +static void push_av_packet(struct bmu_codec_client *codec, AVPacket *pkt) +{ + struct camu_packet packet; + packet.av.pkt = pkt; + s32 ret = codec->dec->push(codec->dec, &packet); + av_packet_unref(pkt); + av_packet_free(&pkt); + al_assert(ret == CAMU_OK); +} +#endif + +static void push_packet(struct bmu_codec_client *codec, struct aki_buffer *buffer) +{ + struct camu_packet packet; + packet.buffer = buffer; + s32 ret = codec->dec->push(codec->dec, &packet); + al_assert(ret == CAMU_OK); +} + +static void codec_client_handle_data_packet(struct bmu_client_handler *handler, struct aki_packet *packet) +{ + struct bmu_codec_client *codec = (struct bmu_codec_client *)handler; + switch (aki_packet_read_u8(packet)) { + case CAMU_NORMAL: { + struct aki_buffer buffer; + aki_packet_read_buffer(packet, &buffer); + push_packet(codec, &buffer); + break; + } +#ifdef HAVE_FFMPEG + case CAMU_FFMPEG_COMPAT: { + push_av_packet(codec, aki_packet_read_av_packet(packet)); + break; + } +#endif + } + s32 ret = codec->dec->process(codec->dec); + al_assert(ret == CAMU_ERR_AGAIN || ret == CAMU_ERR_EOF); +} + +static void codec_client_handle_eof(struct bmu_client_handler *handler) +{ + struct bmu_codec_client *codec = (struct bmu_codec_client *)handler; + s32 ret = codec->dec->push(codec->dec, NULL); + // Flush returns success. + ret = codec->dec->process(codec->dec); + al_assert(ret == CAMU_ERR_EOF); + codec->handler.callback(codec->handler.userdata, BIMU_CLIENT_EOF, codec->handler.stream, NULL); +} + +static void codec_client_flush(struct bmu_client_handler *handler) +{ + struct bmu_codec_client *codec = (struct bmu_codec_client *)handler; + codec->dec->flush(codec->dec); +} + +static void codec_client_free(struct bmu_client_handler **handler) +{ + struct bmu_codec_client *codec = (struct bmu_codec_client *)*handler; + codec->dec->free(&codec->dec); + al_free(codec); + *handler = NULL; +} + +struct bmu_client_handler *bmu_codec_client_create(void) +{ + struct bmu_codec_client *codec = al_alloc_object(struct bmu_codec_client); + codec->handler.init = codec_client_init; + codec->handler.handle_data_packet = codec_client_handle_data_packet; + codec->handler.handle_eof = codec_client_handle_eof; + codec->handler.flush = codec_client_flush; + codec->handler.free = codec_client_free; + return (struct bmu_client_handler *)codec; +} diff --git a/src/bimu/handlers/codec_server.c b/src/bimu/handlers/codec_server.c new file mode 100644 index 0000000..b555370 --- /dev/null +++ b/src/bimu/handlers/codec_server.c @@ -0,0 +1,159 @@ +#include "../../codec/codec.h" +#include "../../codec/libav/demuxer.h" +#include "../../codec/libav/packet_ext.h" +#include "../../codec/stb_image/demuxer.h" +#include "../../codec/wuffs/demuxer.h" +#include "../../codec/spng/demuxer.h" + +#include "../handler.h" + +#include "codec.h" + +static bool codec_server_init(struct bmu_server_handler *handler, struct cch_handle *handle, struct aki_packet_pool *pool) +{ + struct bmu_codec_server *codec = (struct bmu_codec_server *)handler; + codec->demux = camu_lav_demuxer_create(); + //codec->demux = camu_spng_demuxer_create(); + //codec->demux = camu_stbi_demuxer_create(); + //codec->demux = camu_wuffs_demuxer_create(); + if (!codec->demux->init(codec->demux, handle)) { + return false; + } + switch (codec->demux->type) { + case CAMU_NORMAL: + break; +#ifdef HAVE_FFMPEG + case CAMU_FFMPEG_COMPAT: + codec->packet.av.pkt = av_packet_alloc(); + break; +#endif + } + codec->pool = pool; + return true; +} + +static void codec_server_write_info(struct bmu_server_handler *handler, struct aki_packet *packet) +{ + struct bmu_codec_server *codec = (struct bmu_codec_server *)handler; + aki_packet_write_u32(packet, codec->demux->streams.size); + struct camu_stream *stream; + al_array_foreach_ptr(codec->demux->streams, i, stream) { + aki_packet_write_u8(packet, stream->type); + switch (stream->type) { + case CAMU_NORMAL: + aki_packet_write_s32(packet, stream->video.width); + aki_packet_write_s32(packet, stream->video.height); + break; +#ifdef HAVE_FFMPEG + case CAMU_FFMPEG_COMPAT: + aki_packet_write_av_codec_id(packet, stream->av.stream->codecpar->codec_id); + aki_packet_write_av_stream(packet, stream->av.stream); + break; +#endif + } + } +} + +static s64 codec_server_get_duration(struct bmu_server_handler *handler) +{ + struct bmu_codec_server *codec = (struct bmu_codec_server *)handler; + return codec->demux->get_duration(codec->demux); +} + +static bool codec_server_seek(struct bmu_server_handler *handler, s64 pos) +{ + struct bmu_codec_server *codec = (struct bmu_codec_server *)handler; + return codec->demux->seek(codec->demux, pos); +} + +static bool codec_server_step(struct bmu_server_handler *handler) +{ + struct bmu_codec_server *codec = (struct bmu_codec_server *)handler; + struct aki_packet *packet = aki_packet_pool_get(codec->pool); + if (!packet) return false; + aki_packet_write_u8(packet, 0); + s32 ret = codec->demux->get_packet(codec->demux, &codec->packet); + do { + if (ret == CAMU_OK) { + switch (codec->packet.type) { + case CAMU_NORMAL: { + aki_packet_write_s32(packet, 0); + aki_packet_write_u8(packet, 0); + aki_packet_write_u8(packet, codec->packet.type); + aki_packet_write_buffer(packet, codec->packet.buffer); + break; + } +#ifdef HAVE_FFMPEG + case CAMU_FFMPEG_COMPAT: { + AVPacket *pkt = codec->packet.av.pkt; + aki_packet_write_s32(packet, pkt->stream_index); + aki_packet_write_u8(packet, 0); + aki_packet_write_u8(packet, codec->packet.type); + aki_packet_write_av_packet(packet, pkt); + av_packet_unref(pkt); + break; + } +#endif + } + aki_packet_pool_submit(codec->pool, packet); + } else if (ret == CAMU_ERR_EOF) { + struct camu_stream *stream; + al_array_foreach_ptr(codec->demux->streams, i, stream) { + if (i != 0) { // HACK + packet = aki_packet_pool_get(codec->pool); + if (!packet) return false; + aki_packet_write_u8(packet, 0); + } + switch (stream->type) { + case CAMU_NORMAL: + aki_packet_write_s32(packet, 0); + break; +#ifdef HAVE_FFMPEG + case CAMU_FFMPEG_COMPAT: + aki_packet_write_s32(packet, stream->av.stream->index); + break; +#endif + } + aki_packet_write_u8(packet, 1); + aki_packet_pool_submit(codec->pool, packet); + } + return false; + } else { + aki_packet_write_s32(packet, -2); + aki_packet_write_u8(packet, 2); + aki_packet_pool_submit(codec->pool, packet); + return false; + } + break; + } while (1); + return true; +} + +static void codec_server_free(struct bmu_server_handler **handler) +{ + struct bmu_codec_server *codec = (struct bmu_codec_server *)*handler; + switch (codec->demux->type) { + case CAMU_NORMAL: + break; +#ifdef HAVE_FFMPEG + case CAMU_FFMPEG_COMPAT: + av_packet_free(&codec->packet.av.pkt); + break; +#endif + } + codec->demux->free(&codec->demux); + al_free(codec); + *handler = NULL; +} + +struct bmu_server_handler *bmu_codec_server_create(void) +{ + struct bmu_codec_server *codec = al_alloc_object(struct bmu_codec_server); + codec->handler.init = codec_server_init; + codec->handler.write_info = codec_server_write_info; + codec->handler.get_duration = codec_server_get_duration; + codec->handler.seek = codec_server_seek; + codec->handler.step = codec_server_step; + codec->handler.free = codec_server_free; + return (struct bmu_server_handler *)codec; +} diff --git a/src/bimu/local.c b/src/bimu/local.c new file mode 100644 index 0000000..3f62f75 --- /dev/null +++ b/src/bimu/local.c @@ -0,0 +1,281 @@ +#include "../codec/libav/packet_ext.h" + +#include "local.h" + +#define PACKETS 1400 + +static struct bmu_local_stream *stream_at_index(struct bmu_local *runner, s32 index) +{ + struct bmu_local_stream *stream = NULL; + al_array_foreach(runner->streams, i, stream) { + if (stream->s.index == index) { + return stream; + } + } + return NULL; +} + +static aki_thread_result AKI_THREADCALL stream_thread(void *userdata) +{ + struct bmu_local_stream *stream = (struct bmu_local_stream *)userdata; + do { + if (!aki_packet_cache_wait(&stream->cache)) break; + struct aki_packet *packet = aki_packet_cache_pop(&stream->cache); + if (!packet) break; + aki_mutex_lock(&stream->mutex); + if (al_atomic_s32_load(&stream->state, AL_ATOMIC_RELAXED) == BIMU_LOCAL_STOPPED) { + aki_cond_wait(&stream->cond, &stream->mutex); + } + s32 state = al_atomic_s32_load(&stream->state, AL_ATOMIC_RELAXED); + aki_mutex_unlock(&stream->mutex); + if (state == BIMU_LOCAL_CLOSED) { + aki_packet_pool_return(&stream->runner->pool, packet); + break; + } + switch (aki_packet_read_u8(packet)) { + case 0: + stream->client->handle_data_packet(stream->client, packet); + break; + case 1: + stream->client->handle_eof(stream->client); + break; + } + aki_packet_pool_return(&stream->runner->pool, packet); + } while (1); + return 0; +} + +static u8 packet_pool_callback(void *userdata, struct aki_packet *packet) +{ + struct bmu_local *runner = (struct bmu_local *)userdata; + if (!packet) { + aki_event_loop_break(&runner->loop); + return AKI_PACKET_POOL_CLOSED; + } + aki_packet_write_size(packet); + u8 close = aki_packet_read_u8(packet); + if (close) return AKI_PACKET_POOL_DISABLE; + struct bmu_local_stream *stream = NULL; + s32 index = aki_packet_read_s32(packet); + if (index >= 0) { + stream = stream_at_index(runner, index); + if (!stream) return AKI_PACKET_POOL_RETURN; + if (al_atomic_s32_load(&stream->state, AL_ATOMIC_RELAXED) != BIMU_LOCAL_CLOSED) { + if (aki_packet_cache_send_packet(&stream->cache, packet)) { + return AKI_PACKET_POOL_KEEP; + } + } + } + return AKI_PACKET_POOL_RETURN; +} + +bool bmu_local_init(struct bmu_local *runner, struct cch_entry *entry) +{ + runner->entry = entry; + runner->server = bmu_codec_server_create(); + cch_entry_get_handle(runner->entry, &runner->handle); + aki_event_loop_init(&runner->loop); + aki_packet_pool_init(&runner->pool, PACKETS, &runner->loop, packet_pool_callback, runner); + if (!runner->server->init(runner->server, &runner->handle, &runner->pool)) { + return false; + } + al_array_init(runner->streams); + al_atomic_bool_store(&runner->closed, false, AL_ATOMIC_RELAXED); + return true; +} + +bool bmu_local_prepare_clients(struct bmu_local *runner, struct camu_renderer *renderer) +{ + struct aki_packet *packet = aki_packet_create(); + runner->server->write_info(runner->server, packet); + aki_packet_write_size(packet); + u32 count = aki_packet_read_u32(packet); + for (u32 i = 0; i < count; i++) { + struct bmu_local_stream *local = al_alloc_object(struct bmu_local_stream); + struct bmu_client_stream *stream = &local->s; + local->runner = runner; + local->client = bmu_codec_client_create(); + local->client->callback = runner->callback; + local->client->userdata = runner->userdata; + aki_cond_init(&local->cond); + aki_mutex_init(&local->mutex); + aki_packet_cache_init(&local->cache, PACKETS); + al_atomic_s32_store(&local->state, BIMU_LOCAL_RUNNING, AL_ATOMIC_RELAXED); + stream->stream.type = aki_packet_read_u8(packet); + switch (stream->stream.type) { + case CAMU_NORMAL: + stream->index = 0; + stream->type = BIMU_STREAM_VIDEO; + stream->stream.video.width = aki_packet_read_s32(packet); + stream->stream.video.height = aki_packet_read_s32(packet); + break; +#ifdef HAVE_FFMPEG + case CAMU_FFMPEG_COMPAT: { + stream->stream.type = CAMU_FFMPEG_COMPAT; + stream->stream.av.format_context = avformat_alloc_context(); + const AVCodec *codec = avcodec_find_decoder(aki_packet_read_av_codec_id(packet)); + stream->stream.av.stream = aki_packet_read_av_stream(stream->stream.av.format_context, codec, packet); + stream->index = stream->stream.av.stream->index; + switch (stream->stream.av.stream->codecpar->codec_type) { + case AVMEDIA_TYPE_AUDIO: + stream->type = BIMU_STREAM_AUDIO; + break; + case AVMEDIA_TYPE_VIDEO: + stream->type = BIMU_STREAM_VIDEO; + break; + case AVMEDIA_TYPE_SUBTITLE: + default: + stream->type = BIMU_STREAM_UNKNOWN; + break; + } + } +#endif + } + if (!local->client->init(local->client, renderer, stream)) { + aki_packet_free(packet); + return false; + } + aki_thread_create(&local->thread, stream_thread, stream); + al_array_push(runner->streams, local); + } + aki_packet_free(packet); + return true; +} + +void bmu_local_stream_stop(struct bmu_local_stream *stream) +{ + aki_mutex_lock(&stream->mutex); + if (al_atomic_s32_load(&stream->state, AL_ATOMIC_RELAXED) == BIMU_LOCAL_RUNNING) { + al_atomic_s32_store(&stream->state, BIMU_LOCAL_STOPPED, AL_ATOMIC_RELAXED); + } + aki_mutex_unlock(&stream->mutex); +} + +void bmu_local_stream_continue(struct bmu_local_stream *stream) +{ + aki_mutex_lock(&stream->mutex); + al_atomic_s32_store(&stream->state, BIMU_LOCAL_RUNNING, AL_ATOMIC_RELAXED); + if (aki_cond_is_waiting(&stream->cond)) { + aki_cond_signal(&stream->cond); + } + aki_mutex_unlock(&stream->mutex); +} + +void bmu_local_stream_close(struct bmu_local_stream *stream) +{ + aki_mutex_lock(&stream->mutex); + u8 state = al_atomic_s32_load(&stream->state, AL_ATOMIC_RELAXED); + if (state == BIMU_LOCAL_CLOSED) { + aki_mutex_unlock(&stream->mutex); + return; + } + al_atomic_s32_store(&stream->state, BIMU_LOCAL_CLOSED, AL_ATOMIC_RELAXED); + aki_packet_cache_disable(&stream->cache); + if (aki_cond_is_waiting(&stream->cond)) { + aki_cond_signal(&stream->cond); + } + aki_mutex_unlock(&stream->mutex); + aki_thread_join(&stream->thread); +} + +static aki_thread_result AKI_THREADCALL decode_thread(void *userdata) +{ + struct bmu_local *runner = (struct bmu_local *)userdata; + while (!al_atomic_bool_load(&runner->closed, AL_ATOMIC_RELAXED) + && runner->server->step(runner->server)) {} + return 0; +} + +static aki_thread_result AKI_THREADCALL event_loop_thread(void *userdata) +{ + struct bmu_local *runner = (struct bmu_local *)userdata; + aki_event_loop_run(&runner->loop); + return 0; +} + +void bmu_local_run(struct bmu_local *runner) +{ + aki_thread_create(&runner->decode_thread, decode_thread, runner); + aki_thread_create(&runner->event_loop_thread, event_loop_thread, runner); +} + +f64 bmu_local_get_duration(struct bmu_local *runner) +{ + return runner->server->get_duration(runner->server) / 1000000.0; +} + +void bmu_local_seek(struct bmu_local *runner, f64 percent) +{ + s64 pos = runner->server->get_duration(runner->server) * percent; + f64 fpos = pos / 1000000.0; + al_atomic_bool_store(&runner->closed, true, AL_ATOMIC_RELAXED); + aki_packet_pool_disable(&runner->pool); + struct bmu_local_stream *stream = NULL; + al_array_foreach(runner->streams, i, stream) { + bmu_local_stream_close(stream); + struct aki_packet *packet; + while ((packet = aki_packet_cache_pop(&stream->cache))) { + aki_packet_pool_return(&stream->runner->pool, packet); + } + } + aki_thread_join(&runner->decode_thread); + runner->callback(runner->userdata, BIMU_CLIENT_SEEK, NULL, &fpos); + runner->server->seek(runner->server, pos); + al_atomic_bool_store(&runner->closed, false, AL_ATOMIC_RELAXED); + aki_packet_pool_enable(&runner->pool); + al_array_foreach(runner->streams, i, stream) { + stream->client->flush(stream->client); + al_atomic_s32_store(&stream->state, BIMU_LOCAL_RUNNING, AL_ATOMIC_RELAXED); + aki_packet_cache_enable(&stream->cache); + aki_thread_create(&stream->thread, stream_thread, stream); + } + aki_thread_create(&runner->decode_thread, decode_thread, runner); +} + +void bmu_local_stop(struct bmu_local *runner) +{ + al_atomic_bool_store(&runner->closed, true, AL_ATOMIC_RELAXED); + cch_handle_disable(&runner->handle); + struct bmu_local_stream *stream = NULL; + al_array_foreach(runner->streams, i, stream) { + bmu_local_stream_close(stream); + } + aki_thread_join(&runner->decode_thread); + cch_entry_return_handle(runner->entry, &runner->handle); + struct aki_packet *packet = aki_packet_pool_get(&runner->pool); + if (!packet) al_assert(false); + aki_packet_write_u8(packet, 1); + aki_packet_pool_submit(&runner->pool, packet); + aki_thread_join(&runner->event_loop_thread); + runner->callback(runner->userdata, BIMU_CLIENT_CLOSED, NULL, NULL); +} + +void bmu_local_close(struct bmu_local *runner) +{ + struct bmu_local_stream *stream = NULL; + al_array_foreach(runner->streams, i, stream) { + stream->client->free(&stream->client); + switch (stream->s.stream.type) { + case CAMU_NORMAL: + break; +#ifdef HAVE_FFMPEG + case CAMU_FFMPEG_COMPAT: + avformat_free_context(stream->s.stream.av.format_context); + break; +#endif + } + struct aki_packet *packet; + while ((packet = aki_packet_cache_pop(&stream->cache))) { + aki_packet_pool_return(&stream->runner->pool, packet); + } + aki_mutex_destroy(&stream->mutex); + aki_cond_destroy(&stream->cond); + aki_packet_cache_free(&stream->cache); + al_free(stream); + al_array_remove_at_iter(runner->streams, i); + } + al_array_free(runner->streams); + runner->server->free(&runner->server); + aki_packet_pool_free(&runner->pool); + aki_event_loop_destroy(&runner->loop); +} diff --git a/src/bimu/local.h b/src/bimu/local.h new file mode 100644 index 0000000..1928249 --- /dev/null +++ b/src/bimu/local.h @@ -0,0 +1,53 @@ +#pragma once + +#include <al/atomic.h> +#include <aki/packet_pool.h> +#include <aki/packet_cache.h> +#include <aki/signal.h> + +#include "../cache/entry.h" + +#include "client.h" +#include "common.h" + +enum { + BIMU_LOCAL_RUNNING = 0, + BIMU_LOCAL_STOPPED, + BIMU_LOCAL_CLOSED +}; + +struct bmu_local_stream { + struct bmu_client_stream s; + struct bmu_client_handler *client; + atomic_s32 state; + struct aki_cond cond; + struct aki_mutex mutex; + struct aki_packet_cache cache; + struct aki_thread thread; + struct bmu_local *runner; +}; + +struct bmu_local { + struct cch_entry *entry; + struct cch_handle handle; + struct bmu_server_handler *server; + struct aki_thread decode_thread; + struct aki_event_loop loop; + struct aki_packet_pool pool; + struct aki_thread event_loop_thread; + atomic_bool closed; + array(struct bmu_local_stream *) streams; + void (*callback)(void *, u8, struct bmu_client_stream *stream, void *); + void *userdata; +}; + +bool bmu_local_init(struct bmu_local *runner, struct cch_entry *entry); +bool bmu_local_prepare_clients(struct bmu_local *runner, struct camu_renderer *renderer); +void bmu_local_stream_stop(struct bmu_local_stream *stream); +void bmu_local_stream_continue(struct bmu_local_stream *stream); +void bmu_local_stream_close(struct bmu_local_stream *stream); +void bmu_local_run(struct bmu_local *runner); +f64 bmu_local_get_duration(struct bmu_local *runner); +void bmu_local_seek(struct bmu_local *runner, f64 percent); +void bmu_local_stop(struct bmu_local *runner); +void bmu_local_close(struct bmu_local *runner); diff --git a/src/bimu/meson.build b/src/bimu/meson.build new file mode 100644 index 0000000..086114d --- /dev/null +++ b/src/bimu/meson.build @@ -0,0 +1,8 @@ +bimu_src = [ + 'local.c', + 'server.c', + 'client.c', + 'handlers/codec_server.c', + 'handlers/codec_client.c' +] +bimu = declare_dependency(sources: bimu_src) diff --git a/src/bimu/server.c b/src/bimu/server.c new file mode 100644 index 0000000..c1b0c38 --- /dev/null +++ b/src/bimu/server.c @@ -0,0 +1,46 @@ +#include "server.h" + +static void packet_callback(void *userdata, struct aki_packet_stream *stream, struct aki_packet *packet) +{ + (void)userdata; + (void)stream; + aki_packet_free(packet); +} + +static void connection_closed_callback(void *userdata, struct aki_packet_stream *stream) +{ + (void)userdata; + (void)stream; +} + +static void packet_sent_callback(void *userdata, struct aki_packet *packet) +{ + (void)userdata; + (void)packet; +} + +static void flushed_callback(void *userdata) +{ + (void)userdata; +} + +static void connection_callback(void *userdata, struct aki_packet_stream *stream) +{ + stream->userdata = userdata; + stream->packet_callback = packet_callback; + stream->packet_sent_callback = packet_sent_callback; + stream->connection_closed_callback = connection_closed_callback; + stream->flushed_callback = flushed_callback; +} + +bool bmu_server_init(struct bmu_server *server) +{ + return aki_packet_stream_init(&server->server, AKI_SOCKET_TCP, connection_callback, + NULL, NULL, NULL, server); +} + +void bmu_server_listen(struct bmu_server *server, struct aki_event_loop *loop, str *addr, s32 port) +{ + server->loop = loop; + aki_packet_stream_listen(&server->server, server->loop, addr, port); +} diff --git a/src/bimu/server.h b/src/bimu/server.h new file mode 100644 index 0000000..ba3093e --- /dev/null +++ b/src/bimu/server.h @@ -0,0 +1,21 @@ +#pragma once + +#include <aki/packet_stream.h> + +#include "../cache/entry.h" + +struct bmu_node_connection { + struct cch_handle handle; +}; + +struct bmu_node { + struct cch_entry *entry; +}; + +struct bmu_server { + struct aki_packet_stream server; + struct aki_event_loop *loop; +}; + +bool bmu_server_init(struct bmu_server *server); +void bmu_server_listen(struct bmu_server *server, struct aki_event_loop *loop, str *addr, s32 port); diff --git a/src/buffer/audio.c b/src/buffer/audio.c new file mode 100644 index 0000000..34d3c65 --- /dev/null +++ b/src/buffer/audio.c @@ -0,0 +1,361 @@ +#include <al/log.h> + +#include "audio.h" + +#define BUFFER_USEC (10 * 1000000L) +#define BUFFER_WATERMARK_LOW (3 * 1000000L) // Must be a most half of the buffer size. +#define BUFFER_WATERMARK_HIGH (4 * 1000000L) + +#define DESYNC_PTS 0.022 + +#ifdef CAMU_AUDIO_BUFFER_FADE +#define FADE 0.9999 +#define FADE_LENGTH 6 +#define FADE_STEP(rate) ((FADE / (rate)) / (FADE_LENGTH - 1)) +#endif + +enum { + FLOWING = 0, + FLUSHED, + SIGNALED +}; + +enum { + PAUSE_PRE = 0, + PAUSE_UNPAUSED, + PAUSE_IGNORE_DESYNC, +#ifdef CAMU_AUDIO_BUFFER_FADE + PAUSE_FADING, +#endif + PAUSE_PLAYING +}; + +static bool setup_optimal_resampler(struct camu_audio_buffer *buf, struct camu_lav_resampler *resamp, + struct camu_lav_resample_fmt *fmt) +{ + /* + switch ((enum AVSampleFormat)fmt->in_format) { + case AV_SAMPLE_FMT_S16P: + fmt->req_format = AV_SAMPLE_FMT_S16; + break; + case AV_SAMPLE_FMT_S32P: + fmt->req_format = AV_SAMPLE_FMT_S32; + break; + case AV_SAMPLE_FMT_DBL: + case AV_SAMPLE_FMT_DBLP: + case AV_SAMPLE_FMT_FLTP: + fmt->req_format = AV_SAMPLE_FMT_FLT; + break; + case AV_SAMPLE_FMT_S16: + case AV_SAMPLE_FMT_S32: + case AV_SAMPLE_FMT_FLT: + fmt->req_format = fmt->in_format; + break; + default: + al_log_warn("audio_buffer", "Unhandled sample format."); + break; + } + + fmt->req_format = fmt->in_format; + fmt->req_sample_rate = fmt->in_sample_rate; + */ + + buf->fade_rate = buf->fmt.req_sample_rate / 10; + buf->bytes_per_sample = (s32)camu_lav_resample_fmt_bytes_per_sample(fmt); + + return camu_lav_resampler_init(resamp, fmt); +} + +bool camu_audio_buffer_init(struct camu_audio_buffer *buf, struct camu_clock *clock, + struct camu_mixer *mixer) +{ + buf->mixer = mixer; + buf->clock = clock; + buf->pts = camu_mixer_get_latency(buf->mixer); + buf->buffered = false; + buf->pause = PAUSE_PRE; + //buf->pause = PAUSE_IGNORE_DESYNC; +#ifdef CAMU_AUDIO_BUFFER_FADE + buf->fade_period = 0; + buf->fade_offset = 0; +#endif + al_atomic_size_t_store(&buf->continue_mark, 0, AL_ATOMIC_RELAXED); + al_atomic_u8_store(&buf->flow, FLOWING, AL_ATOMIC_RELAXED); +#ifdef CAMU_MIXER_THREADED + al_atomic_bool_store(&buf->ref, false, AL_ATOMIC_RELAXED); +#endif + return true; +} + +bool camu_audio_buffer_configure(struct camu_audio_buffer *buf, struct camu_stream *stream) +{ + buf->fmt.in_format = stream->av.stream->codecpar->format; + av_channel_layout_copy(&buf->fmt.in_channel_layout, &stream->av.stream->codecpar->ch_layout); + buf->fmt.in_channel_count = buf->fmt.in_channel_layout.nb_channels; + buf->fmt.in_sample_rate = stream->av.stream->codecpar->sample_rate; + camu_mixer_pick_format(buf->mixer, &buf->fmt); + av_channel_layout_default(&buf->fmt.req_channel_layout, buf->fmt.req_channel_count); + if (!setup_optimal_resampler(buf, &buf->resamp, &buf->fmt)) { + return false; + } + const char *in_format_name = av_get_sample_fmt_name(buf->fmt.in_format); + const char *req_format_name = av_get_sample_fmt_name(buf->fmt.req_format); + al_log_info("audio_buffer", "Stream: %s (%dch) %dHz -> %s (%dch) %dHz.", + in_format_name, buf->fmt.in_channel_layout.nb_channels, buf->fmt.in_sample_rate, + req_format_name, buf->fmt.req_channel_layout.nb_channels, buf->fmt.req_sample_rate); + buf->size = camu_lav_resample_fmt_usec_to_bytes(&buf->fmt, BUFFER_USEC); + buf->data = (u8 *)al_malloc(buf->size); + al_ring_buffer_init(&buf->rb, buf->data, buf->size); + buf->watermark.low = camu_lav_resample_fmt_usec_to_bytes(&buf->fmt, BUFFER_WATERMARK_LOW); + buf->watermark.high = camu_lav_resample_fmt_usec_to_bytes(&buf->fmt, BUFFER_WATERMARK_HIGH); + camu_peak_buffer_init(&buf->peak); + buf->stream = stream; + return true; +} + +static bool push_internal(struct camu_audio_buffer *buf, u8 **data, s32 sample_count, f64 pts) +{ + if (buf->fmt.resampler_needed) { + sample_count = camu_lav_resampler_convert(&buf->resamp, (const u8 **)data, sample_count); + data = camu_lav_resampler_get_data(&buf->resamp); + } + + f64 duration = camu_lav_resample_fmt_samples_to_sec(&buf->fmt, sample_count); + if (pts + duration < camu_clock_get_base_pts(buf->clock)) { + return false; + } + + if (sample_count <= 0) return false; + + size_t size, have = camu_lav_resample_fmt_samples_to_bytes(&buf->fmt, (size_t)sample_count); + size_t space = al_ring_buffer_space(&buf->rb); + if (!buf->buffered && buf->size - space > buf->watermark.high) { + buf->callback(buf->userdata, CAMU_BUFFER_BUFFERED); + buf->buffered = true; + } + size_t peak = buf->peak.buf.size; + if (space < (have + peak)) { + camu_peak_buffer_push(&buf->peak, data[0], have); + if (peak >= buf->watermark.low) { + // Discard the peak buffer. If this happens the user of this buffer is + // taking _way_ too long to stop. We could attempt to recover by + // re-seeking the whole stream. + camu_peak_buffer_flush(&buf->peak, &size); + } + // Continuing based on an outdated continue_mark value is safe as long as the + // peak buffer is smaller than the low watermark and the low watermark is + // less than or equal to half the buffer. + al_atomic_size_t_store(&buf->continue_mark, buf->watermark.low + peak, AL_ATOMIC_RELAXED); + buf->callback(buf->userdata, CAMU_BUFFER_STOP); + return false; + } + + if (peak > 0) { + u8 *ptr = camu_peak_buffer_flush(&buf->peak, &size); + al_ring_buffer_write(&buf->rb, ptr, size); + } + + al_ring_buffer_write(&buf->rb, data[0], have); + + return true; +} + +#ifdef HAVE_FFMPEG +static void push_av_frame_internal(struct camu_audio_buffer *buf, AVFrame *frame) +{ + if (al_atomic_u8_load(&buf->flow, AL_ATOMIC_RELAXED) == FLOWING) { + u8 **data = frame->data; + s32 sample_count = frame->nb_samples; + AVStream *stream = buf->stream->av.stream; + f64 pts = (frame->best_effort_timestamp - stream->start_time) * av_q2d(stream->time_base); + push_internal(buf, data, sample_count, pts); + } + av_frame_free(&frame); +} +#endif + +void camu_audio_buffer_push(struct camu_audio_buffer *buf, struct camu_frame *frame) +{ + switch (frame->type) { + case CAMU_NORMAL: + break; +#ifdef HAVE_FFMPEG + case CAMU_FFMPEG_COMPAT: { + push_av_frame_internal(buf, frame->av.frame); + break; + } +#endif + } + al_free(frame); +} + +void camu_audio_buffer_reset(struct camu_audio_buffer *buf) +{ + al_atomic_size_t_store(&buf->continue_mark, 0, AL_ATOMIC_RELAXED); + al_atomic_u8_store(&buf->flow, FLOWING, AL_ATOMIC_RELAXED); + buf->pts = camu_clock_get_base_pts(buf->clock); + buf->buffered = false; + al_ring_buffer_reset(&buf->rb); +} + +void camu_audio_buffer_flush(struct camu_audio_buffer *buf) +{ + al_atomic_u8_store(&buf->flow, FLUSHED, AL_ATOMIC_RELAXED); + if (!buf->buffered) { + buf->callback(buf->userdata, CAMU_BUFFER_BUFFERED); + buf->buffered = true; + } +} + +#ifdef CAMU_AUDIO_BUFFER_FADE +#define MAX_BYTES 8 +#define TYPED_CAST(f, type) (f64)(*((type *)f)) +#define TYPED_CLAMP(f, type, min, max, ret) *((type *)ret) = (type)AL_CLAMP(f, min, max) + +static void handle_fade(struct camu_audio_buffer *buf, u8 *data, size_t size, bool out) +{ + u8 bits[MAX_BYTES]; + u8 *ptr = bits + (MAX_BYTES - buf->bytes_per_sample); + f64 value; + u32 sample_count = size / buf->bytes_per_sample; + u32 channel_count = 2; + for (u32 i = 0; i < sample_count; i++) { + al_memcpy(ptr, data, buf->bytes_per_sample); + switch (buf->bytes_per_sample) { + case 2: + value = TYPED_CAST(ptr, s16); + break; + case 4: + value = TYPED_CAST(ptr, s32); + break; + } + value *= (buf->volume); + switch (buf->bytes_per_sample) { + case 2: + TYPED_CLAMP(value, s16, INT16_MIN, INT16_MAX, ptr); + break; + case 4: + TYPED_CLAMP(value, s32, INT32_MIN, INT32_MAX, ptr); + break; + } + al_memcpy(data, ptr, buf->bytes_per_sample); + if ((i + 1) % channel_count == 0) { + if (out) buf->volume = AL_CLAMP((buf->volume - FADE_STEP(buf->fade_rate)), 0.0, 1.0); + else buf->volume = AL_CLAMP((buf->volume + FADE_STEP(buf->fade_rate)), 0.0, 1.0); + } + data += buf->bytes_per_sample; + } +} +#endif + +size_t camu_audio_buffer_read(struct camu_audio_buffer *buf, u8 *data, size_t req) +{ + f64 pts = camu_clock_get_pts(buf->clock); + if (pts == -1.0) { +#ifdef CAMU_AUDIO_BUFFER_FADE + if (buf->pause == PAUSE_PLAYING) { + buf->volume = FADE; + buf->fade_period = FADE_LENGTH; + buf->fade_offset = 0; + buf->pause = PAUSE_FADING; + } else if (buf->fade_period == 0) { + al_memset(data, 0, req); + if (buf->pause != PAUSE_UNPAUSED && buf->pause != PAUSE_PRE) { + buf->pause = PAUSE_UNPAUSED; + buf->callback(buf->userdata, CAMU_BUFFER_PAUSED); + } + return req; + } + } else if (buf->pause == PAUSE_FADING || buf->pause == PAUSE_UNPAUSED) { + buf->volume = 1.0 - FADE; + buf->fade_period = FADE_LENGTH; + buf->fade_offset = 0; + buf->pause = PAUSE_PLAYING; +#else + al_memset(data, 0, req); + if (buf->pause != PAUSE_UNPAUSED && buf->pause != PAUSE_PRE) { + buf->pause = PAUSE_UNPAUSED; + buf->callback(buf->userdata, CAMU_BUFFER_PAUSED); + } + return req; +#endif + } + size_t ret, signal = req; + size_t size = al_ring_buffer_occupied(&buf->rb); + if (size < buf->fade_offset) size = 0; + else size -= buf->fade_offset; +#ifdef CAMU_AUDIO_BUFFER_FADE + if (buf->pause != PAUSE_FADING) { +#endif + pts -= buf->pts; + if (UNLIKELY(buf->pause == PAUSE_PRE || buf->pause == PAUSE_UNPAUSED)) { + if (pts > 0.0) { + ret = camu_lav_resample_fmt_sec_to_bytes(&buf->fmt, pts); + ret = AL_MIN(ret, size); + al_log_debug("audio_buffer", "Skipping %.5fs of audio.", pts); + ret = al_ring_buffer_discard(&buf->rb, ret); + size -= ret; + buf->pts += camu_lav_resample_fmt_bytes_to_sec(&buf->fmt, ret); + } else if (pts < 0.0) { + pts = -pts; + ret = camu_lav_resample_fmt_sec_to_bytes(&buf->fmt, pts); + ret = AL_MIN(ret, req); + al_log_debug("audio_buffer", "Delaying audio by %.5fs.", pts); + al_memset(data, 0, ret); + data += ret; + req -= ret; + } + } + if (buf->pause != PAUSE_PLAYING) buf->pause = PAUSE_PLAYING; +#ifdef CAMU_AUDIO_BUFFER_FADE + } +#endif + u8 flow = al_atomic_u8_load(&buf->flow, AL_ATOMIC_RELAXED); + if (size < req) { + if (flow == FLUSHED) { + signal = size; + al_atomic_u8_store(&buf->flow, SIGNALED, AL_ATOMIC_RELAXED); + al_log_debug("audio_buffer", "Flushed (signal: %li).", signal); + buf->callback(buf->userdata, CAMU_BUFFER_EOF); + } else { + al_memset(data + size, 0, req - size); + if (flow == FLOWING) { + al_log_warn("audio_buffer", "Underrun (req: %i, have: %i).", req, size); + } + } + req = size; + } + if (req > 0) { +#ifdef CAMU_AUDIO_BUFFER_FADE + if (buf->pause == PAUSE_FADING) { + ret = al_ring_buffer_peek(&buf->rb, data, buf->fade_offset, req); + buf->fade_offset += ret; + } else { +#endif + ret = al_ring_buffer_read(&buf->rb, data, req); + buf->pts += camu_lav_resample_fmt_bytes_to_sec(&buf->fmt, ret); + if (flow == FLOWING) { + ret = al_atomic_size_t_load(&buf->continue_mark, AL_ATOMIC_RELAXED); + if (ret && ((buf->size - size) - req) >= ret) { + buf->callback(buf->userdata, CAMU_BUFFER_CONTINUE); + } + } +#ifdef CAMU_AUDIO_BUFFER_FADE + } + if (buf->fade_period > 0) { + buf->fade_period--; + handle_fade(buf, data, req, buf->pause == PAUSE_FADING); + } +#endif + } + return signal; +} + +void camu_audio_buffer_free(struct camu_audio_buffer *buf) +{ + if (buf->fmt.resampler_needed) { + camu_lav_resampler_close(&buf->resamp); + } + if (buf->data) al_free(buf->data); + camu_peak_buffer_free(&buf->peak); +} diff --git a/src/buffer/audio.h b/src/buffer/audio.h new file mode 100644 index 0000000..1a51807 --- /dev/null +++ b/src/buffer/audio.h @@ -0,0 +1,59 @@ +#pragma once + +#include <al/types.h> +#include <al/ring_buffer.h> + +#include "../codec/codec.h" +#include "../codec/libav/resampler.h" +#include "../mixer/mixer.h" +#include "../mixer/audio.h" + +#include "clock.h" +#include "common.h" +#include "peak_buffer.h" + +#define CAMU_AUDIO_BUFFER_FADE + +struct camu_audio_buffer { + struct camu_stream *stream; + struct camu_mixer *mixer; + + f64 pts; + u8 pause; + struct camu_clock *clock; + + f64 volume; + s32 fade_period; + size_t fade_offset; + s32 fade_rate; + + s32 bytes_per_sample; + struct camu_lav_resampler resamp; + struct camu_lav_resample_fmt fmt; + + atomic_u8 flow; + + u8 *data; + size_t size; + struct al_ring_buffer rb; + struct { size_t low, high; } watermark; + bool buffered; + + struct camu_peak_buffer peak; + atomic_size_t continue_mark; + +#ifdef CAMU_MIXER_THREADED + atomic_bool ref; +#endif + + void (*callback)(void *, u8); + void *userdata; +}; + +bool camu_audio_buffer_init(struct camu_audio_buffer *buf, struct camu_clock *clock, struct camu_mixer *mixer); +bool camu_audio_buffer_configure(struct camu_audio_buffer *buf, struct camu_stream *stream); +void camu_audio_buffer_push(struct camu_audio_buffer *buf, struct camu_frame *frame); +void camu_audio_buffer_reset(struct camu_audio_buffer *buf); +void camu_audio_buffer_flush(struct camu_audio_buffer *buf); +size_t camu_audio_buffer_read(struct camu_audio_buffer *buf, u8 *data, size_t req); +void camu_audio_buffer_free(struct camu_audio_buffer *buf); diff --git a/src/buffer/clock.c b/src/buffer/clock.c new file mode 100644 index 0000000..5f4133e --- /dev/null +++ b/src/buffer/clock.c @@ -0,0 +1,53 @@ +#include "clock.h" + +void camu_clock_init(struct camu_clock *clock) +{ + f64 tick = aki_get_tick(); + al_atomic_f64_store(&clock->pause, tick, AL_ATOMIC_RELAXED); + al_atomic_f64_store(&clock->base, 0.0, AL_ATOMIC_RELAXED); + al_atomic_f64_store(&clock->start, 0.0, AL_ATOMIC_RELAXED); + al_atomic_f64_store(&clock->offset, 0.0, AL_ATOMIC_RELAXED); +} + +void camu_clock_pause(struct camu_clock *clock) +{ + al_atomic_f64_store(&clock->pause, aki_get_tick(), AL_ATOMIC_RELAXED); +} + +void camu_clock_resume(struct camu_clock *clock) +{ + f64 pause = al_atomic_f64_load(&clock->pause, AL_ATOMIC_RELAXED); + f64 start = al_atomic_f64_load(&clock->start, AL_ATOMIC_RELAXED); + f64 base = al_atomic_f64_load(&clock->base, AL_ATOMIC_RELAXED); + if (start == 0.0) start = (pause - base); + al_atomic_f64_store(&clock->start, aki_get_tick() - (pause - start), AL_ATOMIC_RELAXED); + al_atomic_f64_store(&clock->pause, -1.0, AL_ATOMIC_RELAXED); +} + +void camu_clock_seek(struct camu_clock *clock, f64 pos) +{ + al_atomic_f64_store(&clock->start, 0.0, AL_ATOMIC_RELAXED); + al_atomic_f64_store(&clock->base, pos, AL_ATOMIC_RELAXED); + f64 tick = aki_get_tick(); + al_atomic_f64_store(&clock->pause, tick, AL_ATOMIC_RELAXED); +} + +bool camu_clock_is_paused(struct camu_clock *clock) +{ + f64 pause = al_atomic_f64_load(&clock->pause, AL_ATOMIC_RELAXED); + return pause != -1.0; +} + +f64 camu_clock_get_base_pts(struct camu_clock *clock) +{ + f64 base = al_atomic_f64_load(&clock->base, AL_ATOMIC_RELAXED); + return base; +} + +f64 camu_clock_get_pts(struct camu_clock *clock) +{ + f64 pause = al_atomic_f64_load(&clock->pause, AL_ATOMIC_RELAXED); + if (pause != -1.0) return -1.0; + f64 start = al_atomic_f64_load(&clock->start, AL_ATOMIC_RELAXED); + return aki_get_tick() - start; +} diff --git a/src/buffer/clock.h b/src/buffer/clock.h new file mode 100644 index 0000000..072f7d3 --- /dev/null +++ b/src/buffer/clock.h @@ -0,0 +1,16 @@ +#pragma once + +#include <aki/thread.h> +#include <al/atomic.h> + +struct camu_clock { + atomic_f64 base, start, pause, offset; +}; + +void camu_clock_init(struct camu_clock *clock); +void camu_clock_pause(struct camu_clock *clock); +void camu_clock_resume(struct camu_clock *clock); +void camu_clock_seek(struct camu_clock *clock, f64 pos); +bool camu_clock_is_paused(struct camu_clock *clock); +f64 camu_clock_get_base_pts(struct camu_clock *clock); +f64 camu_clock_get_pts(struct camu_clock *clock); diff --git a/src/buffer/common.h b/src/buffer/common.h new file mode 100644 index 0000000..8a99c96 --- /dev/null +++ b/src/buffer/common.h @@ -0,0 +1,9 @@ +#pragma once + +enum { + CAMU_BUFFER_BUFFERED = 0, + CAMU_BUFFER_STOP, + CAMU_BUFFER_CONTINUE, + CAMU_BUFFER_PAUSED, + CAMU_BUFFER_EOF +}; diff --git a/src/buffer/frame_queue.h b/src/buffer/frame_queue.h new file mode 100644 index 0000000..35a84e4 --- /dev/null +++ b/src/buffer/frame_queue.h @@ -0,0 +1,28 @@ +#pragma once + +#ifdef HAVE_FFMPEG +#include <libavutil/frame.h> +#endif + +#include "../codec/codec.h" + +#include "common.h" + +enum { + CAMU_QUEUE_OK = 0, + CAMU_QUEUE_MORE, + CAMU_QUEUE_EOF, + CAMU_QUEUE_ERR +}; + +struct camu_frame_queue { + void (*push)(struct camu_frame_queue *, struct camu_frame *, f64); +#ifdef HAVE_FFMPEG + void (*push_av_frame)(struct camu_frame_queue *, AVFrame *, f64); +#endif + void (*flush)(struct camu_frame_queue *); + s32 (*count)(struct camu_frame_queue *); + u8 (*read)(struct camu_frame_queue *, f64, void *); + void (*reset)(struct camu_frame_queue *); + void (*free)(struct camu_frame_queue **); +}; diff --git a/src/buffer/meson.build b/src/buffer/meson.build new file mode 100644 index 0000000..1fb3cfa --- /dev/null +++ b/src/buffer/meson.build @@ -0,0 +1,8 @@ +if meson.is_subproject() # TMP + buffer_src = ['audio.c', 'clock.c', 'peak_buffer.c'] +else + buffer_src = ['video.c', 'audio.c', 'clock.c', 'peak_buffer.c'] +endif +buffer_deps = [common_deps] +buffer = declare_dependency(sources: buffer_src, + dependencies: buffer_deps) diff --git a/src/buffer/peak_buffer.c b/src/buffer/peak_buffer.c new file mode 100644 index 0000000..22a007d --- /dev/null +++ b/src/buffer/peak_buffer.c @@ -0,0 +1,24 @@ +#include "peak_buffer.h" + +void camu_peak_buffer_init(struct camu_peak_buffer *buf) +{ + aki_buffer_init(&buf->buf); + aki_buffer_ensure_space(&buf->buf, 128 * 1024); +} + +void camu_peak_buffer_push(struct camu_peak_buffer *buf, u8 *data, size_t size) +{ + aki_buffer_append(&buf->buf, data, size); +} + +u8 *camu_peak_buffer_flush(struct camu_peak_buffer *buf, size_t *size) +{ + *size = aki_buffer_get_size(&buf->buf); + aki_buffer_set_size(&buf->buf, 0); + return aki_buffer_get_ptr(&buf->buf, 0); +} + +void camu_peak_buffer_free(struct camu_peak_buffer *buf) +{ + aki_buffer_free(&buf->buf); +} diff --git a/src/buffer/peak_buffer.h b/src/buffer/peak_buffer.h new file mode 100644 index 0000000..b8d9934 --- /dev/null +++ b/src/buffer/peak_buffer.h @@ -0,0 +1,13 @@ +#pragma once + +#include <al/types.h> +#include <aki/common.h> + +struct camu_peak_buffer { + struct aki_buffer buf; +}; + +void camu_peak_buffer_init(struct camu_peak_buffer *buf); +void camu_peak_buffer_push(struct camu_peak_buffer *buf, u8 *data, size_t size); +u8 *camu_peak_buffer_flush(struct camu_peak_buffer *buf, size_t *size); +void camu_peak_buffer_free(struct camu_peak_buffer *buf); diff --git a/src/buffer/video.c b/src/buffer/video.c new file mode 100644 index 0000000..d5d2c12 --- /dev/null +++ b/src/buffer/video.c @@ -0,0 +1,148 @@ +#include <al/log.h> + +#include "video.h" + +//#define CAMU_VIDEO_BUFFER_FORCE_SCALER + +bool camu_video_buffer_init(struct camu_video_buffer *buf, struct camu_clock *clock, + struct camu_renderer *renderer) +{ + buf->queue = renderer->create_queue(renderer); + buf->buffered = false; + buf->clock = clock; + buf->last_pts = 0.0; +#ifdef CAMU_SCREEN_THREADED + al_atomic_bool_store(&buf->ref, false, AL_ATOMIC_RELAXED); +#endif + return true; +} + +bool camu_video_buffer_configure(struct camu_video_buffer *buf, struct camu_stream *stream) +{ + buf->stream = stream; + switch (stream->type) { + case CAMU_NORMAL: { + buf->single_frame = true; + buf->avg_frame_duration = 0.0; + break; + } +#ifdef HAVE_FFMPEG + case CAMU_FFMPEG_COMPAT: { + s32 width = stream->av.stream->codecpar->width; + s32 height = stream->av.stream->codecpar->height; + buf->fmt.in_width = buf->stream->video.width = width; + buf->fmt.in_height = buf->stream->video.height = height; + buf->single_frame = stream->av.stream->duration == 0 || + stream->av.stream->avg_frame_rate.den == 0; + buf->avg_frame_duration = buf->single_frame ? 0.0 : + av_q2d(av_inv_q(stream->av.stream->avg_frame_rate)); + buf->fmt.in_format = stream->av.stream->codecpar->format; + const char *format_name = av_get_pix_fmt_name(buf->fmt.in_format); + al_log_info("video_buffer", "Stream: %s (%dx%d) %s %.2ffps.", + format_name, width, height, buf->single_frame ? "IMAGE" : "VIDEO", + buf->single_frame ? 0.0 : 1.0 / buf->avg_frame_duration); +#ifdef CAMU_VIDEO_BUFFER_FORCE_SCALER + buf->fmt.req_width = width; + buf->fmt.req_height = height; + buf->fmt.req_format = AV_PIX_FMT_RGBA; + if (!camu_lav_scaler_init(&buf->scale, &buf->fmt)) { + return false; + } +#endif + break; + } +#endif + } + return true; +} + +static void after_push_internal(struct camu_video_buffer *buf) +{ + s32 count = buf->queue->count(buf->queue); + if (!buf->buffered && (buf->single_frame || count >= 15)) { + buf->callback(buf->userdata, CAMU_BUFFER_BUFFERED); + buf->buffered = true; + } + if (count >= 40) buf->queue->reset(buf->queue); + else if (count >= 30) buf->callback(buf->userdata, CAMU_BUFFER_STOP); + if (buf->single_frame) camu_video_buffer_flush(buf); +} + +#ifdef HAVE_FFMPEG +static void push_av_frame_internal(struct camu_video_buffer *buf, AVFrame *frame) +{ + AVStream *stream = buf->stream->av.stream; + f64 pts = buf->single_frame ? 0.0 : + (frame->best_effort_timestamp - stream->start_time) * av_q2d(stream->time_base); + if (pts + buf->avg_frame_duration < camu_clock_get_base_pts(buf->clock)) { + av_frame_free(&frame); + return; + } + AVFrame *scaled_frame; +#ifdef CAMU_VIDEO_BUFFER_FORCE_SCALER + if (buf->fmt.scaler_needed) { + if (!camu_lav_scaler_scale(&buf->scale, (const u8 **)frame->data, frame->linesize)) { + return; + } + scaled_frame = buf->scale.frame; + } else { + scaled_frame = frame; + } +#else + scaled_frame = frame; +#endif + if (scaled_frame) scaled_frame->opaque = buf; + buf->queue->push_av_frame(buf->queue, scaled_frame, pts); +} +#endif + +void camu_video_buffer_push(struct camu_video_buffer *buf, struct camu_frame *frame) +{ + switch (frame->type) { + case CAMU_NORMAL: + buf->queue->push(buf->queue, frame, 0.0); + break; +#ifdef HAVE_FFMPEG + case CAMU_FFMPEG_COMPAT: + push_av_frame_internal(buf, frame->av.frame); + al_free(frame); + break; +#endif + } + after_push_internal(buf); +} + +void camu_video_buffer_reset(struct camu_video_buffer *buf) +{ + buf->queue->reset(buf->queue); + buf->buffered = false; +} + +bool camu_video_buffer_is_single_frame(struct camu_video_buffer *buf) +{ + return buf->single_frame; +} + +void camu_video_buffer_flush(struct camu_video_buffer *buf) +{ + buf->queue->flush(buf->queue); +} + +bool camu_video_buffer_read(struct camu_video_buffer *buf, void *out) +{ + f64 pts = (buf->single_frame) ? 0.0 : camu_clock_get_pts(buf->clock); + if (pts == -1.0) pts = buf->last_pts; + buf->last_pts = pts; + u8 ret = buf->queue->read(buf->queue, pts, out); + if (ret == CAMU_QUEUE_EOF || (ret == CAMU_QUEUE_OK && buf->single_frame)) { + buf->callback(buf->userdata, CAMU_BUFFER_EOF); + } else if (buf->queue->count(buf->queue) <= 20) { + buf->callback(buf->userdata, CAMU_BUFFER_CONTINUE); + } + return ret == CAMU_QUEUE_OK || ret == CAMU_QUEUE_MORE; +} + +void camu_video_buffer_free(struct camu_video_buffer *buf) +{ + buf->queue->free(&buf->queue); +} diff --git a/src/buffer/video.h b/src/buffer/video.h new file mode 100644 index 0000000..534f797 --- /dev/null +++ b/src/buffer/video.h @@ -0,0 +1,42 @@ +#pragma once + +#include <al/types.h> + +#include "../codec/codec.h" +#include "../codec/libav/scaler.h" +#include "../render/renderer.h" +#include "../screen/screen.h" + +#include "clock.h" +#include "frame_queue.h" + +struct camu_video_buffer { + struct camu_stream *stream; + + struct camu_clock *clock; + bool single_frame; + f64 last_pts; + f64 avg_frame_duration; + + struct camu_lav_scaler scale; + struct camu_lav_scale_fmt fmt; + + struct camu_frame_queue *queue; + bool buffered; + +#ifdef CAMU_SCREEN_THREADED + atomic_bool ref; +#endif + + void (*callback)(void *, u8); + void *userdata; +}; + +bool camu_video_buffer_init(struct camu_video_buffer *buf, struct camu_clock *clock, struct camu_renderer *renderer); +bool camu_video_buffer_configure(struct camu_video_buffer *buf, struct camu_stream *stream); +void camu_video_buffer_push(struct camu_video_buffer *buf, struct camu_frame *frame); +void camu_video_buffer_reset(struct camu_video_buffer *buf); +bool camu_video_buffer_is_single_frame(struct camu_video_buffer *buf); +void camu_video_buffer_flush(struct camu_video_buffer *buf); +bool camu_video_buffer_read(struct camu_video_buffer *buf, void *out); +void camu_video_buffer_free(struct camu_video_buffer *buf); diff --git a/src/cache/backing.h b/src/cache/backing.h new file mode 100644 index 0000000..24aff44 --- /dev/null +++ b/src/cache/backing.h @@ -0,0 +1,20 @@ +#pragma once + +#include <al/types.h> + +enum { + CCH_BACKING_READ = 0, + CCH_BACKING_MAPPED +}; + +struct cch_backing { + u8 mode; + void (*write)(struct cch_backing *, u8 *, off_t, size_t *); + void (*read)(struct cch_backing *, u8 *, off_t, size_t *); + u8 *(*get_ptr)(struct cch_backing *, off_t, size_t *); + void (*unlock)(struct cch_backing *); + void *(*get_mmap)(struct cch_backing *); + off_t (*get_size_estimate)(struct cch_backing *); + void (*resize)(struct cch_backing *, size_t); + void (*free)(struct cch_backing **); +}; diff --git a/src/cache/backings/file.c b/src/cache/backings/file.c new file mode 100644 index 0000000..4c5f344 --- /dev/null +++ b/src/cache/backings/file.c @@ -0,0 +1,75 @@ +#include "file.h" +#include "file_common.c" + +static void file_backing_write(struct cch_backing *backing, u8 *buf, off_t index, size_t *size) +{ + struct cch_backing_file *file = (struct cch_backing_file *)backing; + aki_mutex_lock(&file->mutex); + if (file->u.pointer != index) { + file->u.pointer = aki_file_seek(&file->file, index, SEEK_SET); + al_assert(file->u.pointer == index); + } + if (file->size >= 0 && index + ((off_t)*size) >= file->size) { + *size = file->size - index; + } + aki_file_write(&file->file, buf, *size); + file->u.pointer += *size; + aki_mutex_unlock(&file->mutex); +} + +static void file_backing_read(struct cch_backing *backing, u8 *buf, off_t index, size_t *size) +{ + struct cch_backing_file *file = (struct cch_backing_file *)backing; + aki_mutex_lock(&file->mutex); + if (file->u.pointer != index) { + file->u.pointer = aki_file_seek(&file->file, index, SEEK_SET); + al_assert(file->u.pointer == index); + } + if (file->size >= 0 && index + ((off_t)*size) >= file->size) { + *size = file->size - index; + } + aki_file_read(&file->file, buf, *size); + file->u.pointer += *size; + aki_mutex_unlock(&file->mutex); +} + +static void file_backing_resize(struct cch_backing *backing, size_t size) +{ + struct cch_backing_file *file = (struct cch_backing_file *)backing; + aki_mutex_lock(&file->mutex); + file->size = size; + if ((off_t)size > file->filesize) { + aki_file_truncate(&file->file, size); + file->filesize = size; + } + aki_mutex_unlock(&file->mutex); +} + +static void file_backing_free(struct cch_backing **backing) +{ + struct cch_backing_file *file = (struct cch_backing_file *)*backing; + aki_file_close(&file->file); + al_free(file); + *backing = NULL; +} + +struct cch_backing *cch_backing_file_create(str *path, size_t size) +{ + struct cch_backing_file *file = al_alloc_object(struct cch_backing_file); + file->backing.mode = CCH_BACKING_READ; + file->backing.write = file_backing_write; + file->backing.read = file_backing_read; + file->backing.get_ptr = NULL; + file->backing.unlock = NULL; + file->backing.get_mmap = NULL; + file->backing.get_size_estimate = file_backing_get_size_estimate; + file->backing.resize = file_backing_resize; + file->backing.free = file_backing_free; + if (!file_open_internal(file, path, size)) { + al_free(file); + return NULL; + } + file->u.pointer = 0; + aki_mutex_init(&file->mutex); + return (struct cch_backing *)file; +} diff --git a/src/cache/backings/file.h b/src/cache/backings/file.h new file mode 100644 index 0000000..9794bbb --- /dev/null +++ b/src/cache/backings/file.h @@ -0,0 +1,22 @@ +#pragma once + +#include <al/str.h> +#include <aki/thread.h> +#include <aki/common.h> +#include <aki/file.h> + +#include "../backing.h" + +struct cch_backing_file { + struct cch_backing backing; + struct aki_file file; + off_t size; + off_t filesize; + union { + void *map; // CCH_BACKING_MAPPED + off_t pointer; // CCH_BACKING_READ + } u; + struct aki_mutex mutex; +}; + +struct cch_backing *cch_backing_file_create(str *path, size_t filesize); diff --git a/src/cache/backings/file_common.c b/src/cache/backings/file_common.c new file mode 100644 index 0000000..c0b54c0 --- /dev/null +++ b/src/cache/backings/file_common.c @@ -0,0 +1,23 @@ +static off_t file_backing_get_size_estimate(struct cch_backing *backing) +{ + struct cch_backing_file *file = (struct cch_backing_file *)backing; + aki_mutex_lock(&file->mutex); + off_t size = file->size; + aki_mutex_unlock(&file->mutex); + return size; +} + +static bool file_open_internal(struct cch_backing_file *file, str *path, size_t size) +{ + if (!aki_file_open(&file->file, path, size != 0)) return false; + file->filesize = aki_file_get_filesize(&file->file); + if (!size) { + file->size = file->filesize; + } else { + file->size = -1; + if (file->filesize < (off_t)size) { + aki_file_truncate(&file->file, size); + } + } + return true; +} diff --git a/src/cache/backings/file_mapped.c b/src/cache/backings/file_mapped.c new file mode 100644 index 0000000..1b06ad2 --- /dev/null +++ b/src/cache/backings/file_mapped.c @@ -0,0 +1,81 @@ +#include "file.h" +#include "file_common.c" + +static void file_backing_write(struct cch_backing *backing, u8 *buf, off_t index, size_t *size) +{ + struct cch_backing_file *file = (struct cch_backing_file *)backing; + aki_mutex_lock(&file->mutex); + if (file->size >= 0 && index + ((off_t)*size) >= file->size) { + *size = file->size - index; + } + al_memcpy(file->u.map + index, buf, *size); + aki_mutex_unlock(&file->mutex); +} + +static u8 *file_backing_get_ptr(struct cch_backing *backing, off_t index, size_t *size) +{ + struct cch_backing_file *file = (struct cch_backing_file *)backing; + aki_mutex_lock(&file->mutex); + if (file->size >= 0 && index + ((off_t)*size) >= file->size) { + *size = file->size - index; + } + return (u8 *)(file->u.map + index); +} + +static void file_backing_unlock(struct cch_backing *backing) +{ + struct cch_backing_file *file = (struct cch_backing_file *)backing; + aki_mutex_unlock(&file->mutex); +} + +static void *file_backing_get_mmap(struct cch_backing *backing) +{ + struct cch_backing_file *file = (struct cch_backing_file *)backing; + aki_mutex_lock(&file->mutex); + void *map = file->u.map; + aki_mutex_unlock(&file->mutex); + return map; +} + +static void file_backing_resize(struct cch_backing *backing, size_t size) +{ + struct cch_backing_file *file = (struct cch_backing_file *)backing; + aki_mutex_lock(&file->mutex); + file->size = size; + if ((off_t)size > file->filesize) { + file->u.map = aki_file_mremap(&file->file, size, file->u.map); + aki_file_truncate(&file->file, size); + file->filesize = size; + } + aki_mutex_unlock(&file->mutex); +} + +static void file_backing_free(struct cch_backing **backing) +{ + struct cch_backing_file *file = (struct cch_backing_file *)*backing; + aki_file_munmap(&file->file, file->u.map); + aki_file_close(&file->file); + al_free(file); + *backing = NULL; +} + +struct cch_backing *cch_backing_file_create(str *path, size_t size) +{ + struct cch_backing_file *file = al_alloc_object(struct cch_backing_file); + file->backing.mode = CCH_BACKING_MAPPED; + file->backing.write = file_backing_write; + file->backing.read = NULL; + file->backing.get_mmap = file_backing_get_mmap; + file->backing.get_ptr = file_backing_get_ptr; + file->backing.unlock = file_backing_unlock; + file->backing.get_size_estimate = file_backing_get_size_estimate; + file->backing.resize = file_backing_resize; + file->backing.free = file_backing_free; + if (!file_open_internal(file, path, size)) { + al_free(file); + return NULL; + } + file->u.map = aki_file_mmap(&file->file); + aki_mutex_init(&file->mutex); + return (struct cch_backing *)file; +} diff --git a/src/cache/backings/memory.c b/src/cache/backings/memory.c new file mode 100644 index 0000000..194d66c --- /dev/null +++ b/src/cache/backings/memory.c @@ -0,0 +1,82 @@ +#include "memory.h" + +static void ensure_alloced(struct cch_backing_memory *mem, size_t size) +{ + if (size > mem->alloc) { + mem->data = al_realloc(mem->data, size); + mem->alloc = size; + } +} + +static void memory_backing_write(struct cch_backing *backing, u8 *buf, off_t index, size_t *size) +{ + struct cch_backing_memory *mem = (struct cch_backing_memory *)backing; + aki_mutex_lock(&mem->mutex); + if (mem->size >= 0 && index + ((off_t)*size) >= mem->size) { + *size = mem->size - index; + } + ensure_alloced(mem, index + *size); + al_memcpy(mem->data + index, buf, *size); + aki_mutex_unlock(&mem->mutex); +} + +static u8 *memory_backing_get_ptr(struct cch_backing *backing, off_t index, size_t *size) +{ + struct cch_backing_memory *mem = (struct cch_backing_memory *)backing; + aki_mutex_lock(&mem->mutex); + if (mem->size >= 0 && index + ((off_t)*size) >= mem->size) { + *size = mem->size - index; + } + return (u8 *)(mem->data + index); +} + +static void memory_backing_unlock(struct cch_backing *backing) +{ + struct cch_backing_memory *mem = (struct cch_backing_memory *)backing; + aki_mutex_unlock(&mem->mutex); +} + +static off_t memory_backing_get_size_estimate(struct cch_backing *backing) +{ + struct cch_backing_memory *mem = (struct cch_backing_memory *)backing; + aki_mutex_lock(&mem->mutex); + off_t size = mem->size; + aki_mutex_unlock(&mem->mutex); + return size; +} + +static void memory_backing_resize(struct cch_backing *backing, size_t size) +{ + struct cch_backing_memory *mem = (struct cch_backing_memory *)backing; + aki_mutex_lock(&mem->mutex); + ensure_alloced(mem, size); + mem->size = size; + aki_mutex_unlock(&mem->mutex); +} + +static void memory_backing_free(struct cch_backing **backing) +{ + struct cch_backing_memory *mem = (struct cch_backing_memory *)*backing; + al_free(mem->data); + aki_mutex_destroy(&mem->mutex); + al_free(mem); + *backing = NULL; +} + +struct cch_backing *cch_backing_memory_create(size_t size) +{ + struct cch_backing_memory *mem = al_alloc_object(struct cch_backing_memory); + mem->backing.mode = CCH_BACKING_MAPPED; + mem->backing.write = memory_backing_write; + mem->backing.get_mmap = NULL; + mem->backing.get_ptr = memory_backing_get_ptr; + mem->backing.unlock = memory_backing_unlock; + mem->backing.get_size_estimate = memory_backing_get_size_estimate; + mem->backing.resize = memory_backing_resize; + mem->backing.free = memory_backing_free; + mem->data = al_malloc(size); + mem->alloc = size; + mem->size = -1; + aki_mutex_init(&mem->mutex); + return (struct cch_backing *)mem; +} diff --git a/src/cache/backings/memory.h b/src/cache/backings/memory.h new file mode 100644 index 0000000..107ae4b --- /dev/null +++ b/src/cache/backings/memory.h @@ -0,0 +1,16 @@ +#pragma once + +#include <al/lib.h> +#include <aki/thread.h> + +#include "../backing.h" + +struct cch_backing_memory { + struct cch_backing backing; + u8 *data; + size_t alloc; + off_t size; + struct aki_mutex mutex; +}; + +struct cch_backing *cch_backing_memory_create(size_t size); diff --git a/src/cache/entry.c b/src/cache/entry.c new file mode 100644 index 0000000..04e8f92 --- /dev/null +++ b/src/cache/entry.c @@ -0,0 +1,44 @@ +#include <al/lib.h> + +#include "entry.h" + +bool cch_entry_get_handle(struct cch_entry *entry, struct cch_handle *handle) +{ + handle->entry = entry; + handle->pointer = 0; + handle->prev_pointer = 0; + aki_cond_init(&handle->wait.cond); + aki_mutex_init(&handle->wait.mutex); + handle->wait.disabled = false; + aki_mutex_lock(&entry->mutex); + entry->ref_count++; + aki_mutex_unlock(&entry->mutex); + return true; +} + +void cch_entry_set_size(struct cch_entry *entry, off_t filesize) +{ + return entry->backing->resize(entry->backing, filesize); +} + +off_t cch_entry_get_size(struct cch_entry *entry) +{ + return entry->backing->get_size_estimate(entry->backing); +} + +void cch_entry_return_handle(struct cch_entry *entry, struct cch_handle *handle) +{ + handle->entry = NULL; + entry->ref_count--; + aki_mutex_destroy(&handle->wait.mutex); + aki_cond_destroy(&handle->wait.cond); +} + +void cch_entry_free(struct cch_entry **entry) +{ + (*entry)->handler->free(&(*entry)->handler); + (*entry)->backing->free(&(*entry)->backing); + aki_mutex_destroy(&(*entry)->mutex); + al_free(*entry); + *entry = NULL; +} diff --git a/src/cache/entry.h b/src/cache/entry.h new file mode 100644 index 0000000..305fb1f --- /dev/null +++ b/src/cache/entry.h @@ -0,0 +1,21 @@ +#pragma once + +#include <al/types.h> + +#include "handle.h" +#include "backing.h" +#include "handler.h" + +struct cch_entry { + u32 hash; + s32 ref_count; + struct cch_backing *backing; + struct cch_handler *handler; + struct aki_mutex mutex; +}; + +bool cch_entry_get_handle(struct cch_entry *entry, struct cch_handle *handle); +void cch_entry_set_size(struct cch_entry *entry, off_t filesize); +off_t cch_entry_get_size(struct cch_entry *entry); +void cch_entry_return_handle(struct cch_entry *entry, struct cch_handle *handle); +void cch_entry_free(struct cch_entry **entry); diff --git a/src/cache/handle.c b/src/cache/handle.c new file mode 100644 index 0000000..9dfaf70 --- /dev/null +++ b/src/cache/handle.c @@ -0,0 +1,100 @@ +#include <al/lib.h> +#include <al/log.h> +#include <al/random.h> + +#include "../codec/codec.h" + +#include "handle.h" +#include "entry.h" + +static bool wait_for_size(struct cch_handle *handle, struct cch_handler *handler) +{ + handle->wait.id = al_rand_u16(); + handle->wait.end = -1; + return handler->wait_for_range(handler, &handle->wait); +} + +s32 cch_handle_read(struct cch_handle *handle, u8 *buf, s32 size) +{ + struct cch_handler *handler = handle->entry->handler; + wait_for_size(handle, handler); + off_t filesize = cch_entry_get_size(handle->entry); + if (filesize >= 0) { + if (handle->pointer >= filesize) return CAMU_ERR_EOF; + if (handle->pointer + size >= filesize) { + size = filesize - handle->pointer; + } + } + handle->wait.id = al_rand_u16(); + handle->wait.start = handle->pointer; + handle->wait.end = handle->pointer + size; + if (!handler->wait_for_range(handler, &handle->wait)) { + return CAMU_ERR_EOF; + } + struct cch_backing *backing = handle->entry->backing; + size_t available = (size_t)size; + if (backing->mode == CCH_BACKING_MAPPED) { + u8 *ptr = backing->get_ptr(backing, handle->pointer, &available); + if (ptr && available > 0) { + al_memcpy(buf, ptr, available); + } + backing->unlock(backing); + } else if (backing->mode == CCH_BACKING_READ) { + backing->read(backing, buf, handle->pointer, &available); + } + handle->pointer += available; + return (available > 0) ? (s32)available : CAMU_ERR_EOF; +} + +off_t cch_handle_seek(struct cch_handle *handle, off_t offset, s32 whence) +{ + struct cch_handler *handler = handle->entry->handler; + wait_for_size(handle, handler); + off_t filesize = cch_entry_get_size(handle->entry); + if (filesize < 0) return -1; + if (whence == CAMU_SEEK_SIZE) { + al_log_debug("cache_handle", "seek_size"); + return filesize; + } + switch (whence) { + case SEEK_SET: + if (offset >= filesize || offset < 0) { + return -1; + } + handle->pointer = offset; + al_log_debug("cache_handle", "seek_set %li", offset); + break; + case SEEK_CUR: + if (handle->pointer + offset >= filesize || handle->pointer + offset < 0) { + return -1; + } + handle->pointer += offset; + al_log_debug("cache_handle", "seek_cur %li", offset); + break; + case SEEK_END: + handle->prev_pointer = handle->pointer; + handle->pointer = filesize + offset; + al_log_debug("cache_handle", "seek_end %li", offset); + break; + default: + return -1; + } + handle->prev_pointer = -1; + return handle->pointer; +} + +bool cch_handle_can_seek(struct cch_handle *handle) +{ + struct cch_handler *handler = handle->entry->handler; + return handler->can_seek(handler); +} + +void cch_handle_disable(struct cch_handle *handle) +{ + aki_mutex_lock(&handle->wait.mutex); + handle->wait.disabled = true; + if (aki_cond_is_waiting(&handle->wait.cond)) { + aki_cond_signal(&handle->wait.cond); + } + aki_mutex_unlock(&handle->wait.mutex); +} diff --git a/src/cache/handle.h b/src/cache/handle.h new file mode 100644 index 0000000..55223aa --- /dev/null +++ b/src/cache/handle.h @@ -0,0 +1,20 @@ +#pragma once + +#include <al/types.h> +#include <aki/thread.h> + +#include "handler.h" + +struct cch_entry; +struct cch_handle { + struct cch_entry *entry; + off_t pointer; + // Set after a seek_end and reset after any seek_cur or seek_set. + off_t prev_pointer; + struct cch_handler_wait wait; +}; + +s32 cch_handle_read(struct cch_handle *handle, u8 *buf, s32 size); +off_t cch_handle_seek(struct cch_handle *handle, off_t offset, s32 whence); +bool cch_handle_can_seek(struct cch_handle *handle); +void cch_handle_disable(struct cch_handle *handle); diff --git a/src/cache/handler.h b/src/cache/handler.h new file mode 100644 index 0000000..5a2861e --- /dev/null +++ b/src/cache/handler.h @@ -0,0 +1,23 @@ +#pragma once + +#include <al/types.h> +#include <al/array.h> +#include <aki/thread.h> + +struct cch_handler_wait { + u16 id; + off_t start, end; + struct aki_cond cond; + struct aki_mutex mutex; + bool disabled; +}; + +struct cch_handler { + bool (*can_seek)(struct cch_handler *); + void (*maybe_spawn_worker)(struct cch_handler *, size_t); + bool (*wait_for_range)(struct cch_handler *, struct cch_handler_wait *); + void (*free)(struct cch_handler **); + struct cch_entry *entry; + struct cch_backing *backing; + array(struct cch_handler_wait *) waits; +}; diff --git a/src/cache/handlers/file.c b/src/cache/handlers/file.c new file mode 100644 index 0000000..ec6a146 --- /dev/null +++ b/src/cache/handlers/file.c @@ -0,0 +1,49 @@ +#include "../backings/file.h" + +#include "file.h" + +static bool handler_file_can_seek(struct cch_handler *handler) +{ + (void)handler; + return true; +} + +static void handler_file_maybe_spawn_worker(struct cch_handler *handler, size_t index) +{ + struct cch_handler_file *file = (struct cch_handler_file *)handler; + (void)file; + (void)index; +} + +static bool handler_file_wait_for_range(struct cch_handler *handler, struct cch_handler_wait *wait) +{ + (void)handler; + (void)wait; + return true; +} + +static void handler_file_free(struct cch_handler **handler) +{ + struct cch_handler_file *file = (struct cch_handler_file *)*handler; + al_free(file); + *handler = NULL; +} + +struct cch_entry *cch_handler_file_create(str *path) +{ + struct cch_backing *backing = cch_backing_file_create(path, 0); + if (!backing) return NULL; + struct cch_entry *entry = al_alloc_object(struct cch_entry); + entry->backing = backing; + entry->ref_count = 0; + entry->hash = 0; + entry->handler = (struct cch_handler *)al_alloc_object(struct cch_handler_file); + aki_mutex_init(&entry->mutex); + entry->handler->can_seek = handler_file_can_seek; + entry->handler->maybe_spawn_worker = handler_file_maybe_spawn_worker; + entry->handler->wait_for_range = handler_file_wait_for_range; + entry->handler->free = handler_file_free; + entry->handler->entry = entry; + entry->handler->backing = entry->backing; + return entry; +} diff --git a/src/cache/handlers/file.h b/src/cache/handlers/file.h new file mode 100644 index 0000000..52e5c94 --- /dev/null +++ b/src/cache/handlers/file.h @@ -0,0 +1,12 @@ +#pragma once + +#include <al/str.h> + +#include "../handler.h" +#include "../entry.h" + +struct cch_handler_file { + struct cch_handler handler; +}; + +struct cch_entry *cch_handler_file_create(str *path); diff --git a/src/cache/handlers/http.c b/src/cache/handlers/http.c new file mode 100644 index 0000000..1daf231 --- /dev/null +++ b/src/cache/handlers/http.c @@ -0,0 +1,207 @@ +#include <al/random.h> +#include <al/log.h> + +#include "../backings/file.h" +#include "../backings/memory.h" + +#include "http.h" + +#define USER_AGENT al_str_c("Mozilla/5.0 (X11; Linux x86_64; rv:96.0) Gecko/20100101 Firefox/96.0") + +static bool handler_http_can_seek(struct cch_handler *handler) +{ + (void)handler; + return false; +} + +static size_t http_callback(void *userdata, u8 op, u8 *buf, s64 int0) +{ + struct cch_handler_http *http = (struct cch_handler_http *)userdata; + size_t ret = (size_t)int0; + switch (op) { + case AKI_HTTP_FINISHED: { + al_log_debug("cache_handler_http", "Transfer finished."); + break; + } + case AKI_HTTP_ERROR: { + al_log_debug("cache_handler_http", "Error."); + break; // Unhandled. + } + case AKI_HTTP_RESPONSE_CODE: { + al_log_debug("cache_handler_http", "HTTP %ld.", int0); + break; + } + case AKI_HTTP_REDIRECT: { + al_log_debug("cache_handler_http", "Redirect."); + break; + } + case AKI_HTTP_CONTENT_LENGTH: { + off_t content_length = (off_t)int0; + al_log_debug("cache_handler_http", "Content-Length: %lld.", content_length); + aki_mutex_lock(&http->mutex); + cch_entry_set_size(http->handler.entry, content_length); + struct cch_handler_wait *wait; + al_array_foreach(http->handler.waits, i, wait) { + if (wait->end < 0) { + aki_mutex_lock(&wait->mutex); + aki_cond_signal(&wait->cond); + al_array_remove_at_iter(http->handler.waits, i); + aki_mutex_unlock(&wait->mutex); + } + } + aki_mutex_unlock(&http->mutex); + break; + } + case AKI_HTTP_WRITE: { + http->handler.backing->write(http->handler.backing, buf, http->pointer, &ret); + aki_mutex_lock(&http->mutex); + http->pointer += ret; + struct cch_handler_wait *wait; + al_array_foreach(http->handler.waits, i, wait) { + aki_mutex_lock(&wait->mutex); + // Assume that if we get data before a length, we won't get a length. + // So, signal the waiter (wait_for_range returns false). + if (wait->end < 0) wait->disabled = true; + if (wait->disabled || http->pointer >= wait->end) { + aki_cond_signal(&wait->cond); + al_array_remove_at_iter(http->handler.waits, i); + } + aki_mutex_unlock(&wait->mutex); + } + aki_mutex_unlock(&http->mutex); + break; + } + default: + break; + } + return ret; +} + +static void signal_callback(void *userdata) +{ + struct cch_handler_http *http = (struct cch_handler_http *)userdata; + do { + u32 size; + camu_queue_size(http->queue, size); + if (size == 0) break; + s32 index; + camu_queue_pop(http->queue, index); + if (index == -1) { + aki_event_loop_break(&http->loop); + break; + } + al_array_push(http->requests, (struct aki_http){0}); + struct aki_http *request = &al_array_last(http->requests); + aki_http_init(request); + aki_http_set_url(request, &http->url); + aki_http_set_user_agent(request, USER_AGENT); + aki_http_request_stream(request, AKI_HTTP_GET, &http->loop, http_callback, http); + } while (1); +} + +static void handler_http_maybe_spawn_worker(struct cch_handler *handler, size_t index) +{ + struct cch_handler_http *http = (struct cch_handler_http *)handler; + camu_queue_push(http->queue, index); + aki_signal_send(&http->signal); +} + +static bool handler_http_wait_for_range(struct cch_handler *handler, struct cch_handler_wait *wait) +{ + struct cch_handler_http *http = (struct cch_handler_http *)handler; + aki_mutex_lock(&http->mutex); + bool canceled = false; + if (cch_entry_get_size(http->handler.entry) < 0 || http->pointer < wait->end) { + aki_mutex_lock(&wait->mutex); + if (!wait->disabled) { + al_array_push(http->handler.waits, wait); + aki_mutex_unlock(&http->mutex); + aki_cond_wait(&wait->cond, &wait->mutex); + } else { + aki_mutex_unlock(&wait->mutex); + aki_mutex_unlock(&http->mutex); + return false; + } + aki_mutex_unlock(&wait->mutex); + aki_mutex_lock(&http->mutex); + if (wait->disabled) { + u16 id = wait->id; + al_array_foreach(http->handler.waits, i, wait) { + if (wait->id == id) { + al_array_remove_at_iter(http->handler.waits, i); + break; + } + } + canceled = true; + } + } + aki_mutex_unlock(&http->mutex); + return !canceled; +} + +static void handler_http_free(struct cch_handler **handler) +{ + struct cch_handler_http *http = (struct cch_handler_http *)*handler; + aki_mutex_lock(&http->mutex); + struct cch_handler_wait *wait; + al_array_foreach(http->handler.waits, i, wait) { + aki_mutex_lock(&wait->mutex); + wait->disabled = true; + aki_cond_signal(&wait->cond); + aki_mutex_unlock(&wait->mutex); + } + aki_mutex_unlock(&http->mutex); + camu_queue_push(http->queue, -1); + aki_signal_send(&http->signal); + aki_thread_join(&http->thread); + aki_mutex_destroy(&http->mutex); + struct aki_http *request; + al_array_foreach_ptr(http->requests, i, request) { + aki_http_close(request); + } + al_array_free(http->requests); + camu_queue_free(http->queue); + al_str_free(&http->url); + al_array_free(http->handler.waits); + al_free(http); + *handler = NULL; +} + +static aki_thread_result AKI_THREADCALL event_loop_thread(void *userdata) +{ + struct cch_handler_http *http = (struct cch_handler_http *)userdata; + aki_event_loop_run(&http->loop); + aki_event_loop_destroy(&http->loop); + return 0; +} + +struct cch_entry *cch_handler_http_create(str *url) +{ + //struct cch_backing *backing = cch_backing_file_create(al_str_c("/tmp/camu_http_data"), 1024 * 128); + struct cch_backing *backing = cch_backing_memory_create(1024 * 128); + if (!backing) return NULL; + struct cch_entry *entry = al_alloc_object(struct cch_entry); + entry->backing = backing; + entry->ref_count = 0; + entry->hash = 0; + entry->handler = (struct cch_handler *)al_alloc_object(struct cch_handler_http); + aki_mutex_init(&entry->mutex); + entry->handler->can_seek = handler_http_can_seek; + entry->handler->maybe_spawn_worker = handler_http_maybe_spawn_worker; + entry->handler->wait_for_range = handler_http_wait_for_range; + entry->handler->free = handler_http_free; + entry->handler->entry = entry; + entry->handler->backing = entry->backing; + al_array_init(entry->handler->waits); + struct cch_handler_http *http = (struct cch_handler_http *)entry->handler; + al_str_clone(&http->url, url); + http->pointer = 0; + al_array_init(http->requests); + camu_queue_init(http->queue); + aki_event_loop_init(&http->loop); + aki_signal_init(&http->signal, signal_callback, http); + aki_signal_start(&http->signal, &http->loop); + aki_mutex_init(&http->mutex); + aki_thread_create(&http->thread, event_loop_thread, http); + return entry; +} diff --git a/src/cache/handlers/http.h b/src/cache/handlers/http.h new file mode 100644 index 0000000..f79a39a --- /dev/null +++ b/src/cache/handlers/http.h @@ -0,0 +1,24 @@ +#pragma once + +#include <aki/http.h> +#include <aki/thread.h> +#include <aki/signal.h> + +#include "../../util/queue.h" + +#include "../handler.h" +#include "../entry.h" + +struct cch_handler_http { + struct cch_handler handler; + off_t pointer; + str url; + array(struct aki_http) requests; + queue(s32) queue; + struct aki_event_loop loop; + struct aki_signal signal; + struct aki_mutex mutex; + struct aki_thread thread; +}; + +struct cch_entry *cch_handler_http_create(str *url); diff --git a/src/cache/meson.build b/src/cache/meson.build new file mode 100644 index 0000000..099b2a6 --- /dev/null +++ b/src/cache/meson.build @@ -0,0 +1,18 @@ +cache_src = [ + 'entry.c', + 'handle.c', + 'handlers/file.c', + 'backings/memory.c' +] + +if akiyo_has_mmap + cache_src += ['backings/file_mapped.c'] +else + cache_src += ['backings/file.c'] +endif + +if akiyo_has_curl + cache_src += ['handlers/http.c'] +endif + +cache = declare_dependency(sources: cache_src) diff --git a/src/codec/codec.h b/src/codec/codec.h new file mode 100644 index 0000000..d30a648 --- /dev/null +++ b/src/codec/codec.h @@ -0,0 +1,115 @@ +#pragma once + +#include <al/types.h> +#include <al/array.h> +#include <aki/common.h> + +#include "../cache/handle.h" + +#ifdef HAVE_FFMPEG +#include <libavutil/pixdesc.h> +#include <libavformat/avformat.h> +#endif + +#ifdef HAVE_FFMPEG +enum { + CAMU_OK = 0, + CAMU_ERR_EOF = AVERROR_EOF, + CAMU_ERR_AGAIN = AVERROR(EAGAIN) +}; +enum { + CAMU_SEEK_SIZE = AVSEEK_SIZE +}; +#else +enum { + CAMU_OK = 0, + CAMU_ERR_EOF = -1, + CAMU_ERR_AGAIN = -2 +}; +enum { + CAMU_SEEK_SIZE = 0x10000 +}; +#endif + +enum { + CAMU_PIXEL_FMT_RGB = 0, + CAMU_PIXEL_FMT_RGBA +}; + +enum { + CAMU_FRAME_DEVICE_ALLOCATED = 1 +}; + +enum { + CAMU_NORMAL = 0, +#ifdef HAVE_FFMPEG + CAMU_FFMPEG_COMPAT +#endif +}; + +struct camu_icc_profile { + u8 *data; + size_t size; +}; + +struct camu_stream { + u8 type; + s64 duration; + struct { + u8 fmt; + s32 width; + s32 height; + struct camu_icc_profile icc; + } video; + //struct { + //} audio; +#ifdef HAVE_FFMPEG + struct { + AVStream *stream; + AVFormatContext *format_context; + } av; +#endif +}; + +struct camu_packet { + u8 type; + struct aki_buffer *buffer; +#ifdef HAVE_FFMPEG + struct { AVPacket *pkt; } av; +#endif +}; + +struct camu_frame { + u8 type; + u8 *data; + u8 flags; + u8 fmt; + s32 width; + s32 height; + f64 pts; +#ifdef HAVE_FFMPEG + struct { AVFrame *frame; } av; +#endif + void *opaque; +}; + +struct camu_demuxer { + bool (*init)(struct camu_demuxer *, struct cch_handle *); + s32 (*get_packet)(struct camu_demuxer *, struct camu_packet *); + s64 (*get_duration)(struct camu_demuxer *); + bool (*seek)(struct camu_demuxer *, s64); + void (*free)(struct camu_demuxer **); + u8 type; + array(struct camu_stream) streams; +}; + +struct camu_renderer; +struct camu_decoder { + bool (*init)(struct camu_decoder *, struct camu_renderer *, struct camu_stream *); + void (*set_callback)(struct camu_decoder *, void (*callback)(void *, struct camu_frame *), void *); + s32 (*push)(struct camu_decoder *, struct camu_packet *); + s32 (*process)(struct camu_decoder *); + void (*flush)(struct camu_decoder *); + void (*free)(struct camu_decoder **); + struct camu_stream *stream; +}; diff --git a/src/codec/common.h b/src/codec/common.h new file mode 100644 index 0000000..f3db182 --- /dev/null +++ b/src/codec/common.h @@ -0,0 +1,29 @@ +#pragma once + +#include <al/lib.h> + +static inline void *camu_page_alloc(size_t size) +{ +#ifdef HAVE_POSIX_MEMALIGN + void *ptr = NULL; + al_posix_memalign(&ptr, al_page_size, size); + return ptr; +#else + return al_malloc(size); +#endif +} + +static inline void *camu_page_realloc(void *ptr, size_t old_size, size_t size) +{ +#ifdef HAVE_POSIX_MEMALIGN + void *old_ptr = ptr; + al_posix_memalign(&ptr, al_page_size, size); + if (old_ptr) { + al_memcpy(ptr, old_ptr, old_size); + al_free(old_ptr); + } + return ptr; +#else + return al_realloc(ptr, size); +#endif +} diff --git a/src/codec/libav/avio.c b/src/codec/libav/avio.c new file mode 100644 index 0000000..97f66da --- /dev/null +++ b/src/codec/libav/avio.c @@ -0,0 +1,13 @@ +#include "avio.h" + +s32 camu_avio_read(void *data, u8 *buf, s32 buf_size) +{ + struct cch_handle *handle = (struct cch_handle *)data; + return cch_handle_read(handle, buf, buf_size); +} + +s64 camu_avio_seek(void *data, s64 offset, s32 whence) +{ + struct cch_handle *handle = (struct cch_handle *)data; + return (s64)cch_handle_seek(handle, offset, whence); +} diff --git a/src/codec/libav/avio.h b/src/codec/libav/avio.h new file mode 100644 index 0000000..09df4ec --- /dev/null +++ b/src/codec/libav/avio.h @@ -0,0 +1,10 @@ +#pragma once + +#include <libavutil/error.h> +#include <libavformat/avio.h> + +#include "../../codec/codec.h" +#include "../../cache/handle.h" + +s32 camu_avio_read(void *data, u8 *buf, s32 buf_size); +s64 camu_avio_seek(void *data, s64 offset, s32 whence); diff --git a/src/codec/libav/common.c b/src/codec/libav/common.c new file mode 100644 index 0000000..8ce6667 --- /dev/null +++ b/src/codec/libav/common.c @@ -0,0 +1,6 @@ +#include "common.h" + +void camu_lav_set_log_callback(void (*callback)(void *, int, const char *, va_list)) +{ + av_log_set_callback(callback); +} diff --git a/src/codec/libav/common.h b/src/codec/libav/common.h new file mode 100644 index 0000000..854e4c7 --- /dev/null +++ b/src/codec/libav/common.h @@ -0,0 +1,6 @@ +#pragma once + +#include <al/types.h> +#include <libavutil/log.h> + +void camu_lav_set_log_callback(void (*callback)(void *, int, const char *, va_list)); diff --git a/src/codec/libav/decoder.c b/src/codec/libav/decoder.c new file mode 100644 index 0000000..ebdcb03 --- /dev/null +++ b/src/codec/libav/decoder.c @@ -0,0 +1,165 @@ +#include <libavutil/cpu.h> +#include <al/lib.h> +#include <al/log.h> + +//#include "../../render/renderer.h" + +#include "decoder.h" + +static void close_internal(struct camu_lav_decoder *av) +{ + if (av->codec_context) { + avcodec_close(av->codec_context); + avcodec_free_context(&av->codec_context); + } +} + +static bool lav_decoder_init(struct camu_decoder *dec, struct camu_renderer *renderer, struct camu_stream *stream) +{ + struct camu_lav_decoder *av = (struct camu_lav_decoder *)dec; + + AVCodecParameters *codecpar = stream->av.stream->codecpar; + const AVCodec *codec = avcodec_find_decoder(codecpar->codec_id); + + if (!codec) { + al_log_error("lav_decoder", "Failed to find decoder."); + goto err; + } + + av->codec_context = avcodec_alloc_context3(codec); + + if (!av->codec_context) { + al_log_error("lav_decoder", "Failed to alloc codec context."); + goto err; + } + + if (avcodec_parameters_to_context(av->codec_context, codecpar) < 0) { + al_log_error("lav_decoder", "Failed to copy codec parameters."); + goto err; + } + + s32 cpus = av_cpu_count(); + cpus = (cpus > 16) ? 4 : cpus / 4; + if (cpus == 0) cpus = 1; + av->codec_context->thread_count = cpus; + al_log_debug("lav_decoder", "Using %i threads for decoder.", cpus); + + (void)renderer; + /* + if (renderer && renderer->get_buffer2 && codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + av->codec_context->get_buffer2 = renderer->get_buffer2; + av->codec_context->opaque = renderer->opaque; + } + */ + + if (avcodec_open2(av->codec_context, codec, NULL) < 0) { + al_log_error("lav_decoder", "Failed to open codec (%s).", codec->name); + goto err; + } + + al_log_info("lav_decoder", "Codec: %s (%s) %ldkbps.", codec->name, codec->long_name, codecpar->bit_rate / 1000); + + av->time_base = stream->av.stream->time_base; + av->last_pts = 0; + av->last_duration = 0; + //av->seek_pos = -1; + + return true; +err: + close_internal(av); + + return false; +} + +static void lav_decoder_set_callback(struct camu_decoder *dec, void (*callback)(void *, struct camu_frame *), void *userdata) +{ + struct camu_lav_decoder *av = (struct camu_lav_decoder *)dec; + av->callback = callback; + av->userdata = userdata; +} + +static s32 send_packet(struct camu_lav_decoder *av, AVPacket *pkt) +{ + /* + if (pkt && av->seek_pos >= 0 && pkt->pts + pkt->duration < av->seek_pos) { + av->codec_context->skip_frame = AVDISCARD_NONKEY; + } else { + av->codec_context->skip_frame = AVDISCARD_NONE; + } + */ + + s32 ret = avcodec_send_packet(av->codec_context, pkt); + if (ret < 0 && ret != AVERROR(EAGAIN) && ret != AVERROR_EOF) { + al_log_error("lav_decoder", "Error sending packet to decoder: (%s).", av_err2str(ret)); + } + + return ret; +} + +static s32 lav_decoder_push(struct camu_decoder *dec, struct camu_packet *packet) +{ + struct camu_lav_decoder *av = (struct camu_lav_decoder *)dec; + return send_packet(av, (packet) ? packet->av.pkt : NULL); +} + +static void lav_decoder_flush(struct camu_decoder *dec) +{ + struct camu_lav_decoder *av = (struct camu_lav_decoder *)dec; + avcodec_flush_buffers(av->codec_context); +} + +static s32 receive_frames(struct camu_lav_decoder *av) +{ + s32 ret; + do { + struct camu_frame *frame = al_alloc_object(struct camu_frame); + frame->type = CAMU_FFMPEG_COMPAT; + frame->av.frame = av_frame_alloc(); + ret = avcodec_receive_frame(av->codec_context, frame->av.frame); + if (ret < 0) { + av_frame_free(&frame->av.frame); + al_free(frame); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break; + al_log_error("lav_decoder", "Error receiving packet from decoder: (%s).", av_err2str(ret)); + continue; + } + // Track pts and duration of the previous frame so we can handle multiple frames + // in a single packet. + //if (av->codec_context->codec_type == AVMEDIA_TYPE_AUDIO) { + // if (frame->av.frame->best_effort_timestamp < 0) { + // frame->av.frame->best_effort_timestamp = av->last_pts + av->last_duration; + // } + // av->last_pts = frame->av.frame->best_effort_timestamp; + // av->last_duration = av_get_audio_frame_duration(av->codec_context, frame->av.frame->linesize[0]) + // / (av->codec_context->sample_rate * av->time_base.num / (f64)av->time_base.den); + //} + av->callback(av->userdata, frame); + } while (1); + return ret; +} + +static s32 lav_decoder_process(struct camu_decoder *dec) +{ + struct camu_lav_decoder *av = (struct camu_lav_decoder *)dec; + return receive_frames(av); +} + +static void lav_decoder_free(struct camu_decoder **dec) +{ + struct camu_lav_decoder *av = (struct camu_lav_decoder *)*dec; + close_internal(av); + al_free(av); + *dec = NULL; +} + +struct camu_decoder *camu_lav_decoder_create(void) +{ + struct camu_lav_decoder *av = al_alloc_object(struct camu_lav_decoder); + av->dec.init = lav_decoder_init; + av->dec.set_callback = lav_decoder_set_callback; + av->dec.push = lav_decoder_push; + av->dec.process = lav_decoder_process; + av->dec.flush = lav_decoder_flush; + av->dec.free = lav_decoder_free; + return (struct camu_decoder *)av; +} diff --git a/src/codec/libav/decoder.h b/src/codec/libav/decoder.h new file mode 100644 index 0000000..44105ce --- /dev/null +++ b/src/codec/libav/decoder.h @@ -0,0 +1,20 @@ +#pragma once + +#include <al/types.h> + +#include <libavcodec/avcodec.h> + +#include "../codec.h" + +struct camu_lav_decoder { + struct camu_decoder dec; + AVCodecContext *codec_context; + AVRational time_base; + s64 last_pts; + s64 last_duration; + //s64 seek_pos; + void (*callback)(void *, struct camu_frame *); + void *userdata; +}; + +struct camu_decoder *camu_lav_decoder_create(void); diff --git a/src/codec/libav/demuxer.c b/src/codec/libav/demuxer.c new file mode 100644 index 0000000..f7ff35c --- /dev/null +++ b/src/codec/libav/demuxer.c @@ -0,0 +1,184 @@ +#include <al/log.h> + +#include "../common.h" + +#include "demuxer.h" + +#define BUF_SIZE 16384 + +static void close_internal(struct camu_lav_demuxer *av) +{ + if (av->io_context) { + av_free(av->io_context->buffer); + avio_context_free(&av->io_context); + } + if (av->format_context) { + avformat_flush(av->format_context); + avformat_close_input(&av->format_context); + avformat_free_context(av->format_context); + } +} + +static bool lav_demuxer_init(struct camu_demuxer *demux, struct cch_handle *handle) +{ + struct camu_lav_demuxer *av = (struct camu_lav_demuxer *)demux; + + av->format_context = avformat_alloc_context(); + if (!av->format_context) { + al_log_error("lav_demuxer", "Failed to create format context."); + goto err; + } + + u8 *buf = (u8 *)av_malloc(BUF_SIZE); + av->io_context = avio_alloc_context(buf, BUF_SIZE, 0, handle, camu_avio_read, NULL, camu_avio_seek); + if (!av->io_context) { + al_log_error("lav_demuxer", "Failed to create custom io context."); + goto err; + } + + if (!cch_handle_can_seek(handle)) { + // This should always be unset if we know we cannot seek. + // But, should also never be unset if we *do* want to seek because it + // disables long seeks (Not tested in a long time). + av->io_context->seekable &= ~AVIO_SEEKABLE_NORMAL; + } else { + av->io_context->seekable |= AVIO_SEEKABLE_NORMAL; + } + + av->format_context->pb = av->io_context; + av->format_context->flags |= AVFMT_FLAG_CUSTOM_IO; + + if (avformat_open_input(&av->format_context, "", 0, 0) < 0) { + al_log_error("lav_demuxer", "Failed to open input."); + goto err; + } + + if (avformat_find_stream_info(av->format_context, 0) < 0) { + al_log_error("lav_demux", "Failed to find stream info."); + goto err; + } + + if (!av->format_context->nb_streams) { + al_log_error("lav_demux", "No streams found."); + goto err; + } + + av->duration = 0; + + bool audio_selected = false; + bool video_selected = false; + + for (u32 i = 0; i < av->format_context->nb_streams; i++) { + AVStream *stream = av->format_context->streams[i]; + enum AVMediaType type = stream->codecpar->codec_type; + if (!(type == AVMEDIA_TYPE_AUDIO || type == AVMEDIA_TYPE_VIDEO)) { + continue; + } + if (type == AVMEDIA_TYPE_AUDIO) { + if (audio_selected) continue; + else audio_selected = true; + } + if (type == AVMEDIA_TYPE_VIDEO) { + if (video_selected) continue; + else video_selected = true; + } + // Try to detect attached images. + if (type == AVMEDIA_TYPE_VIDEO && stream->duration >= 0 && stream->nb_frames <= 1 && + (stream->avg_frame_rate.den == 0 && stream->r_frame_rate.den > 0)) { + stream->duration = 0; + al_log_info("lav_demux", "Assuming stream #%i is an image.", i); + } else if (stream->duration < 0) { + if (av->format_context->duration < 0) { + stream->duration = 0; + } else { + // This could be completely wrong. In that case, it's still probably + // better than 0 (assuming the stream is not an image). + stream->duration = av_rescale_q(av->format_context->duration, + AV_TIME_BASE_Q, stream->time_base); + } + al_log_info("lav_demux", "Setting stream #%i to a duration of %.3fs.", + i, stream->duration * av_q2d(stream->time_base)); + } + if (stream->start_time < 0) { + stream->start_time = 0; + } + s64 duration = av_rescale_q(stream->duration, stream->time_base, AV_TIME_BASE_Q); + if (duration > av->duration) { + av->duration = duration; + } + al_array_push(av->demux.streams, ((struct camu_stream){ + .type = CAMU_FFMPEG_COMPAT, + .av.stream = stream + })); + } + + av_format_inject_global_side_data(av->format_context); + + //av_dump_format(av->format_context, 0, "", 0); + + av->eof = false; + + return true; +err: + close_internal(av); + + return false; +} + +static s32 lav_demuxer_get_packet(struct camu_demuxer *demux, struct camu_packet *packet) +{ + struct camu_lav_demuxer *av = (struct camu_lav_demuxer *)demux; + packet->type = CAMU_FFMPEG_COMPAT; + s32 ret = 0; + do { + ret = av_read_frame(av->format_context, packet->av.pkt); + if (ret < 0 && ret != AVERROR_EOF) { + al_log_error("lav_demuxer", "Failed to read frame (%s).", av_err2str(ret)); + } + break; + } while (1); + if (ret == AVERROR_EOF) av->eof = true; + return ret; +} + +static s64 lav_demuxer_get_duration(struct camu_demuxer *demux) +{ + struct camu_lav_demuxer *av = (struct camu_lav_demuxer *)demux; + return av->duration; +} + +static bool lav_demuxer_seek(struct camu_demuxer *demux, s64 pos) +{ + struct camu_lav_demuxer *av = (struct camu_lav_demuxer *)demux; + s64 ts = av_rescale_q(pos, (AVRational){ 1, 1000000 }, AV_TIME_BASE_Q); + if (av->eof) { + avformat_flush(av->format_context); + av->eof = false; + } + if (avformat_seek_file(av->format_context, -1, INT64_MIN, ts, ts, AVSEEK_FLAG_BACKWARD) >= 0) { + return true; + } + return false; +} + +static void lav_demuxer_free(struct camu_demuxer **demux) +{ + struct camu_lav_demuxer *av = (struct camu_lav_demuxer *)*demux; + close_internal(av); + al_array_free(av->demux.streams); + al_free(av); + *demux = NULL; +} + +struct camu_demuxer *camu_lav_demuxer_create(void) +{ + struct camu_lav_demuxer *av = al_alloc_object(struct camu_lav_demuxer); + av->demux.type = CAMU_FFMPEG_COMPAT; + al_array_init(av->demux.streams); + av->demux.init = lav_demuxer_init; + av->demux.get_packet = lav_demuxer_get_packet; + av->demux.get_duration = lav_demuxer_get_duration; + av->demux.seek = lav_demuxer_seek; + av->demux.free = lav_demuxer_free; + return (struct camu_demuxer *)av; +} diff --git a/src/codec/libav/demuxer.h b/src/codec/libav/demuxer.h new file mode 100644 index 0000000..a9d8c5c --- /dev/null +++ b/src/codec/libav/demuxer.h @@ -0,0 +1,21 @@ +#pragma once + +#include <al/types.h> +#include <al/lib.h> +#include <al/array.h> + +#include <libavformat/avformat.h> + +#include "../codec.h" + +#include "avio.h" + +struct camu_lav_demuxer { + struct camu_demuxer demux; + AVIOContext *io_context; + AVFormatContext *format_context; + s64 duration; + bool eof; +}; + +struct camu_demuxer *camu_lav_demuxer_create(void); diff --git a/src/codec/libav/encoder.c b/src/codec/libav/encoder.c new file mode 100644 index 0000000..4f3fa80 --- /dev/null +++ b/src/codec/libav/encoder.c @@ -0,0 +1,186 @@ +#include <libavcodec/avcodec.h> +#include <libavcodec/codec.h> +#include <libavformat/avformat.h> +#include <libavutil/audio_fifo.h> +#include <libavutil/channel_layout.h> +#include <libavutil/error.h> +#include <libavutil/frame.h> +#include <libavutil/samplefmt.h> +#include <al/lib.h> +#include <al/log.h> + +#include "encoder.h" + +#define BITRATE 256000 +#define FRAME_SIZE 20 +#define CHANNELS 2 + +bool camu_lav_encoder_init(struct camu_lav_encoder *enc, char *encoder_name, char *ext) +{ + al_memset(enc, 0, sizeof(struct camu_lav_encoder)); + + const AVCodec *codec = avcodec_find_encoder_by_name(encoder_name); + + if (!codec) { + al_log_error("lav_encoder", "Failed to find codec with name: %s.", encoder_name); + goto err; + } + + enc->codec_context = avcodec_alloc_context3(codec); + + if (!enc->codec_context) { + al_log_error("lav_encoder", "Failed to alloc codec context."); + goto err; + } + + enc->codec_context->sample_rate = 48000; + av_channel_layout_default(&enc->codec_context->ch_layout, CHANNELS); + enc->codec_context->sample_fmt = AV_SAMPLE_FMT_FLT; + enc->codec_context->bit_rate = BITRATE; + enc->codec_context->frame_size = FRAME_SIZE; + enc->codec_context->time_base = (AVRational){ 1, enc->codec_context->sample_rate }; + enc->codec_context->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL; + + enc->output_format = av_guess_format(ext, NULL, NULL); + if (enc->output_format && enc->output_format->flags & AVFMT_GLOBALHEADER) { + enc->codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + } + + AVDictionary *opts = NULL; + av_dict_set(&opts, "b", "256k", 0); + av_dict_set(&opts, "vbr", "off", 0); + av_dict_set(&opts, "compression_level", "10", 0); + av_dict_set(&opts, "frame_duration", "20", 0); + av_dict_set(&opts, "application", "audio", 0); + + if (avcodec_open2(enc->codec_context, codec, &opts) < 0) { + al_log_error("lav_encoder", "Failed to open codec."); + av_dict_free(&opts); + goto err; + } + + av_dict_free(&opts); + + AVCodecContext *out_codec = enc->codec_context; + + av_channel_layout_copy(&enc->fmt.req_channel_layout, &out_codec->ch_layout); + enc->fmt.req_sample_rate = out_codec->sample_rate; + enc->fmt.req_format = out_codec->sample_fmt; + + enc->audio_fifo = av_audio_fifo_alloc(enc->fmt.req_format, + enc->fmt.req_channel_layout.nb_channels, 1); + + if (!enc->audio_fifo) { + al_log_error("lav_encoder", "Failed to alloc fifo."); + goto err; + } + + enc->frame = av_frame_alloc(); + enc->frame->nb_samples = 0; + enc->frame->sample_rate = enc->fmt.req_sample_rate; + av_channel_layout_copy(&enc->frame->ch_layout, &enc->fmt.req_channel_layout); + enc->frame->format = enc->fmt.req_format; + enc->frame->pts = 0; + + return true; +err: + camu_lav_encoder_close(enc); + return false; +} + +void camu_lav_encoder_push(struct camu_lav_encoder *enc, u8 **data, s32 sample_count) +{ + s32 alloc_size = av_audio_fifo_size(enc->audio_fifo) + sample_count; + if (av_audio_fifo_space(enc->audio_fifo) < alloc_size) { + if (av_audio_fifo_realloc(enc->audio_fifo, alloc_size) < 0) { + al_assert(false); + } + } + av_audio_fifo_write(enc->audio_fifo, (void **)data, sample_count); +} + +static void free_frame_data(struct camu_lav_encoder *enc) +{ + if (enc->frame && enc->frame->nb_samples > 0) av_freep(&enc->frame->data[0]); +} + +static void alloc_frame_data(struct camu_lav_encoder *enc, s32 frame_size) +{ + if (enc->frame->nb_samples >= frame_size) return; + free_frame_data(enc); + s32 nb_channels = enc->frame->ch_layout.nb_channels; + av_samples_alloc(enc->frame->data, NULL, nb_channels, frame_size, enc->frame->format, 0); + enc->frame->nb_samples = frame_size; +} + +static bool receive_packets(struct camu_lav_encoder *enc) +{ + s32 ret; + AVPacket pkt = { 0 }; + + do { + ret = avcodec_receive_packet(enc->codec_context, &pkt); + if (ret < 0) break; + enc->callback(enc->userdata, &pkt); + } while (1); + + if (ret != AVERROR(EAGAIN) && ret != AVERROR_EOF) { + al_log_error("lav_encoder", "Error receiving packet from encoder: (%s).", av_err2str(ret)); + return false; + } + + return true; +} + +static s32 send_frame(struct camu_lav_encoder *enc, AVFrame *in_frame) +{ + s32 ret = avcodec_send_frame(enc->codec_context, in_frame); + if (ret < 0 && ret != AVERROR_EOF) { + al_log_error("lav_encoder", "Error sending frame to encoder: (%s).", av_err2str(ret)); + } + return ret; +} + +bool camu_lav_encoder_process(struct camu_lav_encoder *enc, bool flush) +{ + s32 samples_available = av_audio_fifo_size(enc->audio_fifo); + + if (samples_available == 0 && flush) { + // Flush encoder. + send_frame(enc, NULL); + receive_packets(enc); + // This should guarantee that there are not packets left. + return false; + } + + s32 frame_size = enc->codec_context->frame_size; + + if (samples_available == 0 || (samples_available < frame_size && !flush)) { + return false; + } + + if (samples_available < frame_size) frame_size = samples_available; + + alloc_frame_data(enc, frame_size); + av_audio_fifo_read(enc->audio_fifo, (void **)enc->frame->data, frame_size); + + s32 ret; + do { + ret = send_frame(enc, enc->frame); + // TODO: Check ret < 0 ? + if (!receive_packets(enc)) return false; + } while (ret == AVERROR(EAGAIN)); + + enc->frame->pts += frame_size; + + // ret < 0 means encoder erroed while sending frame. + return ret == 0; +} + +void camu_lav_encoder_close(struct camu_lav_encoder *enc) +{ + if (enc->codec_context) avcodec_free_context(&enc->codec_context); + if (enc->audio_fifo) av_audio_fifo_free(enc->audio_fifo); + free_frame_data(enc); + if (enc->frame) av_frame_free(&enc->frame); +} diff --git a/src/codec/libav/encoder.h b/src/codec/libav/encoder.h new file mode 100644 index 0000000..b5a707a --- /dev/null +++ b/src/codec/libav/encoder.h @@ -0,0 +1,26 @@ +#pragma once + +#include <libavcodec/avcodec.h> +#include <libavformat/avformat.h> +#include <libavutil/audio_fifo.h> +#include <libavutil/rational.h> + +#include "resampler.h" + +struct camu_lav_encoder { + AVCodecContext *codec_context; + const AVOutputFormat *output_format; + + struct camu_lav_resample_fmt fmt; + + AVFrame *frame; + AVAudioFifo *audio_fifo; + + void (*callback)(void *, AVPacket *); + void *userdata; +}; + +bool camu_lav_encoder_init(struct camu_lav_encoder *enc, char *encoder_name, char *ext); +void camu_lav_encoder_push(struct camu_lav_encoder *enc, u8 **data, s32 sample_count); +bool camu_lav_encoder_process(struct camu_lav_encoder *enc, bool flush); +void camu_lav_encoder_close(struct camu_lav_encoder *enc); diff --git a/src/codec/libav/packet_ext.c b/src/codec/libav/packet_ext.c new file mode 100644 index 0000000..8fe4c39 --- /dev/null +++ b/src/codec/libav/packet_ext.c @@ -0,0 +1,168 @@ +#include "packet_ext.h" + +void aki_packet_write_av_codec_parameters(struct aki_packet *packet, AVCodecParameters *codecpar) +{ + AKI_PACKET_WRITE_TYPE(packet, enum AVMediaType, codecpar->codec_type); + AKI_PACKET_WRITE_TYPE(packet, enum AVCodecID, codecpar->codec_id); + AKI_PACKET_WRITE_TYPE(packet, u32, codecpar->codec_tag); + AKI_PACKET_WRITE_TYPE(packet, s32, codecpar->extradata_size); + AKI_PACKET_WRITE_DATA(packet, codecpar->extradata, codecpar->extradata_size); + AKI_PACKET_WRITE_TYPE(packet, s32, codecpar->format); + AKI_PACKET_WRITE_TYPE(packet, s64, codecpar->bit_rate); + AKI_PACKET_WRITE_TYPE(packet, s32, codecpar->bits_per_coded_sample); + AKI_PACKET_WRITE_TYPE(packet, s32, codecpar->bits_per_raw_sample); + AKI_PACKET_WRITE_TYPE(packet, s32, codecpar->profile); + AKI_PACKET_WRITE_TYPE(packet, s32, codecpar->level); + AKI_PACKET_WRITE_TYPE(packet, s32, codecpar->width); + AKI_PACKET_WRITE_TYPE(packet, s32, codecpar->height); + AKI_PACKET_WRITE_TYPE(packet, AVRational, codecpar->sample_aspect_ratio); + AKI_PACKET_WRITE_TYPE(packet, enum AVFieldOrder, codecpar->field_order); + AKI_PACKET_WRITE_TYPE(packet, enum AVColorRange, codecpar->color_range); + AKI_PACKET_WRITE_TYPE(packet, enum AVColorPrimaries, codecpar->color_primaries); + AKI_PACKET_WRITE_TYPE(packet, enum AVColorTransferCharacteristic, codecpar->color_trc); + AKI_PACKET_WRITE_TYPE(packet, enum AVColorSpace, codecpar->color_space); + AKI_PACKET_WRITE_TYPE(packet, enum AVChromaLocation, codecpar->chroma_location); + AKI_PACKET_WRITE_TYPE(packet, s32, codecpar->video_delay); + AKI_PACKET_WRITE_TYPE(packet, s32, codecpar->sample_rate); + AKI_PACKET_WRITE_TYPE(packet, s32, codecpar->block_align); + AKI_PACKET_WRITE_TYPE(packet, s32, codecpar->frame_size); + AKI_PACKET_WRITE_TYPE(packet, s32, codecpar->initial_padding); + AKI_PACKET_WRITE_TYPE(packet, s32, codecpar->trailing_padding); + AKI_PACKET_WRITE_TYPE(packet, s32, codecpar->seek_preroll); + AKI_PACKET_WRITE_TYPE(packet, AVChannelLayout, codecpar->ch_layout); +} + +void aki_packet_write_av_rational(struct aki_packet *packet, AVRational rational) +{ + AKI_PACKET_WRITE_TYPE(packet, AVRational, rational); +} + +void aki_packet_write_av_codec_id(struct aki_packet *packet, enum AVCodecID codec_id) +{ + AKI_PACKET_WRITE_TYPE(packet, enum AVCodecID, codec_id); +} + +void aki_packet_write_av_stream(struct aki_packet *packet, AVStream *stream) +{ + AKI_PACKET_WRITE_TYPE(packet, s32, stream->index); + aki_packet_write_av_codec_parameters(packet, stream->codecpar); + aki_packet_write_av_rational(packet, stream->time_base); + AKI_PACKET_WRITE_TYPE(packet, s64, stream->duration); + AKI_PACKET_WRITE_TYPE(packet, s64, stream->start_time); + AKI_PACKET_WRITE_TYPE(packet, s64, stream->nb_frames); + if (stream->avg_frame_rate.den == 0) { + // r_frame_rate.den == 0 handled on the client. + aki_packet_write_av_rational(packet, stream->r_frame_rate); + } else { + aki_packet_write_av_rational(packet, stream->avg_frame_rate); + } +} + +void aki_packet_write_av_packet(struct aki_packet *packet, AVPacket *pkt) +{ + AKI_PACKET_WRITE_TYPE(packet, s64, pkt->pts); + AKI_PACKET_WRITE_TYPE(packet, s64, pkt->dts); + AKI_PACKET_WRITE_TYPE(packet, s32, pkt->size); + AKI_PACKET_WRITE_DATA(packet, pkt->data, pkt->size); + AKI_PACKET_WRITE_TYPE(packet, s32, pkt->stream_index); + AKI_PACKET_WRITE_TYPE(packet, s32, pkt->flags); + AKI_PACKET_WRITE_TYPE(packet, s32, pkt->side_data_elems); + for (s32 i = 0; i < pkt->side_data_elems; i++) { + AKI_PACKET_WRITE_TYPE(packet, size_t, pkt->side_data[i].size); + AKI_PACKET_WRITE_DATA(packet, pkt->side_data[i].data, pkt->side_data[i].size); + AKI_PACKET_WRITE_TYPE(packet, enum AVPacketSideDataType, pkt->side_data[i].type); + } + AKI_PACKET_WRITE_TYPE(packet, s64, pkt->duration); + AKI_PACKET_WRITE_TYPE(packet, s64, pkt->pos); + AKI_PACKET_WRITE_TYPE(packet, AVRational, pkt->time_base); +} + +void aki_packet_read_av_codec_parameters(struct aki_packet *packet, AVCodecParameters *codecpar) +{ + AKI_PACKET_READ_TYPE(packet, enum AVMediaType, codecpar->codec_type); + AKI_PACKET_READ_TYPE(packet, enum AVCodecID, codecpar->codec_id); + AKI_PACKET_READ_TYPE(packet, u32, codecpar->codec_tag); + AKI_PACKET_READ_TYPE(packet, s32, codecpar->extradata_size); + codecpar->extradata = av_malloc(codecpar->extradata_size); + u8 *extradata; + AKI_PACKET_READ_DATA(packet, codecpar->extradata_size, extradata); + al_memcpy(codecpar->extradata, extradata, codecpar->extradata_size); + AKI_PACKET_READ_TYPE(packet, s32, codecpar->format); + AKI_PACKET_READ_TYPE(packet, s64, codecpar->bit_rate); + AKI_PACKET_READ_TYPE(packet, s32, codecpar->bits_per_coded_sample); + AKI_PACKET_READ_TYPE(packet, s32, codecpar->bits_per_raw_sample); + AKI_PACKET_READ_TYPE(packet, s32, codecpar->profile); + AKI_PACKET_READ_TYPE(packet, s32, codecpar->level); + AKI_PACKET_READ_TYPE(packet, s32, codecpar->width); + AKI_PACKET_READ_TYPE(packet, s32, codecpar->height); + AKI_PACKET_READ_TYPE(packet, AVRational, codecpar->sample_aspect_ratio); + AKI_PACKET_READ_TYPE(packet, enum AVFieldOrder, codecpar->field_order); + AKI_PACKET_READ_TYPE(packet, enum AVColorRange, codecpar->color_range); + AKI_PACKET_READ_TYPE(packet, enum AVColorPrimaries, codecpar->color_primaries); + AKI_PACKET_READ_TYPE(packet, enum AVColorTransferCharacteristic, codecpar->color_trc); + AKI_PACKET_READ_TYPE(packet, enum AVColorSpace, codecpar->color_space); + AKI_PACKET_READ_TYPE(packet, enum AVChromaLocation, codecpar->chroma_location); + AKI_PACKET_READ_TYPE(packet, s32, codecpar->video_delay); + AKI_PACKET_READ_TYPE(packet, s32, codecpar->sample_rate); + AKI_PACKET_READ_TYPE(packet, s32, codecpar->block_align); + AKI_PACKET_READ_TYPE(packet, s32, codecpar->frame_size); + AKI_PACKET_READ_TYPE(packet, s32, codecpar->initial_padding); + AKI_PACKET_READ_TYPE(packet, s32, codecpar->trailing_padding); + AKI_PACKET_READ_TYPE(packet, s32, codecpar->seek_preroll); + AKI_PACKET_READ_TYPE(packet, AVChannelLayout, codecpar->ch_layout); +} + +AVRational aki_packet_read_av_rational(struct aki_packet *packet) +{ + AVRational r; + AKI_PACKET_READ_TYPE(packet, AVRational, r); + return r; +} + +enum AVCodecID aki_packet_read_av_codec_id(struct aki_packet *packet) +{ + enum AVCodecID id; + AKI_PACKET_READ_TYPE(packet, enum AVCodecID, id); + return id; +} + +AVStream *aki_packet_read_av_stream(AVFormatContext *format_context, const AVCodec *codec, struct aki_packet *packet) +{ + AVStream *stream = avformat_new_stream(format_context, codec); + AKI_PACKET_READ_TYPE(packet, s32, stream->index); + aki_packet_read_av_codec_parameters(packet, stream->codecpar); + stream->time_base = aki_packet_read_av_rational(packet); + AKI_PACKET_READ_TYPE(packet, s64, stream->duration); + AKI_PACKET_READ_TYPE(packet, s64, stream->start_time); + AKI_PACKET_READ_TYPE(packet, s64, stream->nb_frames); + stream->avg_frame_rate = aki_packet_read_av_rational(packet); + return stream; +} + +AVPacket *aki_packet_read_av_packet(struct aki_packet *packet) +{ + AVPacket *pkt = av_packet_alloc(); + AKI_PACKET_READ_TYPE(packet, s64, pkt->pts); + AKI_PACKET_READ_TYPE(packet, s64, pkt->dts); + AKI_PACKET_READ_TYPE(packet, s32, pkt->size); + AKI_PACKET_READ_DATA(packet, pkt->size, pkt->data); + AKI_PACKET_READ_TYPE(packet, s32, pkt->stream_index); + AKI_PACKET_READ_TYPE(packet, s32, pkt->flags); + s32 side_data_elems; + AKI_PACKET_READ_TYPE(packet, s32, side_data_elems); + pkt->side_data_elems = 0; + for (s32 i = 0; i < side_data_elems; i++) { + size_t size; + u8 *data; + enum AVPacketSideDataType type; + AKI_PACKET_READ_TYPE(packet, s32, size); + AKI_PACKET_READ_DATA(packet, size, data); + AKI_PACKET_READ_TYPE(packet, enum AVPacketSideDataType, type); + u8 *side_data = av_packet_new_side_data(pkt, type, size); + al_memcpy(side_data, data, size); + } + AKI_PACKET_READ_TYPE(packet, s64, pkt->duration); + AKI_PACKET_READ_TYPE(packet, s64, pkt->pos); + AKI_PACKET_READ_TYPE(packet, AVRational, pkt->time_base); + return pkt; +} diff --git a/src/codec/libav/packet_ext.h b/src/codec/libav/packet_ext.h new file mode 100644 index 0000000..e2f7cd5 --- /dev/null +++ b/src/codec/libav/packet_ext.h @@ -0,0 +1,19 @@ +#pragma once + +#include <aki/common.h> + +#include <libavcodec/packet.h> +#include <libavcodec/codec_par.h> +#include <libavformat/avformat.h> + +void aki_packet_write_av_codec_parameters(struct aki_packet *packet, AVCodecParameters *codecpar); +void aki_packet_write_av_rational(struct aki_packet *packet, AVRational rational); +void aki_packet_write_av_codec_id(struct aki_packet *packet, enum AVCodecID codec_id); +void aki_packet_write_av_stream(struct aki_packet *packet, AVStream *stream); +void aki_packet_write_av_packet(struct aki_packet *packet, AVPacket *pkt); + +void aki_packet_read_av_codec_parameters(struct aki_packet *packet, AVCodecParameters *codecpar); +AVRational aki_packet_read_av_rational(struct aki_packet *packet); +enum AVCodecID aki_packet_read_av_codec_id(struct aki_packet *packet); +AVStream *aki_packet_read_av_stream(AVFormatContext *format_context, const AVCodec *codec, struct aki_packet *packet); +AVPacket *aki_packet_read_av_packet(struct aki_packet *packet); diff --git a/src/codec/libav/resampler.c b/src/codec/libav/resampler.c new file mode 100644 index 0000000..3dcb6d4 --- /dev/null +++ b/src/codec/libav/resampler.c @@ -0,0 +1,163 @@ +#include <libavutil/channel_layout.h> +#include <libavutil/frame.h> +#include <libavutil/opt.h> +#include <libavutil/samplefmt.h> +#include <libswresample/swresample.h> +#include <al/lib.h> +#include <al/log.h> + +#include "resampler.h" + +static bool in_req_match(struct camu_lav_resample_fmt *fmt) +{ + return (av_channel_layout_compare(&fmt->in_channel_layout, &fmt->req_channel_layout) == 0) && + (fmt->in_format == fmt->req_format) && (fmt->in_sample_rate == fmt->req_sample_rate); +} + +bool camu_lav_resampler_init(struct camu_lav_resampler *resamp, struct camu_lav_resample_fmt *fmt) +{ + al_memset(resamp, 0, sizeof(struct camu_lav_resampler)); + + fmt->resampler_needed = !in_req_match(fmt); + + camu_lav_resample_fmt_copy(&resamp->fmt, fmt); + + if (!fmt->resampler_needed) { + return true; + } + + resamp->resample_context = swr_alloc(); + + SwrContext *ctx = resamp->resample_context; + + if (!ctx) { + al_log_error("lav_resampler", "Failed to create resampler context."); + return false; + } + + av_opt_set_chlayout(ctx, "in_chlayout", &fmt->in_channel_layout, 0); + av_opt_set_chlayout(ctx, "out_chlayout", &fmt->req_channel_layout, 0); + av_opt_set_int(ctx, "in_sample_rate", fmt->in_sample_rate, 0); + av_opt_set_int(ctx, "out_sample_rate", fmt->req_sample_rate, 0); + av_opt_set_sample_fmt(ctx, "in_sample_fmt", fmt->in_format, 0); + av_opt_set_sample_fmt(ctx, "out_sample_fmt", fmt->req_format, 0); + + // Slower but more accurate resampler. + av_opt_set_int(ctx, "resampler", SWR_ENGINE_SOXR, 0); + + if (swr_init(ctx) != 0) { + swr_free(&ctx); + al_log_error("lav_resampler", "Failed to open resampler context."); + return false; + } + + return true; +} + +s32 camu_lav_resampler_sample_count(struct camu_lav_resampler *resamp, s32 in_samples) +{ + al_assert(resamp->fmt.resampler_needed); + return swr_get_out_samples(resamp->resample_context, in_samples); +} + +static inline void free_sample_data(struct camu_lav_resampler *resamp) +{ + if (resamp->data[0]) av_freep(&resamp->data[0]); +} + +static inline void alloc_sample_data(struct camu_lav_resampler *resamp, s32 sample_count) +{ + if (resamp->sample_count >= sample_count) return; + free_sample_data(resamp); + s32 nb_channels = resamp->fmt.req_channel_layout.nb_channels; + av_samples_alloc(resamp->data, NULL, nb_channels, sample_count, resamp->fmt.req_format, 0); + resamp->sample_count = sample_count; +} + +s32 camu_lav_resampler_convert(struct camu_lav_resampler *resamp, const u8 **in_data, s32 in_samples) +{ + al_assert(resamp->fmt.resampler_needed); + s32 max_resample_count = in_samples ? + camu_lav_resampler_sample_count(resamp, in_samples) : resamp->sample_count; + alloc_sample_data(resamp, max_resample_count); + s32 resample_count = swr_convert(resamp->resample_context, resamp->data, + max_resample_count, in_data, in_samples); + if (resample_count < 0) { + al_log_error("lav_resampler", "Failed to convert samples (%i).", resample_count); + } + return resample_count; +} + +u8 **camu_lav_resampler_get_data(struct camu_lav_resampler *resamp) +{ + return (u8 **)resamp->data; +} + +void camu_lav_resampler_close(struct camu_lav_resampler *resamp) +{ + al_assert(resamp->fmt.resampler_needed); + if (resamp->resample_context) { + swr_close(resamp->resample_context); + swr_free(&resamp->resample_context); + } + free_sample_data(resamp); +} + +void camu_lav_resample_fmt_copy(struct camu_lav_resample_fmt *dest, struct camu_lav_resample_fmt *src) +{ + dest->in_format = src->in_format; + av_channel_layout_copy(&dest->in_channel_layout, &src->in_channel_layout); + dest->in_sample_rate = src->in_sample_rate; + dest->req_format = src->req_format; + av_channel_layout_copy(&dest->req_channel_layout, &src->req_channel_layout); + dest->req_sample_rate = src->req_sample_rate; + dest->resampler_needed = src->resampler_needed; +} + +size_t camu_lav_resample_fmt_bytes_per_sample(struct camu_lav_resample_fmt *fmt) +{ + return (size_t)av_get_bytes_per_sample(fmt->req_format); +} + +size_t camu_lav_resample_fmt_samples_to_bytes(struct camu_lav_resample_fmt *fmt, size_t samples) +{ + s32 size, nb_channels = fmt->req_channel_layout.nb_channels; + if (av_samples_get_buffer_size(&size, nb_channels, (s32)samples, fmt->req_format, 1) < 0) { + return 0; + } + return (size_t)size; +} + +size_t camu_lav_resample_fmt_samples_to_usec(struct camu_lav_resample_fmt *fmt, size_t samples) +{ + return samples / (fmt->req_sample_rate / 1000000.0); +} + +f64 camu_lav_resample_fmt_samples_to_sec(struct camu_lav_resample_fmt *fmt, size_t samples) +{ + return samples / (f64)fmt->req_sample_rate; +} + +size_t camu_lav_resample_fmt_usec_to_bytes(struct camu_lav_resample_fmt *fmt, size_t usec) +{ + size_t samples = usec * (fmt->req_sample_rate / 1000000.0); + return samples * (camu_lav_resample_fmt_bytes_per_sample(fmt) * fmt->req_channel_layout.nb_channels); +} + +size_t camu_lav_resample_fmt_sec_to_bytes(struct camu_lav_resample_fmt *fmt, f64 sec) +{ + size_t samples = sec * fmt->req_sample_rate; + return samples * (camu_lav_resample_fmt_bytes_per_sample(fmt) * fmt->req_channel_layout.nb_channels); +} + +size_t camu_lav_resample_fmt_bytes_to_usec(struct camu_lav_resample_fmt *fmt, size_t bytes) +{ + size_t samples = bytes / (camu_lav_resample_fmt_bytes_per_sample(fmt) * fmt->req_channel_layout.nb_channels); + return samples / (fmt->req_sample_rate / 1000000.0); +} + +f64 camu_lav_resample_fmt_bytes_to_sec(struct camu_lav_resample_fmt *fmt, size_t bytes) +{ + size_t samples = bytes / (camu_lav_resample_fmt_bytes_per_sample(fmt) * fmt->req_channel_layout.nb_channels); + return samples / ((f64)fmt->req_sample_rate); +} diff --git a/src/codec/libav/resampler.h b/src/codec/libav/resampler.h new file mode 100644 index 0000000..703d9a7 --- /dev/null +++ b/src/codec/libav/resampler.h @@ -0,0 +1,40 @@ +#pragma once + +#include <libswresample/swresample.h> +#include <libavutil/opt.h> +#include <al/types.h> + +struct camu_lav_resample_fmt { + s32 in_channel_count; + s32 req_channel_count; + AVChannelLayout in_channel_layout; + AVChannelLayout req_channel_layout; + s32 in_sample_rate; + s32 req_sample_rate; + enum AVSampleFormat in_format; + enum AVSampleFormat req_format; + bool resampler_needed; +}; + +struct camu_lav_resampler { + SwrContext *resample_context; + struct camu_lav_resample_fmt fmt; + u8 *data[AV_NUM_DATA_POINTERS]; + s32 sample_count; +}; + +bool camu_lav_resampler_init(struct camu_lav_resampler *resamp, struct camu_lav_resample_fmt *fmt); +s32 camu_lav_resampler_sample_count(struct camu_lav_resampler *resamp, s32 in_samples); +s32 camu_lav_resampler_convert(struct camu_lav_resampler *resamp, const u8 **in_data, s32 in_samples); +u8 **camu_lav_resampler_get_data(struct camu_lav_resampler *resamp); +void camu_lav_resampler_close(struct camu_lav_resampler *resamp); + +void camu_lav_resample_fmt_copy(struct camu_lav_resample_fmt *dest, struct camu_lav_resample_fmt *src); +size_t camu_lav_resample_fmt_bytes_per_sample(struct camu_lav_resample_fmt *fmt); +size_t camu_lav_resample_fmt_samples_to_bytes(struct camu_lav_resample_fmt *fmt, size_t samples); +size_t camu_lav_resample_fmt_samples_to_usec(struct camu_lav_resample_fmt *fmt, size_t samples); +f64 camu_lav_resample_fmt_samples_to_sec(struct camu_lav_resample_fmt *fmt, size_t samples); +size_t camu_lav_resample_fmt_usec_to_bytes(struct camu_lav_resample_fmt *fmt, size_t usec); +size_t camu_lav_resample_fmt_sec_to_bytes(struct camu_lav_resample_fmt *fmt, f64 sec); +size_t camu_lav_resample_fmt_bytes_to_usec(struct camu_lav_resample_fmt *fmt, size_t bytes); +f64 camu_lav_resample_fmt_bytes_to_sec(struct camu_lav_resample_fmt *fmt, size_t bytes); diff --git a/src/codec/libav/scaler.c b/src/codec/libav/scaler.c new file mode 100644 index 0000000..6146c69 --- /dev/null +++ b/src/codec/libav/scaler.c @@ -0,0 +1,79 @@ +#include <al/lib.h> +#include <al/log.h> +#include <libavutil/imgutils.h> +#include <libavcodec/avcodec.h> + +#include "scaler.h" + +static AVFrame *alloc_picture(enum AVPixelFormat pix_fmt, s32 width, s32 height) +{ + AVFrame *frame = av_frame_alloc(); + if (!frame) return NULL; + s32 size = av_image_get_buffer_size(pix_fmt, width, height, 1); + u8 *buffer = (u8 *)av_malloc(size); + if (!buffer) { + av_free(frame); + return NULL; + } + s32 ret = av_image_fill_arrays(frame->data, frame->linesize, buffer, pix_fmt, width, height, 1); + if (ret < 0) { + al_log_error("lav_scaler", "Failed to allocate picture buffer (%s).", av_err2str(ret)); + av_free(frame); + return NULL; + } + frame->width = width; + frame->height = height; + frame->format = pix_fmt; + return frame; +} + +bool camu_lav_scaler_init(struct camu_lav_scaler *scale, struct camu_lav_scale_fmt *fmt) +{ + al_memset(scale, 0, sizeof(struct camu_lav_scaler)); + + /* + if (fmt->in_width == fmt->req_width && fmt->in_height == fmt->req_height && + fmt->in_format == fmt->req_format) { + fmt->scaler_needed = false; + return true; + } + */ + + fmt->scaler_needed = true; + + scale->scaler_context = sws_getContext(fmt->in_width, fmt->in_height, fmt->in_format, + fmt->req_width, fmt->req_height, fmt->req_format, + SWS_BITEXACT, NULL, NULL, NULL); + + if (!scale->scaler_context) { + al_log_error("lav_scaler", "Failed to create scaler context."); + return false; + } + + scale->frame = alloc_picture(fmt->req_format, fmt->req_width, fmt->req_height); + + if (!scale->frame) { + al_log_error("lav_scaler", "Failed to allocate frame."); + return false; + } + + scale->fmt = *fmt; + + return true; +} + +bool camu_lav_scaler_scale(struct camu_lav_scaler *scale, const u8 **in_slice, s32 *in_strides) +{ + s32 ret = sws_scale(scale->scaler_context, in_slice, in_strides, 0, + scale->fmt.in_height, scale->frame->data, scale->frame->linesize); + if (ret != scale->fmt.req_height) { + al_log_error("lav_scaler", "Failed to scale frame (%s).", av_err2str(ret)); + return false; + } + return true; +} + +void camu_lav_scaler_close(struct camu_lav_scaler *scale) +{ + sws_freeContext(scale->scaler_context); +} diff --git a/src/codec/libav/scaler.h b/src/codec/libav/scaler.h new file mode 100644 index 0000000..4d54085 --- /dev/null +++ b/src/codec/libav/scaler.h @@ -0,0 +1,24 @@ +#pragma once + +#include <libswscale/swscale.h> +#include <al/types.h> + +struct camu_lav_scale_fmt { + s32 in_width; + s32 in_height; + enum AVPixelFormat in_format; + s32 req_width; + s32 req_height; + enum AVPixelFormat req_format; + bool scaler_needed; +}; + +struct camu_lav_scaler { + struct SwsContext *scaler_context; + struct camu_lav_scale_fmt fmt; + AVFrame *frame; +}; + +bool camu_lav_scaler_init(struct camu_lav_scaler *scale, struct camu_lav_scale_fmt *fmt); +bool camu_lav_scaler_scale(struct camu_lav_scaler *scale, const u8 **in_slice, s32 *in_strides); +void camu_lav_scaler_close(struct camu_lav_scaler *scale); diff --git a/src/codec/meson.build b/src/codec/meson.build new file mode 100644 index 0000000..e1fd5f7 --- /dev/null +++ b/src/codec/meson.build @@ -0,0 +1,67 @@ +av_src = [ + 'libav/common.c', + 'libav/demuxer.c', + 'libav/decoder.c', + 'libav/resampler.c', + 'libav/scaler.c', + 'libav/encoder.c', + 'libav/packet_ext.c', + 'libav/avio.c' +] +av_inc = [] +av_deps = [] + +#libdir = meson.source_root() + '/build-mingw/subprojects/ffmpeg-6.0/dist/msys64/clang64lib' +#incdir = meson.source_root() + '/build-mingw/subprojects/ffmpeg-6.0/dist/msys64/clang64include' +#av_inc += [include_directories(incdir)] +#libavutil = compiler.find_library('avutil', dirs: libdir) +#libavformat = compiler.find_library('avformat', dirs: libdir) +#libavcodec = compiler.find_library('avcodec', dirs: libdir) +#libswresample = compiler.find_library('swresample', dirs: libdir) +#libswscale = compiler.find_library('swscale', dirs: libdir) + +libavutil = dependency('libavutil', required: false) +libavformat = dependency('libavformat', required: false) +libavcodec = dependency('libavcodec', required: false) +libswresample = dependency('libswresample', required: false) +libswscale = dependency('libswscale', required: false) +if not (libavutil.found() and libavformat.found() and libavcodec.found() and libswresample.found() and libswscale.found()) + av_proj = subproject('ffmpeg', required: false) + if av_proj.found() + libavutil = av_proj.get_variable('avutil') + libavformat = av_proj.get_variable('avformat') + libavcodec = av_proj.get_variable('avcodec') + libswresample = av_proj.get_variable('swresample') + libswscale = av_proj.get_variable('swscale') + zlib = compiler.find_library('z', required: false) + if zlib.found() + av_deps += [zlib] + endif + soxr = compiler.find_library('soxr', required: false) + if soxr.found() + av_deps += [soxr] + endif + if av_proj.get_variable('is_msvc') + av_deps += [compiler.find_library('Bcrypt', required: true)] + endif + av_inc += [av_proj.get_variable('inc')] + endif +endif + +if libavutil.found() and libavformat.found() and libavcodec.found() and libswresample.found() and libswscale.found() + av_deps += [libavutil, libavformat, libavcodec, libswresample, libswscale] + av = declare_dependency(sources: av_src, dependencies: av_deps, include_directories: av_inc) + add_project_arguments('-DHAVE_FFMPEG', language: ['c', 'cpp']) +endif + +stb_image = subproject('stb_image').get_variable('stb_image') +#stb_image_decoder = declare_dependency(sources: ['stb_image/impl.c'], +# dependencies: stb_image) +# +wuffs = subproject('wuffs').get_variable('wuffs') +wuffs_decoder = declare_dependency(sources: ['wuffs/impl.c'], + dependencies: wuffs) + +#spng = dependency('spng') +#spng_decoder = declare_dependency(sources: ['spng/impl.c'], +# dependencies: spng) diff --git a/src/codec/spng/decoder.h b/src/codec/spng/decoder.h new file mode 100644 index 0000000..fee30c6 --- /dev/null +++ b/src/codec/spng/decoder.h @@ -0,0 +1,11 @@ +#pragma once + +#include "../codec.h" + +struct camu_spng_decoder { + struct camu_decoder dec; + void (*callback)(void *, struct camu_frame *); + void (*userdata); +}; + +struct camu_decoder *camu_spng_decoder_create(void); diff --git a/src/codec/spng/demuxer.h b/src/codec/spng/demuxer.h new file mode 100644 index 0000000..1947bb7 --- /dev/null +++ b/src/codec/spng/demuxer.h @@ -0,0 +1,12 @@ +#pragma once + +#include "../codec.h" + +struct camu_spng_demuxer { + struct camu_demuxer demux; + struct cch_handle *handle; + struct aki_buffer buffer; + bool eof; +}; + +struct camu_demuxer *camu_spng_demuxer_create(void); diff --git a/src/codec/spng/impl.c b/src/codec/spng/impl.c new file mode 100644 index 0000000..d090bdc --- /dev/null +++ b/src/codec/spng/impl.c @@ -0,0 +1,166 @@ +#include <al/lib.h> +#include <aki/common.h> +#include <spng.h> + +#include "../../cache/entry.h" + +#include "../common.h" + +#include "demuxer.h" +#include "decoder.h" + +static bool spng_demuxer_init(struct camu_demuxer *demux, struct cch_handle *handle) +{ + struct camu_spng_demuxer *spng = (struct camu_spng_demuxer *)demux; + + spng->handle = handle; + aki_buffer_init(&spng->buffer); + spng->eof = false; + + cch_handle_seek(spng->handle, 0, SEEK_SET); + off_t size = cch_entry_get_size(spng->handle->entry); + aki_buffer_ensure_space(&spng->buffer, size); + cch_handle_read(spng->handle, aki_buffer_get_ptr(&spng->buffer, 0), size); + spng->buffer.size = size; + + spng_ctx *ctx = spng_ctx_new(SPNG_CTX_IGNORE_ADLER32); + spng_set_png_buffer(ctx, aki_buffer_get_ptr(&spng->buffer, 0), spng->buffer.size); + spng_set_option(ctx, SPNG_CHUNK_COUNT_LIMIT, 6250); + spng_set_crc_action(ctx, SPNG_CRC_DISCARD, SPNG_CRC_DISCARD); + + struct spng_ihdr ihdr; + spng_get_ihdr(ctx, &ihdr); + + struct camu_stream stream; + stream.type = CAMU_NORMAL; + stream.video.width = ihdr.width; + stream.video.height = ihdr.height; + + al_array_push(spng->demux.streams, stream); + + spng_ctx_free(ctx); + + return true; +} + +static s32 spng_demuxer_get_packet(struct camu_demuxer *demux, struct camu_packet *packet) +{ + struct camu_spng_demuxer *spng = (struct camu_spng_demuxer *)demux; + if (spng->eof) return CAMU_ERR_EOF; + packet->type = CAMU_NORMAL; + packet->buffer = &spng->buffer; + spng->eof = true; + return CAMU_OK; +} + +static s64 spng_demuxer_get_duration(struct camu_demuxer *demux) +{ + (void)demux; + return 0; +} + +static bool spng_demuxer_seek(struct camu_demuxer *demux, s64 pos) +{ + (void)demux; + (void)pos; + return true; +} + +static void spng_demuxer_free(struct camu_demuxer **demux) +{ + struct camu_spng_demuxer *spng = (struct camu_spng_demuxer *)*demux; + al_free(spng); + *demux = NULL; +} + +static bool spng_decoder_init(struct camu_decoder *dec, struct camu_renderer *renderer, struct camu_stream *stream) +{ + struct camu_spng_decoder *spng = (struct camu_spng_decoder *)dec; + (void)spng; + (void)stream; + (void)renderer; + return true; +} + +static void spng_decoder_set_callback(struct camu_decoder *dec, void (*callback)(void *, struct camu_frame *), void *userdata) +{ + struct camu_spng_decoder *spng = (struct camu_spng_decoder *)dec; + spng->callback = callback; + spng->userdata = userdata; +} + +static s32 spng_decoder_push(struct camu_decoder *dec, struct camu_packet *packet) +{ + if (!packet) return CAMU_OK; + + struct camu_spng_decoder *spng = (struct camu_spng_decoder *)dec; + + spng_ctx *ctx = spng_ctx_new(SPNG_CTX_IGNORE_ADLER32); + spng_set_png_buffer(ctx, aki_buffer_get_ptr(packet->buffer, 0), packet->buffer->size); + spng_set_option(ctx, SPNG_CHUNK_COUNT_LIMIT, 6250); + spng_set_crc_action(ctx, SPNG_CRC_DISCARD, SPNG_CRC_DISCARD); + + size_t out_size; + spng_decoded_image_size(ctx, SPNG_FMT_RGBA8, &out_size); + + struct camu_frame *frame = al_alloc_object(struct camu_frame); + frame->type = CAMU_NORMAL; + frame->width = spng->dec.stream->video.width; + frame->height = spng->dec.stream->video.height; + frame->fmt = CAMU_PIXEL_FMT_RGBA; + frame->pts = 0.0; + frame->flags = 0; + + frame->data = camu_page_alloc(out_size); + spng_decode_chunks(ctx); + spng_decode_image(ctx, frame->data, out_size, SPNG_FMT_RGBA8, 0); + + spng_ctx_free(ctx); + + spng->callback(spng->userdata, frame); + + return CAMU_OK; +} + +static void spng_decoder_flush(struct camu_decoder *dec) +{ + (void)dec; +} + +static s32 spng_decoder_process(struct camu_decoder *dec) +{ + (void)dec; + return CAMU_ERR_EOF; +} + +static void spng_decoder_free(struct camu_decoder **dec) +{ + struct camu_spng_decoder *spng = (struct camu_spng_decoder *)*dec; + al_free(spng); + *dec = NULL; +} + +struct camu_demuxer *camu_spng_demuxer_create(void) +{ + struct camu_spng_demuxer *spng = al_alloc_object(struct camu_spng_demuxer); + spng->demux.type = CAMU_NORMAL; + al_array_init(spng->demux.streams); + spng->demux.init = spng_demuxer_init; + spng->demux.get_packet = spng_demuxer_get_packet; + spng->demux.get_duration = spng_demuxer_get_duration; + spng->demux.seek = spng_demuxer_seek; + spng->demux.free = spng_demuxer_free; + return (struct camu_demuxer *)spng; +} + +struct camu_decoder *camu_spng_decoder_create(void) +{ + struct camu_spng_decoder *spng = al_alloc_object(struct camu_spng_decoder); + spng->dec.init = spng_decoder_init; + spng->dec.set_callback = spng_decoder_set_callback; + spng->dec.push = spng_decoder_push; + spng->dec.process = spng_decoder_process; + spng->dec.flush = spng_decoder_flush; + spng->dec.free = spng_decoder_free; + return (struct camu_decoder *)spng; +} diff --git a/src/codec/stb_image/decoder.h b/src/codec/stb_image/decoder.h new file mode 100644 index 0000000..7083a09 --- /dev/null +++ b/src/codec/stb_image/decoder.h @@ -0,0 +1,11 @@ +#pragma once + +#include "../codec.h" + +struct camu_stbi_decoder { + struct camu_decoder dec; + void (*callback)(void *, struct camu_frame *); + void (*userdata); +}; + +struct camu_decoder *camu_stbi_decoder_create(void); diff --git a/src/codec/stb_image/demuxer.h b/src/codec/stb_image/demuxer.h new file mode 100644 index 0000000..52f9256 --- /dev/null +++ b/src/codec/stb_image/demuxer.h @@ -0,0 +1,12 @@ +#pragma once + +#include "../codec.h" + +struct camu_stbi_demuxer { + struct camu_demuxer demux; + struct cch_handle *handle; + struct aki_buffer buffer; + bool eof; +}; + +struct camu_demuxer *camu_stbi_demuxer_create(void); diff --git a/src/codec/stb_image/impl.c b/src/codec/stb_image/impl.c new file mode 100644 index 0000000..953cca8 --- /dev/null +++ b/src/codec/stb_image/impl.c @@ -0,0 +1,155 @@ +#include <al/lib.h> +#include <aki/common.h> +#include "../common.h" +#define STB_IMAGE_IMPLEMENTATION +#define STBI_NO_STDIO +#define STBI_NO_FAILURE_STRINGS +#define STB_IMAGE_STATIC +#define STBI_MALLOC camu_page_alloc +#define STBI_FREE al_free +#define STBI_REALLOC al_realloc +#include <stb_image.h> + +#include "../../cache/entry.h" + +#include "demuxer.h" +#include "decoder.h" + +static bool stbi_demuxer_init(struct camu_demuxer *demux, struct cch_handle *handle) +{ + struct camu_stbi_demuxer *stb = (struct camu_stbi_demuxer *)demux; + + stb->handle = handle; + aki_buffer_init(&stb->buffer); + stb->eof = false; + + cch_handle_seek(stb->handle, 0, SEEK_SET); + off_t size = cch_entry_get_size(stb->handle->entry); + aki_buffer_ensure_space(&stb->buffer, size); + cch_handle_read(stb->handle, aki_buffer_get_ptr(&stb->buffer, 0), size); + stb->buffer.size = size; + s32 w, h, channels; + stbi_info_from_memory(aki_buffer_get_ptr(&stb->buffer, 0), stb->buffer.size, &w, &h, &channels); + + struct camu_stream stream = { 0 }; + stream.type = CAMU_NORMAL; + stream.video.width = w; + stream.video.height = h; + + al_array_push(stb->demux.streams, stream); + + return true; +} + +static s32 stbi_demuxer_get_packet(struct camu_demuxer *demux, struct camu_packet *packet) +{ + struct camu_stbi_demuxer *stb = (struct camu_stbi_demuxer *)demux; + if (stb->eof) return CAMU_ERR_EOF; + packet->type = CAMU_NORMAL; + packet->buffer = &stb->buffer; + stb->eof = true; + return CAMU_OK; +} + +static s64 stbi_demuxer_get_duration(struct camu_demuxer *demux) +{ + (void)demux; + return 0; +} + +static bool stbi_demuxer_seek(struct camu_demuxer *demux, s64 pos) +{ + (void)demux; + (void)pos; + return true; +} + +static void stbi_demuxer_free(struct camu_demuxer **demux) +{ + struct camu_stbi_demuxer *stb = (struct camu_stbi_demuxer *)*demux; + al_free(stb); + *demux = NULL; +} + +static bool stbi_decoder_init(struct camu_decoder *dec, struct camu_renderer *renderer, struct camu_stream *stream) +{ + struct camu_stbi_decoder *stb = (struct camu_stbi_decoder *)dec; + (void)stb; + (void)stream; + (void)renderer; + return true; +} + +static void stbi_decoder_set_callback(struct camu_decoder *dec, void (*callback)(void *, struct camu_frame *), void *userdata) +{ + struct camu_stbi_decoder *stb = (struct camu_stbi_decoder *)dec; + stb->callback = callback; + stb->userdata = userdata; +} + +static s32 stbi_decoder_push(struct camu_decoder *dec, struct camu_packet *packet) +{ + if (!packet) return CAMU_OK; + + struct camu_stbi_decoder *stb = (struct camu_stbi_decoder *)dec; + + s32 w, h, channels; + u8 *pixels = stbi_load_from_memory(aki_buffer_get_ptr(packet->buffer, 0), + packet->buffer->size, &w, &h, &channels, 4); + + struct camu_frame *frame = al_alloc_object(struct camu_frame); + frame->type = CAMU_NORMAL; + frame->data = pixels; + frame->width = stb->dec.stream->video.width; + frame->height = stb->dec.stream->video.height; + frame->fmt = CAMU_PIXEL_FMT_RGBA; + frame->pts = 0.0; + frame->flags = 0; + + stb->callback(stb->userdata, frame); + + return CAMU_OK; +} + +static void stbi_decoder_flush(struct camu_decoder *dec) +{ + (void)dec; +} + +static s32 stbi_decoder_process(struct camu_decoder *dec) +{ + (void)dec; + return CAMU_ERR_EOF; +} + +static void stbi_decoder_free(struct camu_decoder **dec) +{ + struct camu_stbi_decoder *stb = (struct camu_stbi_decoder *)*dec; + al_free(stb); + *dec = NULL; +} + +struct camu_demuxer *camu_stbi_demuxer_create(void) +{ + struct camu_stbi_demuxer *stb = al_alloc_object(struct camu_stbi_demuxer); + stb->demux.type = CAMU_NORMAL; + al_array_init(stb->demux.streams); + stb->demux.init = stbi_demuxer_init; + stb->demux.get_packet = stbi_demuxer_get_packet; + stb->demux.get_duration = stbi_demuxer_get_duration; + stb->demux.seek = stbi_demuxer_seek; + stb->demux.free = stbi_demuxer_free; + return (struct camu_demuxer *)stb; +} + +struct camu_decoder *camu_stbi_decoder_create(void) +{ + struct camu_stbi_decoder *stb = al_alloc_object(struct camu_stbi_decoder); + stb->dec.init = stbi_decoder_init; + stb->dec.set_callback = stbi_decoder_set_callback; + stb->dec.push = stbi_decoder_push; + stb->dec.process = stbi_decoder_process; + stb->dec.flush = stbi_decoder_flush; + stb->dec.free = stbi_decoder_free; + return (struct camu_decoder *)stb; +} diff --git a/src/codec/wuffs/decoder.h b/src/codec/wuffs/decoder.h new file mode 100644 index 0000000..71f0290 --- /dev/null +++ b/src/codec/wuffs/decoder.h @@ -0,0 +1,13 @@ +#pragma once + +#include "../common.h" +#include "../codec.h" + +struct camu_wuffs_decoder { + struct camu_decoder dec; + u8 file_type; + void (*callback)(void *, struct camu_frame *); + void (*userdata); +}; + +struct camu_decoder *camu_wuffs_decoder_create(void); diff --git a/src/codec/wuffs/demuxer.h b/src/codec/wuffs/demuxer.h new file mode 100644 index 0000000..6e9e391 --- /dev/null +++ b/src/codec/wuffs/demuxer.h @@ -0,0 +1,12 @@ +#pragma once + +#include "../codec.h" + +struct camu_wuffs_demuxer { + struct camu_demuxer demux; + struct cch_handle *handle; + struct aki_buffer buffer; + bool eof; +}; + +struct camu_demuxer *camu_wuffs_demuxer_create(void); diff --git a/src/codec/wuffs/impl.c b/src/codec/wuffs/impl.c new file mode 100644 index 0000000..a1e58e4 --- /dev/null +++ b/src/codec/wuffs/impl.c @@ -0,0 +1,213 @@ +#define WUFFS_IMPLEMENTATION +#define WUFFS_CONFIG__STATIC_FUNCTIONS +#define WUFFS_CONFIG__MODULES +#define WUFFS_CONFIG__MODULE__BASE +#define WUFFS_CONFIG__MODULE__ADLER32 +#define WUFFS_CONFIG__MODULE__DEFLATE +#define WUFFS_CONFIG__MODULE__ZLIB +#define WUFFS_CONFIG__MODULE__CRC32 +#define WUFFS_CONFIG__MODULE__PNG +#define WUFFS_CONFIG__MODULE__JPEG +#define WUFFS_CONFIG__MODULE__IMAGE +#include <wuffs-unsupported-snapshot.c> + +#include <aki/common.h> + +#include "../../cache/entry.h" + +#include "demuxer.h" +#include "decoder.h" + +static bool wuffs_demuxer_init(struct camu_demuxer *demux, struct cch_handle *handle) +{ + struct camu_wuffs_demuxer *wfs = (struct camu_wuffs_demuxer *)demux; + + wfs->handle = handle; + aki_buffer_init(&wfs->buffer); + wfs->eof = false; + + cch_handle_seek(wfs->handle, 0, SEEK_SET); + off_t size = cch_entry_get_size(wfs->handle->entry); + aki_buffer_ensure_space(&wfs->buffer, size); + cch_handle_read(wfs->handle, aki_buffer_get_ptr(&wfs->buffer, 0), size); + wfs->buffer.size = size; + + wuffs_png__decoder wf_dec = {0}; + wuffs_base__status status = wuffs_png__decoder__initialize(&wf_dec, sizeof(wf_dec), WUFFS_VERSION, 0); + (void)status; + //if (!wuffs_base__status__is_ok(&status)) goto err; + + wuffs_png__decoder__set_quirk(&wf_dec, WUFFS_BASE__QUIRK_IGNORE_CHECKSUM, true); + + //wuffs_png__decoder__set_report_metadata(&wf_dec, WUFFS_BASE__FOURCC__ICCP, true); + + wuffs_base__io_buffer src = {0}; + src.data.ptr = aki_buffer_get_ptr(&wfs->buffer, 0); + src.data.len = wfs->buffer.size; + src.meta.wi = wfs->buffer.size; + src.meta.closed = true; + + wuffs_base__image_config ic = {0}; + status = wuffs_png__decoder__decode_image_config(&wf_dec, &ic, &src); + //if (!wuffs_base__status__is_ok(&status)) goto err; + //if (!wuffs_base__image_config__is_valid(&ic)) goto err; + + struct camu_stream stream; + stream.type = CAMU_NORMAL; + stream.video.width = wuffs_base__pixel_config__width(&ic.pixcfg); + stream.video.height = wuffs_base__pixel_config__height(&ic.pixcfg); + + al_array_push(wfs->demux.streams, stream); + + return true; +} + +static s32 wuffs_demuxer_get_packet(struct camu_demuxer *demux, struct camu_packet *packet) +{ + struct camu_wuffs_demuxer *wfs = (struct camu_wuffs_demuxer *)demux; + if (wfs->eof) return CAMU_ERR_EOF; + packet->type = CAMU_NORMAL; + packet->buffer = &wfs->buffer; + wfs->eof = true; + return CAMU_OK; +} + +static s64 wuffs_demuxer_get_duration(struct camu_demuxer *demux) +{ + (void)demux; + return 0; +} + +static bool wuffs_demuxer_seek(struct camu_demuxer *demux, s64 pos) +{ + (void)demux; + (void)pos; + return true; +} + +static void wuffs_demuxer_free(struct camu_demuxer **demux) +{ + struct camu_wuffs_demuxer *wfs = (struct camu_wuffs_demuxer *)*demux; + al_free(wfs); + *demux = NULL; +} + +static bool wuffs_decoder_init(struct camu_decoder *dec, struct camu_renderer *renderer, struct camu_stream *stream) +{ + struct camu_wuffs_decoder *wfs = (struct camu_wuffs_decoder *)dec; + (void)wfs; + (void)stream; + (void)renderer; + return true; +} + +static void wuffs_decoder_set_callback(struct camu_decoder *dec, void (*callback)(void *, struct camu_frame *), void *userdata) +{ + struct camu_wuffs_decoder *wfs = (struct camu_wuffs_decoder *)dec; + wfs->callback = callback; + wfs->userdata = userdata; +} + +static s32 wuffs_decoder_push(struct camu_decoder *dec, struct camu_packet *packet) +{ + if (!packet) return CAMU_OK; + + struct camu_wuffs_decoder *wfs = (struct camu_wuffs_decoder *)dec; + + wuffs_png__decoder wf_dec = {0}; + wuffs_base__status status = wuffs_png__decoder__initialize(&wf_dec, sizeof(wf_dec), WUFFS_VERSION, 0); + //if (!wuffs_base__status__is_ok(&status)) goto err; + + wuffs_png__decoder__set_quirk(&wf_dec, WUFFS_BASE__QUIRK_IGNORE_CHECKSUM, true); + + //wuffs_png__decoder__set_report_metadata(&wf_dec, WUFFS_BASE__FOURCC__ICCP, true); + + wuffs_base__io_buffer src = {0}; + src.data.ptr = aki_buffer_get_ptr(packet->buffer, 0); + src.data.len = packet->buffer->size; + src.meta.wi = packet->buffer->size; + src.meta.closed = true; + + //wf_dec.private_impl.p_decode_iccp[0] = 1; + + wuffs_base__image_config ic = {0}; + status = wuffs_png__decoder__decode_image_config(&wf_dec, &ic, &src); + //if (!wuffs_base__status__is_ok(&status)) goto err; + //if (!wuffs_base__image_config__is_valid(&ic)) goto err; + + struct camu_frame *frame = al_alloc_object(struct camu_frame); + frame->type = CAMU_NORMAL; + frame->width = wfs->dec.stream->video.width; + frame->height = wfs->dec.stream->video.height; + frame->fmt = CAMU_PIXEL_FMT_RGBA; + + u32 pixsub = WUFFS_BASE__PIXEL_SUBSAMPLING__NONE; + u32 pixfmt = WUFFS_BASE__PIXEL_FORMAT__RGBA_NONPREMUL; + wuffs_base__pixel_config__set(&ic.pixcfg, pixfmt, pixsub, frame->width, frame->height); + + wuffs_base__pixel_buffer pb = {0}; + size_t dst_size = frame->width * frame->height * 4; + size_t wrk_size = wuffs_png__decoder__workbuf_len(&wf_dec).max_incl; + /* + void *device_ptr = dec->renderer->get_device_ptr(dec->renderer, dst_size + wrk_size); + wuffs_base__slice_u8 wrk = wuffs_base__make_slice_u8(device_ptr + dst_size, wrk_size); + status = wuffs_base__pixel_buffer__set_from_slice(&pb, &ic.pixcfg, + wuffs_base__make_slice_u8(device_ptr, dst_size)); + */ + frame->data = camu_page_alloc(dst_size + wrk_size); + status = wuffs_base__pixel_buffer__set_from_slice(&pb, &ic.pixcfg, + wuffs_base__make_slice_u8(frame->data, dst_size)); + //if (!wuffs_base__status__is_ok(&status)) goto err; + wuffs_base__slice_u8 wrk = wuffs_base__make_slice_u8(frame->data + dst_size, wrk_size); + + status = wuffs_png__decoder__decode_frame(&wf_dec, &pb, &src, 0, wrk, NULL); + (void)status; + //if (!wuffs_base__status__is_ok(&status)) goto err; + + wfs->callback(wfs->userdata, frame); + + return CAMU_OK; +} + +static void wuffs_decoder_flush(struct camu_decoder *dec) +{ + (void)dec; +} + +static s32 wuffs_decoder_process(struct camu_decoder *dec) +{ + (void)dec; + return CAMU_ERR_EOF; +} + +static void wuffs_decoder_free(struct camu_decoder **dec) +{ + struct camu_wuffs_decoder *wfs = (struct camu_wuffs_decoder *)*dec; + al_free(wfs); + *dec = NULL; +} + +struct camu_demuxer *camu_wuffs_demuxer_create(void) +{ + struct camu_wuffs_demuxer *wfs = al_alloc_object(struct camu_wuffs_demuxer); + wfs->demux.type = CAMU_NORMAL; + al_array_init(wfs->demux.streams); + wfs->demux.init = wuffs_demuxer_init; + wfs->demux.get_packet = wuffs_demuxer_get_packet; + wfs->demux.get_duration = wuffs_demuxer_get_duration; + wfs->demux.seek = wuffs_demuxer_seek; + wfs->demux.free = wuffs_demuxer_free; + return (struct camu_demuxer *)wfs; +} + +struct camu_decoder *camu_wuffs_decoder_create(void) +{ + struct camu_wuffs_decoder *wfs = al_alloc_object(struct camu_wuffs_decoder); + wfs->dec.init = wuffs_decoder_init; + wfs->dec.set_callback = wuffs_decoder_set_callback; + wfs->dec.push = wuffs_decoder_push; + wfs->dec.process = wuffs_decoder_process; + wfs->dec.flush = wuffs_decoder_flush; + wfs->dec.free = wuffs_decoder_free; + return (struct camu_decoder *)wfs; +} diff --git a/src/fruits/cap/cap.c b/src/fruits/cap/cap.c new file mode 100644 index 0000000..2f60cb4 --- /dev/null +++ b/src/fruits/cap/cap.c @@ -0,0 +1,219 @@ +#include "cap.h" + +#if CAP_USE_PYTHON +#include "../../shoki/src/search.h" +#endif +#include "../../cache/handlers/file.h" +#ifdef AKIYO_HAS_CURL +#include "../../cache/handlers/http.h" +#endif + +void cap_init(struct cap_runner *cap, bool (*callback)(void *, u8, str *, str *, void *), void *userdata) +{ + cap->callback = callback; + cap->userdata = userdata; + al_array_init(cap->lists); +} + +static struct cap_list *list_from_name(struct cap_runner *cap, str *name) +{ + struct cap_list *list; + al_array_foreach_ptr(cap->lists, i, list) { + if (al_str_eq(&list->name, name)) { + return list; + } + } + return NULL; +} + +static struct cch_entry *entry_for_unique_id(str *unique_id) +{ + struct cch_entry *entry = NULL; +#ifdef AKIYO_HAS_CURL + bool is_search = al_str_at(unique_id, 0) == ';'; + if (is_search || al_str_cmp(unique_id, al_str_c("https://"), 0, 8) == 0 || + al_str_cmp(unique_id, al_str_c("http://"), 0, 7) == 0) { +#if CAP_USE_PYTHON + struct sho_search s; + sho_search_init(&s); + if (al_str_cmp(unique_id, al_str_c("https://twitter.com"), 0, 19) == 0 || + al_str_cmp(unique_id, al_str_c("https://x.com"), 0, 13) == 0) { + str query; + al_str_from(&query, "tweet:"); + al_str_cat(&query, unique_id); + if (!sho_search_more_results(&s, al_str_c("twitter"), &query)) { + } + al_str_free(&query); + } else { + str query; + if (is_search) { + //al_str_from(&query, "\""); + al_str_from(&query, ""); + al_str_cat(&query, al_str_substr(unique_id, 1, unique_id->len)); + //al_str_cat(&query, al_str_c("\"")); + } else { + al_str_from(&query, "link:"); + al_str_cat(&query, unique_id); + } + if (!sho_search_more_results(&s, al_str_c("youtube"), &query)) { + } + } + entry = NULL; + struct sho_result_page *page; + al_array_foreach_ptr(s.result.pages, k, page) { + struct sho_post *post; + al_array_foreach_ptr(page->posts, i, post) { + struct sho_post_media *media; + al_array_foreach_ptr(post->media, j, media) { + if (media->url.len > 0) { + entry = cch_handler_http_create(&media->url); + break; + } + } + if (entry) break; + } + } + sho_search_free(&s); +#else + if (!is_search) entry = cch_handler_http_create(unique_id); +#endif + entry->handler->maybe_spawn_worker(entry->handler, 0); + } else // { +#endif + entry = cch_handler_file_create(unique_id); + // } + return entry; +} + +static bool buffer_entry(struct cap_runner *cap, struct cap_list *list, struct cap_list_entry *entry) +{ + if (entry->buffer_requested) return true; + struct cch_entry *centry = entry_for_unique_id(&entry->unique_id); + if (!centry) return false; + if (!cap->callback(cap->userdata, CAP_BUFFER, &list->name, &entry->unique_id, centry)) { + entry->errored = true; + return false; + } + entry->buffer_requested = true; + return true; +} + +static void maybe_cleanup_entries(struct cap_runner *cap, struct cap_list *list) +{ + struct cap_list_entry *entry; + al_array_foreach_ptr(list->entries, i, entry) { + if (entry->buffer_requested && abs((s32)i - list->current) >= 3) { + cap->callback(cap->userdata, CAP_UNLOAD, &list->name, &entry->unique_id, NULL); + entry->buffer_requested = false; + } + } +} + +static bool list_skip_internal(struct cap_runner *cap, struct cap_list *list, s32 n); + +static void list_pump_internal(struct cap_runner *cap, struct cap_list *list) +{ + s32 size = (s32)list->entries.size; + if (size <= list->current) return; + if (list->set != list->current) { + struct cap_list_entry *entry = &al_array_at(list->entries, list->current); + if (!buffer_entry(cap, list, entry)) { + list_skip_internal(cap, list, 1); + return; + } + list->set = list->current; + if (list->swap && entry->buffer_requested) { + cap->callback(cap->userdata, CAP_SWAP, &list->name, &entry->unique_id, NULL); + } else { + cap->callback(cap->userdata, CAP_SET, &list->name, &entry->unique_id, NULL); + } + } + if (list->current + 1 < size) { + buffer_entry(cap, list, &al_array_at(list->entries, list->current + 1)); + } + if (list->current - 1 >= 0) { + buffer_entry(cap, list, &al_array_at(list->entries, list->current - 1)); + } + maybe_cleanup_entries(cap, list); +} + +bool list_skip_internal(struct cap_runner *cap, struct cap_list *list, s32 n) +{ + s32 size = (s32)list->entries.size; + if (list->current + n < 0 || list->current + n >= size) { + return false; + } + list->current += n; + list_pump_internal(cap, list); + return true; +} + +void cap_make_list(struct cap_runner *cap, str *name) +{ + struct cap_list list; + al_str_clone(&list.name, name); + list.current = 0; + list.set = -1; + list.idle = false; + list.swap = false; + al_array_init(list.entries); + al_array_push(cap->lists, list); +} + +void cap_list_add(struct cap_runner *cap, str *name, str *unique_id) +{ + struct cap_list *list = list_from_name(cap, name); + if (!list) return; + struct cap_list_entry entry; + al_str_clone(&entry.unique_id, unique_id); + entry.buffer_requested = false; + entry.errored = false; + al_array_push(list->entries, entry); + if (list->idle && list_skip_internal(cap, list, 1)) { + list->idle = false; + return; + } + list_pump_internal(cap, list); +} + +bool cap_list_skip(struct cap_runner *cap, str *name, s32 n) +{ + struct cap_list *list = list_from_name(cap, name); + if (!list) return false; + return list_skip_internal(cap, list, n); +} + +void cap_list_pump(struct cap_runner *cap, str *name) +{ + struct cap_list *list = list_from_name(cap, name); + if (list) list_pump_internal(cap, list); +} + +bool cap_list_set_completed(struct cap_runner *cap, str *name) +{ + struct cap_list *list = list_from_name(cap, name); + if (!list) return false; + list->swap = true; + list->idle = !list_skip_internal(cap, list, 1); + list->swap = false; + return !list->idle; +} + +static void list_close_internal(struct cap_list *list) +{ + struct cap_list_entry *entry; + al_array_foreach_ptr(list->entries, i, entry) { + al_str_free(&entry->unique_id); + } + al_array_free(list->entries); + al_str_free(&list->name); +} + +void cap_close(struct cap_runner *cap) +{ + struct cap_list *list; + al_array_foreach_ptr(cap->lists, i, list) { + list_close_internal(list); + } + al_array_free(cap->lists); +} diff --git a/src/fruits/cap/cap.h b/src/fruits/cap/cap.h new file mode 100644 index 0000000..d522a82 --- /dev/null +++ b/src/fruits/cap/cap.h @@ -0,0 +1,42 @@ +#pragma once + +#define CAP_USE_PYTHON 1 + +#include <al/array.h> +#include <al/str.h> + +struct cap_list_entry { + str unique_id; + bool buffer_requested; + bool errored; +}; + +struct cap_list { + str name; + s32 current; + s32 set; + bool idle; + bool swap; + array(struct cap_list_entry) entries; +}; + +enum { + CAP_BUFFER = 0, + CAP_SET, + CAP_SWAP, + CAP_UNLOAD +}; + +struct cap_runner { + array(struct cap_list) lists; + bool (*callback)(void *, u8, str *, str *, void *); + void *userdata; +}; + +void cap_init(struct cap_runner *cap, bool (*callback)(void *, u8, str *, str *, void *), void *userdata); +void cap_make_list(struct cap_runner *cap, str *name); +void cap_list_add(struct cap_runner *cap, str *name, str *unique_id); +bool cap_list_skip(struct cap_runner *cap, str *name, s32 n); +void cap_list_pump(struct cap_runner *cap, str *name); +bool cap_list_set_completed(struct cap_runner *cap, str *name); +void cap_close(struct cap_runner *cap); diff --git a/src/fruits/cap/meson.build b/src/fruits/cap/meson.build new file mode 100644 index 0000000..b5930ac --- /dev/null +++ b/src/fruits/cap/meson.build @@ -0,0 +1,4 @@ +cap_src = ['cap.c'] +cap_deps = [common_deps, shoki] +cap = declare_dependency(sources: cap_src, + dependencies: cap_deps) diff --git a/src/fruits/cmv/cmv2.c b/src/fruits/cmv/cmv2.c new file mode 100644 index 0000000..a7b688c --- /dev/null +++ b/src/fruits/cmv/cmv2.c @@ -0,0 +1,331 @@ +#define CMV_USE_TUI 1 + +#include <al/log.h> +#include <aki/line_processor.h> + +#include "../../codec/libav/common.h" + +#include "../../mixer/audio_miniaudio.h" + +#include "../../screen/screen.h" +#include "../../render/renderer_libplacebo.h" +//#include "../../render/renderer_tiger.h" + +#include "../../libsink/sink.h" + +#include "../../shoki/src/search.h" + +#include "../cap/cap.h" + +#if CMV_USE_TUI +#include "tui.h" +#endif + +struct cmv { + s32 quit; + struct camu_screen scr; + struct camu_renderer *renderer; + struct camu_mixer mixer; + struct aki_event_loop loop; + struct camu_sink sink; + struct aki_socket socket; + struct aki_line_processor pro; + struct cap_runner cap; +#if CMV_USE_TUI + struct cmv_tui tui; + struct aki_timer timer; +#endif +}; + +static struct cmv c; +static str *default_list = al_str_c("default"); + +static u8 sink_callback(void *userdata, u8 op, u8 type, void *opaque) +{ + struct cmv *c = (struct cmv *)userdata; + switch (op) { + case CAMU_SINK_ADD_BUFFER: + switch (type) { + case CAMU_SINK_AUDIO: { + struct camu_audio_buffer *buf = (struct camu_audio_buffer *)opaque; + camu_mixer_add_buffer(&c->mixer, buf); + al_log_debug("cmv", "Audio buffer added."); + break; + } + case CAMU_SINK_VIDEO: { + struct camu_video_buffer *buf = (struct camu_video_buffer *)opaque; + camu_screen_add_buffer(&c->scr, buf); + al_log_debug("cmv", "Video buffer added."); + break; + } + } + break; + case CAMU_SINK_REMOVE_BUFFER: + switch (type) { + case CAMU_SINK_AUDIO: { + struct camu_audio_buffer *buf = (struct camu_audio_buffer *)opaque; + camu_mixer_remove_buffer(&c->mixer, buf); + al_log_debug("cmv", "Audio buffer removed."); + break; + } + case CAMU_SINK_VIDEO: { + struct camu_video_buffer *buf = (struct camu_video_buffer *)opaque; + camu_screen_remove_buffer(&c->scr, buf); + al_log_debug("cmv", "Video buffer removed."); + break; + } + } + break; + case CAMU_SINK_SWAP_BUFFER: + switch (type) { + case CAMU_SINK_AUDIO: { + struct camu_audio_buffer *buf = (struct camu_audio_buffer *)opaque; + camu_mixer_remove_buffer(&c->mixer, buf); + if (cap_list_set_completed(&c->cap, default_list)) { + al_log_debug("cmv", "Audio buffers swapped (gapless)."); + return CAMU_SINK_BUFFERS_SWAPPED; + } else { + al_log_debug("cmv", "Audio buffer removed."); + } + break; + } + } + break; + case CAMU_SINK_START: + switch (type) { + case CAMU_SINK_AUDIO: { + camu_mixer_resume(&c->mixer); + al_log_debug("cmv", "Audio started."); + break; + } + case CAMU_SINK_VIDEO: { + camu_screen_set_state(&c->scr, CAMU_SCREEN_PLAYING); + camu_screen_wake(&c->scr); + al_log_debug("cmv", "Video started."); + break; + } + } + break; + case CAMU_SINK_STOP: + switch (type) { + case CAMU_SINK_AUDIO: { + camu_mixer_pause(&c->mixer); + al_log_debug("cmv", "Audio stopped."); + break; + } + case CAMU_SINK_VIDEO: { + camu_screen_set_state(&c->scr, CAMU_SCREEN_PAUSED); + al_log_debug("cmv", "Video stopped."); + break; + } + } + break; + case CAMU_SINK_EXIT: + aki_event_loop_break(&c->loop); + camu_sink_close(&c->sink); + break; + } + return CAMU_SINK_OK; +} + +static void screen_callback(void *userdata, u8 op, f64 float0) +{ + struct cmv *c = (struct cmv *)userdata; + switch (op) { + case CAMU_SCREEN_CLOSE: + c->quit = 1; + break; + case CAMU_SCREEN_NEXT: + cap_list_skip(&c->cap, default_list, 1); + break; + case CAMU_SCREEN_PREVIOUS: + cap_list_skip(&c->cap, default_list, -1); + break; + case CAMU_SCREEN_TOGGLE_PAUSE: + camu_sink_toggle_pause(&c->sink); + break; + case CAMU_SCREEN_SEEK: + camu_sink_seek(&c->sink, float0); + break; + default: + break; + } +} + +static aki_thread_result AKI_THREADCALL event_loop_thread(void *userdata) +{ + struct cmv *c = (struct cmv *)userdata; + aki_event_loop_run(&c->loop); + aki_event_loop_destroy(&c->loop); + return 0; +} + +static bool cap_callback(void *userdata, u8 op, str *name, str *unique_id, void *opaque) +{ + struct cmv *c = (struct cmv *)userdata; + (void)name; + switch (op) { + case CAP_BUFFER: + return camu_sink_local_buffer(&c->sink, unique_id, (struct cch_entry *)opaque); + case CAP_SWAP: + camu_sink_local_swap(&c->sink, unique_id); + break; + case CAP_SET: + camu_sink_local_set(&c->sink, unique_id); + break; + case CAP_UNLOAD: + camu_sink_local_unload(&c->sink, unique_id); + break; + } + return true; +} + +#ifndef _WIN32 +static u8 line_callback(void *userdata, str *line) +{ + struct cmv *c = (struct cmv *)userdata; + if (al_str_eq(line, al_str_c(";NEXT"))) { + cap_list_skip(&c->cap, default_list, 1); + } else if (al_str_eq(line, al_str_c(";PREV"))) { + cap_list_skip(&c->cap, default_list, -1); + } else { + cap_list_add(&c->cap, default_list, line); + } + return AKI_LINE_PROCESSOR_CONTINUE; +} +#endif + +#if CMV_USE_TUI +static void timer_callback(void *userdata, struct aki_timer *timer) +{ + struct cmv *c = (struct cmv *)userdata; + struct camu_sink_entry *entry = camu_sink_get_current(&c->sink); + if (entry) { + tui_draw(&c->tui, &entry->clock, &entry->runner); + } else { + tui_draw(&c->tui, NULL, NULL); + } + aki_timer_again(timer); + camu_sink_return_current(&c->sink); +} + +static s32 log_callback(void *userdata, char *s) +{ + struct cmv *c = (struct cmv *)userdata; + tui_push_log_msg(&c->tui, s); + return 0; +} + +#ifdef HAVE_FFMPEG +static void lav_log_callback(void *userdata, s32 level, const char *fmt, va_list args) +{ + (void)userdata; + if (level < AV_LOG_VERBOSE) al_logv(warn, "lav_internal", (char *)fmt, args); +} +#endif +#endif + +s32 main(s32 argc, char *argv[]) +{ + aki_common_init(); + +#if CMV_USE_TUI + if (!tui_init(&c.tui)) { + aki_common_close(); + return EXIT_FAILURE; + } + al_set_print(&c, log_callback); +#ifdef HAVE_FFMPEG + camu_lav_set_log_callback(lav_log_callback); +#endif +#endif + + c.scr.callback = screen_callback; + c.scr.userdata = &c; + if (!camu_screen_init(&c.scr)) { + aki_common_close(); + return EXIT_FAILURE; + } + + if (!camu_screen_create_window(&c.scr, "cmv")) { + aki_common_close(); + return EXIT_FAILURE; + } + c.renderer = camu_renderer_lp_create(); + //c.renderer = camu_renderer_tiger_create(); + camu_screen_create_renderer(&c.scr, c.renderer); + +#if CAP_USE_PYTHON + bool py_init = sho_python_init(); +#endif + + camu_mixer_init(&c.mixer, (struct camu_audio *)&audio_plugin_miniaudio); + c.mixer.audio->configure_stream(c.mixer.audio, NULL); + + aki_event_loop_init(&c.loop); + + c.sink.callback = sink_callback; + c.sink.userdata = &c; + camu_sink_init(&c.sink, &c.loop, &c.mixer, c.renderer); + + cap_init(&c.cap, cap_callback, &c); + cap_make_list(&c.cap, default_list); + for (s32 i = 1; i < argc; i++) { + cap_list_add(&c.cap, default_list, al_str_c(argv[i])); + } + +#ifndef _WIN32 + c.socket.type = AKI_SOCKET_UNIX; + aki_socket_init(&c.socket); + aki_socket_set_blocking(&c.socket, false); + aki_socket_set_no_delay(&c.socket, 1); + c.pro.callback = line_callback; + c.pro.userdata = &c; + aki_line_processor_init(&c.pro, al_str_c("\n")); + aki_line_processor_open_socket(&c.pro, &c.socket); + if (aki_socket_listen(&c.socket, al_str_c("/tmp/cmv_sock"), 0)) { + aki_line_processor_run(&c.pro, &c.loop); + } +#endif + +#if CMV_USE_TUI + aki_timer_init(&c.timer, &c.loop, timer_callback, &c); + aki_timer_set_repeat(&c.timer, 0.15); + aki_timer_again(&c.timer); +#endif + + struct aki_thread thread0; + aki_thread_create(&thread0, event_loop_thread, &c); + + c.quit = 0; + + while (!c.quit) { + if (camu_screen_tick(&c.scr)) { + c.renderer->render(c.renderer, &c.scr); + } + } + + camu_screen_set_state(&c.scr, CAMU_SCREEN_CLOSED); + + camu_sink_stop(&c.sink); + + aki_thread_join(&thread0); + + camu_mixer_close(&c.mixer); + camu_screen_close(&c.scr); + c.renderer->free(&c.renderer); + + cap_close(&c.cap); + +#if CAP_USE_PYTHON + if (py_init) sho_python_close(); +#endif + +#if CMV_USE_TUI + tui_close(&c.tui); +#endif + + aki_common_close(); + + return EXIT_SUCCESS; +} diff --git a/src/fruits/cmv/meson.build b/src/fruits/cmv/meson.build new file mode 100644 index 0000000..b604104 --- /dev/null +++ b/src/fruits/cmv/meson.build @@ -0,0 +1,26 @@ +cmv2_src = ['cmv2.c', 'tui.c'] +#cmv2_src = ['cmv2.c'] +cmv_deps = [common_deps, buffer, cache, av, screen, render, mixer, bimu, libsink, cap] + +miniaudio_args = [ + '-DMA_NO_JACK', + '-DMA_NO_ALSA', + '-DMA_NO_DECODING', + '-DMA_NO_ENCODING', + '-DMA_NO_GENERATION', + '-DMA_NO_RESOURCE_MANAGER', + '-DMA_NO_NODE_GRAPH', + '-DMA_NO_ENGINE', + '-DMA_NO_RUNTIME_LINKING' +] + +sekihi_args = [ + '-DSEKIHI_WINDOW_WAYLAND', +# '-DSEKIHI_WINDOW_GLFW', + '-DSEKIHI_API_OPENGL', +# '-DSEKIHI_API_VULKAN', + '-DSEKIHI_POLL_INLINE', + '-DSEKIHI_PAUSE' +] + +executable('cmv2', cmv2_src, dependencies: cmv_deps, c_args: [miniaudio_args, sekihi_args]) diff --git a/src/fruits/cmv/tui.c b/src/fruits/cmv/tui.c new file mode 100644 index 0000000..675d5f4 --- /dev/null +++ b/src/fruits/cmv/tui.c @@ -0,0 +1,118 @@ +#include <aki/file.h> +#include <al/log.h> +#include <sys/ioctl.h> +#include <unistd.h> +#include <termios.h> + +#include "tui.h" + +static struct cmv_tui *tui_global; + +static void update_term_size(struct cmv_tui *tui) +{ + struct winsize w; + ioctl(tui->fd, TIOCGWINSZ, &w); + tui->width = w.ws_col; + tui->height = w.ws_row; +} + +static void handle_winch(s32 sig) +{ + (void)sig; + struct cmv_tui *tui = tui_global; + aki_mutex_lock(&tui->mutex); + tui->update_size = true; + aki_mutex_unlock(&tui->mutex); +} + +bool tui_init(struct cmv_tui *tui) +{ + tui->update_size = true; + tui->width = 0; + tui->height = 0; + //tui->prev_lines = 0; + + tui->fd = STDIN_FILENO; + tui->out = fdopen(tui->fd, "w"); + if (!tui->out) return false; + setbuf(tui->out, NULL); + + tui_global = tui; + aki_mutex_init(&tui->mutex); + signal(SIGWINCH, handle_winch); + + al_array_init(tui->log_buffer); + tui->last_pts = 0.0; + + return true; +} + +void tui_push_log_msg(struct cmv_tui *tui, char *msg) +{ + aki_mutex_lock(&tui->mutex); + al_array_push(tui->log_buffer, msg); + aki_mutex_unlock(&tui->mutex); +} + +static char buffer[128]; + +static void draw_now_playing(struct cmv_tui *tui, struct camu_clock *clock, struct bmu_local *runner) +{ + f64 pts = camu_clock_get_pts(clock); + if (pts == -1) pts = tui->last_pts; + else tui->last_pts = pts; + s32 parts = 0; + f64 duration = bmu_local_get_duration(runner); + if (pts > 0.0 && duration != 0.0) { + if (pts > duration) pts = duration; + f64 percent = pts / duration; + parts = tui->width * percent; + } else if (duration == 0.0) { + parts = tui->width; + } + al_memset(buffer, '-', parts); + buffer[parts] = '\0'; + fprintf(tui->out, "%s", buffer); +} + +void tui_draw(struct cmv_tui *tui, struct camu_clock *clock, struct bmu_local *runner) +{ + aki_mutex_lock(&tui->mutex); + + if (tui->update_size) { + update_term_size(tui); + tui->update_size = false; + } + + fprintf(tui->out, "\r\033[K"); + //for (u32 i = 1; i < tui->prev_lines; i++) { + // fprintf(tui->out, "\033[A\r\033[K"); + //} + + char *msg; + for (u32 i = 0; i < tui->log_buffer.size; i++) { + al_array_pop_at(tui->log_buffer, 0, msg); + fprintf(tui->out, "%s\n", msg); + } + + //tui->prev_lines = 0; + + if (runner) { + draw_now_playing(tui, clock, runner); + } + + aki_mutex_unlock(&tui->mutex); +} + +void tui_close(struct cmv_tui *tui) +{ + char *msg; + al_array_foreach(tui->log_buffer, i, msg) { + al_free(msg); + } + al_array_free(tui->log_buffer); + aki_mutex_destroy(&tui->mutex); + fprintf(tui->out, "\n"); + fflush(tui->out); + fclose(tui->out); +} diff --git a/src/fruits/cmv/tui.h b/src/fruits/cmv/tui.h new file mode 100644 index 0000000..566a5e5 --- /dev/null +++ b/src/fruits/cmv/tui.h @@ -0,0 +1,21 @@ +#pragma once + +#include "../../buffer/clock.h" +#include "../../bimu/local.h" + +struct cmv_tui { + s32 fd; + FILE *out; + bool update_size; + u32 width; + u32 height; + //u32 prev_lines; + array(char *) log_buffer; + f64 last_pts; + struct aki_mutex mutex; +}; + +bool tui_init(struct cmv_tui *tui); +void tui_push_log_msg(struct cmv_tui *tui, char *msg); +void tui_draw(struct cmv_tui *tui, struct camu_clock *clock, struct bmu_local *runner); +void tui_close(struct cmv_tui *tui); diff --git a/src/libclient/client.c b/src/libclient/client.c new file mode 100644 index 0000000..60998c6 --- /dev/null +++ b/src/libclient/client.c @@ -0,0 +1,98 @@ +#include "../tree/commands.h" +#include "../shoki/src/packet_ext.h" + +#include "client.h" + +static void identifed_callback(void *userdata, struct aki_packet *packet) +{ + struct camu_client *client = (struct camu_client *)userdata; + client->callback(client->userdata, CAMU_CLIENT_CONNECTED, NULL); + aki_packet_free(packet); +} + +static void connection_callback(void *userdata, struct aki_rpc_connection *conn) +{ + struct camu_client *client = (struct camu_client *)userdata; + client->conn = conn; + struct aki_packet *packet = aki_rpc_get_packet(&client->client, TREE_CMD_IDENTIFY); + aki_packet_write_u8(packet, TREE_CLIENT); + aki_packet_write_string(packet, &client->username); + aki_rpc_connection_command(client->conn, packet, identifed_callback, client); +} + +static void connection_closed_callback(void *userdata, struct aki_rpc_connection *conn) +{ + (void)userdata; + (void)conn; +} + +static u8 packet_pool_callback(void *userdata, struct aki_packet *packet) +{ + struct camu_client *client = (struct camu_client *)userdata; + aki_rpc_connection_command(client->conn, packet, packet->userdata, client); + return AKI_PACKET_POOL_NOP; +} + +bool camu_client_init(struct camu_client *client) +{ + aki_event_loop_init(&client->loop); + aki_packet_pool_init_ex(&client->pool, 0, &client->loop, + AKI_PACKET_POOL_MODE_PASSTHROUGH, packet_pool_callback, client); + return aki_rpc_init(&client->client, AKI_SOCKET_TCP, connection_callback, + connection_closed_callback, client); +} + +static aki_thread_result AKI_THREADCALL event_loop_thread(void *userdata) +{ + struct camu_client *client = (struct camu_client *)userdata; + aki_event_loop_run(&client->loop); + return 0; +} + +bool camu_client_login(struct camu_client *client, str *username, str *addr, s32 port, + void (*callback)(void *, u8, void *), void *userdata) +{ + al_str_clone(&client->username, username); + client->callback = callback; + client->userdata = userdata; + if (!aki_rpc_connect(&client->client, &client->loop, addr, port)) { + return false; + } + aki_thread_create(&client->thread, event_loop_thread, client); + return true; +} + +static void search_callback(void *userdata, struct aki_packet *packet) +{ + struct camu_client *client = (struct camu_client *)userdata; + struct camu_search_results *results = al_alloc_object(struct camu_search_results); + al_array_init(results->results); + s32 search_id = aki_packet_read_s32(packet); + if (search_id == -1) return; + results->page = aki_packet_read_s32(packet); + u32 size = aki_packet_read_u32(packet); + for (u32 i = 0; i < size; i++) { + struct sho_post post; + aki_packet_read_sho_post(packet, &post); + sho_post_cache_push(&client->search.cache, &post); + } + size = aki_packet_read_u32(packet); + for (u32 i = 0; i < size; i++) { + str s, unique_id; + aki_packet_read_string(packet, &s); + al_str_clone(&unique_id, &s); + al_array_push(results->results, unique_id); + } + client->callback(client->userdata, CAMU_CLIENT_RESULTS, results); + aki_packet_free(packet); +} + +void camu_client_more_results(struct camu_client *client, struct camu_search *search) +{ + struct aki_packet *packet = aki_rpc_get_packet(&client->client, TREE_CMD_SEARCH); + aki_packet_write_s32(packet, search->id); + aki_packet_write_string(packet, &search->provider); + aki_packet_write_string(packet, &search->query); + packet->userdata = search_callback; + aki_packet_pool_submit(&client->pool, packet); +} diff --git a/src/libclient/client.h b/src/libclient/client.h new file mode 100644 index 0000000..989c839 --- /dev/null +++ b/src/libclient/client.h @@ -0,0 +1,31 @@ +#pragma once + +#include <aki/rpc2.h> +#include <aki/thread.h> +#include <aki/packet_pool.h> + +#include "search.h" + +enum { + CAMU_CLIENT_CONNECTED = 0, + // Search client. + CAMU_CLIENT_RESULTS +}; + +struct camu_client { + struct aki_event_loop loop; + struct aki_rpc client; + str username; + struct aki_rpc_connection *conn; + struct camu_search_client search; + struct aki_packet_pool pool; + struct aki_thread thread; + void (*callback)(void *, u8, void *); + void *userdata; +}; + +bool camu_client_init(struct camu_client *client); +bool camu_client_login(struct camu_client *client, str *username, str *addr, s32 port, + void (*callback)(void *, u8, void *), void *userdata); +void camu_client_more_results(struct camu_client *client, struct camu_search *search); +void camu_client_close(struct camu_client *client); diff --git a/src/libclient/commands.h b/src/libclient/commands.h new file mode 100644 index 0000000..6f70f09 --- /dev/null +++ b/src/libclient/commands.h @@ -0,0 +1 @@ +#pragma once diff --git a/src/libclient/meson.build b/src/libclient/meson.build new file mode 100644 index 0000000..a2f23b0 --- /dev/null +++ b/src/libclient/meson.build @@ -0,0 +1,3 @@ +libclient_src = ['client.c', 'resource_client.c'] +libclient_deps = [shoki] +libclient = declare_dependency(sources: libclient_src, dependencies: libclient_deps) diff --git a/src/libclient/resource_client.c b/src/libclient/resource_client.c new file mode 100644 index 0000000..1069755 --- /dev/null +++ b/src/libclient/resource_client.c @@ -0,0 +1,91 @@ +#include <al/random.h> + +#include "../tree/tree.h" + +#include "resource_client.h" + +static void connection_callback(void *userdata, struct aki_packet_stream *stream) +{ + struct camu_resource_client *client = (struct camu_resource_client *)userdata; + (void)stream; + client->connected = true; +} + +static void connection_closed_callback(void *userdata, struct aki_packet_stream *stream) +{ + struct camu_resource_client *client = (struct camu_resource_client *)userdata; + (void)stream; + client->connected = true; +} + +static void packet_callback(void *userdata, struct aki_packet_stream *stream, + struct aki_packet *packet) +{ + struct camu_resource_client *client = (struct camu_resource_client *)userdata; + (void)stream; + u16 id = aki_packet_read_u16(packet); + struct aki_buffer buffer; + aki_buffer_init(&buffer); + aki_packet_read_buffer(packet, &buffer); + client->callback(client->userdata, id, &buffer); +} + +static void packet_sent_callback(void *userdata, struct aki_packet *packet) +{ + (void)userdata; + aki_packet_free(packet); +} + +static void signal_callback(void *userdata) +{ + struct camu_resource_client *client = (struct camu_resource_client *)userdata; + u32 size; + struct camu_resource_request request; + do { + camu_queue_size(client->queue, size); + if (!size) break; + camu_queue_pop(client->queue, request); + struct aki_packet *packet = aki_packet_create(); + aki_packet_write_u16(packet, request.id); + aki_packet_write_string(packet, &request.unique_id); + aki_packet_write_u32(packet, request.index); + aki_packet_stream_send_packet(&client->client, packet); + } while (1); +} + +static aki_thread_result AKI_THREADCALL event_loop_thread(void *userdata) +{ + struct camu_resource_client *client = (struct camu_resource_client *)userdata; + aki_event_loop_run(&client->loop); + return 0; +} + +void camu_resource_client_run(struct camu_resource_client *client, + void (*callback)(void *, u16, struct aki_buffer *), void *userdata) +{ + client->callback = callback; + client->userdata = userdata; + camu_queue_init(client->queue); + aki_event_loop_init(&client->loop); + aki_signal_init(&client->signal, signal_callback, client); + aki_signal_start(&client->signal, &client->loop); + aki_packet_stream_init(&client->client, AKI_SOCKET_TCP, connection_callback, + connection_closed_callback, packet_callback, packet_sent_callback, client); + aki_packet_stream_connect(&client->client, &client->loop, al_str_c("127.0.0.1"), TREE_RESOURCE_PORT); + client->connected = false; + while (!client->connected) { + aki_event_loop_run_once(&client->loop); + } + aki_thread_create(&client->thread, event_loop_thread, client); +} + +void camu_resource_request(struct camu_resource_client *client, + str *unique_id, u32 index, u16 id) +{ + struct camu_resource_request request; + request.id = id; + al_str_clone(&request.unique_id, unique_id); + request.index = index; + camu_queue_push(client->queue, request); + aki_signal_send(&client->signal); +} diff --git a/src/libclient/resource_client.h b/src/libclient/resource_client.h new file mode 100644 index 0000000..4cc1bbf --- /dev/null +++ b/src/libclient/resource_client.h @@ -0,0 +1,29 @@ +#pragma once + +#include <aki/packet_stream.h> +#include <aki/thread.h> +#include <aki/signal.h> + +#include "../util/queue.h" + +struct camu_resource_request { + u16 id; + str unique_id; + u32 index; +}; + +struct camu_resource_client { + struct aki_event_loop loop; + struct aki_packet_stream client; + bool connected; + struct aki_thread thread; + struct aki_signal signal; + queue(struct camu_resource_request) queue; + void (*callback)(void *, u16, struct aki_buffer *); + void *userdata; +}; + +void camu_resource_client_run(struct camu_resource_client *client, + void (*callback)(void *, u16, struct aki_buffer *), void *userdata); +void camu_resource_request(struct camu_resource_client *client, + str *unique_id, u32 index, u16 id); diff --git a/src/libclient/search.c b/src/libclient/search.c new file mode 100644 index 0000000..f74f83d --- /dev/null +++ b/src/libclient/search.c @@ -0,0 +1,31 @@ +#include "search.h" + +void camu_search_client_init(struct camu_search_client *client) +{ + client->id = 0; + sho_post_cache_init(&client->cache); +} + +void camu_search_set(struct camu_search_client *client, struct camu_search *search, + str *provider, str *query) +{ + search->id = client->id++; + al_str_clone(&search->provider, provider); + al_str_clone(&search->query, query); +} + +void camu_search_free(struct camu_search *search) +{ + al_str_free(&search->provider); + al_str_free(&search->query); +} + +void camu_search_results_free(struct camu_search_results *results) +{ + str *unique_id; + al_array_foreach_ptr(results->results, i, unique_id) { + al_str_free(unique_id); + } + al_array_free(results->results); + al_free(results); +} diff --git a/src/libclient/search.h b/src/libclient/search.h new file mode 100644 index 0000000..b0d8554 --- /dev/null +++ b/src/libclient/search.h @@ -0,0 +1,28 @@ +#pragma once + +#include <al/array.h> +#include <al/str.h> + +#include "../shoki/src/post_cache.h" + +struct camu_search { + s32 id; + str provider; + str query; +}; + +struct camu_search_results { + u32 page; + array(str) results; +}; + +struct camu_search_client { + s32 id; + struct sho_post_cache cache; +}; + +void camu_search_client_init(struct camu_search_client *client); +void camu_search_set(struct camu_search_client *client, struct camu_search *search, + str *provider, str *query); +void camu_search_free(struct camu_search *search); +void camu_search_results_free(struct camu_search_results *results); 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); diff --git a/src/mixer/audio.h b/src/mixer/audio.h new file mode 100644 index 0000000..e460536 --- /dev/null +++ b/src/mixer/audio.h @@ -0,0 +1,37 @@ +#pragma once + +#include <al/str.h> + +#include "../codec/libav/resampler.h" + +enum { + // Not initialized. + CAMU_AUDIO_STATE_NULL = 0, + // Context created, waiting for stream. + CAMU_AUDIO_STATE_INITIALIZED, + // Stream created, and running. + CAMU_AUDIO_STATE_STARTED, + // Error occured, no data will be requested. + CAMU_AUDIO_STATE_ERROR, + // Not built. + CAMU_AUDIO_STATE_UNSUPPORTED +}; + +struct camu_audio { + u8 state; + + s32 (*data_callback)(void *, u8 *, s32); + void *userdata; + + bool (*init)(struct camu_audio *, str *); + + void (*pick_format)(struct camu_audio *, struct camu_lav_resample_fmt *); + bool (*configure_stream)(struct camu_audio *, void *); + + u64 (*get_latency)(struct camu_audio *); + + void (*start)(struct camu_audio *); + void (*stop)(struct camu_audio *); + + void (*free)(struct camu_audio **); +}; diff --git a/src/mixer/audio_miniaudio.c b/src/mixer/audio_miniaudio.c new file mode 100644 index 0000000..1388fc4 --- /dev/null +++ b/src/mixer/audio_miniaudio.c @@ -0,0 +1,190 @@ +#include <al/log.h> + +#include "audio_miniaudio.h" + +static ma_backend backends[] = { +#ifndef _WIN32 +// ma_backend_alsa, + ma_backend_pulseaudio, +#else + ma_backend_wasapi +#endif +}; + +static void *miniaudio_malloc(size_t size, void *userdata) +{ + (void)userdata; + return al_malloc(size); +} + +static void *miniaudio_realloc(void *ptr, size_t size, void *userdata) +{ + (void)userdata; + return al_realloc(ptr, size); +} + +static void miniaudio_free(void *ptr, void *userdata) +{ + (void)userdata; + al_free(ptr); +} + +static ma_allocation_callbacks alloc_callbacks = { + .pUserData = NULL, + .onMalloc = miniaudio_malloc, + .onRealloc = miniaudio_realloc, + .onFree = miniaudio_free +}; + +static void miniaudio_log_callback(void *userdata, u32 level, const char *message) +{ + (void)userdata; + if (level <= MA_LOG_LEVEL_WARNING) al_log_debug("audio_miniaudio", message); +} + +static bool audio_miniaudio_init(struct camu_audio *audio, str *name) +{ + struct camu_audio_miniaudio *ma = (struct camu_audio_miniaudio *)audio; + ma_log_init(&alloc_callbacks, &ma->log); + ma_log_register_callback(&ma->log, ma_log_callback_init(miniaudio_log_callback, NULL)); + ma->context_config = ma_context_config_init(); + ma->context_config.pLog = &ma->log; + ma->context_config.alsa.useVerboseDeviceEnumeration = true; + char *c_str = al_str_to_c_str(name); + ma->context_config.pulse.pApplicationName = c_str; + //ma->context_config.pulse.pServerName = ""; + ma->context_config.pulse.tryAutoSpawn = true;; + s32 ret = ma_context_init(backends, AL_ARRAY_SIZE(backends), &ma->context_config, &ma->context); + if (ret != MA_SUCCESS) { + al_log_error("audio_miniaudio", "Failed to initialize context (%d).", ret); + return false; + } + //if (ma_context_get_devices(&ma->context, &ma->infos, &ma->info_count, NULL, NULL) != MA_SUCCESS) { + // return false; + //} + //ma->selected_device = NULL; + //for (u32 i = 0; i < ma->info_count; i++) { + // ma_device_info *info = &ma->infos[i]; + // ma_device_info real_info; + // ma_context_get_device_info(&ma->context, ma_device_type_playback, &info->id, &real_info); + // for (u32 j = 0; j < real_info.nativeDataFormatCount; j++) { + // if (strcmp(real_info.id.alsa, "hw:CARD=D10,DEV=0") == 0) { + // ma->selected_device = info; + // break; + // } + // /* + // al_printf("%s\n", real_info.id.alsa); + // al_printf("%i\n", real_info.nativeDataFormats[j].format); + // al_printf("%i\n", real_info.nativeDataFormats[j].channels); + // al_printf("%i\n", real_info.nativeDataFormats[j].sampleRate); + // */ + // } + // if (ma->selected_device) break; + // //al_printf("\n"); + //} + ////al_printf("%i\n", ma->info_count); + return true; +} + +static void audio_miniaudio_pick_format(struct camu_audio *audio, struct camu_lav_resample_fmt *fmt) +{ + struct camu_audio_miniaudio *ma = (struct camu_audio_miniaudio *)audio; + (void)ma; + fmt->req_channel_count = 2; + fmt->req_format = AV_SAMPLE_FMT_S32; + fmt->req_sample_rate = 44100; +} + +static void data_callback(ma_device *device, void *output, const void *input, ma_uint32 frame_count) +{ + struct camu_audio_miniaudio *ma = (struct camu_audio_miniaudio *)device->pUserData; + (void)input; + ma->a.data_callback(ma->a.userdata, output, frame_count); +} + +#define DEFAULT_PERIODS 3 +#define DEFAULT_PERIOD_SIZE_IN_MILLISECONDS 100 + +static bool audio_miniaudio_configure_stream(struct camu_audio *audio, void *opaque) +{ + struct camu_audio_miniaudio *ma = (struct camu_audio_miniaudio *)audio; + struct camu_lav_resample_fmt *fmt = (struct camu_lav_resample_fmt *)opaque; + (void)fmt; + ma->config = ma_device_config_init(ma_device_type_playback); + //ma->config.playback.pDeviceID = &ma->selected_device->id; + ma->config.performanceProfile = ma_performance_profile_conservative; + ma->config.periods = DEFAULT_PERIODS; + ma->config.periodSizeInMilliseconds = DEFAULT_PERIOD_SIZE_IN_MILLISECONDS; + ma->config.playback.channels = 2; + ma->config.playback.format = ma_format_s32; + ma->config.sampleRate = 44100; + ma->config.dataCallback = data_callback; + ma->config.pUserData = ma; + ma->config.noPreSilencedOutputBuffer = true; + s32 ret = ma_device_init(&ma->context, &ma->config, &ma->device); + if (ret != MA_SUCCESS) { + al_log_error("audio_miniaudio", "Failed to initialize device (%d).", ret); + return false; + } + const char *format_name = av_get_sample_fmt_name(AV_SAMPLE_FMT_S32); + al_log_info("audio_miniaudio", "Output: %s (%dch) %dHz.", format_name, 2, 44100); + return true; +} + +static u64 audio_miniaudio_get_latency(struct camu_audio *audio) +{ + struct camu_audio_miniaudio *ma = (struct camu_audio_miniaudio *)audio; + // Latency is one period on PulseAudio even if periods is >1. On WASAPI, latency is periods + // times period size. + // This is a hack, should be possible to determine at runtime. +#ifdef _WIN32 + return ((ma->device.playback.internalPeriodSizeInFrames * ma->device.playback.internalPeriods) / +#else + return ((ma->device.playback.internalPeriodSizeInFrames) / +#endif + (ma->device.playback.internalSampleRate / 1000000.0)); +} + +static void audio_miniaudio_start(struct camu_audio *audio) +{ + struct camu_audio_miniaudio *ma = (struct camu_audio_miniaudio *)audio; + ma_device_start(&ma->device); +} + +static void audio_miniaudio_stop(struct camu_audio *audio) +{ + struct camu_audio_miniaudio *ma = (struct camu_audio_miniaudio *)audio; + ma_device_stop(&ma->device); +} + +static void audio_miniaudio_free(struct camu_audio **audio) +{ + struct camu_audio_miniaudio *ma = (struct camu_audio_miniaudio *)*audio; + ma_device_uninit(&ma->device); + ma_context_uninit(&ma->context); + al_free((void *)ma->context_config.pulse.pApplicationName); + *audio = NULL; +} + +struct camu_audio_miniaudio audio_plugin_miniaudio = { + .a = { + .state = CAMU_AUDIO_STATE_NULL, + .data_callback = NULL, + .userdata = NULL, + .init = audio_miniaudio_init, + .pick_format = audio_miniaudio_pick_format, + .configure_stream = audio_miniaudio_configure_stream, + .get_latency = audio_miniaudio_get_latency, + .start = audio_miniaudio_start, + .stop = audio_miniaudio_stop, + .free = audio_miniaudio_free + }, + .log = { }, + .context_config = { }, + .context = { }, + .infos = NULL, + .info_count = 0, + .selected_device = NULL, + .config = { }, + .device = { } +}; diff --git a/src/mixer/audio_miniaudio.h b/src/mixer/audio_miniaudio.h new file mode 100644 index 0000000..5d5cd03 --- /dev/null +++ b/src/mixer/audio_miniaudio.h @@ -0,0 +1,20 @@ +#pragma once + +#define MINIAUDIO_IMPLEMENTATION +#include <miniaudio.h> + +#include "audio.h" + +struct camu_audio_miniaudio { + struct camu_audio a; + ma_log log; + ma_context_config context_config; + ma_context context; + ma_device_info *infos; + u32 info_count; + ma_device_info *selected_device; + ma_device_config config; + ma_device device; +}; + +extern struct camu_audio_miniaudio audio_plugin_miniaudio; diff --git a/src/mixer/meson.build b/src/mixer/meson.build new file mode 100644 index 0000000..4053210 --- /dev/null +++ b/src/mixer/meson.build @@ -0,0 +1,12 @@ +mixer_src = ['mixer.c'] +mixer_deps = [common_deps] + +miniaudio = subproject('miniaudio').get_variable('miniaudio') +mixer_src += ['audio_miniaudio.c'] +mixer_deps += [miniaudio] +if host_machine.system() != 'windows' + libpulse = dependency('libpulse') + mixer_deps += [libpulse] +endif + +mixer = declare_dependency(sources: mixer_src, dependencies: mixer_deps) diff --git a/src/mixer/mixer.c b/src/mixer/mixer.c new file mode 100644 index 0000000..8cfc2cc --- /dev/null +++ b/src/mixer/mixer.c @@ -0,0 +1,167 @@ +#include "../buffer/audio.h" +#include "../buffer/clock.h" + +#include "mixer.h" + +static s32 data_callback(void *userdata, u8 *data, s32 frame_count) +{ + struct camu_mixer *mixer = (struct camu_mixer *)userdata; +#ifdef CAMU_MIXER_THREADED + if (al_atomic_bool_load(&mixer->queued, AL_ATOMIC_RELAXED)) { + camu_mixer_run_queue(mixer); + } +#endif + size_t req = camu_lav_resample_fmt_samples_to_bytes(&mixer->fmt, (size_t)frame_count); + do { + if (mixer->buffers.size == 0) { + al_memset(data, 0, req); + } else { + struct camu_audio_buffer *buf = al_array_last(mixer->buffers); + size_t signal; + if ((signal = camu_audio_buffer_read(buf, data, req)) < req) { +#ifdef CAMU_MIXER_THREADED + if (al_atomic_bool_load(&mixer->queued, AL_ATOMIC_RELAXED)) { + camu_mixer_run_queue(mixer); + } +#endif + data += signal; + req = req - signal; + continue; + } + } + break; + } while (1); + return frame_count; +} + +bool camu_mixer_init(struct camu_mixer *mixer, struct camu_audio *audio) +{ + mixer->audio = audio; + mixer->audio->data_callback = data_callback; + mixer->audio->userdata = mixer; + mixer->audio->init(mixer->audio, al_str_c("camu")); + mixer->audio->pick_format(mixer->audio, &mixer->fmt); + av_channel_layout_default(&mixer->fmt.req_channel_layout, mixer->fmt.req_channel_count); + mixer->paused = true; + al_array_init(mixer->buffers); +#ifdef CAMU_MIXER_THREADED + al_array_init(mixer->add_queue); + al_array_init(mixer->rem_queue); + al_atomic_bool_store(&mixer->queued, false, AL_ATOMIC_RELAXED); + aki_mutex_init(&mixer->mutex); +#endif + return true; +} + +void camu_mixer_pick_format(struct camu_mixer *mixer, struct camu_lav_resample_fmt *fmt) +{ + mixer->audio->pick_format(mixer->audio, fmt); +} + +f64 camu_mixer_get_latency(struct camu_mixer *mixer) +{ + return mixer->audio->get_latency(mixer->audio) / 1000000.0; +} + +static void add_buffer_internal(struct camu_mixer *mixer, struct camu_audio_buffer *buf) +{ +#ifdef CAMU_MIXER_THREADED + al_atomic_bool_store(&buf->ref, true, AL_ATOMIC_RELAXED); +#endif + al_array_push(mixer->buffers, buf); +} + +static void remove_buffer_internal(struct camu_mixer *mixer, struct camu_audio_buffer *buf) +{ + struct camu_audio_buffer *cbuf; + al_array_foreach(mixer->buffers, i, cbuf) { + if (cbuf == buf) { +#ifdef CAMU_MIXER_THREADED + al_atomic_bool_store(&buf->ref, false, AL_ATOMIC_RELAXED); +#endif + al_array_remove_at_iter(mixer->buffers, i); + break; + } + } +} + +void camu_mixer_add_buffer(struct camu_mixer *mixer, struct camu_audio_buffer *buf) +{ +#ifdef CAMU_MIXER_THREADED + aki_mutex_lock(&mixer->mutex); + al_array_push(mixer->add_queue, buf); + // Ensure an allocation doesn't happen in data_callback. + al_array_reserve(mixer->buffers, mixer->buffers.size + mixer->add_queue.size); + al_atomic_bool_store(&mixer->queued, true, AL_ATOMIC_RELAXED); + aki_mutex_unlock(&mixer->mutex); +#else + add_buffer_internal(mixer, buf); +#endif +} + +#ifdef CAMU_MIXER_THREADED +static void run_queue_internal(struct camu_mixer *mixer) +{ + struct camu_audio_buffer *buf; + al_array_foreach(mixer->add_queue, i, buf) { + add_buffer_internal(mixer, buf); + al_array_remove_at_iter(mixer->add_queue, i); + } + al_array_foreach(mixer->rem_queue, i, buf) { + remove_buffer_internal(mixer, buf); + al_array_remove_at_iter(mixer->rem_queue, i); + } + al_atomic_bool_store(&mixer->queued, false, AL_ATOMIC_RELAXED); +} +#endif + +void camu_mixer_remove_buffer(struct camu_mixer *mixer, struct camu_audio_buffer *buf) +{ +#ifdef CAMU_MIXER_THREADED + aki_mutex_lock(&mixer->mutex); + al_array_push(mixer->rem_queue, buf); + al_atomic_bool_store(&mixer->queued, true, AL_ATOMIC_RELAXED); + if (mixer->paused) { + run_queue_internal(mixer); + } + aki_mutex_unlock(&mixer->mutex); +#else + remove_buffer_internal(mixer, buf); +#endif +} + +#ifdef CAMU_MIXER_THREADED +void camu_mixer_run_queue(struct camu_mixer *mixer) +{ + aki_mutex_lock(&mixer->mutex); + run_queue_internal(mixer); + aki_mutex_unlock(&mixer->mutex); +} +#endif + +void camu_mixer_pause(struct camu_mixer *mixer) +{ + if (!mixer->paused) { + mixer->audio->stop(mixer->audio); + mixer->paused = true; + } +} + +void camu_mixer_resume(struct camu_mixer *mixer) +{ + if (mixer->paused) { + mixer->audio->start(mixer->audio); + mixer->paused = false; + } +} + +void camu_mixer_close(struct camu_mixer *mixer) +{ + al_array_free(mixer->buffers); +#ifdef CAMU_MIXER_THREADED + al_array_free(mixer->add_queue); + al_array_free(mixer->rem_queue); + aki_mutex_destroy(&mixer->mutex); +#endif + mixer->audio->free(&mixer->audio); +} diff --git a/src/mixer/mixer.h b/src/mixer/mixer.h new file mode 100644 index 0000000..464431e --- /dev/null +++ b/src/mixer/mixer.h @@ -0,0 +1,37 @@ +#pragma once + +#define CAMU_MIXER_THREADED + +#include <al/types.h> +#include <al/array.h> +#ifdef CAMU_MIXER_THREADED +#include <al/atomic.h> +#include <aki/thread.h> +#endif + +#include "../codec/libav/resampler.h" + +struct camu_mixer { + struct camu_audio *audio; + bool paused; + struct camu_lav_resample_fmt fmt; + array(struct camu_audio_buffer *) buffers; +#ifdef CAMU_MIXER_THREADED + array(struct camu_audio_buffer *) add_queue; + array(struct camu_audio_buffer *) rem_queue; + atomic_bool queued; + struct aki_mutex mutex; +#endif +}; + +bool camu_mixer_init(struct camu_mixer *mixer, struct camu_audio *audio); +void camu_mixer_pick_format(struct camu_mixer *mixer, struct camu_lav_resample_fmt *fmt); +f64 camu_mixer_get_latency(struct camu_mixer *mixer); +void camu_mixer_add_buffer(struct camu_mixer *mixer, struct camu_audio_buffer *buf); +void camu_mixer_remove_buffer(struct camu_mixer *mixer, struct camu_audio_buffer *buf); +#ifdef CAMU_MIXER_THREADED +void camu_mixer_run_queue(struct camu_mixer *mixer); +#endif +void camu_mixer_pause(struct camu_mixer *mixer); +void camu_mixer_resume(struct camu_mixer *mixer); +void camu_mixer_close(struct camu_mixer *mixer); diff --git a/src/render/meson.build b/src/render/meson.build new file mode 100644 index 0000000..cc11eea --- /dev/null +++ b/src/render/meson.build @@ -0,0 +1,35 @@ +render_src = [] +render_deps = [common_deps] + +#linmath = subproject('linmath.h').get_variable('linmath') +#libm = compiler.find_library('m', required: false) +#if libm.found() +# render_deps += [libm] +#endif +#render_src += ['renderer_tiger.c', 'queue_tiger.c'] +#render_deps += [linmath] + +libplacebo = dependency('libplacebo', + default_options: [ + 'warning_level=2', + 'demos=false', + 'tests=false', + 'bench=false', + 'fuzz=false', + 'shaderc=enabled', + 'glslang=disabled', + 'opengl=enabled', + 'vulkan=disabled', + 'd3d11=disabled', + 'gl-proc-addr=disabled', + 'vk-proc-addr=disabled', + 'lcms=disabled', + 'dovi=disabled', + 'libdovi=disabled' + ], required: false, allow_fallback: true) +if libplacebo.found() + render_src += ['renderer_libplacebo.c', 'queue_libplacebo.c'] + render_deps += [libplacebo] +endif + +render = declare_dependency(sources: render_src, dependencies: render_deps) diff --git a/src/render/queue_libplacebo.c b/src/render/queue_libplacebo.c new file mode 100644 index 0000000..ee64920 --- /dev/null +++ b/src/render/queue_libplacebo.c @@ -0,0 +1,195 @@ +#include <al/lib.h> +#include <al/log.h> + +#include "../buffer/video.h" + +#include "queue_libplacebo.h" + +static bool map_frame(pl_gpu gpu, pl_tex *tex, const struct pl_source_frame *src, + struct pl_frame *out_frame) +{ + struct camu_frame *frame = src->frame_data; + + u64 masks[4] = { 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000 }; + s32 pixel_stride = 4; + + /* + switch (frame->fmt) { + case CAMU_PIXEL_FMT_RGBA: + break; + default: + al_log_error("frame_queue_libplacebo", "Unsuppored pixel format."); + return false; + } + */ + + struct pl_plane_data data = { + .type = PL_FMT_UNORM, + .width = frame->width, + .height = frame->height, + .pixel_stride = pixel_stride, + .row_stride = frame->width * pixel_stride, + .pixels = frame->data + }; + + pl_plane_data_from_mask(&data, masks); + + struct pl_plane plane = { 0 }; + bool ok = pl_upload_plane(gpu, &plane, tex, &data); + + al_free(frame); + + if (!ok) { + al_log_error("frame_queue_libplacebo", "Failed to upload texture."); + return false; + } + + out_frame->num_planes = 1; + out_frame->planes[0] = plane; + out_frame->repr = pl_color_repr_unknown; + out_frame->color = pl_color_space_unknown; + out_frame->crop = (pl_rect2df){ 0, 0, frame->width, frame->height }; + //out_frame->profile = + + return true; +} + +static void unmap_frame(pl_gpu gpu, struct pl_frame *frame, const struct pl_source_frame *src) +{ + (void)gpu; + (void)frame; + (void)src; +} + +static void discard_frame(const struct pl_source_frame *src) +{ + struct camu_frame *frame = src->frame_data; + al_free(frame); +} + +static void queue_lp_push(struct camu_frame_queue *queue, struct camu_frame *frame, f64 pts) +{ + struct camu_frame_queue_lp *lq = (struct camu_frame_queue_lp *)queue; + pl_queue_push(lq->queue, &(struct pl_source_frame){ + .pts = pts, + .duration = 0.0, + .map = map_frame, + .unmap = unmap_frame, + .discard = discard_frame, + .frame_data = frame, + }); +} + +#ifdef HAVE_FFMPEG +static bool map_av_frame(pl_gpu gpu, pl_tex *tex, const struct pl_source_frame *src, + struct pl_frame *out_frame) +{ + AVFrame *frame = src->frame_data; + struct camu_video_buffer *buf = frame->opaque; + AVStream *stream = buf->stream->av.stream; + + bool ok = pl_map_avframe_ex(gpu, out_frame, pl_avframe_params( + .frame = frame, + .tex = tex, + .map_dovi = false + )); + + av_frame_free(&frame); + + if (!ok) { + al_log_error("frame_queue_libplacebo", "Failed to map AVFrame."); + return false; + } + + pl_frame_copy_stream_props(out_frame, stream); + + return true; +} + +static void unmap_av_frame(pl_gpu gpu, struct pl_frame *frame, const struct pl_source_frame *src) +{ + (void)src; + pl_unmap_avframe(gpu, frame); +} + +static void discard_av_frame(const struct pl_source_frame *src) +{ + AVFrame *frame = src->frame_data; + av_frame_free(&frame); + al_log_info("frame_queue_libplacebo", "Dropped frame with PTS %.3f.", src->pts); +} + +static void queue_lp_push_av_frame(struct camu_frame_queue *queue, AVFrame *frame, f64 pts) +{ + struct camu_frame_queue_lp *lq = (struct camu_frame_queue_lp *)queue; + struct camu_video_buffer *buf = frame->opaque; + pl_queue_push(lq->queue, &(struct pl_source_frame){ + .pts = pts, + .duration = buf->avg_frame_duration, + .map = map_av_frame, + .unmap = unmap_av_frame, + .discard = discard_av_frame, + .frame_data = frame, + .first_field = pl_field_from_avframe(frame) + }); +} +#endif + +static void queue_lp_flush(struct camu_frame_queue *queue) +{ + struct camu_frame_queue_lp *lq = (struct camu_frame_queue_lp *)queue; + pl_queue_push(lq->queue, NULL); +} + +static s32 queue_lp_count(struct camu_frame_queue *queue) +{ + struct camu_frame_queue_lp *lq = (struct camu_frame_queue_lp *)queue; + return pl_queue_num_frames(lq->queue); +} + +static u8 queue_lp_read(struct camu_frame_queue *queue, f64 pts, void *out) +{ + struct camu_frame_queue_lp *lq = (struct camu_frame_queue_lp *)queue; + lq->params.pts = pts; + switch (pl_queue_update(lq->queue, (struct pl_frame_mix *)out, &lq->params)) { + case PL_QUEUE_OK: + return CAMU_QUEUE_OK; + case PL_QUEUE_MORE: + return CAMU_QUEUE_MORE; + case PL_QUEUE_EOF: + return CAMU_QUEUE_EOF; + case PL_QUEUE_ERR: + default: + return CAMU_QUEUE_ERR; + } +} + +static void queue_lp_reset(struct camu_frame_queue *queue) +{ + struct camu_frame_queue_lp *lq = (struct camu_frame_queue_lp *)queue; + pl_queue_push(lq->queue, NULL); + pl_queue_reset(lq->queue); +} + +static void queue_lp_free(struct camu_frame_queue **queue) +{ + struct camu_frame_queue_lp *lq = (struct camu_frame_queue_lp *)*queue; + pl_queue_destroy(&lq->queue); + al_free(lq); + *queue = NULL; +} + +struct camu_frame_queue *camu_frame_queue_lp_create(void) +{ + struct camu_frame_queue_lp *lq = al_alloc_object(struct camu_frame_queue_lp); + lq->q.push = queue_lp_push; +#ifdef HAVE_FFMPEG + lq->q.push_av_frame = queue_lp_push_av_frame; +#endif + lq->q.flush = queue_lp_flush; + lq->q.count = queue_lp_count; + lq->q.read = queue_lp_read; + lq->q.reset = queue_lp_reset; + lq->q.free = queue_lp_free; + return (struct camu_frame_queue *)lq; +} diff --git a/src/render/queue_libplacebo.h b/src/render/queue_libplacebo.h new file mode 100644 index 0000000..7da99ca --- /dev/null +++ b/src/render/queue_libplacebo.h @@ -0,0 +1,17 @@ +#pragma once + +#include "../buffer/frame_queue.h" + +#include <libplacebo/utils/frame_queue.h> +#include <libplacebo/utils/upload.h> +#ifdef HAVE_FFMPEG +#include <libplacebo/utils/libav.h> +#endif + +struct camu_frame_queue_lp { + struct camu_frame_queue q; + pl_queue queue; + struct pl_queue_params params; +}; + +struct camu_frame_queue *camu_frame_queue_lp_create(void); diff --git a/src/render/queue_tiger.c b/src/render/queue_tiger.c new file mode 100644 index 0000000..575b826 --- /dev/null +++ b/src/render/queue_tiger.c @@ -0,0 +1,92 @@ +#include <al/lib.h> + +#include "renderer_tiger.h" +#include "queue_tiger.h" + +static void upload_texture(struct camu_frame_queue_tiger *tq, s32 width, s32 height, + u8 *data, s32 linesize) +{ + glGenTextures(1, &tq->texture_id); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, tq->texture_id); + //glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0); + //glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + //if (frame->flags & CAMU_FRAME_DEVICE_ALLOCATED) { + // glBindBuffer(GL_PIXEL_UNPACK_BUFFER, tq->tr->texture_buffer); + //} + //if (linesize) { + // glPixelStorei(GL_UNPACK_ROW_LENGTH, linesize); + //} + (void)linesize; + //glPixelStorei(GL_UNPACK_ROW_LENGTH, linesize); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); + //glGenerateMipmap(GL_TEXTURE_2D); +} + +static void queue_tiger_push(struct camu_frame_queue *queue, struct camu_frame *frame, f64 pts) +{ + struct camu_frame_queue_tiger *tq = (struct camu_frame_queue_tiger *)queue; + (void)pts; + upload_texture(tq, frame->width, frame->height, frame->data, 0); +} + +#ifdef HAVE_FFMPEG +static void queue_tiger_push_av_frame(struct camu_frame_queue *queue, AVFrame *frame, f64 pts) +{ + struct camu_frame_queue_tiger *tq = (struct camu_frame_queue_tiger *)queue; + (void)pts; + upload_texture(tq, frame->width, frame->height, frame->data[0], frame->linesize[0]); +} +#endif + +static void queue_tiger_flush(struct camu_frame_queue *queue) +{ + (void)queue; +} + +static s32 queue_tiger_count(struct camu_frame_queue *queue) +{ + struct camu_frame_queue_tiger *tq = (struct camu_frame_queue_tiger *)queue; + (void)tq; + return 1; +} + +static u8 queue_tiger_read(struct camu_frame_queue *queue, f64 pts, void *out) +{ + struct camu_frame_queue_tiger *tq = (struct camu_frame_queue_tiger *)queue; + (void)pts; + *((GLuint *)out) = tq->texture_id; + return CAMU_QUEUE_OK; +} + +static void queue_tiger_reset(struct camu_frame_queue *queue) +{ + struct camu_frame_queue_tiger *tq = (struct camu_frame_queue_tiger *)queue; + (void)tq; +} + +static void queue_tiger_free(struct camu_frame_queue **queue) +{ + struct camu_frame_queue_tiger *tq = (struct camu_frame_queue_tiger *)*queue; + glDeleteTextures(1, &tq->texture_id); + al_free(tq); + *queue = NULL; +} + +struct camu_frame_queue *camu_frame_queue_tiger_create(void) +{ + struct camu_frame_queue_tiger *tq = al_alloc_object(struct camu_frame_queue_tiger); + tq->q.push = queue_tiger_push; +#ifdef HAVE_FFMPEG + tq->q.push_av_frame = queue_tiger_push_av_frame; +#endif + tq->q.flush = queue_tiger_flush; + tq->q.count = queue_tiger_count; + tq->q.read = queue_tiger_read; + tq->q.reset = queue_tiger_reset; + tq->q.free = queue_tiger_free; + return (struct camu_frame_queue *)tq; +} diff --git a/src/render/queue_tiger.h b/src/render/queue_tiger.h new file mode 100644 index 0000000..1524a29 --- /dev/null +++ b/src/render/queue_tiger.h @@ -0,0 +1,13 @@ +#pragma once + +#include <skh/egl.h> + +#include "../buffer/frame_queue.h" + +struct camu_frame_queue_tiger { + struct camu_frame_queue q; + GLuint texture_id; + struct camu_renderer_tiger *tr; +}; + +struct camu_frame_queue *camu_frame_queue_tiger_create(void); diff --git a/src/render/renderer.h b/src/render/renderer.h new file mode 100644 index 0000000..61965ed --- /dev/null +++ b/src/render/renderer.h @@ -0,0 +1,45 @@ +#pragma once + +#include <al/types.h> +#include <skh/window.h> +#ifdef HAVE_FFMPEG +#include <libavcodec/avcodec.h> +#endif + +#include "../buffer/frame_queue.h" + +#if defined SEKIHI_API_OPENGL +#define CAMU_RENDERER_OPENGL +#elif defined SEKIHI_API_VULKAN +#define CAMU_RENDERER_VULKAN +#endif + +struct camu_screen; +struct camu_renderer { + bool (*create_renderer)( + struct camu_renderer *, s32 *, s32 *, + bool (*poll)(struct skh_window *, bool), +#if defined CAMU_RENDERER_VULKAN + VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL (*get_vk_proc_address)(VkInstance, const char *), + VkResult (*vk_create_surface)(void *, VkInstance, VkSurfaceKHR *), + const char *const *(*vk_get_extensions)(u32 *), +#elif defined CAMU_RENDERER_OPENGL + void (*get_gl_proc_address(char const *procname))(void), + bool (*gl_load_loader)(void *), + bool (*gl_make_current)(void *), + void (*gl_release_current)(void *), + void (*gl_swap_buffers)(void *), +#elif defined CAMU_RENDERER_DX11 +#endif + void *priv); + struct camu_frame_queue *(*create_queue)(struct camu_renderer *); + void (*resize)(struct camu_renderer *, s32 *, s32 *); + void (*render)(struct camu_renderer *, struct camu_screen *); +#ifdef HAVE_FFMPEG + s32 (*get_buffer2)(struct AVCodecContext *s, AVFrame *frame, s32 flags); + void *opaque; +#endif + void *(*get_device_ptr)(struct camu_renderer *, size_t); + void (*commit_device_ptr)(struct camu_renderer *); + void (*free)(struct camu_renderer **); +}; diff --git a/src/render/renderer_libplacebo.c b/src/render/renderer_libplacebo.c new file mode 100644 index 0000000..019a5ce --- /dev/null +++ b/src/render/renderer_libplacebo.c @@ -0,0 +1,237 @@ +#include <al/lib.h> +#include <al/log.h> +#include <aki/file.h> + +#include "../screen/screen.h" +#include "../util/color_palette.h" + +#include "renderer_libplacebo.h" +#include "queue_libplacebo.h" + +static f32 clear_color[4] = { 0.0f, 0.0f, 0.0f, 1.f }; + +static void renderer_lp_resize(struct camu_renderer *renderer, s32 *width, s32 *height) +{ + struct camu_renderer_lp *lr = (struct camu_renderer_lp *)renderer; + if (lr->swapchain) pl_swapchain_resize(lr->swapchain, width, height); +} + +static bool renderer_lp_create_renderer(struct camu_renderer *renderer, s32 *width, s32 *height, + bool (*poll)(struct skh_window *, bool), +#if defined CAMU_RENDERER_VULKAN + VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL (*get_vk_proc_address)(VkInstance, const char *), + VkResult (*vk_create_surface)(void *, VkInstance, VkSurfaceKHR *), + const char *const *(*vk_get_extensions)(u32 *), +#elif defined CAMU_RENDERER_OPENGL + void (*get_gl_proc_address(char const *procname))(void), + bool (*gl_load_loader)(void *), + bool (*gl_make_current)(void *), + void (*gl_release_current)(void *), + void (*gl_swap_buffers)(void *), +#endif + void *priv) +{ + struct camu_renderer_lp *lr = (struct camu_renderer_lp *)renderer; + + clear_color[0] = ((global_color_palette.colors[0] >> 16) & 0xff) / 255.f; + clear_color[1] = ((global_color_palette.colors[0] >> 8) & 0xff) / 255.f; + clear_color[2] = ((global_color_palette.colors[0]) & 0xff) / 255.f; + + lr->logger = pl_log_create(PL_API_VER, pl_log_params( + .log_cb = pl_log_color, + .log_level = PL_LOG_NONE, +// .log_level = PL_LOG_INFO, + )); + +#if defined CAMU_RENDERER_VULKAN + VkResult err; + + u32 num; + lr->vk_inst = pl_vk_inst_create(lr->logger, pl_vk_inst_params( + .get_proc_addr = get_vk_proc_address, + .debug = false, + .extensions = vk_get_extensions(&num), + .num_extensions = num, + )); + + if (!lr->vk_inst) { + al_log_error("render_libplacebo", "Failed to create vulkan instance."); + } + + err = vk_create_surface(priv, lr->vk_inst->instance, &lr->surface); + if (err != VK_SUCCESS) { + al_log_error("render_libplacebo", "Failed to create vulkan surface."); + } + + lr->vk = pl_vulkan_create(lr->logger, pl_vulkan_params( + .allow_software = true, + .instance = lr->vk_inst->instance, + .get_proc_addr = lr->vk_inst->get_proc_addr, + .surface = lr->surface + )); + + if (!lr->vk) { + al_log_error("render_libplacebo", "Failed to create vulkan device."); + } + + lr->swapchain = pl_vulkan_create_swapchain(lr->vk, pl_vulkan_swapchain_params( + .surface = lr->surface, + .present_mode = VK_PRESENT_MODE_FIFO_KHR + )); + + if (!lr->swapchain) { + al_log_error("render_libplacebo", "Failed to create vulkan swapchain."); + } + + lr->gpu = lr->vk->gpu; +#elif defined CAMU_RENDERER_OPENGL + (void)gl_load_loader; + lr->gl = pl_opengl_create(lr->logger, pl_opengl_params( + .allow_software = true, + .debug = false, + .make_current = gl_make_current, + .release_current = gl_release_current, + .get_proc_addr = get_gl_proc_address, + .get_proc_addr_ex = NULL, + .priv = priv, + )); + + if (!lr->gl) { + al_log_error("render_libplacebo", "Failed to create GL device."); + } + + lr->swapchain = pl_opengl_create_swapchain(lr->gl, pl_opengl_swapchain_params( + .swap_buffers = gl_swap_buffers, + .priv = priv + )); + + if (!lr->swapchain) { + al_log_error("render_libplacebo", "Failed to create GL swapchain."); + } + + lr->gpu = lr->gl->gpu; +#endif + + lr->renderer = pl_renderer_create(lr->logger, lr->gpu); + + renderer_lp_resize(renderer, width, height); + +#define PRE_SWAPS 2 + // See renderer_tiger.c:122. + for (u32 i = 0; i < PRE_SWAPS; i++) { + poll(priv, true); + struct pl_swapchain_frame frame; + pl_swapchain_start_frame(lr->swapchain, &frame); + struct pl_frame target; + pl_frame_from_swapchain(&target, &frame); + pl_frame_clear_rgba(lr->gpu, &target, clear_color); + pl_swapchain_submit_frame(lr->swapchain); + pl_swapchain_swap_buffers(lr->swapchain); + } + + al_memset(&lr->params, 0, sizeof(struct pl_render_params)); + lr->params = pl_render_default_params; + //lr->params = pl_render_high_quality_params; + lr->params.upscaler = &pl_filter_bicubic; + // Clear manually so we can draw multiple images per frame. + lr->params.skip_target_clearing = true; + lr->params.skip_caching_single_frame = true; + +#if 0 + struct aki_file file; + if (aki_file_open(&file, al_str_c(""))) { + char *str; + size_t len = aki_file_read_as_c_str(&file, &str); + const struct pl_hook *hook = pl_mpv_user_shader_parse(lr->gpu, (const char *)str, len); + const struct pl_hook **hooks = al_malloc(sizeof(struct pl_hook *)); + hooks[0] = al_malloc(sizeof(struct pl_hook)); + al_memcpy((void *)hooks[0], (void *)hook, sizeof(struct pl_hook)); + lr->params.hooks = hooks; + lr->params.num_hooks = 1; + } +#endif + +#ifdef HAVE_FFMPEG + //lr->r.get_buffer2 = pl_get_buffer2; + //lr->r.opaque = &lr->gpu; +#endif + + return true; +} + +static struct camu_frame_queue *renderer_lp_create_queue(struct camu_renderer *renderer) +{ + struct camu_renderer_lp *lr = (struct camu_renderer_lp *)renderer; + struct camu_frame_queue *queue = camu_frame_queue_lp_create(); + struct camu_frame_queue_lp *lq = (struct camu_frame_queue_lp *)queue; + lq->queue = pl_queue_create(lr->gpu); + al_memset(&lq->params, 0, sizeof(struct pl_queue_params)); + lq->params.interpolation_threshold = 0.01; + lq->params.radius = pl_frame_mix_radius(&lr->params); + lq->params.timeout = 0; // vsync + return queue; +} + +static void renderer_lp_render(struct camu_renderer *renderer, struct camu_screen *scr) +{ + struct camu_renderer_lp *lr = (struct camu_renderer_lp *)renderer; + + struct pl_swapchain_frame frame; + pl_swapchain_start_frame(lr->swapchain, &frame); + + struct pl_frame target; + pl_frame_from_swapchain(&target, &frame); + +#ifdef CAMU_SCREEN_THREADED + if (al_atomic_bool_load(&scr->queued, AL_ATOMIC_RELAXED)) { + camu_screen_run_queue(scr); + } +#endif + + pl_frame_clear_rgba(lr->gpu, &target, clear_color); + + struct pl_frame_mix mix; + if (scr->videos.size > 0) { + struct camu_screen_video *video = &al_array_last(scr->videos); + //al_array_foreach_ptr(scr->videos, i, video) { + if (camu_video_buffer_read(video->buf, &mix) && mix.frames) { + target.crop = mix.frames[0]->crop; + target.crop.x1 *= video->view.zoom; + target.crop.y1 *= video->view.zoom; + target.crop.x0 += video->view.x_offset; + target.crop.y0 += video->view.y_offset; + target.crop.x1 += video->view.x_offset; + target.crop.y1 += video->view.y_offset; + pl_render_image_mix(lr->renderer, &mix, &target, &lr->params); + } + //} + } + + pl_swapchain_submit_frame(lr->swapchain); + pl_swapchain_swap_buffers(lr->swapchain); +} + +static void renderer_lp_free(struct camu_renderer **renderer) +{ + struct camu_renderer_lp *lr = (struct camu_renderer_lp *)*renderer; + if (lr->renderer) pl_renderer_destroy(&lr->renderer); + if (lr->swapchain) pl_swapchain_destroy(&lr->swapchain); +#if defined CAMU_RENDERER_VULKAN +#elif defined CAMU_RENDERER_OPENGL + if (lr->gl) pl_opengl_destroy(&lr->gl); +#endif + if (lr->logger) pl_log_destroy(&lr->logger); + al_free(lr); + *renderer = NULL; +} + +struct camu_renderer *camu_renderer_lp_create(void) +{ + struct camu_renderer_lp *lr = al_alloc_object(struct camu_renderer_lp); + lr->r.create_renderer = renderer_lp_create_renderer; + lr->r.create_queue = renderer_lp_create_queue; + lr->r.resize = renderer_lp_resize; + lr->r.render = renderer_lp_render; + lr->r.free = renderer_lp_free; + return (struct camu_renderer *)lr; +} diff --git a/src/render/renderer_libplacebo.h b/src/render/renderer_libplacebo.h new file mode 100644 index 0000000..661faff --- /dev/null +++ b/src/render/renderer_libplacebo.h @@ -0,0 +1,38 @@ +#pragma once + +#include <libplacebo/common.h> +#include <libplacebo/opengl.h> +#include <libplacebo/renderer.h> +#include <libplacebo/shaders/lut.h> +#include <libplacebo/colorspace.h> + +#include "renderer.h" + +#if defined CAMU_RENDERER_VULKAN +#include <libplacebo/vulkan.h> +#endif + +struct camu_renderer_lp_resource { + pl_tex tex; + struct pl_plane plane; +}; + +struct camu_renderer_lp { + struct camu_renderer r; +#if defined CAMU_RENDERER_VULKAN + VkSurfaceKHR surface; + pl_vulkan vk; + pl_vk_inst vk_inst; +#elif defined CAMU_RENDERER_OPENGL + pl_opengl gl; +#endif + pl_gpu gpu; + pl_log logger; + pl_swapchain swapchain; + pl_renderer renderer; + struct pl_render_params params; + struct pl_overlay overlays[10]; + s32 overlay_count; +}; + +struct camu_renderer *camu_renderer_lp_create(void); diff --git a/src/render/renderer_tiger.c b/src/render/renderer_tiger.c new file mode 100644 index 0000000..7cbbbb9 --- /dev/null +++ b/src/render/renderer_tiger.c @@ -0,0 +1,305 @@ +#include <al/lib.h> +#include <al/log.h> +#include <linmath.h> + +#include "../screen/view.h" +#include "../screen/screen.h" +#include "../util/color_palette.h" + +#include "renderer_tiger.h" +#include "queue_tiger.h" + +#ifdef CAMU_RENDERER_OPENGL +static const GLfloat default_vertices[] = { + 0.f, 0.f, 0.f, 0.f, 0.f, + 0.f, 1.f, 0.f, 0.f, 1.f, + 1.f, 0.f, 0.f, 1.f, 0.f, + 1.f, 0.f, 0.f, 1.f, 0.f, + 0.f, 1.f, 0.f, 0.f, 1.f, + 1.f, 1.f, 0.f, 1.f, 1.f +}; + +static f32 clear_color[4] = { 0.0f, 0.0f, 0.0f, 1.f }; + +static const GLchar *default_vs = \ + "#version 100\n" \ + "attribute vec3 a_Position;\n" \ + "attribute vec2 a_TexCoord;\n" \ + "varying vec2 v_TexCoord;\n" \ + "uniform mat4 g_ModelViewProjectionMatrix;\n" \ + "void main() {\n" \ + " gl_Position = g_ModelViewProjectionMatrix * vec4(a_Position, 1.0);\n" \ + " v_TexCoord = a_TexCoord;\n" \ + "}"; + +static const GLchar *default_fs = \ + "#version 100\n" \ + "precision mediump float;\n" \ + "varying vec2 v_TexCoord;\n" \ + "uniform sampler2D g_Texture0;\n" \ + "void main() {\n" \ + " gl_FragColor = texture2D(g_Texture0, v_TexCoord);\n" \ + "}"; + +static bool compile(GLuint shader, const GLchar *source, GLchar *error) +{ + glShaderSource(shader, 1, &source, 0); + glCompileShader(shader); + + GLint status; + + glGetShaderiv(shader, GL_COMPILE_STATUS, &status); + + if (status == GL_FALSE) { + glGetShaderInfoLog(shader, 256, NULL, error); + return false; + } + + return true; +} + +static bool compile_shaders(struct camu_renderer_tiger *tr) +{ + tr->program = glCreateProgram(); + + GLchar error[256]; + + GLuint vs_shader = glCreateShader(GL_VERTEX_SHADER); + if (!compile(vs_shader, default_vs, error)) { + al_log_error("render_tiger", "Failed to compile vertex shader (%.*s)", + sizeof(error), error); + return false; + } + + GLuint fs_shader = glCreateShader(GL_FRAGMENT_SHADER); + if (!compile(fs_shader, default_fs, error)) { + al_log_error("render_tiger", "Failed to compile fragment shader (%.*s)", + sizeof(error), error); + return false; + } + + glAttachShader(tr->program, vs_shader); + glAttachShader(tr->program, fs_shader); + glLinkProgram(tr->program); + + return true; +} +#endif + +static void renderer_tiger_resize(struct camu_renderer *renderer, s32 *width, s32 *height) +{ + struct camu_renderer_tiger *tr = (struct camu_renderer_tiger *)renderer; + (void)tr; +#ifdef CAMU_RENDERER_OPENGL + glViewport(0, 0, *width, *height); +#endif +} + +static bool renderer_tiger_create_renderer(struct camu_renderer *renderer, s32 *width, s32 *height, + bool (*poll)(struct skh_window *, bool), +#if defined CAMU_RENDERER_VULKAN + VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL (*get_vk_proc_address)(VkInstance, const char *), + void (*vk_create_surface)(void *, VkInstance, VkSurfaceKHR *), + const char *const *(*vk_get_extensions)(u32 *), +#elif defined CAMU_RENDERER_OPENGL + void (*get_gl_proc_address(char const *procname))(void), + bool (*gl_load_loader)(void *), + bool (*gl_make_current)(void *), + void (*gl_release_current)(void *), + void (*gl_swap_buffers)(void *), +#endif + void *priv) +{ + struct camu_renderer_tiger *tr = (struct camu_renderer_tiger *)renderer; +#ifdef CAMU_RENDERER_OPENGL + (void)get_gl_proc_address; + + tr->load_loader = gl_load_loader; + tr->make_current = gl_make_current; + tr->release_current = gl_release_current; + tr->swap_buffers = gl_swap_buffers; + tr->priv = priv; + + tr->make_current(tr->priv); + + if (!tr->load_loader(tr->priv)) return false; + + renderer_tiger_resize(renderer, width, height); + + clear_color[0] = ((global_color_palette.colors[0] >> 16) & 0xff) / 255.f; + clear_color[1] = ((global_color_palette.colors[0] >> 8) & 0xff) / 255.f; + clear_color[2] = ((global_color_palette.colors[0]) & 0xff) / 255.f; + + glClearColor(clear_color[0], clear_color[1], clear_color[2], 1.f); + +#define PRE_SWAPS 2 + // Hack? to make the window appear quicker. Only tested on linux/wayland. + for (u32 i = 0; i < PRE_SWAPS; i++) { + poll(priv, true); + glClear(GL_COLOR_BUFFER_BIT); + tr->swap_buffers(tr->priv); + } + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + glGenBuffers(1, &tr->vbo); + glBindBuffer(GL_ARRAY_BUFFER, tr->vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(default_vertices), default_vertices, GL_STATIC_DRAW); + + if (!compile_shaders(tr)) return false; + + glUseProgram(tr->program); + + GLint pos = glGetAttribLocation(tr->program, "a_Position"); + glVertexAttribPointer(pos, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (void *)0); + glEnableVertexAttribArray(pos); + GLint texcoord = glGetAttribLocation(tr->program, "a_TexCoord"); + glVertexAttribPointer(texcoord, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (void *)12); + glEnableVertexAttribArray(texcoord); + + //glGenBuffers(1, &tr->texture_buffer); +#endif + +#ifdef HAVE_FFMPEG + tr->r.get_buffer2 = NULL; + tr->r.opaque = NULL; +#endif + + return true; +} + +static struct camu_frame_queue *renderer_tiger_create_queue(struct camu_renderer *renderer) +{ +#ifdef CAMU_RENDERER_OPENGL + struct camu_renderer_tiger *tr = (struct camu_renderer_tiger *)renderer; + struct camu_frame_queue *q = camu_frame_queue_tiger_create(); + struct camu_frame_queue_tiger *tq = (struct camu_frame_queue_tiger *)q; + tq->tr = tr; + return q; +#endif +} + +static void set_uniform(struct camu_renderer_tiger *tr, const GLchar *name, + u8 type, void *value) +{ +#ifdef CAMU_RENDERER_OPENGL + GLint loc = glGetUniformLocation(tr->program, name); + if (loc == -1) return; + + switch (type) + { + case TYPE_FLOAT: + glUniform1f(loc, *((GLfloat *)value)); + break; + case TYPE_SAMPLER2D: + glUniform1i(loc, (GLuint)(*((GLfloat *)value))); + break; + case TYPE_VEC2: + glUniform2fv(loc, 1, (GLfloat *)value); + break; + case TYPE_VEC3: + glUniform3fv(loc, 1, (GLfloat *)value); + break; + case TYPE_VEC4: + glUniform4fv(loc, 1, (GLfloat *)value); + break; + case TYPE_MAT4: + glUniformMatrix4fv(loc, 1, GL_FALSE, (GLfloat *)value); + break; + case TYPE_MAT3: + glUniformMatrix3fv(loc, 1, GL_FALSE, (GLfloat *)value); + break; + case TYPE_INVALID: + break; + } +#endif +} + +#ifdef CAMU_RENDERER_OPENGL +static mat4x4 model; +static GLuint texture0 = 0; +#endif + +static void renderer_tiger_render(struct camu_renderer *renderer, struct camu_screen *scr) +{ +#ifdef CAMU_RENDERER_OPENGL + struct camu_renderer_tiger *tr = (struct camu_renderer_tiger *)renderer; + + glClear(GL_COLOR_BUFFER_BIT); + + glUseProgram(tr->program); + glBindBuffer(GL_ARRAY_BUFFER, tr->vbo); + glActiveTexture(GL_TEXTURE0); + +#ifdef CAMU_SCREEN_THREADED + if (al_atomic_bool_load(&scr->queued, AL_ATOMIC_RELAXED)) { + camu_screen_run_queue(scr); + } +#endif + + if (scr->videos.size > 0) { + struct camu_screen_video *video = &al_array_last(scr->videos); + + GLuint texture_id; + camu_video_buffer_read(video->buf, &texture_id); + glBindTexture(GL_TEXTURE_2D, texture_id); + + mat4x4_ortho(model, 0.f, (f32)scr->width, (f32)scr->height, 0.f, -1.f, 1.f); + mat4x4_translate_in_place(model, (f32)video->view.x_offset, (f32)video->view.y_offset, 0.f); + mat4x4_scale_aniso(model, model, (f32)video->view.zoom, (f32)video->view.zoom, 1.f); + mat4x4_scale_aniso(model, model, (f32)video->view.width, (f32)video->view.height, 1.f); + set_uniform(tr, "g_ModelViewProjectionMatrix", TYPE_MAT4, model); + set_uniform(tr, "g_Texture0", TYPE_SAMPLER2D, &texture0); + + glDrawArrays(GL_TRIANGLES, 0, 6); + } + + tr->swap_buffers(tr->priv); +#endif +} + +static void *renderer_tiger_get_device_ptr(struct camu_renderer *renderer, size_t length) +{ + (void)renderer; + (void)length; + return NULL; +#ifdef CAMU_RENDERER_OPENGL + //struct camu_renderer_tiger *tr = (struct camu_renderer_tiger *)renderer; + //glBindBuffer(GL_PIXEL_UNPACK_BUFFER, tr->texture_buffer); + //glBufferData(GL_PIXEL_UNPACK_BUFFER, length, NULL, GL_STREAM_DRAW); + //return glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, length, GL_MAP_WRITE_BIT); +#endif +} + +static void renderer_tiger_commit_device_ptr(struct camu_renderer *renderer) +{ + (void)renderer; +#ifdef CAMU_RENDERER_OPENGL + //struct camu_renderer_tiger *tr = (struct camu_renderer_tiger *)renderer; + //glBindBuffer(GL_PIXEL_UNPACK_BUFFER, tr->texture_buffer); + //glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); +#endif +} + +static void renderer_tiger_free(struct camu_renderer **renderer) +{ + struct camu_renderer_tiger *tr = (struct camu_renderer_tiger *)*renderer; + glDeleteProgram(tr->program); + glDeleteBuffers(1, &tr->vbo); + al_free(tr); + *renderer = NULL; +} + +struct camu_renderer *camu_renderer_tiger_create(void) +{ + struct camu_renderer_tiger *tr = al_alloc_object(struct camu_renderer_tiger); + tr->r.create_renderer = renderer_tiger_create_renderer; + tr->r.create_queue = renderer_tiger_create_queue; + tr->r.resize = renderer_tiger_resize; + tr->r.render = renderer_tiger_render; + tr->r.get_device_ptr = renderer_tiger_get_device_ptr; + tr->r.commit_device_ptr = renderer_tiger_commit_device_ptr; + tr->r.free = renderer_tiger_free; + return (struct camu_renderer *)tr; +} diff --git a/src/render/renderer_tiger.h b/src/render/renderer_tiger.h new file mode 100644 index 0000000..e9a7eb0 --- /dev/null +++ b/src/render/renderer_tiger.h @@ -0,0 +1,31 @@ +#pragma once + +#include <skh/egl.h> + +#include "renderer.h" + +enum { + TYPE_INVALID = 0, + TYPE_FLOAT, + TYPE_VEC4, + TYPE_VEC3, + TYPE_VEC2, + TYPE_MAT4, + TYPE_MAT3, + TYPE_SAMPLER2D +}; + +struct camu_renderer_tiger { + struct camu_renderer r; + GLuint vao; + GLuint vbo; + GLuint program; + GLuint texture_buffer; + bool (*load_loader)(void *); + bool (*make_current)(void *); + void (*release_current)(void *); + void (*swap_buffers)(void *); + void *priv; +}; + +struct camu_renderer *camu_renderer_tiger_create(void); diff --git a/src/screen/meson.build b/src/screen/meson.build new file mode 100644 index 0000000..cd78988 --- /dev/null +++ b/src/screen/meson.build @@ -0,0 +1,4 @@ +screen_src = ['screen.c', 'view.c'] +sekihi = subproject('sekihi').get_variable('sekihi') +screen = declare_dependency(sources: screen_src, + dependencies: [sekihi]) diff --git a/src/screen/screen.c b/src/screen/screen.c new file mode 100644 index 0000000..82c7b12 --- /dev/null +++ b/src/screen/screen.c @@ -0,0 +1,402 @@ +#include <aki/thread.h> +#include <al/log.h> + +#include "screen.h" + +#define ZOOM_MAX 100.0 +#define ZOOM_MIN 0.02 + +#define PAN_BOUNDS 35.0 + +#define X_IN_BOUNDS(scr, view, x) \ + ((x + PAN_BOUNDS <= scr->width) && (PAN_BOUNDS - x <= view->width * view->zoom)) +#define Y_IN_BOUNDS(scr, view, y) \ + ((y + PAN_BOUNDS <= scr->height) && (PAN_BOUNDS - y <= view->height * view->zoom)) + +#define CAMU_DEFAULT_VIEW CAMU_VIEW_AUTOFIT + +static struct camu_view *get_view_from_mouse_pos(struct camu_screen *scr) +{ + struct camu_view *view = NULL; + struct camu_screen_video *video; + al_array_foreach_ptr(scr->videos, i, video) { + if (!view) { + view = &video->view; + continue; + } + if (view->zindex < video->view.zindex) { + view = &video->view; + } + } + return view; +} + +static void should_close_callback(void *userdata) +{ + struct camu_screen *scr = (struct camu_screen *)userdata; + scr->callback(scr->userdata, CAMU_SCREEN_CLOSE, 0.0); +} + +static void resize_callback(void *userdata, s32 width, s32 height) +{ + struct camu_screen *scr = (struct camu_screen *)userdata; + if (scr->renderer) { + scr->renderer->resize(scr->renderer, &width, &height); + } + scr->width = width; + scr->height = height; + struct camu_screen_video *video; + al_array_foreach_ptr(scr->videos, i, video) { + camu_view_calculate(&video->view, scr->width, scr->height); + } +} + +static void pointer_pos_callback(void *userdata, f64 x, f64 y) +{ + struct camu_screen *scr = (struct camu_screen *)userdata; + struct camu_view *view = get_view_from_mouse_pos(scr); + if (view) { + if (scr->flags & CAMU_SCREEN_DRAGGING) { + if (scr->flags & CAMU_SCREEN_ZOOM_PAN_SIMPLE) { + f64 nx = view->x_offset + x - scr->last_mouse_x; + if (X_IN_BOUNDS(scr, view, nx)) { + view->x_offset = nx; + } + f64 ny = view->y_offset + y - scr->last_mouse_y; + if (Y_IN_BOUNDS(scr, view, ny)) { + view->y_offset = ny; + } + view->mode = CAMU_VIEW_DETACHED; + } + scr->last_click_ts = 0; + } + } + scr->last_mouse_x = x; + scr->last_mouse_y = y; +} + +static void mouse_button_callback(void *userdata, u8 state, u8 button) +{ + struct camu_screen *scr = (struct camu_screen *)userdata; + switch (button) { + case SEKIHI_MOUSE1: + switch (state) { + case SEKIHI_BUTTON_PRESSED: + scr->flags |= CAMU_SCREEN_DRAGGING; + scr->last_click_ts = aki_get_timestamp(); + break; + case SEKIHI_BUTTON_RELEASED: + scr->flags &= ~CAMU_SCREEN_DRAGGING; + if (scr->last_click_ts && aki_get_timestamp() - scr->last_click_ts <= 300000) { + if (scr->flags & CAMU_SCREEN_MODIFIER) { + scr->callback(scr->userdata, CAMU_SCREEN_SEEK, scr->last_mouse_x / scr->width); + } else { + if (scr->last_mouse_x >= scr->width / 2.f) { + scr->callback(scr->userdata, CAMU_SCREEN_NEXT, 0.0); + } else { + scr->callback(scr->userdata, CAMU_SCREEN_PREVIOUS, 0.0); + } + } + } + break; + } + break; + case SEKIHI_MOUSE2: + break; // Move window. + default: + break; + } +} + +static void zoom_on_point(struct camu_screen *scr, struct camu_view *view, f64 x, f64 y, f64 v) +{ + x -= view->x_offset; + y -= view->y_offset; + x /= view->zoom; + y /= view->zoom; + + f64 prev_width = scr->width * view->zoom; + f64 prev_height = scr->height * view->zoom; + + f64 x_ratio = x / (f64)scr->width; + f64 y_ratio = y / (f64)scr->height; + + f64 zoom = view->zoom + (v * view->zoom); + if (zoom > ZOOM_MAX || zoom < ZOOM_MIN) return; + + f64 nx = view->x_offset - x_ratio * ((scr->width * zoom) - prev_width); + f64 ny = view->y_offset - y_ratio * ((scr->height * zoom) - prev_height); + if (X_IN_BOUNDS(scr, view, nx) && Y_IN_BOUNDS(scr, view, ny)) { + view->x_offset = nx; + view->y_offset = ny; + view->zoom = zoom; + } +} + +static void scroll_callback(void *userdata, f64 y) +{ + struct camu_screen *scr = (struct camu_screen *)userdata; + + struct camu_view *view = get_view_from_mouse_pos(scr); + if (!view) return; + + if (scr->flags & CAMU_SCREEN_ZOOM_PAN_SIMPLE) { + y = -y; +#if defined SEKIHI_WINDOW_GLFW + y /= 4.5; +#elif defined SEKIHI_WINDOW_WAYLAND + y /= 100.0; +#else + y /= 14.0; +#endif + zoom_on_point(scr, view, scr->last_mouse_x, scr->last_mouse_y, y); + view->mode = CAMU_VIEW_DETACHED; + } +} + +static void key_callback(void *userdata, u8 state, u8 button) +{ + struct camu_screen *scr = (struct camu_screen *)userdata; + switch (state) { + case SEKIHI_BUTTON_PRESSED: + switch (button) { + case 0x10: + case 0x18: + case 'q': + scr->callback(scr->userdata, CAMU_SCREEN_CLOSE, 0.0); + break; + case 0x6a: + case 0x31: + case 'n': + scr->callback(scr->userdata, CAMU_SCREEN_NEXT, 0.0); + break; + case 0x69: + case 0x30: + case 'b': + scr->callback(scr->userdata, CAMU_SCREEN_PREVIOUS, 0.0); + break; + case 0x41: + case 0x39: + scr->callback(scr->userdata, CAMU_SCREEN_TOGGLE_PAUSE, 0.0); + break; + case 0x13: + case 0x1b: + case 'r': { + struct camu_view *view = get_view_from_mouse_pos(scr); + if (!view) return; + view->mode = CAMU_DEFAULT_VIEW; + camu_view_calculate(view, scr->width, scr->height); + break; + } + case 0x1d: + scr->flags |= CAMU_SCREEN_MODIFIER; + break; + default: + break; + } + break; + case SEKIHI_BUTTON_RELEASED: + switch (button) { + case 0x1d: + scr->flags &= ~CAMU_SCREEN_MODIFIER; + break; + } + default: + break; + } +} + +static void key_immediate_callback(void *userdata, u8 state, u8 button) +{ + struct camu_screen *scr = (struct camu_screen *)userdata; + switch (state) { + case SEKIHI_BUTTON_PRESSED: + switch (button) { + case 0x29: + case 0x21: + scr->window->toggle_fullscreen(scr->window); + break; + } + break; + default: + break; + } +} + +bool camu_screen_init(struct camu_screen *scr) +{ + al_atomic_s32_store(&scr->state, CAMU_SCREEN_PAUSED, AL_ATOMIC_RELAXED); + al_array_init(scr->videos); + scr->renderer = NULL; + scr->window = skh_window_create(); + scr->window->pointer_pos_callback = pointer_pos_callback; + scr->window->scroll_callback = scroll_callback; + scr->window->mouse_button_callback = mouse_button_callback; + scr->window->key_callback = key_callback; + scr->window->key_immediate_callback = key_immediate_callback; + scr->window->resize_callback = resize_callback; + scr->window->should_close_callback = should_close_callback; + scr->window->userdata = scr; +#ifdef CAMU_SCREEN_THREADED + al_array_init(scr->add_queue); + al_array_init(scr->rem_queue); + al_atomic_bool_store(&scr->queued, false, AL_ATOMIC_RELAXED); + aki_mutex_init(&scr->mutex); +#endif + scr->last_mouse_x = 0; + scr->last_mouse_y = 0; + scr->flags = CAMU_SCREEN_ZOOM_PAN_SIMPLE; + return true; +} + +bool camu_screen_create_window(struct camu_screen *scr, const char *name) +{ + if (!scr->window->create_window(scr->window, CAMU_SCREEN_WIDTH, CAMU_SCREEN_HEIGHT, name)) { + return false; + } + scr->width = scr->window->width; + scr->height = scr->window->height; + return true; +} + +bool camu_screen_create_renderer(struct camu_screen *scr, struct camu_renderer *renderer) +{ + scr->renderer = renderer; + return renderer->create_renderer(renderer, &scr->width, &scr->height, + scr->window->poll, +#if defined CAMU_RENDERER_VULKAN + scr->window->get_vk_proc_address, + scr->window->vk_create_surface, + scr->window->vk_get_extensions, +#elif defined CAMU_RENDERER_OPENGL + scr->window->get_gl_proc_address, + scr->window->gl_load_loader, + scr->window->gl_make_current, + scr->window->gl_release_current, + scr->window->gl_swap_buffers, +#endif + scr->window); +} + +static void add_buffer_internal(struct camu_screen *scr, struct camu_video_buffer *buf) +{ + struct camu_stream *stream = buf->stream; + struct camu_screen_video video = { + .buf = buf, + .view = { + .mode = CAMU_DEFAULT_VIEW, + .width = stream->video.width, + .height = stream->video.height, + .zindex = 0 + } + }; +#ifdef CAMU_SCREEN_THREADED + al_atomic_bool_store(&buf->ref, true, AL_ATOMIC_RELAXED); +#endif + camu_view_calculate(&video.view, scr->width, scr->height); + al_array_push(scr->videos, video); + scr->window->resize(scr->window, stream->video.width, stream->video.height); +} + +static void remove_buffer_internal(struct camu_screen *scr, struct camu_video_buffer *buf) +{ + struct camu_screen_video *video; + al_array_foreach_ptr(scr->videos, i, video) { + if (video->buf == buf) { +#ifdef CAMU_SCREEN_THREADED + al_atomic_bool_store(&buf->ref, false, AL_ATOMIC_RELAXED); +#endif + al_array_remove_at_iter(scr->videos, i); + break; + } + } +} + +void camu_screen_add_buffer(struct camu_screen *scr, struct camu_video_buffer *buf) +{ +#ifdef CAMU_SCREEN_THREADED + aki_mutex_lock(&scr->mutex); + al_array_push(scr->add_queue, buf); + al_array_reserve(scr->videos, scr->videos.size + scr->add_queue.size); + al_atomic_bool_store(&scr->queued, true, AL_ATOMIC_RELAXED); + aki_mutex_unlock(&scr->mutex); +#else + add_buffer_internal(scr, buf); +#endif + camu_screen_wake(scr); +} + +#ifdef CAMU_SCREEN_THREADED +static void run_queue_internal(struct camu_screen *scr) +{ + struct camu_video_buffer *buf; + al_array_foreach(scr->add_queue, i, buf) { + add_buffer_internal(scr, buf); + al_array_remove_at_iter(scr->add_queue, i); + } + al_array_foreach(scr->rem_queue, i, buf) { + remove_buffer_internal(scr, buf); + al_array_remove_at_iter(scr->rem_queue, i); + } +} +#endif + +void camu_screen_remove_buffer(struct camu_screen *scr, struct camu_video_buffer *buf) +{ +#ifdef CAMU_SCREEN_THREADED + aki_mutex_lock(&scr->mutex); + al_array_push(scr->rem_queue, buf); + al_atomic_bool_store(&scr->queued, true, AL_ATOMIC_RELAXED); + if (al_atomic_s32_load(&scr->state, AL_ATOMIC_RELAXED) == CAMU_SCREEN_CLOSED) { + run_queue_internal(scr); + } + aki_mutex_unlock(&scr->mutex); +#else + remove_buffer_internal(scr, buf); +#endif + camu_screen_wake(scr); +} + +#ifdef CAMU_SCREEN_THREADED +void camu_screen_run_queue(struct camu_screen *scr) +{ + aki_mutex_lock(&scr->mutex); + run_queue_internal(scr); + al_atomic_bool_store(&scr->queued, false, AL_ATOMIC_RELAXED); + aki_mutex_unlock(&scr->mutex); +} +#endif + +void camu_screen_set_state(struct camu_screen *scr, u8 state) +{ + al_atomic_s32_store(&scr->state, state, AL_ATOMIC_RELAXED); +} + +bool camu_screen_tick(struct camu_screen *scr) +{ +#if defined SEKIHI_POLL_CONSERVATIVE + scr->window->poll(scr->window, + al_atomic_s32_load(&scr->state, AL_ATOMIC_RELAXED) == CAMU_SCREEN_PAUSED); + return skh_window_read_events(scr->window); +#elif defined SEKIHI_POLL_INLINE + return scr->window->poll(scr->window, + al_atomic_s32_load(&scr->state, AL_ATOMIC_RELAXED) == CAMU_SCREEN_PAUSED); +#endif +} + +void camu_screen_wake(struct camu_screen *scr) +{ +#ifdef SEKIHI_PAUSE + scr->window->wake(scr->window); +#endif +} + +void camu_screen_close(struct camu_screen *scr) +{ +#ifdef CAMU_SCREEN_THREADED + al_array_free(scr->add_queue); + al_array_free(scr->rem_queue); + aki_mutex_destroy(&scr->mutex); +#endif + al_array_free(scr->videos); +} diff --git a/src/screen/screen.h b/src/screen/screen.h new file mode 100644 index 0000000..d45d5ee --- /dev/null +++ b/src/screen/screen.h @@ -0,0 +1,78 @@ +#pragma once + +#define CAMU_SCREEN_THREADED + +#include <al/array.h> +#ifdef CAMU_SCREEN_THREADED +#include <al/atomic.h> +#include <aki/thread.h> +#endif +#include <skh/window.h> + +#include "../buffer/video.h" +#include "../render/renderer.h" +#include "../codec/codec.h" + +#include "view.h" + +#define CAMU_SCREEN_WIDTH 700 +#define CAMU_SCREEN_HEIGHT 700 + +enum { + CAMU_SCREEN_ZOOM_PAN_SIMPLE = 1, + CAMU_SCREEN_DRAGGING = 1 << 1, + CAMU_SCREEN_MODIFIER = 1 << 2, +}; + +enum { + CAMU_SCREEN_PAUSED = 0, + CAMU_SCREEN_PLAYING, + CAMU_SCREEN_CLOSED +}; + +enum { + CAMU_SCREEN_TOGGLE_PAUSE = 0, + CAMU_SCREEN_NEXT, + CAMU_SCREEN_PREVIOUS, + CAMU_SCREEN_SEEK, + CAMU_SCREEN_CLOSE +}; + +struct camu_screen_video { + struct camu_view view; + struct camu_video_buffer *buf; +}; + +struct camu_screen { + atomic_s32 state; + u32 flags; + struct skh_window *window; + struct camu_renderer *renderer; + s32 width; + s32 height; + u64 last_click_ts; + f64 last_mouse_y; + f64 last_mouse_x; + array(struct camu_screen_video) videos; +#ifdef CAMU_SCREEN_THREADED + array(struct camu_video_buffer *) add_queue; + array(struct camu_video_buffer *) rem_queue; + atomic_bool queued; + struct aki_mutex mutex; +#endif + void (*callback)(void *, u8, f64); + void *userdata; +}; + +bool camu_screen_init(struct camu_screen *scr); +bool camu_screen_create_window(struct camu_screen *scr, const char *name); +bool camu_screen_create_renderer(struct camu_screen *scr, struct camu_renderer *renderer); +void camu_screen_add_buffer(struct camu_screen *scr, struct camu_video_buffer *buf); +void camu_screen_remove_buffer(struct camu_screen *scr, struct camu_video_buffer *buf); +#ifdef CAMU_SCREEN_THREADED +void camu_screen_run_queue(struct camu_screen *scr); +#endif +void camu_screen_set_state(struct camu_screen *scr, u8 state); +bool camu_screen_tick(struct camu_screen *scr); +void camu_screen_wake(struct camu_screen *scr); +void camu_screen_close(struct camu_screen *scr); diff --git a/src/screen/view.c b/src/screen/view.c new file mode 100644 index 0000000..cf23ed4 --- /dev/null +++ b/src/screen/view.c @@ -0,0 +1,28 @@ +#include "view.h" + +void camu_view_calculate(struct camu_view *view, s32 window_width, s32 window_height) +{ + f64 ratio = window_width / (f64)window_height; + f64 frame_ratio = view->width / (f64)view->height; + view->ratio = ratio / frame_ratio; + switch (view->mode) { + case CAMU_VIEW_AUTOFIT: + if (frame_ratio > ratio) { + view->zoom = (window_width / (f64)view->width); + } else { + view->zoom = (window_height / (f64)view->height); + } + break; + case CAMU_VIEW_ZOOM: + if (frame_ratio < ratio) { + view->zoom = (window_width / (f64)view->width); + } else { + view->zoom = (window_height / (f64)view->height); + } + break; + default: + return; + } + view->x_offset = ((window_width - (view->width * view->zoom)) / 2.0); + view->y_offset = ((window_height - (view->height * view->zoom)) / 2.0); +} diff --git a/src/screen/view.h b/src/screen/view.h new file mode 100644 index 0000000..0546062 --- /dev/null +++ b/src/screen/view.h @@ -0,0 +1,23 @@ +#pragma once + +#include <al/types.h> + +enum { + CAMU_VIEW_HIDDEN = 0, + CAMU_VIEW_AUTOFIT, + CAMU_VIEW_ZOOM, + CAMU_VIEW_DETACHED +}; + +struct camu_view { + u8 mode; + s32 width; + s32 height; + f64 ratio; + f64 zoom; + f64 x_offset; + f64 y_offset; + s32 zindex; +}; + +void camu_view_calculate(struct camu_view *view, s32 window_width, s32 window_height); diff --git a/src/shoki/cpy/lib.pxd b/src/shoki/cpy/lib.pxd new file mode 100644 index 0000000..5749141 --- /dev/null +++ b/src/shoki/cpy/lib.pxd @@ -0,0 +1,13 @@ +include "types.pxd" + +cdef extern from "<al/lib.h>": + void al_free(void *ptr) + void al_malloc(size_t size) + void al_strlen(char *s) + void al_bzero(void *s, size_t n) + +cdef extern from "<al/log.h>": + s32 al_log_info(const char *ns, const char *, ...) + s32 al_log_warn(const char *ns, const char *, ...) + s32 al_log_error(const char *ns, const char *, ...) + s32 al_log_debug(const char *ns, const char *, ...) diff --git a/src/shoki/cpy/post.pxd b/src/shoki/cpy/post.pxd new file mode 100644 index 0000000..0b0a97e --- /dev/null +++ b/src/shoki/cpy/post.pxd @@ -0,0 +1,58 @@ +include "str.pxd" + +cdef extern from "../../shoki/src/post.h": + ctypedef struct sho_optional_int: + s64 i + bool set + + cdef struct sho_post_date: + u8 type + s64 timestamp + + cdef struct sho_post_media: + u8 type + str url + str thumbnail_url + + ctypedef struct sho_dates_array: + u32 size + u32 alloc + sho_post_date *data + + ctypedef struct sho_media_array: + u32 size + u32 alloc + sho_post_media *data + + cdef struct sho_post_user: + str unique_id + str username + str display_name + str profile_image_url + + cdef struct sho_post_ref: + str unique_id + + cdef struct sho_post: + u16 version + u8 type + str unique_id + str raw_responses + str url + sho_dates_array dates + sho_post_user author + str title + str text + sho_optional_int likes + sho_optional_int reposts + sho_optional_int quotes + sho_optional_int comments + sho_optional_int views + sho_media_array media + sho_post_ref post + sho_post_ref quoted + sho_post_ref in_reply_to + + void sho_post_reset(sho_post *post) + void sho_post_add_media(sho_post *post, u8 type, str *url, str *thumbnail_url) + void sho_post_add_date(sho_post *post, u8 type, s64 timestamp) diff --git a/src/shoki/cpy/result.pxd b/src/shoki/cpy/result.pxd new file mode 100644 index 0000000..1bebb55 --- /dev/null +++ b/src/shoki/cpy/result.pxd @@ -0,0 +1,8 @@ +include "post.pxd" + +cdef extern from "../../shoki/src/search.h": + cdef struct sho_result: + pass + + void sho_result_add_post(sho_result *res, s32 page, sho_post *post) + void sho_result_add_to_list(sho_result *res, s32 page, str *unique_id) diff --git a/src/shoki/cpy/setup.py b/src/shoki/cpy/setup.py new file mode 100644 index 0000000..1b491f1 --- /dev/null +++ b/src/shoki/cpy/setup.py @@ -0,0 +1,6 @@ +from Cython.Build import cythonize +from Cython.Compiler import Options +from setuptools import Extension +Options.embed = True +alabaster_inc = "../../../subprojects/libalabaster/include" +cythonize([Extension("shoki", ["shoki.pyx"], include_dirs=[alabaster_inc], extra_link_args=[])], language_level=3) diff --git a/src/shoki/cpy/shoki.pyx b/src/shoki/cpy/shoki.pyx new file mode 100644 index 0000000..6d1dd56 --- /dev/null +++ b/src/shoki/cpy/shoki.pyx @@ -0,0 +1,132 @@ +cimport lib +cimport str +cimport post +cimport result + +import sys +sys.path.append('./py') + +cdef public int log_info(char *msg) except -1: + return lib.al_log_info("shoki", "%s", msg) + +cdef public int log_warn(char *msg) except -1: + return lib.al_log_warn("shoki", "%s", msg) + +cdef public int log_error(char *msg) except -1: + return lib.al_log_error("shoki", "%s", msg) + +cdef public int log_debug(char *msg) except -1: + return lib.al_log_debug("shoki", "%s", msg) + +def encode_str(str): + return str.encode('UTF-8') + +def decode_str(str): + return str.decode('UTF-8') + +class OutputHook(object): + def __init__(self): + self.fd = sys.__stdout__ + + def __getattr__(self, attr): + return getattr(self.fd, attr) + + def write(self, msg): + if len(msg) > 0 and msg != '\n': + log_debug(encode_str(msg)) + +hook = OutputHook() +sys.stdout = hook +sys.stderr = hook + +class ShokiLogger(): + def debug(self, msg): + log_debug(encode_str(msg)) + + def info(self, msg): + log_info(encode_str(msg)) + + def warning(self, msg): + log_warn(encode_str(msg)) + + def error(self, msg): + log_error(encode_str(msg)) + +import log +log.set_logger(ShokiLogger()) + +def exception_handler(exception_type, exception, traceback): + log.error(repr(exception)) +sys.excepthook = exception_handler + +import client +from post import PostType, DateType + +shoki = client.ShokiClient() +for name, p in client.ALL_PROVIDERS.items(): + if p[0].init(): + shoki.register_provider(name, p) + +cdef public int sho_client_search(str.str *provider, str.str *query) except -1: + cdef char *c_str0 = str.al_str_to_c_str(provider) + cdef char *c_str1 = str.al_str_to_c_str(query) + ret = shoki.search(decode_str(c_str0), decode_str(c_str1)) + lib.al_free(c_str0) + lib.al_free(c_str1) + return ret + +cdef public int py_str_to_str(str.str *s, char *py_c_str) except -1: + str.al_str_from(s, py_c_str) + return 0 + +cdef public int sho_client_get_page(result.sho_result *res, int search_id, int num) except -1: + cdef post.sho_post cpost + cdef str.str url + cdef str.str thumbnail_url + cdef str.str unique_id + page = shoki.get_page(search_id, num) + if not page: return -1 + for py_post in page['posts']: + post.sho_post_reset(&cpost) + cpost.version = py_post.version + cpost.type = py_post.type.value + py_str_to_str(&cpost.unique_id, encode_str(py_post.unique_id)) + py_str_to_str(&cpost.url, encode_str(py_post.url)) + for date in py_post.dates: + post.sho_post_add_date(&cpost, date.value, py_post.dates[date]) + py_str_to_str(&cpost.author.unique_id, encode_str(py_post.author.unique_id)) + py_str_to_str(&cpost.author.username, encode_str(py_post.author.username)) + py_str_to_str(&cpost.author.display_name, encode_str(py_post.author.display_name)) + py_str_to_str(&cpost.author.profile_image_url, encode_str(py_post.author.profile_image_url)) + if cpost.type == PostType.REPOST.value: + py_str_to_str(&cpost.post.unique_id, encode_str(py_post.post.unique_id)) + result.sho_result_add_post(res, num, &cpost) + continue + py_str_to_str(&cpost.title, encode_str(py_post.title)) + py_str_to_str(&cpost.text, encode_str(py_post.text)) + cpost.likes.set = py_post.likes != None + if cpost.likes.set: + cpost.likes.i = py_post.likes + cpost.reposts.set = py_post.reposts != None + if cpost.reposts.set: + cpost.reposts.i = py_post.reposts + cpost.quotes.set = py_post.quotes != None + if cpost.quotes.set: + cpost.quotes.i = py_post.quotes + cpost.comments.set = py_post.comments != None + if cpost.comments.set: + cpost.comments.i = py_post.comments + cpost.views.set = py_post.views != None + if cpost.views.set: + cpost.views.i = py_post.views + for media in py_post.media: + py_str_to_str(&url, encode_str(media.url)) + py_str_to_str(&thumbnail_url, encode_str(media.thumbnail_url)) + post.sho_post_add_media(&cpost, 0, &url, &thumbnail_url) + py_str_to_str(&cpost.quoted.unique_id, encode_str(py_post.quoted.unique_id)) + py_str_to_str(&cpost.in_reply_to.unique_id, encode_str(py_post.in_reply_to.unique_id)) + result.sho_result_add_post(res, num, &cpost) + for py_unique_id in page['list']: + py_str_to_str(&unique_id, encode_str(py_unique_id)) + result.sho_result_add_to_list(res, num, &unique_id) + return 0 diff --git a/src/shoki/cpy/str.pxd b/src/shoki/cpy/str.pxd new file mode 100644 index 0000000..a12ced4 --- /dev/null +++ b/src/shoki/cpy/str.pxd @@ -0,0 +1,11 @@ +include "types.pxd" + +cdef extern from "<al/str.h>": + ctypedef struct str: + u32 len + s32 alloc + char *data + + void al_str_from(str *s, const char *str) + char *al_str_to_c_str(str *s) + diff --git a/src/shoki/cpy/types.pxd b/src/shoki/cpy/types.pxd new file mode 100644 index 0000000..fec35a5 --- /dev/null +++ b/src/shoki/cpy/types.pxd @@ -0,0 +1,11 @@ +ctypedef unsigned char u8; +ctypedef char s8; +ctypedef unsigned short u16; +ctypedef short s16; +ctypedef unsigned int u32; +ctypedef int s32; +ctypedef unsigned long u64; +ctypedef long s64; +ctypedef float f32; +ctypedef double f64; +ctypedef bint bool diff --git a/src/shoki/meson.build b/src/shoki/meson.build new file mode 100644 index 0000000..557d497 --- /dev/null +++ b/src/shoki/meson.build @@ -0,0 +1,14 @@ +shoki_src = [ + 'src/search.c', + 'src/post.c', + 'src/packet_ext.c', + 'src/post_cache.c' +] +shoki_inc = [] +shoki_deps = [common_deps] + +python = dependency('python-3.11-embed', required: true) +shoki_deps += [python] + +shoki = declare_dependency(sources: shoki_src, + include_directories: shoki_inc, dependencies: shoki_deps) diff --git a/src/shoki/py/__init__.py b/src/shoki/py/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/shoki/py/__init__.py diff --git a/src/shoki/py/client.py b/src/shoki/py/client.py new file mode 100644 index 0000000..3986060 --- /dev/null +++ b/src/shoki/py/client.py @@ -0,0 +1,51 @@ +import log +import config +from providers.twitter import TwitterProvider +from providers.youtube import YoutubeProvider + +ALL_PROVIDERS = { + 'twitter': (TwitterProvider( + config.TWITTER_ACCESS_TOKEN, config.TWITTER_ACCESS_TOKEN_SECRET, + config.TWITTER_CONSUMER_TOKEN, config.TWITTER_CONSUMER_TOKEN_SECRET, + config.TWITTER_COOKIES_PATH), []), + 'youtube': (YoutubeProvider(), []) +} + +class ShokiClient(): + def __init__(self): + self.providers = {} + self.searches = {} + self.inc = 0 + + def register_provider(self, name, p): + log.info('Registered provider \'{}\'.'.format(name)) + self.providers[name] = p + + def get_providers(self): + return self.providers.keys() + + def search(self, provider, query): + if provider not in self.providers: + return -1 + search_id = self.inc + p = self.providers[provider] + try: + search = p[0].search(query, *p[1]) + self.searches[search_id] = search + self.inc += 1 + except Exception as e: + log.error(repr(e)) + return -1 + return search_id + + def get_page(self, search_id, num): + if search_id not in self.searches: + return None + try: + return self.searches[search_id].get_page(num) + except Exception as e: + log.error(repr(e)) + return None + + def get_playback_data(self, unique_id): + return '' diff --git a/src/shoki/py/config.def.py b/src/shoki/py/config.def.py new file mode 100644 index 0000000..237d658 --- /dev/null +++ b/src/shoki/py/config.def.py @@ -0,0 +1,19 @@ +from pathlib import Path + +BASE_DIR = '' + +TWITTER_ACCESS_TOKEN = '' +TWITTER_ACCESS_TOKEN_SECRET = '' +TWITTER_CONSUMER_TOKEN = '' +TWITTER_CONSUMER_TOKEN_SECRET = '' +TWITTER_COOKIES_PATH = '{}/cookies_twitter.txt'.format(BASE_DIR) +TWITTER_TIMEOUT = 5 # seconds + +FANBOX_SESSID = '' + +PIXIV_REFRESH_TOKEN = '' + +INSTAGRAM_USERNAME = '' +INSTAGRAM_PASSWORD = '' +INSTAGRAM_SETTINGS_PATH = Path('{}/ig_settings.json'.format(BASE_DIR)) +INSTAGRAM_USER_AGENT = 'Instagram 146.0.0.27.125 (iPhone12,1; iOS 13_3; en_US; en-US; scale=2.00; 1656x3584; 190542906)' diff --git a/src/shoki/py/log.py b/src/shoki/py/log.py new file mode 100644 index 0000000..c36c54e --- /dev/null +++ b/src/shoki/py/log.py @@ -0,0 +1,30 @@ +class DefaultLogger(): + def debug(self, msg): + print(msg) + + def info(self, msg): + print(msg) + + def warning(self, msg): + print(msg) + + def error(self, msg): + print(msg) + +LOGGER = DefaultLogger() + +def set_logger(logger): + global LOGGER + LOGGER = logger + +def debug(msg): + LOGGER.debug(msg) + +def info(msg): + LOGGER.info(msg) + +def warning(msg): + LOGGER.warning(msg) + +def error(msg): + LOGGER.error(msg) diff --git a/src/shoki/py/post.py b/src/shoki/py/post.py new file mode 100644 index 0000000..d489298 --- /dev/null +++ b/src/shoki/py/post.py @@ -0,0 +1,70 @@ +import dataclasses +from dataclasses import field +from typing import Optional +from json import JSONEncoder +from enum import Enum + +class PostType(Enum): + UNKNOWN = -1 + POST = 0 + REPOST = 1 + +class DateType(Enum): + CREATED = 0 + EDITED = 1 + ARCHIVED = 2 + +@dataclasses.dataclass +class Media(): + url: str = '' + base64_data: str = '' + +@dataclasses.dataclass +class Image(Media): + type: str = 'image' + thumbnail_url: str = '' + +@dataclasses.dataclass +class Video(Media): + type: str = 'video' + thumbnail_url: str = '' + +@dataclasses.dataclass +class Audio(Media): + type: str = 'audio' + +@dataclasses.dataclass +class User(): + unique_id: str = '' + username: str = '' + display_name: str = '' + profile_image_url: str = '' + +@dataclasses.dataclass +class PostRef(): + unique_id: str = '' + +@dataclasses.dataclass +class Post(): + version: int = 2 + type: 'PostType' = PostType.UNKNOWN + unique_id: str = '' + raw_responses: dict[str, str] = field(default_factory=lambda: {}) + url: str = '' + dates: dict['DateType', int] = field(default_factory=lambda: {}) + author: 'User' = field(default_factory=lambda: User()) + title: str = '' + text: str = '' + likes: Optional[int] = None + reposts: Optional[int] = None + quotes: Optional[int] = None + comments: Optional[int] = None + views: Optional[int] = None + media: list['Media'] = field(default_factory=lambda: []) + post: 'PostRef' = field(default_factory=lambda: PostRef()) + quoted: 'PostRef' = field(default_factory=lambda: PostRef()) + in_reply_to: 'PostRef' = field(default_factory=lambda: PostRef()) + +class PostEncoder(JSONEncoder): + def default(self, obj): + return obj.__dict__ diff --git a/src/shoki/py/provider.py b/src/shoki/py/provider.py new file mode 100644 index 0000000..a719d01 --- /dev/null +++ b/src/shoki/py/provider.py @@ -0,0 +1,39 @@ +import httpx +import json +import log + +class Provider(): + def __init__(self): + self.cookies = {} + self.headers = {} + self.unique_id_map = {} + + def init(self): + return True + + # TODO: retries. + def get(self, url, params = None): + log.debug('HTTP request {} {}.'.format(url, params)) + r = httpx.get(url, params=params, cookies=self.cookies, headers=self.headers) + # TODO: Non-200 = err, support redirects (if not already done in httpx)? + if r.status_code == 403 or r.status_code == 404: + log.error('HTTP {}.'.format(r.status_code)) + return None + log.debug('HTTP {}.'.format(r.status_code)) + return r + + def get_json(self, url, params = None): + try: + r = self.get(url, params) + if not r: + return None + return json.loads(r.content) + except json.JSONDecodeError as e: + log.error(repr(e)) + return None + + def get_obj_from_unique_id(self, unique_id): + return None + + def search(self, query, *extra_args): + return None diff --git a/src/shoki/py/providers/__init__.py b/src/shoki/py/providers/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/shoki/py/providers/__init__.py diff --git a/src/shoki/py/providers/fanbox.py b/src/shoki/py/providers/fanbox.py new file mode 100644 index 0000000..7739be3 --- /dev/null +++ b/src/shoki/py/providers/fanbox.py @@ -0,0 +1,130 @@ +import urllib.parse +import log +from plugin import Plugin +from search import Search +from post import Post, Image + +BASE_URL = "https://api.fanbox.cc/" + +PAGES_PER_QUERY = 2 + +class FanboxSearch(Search): + def __init__(self, plugin, creator_id): + super().__init__() + self.p = plugin + self.creator_id = creator_id + self.pagination_pages = [] + self.pagination_complete = False + + def paginate(self): + res = self.p.get_json(BASE_URL + 'post.paginateCreator', { 'creatorId': self.creator_id }) + if not res or 'body' not in res or len(res['body']) == 0: + return False + for p in res['body']: + params = urllib.parse.parse_qs(urllib.parse.urlparse(p).query) + page = { + 'url': p, + 'params': params, + 'data': None, + 'limit': -1 if 'limit' not in params else int(params['limit'][0]) + } + self.pagination_pages.append(page) + return True + + def load_pagination_page(self, p): + if not p['data']: + res = self.p.get_json(BASE_URL + 'post.listCreator', p['params']) + if not res: + return False + p['data'] = res['body']['items'] + # The get_post_at_index logic assumes limit will always be exact. + p['limit'] = len(p['data']) + return True + + def get_post_at_index(self, index): + if not self.pagination_complete: + if not self.paginate(): + return None + self.pagination_complete = True + page = None + count = 0 + for p in self.pagination_pages: + if p['limit'] == -1: + if not self.load_pagination_page(p): + return None + ncount = count + p['limit'] + if ncount > index: + page = p + break + count = ncount + if not page or not self.load_pagination_page(page) or count + page['limit'] <= index: + return None + return page['data'][index - count] + + def add_images_to_map(self, body): + if 'imageMap' not in body: + return + for k in body['imageMap'].keys(): + self.p.image_map[k] = body['imageMap'][k]['originalUrl'] + + def load_page(self, index): + self.pages[index] = [] + s = index * PAGES_PER_QUERY + for i in range(s, s + PAGES_PER_QUERY): + p = self.get_post_at_index(i) + # Break to return what we have, which could be less than expected. + if not p: + self.completed = True + break + unique_id = 'fanbox:{}'.format(p['id']) + if unique_id not in self.p.unique_id_map: + res = self.p.get_json( + BASE_URL + 'post.info', { 'postId': p['id'] } + ) + if not res or 'body' not in res: + # TODO: Log this. + continue + if res['body']['type'] == 'article': + self.add_images_to_map(res['body']['body']) + self.p.unique_id_map[unique_id] = res['body'] + # TODO: Should store Post object here instead. + body = self.p.unique_id_map[unique_id] + text = '' + media = [] + # If body is None, that post is restricted (not subbed or from a higher tier than your sub) + if body['body']: + if body['type'] == 'image': + text = body['body']['text'] + for i in body['body']['images']: + media.append(Image(url=i['originalUrl'], thumbnail_url='')) + elif body['type'] == 'article': + for b in body['body']['blocks']: + if b['type'] == 'p': + text += b['text'] + '\n' + elif b['type'] == 'image': + media.append(Image(url=self.p.image_map[b['imageId']], thumbnail_url='')) + else: + log.warn('Unhandled block type.') + else: + log.warn('Unhandled post type.') + # TODO: Raw response + self.pages[index].append(Post( + unique_id=unique_id, url='', title=body['title'], text=text, media=media)) + return True + +class FanboxPlugin(Plugin): + def __init__(self, sessid): + super().__init__() + self.image_map = {} + self.headers.update({ + 'accept': 'application/json, text/plain, */*', + 'accept-encoding': 'gzip, deflate, br', + 'accept-language': 'ja,en-US;q=0.9,en;q=0.8', + 'origin': 'https://www.fanbox.cc', + 'referer': 'https://www.fanbox.cc/', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36' + }) + self.cookies['FANBOXSESSID'] = sessid + + def search(self, query, *extra_args): + return FanboxSearch(self, query) diff --git a/src/shoki/py/providers/instagram.py b/src/shoki/py/providers/instagram.py new file mode 100644 index 0000000..0146ba3 --- /dev/null +++ b/src/shoki/py/providers/instagram.py @@ -0,0 +1,96 @@ +import os +import config +import log +from plugin import Plugin +from search import Search +from instagrapi import Client +from instagrapi.exceptions import UserNotFound, LoginRequired, ChallengeRequired +from post import User, Post, Image, Video + +REACH_LIMIT = 3 + +class InstagramSearch(Search): + def __init__(self, cl, pk): + super().__init__() + self.cl = cl + self.pk = pk + self.pagination_page = 0 + self.pagination_cursor = None + + def load_page(self, index): + if index >= self.pagination_page + REACH_LIMIT: + return False + request_satisfied = False + for _ in range(self.pagination_page, index + 1): + if self.pagination_page > 0 and not self.pagination_cursor: + self.completed = True + return False + try: + media, self.pagination_cursor = self.cl.user_medias_paginated_v1( + self.pk, 0, end_cursor=self.pagination_cursor) + except LoginRequired: + return False + except ChallengeRequired: + return False + self.pages[self.pagination_page] = [] + for m in media: + unique_id = 'instagram:p:{}'.format(m.id) + media = [] + if m.media_type == 8: # Album + for r in m.resources: + if r.media_type == 1: # Photo + media.append(Image(url=r.thumbnail_url)) + elif r.media_type == 2: # Video + media.append(Video(url=r.video_url, thumbnail_url=r.thumbnail_url)) + elif m.media_type == 2: # Video + #print(m.product_type) # feed, igtv, or clips + media.append(Video(url=m.video_url, thumbnail_url=m.thumbnail_url)) + elif m.media_type == 1: # Photo + media.append(Image(url=m.thumbnail_url)) + kwargs = {} + kwargs['unique_id'] = unique_id + kwargs['raw_responses'] = { 'tile': m.json() } + kwargs['url'] = 'https://www.instagram.com/p/{}'.format(m.code) + user = User(unique_id='instagram:u:{}'.format(m.user.pk)) + if m.user.username: + user.username = m.user.username + if m.user.full_name: + user.display_name = m.user.full_name + kwargs['author'] = user + kwargs['title'] = m.title + kwargs['text'] = m.caption_text + kwargs['media'] = media + self.pages[self.pagination_page].append(Post(**kwargs)) + if self.pagination_page == index: + request_satisfied = True + self.pagination_page += 1 + return request_satisfied + +class InstagramPlugin(Plugin): + def __init__(self): + super().__init__() + self.cl = Client() + self.cl.set_user_agent(config.INSTAGRAM_USER_AGENT) + + def init(self): + if os.path.exists(config.INSTAGRAM_SETTINGS_PATH): + self.cl.load_settings(config.INSTAGRAM_SETTINGS_PATH) + if self.cl.login(config.INSTAGRAM_USERNAME, config.INSTAGRAM_PASSWORD): + return True + if not self.cl.login(config.INSTAGRAM_USERNAME, config.INSTAGRAM_PASSWORD): + return False + self.cl.dump_settings(config.INSTAGRAM_SETTINGS_PATH) + return True + + def search(self, query, *extra_args): + try: + user = self.cl.user_info_by_username_v1(query) + except UserNotFound: + return None + except LoginRequired: + log.error('Login required.') + return None + except ChallengeRequired: + log.error('Challenge required.') + return None + return InstagramSearch(self.cl, user.pk) diff --git a/src/shoki/py/providers/pixiv.py b/src/shoki/py/providers/pixiv.py new file mode 100644 index 0000000..9276d61 --- /dev/null +++ b/src/shoki/py/providers/pixiv.py @@ -0,0 +1,113 @@ +import json +import requests +from post import User, Image, Post +from search import Search +from plugin import Plugin +from query_parser import QueryParser +from pixivpy3 import * + +class PixivBase(Search): + def __init__(self, userdata, arg): + super().__init__() + self.plugin = userdata + self.next_page = 0 + + def api_load_page(self, index): + return False, None + + def load_page(self, index): + request_satisfied = False + for i in range(self.next_page, index + 1): + if not self.next_qs: + break + self.pages[i] = [] + res, self.next_qs = self.api_load_page(i) + if not res: + break + if i == index: + request_satisfied = True + self.next_page += 1 + return request_satisfied + + def make_post(self, post): + if len(post['meta_pages']) == 0: + post['meta_pages'].append({ + 'image_urls': { + "medium": post['image_urls']['medium'], + "original": post['meta_single_page']['original_image_url'] + } + }) + kwargs = {} + kwargs['unique_id'] = 'pixiv:i:{}'.format(post['id']) + kwargs['raw_responses'] = { 'api': json.dumps(post) } + kwargs['url'] = 'https://www.pixiv.net/en/artworks/{}'.format(post['id']) + kwargs['author'] = User( + unique_id='pixiv:u:{}'.format(post['user']['id']), + username=post['user']['account'], display_name=post['user']['name']) + kwargs['title'] = post['title'] + kwargs['text'] = post['caption'] + kwargs['likes'] = post['total_bookmarks'] + if 'total_comments' in post: + kwargs['comments'] = post['total_comments'] + media = [] + for img in post['meta_pages']: + media.append(Image( + url=img['image_urls']['original'], thumbnail_url=img['image_urls']['medium'])) + kwargs['media'] = media + return Post(**kwargs) + +class PixivFollowing(PixivBase): + def __init__(self, userdata, arg): + super().__init__(userdata, arg) + self.next_qs = { "user_id": arg, "restrict": "public" } + + def api_load_page(self, index): + obj = self.plugin.api.user_following(**self.next_qs) + if 'user_previews' not in obj: + return False, None + for u in obj['user_previews']: + user = u['user'] + self.pages[index].append(User( + unique_id='pixiv:u:{}'.format(user['id']), username=user['account'], + display_name=user['name'])) + return True, self.plugin.api.parse_qs(obj['next_url']) + +class PixivUser(PixivBase): + def __init__(self, userdata, arg): + super().__init__(userdata, arg) + self.next_qs = { 'user_id': arg, 'type': 'illust' } + + def api_load_page(self, index): + obj = self.plugin.api.user_illusts(**self.next_qs) + if 'illusts' not in obj: + return False, None + for post in obj['illusts']: + self.pages[index].append(self.make_post(post)) + return True, self.plugin.api.parse_qs(obj['next_url']) + +class PixivBookmarks(PixivBase): + def __init__(self, userdata, arg): + super().__init__(userdata, arg) + self.next_qs = { 'user_id': arg, 'restrict': 'public' } + + def api_load_page(self, index): + obj = self.plugin.api.user_bookmarks_illust(**self.next_qs) + if 'illusts' not in obj: + return False, None + for post in obj['illusts']: + self.pages[index].append(self.make_post(post)) + return True, self.plugin.api.parse_qs(obj['next_url']) + +class PixivPlugin(Plugin): + def __init__(self, refresh_token): + super().__init__() + self.parser = QueryParser(self, 'following') + self.parser.add_command('following', PixivFollowing) + self.parser.add_command('user', PixivUser) + self.parser.add_command('bookmarks', PixivBookmarks) + self.api = AppPixivAPI() + self.api.requests = requests.Session() + self.api.auth(refresh_token=refresh_token) + + def search(self, query, *extra_args): + return self.parser.parse_query(query) diff --git a/src/shoki/py/providers/searx.py b/src/shoki/py/providers/searx.py new file mode 100644 index 0000000..b536f28 --- /dev/null +++ b/src/shoki/py/providers/searx.py @@ -0,0 +1,93 @@ +import sys +sys.path.append('../../../subprojects/searxng') +import json +from searx import settings +from searx.utils import gen_useragent +from searx.engines import load_engine, register_engine +from plugin import Plugin +from search import Search +from post import Post, Image + +# TODO: Put these on the Plugin object. +ENGINES = {} + +ENGINE_INITIAL_INDEX = { + 'google images': 0, + 'google': 1, + 'bing images': 0 +} + +class SearxSearch(Search): + def __init__(self, plugin, query, engine): + super().__init__() + global ENGINES + self.query = query + self.plugin = plugin + self.key = engine.replace(' ', '_') + self.engine = ENGINES[engine] + self.index = ENGINE_INITIAL_INDEX[engine] + + def load_page(self, index): + self.pages[index] = [] + res = self.plugin.send_http_request(self.engine.request(self.query, { + 'language': 'en-US', + 'safesearch': 0, + 'time_range': None, + 'pageno': self.index + index, + 'cookies': self.plugin.cookies, + 'headers': self.plugin.headers, + 'data': None + })) + if not res: + return False + try: + ret = self.engine.response(res) + except: + return False + if len(ret) == 0: + # TODO: Test. + self.completed = True + return False + for i, p in enumerate(ret): + if 'url' not in p: + continue + # Don't add this to the global map because it's not actually unique. + unique_id = '{}:{}_{}:{}'.format(self.key, self.query, i, index) + media = [] + if 'img_src' in p: + media.append(Image(url=p['img_src'], thumbnail_url='')) + elif 'image_url' in p: + media.append(Image(url=p['image_url'], thumbnail_url='')) + kwargs = {} + kwargs['unique_id'] = unique_id + kwargs['raw_responses'] = { 'result': json.dumps(p) } + kwargs['url'] = p['url'] + kwargs['title'] = p['title'] + kwargs['text'] = p['content'] + kwargs['media'] = media + self.pages[index].append(Post(**kwargs)) + return True + +class SearxPlugin(Plugin): + def __init__(self): + super().__init__() + global ENGINES + self.headers = { + 'User-Agent': gen_useragent(), + 'Accept-Language': 'en-US,en;q=0.5', + } + for engine_data in settings['engines']: + engine = load_engine(engine_data) + if engine: + register_engine(engine) + ENGINES[engine.name] = engine + + def send_http_request(self, params): + self.cookies.update(params['cookies']) + self.headers.update(params['headers']) + return self.get(params['url'], params=params['data']) + + def search(self, query, *extra_args): + if not extra_args: + return None + return SearxSearch(self, query, extra_args[0]) diff --git a/src/shoki/py/providers/twitter.py b/src/shoki/py/providers/twitter.py new file mode 100644 index 0000000..2fc0faf --- /dev/null +++ b/src/shoki/py/providers/twitter.py @@ -0,0 +1,425 @@ +import json +import config +import http.cookiejar +from datetime import timezone +from socketserver import UDPServer + +import log +from provider import Provider +from search import Search +from post import PostType, DateType, PostRef, Post, User, Image, Video +from query_parser import QueryParser + +# Official Twitter API +#from twitter import Twitter2, TwitterError, OAuth + +# GraphQL Scrape API +import snscrape.modules.twitter +from snscrape.base import ScraperException +from snscrape.modules.twitter import (TwitterSearchScraper, TwitterProfileScraper, + TwitterUserScraper, TwitterTweetScraper, TweetRef) + +# Don't attempt to load a page 4 or more pages past the current page. +REACH_LIMIT = 4 + +# Quite incomplete API based backend. Should *not* be used for archiving. +# HOLD: I can't test this without paying $100 for the X API WTFFF. +''' +class TwitterBase(Search): + ALL_PARAMS = { + 'tweet.fields': 'attachments,author_id,context_annotations,conversation_id,created_at,entities,geo,id,in_reply_to_user_id,lang,public_metrics,possibly_sensitive,referenced_tweets,reply_settings,source,text,withheld', + 'user.fields': 'created_at,description,entities,id,location,name,pinned_tweet_id,profile_image_url,protected,public_metrics,url,username,verified,withheld', + 'media.fields': 'duration_ms,height,media_key,preview_image_url,type,url,width,public_metrics', + 'place.fields': 'contained_within,country,country_code,full_name,geo,id,name,place_type', + 'poll.fields': 'duration_minutes,end_datetime,id,options,voting_status', + } + ALL_EXPRESSIONS = 'author_id,referenced_tweets.id,referenced_tweets.id.author_id,entities.mentions.username,attachments.poll_ids,attachments.media_keys,in_reply_to_user_id,geo.place_id' + + SOME_PARAMS = { + 'tweet.fields': 'attachments,author_id,text,entities,referenced_tweets', + 'user.fields': 'id,name,profile_image_url,url,username', + 'media.fields': 'duration_ms,height,media_key,preview_image_url,type,url,width,public_metrics', + 'place.fields': '', + 'poll.fields': '', + } + SOME_EXPRESSIONS = 'author_id,referenced_tweets.id,referenced_tweets.id.author_id,entities.mentions.username,attachments.media_keys,in_reply_to_user_id' + + def __init__(self, userdata, arg): + super().__init__() + self.provider = userdata + self.params = self.SOME_PARAMS.copy() + self.media_map = {} + self.user_id = None + self.pagination_page = 0 + self.pagination_token = None + self.arg = arg + + def add_attachemnts(self, l, r, data): + if 'media_keys' not in data: + return + for m in data['media_keys']: + if m in r: + continue + r.append(m) + if m in self.media_map: + l.append(Image(url=self.media_map[m], thumbnail_url='')) + + def add_entity_urls(self, data): + if 'urls' in data: + for u in data['urls']: + if 'media_key' in u and u['media_key'] not in self.media_map: + self.media_map[u['media_key']] = u['expanded_url'] + + def make_post(self, tweet): + print(json.dumps(tweet, indent=4)) + media = [] + repeats = [] + if 'referenced_tweets' in t: + print(len(referenced_tweets)) + #for rt in t['referenced_tweets']: + # if rt['id'] in self.tweet_map: + # rrt = self.tweet_map[rt['id']] + # if 'entities' in rrt: + # self.add_entity_urls(rrt['entities']) + # if 'attachments' in rrt: + # self.add_attachemnts(media, repeats, rrt['attachments']) + if 'attachments' in t: + self.add_attachemnts(media, repeats, t['attachments']) + unique_id = 'twitter:t:{}'.format(t['id']) + url = 'https://twitter.com/{}/status/{}'.format(t['author_id'], t['id']) + kwargs = {} + kwargs['unique_id'] = unique_id + kwargs['raw_responses'] = { 'tweet' : json.dumps(t) } + kwargs['url'] = url + kwargs['author'] = User(unique_id='twitter:u:{}'.format(t['author_id'])) + kwargs['title'] = '' + kwargs['text'] = t['text'] + kwargs['media'] = media + return Post(**kwargs) + + def add_items_from_search(self, data): + if 'includes' in data: + includes = data['includes'] + if 'media' in includes: + for i in includes['media']: + if 'url' in i: + self.media_map[i['media_key']] = i['url'] + #if 'tweets' in includes: + # for t in includes['tweets']: + # self.tweet_map[t['id']] = t + if 'data' not in data or not data['data']: + return False + l = data['data'] if type(data['data']) == list else [data['data']] + for t in l: + if 'entities' in t: + self.add_entity_urls(t['entities']) + post = self.make_post(t) + self.pages[self.pagination_page].append(post) + return True + + def add_user_ids_from_search(self, data): + if not data['data']: + return False + for u in data['data']: + self.pages[self.pagination_page].append(User( + 'twitter:u:{}'.format(u['id']), username=u['username'], display_name=u['name'])) + return True + + def should_process_index(self, index): + if index - self.pagination_page >= REACH_LIMIT: + return False + if self.pagination_page > 0 and not self.pagination_token: + self.completed = True + return False + if self.pagination_token: + self.params['pagination_token'] = self.pagination_token + self.pages[self.pagination_page] = [] + return True + + def handle_data(self, data, user_ids=False): + if 'next_token' not in data['meta'].keys(): + self.pagination_token = None + else: + self.pagination_token = data['meta']['next_token'] + if user_ids: + if not self.add_user_ids_from_search(data): + return False + else: + if not self.add_items_from_search(data): + return False + self.pagination_page += 1 + return True + + def get_user_id(self, username): + if self.user_id: + return True + try: + data = self.provider.t.users.by.username._username( + _username=username, _timeout=config.TWITTER_TIMEOUT) + except TwitterError as e: + log.error(repr(e)) + return False + self.user_id = data['data']['id'] + return True + +class TwitterSearch(TwitterBase): + def load_page(self, index): + for _ in range(self.pagination_page, index + 1): + if not self.should_process_index(index): + return False + try: + data = self.provider.t.tweets.search.recent( + query=self.arg, expansions=self.SOME_EXPRESSIONS, params=self.params, + sort_order='relevancy', max_results=25, _timeout=config.TWITTER_TIMEOUT) + except TwitterError as e: + log.error(repr(e)) + return False + return self.handle_data(data) + +class TwitterUser(TwitterBase): + def __init__(self, userdata, arg): + if arg.startswith('@'): + arg = arg[1:] + super().__init__(userdata, arg) + + def load_page(self, index): + for _ in range(self.pagination_page, index + 1): + if not self.should_process_index(index): + return False + if not self.get_user_id(self.arg): + return False + try: + data = self.provider.t.users._id.tweets( + _id=self.user_id, expansions=self.SOME_EXPRESSIONS, params=self.params, + max_results=25, _timeout=config.TWITTER_TIMEOUT) + except TwitterError as e: + log.error(repr(e)) + return False + return self.handle_data(data) + +class TwitterTimeline(TwitterBase): + def __init__(self, userdata, arg): + if not arg: + arg = 'pizzabelly' + super().__init__(userdata, arg) + self.params['exclude'] = 'replies' + + def load_page(self, index): + for _ in range(self.pagination_page, index + 1): + if not self.should_process_index(index): + return False + if not self.get_user_id(self.arg): + return False + try: + data = self.provider.t.users._id.timelines.reverse_chronological( + _id=self.user_id, expansions=self.SOME_EXPRESSIONS, params=self.params, + max_results=25, _timeout=config.TWITTER_TIMEOUT) + except TwitterError as e: + log.error(repr(e)) + return False + return self.handle_data(data) + +class TwitterLikes(TwitterBase): + def __init__(self, userdata, arg): + if arg.startswith('@'): + arg = arg[1:] + super().__init__(userdata, arg) + + def load_page(self, index): + for _ in range(self.pagination_page, index + 1): + if not self.should_process_index(index): + return False + if not self.get_user_id(self.arg): + return False + try: + data = self.provider.t.users._id.liked_tweets( + _id=self.user_id, expansions=self.SOME_EXPRESSIONS, params=self.params, + max_results=25, _timeout=config.TWITTER_TIMEOUT) + except TwitterError as e: + log.error(repr(e)) + return False + return self.handle_data(data) + +class TwitterTweet(TwitterBase): + def load_page(self, index): + for _ in range(self.pagination_page, index + 1): + if not self.should_process_index(index): + return False + try: + data = self.provider.t.tweets( + ids=self.arg, expansions=self.SOME_EXPRESSIONS, params=self.params, + _timeout=config.TWITTER_TIMEOUT) + except TwitterError as e: + log.error(repr(e)) + return False + return self.handle_data(data) + +class TwitterFollowing(TwitterBase): + def __init__(self, userdata, arg): + if arg.startswith('@'): + arg = arg[1:] + super().__init__(userdata, arg) + del self.params['media.fields'] + del self.params['place.fields'] + del self.params['poll.fields'] + + def load_page(self, index): + for _ in range(self.pagination_page, index + 1): + if not self.should_process_index(index): + return False + if not self.get_user_id(self.arg): + return False + try: + data = self.provider.t.users._id.following( + _id=self.user_id, params=self.params, max_results=25, _timeout=config.TWITTER_TIMEOUT) + except TwitterError as e: + log.error(repr(e)) + return False + return self.handle_data(data, True) +''' + +class TwitterScrapeBase(Search): + # Posts per emulated page because snscrape returns an iterator. + POSTS_PER_PAGE = 8 + + def __init__(self, userdata, arg): + super().__init__() + self.provider = userdata + self.cookies = self.provider.cookies + self.arg = arg + self.iterator = None + self.iter_page = 0 + self.sent_hashes = [] + + def parse_media(self, l): + media = [] + for m in l: + if isinstance(m, snscrape.modules.twitter.Photo): + media.append(Image(url=m.fullUrl, thumbnail_url=m.previewUrl)) + elif isinstance(m, snscrape.modules.twitter.Video): + videos = sorted(m.variants, key=lambda x: x.bitrate if x.bitrate else 0, reverse=True) + media.append(Video(url=videos[0].url, thumbnail_url=m.thumbnailUrl)) + elif isinstance(m, snscrape.modules.twitter.Gif): + gifs = sorted(m.variants, key=lambda x: x.bitrate if x.bitrate else 0, reverse=True) + media.append(Video(url=gifs[0].url, thumbnail_url=m.thumbnailUrl)) + return media + + def make_post(self, tweet): + kwargs = {} + kwargs['unique_id'] = 'twitter:t:{}'.format(tweet.id) + if isinstance(tweet, TweetRef): + return Post(**kwargs) + kwargs['raw_responses'] = tweet.rawResponses + h = tweet.rawResponses['hash'] + if h not in self.sent_hashes: + self.sent_hashes.append(h) + else: + del kwargs['raw_responses']['api'] + kwargs['url'] = tweet.url + kwargs['dates'] = { + DateType.CREATED: tweet.date.replace(tzinfo=timezone.utc).timestamp() + } + author = User(unique_id='twitter:u:{}'.format(tweet.user.id), username='@' + tweet.user.username, + profile_image_url=tweet.user.profileImageUrl) + if tweet.user.displayname: + author.display_name = tweet.user.displayname + kwargs['author'] = author + if tweet.retweetedTweet: + kwargs['type'] = PostType.REPOST + kwargs['post'] = PostRef(unique_id='twitter:t:{}'.format(tweet.retweetedTweet.id)) + return Post(**kwargs) + kwargs['type'] = PostType.POST + kwargs['text'] = tweet.rawContent + kwargs['likes'] = tweet.likeCount + kwargs['reposts'] = tweet.retweetCount + kwargs['quotes'] = tweet.quoteCount + kwargs['comments'] = tweet.replyCount + kwargs['views'] = tweet.viewCount + if tweet.media: + kwargs['media'] = self.parse_media(tweet.media) + if tweet.inReplyToTweetId: + kwargs['in_reply_to'] = PostRef(unique_id='twitter:t:{}'.format(tweet.inReplyToTweetId)) + if tweet.quotedTweet: + kwargs['quoted'] = PostRef(unique_id='twitter:t:{}'.format(tweet.quotedTweet.id)) + return Post(**kwargs) + + def add_post(self, i, post): + self.pages[i]['posts'].append(post) + + def add_post_to_list(self, i, post): + self.add_post(i, post) + self.pages[i]['list'].append(post.unique_id) + + def load_page(self, index): + if not self.iterator or index - self.iter_page >= REACH_LIMIT: + return False + if index in self.pages: + return True + # It's possible to load some "pages", but not enough to satisfy the requested index. + request_satisfied = False + for i in range(self.iter_page, index + 1): + if self.completed: + break + self.pages[i] = { 'posts': [], 'list': [] } + for _ in range(0, self.POSTS_PER_PAGE): + try: + tweet = next(self.iterator) + except StopIteration: + self.completed = True + break + except ScraperException as e: + log.error(repr(e)) + continue + self.add_post_to_list(i, self.make_post(tweet)) + if tweet.retweetedTweet: + self.add_post(i, self.make_post(tweet.retweetedTweet)) + if tweet.quotedTweet: + self.add_post(i, self.make_post(tweet.quotedTweet)) + if i == index: + request_satisfied = True + self.iter_page += 1 + return request_satisfied + +class TwitterScrapeSearch(TwitterScrapeBase): + def __init__(self, userdata, arg): + super().__init__(userdata, arg) + self.iterator = TwitterSearchScraper(self.arg, top=True, cookies=self.cookies).get_items() + +class TwitterScrapeUser(TwitterScrapeBase): + def __init__(self, userdata, arg): + super().__init__(userdata, arg) + if (self.arg.startswith('@')): + self.arg = self.arg[1:] + self.iterator = TwitterProfileScraper(self.arg, cookies=self.cookies).get_items() + #self.iterator = TwitterUserScraper(self.arg, cookies=self.cookies).get_items() + +class TwitterScrapeTweet(TwitterScrapeBase): + def __init__(self, userdata, arg): + super().__init__(userdata, arg) + self.single = True + if (self.arg.startswith('https://')): + self.arg = self.arg[self.arg.rfind('/') + 1:] + question = self.arg.find('?') + if question >= 0: + self.arg = self.arg[:question] + self.iterator = TwitterTweetScraper(self.arg, cookies=self.cookies).get_items() + +class TwitterProvider(Provider): + def __init__(self, access_key, access_secret, consumer_key, consumer_secret, cookies_path): + super().__init__() + self.parser = QueryParser(self, 'search') + self.parser.add_command('search', TwitterScrapeSearch) + self.parser.add_command('user', TwitterScrapeUser) + self.parser.add_command('tweet', TwitterScrapeTweet) + #self.parser.add_command('timeline', TwitterTimeline) + #self.parser.add_command('likes', TwitterLikes) + #self.parser.add_command('following', TwitterFollowing) + #self.t = Twitter2(auth=OAuth(access_key, access_secret, consumer_key, consumer_secret), retry=True) + # Cookies are used by the scrape backend. + cookie_jar = http.cookiejar.MozillaCookieJar() + cookie_jar.load(filename=cookies_path, ignore_expires=True) + for c in cookie_jar: + self.cookies[c.name] = c.value + + def search(self, query, *extra_args): + return self.parser.parse_query(query) diff --git a/src/shoki/py/providers/youtube.py b/src/shoki/py/providers/youtube.py new file mode 100644 index 0000000..f17211e --- /dev/null +++ b/src/shoki/py/providers/youtube.py @@ -0,0 +1,115 @@ +import log +import yt_dlp +from provider import Provider +from search import Search +from post import Post, PostType, Image, Video +from query_parser import QueryParser + +class YDLLogger(): + def debug(self, msg): + if msg.startswith('[debug] '): + log.debug(msg) + else: + log.info(msg) + + def info(self, msg): + pass + + def warning(self, msg): + log.warning(msg) + + def error(self, msg): + log.error(msg) + +ydl_opts = { + 'quiet': False, + 'logger': YDLLogger(), + 'cachedir': False, +# 'cookiefile': '', +} + +ydl = yt_dlp.YoutubeDL(ydl_opts) + +def get_playback_url(data, video=True): + if 'entries' in data: + data = data['entries'][0] + + url = data['formats'][0]['url'] + + ''' + data['formats'] = list(filter(lambda f: 'container' not in f, data['formats'])) + + data['formats'] = list(filter( + lambda f: not ('container' in f and (f['container'] in ['mp4_dash', 'webm_dash'])), data['formats'] + )) + ''' + + data['formats'] = list(filter(lambda f: 'manifest_url' not in f, data['formats'])) + + has_audio = list(filter(lambda f: 'acodec' not in f or f['acodec'] != 'none', data['formats'])) + + if video: + if len(has_audio) > 0: + data['formats'] = has_audio + try: + data['formats'] = list(filter(lambda f: 'quality' in f, data['formats'])) + url = max(data['formats'], key=lambda f: f['quality'])['url'] + except: + pass + else: + if len(has_audio) == 0: + return None + data['formats'] = has_audio + try: + audio_only = list(filter(lambda f: f['vcodec'] == 'none', data['formats'])) + if len(audio_only) > 0: + data['formats'] = audio_only + else: + data['formats'] = list(filter(lambda f: f['ext'] in ['mp4'], data['formats'])) + except: + pass + try: + data['formats'] = list(filter(lambda f: 'abr' in f, data['formats'])) + url = max(data['formats'], key=lambda f: f['abr'])['url'] + except: + pass + + return url + +class YoutubeBase(Search): + def __init__(self, userdata, arg): + super().__init__() + self.provider = userdata + self.query = arg + + def load_page(self, index): + self.pages[index] = { 'posts': [], 'list': [] } + res = self.get_info() + if res: + media = [Video(url=get_playback_url(res))] + self.pages[index]['posts'].append(Post(type=PostType.POST, unique_id='', url='', title='', text='', media=media)) + return True + +class YoutubeSearch(YoutubeBase): + def __init__(self, userdata, arg): + super().__init__(userdata, arg) + + def get_info(self): + return ydl.extract_info('ytsearch1:{}'.format(self.query), download=False) + +class YoutubeLink(YoutubeBase): + def __init__(self, userdata, arg): + super().__init__(userdata, arg) + + def get_info(self): + return ydl.extract_info(self.query, download=False) + +class YoutubeProvider(Provider): + def __init__(self): + super().__init__() + self.parser = QueryParser(self, 'search') + self.parser.add_command('search', YoutubeSearch) + self.parser.add_command('link', YoutubeLink) + + def search(self, query, *extra_args): + return self.parser.parse_query(query) diff --git a/src/shoki/py/query_parser.py b/src/shoki/py/query_parser.py new file mode 100644 index 0000000..ee23bef --- /dev/null +++ b/src/shoki/py/query_parser.py @@ -0,0 +1,39 @@ +class QueryParser(): + def __init__(self, userdata, default_command): + self.commands = {} + self.escaped_commands = {} + self.userdata = userdata + self.default_command = default_command + + def add_command(self, name, search): + self.commands[name] = search + self.escaped_commands['\\' + name] = name + + def parse_query(self, query): + arg = query + search = self.commands[self.default_command] + + sp = query.split(' ') + + for i, word in enumerate(sp): + col = word.find(':') + if col == -1: + continue + command = word[0:col] + if command in self.escaped_commands: + query = query.replace(command, self.escaped_commands[command], 1) + continue + if command in self.commands: + if col + 1 < len(word): + arg = word[col+1:] + else: + arg = None + search = self.commands[command] + if i == 0: + if len(sp) > 1: + query = query.replace(word + ' ', '', 1) + else: + query = query.replace(word, '', 1) + else: + query = query.replace(' ' + word + ' ', '', 1) + return search(self.userdata, arg) diff --git a/src/shoki/py/search.py b/src/shoki/py/search.py new file mode 100644 index 0000000..4134851 --- /dev/null +++ b/src/shoki/py/search.py @@ -0,0 +1,35 @@ +class Search(): + def __init__(self): + self.pages = {} + self.iter_page = 0 + self.iter_index = 0 + self.single = False + self.completed = False + + def __iter__(self): + self.iter_page = 0 + self.iter_index = 0 + return self + + def __next__(self): + if self.iter_index >= len(self.pages[self.iter_page]): + self.iter_page += 1 + self.iter_index = 0 + if self.iter_page not in self.pages: + if not self.load_page(self.iter_page): + raise StopIteration + return self.pages[self.iter_page][self.iter_index] + + def load_page(self, index): + return False + + def get_page(self, index): + if index not in self.pages: + if self.completed: + if self.single: + return self.pages[0] + else: + return None + if not self.load_page(index): + return None + return self.pages[index] diff --git a/src/shoki/src/packet_ext.c b/src/shoki/src/packet_ext.c new file mode 100644 index 0000000..26ba9ee --- /dev/null +++ b/src/shoki/src/packet_ext.c @@ -0,0 +1,91 @@ +#include "search.h" +#include "packet_ext.h" + +static void aki_packet_write_sho_optional_int(struct aki_packet *packet, sho_optional_int *o) +{ + AKI_PACKET_WRITE_TYPE(packet, s64, o->i); + AKI_PACKET_WRITE_TYPE(packet, bool, o->set); +} + +void aki_packet_write_sho_post(struct aki_packet *packet, struct sho_post *post) +{ + AKI_PACKET_WRITE_TYPE(packet, u16, post->version); + AKI_PACKET_WRITE_TYPE(packet, u8, post->type); + aki_packet_write_string(packet, &post->unique_id); + aki_packet_write_string(packet, &post->url); + AKI_PACKET_WRITE_TYPE(packet, u32, post->dates.size); + struct sho_post_date *date; + al_array_foreach_ptr(post->dates, i, date) { + AKI_PACKET_WRITE_TYPE(packet, u8, date->type); + AKI_PACKET_WRITE_TYPE(packet, s64, date->timestamp); + } + aki_packet_write_string(packet, &post->author.unique_id); + aki_packet_write_string(packet, &post->author.username); + aki_packet_write_string(packet, &post->author.display_name); + aki_packet_write_string(packet, &post->author.profile_image_url); + aki_packet_write_string(packet, &post->title); + aki_packet_write_string(packet, &post->text); + aki_packet_write_sho_optional_int(packet, &post->likes); + aki_packet_write_sho_optional_int(packet, &post->reposts); + aki_packet_write_sho_optional_int(packet, &post->quotes); + aki_packet_write_sho_optional_int(packet, &post->comments); + aki_packet_write_sho_optional_int(packet, &post->views); + AKI_PACKET_WRITE_TYPE(packet, u32, post->media.size); + struct sho_post_media *media; + al_array_foreach_ptr(post->media, i, media) { + AKI_PACKET_WRITE_TYPE(packet, u8, media->type); + aki_packet_write_string(packet, &media->url); + aki_packet_write_string(packet, &media->thumbnail_url); + } + aki_packet_write_string(packet, &post->post.unique_id); + aki_packet_write_string(packet, &post->quoted.unique_id); + aki_packet_write_string(packet, &post->in_reply_to.unique_id); +} + +static void aki_packet_read_sho_optional_int(struct aki_packet *packet, sho_optional_int *o) +{ + AKI_PACKET_READ_TYPE(packet, s64, o->i); + AKI_PACKET_READ_TYPE(packet, bool, o->set); +} + +void aki_packet_read_sho_post(struct aki_packet *packet, struct sho_post *post) +{ + sho_post_reset(post); + AKI_PACKET_READ_TYPE(packet, u16, post->version); + AKI_PACKET_READ_TYPE(packet, u8, post->type); + aki_packet_read_string(packet, &post->unique_id); + aki_packet_read_string(packet, &post->url); + u32 dates_size; + AKI_PACKET_READ_TYPE(packet, u32, dates_size); + al_array_reserve(post->dates, dates_size); + for (u32 i = 0; i < dates_size; i++) { + struct sho_post_date date; + AKI_PACKET_READ_TYPE(packet, u8, date.type); + AKI_PACKET_READ_TYPE(packet, s64, date.timestamp); + al_array_push(post->dates, date); + } + aki_packet_read_string(packet, &post->author.unique_id); + aki_packet_read_string(packet, &post->author.username); + aki_packet_read_string(packet, &post->author.display_name); + aki_packet_read_string(packet, &post->author.profile_image_url); + aki_packet_read_string(packet, &post->title); + aki_packet_read_string(packet, &post->text); + aki_packet_read_sho_optional_int(packet, &post->likes); + aki_packet_read_sho_optional_int(packet, &post->reposts); + aki_packet_read_sho_optional_int(packet, &post->quotes); + aki_packet_read_sho_optional_int(packet, &post->comments); + aki_packet_read_sho_optional_int(packet, &post->views); + u32 media_size; + AKI_PACKET_READ_TYPE(packet, u32, media_size); + al_array_reserve(post->media, media_size); + for (u32 i = 0; i < media_size; i++) { + struct sho_post_media media; + AKI_PACKET_READ_TYPE(packet, u8, media.type); + aki_packet_read_string(packet, &media.url); + aki_packet_read_string(packet, &media.thumbnail_url); + al_array_push(post->media, media); + } + aki_packet_read_string(packet, &post->post.unique_id); + aki_packet_read_string(packet, &post->quoted.unique_id); + aki_packet_read_string(packet, &post->in_reply_to.unique_id); +} diff --git a/src/shoki/src/packet_ext.h b/src/shoki/src/packet_ext.h new file mode 100644 index 0000000..ff0fda1 --- /dev/null +++ b/src/shoki/src/packet_ext.h @@ -0,0 +1,8 @@ +#pragma once + +#include <aki/common.h> + +#include "post.h" + +void aki_packet_write_sho_post(struct aki_packet *packet, struct sho_post *post); +void aki_packet_read_sho_post(struct aki_packet *packet, struct sho_post *post); diff --git a/src/shoki/src/post.c b/src/shoki/src/post.c new file mode 100644 index 0000000..22fb7ef --- /dev/null +++ b/src/shoki/src/post.c @@ -0,0 +1,55 @@ +#include "post.h" + +void sho_post_reset(struct sho_post *post) +{ + al_bzero(post, sizeof(struct sho_post)); + al_array_init(post->dates); + al_array_init(post->media); +} + +void sho_post_add_media(struct sho_post *post, u8 type, str *url, str *thumbnail_url) +{ + al_array_push(post->media, ((struct sho_post_media){ + .type = type, .url = *url, .thumbnail_url = *thumbnail_url + })); +} + +void sho_post_add_date(struct sho_post *post, u8 type, s64 timestamp) +{ + al_array_push(post->dates, ((struct sho_post_date){ + .type = type, .timestamp = timestamp + })); +} + +void sho_post_clone(struct sho_post *dest, struct sho_post *src) +{ + sho_post_reset(dest); + dest->version = src->version; + dest->type = src->type; + al_str_clone(&dest->unique_id, &src->unique_id); + al_str_clone(&dest->url, &src->url); + al_array_clone(dest->dates, src->dates); + al_str_clone(&dest->author.unique_id, &src->author.unique_id); + al_str_clone(&dest->author.username, &src->author.username); + al_str_clone(&dest->author.display_name, &src->author.display_name); + al_str_clone(&dest->author.profile_image_url, &src->author.profile_image_url); + al_str_clone(&dest->title, &src->title); + al_str_clone(&dest->text, &src->text); + dest->likes = src->likes; + dest->reposts = src->reposts; + dest->quotes = src->quotes; + dest->comments = src->comments; + dest->views = src->views; + struct sho_post_media *media; + struct sho_post_media media_copy; + al_array_foreach_ptr(src->media, i, media) { + al_bzero(&media_copy, sizeof(struct sho_post_media)); + media_copy.type = media->type; + al_str_clone(&media_copy.url, &media->url); + al_str_clone(&media_copy.thumbnail_url, &media->thumbnail_url); + al_array_push(dest->media, media_copy); + } + al_str_clone(&dest->post.unique_id, &src->post.unique_id); + al_str_clone(&dest->quoted.unique_id, &src->quoted.unique_id); + al_str_clone(&dest->in_reply_to.unique_id, &src->in_reply_to.unique_id); +} diff --git a/src/shoki/src/post.h b/src/shoki/src/post.h new file mode 100644 index 0000000..6814b0f --- /dev/null +++ b/src/shoki/src/post.h @@ -0,0 +1,78 @@ +#pragma once + +#include <al/types.h> +#include <al/str.h> +#include <al/array.h> + +enum { + SHOKI_MEDIA_IMAGE = 0, + SHOKI_MEDIA_VIDEO, + SHOKI_MEDIA_AUDIO +}; + +enum { + SHOKI_POST_POST = 0, + SHOKI_POST_REPOST +}; + +enum { + SHOKI_DATE_CREATED = 0, + SHOKI_DATE_EDITED, + SHOKI_DATE_ARCHIVED +}; + +typedef struct { + s64 i; + bool set; +} sho_optional_int; + +struct sho_post_date { + u8 type; + s64 timestamp; +}; + +struct sho_post_media { + u8 type; + str url; + str thumbnail_url; +}; + +typedef array(struct sho_post_date) sho_dates_array; +typedef array(struct sho_post_media) sho_media_array; + +struct sho_post_user { + str unique_id; + str username; + str display_name; + str profile_image_url; +}; + +struct sho_post_ref { + str unique_id; +}; + +struct sho_post { + u16 version; + u8 type; + str unique_id; + str raw_responses; + str url; + sho_dates_array dates; + struct sho_post_user author; + str title; + str text; + sho_optional_int likes; + sho_optional_int reposts; + sho_optional_int quotes; + sho_optional_int comments; + sho_optional_int views; + sho_media_array media; + struct sho_post_ref post; // reposted post. + struct sho_post_ref quoted; + struct sho_post_ref in_reply_to; +}; + +void sho_post_reset(struct sho_post *post); +void sho_post_add_media(struct sho_post *post, u8 type, str *url, str *thumbnail_url); +void sho_post_add_date(struct sho_post *post, u8 type, s64 timestamp); +void sho_post_clone(struct sho_post *dest, struct sho_post *src); diff --git a/src/shoki/src/post_cache.c b/src/shoki/src/post_cache.c new file mode 100644 index 0000000..a8f02eb --- /dev/null +++ b/src/shoki/src/post_cache.c @@ -0,0 +1,30 @@ +#include "post_cache.h" + +void sho_post_cache_init(struct sho_post_cache *cache) +{ + al_array_init(cache->cache); +} + +void sho_post_cache_push(struct sho_post_cache *cache, struct sho_post *post) +{ + struct sho_post *check = sho_post_cache_get(cache, &post->unique_id); + if (!check) { + struct sho_post *npost = al_alloc_object(struct sho_post); + sho_post_clone(npost, post); + al_array_push(cache->cache, npost); + } +} + +struct sho_post *sho_post_cache_get(struct sho_post_cache *cache, str *unique_id) +{ + struct sho_post *post; + al_array_foreach(cache->cache, i, post) { + if (al_str_eq(&post->unique_id, unique_id)) { + return post; + } + if (al_str_eq(&post->author.unique_id, unique_id)) { + return post; + } + } + return NULL; +} diff --git a/src/shoki/src/post_cache.h b/src/shoki/src/post_cache.h new file mode 100644 index 0000000..80cd0b7 --- /dev/null +++ b/src/shoki/src/post_cache.h @@ -0,0 +1,13 @@ +#pragma once + +#include <al/array.h> + +#include "post.h" + +struct sho_post_cache { + array(struct sho_post *) cache; +}; + +void sho_post_cache_init(struct sho_post_cache *cache); +void sho_post_cache_push(struct sho_post_cache *cache, struct sho_post *post); +struct sho_post *sho_post_cache_get(struct sho_post_cache *cache, str *unique_id); diff --git a/src/shoki/src/search.c b/src/shoki/src/search.c new file mode 100644 index 0000000..b4b2157 --- /dev/null +++ b/src/shoki/src/search.c @@ -0,0 +1,99 @@ +#include <al/log.h> + +#include "../cpy/shoki.c" + +#include "search.h" + +bool sho_python_init(void) +{ + PyPreConfig pre; + PyPreConfig_InitPythonConfig(&pre); + pre.utf8_mode = 1; + Py_PreInitialize(&pre); + if (PyImport_AppendInittab("shoki", PyInit_shoki) == -1) { + al_log_error("shoki", "Could not extend in-built modules table."); + sho_python_close(); + return false; + } + Py_Initialize(); + PyObject *module = PyImport_ImportModule("shoki"); + if (!module) { + PyErr_Print(); + al_log_error("shoki", "Could not import module."); + sho_python_close(); + return false; + } + return true; +} + +void sho_result_init(struct sho_result *res) +{ + al_array_init(res->pages); +} + +static struct sho_result_page *page_at_index(struct sho_result *res, s32 num) +{ + struct sho_result_page *page; + al_array_foreach_ptr(res->pages, i, page) { + if (page->num == num) { + return page; + } + } + return NULL; +} + +void sho_result_add_post(struct sho_result *res, s32 num, struct sho_post *post) +{ + struct sho_result_page *page = page_at_index(res, num); + if (!page) { + al_array_push(res->pages, (struct sho_result_page){0}); + page = &al_array_last(res->pages); + page->num = num; + al_array_init(page->posts); + al_array_init(page->list); + } + al_array_push(page->posts, *post); +} + +void sho_result_add_to_list(struct sho_result *res, s32 num, str *unique_id) +{ + struct sho_result_page *page = page_at_index(res, num); + al_array_push(page->list, *unique_id); +} + +void sho_search_init(struct sho_search *search) +{ + search->id = -1; + search->page = 0; + sho_result_init(&search->result); +} + +s32 sho_search_more_results(struct sho_search *search, str *provider, str *query) +{ + if (search->id < 0) { + search->id = sho_client_search(provider, query); + } else { + search->page++; + } + if (search->id < 0) return -1; + al_log_info("shoki", "Loading page %i (%.*s).", search->page, AL_STR_PRINTF(query)); + if (sho_client_get_page(&search->result, search->id, search->page) < 0) { + return -1; + } + return search->page; +} + +void sho_search_free(struct sho_search *search) +{ + struct sho_result_page *page; + al_array_foreach_ptr(search->result.pages, i, page) { + al_array_free(page->posts); + al_array_free(page->list); + } + al_array_free(search->result.pages); +} + +void sho_python_close(void) +{ + if (Py_IsInitialized()) Py_Finalize(); +} diff --git a/src/shoki/src/search.h b/src/shoki/src/search.h new file mode 100644 index 0000000..97e40ae --- /dev/null +++ b/src/shoki/src/search.h @@ -0,0 +1,30 @@ +#pragma once + +#include "post.h" + +struct sho_result_page { + s32 num; + array(struct sho_post) posts; + array(str) list; +}; + +struct sho_result { + array(struct sho_result_page) pages; +}; + +struct sho_search { + s32 id; + s32 page; + struct sho_result result; +}; + +bool sho_python_init(void); +void sho_python_close(void); + +void sho_result_init(struct sho_result *res); +void sho_result_add_post(struct sho_result *res, s32 num, struct sho_post *post); +void sho_result_add_to_list(struct sho_result *res, s32 num, str *unique_id); + +void sho_search_init(struct sho_search *search); +s32 sho_search_more_results(struct sho_search *search, str *provider, str *query); +void sho_search_free(struct sho_search *s); diff --git a/src/tree/commands.h b/src/tree/commands.h new file mode 100644 index 0000000..1b1187e --- /dev/null +++ b/src/tree/commands.h @@ -0,0 +1,13 @@ +#pragma once + +enum { + TREE_NODE = 0, + TREE_CLIENT, + TREE_SINK +}; + +enum { + TREE_CMD_IDENTIFY = 0, + TREE_CMD_SEARCH, + TREE_CMD_ADD +}; diff --git a/src/tree/meson.build b/src/tree/meson.build new file mode 100644 index 0000000..c7301f9 --- /dev/null +++ b/src/tree/meson.build @@ -0,0 +1,3 @@ +tree_src = ['tree.c', 'resource_manager.c'] +tree_deps = [common_deps, shoki, cache, cap, bimu, av] +executable('tree', sources: tree_src, dependencies: tree_deps) diff --git a/src/tree/resource_manager.c b/src/tree/resource_manager.c new file mode 100644 index 0000000..0e95fbb --- /dev/null +++ b/src/tree/resource_manager.c @@ -0,0 +1,75 @@ +#include <al/random.h> +#include <aki/http.h> + +#include "resource_manager.h" + +static u8 packet_pool_callback(void *userdata, struct aki_packet *packet) +{ + (void)userdata; + struct tree_resource_request *request = (struct tree_resource_request *)packet->userdata; + aki_packet_stream_send_packet(request->stream, packet); + return AKI_PACKET_POOL_KEEP; +} + +static void packet_callback(void *userdata, struct aki_packet_stream *stream, struct aki_packet *packet) +{ + struct tree_resource_server *server = (struct tree_resource_server *)userdata; + struct tree_resource_request *request = al_alloc_object(struct tree_resource_request); + request->stream = stream; + request->id = aki_packet_read_u16(packet); + str unique_id; + aki_packet_read_string(packet, &unique_id); + al_str_clone(&request->unique_id, &unique_id); + request->index = aki_packet_read_u32(packet); + request->packet = aki_packet_pool_get(&server->pool); + request->packet->userdata = request; + request->server = server; + if (!server->request_resource(server->userdata, request)) { + aki_packet_pool_return(&server->pool, request->packet); + } + aki_packet_free(packet); +} + +static void connection_closed_callback(void *userdata, struct aki_packet_stream *stream) +{ + (void)userdata; + (void)stream; +} + +static void packet_sent_callback(void *userdata, struct aki_packet *packet) +{ + struct tree_resource_server *server = (struct tree_resource_server *)userdata; + struct tree_resource_request *request = (struct tree_resource_request *)packet->userdata; + aki_packet_pool_return(&server->pool, packet); + aki_http_request_close(&request->request); +} + +static void flushed_callback(void *userdata) +{ + (void)userdata; +} + +static void connection_callback(void *userdata, struct aki_packet_stream *stream) +{ + stream->userdata = userdata; + stream->packet_callback = packet_callback; + stream->packet_sent_callback = packet_sent_callback; + stream->connection_closed_callback = connection_closed_callback; + stream->flushed_callback = flushed_callback; +} + +bool tree_resource_server_init(struct tree_resource_server *server, + bool (*request_resource)(void *, struct tree_resource_request *), void *userdata) +{ + server->request_resource = request_resource; + server->userdata = userdata; + return aki_packet_stream_init(&server->server, AKI_SOCKET_TCP, connection_callback, + NULL, NULL, NULL, server); +} + +void tree_resource_server_listen(struct tree_resource_server *server, + struct aki_event_loop *loop, str *addr, s32 port) +{ + aki_packet_pool_init(&server->pool, 25, loop, packet_pool_callback, server); + aki_packet_stream_listen(&server->server, loop, addr, port); +} diff --git a/src/tree/resource_manager.h b/src/tree/resource_manager.h new file mode 100644 index 0000000..a81e3a9 --- /dev/null +++ b/src/tree/resource_manager.h @@ -0,0 +1,27 @@ +#pragma once + +#include <aki/packet_stream.h> +#include <aki/packet_pool.h> +#include <aki/http.h> + +struct tree_resource_request { + u16 id; + str unique_id; + u32 index; + struct aki_packet *packet; + struct aki_packet_stream *stream; + struct aki_http_request request; + struct tree_resource_server *server; +}; + +struct tree_resource_server { + struct aki_packet_stream server; + struct aki_packet_pool pool; + bool (*request_resource)(void *, struct tree_resource_request *); + void *userdata; +}; + +bool tree_resource_server_init(struct tree_resource_server *server, + bool (*request_resource)(void *, struct tree_resource_request *), void *userdata); +void tree_resource_server_listen(struct tree_resource_server *server, + struct aki_event_loop *loop, str *addr, s32 port); diff --git a/src/tree/tree.c b/src/tree/tree.c new file mode 100644 index 0000000..0947be0 --- /dev/null +++ b/src/tree/tree.c @@ -0,0 +1,298 @@ +#include <al/log.h> +#include <aki/file.h> +#include <jansson.h> + +#include "../libclient/commands.h" + +#include "tree.h" + +#define USER_AGENT al_str_c("Mozilla/5.0 (X11; Linux x86_64; rv:96.0) Gecko/20100101 Firefox/96.0") + +static void http_callback(void *userdata, struct aki_http_request *response, bool success) +{ + struct tree_resource_request *request = (struct tree_resource_request *)userdata; + struct aki_packet *packet = request->packet; + if (!success) { + aki_packet_pool_return(&request->server->pool, packet); + return; + } + aki_packet_write_u16(packet, request->id); + aki_packet_write_buffer(packet, &response->response); + aki_packet_pool_submit(&request->server->pool, packet); +} + +static bool request_resource(void *userdata, struct tree_resource_request *request) +{ + struct tree_server *tree = (struct tree_server *)userdata; + struct sho_post *post = sho_post_cache_get(&tree->cache, &request->unique_id); + if (!post) return false; + str *url = NULL; + if (al_str_eq(&post->author.unique_id, &request->unique_id)) { + url = &post->author.profile_image_url; + } else { + if (post->media.size <= request->index) return false; + struct sho_post_media *media = &al_array_at(post->media, request->index); + url = &media->thumbnail_url; + } + aki_http_request_init(&request->request); + aki_http_set_url(&request->request.http, url); + aki_http_set_user_agent(&request->request.http, USER_AGENT); + aki_http_request(&request->request, AKI_HTTP_GET, &tree->loop, http_callback, request); + return true; +} + +static struct tree_user *get_user_by_username(struct tree_server *tree, str *username) +{ + struct tree_user *user; + al_array_foreach(tree->users, i, user) { + if (al_str_eq(&user->username, username)) { + return user; + } + } + return NULL; +} + +static struct tree_user *get_user_by_connection(struct tree_server *tree, struct aki_rpc_connection *conn) +{ + struct tree_user *user; + al_array_foreach(tree->users, i, user) { + struct tree_client *client; + al_array_foreach(user->clients, j, client) { + if (client->conn == conn) { + return user; + } + } + } + return NULL; +} + +static bool identify_command_callback(void *userdata, struct aki_rpc_connection *conn, + struct aki_packet *packet, struct aki_packet *rpacket) +{ + struct tree_server *tree = (struct tree_server *)userdata; + (void)rpacket; + switch (aki_packet_read_u8(packet)) { + case TREE_NODE: { + struct tree_node *node = al_alloc_object(struct tree_node); + node->conn = conn; + al_array_push(tree->nodes, node); + break; + } + case TREE_CLIENT: { + str username; + aki_packet_read_string(packet, &username); + struct tree_user *user = get_user_by_username(tree, &username); + if (user) { + al_log_info("tree", "User \"%.*s\" logged in.", AL_STR_PRINTF(&user->username)); + struct tree_client *client = al_alloc_object(struct tree_client); + client->conn = conn; + al_array_push(tree->clients, client); + al_array_push(user->clients, client); + } + break; + } + case TREE_SINK: { + struct tree_sink *sink = al_alloc_object(struct tree_sink); + sink->conn = conn; + al_array_push(tree->sinks, sink); + break; + } + } + aki_packet_free(packet); + return true; +} + +static struct tree_query *query_from_id(struct tree_user *user, s32 id) +{ + struct tree_query *query; + al_array_foreach_ptr(user->queries, i, query) { + if (query->search.id == id) { + return query; + } + } + al_array_push(user->queries, (struct tree_query){}); + query = &al_array_last(user->queries); + query->id = id; + sho_search_init(&query->search); + return query; +} + +static bool search_command_callback(void *userdata, struct aki_rpc_connection *conn, + struct aki_packet *packet, struct aki_packet *rpacket) +{ + struct tree_server *tree = (struct tree_server *)userdata; + struct tree_user *user = get_user_by_connection(tree, conn); + if (!user) { + aki_packet_write_s32(rpacket, -1); + goto out; + } + s32 id = aki_packet_read_s32(packet); + struct tree_query *query = query_from_id(user, id); + str provider, query_str; + aki_packet_read_string(packet, &provider); + aki_packet_read_string(packet, &query_str); + s32 page_num = sho_search_more_results(&query->search, &provider, &query_str); + if (page_num < 0) { + al_log_error("tree", "Search failed."); + aki_packet_write_s32(rpacket, -1); + goto out; + } + struct sho_result_page *page = &al_array_at(query->search.result.pages, page_num); + aki_packet_write_s32(rpacket, query->search.id); + aki_packet_write_s32(rpacket, page_num); + aki_packet_write_u32(rpacket, page->posts.size); + struct sho_post *post; + al_array_foreach_ptr(page->posts, i, post) { + aki_packet_write_sho_post(rpacket, post); + sho_post_cache_push(&tree->cache, post); + } + aki_packet_write_u32(rpacket, page->list.size); + str *unique_id; + al_array_foreach_ptr(page->list, i, unique_id) { + aki_packet_write_string(rpacket, unique_id); + } +out: + aki_packet_free(packet); + return true; +} + +static void connection_callback(void *userdata, struct aki_rpc_connection *conn) +{ + (void)userdata; + (void)conn; +} + +static void connection_closed_callback(void *userdata, struct aki_rpc_connection *conn) +{ + (void)userdata; + (void)conn; +} + +static struct aki_rpc_command commands[] = { + { .op = TREE_CMD_IDENTIFY, .callback = identify_command_callback, .userdata = NULL }, + // Client commands. + { .op = TREE_CMD_SEARCH, .callback = search_command_callback, .userdata = NULL }, +}; + +static struct tree_server tree; + +void sigint_handler(s32 signum) +{ + (void)signum; + // explode. + exit(EXIT_SUCCESS); +} + +static bool open_user(struct tree_server *tree, struct aki_dir_entry *dir) +{ + struct aki_file file; + if (!aki_file_open(&file, &dir->path, false)) { + return false; + } + str s; + aki_file_read_as_str(&file, &s); + json_error_t error; + json_t *json = json_loadb(s.data, s.len, 0, &error); + struct tree_user *user = al_alloc_object(struct tree_user); + al_str_from(&user->username, json_string_value(json_object_get(json, "username"))); + al_array_init(user->queries); + al_array_init(user->lists); + struct tree_list default_list; + al_str_from(&default_list.name, "default"); + al_array_push(user->lists, default_list); + al_array_init(user->clients); + al_log_info("tree", "Loaded user \"%.*s\"", AL_STR_PRINTF(&user->username)); + al_array_push(tree->users, user); + return true; +} + +static bool open_db(struct tree_server *tree, str *path) +{ + struct aki_dir camu_db; + if (!aki_dir_open(&camu_db, path)) { + return false; + } + struct aki_dir_entry entry; + while (aki_dir_read(&camu_db, &entry)) { + if (al_str_eq(&entry.name, al_str_c("users"))) { + struct aki_dir users; + if (aki_dir_open(&users, &entry.path)) { + struct aki_dir_entry user; + while (aki_dir_read(&users, &user)) { + if (user.type == AKI_ENTRY_FILE) { + open_user(tree, &user); + } + } + aki_dir_close(&users); + } + } + aki_dir_entry_free(&entry); + } + aki_dir_close(&camu_db); + return true; +} + +static bool cap_callback(void *userdata, u8 op, str *name, str *unique_id, void *opaque) +{ + (void)userdata; + (void)name; + (void)unique_id; + (void)opaque; + switch (op) { + case CAP_BUFFER: + return true; + case CAP_SWAP: + break; + case CAP_SET: + break; + case CAP_UNLOAD: + break; + } + return true; +} + +s32 main(void) +{ + aki_common_init(); + + signal(SIGINT, sigint_handler); + + al_array_init(tree.nodes); + al_array_init(tree.clients); + al_array_init(tree.sinks); + al_array_init(tree.users); + + if (!open_db(&tree, al_str_c(CAMU_DB_PATH))) { + return EXIT_FAILURE; + } + + sho_post_cache_init(&tree.cache); + + bool py_init = sho_python_init(); + + aki_event_loop_init(&tree.loop); + + aki_rpc_init(&tree.server, AKI_SOCKET_TCP, connection_callback, + connection_closed_callback, &tree); + for (u32 i = 0; i < AL_ARRAY_SIZE(commands); i++) { + commands[i].userdata = &tree; + aki_rpc_add_command(&tree.server, &commands[i]); + } + aki_rpc_listen(&tree.server, &tree.loop, al_str_c("0.0.0.0"), TREE_PORT); + + tree_resource_server_init(&tree.resource_server, request_resource, &tree); + tree_resource_server_listen(&tree.resource_server, &tree.loop, al_str_c("0.0.0.0"), TREE_RESOURCE_PORT); + + cap_init(&tree.cap, cap_callback, &tree); + + bmu_server_init(&tree.stream_server); + bmu_server_listen(&tree.stream_server, &tree.loop, al_str_c("0.0.0.0"), TREE_STREAM_PORT); + + aki_event_loop_run(&tree.loop); + + if (py_init) sho_python_close(); + + aki_common_close(); + + return EXIT_SUCCESS; +} diff --git a/src/tree/tree.h b/src/tree/tree.h new file mode 100644 index 0000000..06f98b1 --- /dev/null +++ b/src/tree/tree.h @@ -0,0 +1,59 @@ +#pragma once + +#include <aki/rpc2.h> + +#include "../shoki/src/post_cache.h" +#include "../shoki/src/search.h" +#include "../shoki/src/packet_ext.h" +#include "../bimu/server.h" +#include "../fruits/cap/cap.h" + +#include "commands.h" +#include "resource_manager.h" + +#define CAMU_DB_PATH "/home/andrew/c/camu/data/camu_db_test" + +#define TREE_PORT 4356 +#define TREE_RESOURCE_PORT 4357 +#define TREE_STREAM_PORT 4358 + +struct tree_node { + struct aki_rpc_connection *conn; +}; + +struct tree_client { + struct aki_rpc_connection *conn; +}; + +struct tree_sink { + struct aki_rpc_connection *conn; +}; + +struct tree_query { + s32 id; + struct sho_search search; +}; + +struct tree_list { + str name; +}; + +struct tree_user { + str username; + array(struct tree_query) queries; + array(struct tree_list) lists; + array(struct tree_client *) clients; +}; + +struct tree_server { + struct aki_event_loop loop; + struct aki_rpc server; + array(struct tree_node *) nodes; + array(struct tree_client *) clients; + array(struct tree_sink *) sinks; + array(struct tree_user *) users; + struct tree_resource_server resource_server; + struct sho_post_cache cache; + struct cap_runner cap; + struct bmu_server stream_server; +}; diff --git a/src/util/blocking_ring_buffer.c b/src/util/blocking_ring_buffer.c new file mode 100644 index 0000000..2160fec --- /dev/null +++ b/src/util/blocking_ring_buffer.c @@ -0,0 +1,199 @@ +#include <al/lib.h> + +#include "blocking_ring_buffer.h" + +#define LOCK(n) \ + buffer->req_start = n; \ + aki_cond_wait(&buffer->cond, &buffer->mutex); + +#define SIGNAL() \ + aki_cond_signal(&buffer->cond); + +void camu_ring_buffer_init(struct camu_ring_buffer *buffer, s32 size) +{ + buffer->size = size; + buffer->data = (u8 *)al_malloc(buffer->size); + aki_mutex_init(&buffer->mutex); + aki_cond_init(&buffer->cond); + camu_ring_buffer_reset(buffer); + buffer->reset_unlock = false; + buffer->enabled = true; +} + +// TODO: does locking here actually prevent this from messing something up. +void camu_ring_buffer_reset(struct camu_ring_buffer *buffer) +{ + aki_mutex_lock(&buffer->mutex); + buffer->valid_size = 0; + buffer->start = 0; + buffer->end = 0; + // If get_chunk was waiting it doesn't matter where req_start + // was, there should be room now. + if (aki_cond_is_waiting(&buffer->cond)) { + buffer->reset_unlock = true; + SIGNAL(); + } + buffer->wrap = false; + aki_mutex_unlock(&buffer->mutex); +} + +void camu_ring_buffer_disable(struct camu_ring_buffer *buffer) +{ + aki_mutex_lock(&buffer->mutex); + buffer->enabled = false; + // No state on the buffer will be edited by the time a get_chunk + // returns because of this. So, the next call to get_chunk after + // the buffer is re-enabled should be correct. + if (aki_cond_is_waiting(&buffer->cond)) { + buffer->reset_unlock = true; + SIGNAL(); + } + aki_mutex_unlock(&buffer->mutex); +} + +void camu_ring_buffer_enable(struct camu_ring_buffer *buffer) +{ + aki_mutex_lock(&buffer->mutex); + buffer->enabled = true; + aki_mutex_unlock(&buffer->mutex); +} + +s32 camu_ring_buffer_space(struct camu_ring_buffer *buffer) +{ + s32 available = 0; + aki_mutex_lock(&buffer->mutex); + if (buffer->end < buffer->start) { + available = buffer->start - buffer->end; + } else { + available = buffer->size - buffer->end; + } + aki_mutex_unlock(&buffer->mutex); + return available; +} + +s32 camu_ring_buffer_occupied(struct camu_ring_buffer *buffer) +{ + s32 filled = 0; + aki_mutex_lock(&buffer->mutex); + if (buffer->end < buffer->start) { + filled = (buffer->valid_size - buffer->start) + buffer->end; + } else { + // If the buffer is empty this will equate to 0. + filled = buffer->end - buffer->start; + } + aki_mutex_unlock(&buffer->mutex); + return filled; +} + +u8 *camu_ring_buffer_get_chunk(struct camu_ring_buffer *buffer, s32 n) +{ + aki_mutex_lock(&buffer->mutex); + + if (!buffer->enabled) { + return NULL; + } + + if (buffer->end < buffer->start) { + if (buffer->end + n > buffer->size) { + // Unsatisfiable, need to increase buffer size. + al_assert(n < buffer->end); + // Wait until read to the end of the buffer, wrap around, and read + // past the amount of data requested in this call (n). + buffer->wrap = true; + LOCK(n); + } else if (buffer->end + n >= buffer->start) { + // Wait until we read past our current write position plus the amount + // requested in this call (n). + LOCK(buffer->end + n); + } + } else if (buffer->end + n > buffer->size) { + // valid_size now marks the end of valid data in the buffer, this may be + // less than the actual buffer size. + buffer->valid_size = buffer->end; + if (n >= buffer->start) { + // Unsatisfiable, need to increase buffer size. + al_assert(n < buffer->valid_size); + // Wait until we read past at least the amount of data requested in this call (n). + LOCK(n); + } + // Set end to zero after waiting because it's possible start is also 0 at this point + // and we would end up with start == end while the buffer is full with data. + buffer->end = 0; + } + + // If a reset happened while waiting, the behavior for the structure is to assume + // you didn't want watever data you were waiting to write. + if (buffer->reset_unlock) { + buffer->reset_unlock = false; + return NULL; + } + + // Return a pointer to the start of the writable data and advance end by + // exactly the amount requested (n). + u8 *data = &buffer->data[buffer->end]; + buffer->end += n; + + return data; +} + +s32 camu_ring_buffer_read(struct camu_ring_buffer *buffer, u8 **data, s32 n) +{ + aki_mutex_lock(&buffer->mutex); + + // Set data even if we are going to read 0 bytes to avoid possible issues + // with data being NULL after a call to read. + *data = &buffer->data[buffer->start]; + + // If start == end, the buffer is empty. + if (buffer->start == buffer->end) { + return 0; + } + + if (buffer->end < buffer->start) { + // If we would read past the valid data in the buffer, instead + // read to the end of the valid data. + if (buffer->start + n > buffer->valid_size) { + n = buffer->valid_size - buffer->start; + } + } else if (buffer->start + n > buffer->end) { + // If we would read to or past end, instead read to the end. + n = buffer->end - buffer->start; + } + + buffer->start += n; + + // If we read to the end of the valid data, wrap around to the begining. + //if (buffer->start == buffer->valid_size && buffer->start != buffer->end) { + if (buffer->start == buffer->valid_size) { + buffer->start = 0; + buffer->valid_size = 0; + // We wrapped and aren't specifically waiting for a position after the wrap, so unlock. + if (aki_cond_is_waiting(&buffer->cond) && !buffer->wrap) { + SIGNAL(); + } + buffer->wrap = false; + } else if (aki_cond_is_waiting(&buffer->cond) && (!buffer->wrap && buffer->start > buffer->req_start)) { + // We read past the position we were waiting for, so unlock. + SIGNAL(); + } + + // After SIGNAL(), LOCK() won't proceed until this thread unlocks. + + return n; +} + +// Unlock the mutex, this needs to be called after a call to either get_chunk() or read() but +// only after the pointer has been used. +void camu_ring_buffer_unlock(struct camu_ring_buffer *buffer) +{ + aki_mutex_unlock(&buffer->mutex); +} + +void camu_ring_buffer_free(struct camu_ring_buffer *buffer) +{ + if (buffer->data) { + al_free(buffer->data); + } + aki_mutex_destroy(&buffer->mutex); + aki_cond_destroy(&buffer->cond); +} diff --git a/src/util/blocking_ring_buffer.h b/src/util/blocking_ring_buffer.h new file mode 100644 index 0000000..ff3521e --- /dev/null +++ b/src/util/blocking_ring_buffer.h @@ -0,0 +1,42 @@ +#pragma once + +#include <al/types.h> +#include <aki/thread.h> + +// Ring buffer with blocking write and non-blocking read. +// - Write can block and always returns the exact amount requested. +// - Read will return as many bytes as possible, which can be less than requested or 0. + +struct camu_ring_buffer { + u8 *data; + s32 size; + // The position of the last valid byte at the end of the buffer. + // This can be less than the total size of the buffer. + s32 valid_size; + s32 start; + s32 end; + // The last byte in the chunk that we are waiting to be fully available. + s32 req_start; + // True if we need to wait for the start to wrap around + // to the begining before considering signaling. + bool wrap; + struct aki_mutex mutex; + struct aki_cond cond; + // Signal get_chunk to return null and not modifiy the buffer + // if it was blocking and reset was called. + bool reset_unlock; + // If the buffer is disabled, all calls to get_chunk will return + // early and not block. + bool enabled; +}; + +void camu_ring_buffer_init(struct camu_ring_buffer *buffer, s32 size); +void camu_ring_buffer_reset(struct camu_ring_buffer *buffer); +void camu_ring_buffer_disable(struct camu_ring_buffer *buffer); +void camu_ring_buffer_enable(struct camu_ring_buffer *buffer); +s32 camu_ring_buffer_space(struct camu_ring_buffer *buffer); +s32 camu_ring_buffer_occupied(struct camu_ring_buffer *buffer); +u8 *camu_ring_buffer_get_chunk(struct camu_ring_buffer *buffer, s32 n); +s32 camu_ring_buffer_read(struct camu_ring_buffer *buffer, u8 **data, s32 n); +void camu_ring_buffer_unlock(struct camu_ring_buffer *buffer); +void camu_ring_buffer_free(struct camu_ring_buffer *buffer); diff --git a/src/util/color_palette.c b/src/util/color_palette.c new file mode 100644 index 0000000..e55c9e1 --- /dev/null +++ b/src/util/color_palette.c @@ -0,0 +1,62 @@ +#include <aki/file.h> +#include <jansson.h> + +#include "color_palette.h" + +struct camu_color_palette global_color_palette = {0}; + +static char *special_colors[2] = { + "background", + "foreground" +}; + +static char *normal_colors[16] = { + "color0", + "color1", + "color2", + "color3", + "color4", + "color5", + "color6", + "color7", + "color8", + "color9", + "color10", + "color11", + "color12", + "color13", + "color14", + "color15" +}; + +bool camu_color_palette_init(str *path) +{ + struct aki_file file; + if (!aki_file_open(&file, path, false)) { + return false; + } + str s; + aki_file_read_as_str(&file, &s); + aki_file_close(&file); + json_error_t error; + json_t *root = json_loadb(s.data, s.len, 0, &error); + if (!root) return false; + json_t *special = json_object_get(root, "special"); + json_t *colors = json_object_get(root, "colors"); + if (!special || !colors) return false; + json_t *object; + const char *color; + for (u32 i = 0; i < AL_ARRAY_SIZE(special_colors); i++) { + object = json_object_get(special, special_colors[i]); + if (!object) return false; + color = json_string_value(object); + global_color_palette.colors[i] = (u32)al_str_to_long(al_str_w((char *)color, 1, 6), 16); + } + for (u32 i = 0; i < AL_ARRAY_SIZE(normal_colors); i++) { + object = json_object_get(colors, normal_colors[i]); + if (!object) return false; + color = json_string_value(object); + global_color_palette.colors[i + 2] = (u32)al_str_to_long(al_str_w((char *)color, 1, 6), 16); + } + return true; +} diff --git a/src/util/color_palette.h b/src/util/color_palette.h new file mode 100644 index 0000000..a71360c --- /dev/null +++ b/src/util/color_palette.h @@ -0,0 +1,35 @@ +#pragma once + +#include <al/types.h> +#include <al/str.h> + +enum { + CAMU_COLOR_BACKGROUND = 0, + CAMU_COLOR_FOREGROUND, + CAMU_COLOR0, + CAMU_COLOR1, + CAMU_COLOR2, + CAMU_COLOR3, + CAMU_COLOR4, + CAMU_COLOR5, + CAMU_COLOR6, + CAMU_COLOR7, + CAMU_COLOR8, + CAMU_COLOR9, + CAMU_COLOR10, + CAMU_COLOR11, + CAMU_COLOR12, + CAMU_COLOR13, + CAMU_COLOR14, + CAMU_COLOR15 +}; + +#define CAMU_COLOR_PALETTE_COUNT 18 + +struct camu_color_palette { + u32 colors[CAMU_COLOR_PALETTE_COUNT]; +}; + +extern struct camu_color_palette global_color_palette; + +bool camu_color_palette_init(str *path); diff --git a/src/util/meson.build b/src/util/meson.build new file mode 100644 index 0000000..3cdaf28 --- /dev/null +++ b/src/util/meson.build @@ -0,0 +1,3 @@ +util_src = ['color_palette.c', 'blocking_ring_buffer.c'] +util_deps = [] +util = declare_dependency(sources: util_src, dependencies: util_deps) diff --git a/src/util/queue.h b/src/util/queue.h new file mode 100644 index 0000000..032bb55 --- /dev/null +++ b/src/util/queue.h @@ -0,0 +1,43 @@ +#pragma once + +#include <al/array.h> +#include <aki/thread.h> + +#define queue(type) \ + struct { \ + array(type) a; \ + struct aki_mutex mutex; \ + } + +#define camu_queue_size(q, r) \ + do { \ + aki_mutex_lock(&(q).mutex); \ + r = (q).a.size; \ + aki_mutex_unlock(&(q).mutex); \ + } while (0) + +#define camu_queue_init(q) \ + do { \ + al_array_init((q).a); \ + aki_mutex_init(&(q).mutex); \ + } while (0) + +#define camu_queue_push(q, item) \ + do { \ + aki_mutex_lock(&(q).mutex); \ + al_array_push((q).a, item); \ + aki_mutex_unlock(&(q).mutex); \ + } while (0) + +#define camu_queue_pop(q, r) \ + do { \ + aki_mutex_lock(&(q).mutex); \ + al_array_pop_at((q).a, 0, r); \ + aki_mutex_unlock(&(q).mutex); \ + } while (0) + +#define camu_queue_free(q) \ + do { \ + aki_mutex_destroy(&(q).mutex); \ + al_array_free((q).a); \ + } while (0) |