summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.editorconfig5
-rw-r--r--.gitignore17
-rw-r--r--NOTES34
-rw-r--r--README.md1
-rw-r--r--flake.nix65
-rw-r--r--meson.build50
-rw-r--r--meson_options.txt0
-rw-r--r--pyrightconfig.json6
-rwxr-xr-xscripts/clang_cl_env.bat3
-rwxr-xr-xscripts/copy_libs.sh6
-rwxr-xr-xscripts/line_count.sh3
-rwxr-xr-xscripts/msvc_env.bat3
-rwxr-xr-xscripts/msys2_env.sh5
-rwxr-xr-xscripts/set_al_env.sh4
-rw-r--r--src/bimu/client.c1
-rw-r--r--src/bimu/client.h11
-rw-r--r--src/bimu/common.h7
-rw-r--r--src/bimu/handler.h35
-rw-r--r--src/bimu/handlers/codec.h20
-rw-r--r--src/bimu/handlers/codec_client.c109
-rw-r--r--src/bimu/handlers/codec_server.c159
-rw-r--r--src/bimu/local.c281
-rw-r--r--src/bimu/local.h53
-rw-r--r--src/bimu/meson.build8
-rw-r--r--src/bimu/server.c46
-rw-r--r--src/bimu/server.h21
-rw-r--r--src/buffer/audio.c361
-rw-r--r--src/buffer/audio.h59
-rw-r--r--src/buffer/clock.c53
-rw-r--r--src/buffer/clock.h16
-rw-r--r--src/buffer/common.h9
-rw-r--r--src/buffer/frame_queue.h28
-rw-r--r--src/buffer/meson.build8
-rw-r--r--src/buffer/peak_buffer.c24
-rw-r--r--src/buffer/peak_buffer.h13
-rw-r--r--src/buffer/video.c148
-rw-r--r--src/buffer/video.h42
-rw-r--r--src/cache/backing.h20
-rw-r--r--src/cache/backings/file.c75
-rw-r--r--src/cache/backings/file.h22
-rw-r--r--src/cache/backings/file_common.c23
-rw-r--r--src/cache/backings/file_mapped.c81
-rw-r--r--src/cache/backings/memory.c82
-rw-r--r--src/cache/backings/memory.h16
-rw-r--r--src/cache/entry.c44
-rw-r--r--src/cache/entry.h21
-rw-r--r--src/cache/handle.c100
-rw-r--r--src/cache/handle.h20
-rw-r--r--src/cache/handler.h23
-rw-r--r--src/cache/handlers/file.c49
-rw-r--r--src/cache/handlers/file.h12
-rw-r--r--src/cache/handlers/http.c207
-rw-r--r--src/cache/handlers/http.h24
-rw-r--r--src/cache/meson.build18
-rw-r--r--src/codec/codec.h115
-rw-r--r--src/codec/common.h29
-rw-r--r--src/codec/libav/avio.c13
-rw-r--r--src/codec/libav/avio.h10
-rw-r--r--src/codec/libav/common.c6
-rw-r--r--src/codec/libav/common.h6
-rw-r--r--src/codec/libav/decoder.c165
-rw-r--r--src/codec/libav/decoder.h20
-rw-r--r--src/codec/libav/demuxer.c184
-rw-r--r--src/codec/libav/demuxer.h21
-rw-r--r--src/codec/libav/encoder.c186
-rw-r--r--src/codec/libav/encoder.h26
-rw-r--r--src/codec/libav/packet_ext.c168
-rw-r--r--src/codec/libav/packet_ext.h19
-rw-r--r--src/codec/libav/resampler.c163
-rw-r--r--src/codec/libav/resampler.h40
-rw-r--r--src/codec/libav/scaler.c79
-rw-r--r--src/codec/libav/scaler.h24
-rw-r--r--src/codec/meson.build67
-rw-r--r--src/codec/spng/decoder.h11
-rw-r--r--src/codec/spng/demuxer.h12
-rw-r--r--src/codec/spng/impl.c166
-rw-r--r--src/codec/stb_image/decoder.h11
-rw-r--r--src/codec/stb_image/demuxer.h12
-rw-r--r--src/codec/stb_image/impl.c155
-rw-r--r--src/codec/wuffs/decoder.h13
-rw-r--r--src/codec/wuffs/demuxer.h12
-rw-r--r--src/codec/wuffs/impl.c213
-rw-r--r--src/fruits/cap/cap.c219
-rw-r--r--src/fruits/cap/cap.h42
-rw-r--r--src/fruits/cap/meson.build4
-rw-r--r--src/fruits/cmv/cmv2.c331
-rw-r--r--src/fruits/cmv/meson.build26
-rw-r--r--src/fruits/cmv/tui.c118
-rw-r--r--src/fruits/cmv/tui.h21
-rw-r--r--src/libclient/client.c98
-rw-r--r--src/libclient/client.h31
-rw-r--r--src/libclient/commands.h1
-rw-r--r--src/libclient/meson.build3
-rw-r--r--src/libclient/resource_client.c91
-rw-r--r--src/libclient/resource_client.h29
-rw-r--r--src/libclient/search.c31
-rw-r--r--src/libclient/search.h28
-rw-r--r--src/libsink/meson.build2
-rw-r--r--src/libsink/sink.c632
-rw-r--r--src/libsink/sink.h109
-rw-r--r--src/mixer/audio.h37
-rw-r--r--src/mixer/audio_miniaudio.c190
-rw-r--r--src/mixer/audio_miniaudio.h20
-rw-r--r--src/mixer/meson.build12
-rw-r--r--src/mixer/mixer.c167
-rw-r--r--src/mixer/mixer.h37
-rw-r--r--src/render/meson.build35
-rw-r--r--src/render/queue_libplacebo.c195
-rw-r--r--src/render/queue_libplacebo.h17
-rw-r--r--src/render/queue_tiger.c92
-rw-r--r--src/render/queue_tiger.h13
-rw-r--r--src/render/renderer.h45
-rw-r--r--src/render/renderer_libplacebo.c237
-rw-r--r--src/render/renderer_libplacebo.h38
-rw-r--r--src/render/renderer_tiger.c305
-rw-r--r--src/render/renderer_tiger.h31
-rw-r--r--src/screen/meson.build4
-rw-r--r--src/screen/screen.c402
-rw-r--r--src/screen/screen.h78
-rw-r--r--src/screen/view.c28
-rw-r--r--src/screen/view.h23
-rw-r--r--src/shoki/cpy/lib.pxd13
-rw-r--r--src/shoki/cpy/post.pxd58
-rw-r--r--src/shoki/cpy/result.pxd8
-rw-r--r--src/shoki/cpy/setup.py6
-rw-r--r--src/shoki/cpy/shoki.pyx132
-rw-r--r--src/shoki/cpy/str.pxd11
-rw-r--r--src/shoki/cpy/types.pxd11
-rw-r--r--src/shoki/meson.build14
-rw-r--r--src/shoki/py/__init__.py0
-rw-r--r--src/shoki/py/client.py51
-rw-r--r--src/shoki/py/config.def.py19
-rw-r--r--src/shoki/py/log.py30
-rw-r--r--src/shoki/py/post.py70
-rw-r--r--src/shoki/py/provider.py39
-rw-r--r--src/shoki/py/providers/__init__.py0
-rw-r--r--src/shoki/py/providers/fanbox.py130
-rw-r--r--src/shoki/py/providers/instagram.py96
-rw-r--r--src/shoki/py/providers/pixiv.py113
-rw-r--r--src/shoki/py/providers/searx.py93
-rw-r--r--src/shoki/py/providers/twitter.py425
-rw-r--r--src/shoki/py/providers/youtube.py115
-rw-r--r--src/shoki/py/query_parser.py39
-rw-r--r--src/shoki/py/search.py35
-rw-r--r--src/shoki/src/packet_ext.c91
-rw-r--r--src/shoki/src/packet_ext.h8
-rw-r--r--src/shoki/src/post.c55
-rw-r--r--src/shoki/src/post.h78
-rw-r--r--src/shoki/src/post_cache.c30
-rw-r--r--src/shoki/src/post_cache.h13
-rw-r--r--src/shoki/src/search.c99
-rw-r--r--src/shoki/src/search.h30
-rw-r--r--src/tree/commands.h13
-rw-r--r--src/tree/meson.build3
-rw-r--r--src/tree/resource_manager.c75
-rw-r--r--src/tree/resource_manager.h27
-rw-r--r--src/tree/tree.c298
-rw-r--r--src/tree/tree.h59
-rw-r--r--src/util/blocking_ring_buffer.c199
-rw-r--r--src/util/blocking_ring_buffer.h42
-rw-r--r--src/util/color_palette.c62
-rw-r--r--src/util/color_palette.h35
-rw-r--r--src/util/meson.build3
-rw-r--r--src/util/queue.h43
164 files changed, 11090 insertions, 0 deletions
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..f989111
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,5 @@
+indent_style=space
+indent_size=4
+tab_width=4
+end_of_line=lf
+trim_trailing_whitespace=true
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..79b95a9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,17 @@
+**/__pycache__/
+build*/
+compile_commands.json
+.cache/
+.clangd/
+flake.lock
+bin/
+data/
+doc/
+old/
+old_old/
+subprojects/
+vshell/
+src/fruits/desktop/
+src/shoki/cpy/shoki.c
+src/shoki/cpy/shoki.h
+src/shoki/py/config.py
diff --git a/NOTES b/NOTES
new file mode 100644
index 0000000..b99ff5b
--- /dev/null
+++ b/NOTES
@@ -0,0 +1,34 @@
+TODO:
+ - [x] Runner for video/audio buffer with clock
+ - [x] I/O abstraction
+ - [x] Write tests for al_ring_buffer
+ - [x] Move some stuff
+ - [x] peak buffer into buffer from util
+ - [x] list from root to cmv
+ - [x] add timer to libakiyo
+ - [x] Clean-up audio buffer
+ - [x] Proper close() in cmv tui
+ - [x] fixup line_processor?
+ - [x] Optimize libev usage, feature flags?
+ - [x] Support PASSTHROUGH in packet_pool_disable
+ - [x] al_alloc_object()
+ - [x] Proper stdio backend in libakiyo
+ - [x] strerror_s, ferror
+ - https://learn.microsoft.com/en-us/cpp/c-runtime-library/errno-doserrno-sys-errlist-and-sys-nerr?view=msvc-170
+ - [x] Cleanup libakiyo
+ - [x] Cleanup camu
+ - [x] Fix TODO's in sink.c
+ - [x] Improve cch_handle "wait for any write"
+ - [ ] Upload everything to git
+ - [ ] Add timeout to aki_http
+ - [ ] Proper threaded upload for libsink
+ - [ ] Solidify scroll
+ - [ ] Complete as possible UI widget for a twitter post
+ - [ ] Add desktop code to git
+ - [ ] Adapt all shoki backends to new post structure
+ - [ ] Finally move archived twitter/pixiv files into a proper "camu_db"
+ - [ ] Windows file backend in libakiyo
+ - [ ] Add idle animation to cmv
+ - https://www.shadertoy.com/view/wtsXRX
+ - [ ] Add node paradigm to the server-client stuff
+ - [ ] Allow cmv to act as a node
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..fdaad82
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+## camu
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..7ed5944
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,65 @@
+{
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+ flake-utils.url = "github:numtide/flake-utils";
+ };
+ outputs = { self, nixpkgs, flake-utils }:
+ flake-utils.lib.eachDefaultSystem
+ (system:
+ let
+ pkgs = import nixpkgs { inherit system; };
+ in
+ with pkgs;
+ {
+ devShells.default = mkShell {
+ nativeBuildInputs = [
+ gcc
+ gdb
+ perf-tools
+ valgrind
+ pkg-config
+ ninja
+ meson
+ cmake
+ wayland-scanner
+ python3
+ python3.pkgs.requests
+ python3.pkgs.pip
+ ];
+ buildInputs = [
+ curl
+ libsodium
+ jansson
+ ffmpeg_6-headless
+ alsa-lib
+ pipewire
+ libpulseaudio
+ shaderc
+ libGL
+ libxkbcommon
+ glib
+ dbus
+ gtk3
+ glew
+ glfw3
+ wayland
+ wayland-protocols
+ egl-wayland
+ # TMP:
+ SDL2
+ lz4
+ glm
+ nlohmann_json
+ ];
+ shellHook = ''
+ # https://nixos.wiki/wiki/Python#Emulating_virtualenv_with_nix-shell
+ export PIP_PREFIX=$(pwd)/vshell/pip_packages
+ export PYTHONPATH="$PIP_PREFIX/${pkgs.python3.sitePackages}:$PYTHONPATH"
+ export PATH="$PIP_PREFIX/bin:$PATH"
+ unset SOURCE_DATE_EPOCH
+ export PS1="\e[33m(camu):\w >\e(B\e[m "
+ '';
+ };
+ }
+ );
+}
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..6a0218d
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,50 @@
+project('camu', ['c', 'cpp'], version: '0.2',
+ default_options: ['warning_level=2', 'c_std=c99', 'cpp_std=c++11'])
+
+compiler = meson.get_compiler('c')
+cmake = import('cmake')
+
+arg_syntax = compiler.get_argument_syntax()
+
+is_debug = get_option('buildtype').startswith('debug')
+
+if not is_debug and arg_syntax == 'gcc'
+ add_project_arguments(['-march=native', '-mtune=native'], language: 'c') # TMP
+endif
+
+alabaster = subproject('libalabaster')
+akiyo = subproject('libakiyo')
+akiyo_has_curl = akiyo.get_variable('akiyo_has_curl')
+akiyo_has_mmap = akiyo.get_variable('akiyo_has_mmap')
+
+common_deps = [alabaster.get_variable('alabaster'), akiyo.get_variable('akiyo')]
+
+subdir('src/util')
+
+common_deps += [util]
+
+if meson.is_subproject() # TMP
+ subdir('src/shoki')
+ subdir('src/buffer')
+ subdir('src/codec')
+ subdir('src/cache')
+ subdir('src/bimu')
+ subdir('src/mixer')
+ subdir('src/libsink')
+ subdir('src/fruits/cap')
+else
+ subdir('src/shoki')
+ subdir('src/screen')
+ subdir('src/buffer')
+ subdir('src/codec')
+ subdir('src/cache')
+ subdir('src/bimu')
+ subdir('src/mixer')
+ subdir('src/render')
+ subdir('src/fruits/cap')
+ subdir('src/libclient')
+ subdir('src/libsink')
+ subdir('src/tree')
+ #subdir('src/desktop')
+ subdir('src/fruits/cmv')
+endif
diff --git a/meson_options.txt b/meson_options.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/meson_options.txt
diff --git a/pyrightconfig.json b/pyrightconfig.json
new file mode 100644
index 0000000..2013e8b
--- /dev/null
+++ b/pyrightconfig.json
@@ -0,0 +1,6 @@
+{
+ "extraPaths": [
+ "./subprojects/searxng"
+ ],
+ "reportUnusedVariable": false
+}
diff --git a/scripts/clang_cl_env.bat b/scripts/clang_cl_env.bat
new file mode 100755
index 0000000..0950a4e
--- /dev/null
+++ b/scripts/clang_cl_env.bat
@@ -0,0 +1,3 @@
+set PATH=%PATH%;C:\msys64\usr\bin;Z:\camu\bin
+set CC=clang-cl
+set CXX=clang-cl
diff --git a/scripts/copy_libs.sh b/scripts/copy_libs.sh
new file mode 100755
index 0000000..02ebd27
--- /dev/null
+++ b/scripts/copy_libs.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+cp subprojects/ffmpeg-6.0/dist/lib/libavutil.a subprojects/ffmpeg-6.0/dist/lib/libavutil.lib
+cp subprojects/ffmpeg-6.0/dist/lib/libavformat.a subprojects/ffmpeg-6.0/dist/lib/libavformat.lib
+cp subprojects/ffmpeg-6.0/dist/lib/libavcodec.a subprojects/ffmpeg-6.0/dist/lib/libavcodec.lib
+cp subprojects/ffmpeg-6.0/dist/lib/libswresample.a subprojects/ffmpeg-6.0/dist/lib/libswresample.lib
+cp subprojects/ffmpeg-6.0/dist/lib/libswscale.a subprojects/ffmpeg-6.0/dist/lib/libswscale.lib
diff --git a/scripts/line_count.sh b/scripts/line_count.sh
new file mode 100755
index 0000000..a99b2c4
--- /dev/null
+++ b/scripts/line_count.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env sh
+find ./src -type f ! -path './src/shoki/cpy/shoki.[c|h]' ! -path '*__pycache__*' \
+ ! -path './src/fruits/desktop/camu.[c|h][c|h]' | xargs wc -l
diff --git a/scripts/msvc_env.bat b/scripts/msvc_env.bat
new file mode 100755
index 0000000..ea41011
--- /dev/null
+++ b/scripts/msvc_env.bat
@@ -0,0 +1,3 @@
+set PATH=%PATH%;C:\msys64\usr\bin;Z:\camu\bin
+set CC=cl
+set CXX=cl
diff --git a/scripts/msys2_env.sh b/scripts/msys2_env.sh
new file mode 100755
index 0000000..ea88557
--- /dev/null
+++ b/scripts/msys2_env.sh
@@ -0,0 +1,5 @@
+export PATH="/c/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.37.32822/bin/Hostx64/x64/":"/c/Program Files/Microsoft Visual Studio/2022/Community/VC/Auxiliary/Build":"/z/camu/bin":$PATH
+#export CC=cl
+#export CXX=cl
+export CC=clang
+export CXX=clang++
diff --git a/scripts/set_al_env.sh b/scripts/set_al_env.sh
new file mode 100755
index 0000000..2a17d11
--- /dev/null
+++ b/scripts/set_al_env.sh
@@ -0,0 +1,4 @@
+echo "Setting env using build-dir $1..."
+export LD_LIBRARY_PATH=$HOME/c/camu/$1/subprojects/libakiyo/subprojects/libalabaster
+export LIBRARY_PATH=$HOME/c/camu/$1/subprojects/libakiyo/subprojects/libalabaster
+export C_INCLUDE_PATH=$HOME/c/camu/subprojects/libakiyo/subprojects/libalabaster/include
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)