summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore5
-rw-r--r--NOTES37
-rw-r--r--doc/database.txt8
-rw-r--r--doc/design.txt54
-rw-r--r--doc/pal_vs_ntsc_aspect_ratio.pngbin0 -> 72101 bytes
-rw-r--r--doc/references.txt58
-rw-r--r--flake.lock12
-rw-r--r--flake.nix29
-rw-r--r--meson.build46
-rwxr-xr-xscripts/run.sh5
-rwxr-xr-xscripts/run_debug.sh4
-rwxr-xr-xscripts/run_tree.sh4
-rw-r--r--src/bimu/client.c187
-rw-r--r--src/bimu/client.h22
-rw-r--r--src/bimu/common.h15
-rw-r--r--src/bimu/handler.h4
-rw-r--r--src/bimu/handlers/codec_client.c3
-rw-r--r--src/bimu/handlers/codec_server.c95
-rw-r--r--src/bimu/local.c18
-rw-r--r--src/bimu/local.h1
-rw-r--r--src/bimu/server.c305
-rw-r--r--src/bimu/server.h35
-rw-r--r--src/buffer/audio.c59
-rw-r--r--src/buffer/audio.h1
-rw-r--r--src/buffer/clock.c10
-rw-r--r--src/buffer/clock.h2
-rw-r--r--src/buffer/common.h4
-rw-r--r--src/buffer/video.c65
-rw-r--r--src/buffer/video.h5
-rw-r--r--src/cache/backings/file_common.c3
-rw-r--r--src/cache/backings/file_mapped.c1
-rw-r--r--src/cache/handlers/http.c7
-rw-r--r--src/codec/codec.h6
-rw-r--r--src/codec/libav/demuxer.c56
-rw-r--r--src/codec/libav/packet_ext.h1
-rw-r--r--src/codec/libav/scaler.c25
-rw-r--r--src/codec/meson.build17
-rw-r--r--src/codec/stb_image/impl.c2
-rw-r--r--src/fruits/cap/cap.c59
-rw-r--r--src/fruits/cap/cap.h9
-rw-r--r--src/fruits/cap/meson.build6
-rw-r--r--src/fruits/cmc/cmc.c781
-rw-r--r--src/fruits/cmc/meson.build7
-rw-r--r--src/fruits/cmv/cmv2.c88
-rw-r--r--src/fruits/cmv/meson.build30
-rw-r--r--src/fruits/cmv/tui.c99
-rw-r--r--src/fruits/cmv/tui.h14
-rw-r--r--src/libclient/client.c164
-rw-r--r--src/libclient/client.h41
-rw-r--r--src/libclient/meson.build2
-rw-r--r--src/libclient/resource_client.c33
-rw-r--r--src/libclient/resource_client.h7
-rw-r--r--src/libclient/search.c31
-rw-r--r--src/libclient/search.h28
-rw-r--r--src/libsink/common.h6
-rw-r--r--src/libsink/meson.build2
-rw-r--r--src/libsink/sink.c193
-rw-r--r--src/libsink/sink.h16
-rw-r--r--src/libsink/sink2.c688
-rw-r--r--src/libsink/sink2.h106
-rw-r--r--src/mixer/audio_miniaudio.c10
-rw-r--r--src/mixer/meson.build15
-rw-r--r--src/mixer/mixer.c1
-rw-r--r--src/render/meson.build2
-rw-r--r--src/render/queue_libplacebo.c4
-rw-r--r--src/render/queue_libplacebo.h3
-rw-r--r--src/render/renderer.h1
-rw-r--r--src/render/renderer_libplacebo.c59
-rw-r--r--src/render/renderer_libplacebo.h10
-rw-r--r--src/render/renderer_tiger.c9
-rw-r--r--src/screen/meson.build12
-rw-r--r--src/screen/screen.c126
-rw-r--r--src/screen/screen.h7
-rw-r--r--src/screen/view.c55
-rw-r--r--src/screen/view.h4
-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.py424
-rw-r--r--src/shoki/py/providers/youtube.py122
-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/common.h (renamed from src/tree/commands.h)6
-rw-r--r--src/tree/list.c112
-rw-r--r--src/tree/list.h31
-rw-r--r--src/tree/meson.build6
-rw-r--r--src/tree/resource_manager.c6
-rw-r--r--src/tree/tree.c227
-rw-r--r--src/tree/tree.h39
-rw-r--r--src/util/color_palette.c2
-rw-r--r--src/util/queue.h10
115 files changed, 3552 insertions, 2744 deletions
diff --git a/.gitignore b/.gitignore
index 5499539..9b18362 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,12 +5,9 @@ compile_commands.json
.clangd/
bin/
data/
-doc/
+doc/**.html
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
deleted file mode 100644
index b3cbdd9..0000000
--- a/NOTES
+++ /dev/null
@@ -1,37 +0,0 @@
-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"
- - [x] Upload everything to git
- - [ ] Tree server-sink support
- - [ ] Networked bimu
- - [ ] Remove thread from cch_handler_http (wait_for_range still needs a mutex, BIMU_THREADED_DEMUXING?)
- - [ ] Adapt all shoki backends to new post structure
- - [ ] Finally move archived twitter/pixiv files into a proper "camu_db"
- - [ ] Native 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
-
-PLANNED:
- - [ ] Add timeout to aki_http
- - [ ] Proper threaded upload for libsink
- - [ ] Add GUI
diff --git a/doc/database.txt b/doc/database.txt
new file mode 100644
index 0000000..3cf4d4b
--- /dev/null
+++ b/doc/database.txt
@@ -0,0 +1,8 @@
+Database
+--------
+* Tied to a single directory on the filesystem
+* index.json
+* Easily referenced external files.
+** external:<BLAKE3 checksum>
+
+// vim: set syntax=asciidoc:
diff --git a/doc/design.txt b/doc/design.txt
new file mode 100644
index 0000000..d06679c
--- /dev/null
+++ b/doc/design.txt
@@ -0,0 +1,54 @@
+= CAMU - Self-Hosted Media Streamer/Organizer/Archiver
+
+Breif on Components
+-------------------
+Server
+~~~~~~
+The "server" is the main hub responsible for all handling user account related functions and state, as well
+as managing the network of all other individual components.
+
+User Accounts
+^^^^^^^^^^^^^
+Users accounts contain 4 main concepts: lists, media lists, history, and settings.
+
+* *Lists*: Transient lists of media connected to a user-selected set of sinks. Resources retrived via browse/search instances get added to a list for playback. A list's active sinks can be changed on the fly.
+
+* *Media Lists*: A more organized way to collect related media. They are stored to the user's account and therefore saved to the disk.
+
+* *History*: A detailed history that tracks what a user played and when. Also saved to a user's account.
+
+* *Settings*: Various per-user settings. For example, tag blacklist, start/stop times per resource, skip settings per collection (album tracks).
+
+Node
+~~~~
+Nodes can be seen as distributed resource managers for the server. They each contain up to 3 main parts: A source manager and/or database, and a resource host. A node can have one or both of a source manager and database but a node without either is useless.
+
+Source Manager
+^^^^^^^^^^^^^^
+A source manager implements any or all of browsing, searching, and custom resource actions. A big point of a source manager is to be able to work entirely behind a proxy. Some examples of sources include a social media website, filesystem directory, dvd drive, or radio antenna. An example of browsing could be apple music's "New Music". An example of searching, youtube search. A custom action could be liking a post on twitter.
+
+Database
+^^^^^^^^
+Stores resources locally along with as much metadata as possible. Simply used to retrive resources locally instead of going through the source manager.
+
+Resource Host
+^^^^^^^^^^^^^
+Hosts node-local resources for the server to use for streaming. Should support anything a source manager can give it.
+
+Client
+~~~~~~
+Clients are only way a user interacts with the reset of the network besides the special case of play/pause/seek which can also be done from a sink.
+
+libclient
+^^^^^^^^^
+C library that integrates with the rest of the code-base to abstract communicating with the server as a client.
+
+Sink
+~~~~
+A sink's only purpose is to be a minimal yet optimal way to view the entries in a list.
+
+libsink
+^^^^^^^
+C library that abstracts communicating with the server as a sink in a platform- and environment-independent way. It makes no assumptions about what kind of device (physical or otherwise) it's running on or outputting to.
+
+// vim: set syntax=asciidoc:
diff --git a/doc/pal_vs_ntsc_aspect_ratio.png b/doc/pal_vs_ntsc_aspect_ratio.png
new file mode 100644
index 0000000..e570f70
--- /dev/null
+++ b/doc/pal_vs_ntsc_aspect_ratio.png
Binary files differ
diff --git a/doc/references.txt b/doc/references.txt
new file mode 100644
index 0000000..acd14f9
--- /dev/null
+++ b/doc/references.txt
@@ -0,0 +1,58 @@
+= References
+
+Video
+-----
+Vsync/VRR
+~~~~~~~~~
+* https://github.com/swaywm/sway/issues/5076
+* https://en.wikipedia.org/wiki/Motion_interpolation
+* https://gamedev.stackexchange.com/questions/82318/what-problem-does-double-or-triple-buffering-solve-in-modern-games
+ - Good explanation of vsync and multiple buffering
+* https://linustechtips.com/topic/1508075-no-vsync-with-no-screen-tearing-possible/?do=findComment&comment=15948456
+* https://forums.tomshardware.com/threads/games-screen-tearing-without-vsync.3272775/post-20062033
+* Current understanding of vsync and tearing:
+ - Vsync operates on an interupt.
+ - You cannot avoid tearing without vsync.
+ - A higher fps and refresh rate while tearing will be less noticable.
+ - VRR can eliminiate tearing.
+
+Latency/Present
+~~~~~~~~~~~~~~~
+* https://themaister.net/blog/2023/11/12/my-scuffed-game-streaming-adventure-pyrofling/
+
+Old Video Standards Stuff
+~~~~~~~~~~~~~~~~~~~~~~~~~
+* https://en.wikipedia.org/wiki/24p
+* https://en.wikipedia.org/wiki/Three-two_pull_down
+* https://cinematography.com/index.php?/forums/topic/71346-why-23976-and-not-24-fps/&tab=comments#comment-455454
+* https://forum.doom9.org/archive/index.php/t-163655.html
+* https://en.wikipedia.org/wiki/pixel_aspect_ratio
+* https://en.wikipedia.org/wiki/display_aspect_ratio
+ - PAL and NTSC are both supposed to be displayed on 4:3 physical aspect ratio screens even though they are internally different aspect ratios and resolutions. The difference is the size of the "pixel" (PAL has more, shorter scanlines).
+* https://en.wikipedia.org/wiki/Anamorphic_widescreen
+
+Python
+------
+* https://stackoverflow.com/questions/38243682/whats-the-standard-way-to-package-a-python-project-with-dependencies
+
+Audio
+-----
+Formats
+~~~~~~~
+* https://github.com/derselbst/ANMP
+* https://slack.net/~ant/libs/audio.html#Game_Music_Emu
+
+Drivers
+~~~~~~~
+* https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/master/spa/plugins/alsa/test-timer.c
+* https://stackoverflow.com/questions/41263580/what-exactly-does-alsas-snd-pcm-delay-return
+* https://stackoverflow.com/questions/24040672/the-meaning-of-period-in-alsa
+
+Interface
+---------
+Terminal
+~~~~~~~~
+* https://www.amp-what.com/
+* https://www.unicode.org/charts/beta/nameslist/n_2B00.html
+
+// vim: set syntax=asciidoc:
diff --git a/flake.lock b/flake.lock
index 7bdd046..92eead7 100644
--- a/flake.lock
+++ b/flake.lock
@@ -5,11 +5,11 @@
"systems": "systems"
},
"locked": {
- "lastModified": 1694529238,
- "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
+ "lastModified": 1701680307,
+ "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
- "rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
+ "rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
@@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1699099776,
- "narHash": "sha256-X09iKJ27mGsGambGfkKzqvw5esP1L/Rf8H3u3fCqIiU=",
+ "lastModified": 1703637592,
+ "narHash": "sha256-8MXjxU0RfFfzl57Zy3OfXCITS0qWDNLzlBAdwxGZwfY=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "85f1ba3e51676fa8cc604a3d863d729026a6b8eb",
+ "rev": "cfc3698c31b1fb9cdcf10f36c9643460264d0ca8",
"type": "github"
},
"original": {
diff --git a/flake.nix b/flake.nix
index 1c8abea..cef826c 100644
--- a/flake.nix
+++ b/flake.nix
@@ -12,31 +12,46 @@
with pkgs;
{
devShells.default = mkShell {
+ NIX_HARDENING_ENABLE = "";
nativeBuildInputs = [
- zsh
- gcc
+ gcc13
+ nasm
gdb
+ clang-tools
perf-tools
valgrind
pkg-config
+ libtool
+ automake
+ autoconf
ninja
meson
cmake
+ ant
+ jdk8
wayland-scanner
python3
python3.pkgs.requests
python3.pkgs.pip
+ python3.pkgs.cython
+ python3.pkgs.setuptools
gsettings-desktop-schemas
multimarkdown
+ asciidoc
];
buildInputs = [
curl
libsodium
+ libxml2
jansson
ffmpeg_6-headless
+ libcdio
alsa-lib
pipewire
libpulseaudio
+ #libplacebo
+ xxHash
+ lcms
shaderc
libGL
libxkbcommon
@@ -48,6 +63,7 @@
wayland
wayland-protocols
egl-wayland
+ notcurses
# TMP:
SDL2
lz4
@@ -57,6 +73,15 @@
gst_all_1.gstreamer
gst_all_1.gst-plugins-base
gst_all_1.gst-plugins-good
+ libuv
+ notcurses
+ # --
+ wlroots
+ json_c
+ pcre2
+ libevdev
+ libinput
+ libdrm
];
shellHook = ''
export NIX_SHELL=true
diff --git a/meson.build b/meson.build
index 50b778f..e98113d 100644
--- a/meson.build
+++ b/meson.build
@@ -4,47 +4,31 @@ project('camu', ['c', 'cpp'], version: '0.2',
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')
+shoki = subproject('shoki').get_variable('shoki')
+
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/fruits/desktop')
- subdir('src/fruits/cmv')
-endif
+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/fruits/cmv')
+subdir('src/fruits/cmc')
diff --git a/scripts/run.sh b/scripts/run.sh
index 61acd73..abd024a 100755
--- a/scripts/run.sh
+++ b/scripts/run.sh
@@ -1,2 +1,5 @@
#! /usr/bin/env sh
-rm -f /tmp/cmv_sock && ./src/fruits/cmv/cmv2 $@
+export PYTHONDONTWRITEBYTECODE=1
+export PYTHONPATH=$HOME/c/shoki/vendor/vendor/lib/python3.11/site-packages
+export mesa_glthread=true
+rm -f /tmp/cmv_sock && ./src/fruits/cmv/cmv "$@"
diff --git a/scripts/run_debug.sh b/scripts/run_debug.sh
index 73c336f..b05a533 100755
--- a/scripts/run_debug.sh
+++ b/scripts/run_debug.sh
@@ -1,2 +1,4 @@
#! /usr/bin/env sh
-rm -f /tmp/cmv_sock && gdb -ex run --args ./src/fruits/cmv/cmv2 $@
+export PYTHONDONTWRITEBYTECODE=1
+export PYTHONPATH=$HOME/c/shoki/vendor/vendor/lib/python3.11/site-packages
+rm -f /tmp/cmv_sock && gdb -ex run --args ./src/fruits/cmv/cmv $@
diff --git a/scripts/run_tree.sh b/scripts/run_tree.sh
new file mode 100755
index 0000000..4359c29
--- /dev/null
+++ b/scripts/run_tree.sh
@@ -0,0 +1,4 @@
+#! /usr/bin/env sh
+export PYTHONDONTWRITEBYTECODE=1
+export PYTHONPATH=$HOME/c/shoki/vendor/vendor/lib/python3.11/site-packages
+gdb -ex run --args ./src/tree/tree $@
diff --git a/src/bimu/client.c b/src/bimu/client.c
index f679c0d..d35e3a1 100644
--- a/src/bimu/client.c
+++ b/src/bimu/client.c
@@ -1 +1,188 @@
+#include "../codec/libav/packet_ext.h"
+
#include "client.h"
+
+static void data_connection_callback(void *userdata, struct aki_packet_stream *stream)
+{
+ struct bmu_client *client = (struct bmu_client *)userdata;
+ (void)stream;
+ struct aki_packet *packet = aki_packet_create();
+ aki_packet_write_u8(packet, BIMU_CONNECTION_DATA);
+ aki_packet_write_u16(packet, client->connection_id);
+ aki_packet_write_u16(packet, client->node_id);
+ aki_packet_stream_send_packet(&client->data, packet);
+}
+
+static void data_connection_closed_callback(void *userdata, struct aki_packet_stream *stream)
+{
+ (void)userdata;
+ (void)stream;
+}
+
+static struct bmu_client_stream *stream_from_index(struct bmu_client *client, s32 index)
+{
+ struct bmu_client_stream *stream;
+ al_array_foreach(client->streams, i, stream) {
+ if (stream->index == index) return stream;
+ }
+ return NULL;
+}
+
+static void handle_data_packet_internal(struct bmu_client *client, struct aki_packet *packet)
+{
+ struct bmu_client_stream *stream;
+ switch (aki_packet_read_u8(packet)) {
+ case 0:
+ stream = stream_from_index(client, aki_packet_read_s32(packet));
+ stream->client->handle_data_packet(stream->client, packet);
+ break;
+ case 1:
+ al_array_foreach(client->streams, i, stream) {
+ stream->client->handle_eof(stream->client);
+ }
+ break;
+ case 2:
+ break;
+ }
+}
+
+static void data_packet_callback(void *userdata, struct aki_packet_stream *stream,
+ struct aki_packet *packet)
+{
+ struct bmu_client *client = (struct bmu_client *)userdata;
+ (void)stream;
+ handle_data_packet_internal(client, packet);
+ aki_packet_free(packet);
+}
+
+static void packet_sent_callback(void *userdata, struct aki_packet *packet)
+{
+ (void)userdata;
+ aki_packet_free(packet);
+}
+
+static void connect_data(struct bmu_client *client)
+{
+ aki_packet_stream_init(&client->data, AKI_SOCKET_TCP, data_connection_callback,
+ data_connection_closed_callback, data_packet_callback, packet_sent_callback, client);
+ aki_packet_stream_connect(&client->data, client->loop, &client->addr, client->port);
+}
+
+static void control_connection_callback(void *userdata, struct aki_packet_stream *stream)
+{
+ struct bmu_client *client = (struct bmu_client *)userdata;
+ (void)stream;
+ struct aki_packet *packet = aki_packet_create();
+ aki_packet_write_u8(packet, BIMU_CONNECTION_CONTROL);
+ aki_packet_write_u16(packet, 0);
+ aki_packet_write_u16(packet, client->node_id);
+ aki_packet_stream_send_packet(&client->control, packet);
+}
+
+static void control_connection_closed_callback(void *userdata, struct aki_packet_stream *stream)
+{
+ (void)userdata;
+ (void)stream;
+}
+
+static void parse_info_packet(struct bmu_client *client, struct aki_packet *packet)
+{
+ u32 count = aki_packet_read_u32(packet);
+ for (u32 i = 0; i < count; i++) {
+ struct bmu_client_stream *stream = al_alloc_object(struct bmu_client_stream);
+ stream->client = bmu_codec_client_create();
+ stream->client->callback = client->callback;
+ stream->client->userdata = client->userdata;
+ stream->stream.type = aki_packet_read_u8(packet);
+ switch (stream->stream.type) {
+ case CAMU_NORMAL:
+ stream->type = BIMU_STREAM_VIDEO;
+ stream->index = 0;
+ 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;
+ al_array_push(client->subscribed, stream->index);
+ break;
+ case AVMEDIA_TYPE_VIDEO:
+ stream->type = BIMU_STREAM_VIDEO;
+ al_array_push(client->subscribed, stream->index);
+ break;
+ case AVMEDIA_TYPE_SUBTITLE:
+ default:
+ stream->type = BIMU_STREAM_UNKNOWN;
+ break;
+ }
+ break;
+ }
+#endif
+ }
+ if (!stream->client->init(stream->client, NULL, stream)) {
+ }
+ al_array_push(client->streams, stream);
+ }
+}
+
+static void send_subscribe_packet(struct bmu_client *client)
+{
+ struct aki_packet *packet = aki_packet_create();
+ aki_packet_write_u8(packet, BIMU_CONTROL_SUBSCRIBE);
+ aki_packet_write_u32(packet, client->subscribed.size);
+ s32 index;
+ al_array_foreach(client->subscribed, i, index) {
+ aki_packet_write_s32(packet, index);
+ }
+ aki_packet_stream_send_packet(&client->control, packet);
+}
+
+static void control_packet_callback(void *userdata, struct aki_packet_stream *stream,
+ struct aki_packet *packet)
+{
+ struct bmu_client *client = (struct bmu_client *)userdata;
+ (void)stream;
+
+ switch (aki_packet_read_u8(packet)) {
+ case BIMU_CONTROL_INFO:
+ client->connection_id = aki_packet_read_u16(packet);
+ parse_info_packet(client, packet);
+ send_subscribe_packet(client);
+ break;
+ case BIMU_CONTROL_START:
+ connect_data(client);
+ break;
+ default:
+ break;
+ }
+
+ aki_packet_free(packet);
+}
+
+void bmu_client_connect(struct bmu_client *client, struct aki_event_loop *loop, str *addr, s32 port, u16 node_id)
+{
+ client->loop = loop;
+ client->node_id = node_id;
+ al_str_clone(&client->addr, addr);
+ client->port = port;
+ client->corked = false;
+ al_array_init(client->subscribed);
+ al_array_init(client->streams);
+ aki_packet_stream_init(&client->control, AKI_SOCKET_TCP, control_connection_callback,
+ control_connection_closed_callback, control_packet_callback, packet_sent_callback, client);
+ aki_packet_stream_connect(&client->control, client->loop, &client->addr, client->port);
+}
+
+void bmu_client_cork(struct bmu_client *client, bool cork)
+{
+ if (cork == client->corked) return;
+ aki_packet_stream_cork(&client->data, cork);
+ client->corked = cork;
+}
diff --git a/src/bimu/client.h b/src/bimu/client.h
index ec48275..54c903a 100644
--- a/src/bimu/client.h
+++ b/src/bimu/client.h
@@ -1,11 +1,33 @@
#pragma once
#include <al/types.h>
+#include <aki/packet_stream.h>
#include "handlers/codec.h"
+#include "common.h"
+
struct bmu_client_stream {
u8 type;
s32 index;
struct camu_stream stream;
+ struct bmu_client_handler *client;
+};
+
+struct bmu_client {
+ struct aki_event_loop *loop;
+ u16 node_id;
+ u16 connection_id;
+ str addr;
+ s32 port;
+ struct aki_packet_stream control;
+ struct aki_packet_stream data;
+ bool corked;
+ array(s32) subscribed;
+ array(struct bmu_client_stream *) streams;
+ void (*callback)(void *, u8, struct bmu_client_stream *stream, void *);
+ void *userdata;
};
+
+void bmu_client_connect(struct bmu_client *client, struct aki_event_loop *loop, str *addr, s32 port, u16 node_id);
+void bmu_client_cork(struct bmu_client *client, bool cork);
diff --git a/src/bimu/common.h b/src/bimu/common.h
index 1c4897a..fe8e0af 100644
--- a/src/bimu/common.h
+++ b/src/bimu/common.h
@@ -5,3 +5,18 @@ enum {
BIMU_STREAM_VIDEO,
BIMU_STREAM_UNKNOWN,
};
+
+enum {
+ BIMU_CONNECTION_CONTROL = 0,
+ BIMU_CONNECTION_DATA
+};
+
+enum {
+ BIMU_CONTROL_INFO = 0,
+ BIMU_CONTROL_SUBSCRIBE,
+ BIMU_CONTROL_START,
+ BIMU_CONTROL_TOGGLE_PAUSE,
+ BIMU_CONTROL_PAUSE,
+ BIMU_CONTROL_RESUME,
+ BIMU_CONTROL_SEEK
+};
diff --git a/src/bimu/handler.h b/src/bimu/handler.h
index e51bc2d..3881288 100644
--- a/src/bimu/handler.h
+++ b/src/bimu/handler.h
@@ -6,12 +6,14 @@
#include "../cache/handle.h"
struct bmu_server_handler {
- bool (*init)(struct bmu_server_handler *, struct cch_handle *, struct aki_packet_pool *);
+ bool (*init)(struct bmu_server_handler *, struct cch_handle *, struct aki_packet_pool *, void *);
void (*write_info)(struct bmu_server_handler *, struct aki_packet *);
+ void (*subscribe)(struct bmu_server_handler *, s32, bool);
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 **);
+ void *userdata;
};
enum {
diff --git a/src/bimu/handlers/codec_client.c b/src/bimu/handlers/codec_client.c
index ffe8c16..eda453f 100644
--- a/src/bimu/handlers/codec_client.c
+++ b/src/bimu/handlers/codec_client.c
@@ -19,9 +19,6 @@ static bool codec_client_init(struct bmu_client_handler *handler, struct camu_re
{
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;
diff --git a/src/bimu/handlers/codec_server.c b/src/bimu/handlers/codec_server.c
index b555370..3c27179 100644
--- a/src/bimu/handlers/codec_server.c
+++ b/src/bimu/handlers/codec_server.c
@@ -9,13 +9,11 @@
#include "codec.h"
-static bool codec_server_init(struct bmu_server_handler *handler, struct cch_handle *handle, struct aki_packet_pool *pool)
+static bool codec_server_init(struct bmu_server_handler *handler, struct cch_handle *handle,
+ struct aki_packet_pool *pool, void *userdata)
{
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;
}
@@ -29,6 +27,7 @@ static bool codec_server_init(struct bmu_server_handler *handler, struct cch_han
#endif
}
codec->pool = pool;
+ codec->handler.userdata = userdata;
return true;
}
@@ -54,6 +53,14 @@ static void codec_server_write_info(struct bmu_server_handler *handler, struct a
}
}
+static void codec_server_subscribe(struct bmu_server_handler *handler, s32 index, bool subscribe)
+{
+ struct bmu_codec_server *codec = (struct bmu_codec_server *)handler;
+ if (subscribe) {
+ al_array_push(codec->demux->subscribed, index);
+ }
+}
+
static s64 codec_server_get_duration(struct bmu_server_handler *handler)
{
struct bmu_codec_server *codec = (struct bmu_codec_server *)handler;
@@ -71,61 +78,38 @@ 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);
+ packet->userdata = codec->handler.userdata;
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;
+ if (ret == CAMU_OK) {
+ aki_packet_write_u8(packet, 0);
+ switch (codec->packet.type) {
+ case CAMU_NORMAL: {
+ aki_packet_write_s32(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:
- aki_packet_write_s32(packet, stream->av.stream->index);
- break;
+ case CAMU_FFMPEG_COMPAT: {
+ AVPacket *pkt = codec->packet.av.pkt;
+ aki_packet_write_s32(packet, pkt->stream_index);
+ aki_packet_write_u8(packet, codec->packet.type);
+ aki_packet_write_av_packet(packet, pkt);
+ av_packet_unref(pkt);
+ 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);
+ aki_packet_pool_submit(codec->pool, packet);
+ } else if (ret == CAMU_ERR_EOF) {
+ aki_packet_write_u8(packet, 1);
+ aki_packet_pool_submit(codec->pool, packet);
+ return false;
+ } else {
+ aki_packet_write_u8(packet, 2);
+ aki_packet_pool_submit(codec->pool, packet);
+ return false;
+ }
return true;
}
@@ -151,6 +135,7 @@ 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.subscribe = codec_server_subscribe;
codec->handler.get_duration = codec_server_get_duration;
codec->handler.seek = codec_server_seek;
codec->handler.step = codec_server_step;
diff --git a/src/bimu/local.c b/src/bimu/local.c
index 3f62f75..76824ed 100644
--- a/src/bimu/local.c
+++ b/src/bimu/local.c
@@ -34,10 +34,10 @@ static aki_thread_result AKI_THREADCALL stream_thread(void *userdata)
}
switch (aki_packet_read_u8(packet)) {
case 0:
- stream->client->handle_data_packet(stream->client, packet);
+ stream->s.client->handle_data_packet(stream->s.client, packet);
break;
case 1:
- stream->client->handle_eof(stream->client);
+ stream->s.client->handle_eof(stream->s.client);
break;
}
aki_packet_pool_return(&stream->runner->pool, packet);
@@ -76,7 +76,7 @@ bool bmu_local_init(struct bmu_local *runner, struct cch_entry *entry)
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)) {
+ if (!runner->server->init(runner->server, &runner->handle, &runner->pool, NULL)) {
return false;
}
al_array_init(runner->streams);
@@ -94,9 +94,9 @@ bool bmu_local_prepare_clients(struct bmu_local *runner, struct camu_renderer *r
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;
+ local->s.client = bmu_codec_client_create();
+ local->s.client->callback = runner->callback;
+ local->s.client->userdata = runner->userdata;
aki_cond_init(&local->cond);
aki_mutex_init(&local->mutex);
aki_packet_cache_init(&local->cache, PACKETS);
@@ -131,7 +131,7 @@ bool bmu_local_prepare_clients(struct bmu_local *runner, struct camu_renderer *r
}
#endif
}
- if (!local->client->init(local->client, renderer, stream)) {
+ if (!local->s.client->init(local->s.client, renderer, stream)) {
aki_packet_free(packet);
return false;
}
@@ -224,7 +224,7 @@ void bmu_local_seek(struct bmu_local *runner, f64 percent)
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);
+ stream->s.client->flush(stream->s.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);
@@ -254,7 +254,7 @@ 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);
+ stream->s.client->free(&stream->s.client);
switch (stream->s.stream.type) {
case CAMU_NORMAL:
break;
diff --git a/src/bimu/local.h b/src/bimu/local.h
index 1928249..10c0cc8 100644
--- a/src/bimu/local.h
+++ b/src/bimu/local.h
@@ -18,7 +18,6 @@ enum {
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;
diff --git a/src/bimu/server.c b/src/bimu/server.c
index c1b0c38..f572290 100644
--- a/src/bimu/server.c
+++ b/src/bimu/server.c
@@ -1,46 +1,327 @@
+#include <al/random.h>
+#include <aki/common.h>
+
+#include "handlers/codec.h"
+
#include "server.h"
-static void packet_callback(void *userdata, struct aki_packet_stream *stream, struct aki_packet *packet)
+#define START_DELAY 700000 // 700ms
+#define DELAY_GIVE 200000 // 200ms
+
+static void control_connection_closed_callback(void *userdata, struct aki_packet_stream *stream)
{
(void)userdata;
(void)stream;
- aki_packet_free(packet);
}
-static void connection_closed_callback(void *userdata, struct aki_packet_stream *stream)
+static void data_connection_closed_callback(void *userdata, struct aki_packet_stream *stream)
{
(void)userdata;
(void)stream;
}
+static void default_packet_sent_callback(void *userdata, struct aki_packet *packet)
+{
+ (void)userdata;
+ aki_packet_free(packet);
+}
+
static void packet_sent_callback(void *userdata, struct aki_packet *packet)
{
+ struct bmu_node_connection *connection = (struct bmu_node_connection *)userdata;
+ aki_packet_pool_return(&connection->node->pool, packet);
+}
+
+static void data_packet_callback(void *userdata, struct aki_packet_stream *stream, struct aki_packet *packet)
+{
+ (void)userdata;
+ (void)stream;
+ aki_packet_free(packet);
+}
+
+static u8 packet_pool_callback(void *userdata, struct aki_packet *packet)
+{
(void)userdata;
- (void)packet;
+ struct bmu_node_connection *connection = (struct bmu_node_connection *)packet->userdata;
+ aki_packet_stream_send_packet(connection->data, packet);
+ return AKI_PACKET_POOL_KEEP;
+}
+
+static void send_resume_packet(struct bmu_node_connection *connection, u64 seek_pos, u64 ts)
+{
+ struct aki_packet *packet = aki_packet_create();
+ aki_packet_write_u8(packet, BIMU_CONTROL_RESUME);
+ aki_packet_write_u64(packet, seek_pos);
+ aki_packet_write_u64(packet, ts);
+ aki_packet_stream_send_packet(connection->control, packet);
+}
+
+static void send_pause_packet(struct bmu_node_connection *connection, u64 seek_pos)
+{
+ struct aki_packet *packet = aki_packet_create();
+ aki_packet_write_u8(packet, BIMU_CONTROL_PAUSE);
+ aki_packet_write_u64(packet, seek_pos);
+ aki_packet_stream_send_packet(connection->control, packet);
+}
+
+static void send_start_packet(struct bmu_node_connection *connection)
+{
+ struct aki_packet *packet = aki_packet_create();
+ aki_packet_write_u8(packet, BIMU_CONTROL_START);
+ u64 ts = aki_get_timestamp();
+ u64 ats = ts + START_DELAY;
+ s64 seek_pos = connection->node->seek_pos;
+ if (!connection->node->paused) {
+ if (connection->node->start >= 0) {
+ if ((s64)ts < (connection->node->start - DELAY_GIVE)) {
+ ats = connection->node->start;
+ } else {
+ seek_pos += ats - connection->node->start;
+ }
+ } else {
+ connection->node->start = ats;
+ }
+ }
+ s64 duration = connection->handler->get_duration(connection->handler);
+ if (seek_pos > duration) seek_pos = duration;
+ // -1 signals to not call seek. We have to distinguish between a seek to 0
+ // and the start of a stream.
+ connection->seek_pos = seek_pos > 0 ? seek_pos : -1;
+ aki_packet_write_u64(packet, seek_pos);
+ aki_packet_write_u64(packet, ats);
+ aki_packet_write_u8(packet, connection->node->paused);
+ aki_packet_stream_send_packet(connection->control, packet);
+}
+
+static void control_packet_callback(void *userdata, struct aki_packet_stream *stream, struct aki_packet *packet)
+{
+ struct bmu_node_connection *connection = (struct bmu_node_connection *)userdata;
+ (void)stream;
+
+ switch (aki_packet_read_u8(packet)) {
+ case BIMU_CONTROL_SUBSCRIBE: {
+ u32 count = aki_packet_read_u32(packet);
+ for (u32 i = 0; i < count; i++) {
+ connection->handler->subscribe(connection->handler, aki_packet_read_s32(packet), true);
+ }
+ send_start_packet(connection);
+ break;
+ }
+ case BIMU_CONTROL_TOGGLE_PAUSE: {
+ struct bmu_node *node = connection->node;
+ u64 pos = aki_packet_read_u64(packet);
+ u8 paused = connection->paused;
+ if (paused == BIMU_PLAYING) {
+ node->seek_pos = pos;
+ node->paused = true;
+ // If node is paused, start will not be used for new connections.
+ // So, set it to -1 and update it on the first resume from any connection.
+ node->start = -1;
+ } else if (paused == BIMU_PAUSED_AND_RUNNING) {
+ node->paused = false;
+ node->start = aki_get_timestamp() + START_DELAY;
+ }
+ struct bmu_node_connection *rconnection;
+ al_array_foreach(node->connections, i, rconnection) {
+ switch (rconnection->paused) {
+ case BIMU_PLAYING:
+ al_assert(paused == BIMU_PLAYING || paused == BIMU_NOT_PAUSED);
+ rconnection->paused = BIMU_PAUSED;
+ send_pause_packet(rconnection, node->seek_pos);
+ break;
+ case BIMU_NOT_PAUSED:
+ al_assert(paused != BIMU_PAUSED);
+ rconnection->paused = BIMU_PAUSED;
+ break;
+ case BIMU_PAUSED:
+ al_assert(paused != BIMU_NOT_PAUSED);
+ rconnection->paused = BIMU_NOT_PAUSED;
+ break;
+ case BIMU_PAUSED_AND_RUNNING:
+ al_assert(paused != BIMU_NOT_PAUSED);
+ send_resume_packet(rconnection, node->seek_pos, node->start);
+ rconnection->paused = BIMU_PLAYING;
+ break;
+ }
+ }
+ break;
+ }
+ case BIMU_CONTROL_SEEK: {
+ u64 pos = aki_packet_read_u64(packet);
+ connection->seek_pos = pos;
+ struct aki_packet *npacket = aki_packet_create();
+ aki_packet_write_u8(npacket, BIMU_CONTROL_SEEK);
+ aki_packet_stream_send_packet(connection->control, npacket);
+ break;
+ }
+ }
+ aki_packet_free(packet);
}
-static void flushed_callback(void *userdata)
+static struct bmu_node *get_node_by_id(struct bmu_server *server, u16 id)
+{
+ struct bmu_node *node;
+ al_array_foreach(server->nodes, i, node) {
+ if (node->id == id) return node;
+ }
+ return NULL;
+}
+
+static struct bmu_node_connection *get_connection_by_id(struct bmu_node *node, u16 id)
+{
+ struct bmu_node_connection *connection;
+ al_array_foreach(node->connections, i, connection) {
+ if (connection->id == id) return connection;
+ }
+ return NULL;
+}
+
+static void send_control_info_packet(struct bmu_node_connection *connection)
+{
+ struct aki_packet *packet = aki_packet_create();
+ aki_packet_write_u8(packet, BIMU_CONTROL_INFO);
+ aki_packet_write_u16(packet, connection->id);
+ connection->handler = bmu_codec_server_create();
+ connection->handler->init(connection->handler, &connection->handle, &connection->node->pool, connection);
+ connection->handler->write_info(connection->handler, packet);
+ aki_packet_stream_send_packet(connection->control, packet);
+}
+
+static aki_thread_result AKI_THREADCALL handler_thread(void *userdata)
+{
+ struct bmu_node_connection *connection = (struct bmu_node_connection *)userdata;
+ while (connection->handler->step(connection->handler)) {}
+ return 0;
+}
+
+static void maybe_start_handler(struct bmu_node_connection *connection)
+{
+ al_assert(!connection->running);
+ if (connection->seek_pos >= 0) {
+ connection->handler->seek(connection->handler, connection->seek_pos);
+ }
+ aki_thread_create(&connection->thread, handler_thread, connection);
+ connection->running = true;
+ struct bmu_node *node = connection->node;
+ if (connection->paused == BIMU_NOT_PAUSED) {
+ if (node->start == -1) {
+ node->paused = false;
+ node->start = aki_get_timestamp() + START_DELAY;
+ }
+ //send_resume_packet(connection, connection->seek_pos, node->start);
+ connection->paused = BIMU_PLAYING;
+ } else if (connection->paused == BIMU_PAUSED) {
+ connection->paused = BIMU_PAUSED_AND_RUNNING;
+ }
+}
+
+static void identify_packet_callback(void *userdata, struct aki_packet_stream *stream, struct aki_packet *packet)
+{
+ struct bmu_server *server = (struct bmu_server *)userdata;
+
+ u8 type = aki_packet_read_u8(packet);
+
+ u16 connection_id = aki_packet_read_u16(packet);
+ u16 node_id = aki_packet_read_u16(packet);
+
+ // Disconnecting a stream at any point before setting the new callbacks should
+ // be valid and handled properly by the client.
+ // Generalize erronious connection checking/handling.
+ // - Node with id doesn't exist.
+ // - Control connected more than once and/or after info is sent.
+ // - Data connected before control.
+ // - Data connected more than once.
+
+ struct bmu_node *node = get_node_by_id(server, node_id);
+ al_assert(node);
+
+ struct bmu_node_connection *connection = get_connection_by_id(node, connection_id);
+ if (!connection) {
+ al_assert(type == BIMU_CONNECTION_CONTROL);
+ connection = al_alloc_object(struct bmu_node_connection);
+ al_array_push(node->connections, connection);
+ connection->id = node->connection_id++;
+ connection->node = node;
+ connection->control = NULL;
+ connection->data = NULL;
+ cch_entry_get_handle(connection->node->entry, &connection->handle);
+ connection->paused = BIMU_PLAYING;
+ connection->running = false;
+ }
+
+ switch (type) {
+ case BIMU_CONNECTION_CONTROL:
+ aki_packet_stream_set_no_delay(stream, true);
+ connection->control = stream;
+ stream->packet_callback = control_packet_callback;
+ stream->packet_sent_callback = default_packet_sent_callback;
+ stream->connection_closed_callback = control_connection_closed_callback;
+ stream->userdata = connection;
+ send_control_info_packet(connection);
+ break;
+ case BIMU_CONNECTION_DATA:
+ // TODO: Account for control connection being in a reconnect state.
+ // TODO: is it possible for data->data to be non-NULL during normal operation
+ // (disconnect and reconnect very fast?).
+ // - Call maybe_cleanup_data in this function?
+ connection->data = stream;
+ stream->packet_callback = data_packet_callback;
+ stream->packet_sent_callback = packet_sent_callback;
+ stream->connection_closed_callback = data_connection_closed_callback;
+ stream->userdata = connection;
+ maybe_start_handler(connection);
+ break;
+ }
+
+ aki_packet_free(packet);
+}
+
+static void connection_closed_callback(void *userdata, struct aki_packet_stream *stream)
{
(void)userdata;
+ (void)stream;
}
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;
+ struct bmu_server *server = (struct bmu_server *)userdata;
stream->connection_closed_callback = connection_closed_callback;
- stream->flushed_callback = flushed_callback;
+ stream->packet_callback = identify_packet_callback;
+ stream->packet_sent_callback = default_packet_sent_callback;
+ stream->flushed_callback = NULL;
+ stream->userdata = server;
}
bool bmu_server_init(struct bmu_server *server)
{
- return aki_packet_stream_init(&server->server, AKI_SOCKET_TCP, connection_callback,
- NULL, NULL, NULL, server);
+ al_array_init(server->nodes);
+ 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);
+ aki_packet_stream_listen(&server->server, loop, addr, port);
+}
+
+u16 bmu_server_create_node(struct bmu_server *server, struct cch_entry *entry)
+{
+ struct bmu_node *node = al_alloc_object(struct bmu_node);
+ al_array_push(server->nodes, node);
+ node->id = al_rand_u16();
+ node->entry = entry;
+ node->paused = false;
+ node->start = -1;
+ node->seek_pos = 0;
+ aki_packet_pool_init(&node->pool, 16, server->loop, packet_pool_callback, node);
+ //aki_packet_pool_init(&node->pool, 1400, server->loop, packet_pool_callback, node);
+ node->connection_id = 1;
+ al_array_init(node->connections);
+ node->server = server;
+ return node->id;
+}
+
+void bmu_server_close(struct bmu_server *server)
+{
+ (void)server;
}
diff --git a/src/bimu/server.h b/src/bimu/server.h
index ba3093e..754ab7f 100644
--- a/src/bimu/server.h
+++ b/src/bimu/server.h
@@ -1,21 +1,54 @@
#pragma once
+#include <al/array.h>
#include <aki/packet_stream.h>
+#include <aki/packet_pool.h>
#include "../cache/entry.h"
+#include "../cache/handle.h"
+
+#include "common.h"
+#include "handler.h"
+
+enum {
+ BIMU_PLAYING = 0,
+ BIMU_NOT_PAUSED,
+ BIMU_PAUSED,
+ BIMU_PAUSED_AND_RUNNING
+};
struct bmu_node_connection {
+ u16 id;
+ struct bmu_node *node;
+ struct aki_packet_stream *control;
+ struct aki_packet_stream *data;
+ bool running;
+ u8 paused;
+ s64 seek_pos;
struct cch_handle handle;
+ struct bmu_server_handler *handler;
+ struct aki_thread thread;
};
struct bmu_node {
+ u16 id;
struct cch_entry *entry;
+ bool paused;
+ s64 start;
+ s64 seek_pos;
+ struct aki_packet_pool pool;
+ u16 connection_id;
+ array(struct bmu_node_connection *) connections;
+ struct bmu_server *server;
};
struct bmu_server {
- struct aki_packet_stream server;
struct aki_event_loop *loop;
+ struct aki_packet_stream server;
+ array(struct bmu_node *) nodes;
};
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);
+u16 bmu_server_create_node(struct bmu_server *server, struct cch_entry *entry);
+void bmu_server_close(struct bmu_server *server);
diff --git a/src/buffer/audio.c b/src/buffer/audio.c
index 939b98a..9799a95 100644
--- a/src/buffer/audio.c
+++ b/src/buffer/audio.c
@@ -2,16 +2,16 @@
#include "audio.h"
-#define BUFFER_USEC (10 * 1000000L)
+#define BUFFER_USEC (12 * 1000000L)
#define BUFFER_WATERMARK_LOW (3 * 1000000L) // Must be a most half of the buffer size.
-#define BUFFER_WATERMARK_HIGH (4 * 1000000L)
+#define BUFFER_WATERMARK_HIGH (0.75 * 1000000L)
#define DESYNC_PTS 0.022
#ifdef CAMU_AUDIO_BUFFER_FADE
-#define FADE 0.9999
+#define FADE 1.0
#define FADE_LENGTH 6
-#define FADE_STEP(rate) ((FADE / (rate)) / (FADE_LENGTH - 1))
+#define FADE_STEP(rate) ((FADE / ((f64)rate)) / (FADE_LENGTH - 1.0))
#endif
enum {
@@ -23,7 +23,6 @@ enum {
enum {
PAUSE_PRE = 0,
PAUSE_UNPAUSED,
- PAUSE_IGNORE_DESYNC,
#ifdef CAMU_AUDIO_BUFFER_FADE
PAUSE_FADING,
#endif
@@ -73,8 +72,8 @@ bool camu_audio_buffer_init(struct camu_audio_buffer *buf, struct camu_clock *cl
buf->clock = clock;
buf->pts = camu_mixer_get_latency(buf->mixer);
buf->buffered = false;
- //buf->pause = PAUSE_PRE;
- buf->pause = PAUSE_IGNORE_DESYNC;
+ buf->pause = PAUSE_PRE;
+ buf->ignore_desync = true;
#ifdef CAMU_AUDIO_BUFFER_FADE
buf->fade_period = 0;
buf->fade_offset = 0;
@@ -93,23 +92,29 @@ bool camu_audio_buffer_configure(struct camu_audio_buffer *buf, struct camu_stre
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;
}
@@ -144,9 +149,9 @@ static bool push_internal(struct camu_audio_buffer *buf, u8 **data, s32 sample_c
}
// 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.
+ // less than or equal to half the buffer size.
al_atomic_size_t_store(&buf->continue_mark, buf->watermark.low + peak, AL_ATOMIC_RELAXED);
- buf->callback(buf->userdata, CAMU_BUFFER_STOP);
+ buf->callback(buf->userdata, CAMU_BUFFER_CORK);
return false;
}
@@ -195,6 +200,7 @@ void camu_audio_buffer_reset(struct camu_audio_buffer *buf)
al_atomic_u8_store(&buf->flow, FLOWING, AL_ATOMIC_RELAXED);
buf->pts = camu_clock_get_base_pts(buf->clock);
buf->buffered = false;
+ buf->pause = PAUSE_PRE;
al_ring_buffer_reset(&buf->rb);
}
@@ -212,10 +218,11 @@ void camu_audio_buffer_flush(struct camu_audio_buffer *buf)
#define TYPED_CAST(f, type) (f64)(*((type *)f))
#define TYPED_CLAMP(f, type, min, max, ret) *((type *)ret) = (type)AL_CLAMP(f, min, max)
+// This is obviously bad for optimization, should just make a function for each type.
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);
+ u8 bytes[MAX_BYTES];
+ u8 *ptr = bytes + (MAX_BYTES - buf->bytes_per_sample);
f64 value;
u32 sample_count = size / buf->bytes_per_sample;
u32 channel_count = 2;
@@ -248,14 +255,14 @@ static void handle_fade(struct camu_audio_buffer *buf, u8 *data, size_t size, bo
}
#endif
-#define NOT_UNPAUSED(pause) (pause != PAUSE_UNPAUSED && pause != PAUSE_PRE && pause != PAUSE_IGNORE_DESYNC)
+#define NOT_UNPAUSED(pause) (pause != PAUSE_UNPAUSED && pause != PAUSE_PRE)
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) {
+ if (camu_clock_is_paused(buf->clock)) {
#ifdef CAMU_AUDIO_BUFFER_FADE
if (buf->pause == PAUSE_PLAYING) {
+ // Fade out. Signified by >0 fade_period and PAUSE_FADING.
buf->volume = FADE;
buf->fade_period = FADE_LENGTH;
buf->fade_offset = 0;
@@ -269,10 +276,13 @@ size_t camu_audio_buffer_read(struct camu_audio_buffer *buf, u8 *data, size_t re
return req;
}
} else if (buf->pause == PAUSE_FADING || buf->pause == PAUSE_UNPAUSED) {
+ // Fade in. Signified by >0 fade_period and PAUSE_PLAYING.
buf->volume = 1.0 - FADE;
buf->fade_period = FADE_LENGTH;
buf->fade_offset = 0;
- buf->pause = PAUSE_PLAYING;
+ // We don't want to consider sync if reversing an active
+ // fade but we do if PAUSE_UNPAUSED.
+ if (buf->pause == PAUSE_FADING) buf->pause = PAUSE_PLAYING;
#else
al_memset(data, 0, req);
if (NOT_UNPAUSED(buf->pause)) {
@@ -289,23 +299,30 @@ size_t camu_audio_buffer_read(struct camu_audio_buffer *buf, u8 *data, size_t re
#ifdef CAMU_AUDIO_BUFFER_FADE
if (buf->pause != PAUSE_FADING) {
#endif
- pts -= buf->pts;
- if (UNLIKELY(buf->pause == PAUSE_PRE || buf->pause == PAUSE_UNPAUSED)) {
+ f64 pts = camu_clock_get_pts(buf->clock) - buf->pts;
+ // We can assume a call to audio_buffer_read will happen before
+ // the user NEEDS the data, so, we can't accurately assess the sync here.
+ if (UNLIKELY(!buf->ignore_desync && (buf->pause == PAUSE_PRE || buf->pause == PAUSE_UNPAUSED))) {
+ if (fabs(pts) >= 0.322) {
+ al_log_warn("audio_buffer", "Abnormally large audio desync of %.5fs", pts);
+ }
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);
+ al_log_debug("audio_buffer", "Skipping %.5fs of audio (%zu bytes).", pts, ret);
ret = al_ring_buffer_discard(&buf->rb, ret);
size -= ret;
buf->pts += camu_lav_resample_fmt_bytes_to_sec(&buf->fmt, ret);
+ // Could go on to underrun.
} 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_log_debug("audio_buffer", "Delaying audio by %.5fs (%zu bytes).", pts, ret);
al_memset(data, 0, ret);
data += ret;
- req -= ret;
+ // We can continue to delay.
+ if ((req -= ret) == 0) return signal;
}
}
if (buf->pause != PAUSE_PLAYING) buf->pause = PAUSE_PLAYING;
@@ -339,7 +356,7 @@ size_t camu_audio_buffer_read(struct camu_audio_buffer *buf, u8 *data, size_t re
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);
+ buf->callback(buf->userdata, CAMU_BUFFER_UNCORK);
}
}
#ifdef CAMU_AUDIO_BUFFER_FADE
diff --git a/src/buffer/audio.h b/src/buffer/audio.h
index 1a51807..7bd30a2 100644
--- a/src/buffer/audio.h
+++ b/src/buffer/audio.h
@@ -20,6 +20,7 @@ struct camu_audio_buffer {
f64 pts;
u8 pause;
+ bool ignore_desync;
struct camu_clock *clock;
f64 volume;
diff --git a/src/buffer/clock.c b/src/buffer/clock.c
index 5f4133e..c9b18ce 100644
--- a/src/buffer/clock.c
+++ b/src/buffer/clock.c
@@ -4,24 +4,26 @@ 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->start, 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)
{
+ f64 pause = al_atomic_f64_load(&clock->pause, AL_ATOMIC_RELAXED);
+ if (pause != -1.0) return;
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);
+ if (pause == -1.0) return;
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);
+ al_atomic_f64_store(&clock->start, aki_get_tick() - (pause - start), AL_ATOMIC_RELAXED);
}
void camu_clock_seek(struct camu_clock *clock, f64 pos)
@@ -47,7 +49,7 @@ f64 camu_clock_get_base_pts(struct camu_clock *clock)
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);
+ if (pause != -1.f) return pause - start;
return aki_get_tick() - start;
}
diff --git a/src/buffer/clock.h b/src/buffer/clock.h
index 072f7d3..9f34807 100644
--- a/src/buffer/clock.h
+++ b/src/buffer/clock.h
@@ -4,7 +4,7 @@
#include <al/atomic.h>
struct camu_clock {
- atomic_f64 base, start, pause, offset;
+ atomic_f64 base, start, pause;
};
void camu_clock_init(struct camu_clock *clock);
diff --git a/src/buffer/common.h b/src/buffer/common.h
index 8a99c96..64168ff 100644
--- a/src/buffer/common.h
+++ b/src/buffer/common.h
@@ -2,8 +2,8 @@
enum {
CAMU_BUFFER_BUFFERED = 0,
- CAMU_BUFFER_STOP,
- CAMU_BUFFER_CONTINUE,
+ CAMU_BUFFER_CORK,
+ CAMU_BUFFER_UNCORK,
CAMU_BUFFER_PAUSED,
CAMU_BUFFER_EOF
};
diff --git a/src/buffer/video.c b/src/buffer/video.c
index d5d2c12..6c8d6e3 100644
--- a/src/buffer/video.c
+++ b/src/buffer/video.c
@@ -4,13 +4,17 @@
//#define CAMU_VIDEO_BUFFER_FORCE_SCALER
+#define BUFFER_WATERMARK_LOW 16 // frames.
+#define BUFFER_WATERMARK_BUFFERED 24
+#define BUFFER_WATERMARK_HIGH 48
+#define BUFFER_WATERMARK_RESET BUFFER_WATERMARK_HIGH + 16.
+
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
@@ -32,20 +36,24 @@ bool camu_video_buffer_configure(struct camu_video_buffer *buf, struct camu_stre
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->fmt.in_format = stream->av.stream->codecpar->format;
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;
+ AVRational frame_rate = buf->single_frame ? (AVRational){ 0, 1 } :
+ stream->av.stream->avg_frame_rate;
+ buf->avg_frame_duration = av_q2d(av_inv_q(frame_rate));
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);
+ al_log_info("video_buffer", "Stream: %s (%dx%d) %s %.2ffps.", format_name,
+ width, height, buf->single_frame ? "IMAGE" : "VIDEO", av_q2d(frame_rate));
#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)) {
+ if (camu_lav_scaler_init(&buf->scale, &buf->fmt) && buf->fmt.scaler_needed) {
+ format_name = av_get_pix_fmt_name(buf->fmt.req_format);
+ al_log_info("video_buffer", "Scaling stream to: %s (%dx%d)", format_name,
+ buf->fmt.req_width, buf->fmt.req_height);
+ } else {
return false;
}
#endif
@@ -59,40 +67,38 @@ bool camu_video_buffer_configure(struct camu_video_buffer *buf, struct camu_stre
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)) {
+ if (!buf->buffered && (buf->single_frame || count >= BUFFER_WATERMARK_BUFFERED)) {
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 (count >= BUFFER_WATERMARK_RESET) buf->queue->reset(buf->queue);
+ else if (count >= BUFFER_WATERMARK_HIGH) buf->callback(buf->userdata, CAMU_BUFFER_CORK);
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;
+ f64 pts = 0.0;
+ if (!buf->single_frame) {
+ AVStream *stream = buf->stream->av.stream;
+ pts = (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;
+ av_frame_free(&frame);
+ frame = av_frame_clone(buf->scale.frame);
}
-#else
- scaled_frame = frame;
#endif
- if (scaled_frame) scaled_frame->opaque = buf;
- buf->queue->push_av_frame(buf->queue, scaled_frame, pts);
+ if (frame) frame->opaque = buf;
+ buf->queue->push_av_frame(buf->queue, frame, pts);
}
#endif
@@ -130,19 +136,18 @@ void camu_video_buffer_flush(struct camu_video_buffer *buf)
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;
+ f64 pts = buf->single_frame ? 0.0 : camu_clock_get_pts(buf->clock);
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);
+ } else if (buf->queue->count(buf->queue) <= BUFFER_WATERMARK_LOW) {
+ buf->callback(buf->userdata, CAMU_BUFFER_UNCORK);
}
return ret == CAMU_QUEUE_OK || ret == CAMU_QUEUE_MORE;
}
void camu_video_buffer_free(struct camu_video_buffer *buf)
{
+ if (buf->fmt.scaler_needed) camu_lav_scaler_close(&buf->scale);
buf->queue->free(&buf->queue);
}
diff --git a/src/buffer/video.h b/src/buffer/video.h
index 534f797..a6349f8 100644
--- a/src/buffer/video.h
+++ b/src/buffer/video.h
@@ -14,9 +14,10 @@ struct camu_video_buffer {
struct camu_stream *stream;
struct camu_clock *clock;
+ f32 start_time;
+ f32 avg_frame_duration;
+
bool single_frame;
- f64 last_pts;
- f64 avg_frame_duration;
struct camu_lav_scaler scale;
struct camu_lav_scale_fmt fmt;
diff --git a/src/cache/backings/file_common.c b/src/cache/backings/file_common.c
index c0b54c0..232580a 100644
--- a/src/cache/backings/file_common.c
+++ b/src/cache/backings/file_common.c
@@ -9,7 +9,8 @@ static off_t file_backing_get_size_estimate(struct cch_backing *backing)
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;
+ s32 flags = size != 0 ? AKI_FILE_CREATE : 0;
+ if (!aki_file_open(&file->file, path, flags)) return false;
file->filesize = aki_file_get_filesize(&file->file);
if (!size) {
file->size = file->filesize;
diff --git a/src/cache/backings/file_mapped.c b/src/cache/backings/file_mapped.c
index 1b06ad2..4277c21 100644
--- a/src/cache/backings/file_mapped.c
+++ b/src/cache/backings/file_mapped.c
@@ -43,6 +43,7 @@ static void file_backing_resize(struct cch_backing *backing, size_t size)
aki_mutex_lock(&file->mutex);
file->size = size;
if ((off_t)size > file->filesize) {
+ // TODO: Unmap, truncate, map?
file->u.map = aki_file_mremap(&file->file, size, file->u.map);
aki_file_truncate(&file->file, size);
file->filesize = size;
diff --git a/src/cache/handlers/http.c b/src/cache/handlers/http.c
index 1daf231..dd3c8ac 100644
--- a/src/cache/handlers/http.c
+++ b/src/cache/handlers/http.c
@@ -80,12 +80,11 @@ static size_t http_callback(void *userdata, u8 op, u8 *buf, s64 int0)
static void signal_callback(void *userdata)
{
struct cch_handler_http *http = (struct cch_handler_http *)userdata;
+ u32 size;
+ s32 index;
do {
- u32 size;
- camu_queue_size(http->queue, size);
+ camu_queue_try_pop(http->queue, size, index);
if (size == 0) break;
- s32 index;
- camu_queue_pop(http->queue, index);
if (index == -1) {
aki_event_loop_break(&http->loop);
break;
diff --git a/src/codec/codec.h b/src/codec/codec.h
index d30a648..1309210 100644
--- a/src/codec/codec.h
+++ b/src/codec/codec.h
@@ -61,8 +61,7 @@ struct camu_stream {
s32 height;
struct camu_icc_profile icc;
} video;
- //struct {
- //} audio;
+ //struct { } audio;
#ifdef HAVE_FFMPEG
struct {
AVStream *stream;
@@ -94,13 +93,14 @@ struct camu_frame {
};
struct camu_demuxer {
+ u8 type;
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;
+ array(s32) subscribed;
};
struct camu_renderer;
diff --git a/src/codec/libav/demuxer.c b/src/codec/libav/demuxer.c
index f7ff35c..d823d23 100644
--- a/src/codec/libav/demuxer.c
+++ b/src/codec/libav/demuxer.c
@@ -65,47 +65,30 @@ static bool lav_demuxer_init(struct camu_demuxer *demux, struct cch_handle *hand
av->duration = 0;
- bool audio_selected = false;
- bool video_selected = false;
+#define GUESS_STREAM_IS_IMAGE(stream) \
+ (stream->duration >= 0 && stream->nb_frames <= 1 && (stream->avg_frame_rate.den == 0 && stream->r_frame_rate.den > 0))
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;
+ if (type == AVMEDIA_TYPE_VIDEO && GUESS_STREAM_IS_IMAGE(stream)) {
al_log_info("lav_demux", "Assuming stream #%i is an image.", i);
+ stream->duration = 0;
} else if (stream->duration < 0) {
+ al_log_warn("lav_demux", "Stream #%i has invalid duration (%ld).", av->format_context->duration);
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);
+ 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));
+ 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;
+ al_log_warn("lav_demux", "Stream has a negative start_time (%ld).", stream->start_time);
}
s64 duration = av_rescale_q(stream->duration, stream->time_base, AV_TIME_BASE_Q);
- if (duration > av->duration) {
- av->duration = duration;
- }
+ if (duration > av->duration) av->duration = duration;
al_array_push(av->demux.streams, ((struct camu_stream){
.type = CAMU_FFMPEG_COMPAT,
.av.stream = stream
@@ -125,6 +108,15 @@ err:
return false;
}
+static bool subscribed_to_index(struct camu_demuxer *demux, s32 index)
+{
+ s32 subscribed;
+ al_array_foreach(demux->subscribed, i, subscribed) {
+ if (subscribed == index) return true;
+ }
+ 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;
@@ -132,12 +124,19 @@ static s32 lav_demuxer_get_packet(struct camu_demuxer *demux, struct camu_packet
s32 ret = 0;
do {
ret = av_read_frame(av->format_context, packet->av.pkt);
- if (ret < 0 && ret != AVERROR_EOF) {
+ if (ret == AVERROR_EOF) {
+ av->eof = true;
+ break;
+ }
+ if (ret < 0) {
al_log_error("lav_demuxer", "Failed to read frame (%s).", av_err2str(ret));
}
+ if (!subscribed_to_index(demux, packet->av.pkt->stream_index)) {
+ av_packet_unref(packet->av.pkt);
+ continue;
+ }
break;
} while (1);
- if (ret == AVERROR_EOF) av->eof = true;
return ret;
}
@@ -175,6 +174,7 @@ 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);
+ al_array_init(av->demux.subscribed);
av->demux.init = lav_demuxer_init;
av->demux.get_packet = lav_demuxer_get_packet;
av->demux.get_duration = lav_demuxer_get_duration;
diff --git a/src/codec/libav/packet_ext.h b/src/codec/libav/packet_ext.h
index e2f7cd5..ff6e337 100644
--- a/src/codec/libav/packet_ext.h
+++ b/src/codec/libav/packet_ext.h
@@ -1,6 +1,7 @@
#pragma once
#include <aki/common.h>
+#include <aki/event_loop.h>
#include <libavcodec/packet.h>
#include <libavcodec/codec_par.h>
diff --git a/src/codec/libav/scaler.c b/src/codec/libav/scaler.c
index 6146c69..882116f 100644
--- a/src/codec/libav/scaler.c
+++ b/src/codec/libav/scaler.c
@@ -9,6 +9,12 @@ static AVFrame *alloc_picture(enum AVPixelFormat pix_fmt, s32 width, s32 height)
{
AVFrame *frame = av_frame_alloc();
if (!frame) return NULL;
+ s32 ret = av_image_alloc(frame->data, frame->linesize, width, height, pix_fmt, 32);
+ if (ret < 0) {
+ al_log_error("lav_scaler", "Failed to allocate picture buffer (%s).", av_err2str(ret));
+ return NULL;
+ }
+ /*
s32 size = av_image_get_buffer_size(pix_fmt, width, height, 1);
u8 *buffer = (u8 *)av_malloc(size);
if (!buffer) {
@@ -21,6 +27,7 @@ static AVFrame *alloc_picture(enum AVPixelFormat pix_fmt, s32 width, s32 height)
av_free(frame);
return NULL;
}
+ */
frame->width = width;
frame->height = height;
frame->format = pix_fmt;
@@ -31,32 +38,28 @@ bool camu_lav_scaler_init(struct camu_lav_scaler *scale, struct camu_lav_scale_f
{
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;
+ s32 flags = SWS_BITEXACT;
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);
-
+ fmt->req_width, fmt->req_height, fmt->req_format, flags, 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;
}
+ fmt->scaler_needed = true;
+
scale->fmt = *fmt;
return true;
@@ -64,8 +67,8 @@ bool camu_lav_scaler_init(struct camu_lav_scaler *scale, struct camu_lav_scale_f
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);
+ 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;
@@ -75,5 +78,7 @@ bool camu_lav_scaler_scale(struct camu_lav_scaler *scale, const u8 **in_slice, s
void camu_lav_scaler_close(struct camu_lav_scaler *scale)
{
+ av_freep(&scale->frame->data[0]);
+ av_frame_free(&scale->frame);
sws_freeContext(scale->scaler_context);
}
diff --git a/src/codec/meson.build b/src/codec/meson.build
index e1fd5f7..b4472fb 100644
--- a/src/codec/meson.build
+++ b/src/codec/meson.build
@@ -54,14 +54,11 @@ if libavutil.found() and libavformat.found() and libavcodec.found() and libswres
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)
+stb_image = declare_dependency(sources: ['stb_image/impl.c'],
+ dependencies: subproject('stb').get_variable('stb'))
-#spng = dependency('spng')
-#spng_decoder = declare_dependency(sources: ['spng/impl.c'],
-# dependencies: spng)
+#wuffs = declare_dependency(sources: ['wuffs/impl.c'],
+# dependencies: subproject('wuffs').get_variable('wuffs'))
+
+#spng = declare_dependency(sources: ['spng/impl.c'],
+# dependencies: dependency('spng'))
diff --git a/src/codec/stb_image/impl.c b/src/codec/stb_image/impl.c
index 953cca8..b7ed928 100644
--- a/src/codec/stb_image/impl.c
+++ b/src/codec/stb_image/impl.c
@@ -2,9 +2,9 @@
#include <aki/common.h>
#include "../common.h"
#define STB_IMAGE_IMPLEMENTATION
+#define STB_IMAGE_STATIC
#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
diff --git a/src/fruits/cap/cap.c b/src/fruits/cap/cap.c
index 317abf4..3f5f5e8 100644
--- a/src/fruits/cap/cap.c
+++ b/src/fruits/cap/cap.c
@@ -1,20 +1,20 @@
#include <al/random.h>
-#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
+#include "cap.h"
+
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);
+#if CAP_USE_PYTHON
+ sho_client_init(&cap->search, NULL);
+#endif
}
static struct cap_list *list_from_name(struct cap_runner *cap, str *name)
@@ -28,7 +28,7 @@ static struct cap_list *list_from_name(struct cap_runner *cap, str *name)
return NULL;
}
-static struct cch_entry *entry_for_unique_id(str *unique_id)
+static struct cch_entry *entry_for_unique_id(struct cap_runner *cap, str *unique_id)
{
struct cch_entry *entry = NULL;
#ifdef AKIYO_HAS_CURL
@@ -36,16 +36,21 @@ static struct cch_entry *entry_for_unique_id(str *unique_id)
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);
+ str provider;
str query;
+ al_str_from(&provider, "");
al_str_from(&query, "");
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) {
al_str_cat(&query, al_str_c("tweet:"));
al_str_cat(&query, unique_id);
- if (!sho_search_more_results(&s, al_str_c("twitter"), &query)) {
+ al_str_cat(&provider, al_str_c("twitter"));
+ } else if (al_str_cmp(unique_id, al_str_c("https://instagram.com"), 0, 21) == 0) {
+ s32 slash = al_str_rfind(unique_id, '/');
+ if (slash >= 0) {
+ al_str_cat(&query, al_str_substr(unique_id, slash + 1, unique_id->len));
}
+ al_str_cat(&provider, al_str_c("instagram"));
} else {
if (is_search) {
al_str_cat(&query, al_str_substr(unique_id, 1, unique_id->len));
@@ -53,26 +58,27 @@ static struct cch_entry *entry_for_unique_id(str *unique_id)
al_str_cat(&query, al_str_c("link:"));
al_str_cat(&query, unique_id);
}
- if (!sho_search_more_results(&s, al_str_c("youtube"), &query)) {
- }
+ al_str_cat(&provider, al_str_c("youtube"));
}
+ s32 id = sho_client_create_search(&cap->search, &provider, &query);
+ al_str_free(&provider);
al_str_free(&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 (id < 0) return NULL;
+ struct sho_search *search = sho_client_get_search(&cap->search, id);
+ if (!search || !sho_search_from_page(search, 0)) return NULL;
+ struct sho_result_page *page = &al_array_at(search->pages, 0);
+ 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;
}
+ if (entry) break;
}
- sho_search_free(&s);
+ sho_client_discard_search(&cap->search, id);
#else
if (!is_search) entry = cch_handler_http_create(unique_id);
#endif
@@ -88,9 +94,8 @@ static bool buffer_entry(struct cap_runner *cap, struct cap_list *list, struct c
{
if (entry->errored) return false;
if (entry->buffer_requested) return true;
- entry->entry = entry_for_unique_id(&entry->unique_id);
- if (!entry->entry) return false;
- if (!cap->callback(cap->userdata, CAP_BUFFER, &list->name, &entry->unique_id, entry->entry)) {
+ entry->entry = entry_for_unique_id(cap, &entry->unique_id);
+ if (!entry->entry || !cap->callback(cap->userdata, CAP_BUFFER, &list->name, &entry->unique_id, entry->entry)) {
entry->errored = true;
return false;
}
diff --git a/src/fruits/cap/cap.h b/src/fruits/cap/cap.h
index 57f6889..69d4d6d 100644
--- a/src/fruits/cap/cap.h
+++ b/src/fruits/cap/cap.h
@@ -1,11 +1,15 @@
#pragma once
-#define CAP_USE_PYTHON 1
+#define CAP_USE_PYTHON 0
#include <al/array.h>
#include <al/str.h>
#include <aki/thread.h>
+#if CAP_USE_PYTHON
+#include "../../shoki/src/search.h"
+#endif
+
struct cap_list_entry {
str unique_id;
struct cch_entry *entry;
@@ -34,6 +38,9 @@ enum {
struct cap_runner {
array(struct cap_list) lists;
+#if CAP_USE_PYTHON
+ struct sho_client search;
+#endif
bool (*callback)(void *, u8, str *, str *, void *);
void *userdata;
};
diff --git a/src/fruits/cap/meson.build b/src/fruits/cap/meson.build
index b5930ac..f85ab00 100644
--- a/src/fruits/cap/meson.build
+++ b/src/fruits/cap/meson.build
@@ -1,4 +1,8 @@
cap_src = ['cap.c']
-cap_deps = [common_deps, shoki]
+cap_deps = [common_deps]
+use_python = true
+if use_python
+ cap_deps += [shoki]
+endif
cap = declare_dependency(sources: cap_src,
dependencies: cap_deps)
diff --git a/src/fruits/cmc/cmc.c b/src/fruits/cmc/cmc.c
new file mode 100644
index 0000000..7fd8691
--- /dev/null
+++ b/src/fruits/cmc/cmc.c
@@ -0,0 +1,781 @@
+#include <al/lib.h>
+#include <al/wstr.h>
+#include <al/random.h>
+#include <aki/common.h>
+#include <aki/event_loop.h>
+#include <notcurses/notcurses.h>
+#include <notcurses/nckeys.h>
+
+#define STB_IMAGE_IMPLEMENTATION
+#include <stb_image.h>
+
+#include "../../libclient/client.h"
+#include "../../libclient/resource_client.h"
+#include "../../tree/common.h"
+
+enum {
+ CMC_LIST_SEARCHES = 0,
+ CMC_CREATE_SEARCH,
+ CMC_OPEN_SEARCH,
+ CMC_LIST_LISTS,
+ CMC_CREATE_LIST
+};
+
+struct cmc_view {
+ struct sho_post *post;
+ struct ncplane *container;
+ bool has_visual_data;
+};
+
+struct cmc_view_resource {
+ u16 id;
+ str unique_id;
+ u32 index;
+ struct ncvisual *v;
+ struct ncvgeom geom;
+ struct ncvisual_options vopts;
+};
+
+enum {
+ CMC_VIEW_NONE = 0,
+ CMC_VIEW_SINGLE,
+ CMC_VIEW_PARTITION
+};
+
+struct cmc_twitter_view {
+ struct cmc_view v;
+ struct sho_post *repost;
+ struct {
+ u32 width;
+ u32 height;
+ u32 y;
+ struct ncplane *p;
+ } text;
+ struct {
+ struct ncplane *p;
+ struct cmc_view_resource *resource;
+ } profile;
+ struct {
+ u8 mode;
+ u32 width;
+ u32 height;
+ u32 x;
+ struct ncplane *p[4];
+ struct cmc_view_resource *resources[4];
+ } preview;
+ struct {
+ struct ncplane *p;
+ } stats;
+};
+
+struct cmc {
+ u8 mode;
+ struct aki_event_loop loop;
+ struct camu_client client;
+ struct camu_resource_client resource_client;
+ u32 pending_requests;
+ struct notcurses *nc;
+ struct aki_poll input;
+ u32 rows;
+ u32 cols;
+ struct {
+ str provider;
+ str query;
+ s32 id;
+ struct camu_search *search;
+ } search;
+ struct {
+ struct ncplane *p;
+ array(struct cmc_view *) views;
+ u32 offset;
+ u32 roffset;
+ u32 prev;
+ u32 page;
+ u32 selected;
+ } ui;
+ array(struct cmc_view_resource *) resource_cache;
+};
+
+/*
+u64 color = 0;
+u32 background = 0;
+ncchannel_set_rgb8(&background, 255, 255, 255);
+ncchannel_set_alpha(&background, 0);
+//ncchannels_set_bchannel(&color, background);
+ncchannels_set_fchannel(&color, background);
+*/
+
+//struct nccell cell = NCCELL_INITIALIZER(L'█', ncplane_styles(view->v.container), ncplane_channels(view->v.container));
+//ncplane_vline(view->v.container, &cell, rows - 1);
+/*
+//ncplane_gradient(view->v.container, 0, 0, rows, width, "", 0, color, color, color, color);
+struct nccell ul = NCCELL_TRIVIAL_INITIALIZER, ur = NCCELL_TRIVIAL_INITIALIZER;
+struct nccell ll = NCCELL_TRIVIAL_INITIALIZER, lr = NCCELL_TRIVIAL_INITIALIZER;
+struct nccell hl = NCCELL_TRIVIAL_INITIALIZER, vl = NCCELL_TRIVIAL_INITIALIZER;
+nccells_ascii_box(view->v.container, 0, color, &ul, &ur, &ll, &lr, &hl, &vl);
+ncplane_box(view->v.container, &ul, &ur, &ll, &lr, &hl, &vl, rows, width, 0);
+nccell_release(view->v.container, &ul); nccell_release(view->v.container, &ur);
+nccell_release(view->v.container, &ll); nccell_release(view->v.container, &lr);
+nccell_release(view->v.container, &hl); nccell_release(view->v.container, &vl);
+//ncplane_double_box(view->v.container, NCSTYLE_NONE, color, rows, width, 0);
+*/
+
+static u32 count_wrapped_lines(wstr *w, u32 width)
+{
+ u32 counter = 0, wraps = 1;
+ for (u32 i = 0; i < w->len; i++) {
+ wchar_t wc = al_wstr_at(w, i);
+ s32 wcwidth = al_wchar_width(wc);
+ bool newline = wc == L'\n';
+ if (newline || counter + wcwidth > width) {
+ wraps++;
+ counter = 0;
+ }
+ if (newline) continue;
+ counter += wcwidth;
+ }
+ return wraps;
+}
+
+static void date_to_relative_date(struct sho_post *post, char *out)
+{
+ struct sho_post_date *created = NULL;
+ struct sho_post_date *date;
+ al_array_foreach_ptr(post->dates, i, date) {
+ if (date->type == SHOKI_DATE_CREATED) {
+ created = date;
+ break;
+ }
+ }
+ if (!created) return;
+ f64 seconds = (aki_get_timestamp() / 1000000.0 - created->timestamp);
+ s64 minutes = seconds / 60L;
+ s64 hours = minutes / 60L;
+ s64 days = hours / 24L;
+ if (days > 0) {
+ al_sprintf(out, "%lud", days);
+ } else if (hours > 0) {
+ al_sprintf(out, "%luh", hours);
+ } else if (minutes > 0) {
+ al_sprintf(out, "%lum", minutes);
+ } else {
+ al_sprintf(out, "%lus", (s64)seconds);
+ }
+}
+
+static void number_to_shorthand(s64 number, char *out)
+{
+ if (number >= 1000000) {
+ al_sprintf(out, "%.1fM", number / 1000000.f);
+ } else if (number >= 1000) {
+ al_sprintf(out, "%.1fK", number / 1000.f);
+ } else {
+ al_sprintf(out, "%ld", number);
+ }
+}
+
+#define TEXT_OFFSET 10
+
+#define PROFILE_SIZE 5
+#define PREVIEW_OFFSET_WIDTH 26
+
+/*
+static u32 calc_height_for_display(struct ncvgeom *geom, u32 width)
+{
+ f32 ratio = geom->pixx / (f32)geom->pixy;
+ if (ratio >= 1.f) {
+ return ceil((width / ratio) / 2.f) - 2;
+ } else {
+ return ceil((width / ratio) / 2.f) - 2;
+ }
+}
+*/
+
+static void resource_center_zoom(struct cmc_view_resource *resource, s32 width, s32 height)
+{
+ struct ncvgeom *geom = &resource->geom;
+ struct ncvisual_options *vopts = &resource->vopts;
+
+ f32 ratio = geom->pixx / (f32)geom->pixy;
+ f32 preview_ratio = width / (f32)height;
+ f32 diff;
+
+ f32 x0 = 0.f;
+ f32 y0 = 0.f;
+ f32 x1 = geom->pixx;
+ f32 y1 = geom->pixy;
+
+ // Not properly tested.
+ if (preview_ratio > ratio) {
+ diff = (geom->pixy - (geom->pixy / (preview_ratio * ratio))) / 2.f;
+ y0 += diff;
+ y1 -= diff;
+ } else {
+ diff = (geom->pixx - (geom->pixx / (preview_ratio / ratio))) / 2.f;
+ x0 += diff;
+ x1 -= diff;
+ }
+
+ vopts->begx = x0;
+ vopts->begy = y0;
+ vopts->lenx = x1 - x0;
+ vopts->leny = y1 - y0;
+}
+
+static void get_post_and_maybe_repost(struct cmc *c, str *unique_id, struct sho_post **post, struct sho_post **repost)
+{
+ *post = sho_post_cache_get(&c->client.cache, unique_id);
+ *repost = NULL;
+ if ((*post)->type == SHOKI_POST_REPOST) {
+ *repost = *post;
+ *post = sho_post_cache_get(&c->client.cache, &(*post)->post.unique_id);
+ }
+}
+
+static u32 layout_tweet(struct cmc *c, struct cmc_twitter_view *view, u32 width)
+{
+ struct sho_post *post = view->v.post;
+ struct sho_post *repost = view->repost;
+
+ u32 rows = 1;
+
+ view->text.width = width - TEXT_OFFSET;
+ view->text.height = 1 + count_wrapped_lines(&post->text, view->text.width);
+ if (repost) {
+ view->text.y = 1;
+ } else {
+ view->text.y = 0;
+ }
+ view->text.height += view->text.y;
+ rows += view->text.height;
+
+ view->v.has_visual_data = true;
+
+ for (u32 i = 0; i < 4; i++) {
+ if (view->preview.resources[i] && !view->preview.resources[i]->v) {
+ view->v.has_visual_data = false;
+ break;
+ }
+ }
+
+ if (view->preview.mode == CMC_VIEW_NONE || !view->v.has_visual_data) return rows;
+
+ view->preview.width = view->text.width;
+ u32 partition_width = view->preview.width / 2;
+ view->preview.x = TEXT_OFFSET + 1;
+ view->preview.height = 0;
+
+ struct ncplane_options opts = { 0 };
+ opts.cols = view->preview.width;
+ opts.rows = ncplane_dim_y(notcurses_stdplane(c->nc)) / 3;
+ opts.x = 0;
+ opts.y = 0;
+ struct ncplane *inter = ncplane_create(notcurses_stdplane(c->nc), &opts);
+
+ u32 left = 0, right = 0;
+ struct cmc_view_resource *resource;
+ switch (view->preview.mode) {
+ case CMC_VIEW_SINGLE: {
+ resource = view->preview.resources[0];
+ resource->vopts.n = inter;
+ ncvisual_geom(c->nc, resource->v, &resource->vopts, &resource->geom);
+ left += resource->geom.rcelly + 1;
+ break;
+ }
+ case CMC_VIEW_PARTITION: {
+ ncplane_resize_simple(inter, opts.rows, partition_width);
+
+ resource = view->preview.resources[0];
+ resource->vopts.n = inter;
+ ncvisual_geom(c->nc, resource->v, &resource->vopts, &resource->geom);
+ left += resource->geom.rcelly + 1;
+
+ resource = view->preview.resources[1];
+ resource->vopts.n = inter;
+ ncvisual_geom(c->nc, resource->v, &resource->vopts, &resource->geom);
+ right += resource->geom.rcelly + 1;
+
+ if (view->preview.resources[2]) {
+ resource = view->preview.resources[2];
+ resource->vopts.n = inter;
+ ncvisual_geom(c->nc, resource->v, &resource->vopts, &resource->geom);
+ left += resource->geom.rcelly + 1;
+ }
+
+ if (view->preview.resources[3]) {
+ resource = view->preview.resources[3];
+ resource->vopts.n = inter;
+ ncvisual_geom(c->nc, resource->v, &resource->vopts, &resource->geom);
+ right += resource->geom.rcelly + 1;
+ }
+
+ break;
+ }
+ }
+ view->preview.height = AL_MAX(right, left);
+
+ ncplane_destroy(inter);
+
+ return rows + view->preview.height;
+}
+
+static char format_buf[32];
+
+static void draw_tweet(struct cmc *c, struct cmc_twitter_view *view, struct ncplane *container, u32 rows, u32 width)
+{
+ struct sho_post *post = view->v.post;
+ struct sho_post *repost = view->repost;
+
+ ncplane_resize_simple(container, rows, width);
+
+ if (view->v.has_visual_data) {
+ u32 partition_width = view->preview.width / 2;
+ struct cmc_view_resource *resource;
+ switch (view->preview.mode) {
+ case CMC_VIEW_NONE:
+ break;
+ case CMC_VIEW_SINGLE: {
+ resource = view->preview.resources[0];
+ ncplane_resize_simple(view->preview.p[0], resource->geom.rcelly + 1, view->preview.width);
+ ncplane_move_yx(view->preview.p[0], view->text.height, view->preview.x);
+ resource->vopts.n = view->preview.p[0];
+ ncvisual_blit(c->nc, resource->v, &resource->vopts);
+ break;
+ }
+ case CMC_VIEW_PARTITION: {
+ resource = view->preview.resources[0];
+ resource->vopts.n = view->preview.p[0];
+ ncplane_resize_simple(view->preview.p[0], resource->geom.rcelly + 1, partition_width);
+ ncplane_move_yx(view->preview.p[0], view->text.height, view->preview.x);
+ ncvisual_blit(c->nc, resource->v, &resource->vopts);
+
+ resource = view->preview.resources[1];
+ ncplane_resize_simple(view->preview.p[1], resource->geom.rcelly + 1, partition_width);
+ ncplane_move_yx(view->preview.p[1], view->text.height, (view->preview.x + view->preview.resources[0]->geom.rcellx) + 2);
+ resource->vopts.n = view->preview.p[1];
+ ncvisual_blit(c->nc, resource->v, &resource->vopts);
+
+ if (view->preview.resources[2]) {
+ resource = view->preview.resources[2];
+ ncplane_resize_simple(view->preview.p[2], resource->geom.rcelly + 1, partition_width);
+ ncplane_move_yx(view->preview.p[2], view->preview.resources[0]->geom.rcelly + view->text.height + 2, view->preview.x);
+ resource->vopts.n = view->preview.p[2];
+ ncvisual_blit(c->nc, resource->v, &resource->vopts);
+ }
+
+ if (view->preview.resources[3]) {
+ resource = view->preview.resources[3];
+ ncplane_resize_simple(view->preview.p[3], resource->geom.rcelly + 1, partition_width);
+ ncplane_move_yx(view->preview.p[3], view->preview.resources[2]->geom.rcelly + view->text.height + 2, (view->preview.x + view->preview.resources[2]->geom.rcellx) + 2);
+ resource->vopts.n = view->preview.p[3];
+ ncvisual_blit(c->nc, resource->v, &resource->vopts);
+ }
+
+ break;
+ }
+ }
+ }
+
+ ncplane_resize_simple(view->text.p, view->text.height, width);
+ ncplane_move_yx(view->text.p, 0, 0);
+
+ s32 offset = TEXT_OFFSET;
+
+ if (repost) {
+ const wchar_t *retweet = L"⮤⮧";
+ ncplane_set_styles(view->text.p, NCSTYLE_ITALIC);
+ ncplane_putwstr_yx(view->text.p, 0, offset - 3, retweet);
+ ncplane_set_styles(view->text.p, NCSTYLE_NONE);
+ ncplane_putwstr_yx(view->text.p, 0, offset, &al_wstr_at(&repost->author.display_name, 0));
+ }
+
+ ncplane_set_styles(view->text.p, NCSTYLE_BOLD);
+
+ ncplane_putwstr_yx(view->text.p, view->text.y, offset, &al_wstr_at(&post->author.display_name, 0));
+ offset += al_wstr_width(&post->author.display_name) + 1;
+
+ ncplane_set_styles(view->text.p, NCSTYLE_NONE);
+ ncplane_putwstr_yx(view->text.p, view->text.y, offset, &al_wstr_at(&post->author.username, 0));
+ offset += al_wstr_width(&post->author.username) + 1;
+
+ ncplane_putwstr_yx(view->text.p, view->text.y, offset, L"∙");
+ offset += 2;
+
+ date_to_relative_date(post, format_buf);
+ ncplane_putstr_yx(view->text.p, view->text.y, offset, format_buf);
+
+ u32 text_y = view->text.y + 1, text_x = TEXT_OFFSET;
+ for (u32 i = 0; i < post->text.len; i++) {
+ wchar_t wc = al_wstr_at(&post->text, i);
+ s32 wcwidth = al_wchar_width(wc);
+ bool newline = wc == L'\n';
+ if (newline || text_x + wcwidth > width) {
+ text_y++;
+ text_x = TEXT_OFFSET;
+ }
+ if (newline) continue;
+ ncplane_putwc_yx(view->text.p, text_y, text_x, wc);
+ text_x += wcwidth;
+ }
+
+ ncplane_resize_simple(view->stats.p, 1, view->text.width);
+ ncplane_move_yx(view->stats.p, rows - 1, TEXT_OFFSET);
+
+ u32 part = 10;
+ offset = 1;
+
+ if (post->comments.set) {
+ const wchar_t *comment = L"🗩";
+ //const wchar_t *comment = L"[]";
+ //const wchar_t *comment = L"。○";
+ //const wchar_t *comment = L"❑";
+ //const wchar_t *comment = L"。ロ";
+ //const wchar_t *comment = L".❍";
+ ncplane_set_styles(view->stats.p, NCSTYLE_ITALIC);
+ ncplane_putwstr_yx(view->stats.p, 0, offset, comment);
+ ncplane_set_styles(view->stats.p, NCSTYLE_NONE);
+ number_to_shorthand(post->comments.i, format_buf);
+ ncplane_putstr_yx(view->stats.p, 0, offset + 3, format_buf);
+ }
+ offset += part;
+
+ if (post->reposts.set) {
+ const wchar_t *retweet = L"⮤⮧";
+ ncplane_set_styles(view->stats.p, NCSTYLE_ITALIC);
+ ncplane_putwstr_yx(view->stats.p, 0, offset, retweet);
+ ncplane_set_styles(view->stats.p, NCSTYLE_NONE);
+ number_to_shorthand(post->reposts.i, format_buf);
+ ncplane_putstr_yx(view->stats.p, 0, offset + 3, format_buf);
+ }
+ offset += part;
+
+ if (post->likes.set) {
+ const wchar_t *like = L"❤";
+ ncplane_set_styles(view->stats.p, NCSTYLE_ITALIC);
+ ncplane_putwstr_yx(view->stats.p, 0, offset, like);
+ ncplane_set_styles(view->stats.p, NCSTYLE_NONE);
+ number_to_shorthand(post->likes.i, format_buf);
+ ncplane_putstr_yx(view->stats.p, 0, offset + 3, format_buf);
+ }
+
+ if (view->profile.resource && view->profile.resource->v) {
+ struct cmc_view_resource *resource = view->profile.resource;
+ ncplane_resize_simple(view->profile.p, (PROFILE_SIZE + 1) / 2, PROFILE_SIZE);
+ ncplane_move_yx(view->profile.p, view->text.y, PROFILE_SIZE - 2);
+ resource->vopts.n = view->profile.p;
+ ncvisual_blit(c->nc, resource->v, &resource->vopts);
+ }
+}
+
+static void layout_tweets(struct cmc *c)
+{
+ struct cmc_twitter_view *view;
+ for (u32 i = 0; i < c->ui.views.size; i++) {
+ view = (struct cmc_twitter_view *)al_array_at(c->ui.views, i);
+ ncplane_erase(view->v.container);
+ ncplane_erase(view->text.p);
+ ncplane_erase(view->profile.p);
+ for (u32 i = 0; i < 4; i++) {
+ ncplane_erase(view->preview.p[i]);
+ }
+ ncplane_erase(view->stats.p);
+ ncplane_reparent_family(view->v.container, view->v.container);
+ }
+
+ u32 y = 0;
+ u32 width = ncplane_dim_x(c->ui.p);
+ u32 rows = 0;
+ u32 i = c->ui.offset;
+ c->ui.page = 0;
+ for (; i < c->ui.views.size; i++) {
+ view = (struct cmc_twitter_view *)al_array_at(c->ui.views, i);
+ rows = layout_tweet(c, view, width);
+ if (y + rows > ncplane_dim_y(c->ui.p)) break;
+ c->ui.page++;
+ draw_tweet(c, view, view->v.container, rows, width);
+ ncplane_reparent_family(view->v.container, c->ui.p);
+ ncplane_move_yx(view->v.container, y, 0);
+ if (i - c->ui.offset == c->ui.selected) {
+ wchar_t box = L'█';
+ u32 i = view->repost ? 1 : 0;
+ for (; i < rows - 1; i++) {
+ ncplane_cursor_move_yx(view->v.container, i, 0);
+ ncplane_putwc(view->v.container, box);
+ }
+ }
+ y += rows + 1;
+ }
+
+ if (!c->ui.page) c->ui.page = 1;
+
+ if (c->ui.roffset != c->ui.offset) {
+ notcurses_refresh(c->nc, NULL, NULL);
+ }
+ notcurses_render(c->nc);
+
+ c->ui.roffset = c->ui.offset;
+
+ if (c->pending_requests == 0 && c->ui.offset + c->ui.page >= c->ui.views.size) {
+ camu_client_more_results(&c->client, c->search.search);
+ }
+}
+
+static void resource_client_callback(void *userdata, u16 id, struct aki_buffer *buffer)
+{
+ struct cmc *c = (struct cmc *)userdata;
+ struct cmc_view_resource *resource;
+ al_array_foreach(c->resource_cache, i, resource) {
+ if (resource->id == id) {
+ s32 w, h, channels;
+ u8 *pixels = stbi_load_from_memory(aki_buffer_get_ptr(buffer, 0), buffer->size, &w, &h, &channels, 4);
+ resource->v = ncvisual_from_rgba(pixels, h, w * 4, w);
+ stbi_image_free(pixels);
+ ncvisual_geom(c->nc, resource->v, &resource->vopts, &resource->geom);
+ break;
+ }
+ }
+ if (--c->pending_requests == 0) layout_tweets(c);
+}
+
+static void input_poll_callback(void *userdata, s32 revents)
+{
+ struct cmc *c = (struct cmc *)userdata;
+ (void)revents;
+ struct ncinput input;
+ u32 ret;
+ bool layout = false;
+ do {
+ ret = notcurses_get_nblock(c->nc, &input);
+ if (ret == (u32)-1 || ret == 0) break;
+ if (input.evtype == NCTYPE_PRESS || input.evtype == NCTYPE_UNKNOWN) {
+ if (input.id == 'j') {
+ if (c->ui.selected + 1 >= c->ui.page) {
+ if (c->ui.offset + c->ui.page >= c->ui.views.size) {
+ return;
+ }
+ c->ui.offset += c->ui.page;
+ c->ui.prev = c->ui.page;
+ c->ui.page = 0;
+ c->ui.selected = 0;
+ } else {
+ c->ui.selected++;
+ }
+ layout = true;
+ } else if (input.id == 'k') {
+ if ((s32)c->ui.selected - 1 < 0) {
+ if (c->ui.offset == 0 || (s32)c->ui.offset - (s32)c->ui.prev < 0) {
+ return;
+ }
+ c->ui.offset -= c->ui.prev;
+ c->ui.selected = c->ui.prev - 1;
+ c->ui.prev = 0;
+ } else {
+ c->ui.selected--;
+ }
+ layout = true;
+ } else if (input.id == NCKEY_RETURN) {
+ if (c->ui.views.size > 0) {
+ u32 selected = c->ui.offset + c->ui.selected;
+ struct cmc_twitter_view *view = (struct cmc_twitter_view *)al_array_at(c->ui.views, selected);
+ camu_client_add(&c->client, &view->v.post->unique_id, 0);
+ }
+ } else if (input.id == 'q') {
+ aki_event_loop_break(&c->loop);
+ break;
+ }
+ }
+ } while (1);
+ if (layout) layout_tweets(c);
+}
+
+static struct cmc_view_resource *create_resource(struct cmc *c, str *unique_id, u32 index)
+{
+ struct cmc_view_resource *resource = al_alloc_object(struct cmc_view_resource);
+ al_array_push(c->resource_cache, resource);
+ resource->vopts.blitter = NCBLIT_PIXEL;
+ //resource->vopts.blitter = NCBLIT_2x1;
+ resource->vopts.scaling = NCSCALE_SCALE_HIRES;
+ //resource->vopts.scaling = NCSCALE_STRETCH;
+ resource->id = al_rand_u16();
+ al_str_clone(&resource->unique_id, unique_id);
+ resource->index = index;
+ c->pending_requests++;
+ camu_resource_request(&c->resource_client, unique_id, index, resource->id);
+ return resource;
+}
+
+static struct cmc_view_resource *get_resource_from_cache(struct cmc *c, str *unique_id, u32 index)
+{
+ struct cmc_view_resource *resource;
+ al_array_foreach(c->resource_cache, i, resource) {
+ if (al_str_eq(&resource->unique_id, unique_id) && resource->index == index) {
+ return resource;
+ }
+ }
+ return create_resource(c, unique_id, index);
+}
+
+static int resize_cb(struct ncplane *p)
+{
+ struct cmc *c = (struct cmc *)ncplane_userptr(p);
+ u32 width, height;
+ notcurses_stddim_yx(c->nc, &height, &width);
+ struct ncplane_options nopts = { 0 };
+ nopts.rows = height - 1;
+ nopts.cols = width - 4;
+ nopts.x = 3;
+ nopts.y = 1;
+ nopts.flags = 0;
+ struct ncplane *oldp = c->ui.p;
+ c->ui.p = ncplane_create(p, &nopts);
+ if (oldp) {
+ ncplane_destroy(oldp);
+ layout_tweets(c);
+ }
+ return 0;
+}
+
+static void client_callback(void *userdata, u8 op, void *opaque)
+{
+ struct cmc *c = (struct cmc *)userdata;
+ switch (op) {
+ case CAMU_CLIENT_STATUS_UPDATED: {
+ switch (c->mode) {
+ case CMC_LIST_SEARCHES: {
+ struct camu_search *search;
+ al_array_foreach(c->client.state.searches, i, search) {
+ al_printf("%i %.*s\n", search->id, AL_STR_PRINTF(&search->query));
+ }
+ aki_event_loop_break(&c->loop);
+ break;
+ }
+ case CMC_LIST_LISTS: {
+ struct camu_list *list;
+ al_array_foreach(c->client.state.lists, i, list) {
+ al_printf("%.*s\n", AL_STR_PRINTF(&list->name));
+ }
+ aki_event_loop_break(&c->loop);
+ break;
+ }
+ case CMC_CREATE_SEARCH: {
+ camu_client_create_search(&c->client, &c->search.provider, &c->search.query);
+ break;
+ }
+ case CMC_OPEN_SEARCH: {
+ if (!(c->nc = notcurses_init(NULL, stdin))) return;
+ if (c->client.state.searches.size == 0) return;
+ c->search.search = al_array_at(c->client.state.searches, 0);
+ struct ncplane *stdplane = notcurses_stdplane(c->nc);
+ ncplane_set_resizecb(stdplane, resize_cb);
+ ncplane_set_userptr(stdplane, c);
+ resize_cb(stdplane);
+ camu_client_resume_search(&c->client, c->search.search);
+ aki_poll_init(&c->input, input_poll_callback, c);
+ aki_poll_set(&c->input, notcurses_inputready_fd(c->nc), AKI_POLL_READ);
+ aki_poll_start(&c->input, &c->loop);
+ break;
+ }
+ }
+ break;
+ }
+ case CAMU_CLIENT_SEARCH_CREATED: {
+ aki_event_loop_break(&c->loop);
+ break;
+ }
+ case CAMU_CLIENT_RESULTS: {
+ struct camu_search_results *results = (struct camu_search_results *)opaque;
+ str *unique_id;
+ al_array_foreach_ptr(results->unique_ids, i, unique_id) {
+ struct cmc_twitter_view *view = al_alloc_object(struct cmc_twitter_view);
+ al_array_push(c->ui.views, (struct cmc_view *)view);
+ get_post_and_maybe_repost(c, unique_id, &view->v.post, &view->repost);
+
+ struct ncplane_options default_options = { 0 };
+ default_options.cols = 1;
+ default_options.rows = 1;
+ default_options.x = 0;
+ default_options.y = 0;
+
+ view->v.container = ncpile_create(c->nc, &default_options);
+ view->text.p = ncplane_create(view->v.container, &default_options);
+ view->profile.p = ncplane_create(view->v.container, &default_options);
+ for (u32 i = 0; i < 4; i++) {
+ view->preview.p[i] = ncplane_create(view->v.container, &default_options);
+ }
+ view->stats.p = ncplane_create(view->v.container, &default_options);
+
+ u32 prev_pending = c->pending_requests;
+
+ struct sho_post *post = view->v.post;
+ view->profile.resource = get_resource_from_cache(c, &post->author.unique_id, 0);
+ struct sho_post_media *media;
+ al_array_foreach_ptr(post->media, i, media) {
+ (void)media;
+ view->preview.resources[i] = get_resource_from_cache(c, &post->unique_id, i);
+ }
+
+ view->v.has_visual_data = prev_pending == c->pending_requests;
+
+ if (post->media.size == 0) {
+ view->preview.mode = CMC_VIEW_NONE;
+ } else if (post->media.size == 1) {
+ view->preview.mode = CMC_VIEW_SINGLE;
+ } else {
+ view->preview.mode = CMC_VIEW_PARTITION;
+ }
+ }
+ if (c->pending_requests == 0) layout_tweets(c);
+ break;
+ }
+ }
+}
+
+s32 main(s32 argc, char *argv[])
+{
+ if (argc < 2 || !aki_common_init()) return EXIT_FAILURE;
+
+ struct cmc c = { 0 };
+
+ al_array_init(c.ui.views);
+ al_array_init(c.resource_cache);
+ c.pending_requests = 0;
+ c.ui.offset = 0;
+
+ str *cmd = al_str_c(argv[1]);
+ if (al_str_eq(cmd, al_str_c("search"))) {
+ if (argc == 2) {
+ c.mode = CMC_LIST_SEARCHES;
+ } else {
+ c.mode = CMC_CREATE_SEARCH;
+ al_str_from(&c.search.provider, "twitter");
+ al_str_from(&c.search.query, argv[2]);
+ }
+ } else if (al_str_eq(cmd, al_str_c("open"))) {
+ c.mode = CMC_OPEN_SEARCH;
+ } else if (al_str_eq(cmd, al_str_c("list"))) {
+ if (argc == 2) {
+ c.mode = CMC_LIST_LISTS;
+ } else {
+ }
+ } else {
+ return EXIT_FAILURE;
+ }
+
+ aki_event_loop_init(&c.loop);
+
+ camu_client_init(&c.client, &c.loop, client_callback, &c);
+ camu_client_login(&c.client, al_str_c("andrew"), al_str_c("127.0.0.1"), TREE_PORT);
+
+ camu_resource_client_init(&c.resource_client, &c.loop, resource_client_callback, &c);
+ camu_resource_client_connect(&c.resource_client, al_str_c("127.0.0.1"), TREE_RESOURCE_PORT);
+
+ aki_event_loop_run(&c.loop);
+
+ if (c.mode == CMC_OPEN_SEARCH) notcurses_stop(c.nc);
+
+ camu_client_close(&c.client);
+
+ aki_common_close();
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/fruits/cmc/meson.build b/src/fruits/cmc/meson.build
new file mode 100644
index 0000000..45ac9c2
--- /dev/null
+++ b/src/fruits/cmc/meson.build
@@ -0,0 +1,7 @@
+cmc_src = ['cmc.c']
+cmc_deps = [common_deps, libclient, cache, stb_image]
+
+notcurses = dependency('notcurses')
+cmc_deps += [notcurses]
+
+executable('cmc', cmc_src, dependencies: cmc_deps)
diff --git a/src/fruits/cmv/cmv2.c b/src/fruits/cmv/cmv2.c
index 74a4113..89e3427 100644
--- a/src/fruits/cmv/cmv2.c
+++ b/src/fruits/cmv/cmv2.c
@@ -1,7 +1,8 @@
-#define CMV_USE_TUI 1
-#define CMV_USE_SOCKET 1
+#define CMV_USE_TUI 0
+#define CMV_USE_SOCKET 0
#include <al/log.h>
+#include <aki/event_loop.h>
#if CMV_USE_SOCKET
#include <aki/line_processor.h>
#endif
@@ -14,7 +15,8 @@
#include "../../render/renderer_libplacebo.h"
//#include "../../render/renderer_tiger.h"
-#include "../../libsink/sink.h"
+#include "../../libsink/sink2.h"
+#include "../../tree/common.h"
#include "../../shoki/src/search.h"
@@ -162,14 +164,7 @@ static void screen_callback(void *userdata, u8 op, f64 float0)
}
}
-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;
@@ -190,6 +185,7 @@ static bool cap_callback(void *userdata, u8 op, str *name, str *unique_id, void
}
return true;
}
+*/
#if CMV_USE_SOCKET
static u8 line_callback(void *userdata, str *line)
@@ -214,7 +210,7 @@ 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->runner, &entry->clock);
+ tui_draw(&c->tui, &((struct camu_sink_local *)entry)->runner, &entry->clock);
} else {
tui_draw(&c->tui, NULL, NULL);
}
@@ -233,22 +229,39 @@ static s32 log_callback(void *userdata, char *s)
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);
+ if (level < AV_LOG_VERBOSE) al_logv("warn", "lav_internal", (char *)fmt, args);
}
#endif
#endif
+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 struct cmv c = { 0 };
+
+static void sigint_handler(s32 signum)
+{
+ (void)signum;
+ c.quit = 1;
+}
+
s32 main(s32 argc, char *argv[])
{
- aki_common_init();
+ if (!aki_common_init()) return EXIT_FAILURE;
- struct cmv c = { };
+ signal(SIGINT, sigint_handler);
+
+ aki_event_loop_init(&c.loop);
#if CMV_USE_TUI
- if (!tui_init(&c.tui)) {
- aki_common_close();
- return EXIT_FAILURE;
- }
+ if (!tui_init(&c.tui, &c.loop)) {
+ goto err;
+ };
al_set_print(log_callback, &c);
#ifdef HAVE_FFMPEG
camu_lav_set_log_callback(lav_log_callback);
@@ -257,37 +270,35 @@ s32 main(s32 argc, char *argv[])
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;
+ if (!camu_screen_init(&c.scr) || !camu_screen_create_window(&c.scr, "cmv")) {
+ goto err;
}
c.renderer = camu_renderer_lp_create();
//c.renderer = camu_renderer_tiger_create();
- camu_screen_create_renderer(&c.scr, c.renderer);
+ if (!camu_screen_create_renderer(&c.scr, c.renderer)) {
+ goto err;
+ }
+ c.renderer->render(c.renderer, &c.scr);
#if CAP_USE_PYTHON
- bool py_init = sho_python_init();
+ bool py_init = argc == 1 ? sho_python_init() : false;
#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);
-
+ camu_sink_init(&c.sink, &c.loop, &c.mixer, c.renderer);
c.sink.callback = sink_callback;
c.sink.userdata = &c;
- camu_sink_init(&c.sink, &c.loop, &c.mixer, c.renderer);
+ camu_sink_connect(&c.sink, al_str_c("127.0.0.1"), TREE_PORT);
+ /*
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]));
}
+ */
#if CMV_USE_SOCKET
c.socket.type = AKI_SOCKET_UNIX;
@@ -304,7 +315,7 @@ s32 main(s32 argc, char *argv[])
#if CMV_USE_TUI
aki_timer_init(&c.timer, &c.loop, timer_callback, &c);
- aki_timer_set_repeat(&c.timer, 0.10);
+ aki_timer_set_repeat(&c.timer, 0.05);
aki_timer_again(&c.timer);
#endif
@@ -326,10 +337,10 @@ s32 main(s32 argc, char *argv[])
aki_thread_join(&thread0);
camu_mixer_close(&c.mixer);
- camu_screen_close(&c.scr);
c.renderer->free(&c.renderer);
+ camu_screen_close(&c.scr);
- cap_close(&c.cap);
+ //cap_close(&c.cap);
#if CAP_USE_PYTHON
if (py_init) sho_python_close();
@@ -342,4 +353,11 @@ s32 main(s32 argc, char *argv[])
aki_common_close();
return EXIT_SUCCESS;
+err:
+#if CMV_USE_TUI
+ tui_close(&c.tui);
+#endif
+ aki_event_loop_destroy(&c.loop);
+ aki_common_close();
+ return EXIT_FAILURE;
}
diff --git a/src/fruits/cmv/meson.build b/src/fruits/cmv/meson.build
index b604104..1c9ed2d 100644
--- a/src/fruits/cmv/meson.build
+++ b/src/fruits/cmv/meson.build
@@ -1,26 +1,10 @@
-cmv2_src = ['cmv2.c', 'tui.c']
-#cmv2_src = ['cmv2.c']
+cmv_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'
-]
+use_tui = true
+if use_tui
+ cmv_src += ['tui.c']
+ cmv_deps += [dependency('notcurses')]
+endif
-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])
+executable('cmv', cmv_src, dependencies: cmv_deps)
diff --git a/src/fruits/cmv/tui.c b/src/fruits/cmv/tui.c
index 21aeac8..8315a13 100644
--- a/src/fruits/cmv/tui.c
+++ b/src/fruits/cmv/tui.c
@@ -1,6 +1,7 @@
#include <aki/file.h>
#include <al/log.h>
#include <sys/ioctl.h>
+#include <notcurses/direct.h>
#include "tui.h"
@@ -23,16 +24,39 @@ static void handle_winch(s32 sig)
aki_mutex_unlock(&tui->mutex);
}
-bool tui_init(struct cmv_tui *tui)
+static void input_poll_callback(void *userdata, s32 revents)
+{
+ struct cmv_tui *tui = (struct cmv_tui *)userdata;
+ (void)revents;
+ struct ncinput input;
+ u32 ret;
+ while (1) {
+ ret = ncdirect_get_nblock(tui->dir, &input);
+ if (ret == (u32)-1 || ret == 0) break;
+ if (input.evtype == NCTYPE_PRESS || input.evtype == NCTYPE_UNKNOWN) {
+ }
+ }
+}
+
+bool tui_init(struct cmv_tui *tui, struct aki_event_loop *loop)
{
tui->update_size = true;
tui->width = 0;
tui->height = 0;
- //tui->prev_lines = 0;
+ tui->prev_lines = 0;
- tui->fd = fileno(stdin);
+ tui->fd = fileno(stdout);
tui->out = fdopen(tui->fd, "w");
if (!tui->out) return false;
+ tui->buffer = al_malloc(256);
+
+ //if (!(tui->dir = ncdirect_init(NULL, tui->out, 0))) {
+ // fclose(tui->out);
+ // return false;
+ //}
+
+ //aki_poll_init(&tui->poll, ncdirect_inputready_fd(tui->dir), AKI_POLL_READ, input_poll_callback, tui);
+ //aki_poll_start(&tui->poll, loop);
tui_global = tui;
aki_mutex_init(&tui->mutex);
@@ -46,81 +70,96 @@ bool tui_init(struct cmv_tui *tui)
tui->term.c_lflag &= ~ECHO;
tcsetattr(tui->fd, 0, &tui->term);
- al_array_init(tui->log_buffer);
- tui->last_pts = 0.0;
+ al_queue_init(&tui->log_buffer, 256);
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);
+ al_queue_push(&tui->log_buffer, msg);
}
-static char buffer[128];
-
-static void draw_now_playing(struct cmv_tui *tui, struct camu_clock *clock, struct bmu_local *runner)
+static void draw_now_playing(struct cmv_tui *tui, struct bmu_local *runner, struct camu_clock *clock)
{
+ char *buffer = tui->buffer;
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);
+ s32 text = 0;
+ s64 minute = (s64)(pts / 60L);
+ s64 hour = minute / 60L;
+ minute -= hour * 60L;
+ text += al_sprintf(buffer + text, "[");
+ if (hour > 0) text += al_sprintf(buffer + text, "%.2ld:", hour);
+ text += al_sprintf(buffer + text, "%.2ld:%.2ld/", minute, (s64)pts % 60L);
+ minute = (s64)(duration / 60L);
+ hour = minute / 60L;
+ minute -= hour * 60L;
+ if (hour > 0) text += al_sprintf(buffer + text, "%.2ld:", hour);
+ text += al_sprintf(buffer + text, "%.2ld:%.2ld", minute, (s64)duration % 60L);
+ text += al_sprintf(buffer + text, "]");
+ s32 parts = 0;
if (pts > 0.0 && duration != 0.0) {
if (pts > duration) pts = duration;
f64 percent = pts / duration;
- parts = tui->width * percent;
+ parts = ((tui->width - text) * percent);
} else if (duration == 0.0) {
- parts = tui->width;
+ parts = (tui->width - text);
}
- al_memset(buffer, '-', parts);
- buffer[parts] = '\0';
+ al_memset(buffer + text, '-', parts);
+ buffer[text + parts] = '\0';
fprintf(tui->out, "%s", buffer);
}
void tui_draw(struct cmv_tui *tui, struct bmu_local *runner, struct camu_clock *clock)
{
aki_mutex_lock(&tui->mutex);
-
if (tui->update_size) {
update_term_size(tui);
tui->update_size = false;
}
+ aki_mutex_unlock(&tui->mutex);
+ // Erase line.
fprintf(tui->out, "\r\033[K");
- //for (u32 i = 1; i < tui->prev_lines; i++) {
- // fprintf(tui->out, "\033[A\r\033[K");
- //}
+ for (u32 i = 0; i < tui->prev_lines; i++) {
+ // Up one, erase line.
+ 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);
+ while (al_queue_pop(&tui->log_buffer, (void **)&msg)) {
fprintf(tui->out, "%s\n", msg);
al_free(msg);
}
- //tui->prev_lines = 0;
+ // Simply printing each log message with a newline will leave
+ // a one character high space in the bottom of the terminal window.
+ // We use that to display a very basic status line (like mpv).
+ // Trying to use more than that one line space without a more complete
+ // terminal interface gets scuffed very fast.
+
+ tui->prev_lines = 0;
if (runner) {
- draw_now_playing(tui, clock, runner);
+ draw_now_playing(tui, runner, clock);
}
-
- aki_mutex_unlock(&tui->mutex);
}
void tui_close(struct cmv_tui *tui)
{
+ if (!tui->out) return;
+ al_free(tui->buffer);
char *msg;
- al_array_foreach(tui->log_buffer, i, msg) {
+ while (al_queue_pop(&tui->log_buffer, (void **)&msg)) {
al_free(msg);
}
- al_array_free(tui->log_buffer);
+ al_queue_free(&tui->log_buffer);
aki_mutex_destroy(&tui->mutex);
fprintf(tui->out, "\n");
fflush(tui->out);
tui->term.c_lflag |= ECHO;
tcsetattr(tui->fd, 0, &tui->term);
+ //ncdirect_stop(tui->dir);
fclose(tui->out);
}
diff --git a/src/fruits/cmv/tui.h b/src/fruits/cmv/tui.h
index fb4215e..6898539 100644
--- a/src/fruits/cmv/tui.h
+++ b/src/fruits/cmv/tui.h
@@ -1,24 +1,28 @@
#pragma once
+#include <al/queue.h>
+#include <aki/event_loop.h>
#include <termios.h>
#include "../../bimu/local.h"
-#include "../../buffer/clock.h"
+#include "../../buffer/video.h"
struct cmv_tui {
s32 fd;
FILE *out;
+ char *buffer;
+ struct ncdirect *dir;
+ struct aki_poll poll;
struct aki_mutex mutex;
struct termios term;
bool update_size;
u32 width;
u32 height;
- //u32 prev_lines;
- array(char *) log_buffer;
- f64 last_pts;
+ u32 prev_lines;
+ queue log_buffer;
};
-bool tui_init(struct cmv_tui *tui);
+bool tui_init(struct cmv_tui *tui, struct aki_event_loop *loop);
void tui_push_log_msg(struct cmv_tui *tui, char *msg);
void tui_draw(struct cmv_tui *tui, struct bmu_local *runner, struct camu_clock *clock);
void tui_close(struct cmv_tui *tui);
diff --git a/src/libclient/client.c b/src/libclient/client.c
index 60998c6..e42f662 100644
--- a/src/libclient/client.c
+++ b/src/libclient/client.c
@@ -1,6 +1,3 @@
-#include "../tree/commands.h"
-#include "../shoki/src/packet_ext.h"
-
#include "client.h"
static void identifed_callback(void *userdata, struct aki_packet *packet)
@@ -16,7 +13,7 @@ static void connection_callback(void *userdata, struct aki_rpc_connection *conn)
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_packet_write_str(packet, &client->username);
aki_rpc_connection_command(client->conn, packet, identifed_callback, client);
}
@@ -33,66 +30,161 @@ static u8 packet_pool_callback(void *userdata, struct aki_packet *packet)
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)
+static bool status_command_callback(void *userdata, struct aki_rpc_connection *conn,
+ struct aki_packet *packet, struct aki_packet *rpacket)
{
struct camu_client *client = (struct camu_client *)userdata;
- aki_event_loop_run(&client->loop);
- return 0;
+ (void)conn;
+ (void)rpacket;
+
+ str s;
+ u32 size = aki_packet_read_u32(packet);
+ for (u32 i = 0; i < size; i++) {
+ struct camu_list *list = al_alloc_object(struct camu_list);
+ aki_packet_read_str(packet, &s);
+ al_str_clone(&list->name, &s);
+ al_array_push(client->state.lists, list);
+ }
+
+ size = aki_packet_read_u32(packet);
+ for (u32 i = 0; i < size; i++) {
+ struct camu_search *search = al_alloc_object(struct camu_search);
+ search->id = aki_packet_read_s32(packet);
+ aki_packet_read_str(packet, &s);
+ al_str_clone(&search->provider, &s);
+ aki_packet_read_str(packet, &s);
+ al_str_clone(&search->query, &s);
+ search->page = aki_packet_read_s32(packet);
+ al_array_push(client->state.searches, search);
+ }
+
+ client->callback(client->userdata, CAMU_CLIENT_STATUS_UPDATED, NULL);
+
+ return false;
}
-bool camu_client_login(struct camu_client *client, str *username, str *addr, s32 port,
+static struct aki_rpc_command commands[] = {
+ { .op = TREE_CMD_STATUS, .callback = status_command_callback, .userdata = NULL }
+};
+
+bool camu_client_init(struct camu_client *client, struct aki_event_loop *loop,
void (*callback)(void *, u8, void *), void *userdata)
{
- al_str_clone(&client->username, username);
+ client->loop = loop;
client->callback = callback;
client->userdata = userdata;
- if (!aki_rpc_connect(&client->client, &client->loop, addr, port)) {
+ client->username = AL_STR_EMPTY;
+ al_array_init(client->state.lists);
+ aki_packet_pool_init_ex(&client->pool, 0, client->loop,
+ AKI_PACKET_POOL_MODE_PASSTHROUGH, packet_pool_callback, client);
+ if (!aki_rpc_init(&client->client, AKI_SOCKET_TCP, connection_callback,
+ connection_closed_callback, client)) {
+ return false;
+ }
+ for (u32 i = 0; i < AL_ARRAY_SIZE(commands); i++) {
+ commands[i].userdata = client;
+ aki_rpc_add_command(&client->client, &commands[i]);
+ }
+ sho_post_cache_init(&client->cache);
+ return true;
+}
+
+bool camu_client_login(struct camu_client *client, str *username, str *addr, s32 port)
+{
+ al_str_clone(&client->username, username);
+ 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)
+static void create_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);
+ s32 id = aki_packet_read_s32(packet);
+ client->callback(client->userdata, CAMU_CLIENT_SEARCH_CREATED, &id);
+}
+
+void camu_client_create_search(struct camu_client *client, str *provider, str *query)
+{
+ struct aki_packet *packet = aki_rpc_get_packet(&client->client, TREE_CMD_SEARCH);
+ aki_packet_write_str(packet, provider);
+ aki_packet_write_str(packet, query);
+ packet->userdata = create_search_callback;
+ aki_packet_pool_submit(&client->pool, packet);
+}
+
+static struct camu_search *search_from_id(struct camu_client *client, s32 id)
+{
+ struct camu_search *search;
+ al_array_foreach(client->state.searches, i, search) {
+ if (search->id == id) return search;
+ }
+ return NULL;
+}
+
+static void results_callback(void *userdata, struct aki_packet *packet)
+{
+ struct camu_client *client = (struct camu_client *)userdata;
+ s32 ok = aki_packet_read_s32(packet);
+ if (ok != 0) return;
+ s32 id = aki_packet_read_s32(packet);
+ struct camu_search *search = search_from_id(client, id);
+ if (!search) return;
+ s32 page = aki_packet_read_s32(packet);
+ while (search->pages.size <= (u32)page) {
+ struct camu_search_results result;
+ result.page = page;
+ al_array_init(result.unique_ids);
+ al_array_push(search->pages, result);
+ }
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);
+ sho_post_cache_push(&client->cache, &post);
}
+ struct camu_search_results *result = &al_array_at(search->pages, page);
+ str s, unique_id;
size = aki_packet_read_u32(packet);
for (u32 i = 0; i < size; i++) {
- str s, unique_id;
- aki_packet_read_string(packet, &s);
+ aki_packet_read_str(packet, &s);
al_str_clone(&unique_id, &s);
- al_array_push(results->results, unique_id);
+ al_array_push(result->unique_ids, unique_id);
}
- client->callback(client->userdata, CAMU_CLIENT_RESULTS, results);
- aki_packet_free(packet);
+ client->callback(client->userdata, CAMU_CLIENT_RESULTS, result);
+}
+
+void camu_client_resume_search(struct camu_client *client, struct camu_search *search)
+{
+ struct aki_packet *packet = aki_rpc_get_packet(&client->client, TREE_CMD_RESUME_SEARCH);
+ aki_packet_write_s32(packet, search->id);
+ aki_packet_write_s32(packet, search->page);
+ packet->userdata = results_callback;
+ aki_packet_pool_submit(&client->pool, 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);
+ struct aki_packet *packet = aki_rpc_get_packet(&client->client, TREE_CMD_RESUME_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_write_s32(packet, -1);
+ packet->userdata = results_callback;
aki_packet_pool_submit(&client->pool, packet);
}
+
+void camu_client_add(struct camu_client *client, str *unique_id, u32 index)
+{
+ struct aki_packet *packet = aki_rpc_get_packet(&client->client, TREE_CMD_ADD);
+ aki_packet_write_str(packet, unique_id);
+ aki_packet_write_u32(packet, index);
+ packet->userdata = NULL;
+ aki_packet_pool_submit(&client->pool, packet);
+}
+
+void camu_client_close(struct camu_client *client)
+{
+ aki_packet_pool_free(&client->pool);
+ al_str_free(&client->username);
+ al_array_free(client->state.lists);
+}
diff --git a/src/libclient/client.h b/src/libclient/client.h
index 989c839..cdcaffe 100644
--- a/src/libclient/client.h
+++ b/src/libclient/client.h
@@ -3,29 +3,56 @@
#include <aki/rpc2.h>
#include <aki/thread.h>
#include <aki/packet_pool.h>
+#include <sho/post.h>
+#include <sho/post_cache.h>
-#include "search.h"
+#include "../tree/common.h"
enum {
CAMU_CLIENT_CONNECTED = 0,
- // Search client.
+ CAMU_CLIENT_STATUS_UPDATED,
+ CAMU_CLIENT_SEARCH_CREATED,
CAMU_CLIENT_RESULTS
};
+struct camu_list {
+ str name;
+ array(str) entries;
+};
+
+struct camu_search_results {
+ s32 page;
+ array(str) unique_ids;
+};
+
+struct camu_search {
+ s32 id;
+ str provider;
+ str query;
+ s32 page;
+ array(struct camu_search_results) pages;
+};
+
struct camu_client {
- struct aki_event_loop loop;
+ struct aki_event_loop *loop;
struct aki_rpc client;
str username;
+ struct {
+ array(struct camu_list *) lists;
+ array(struct camu_search *) searches;
+ } state;
+ struct sho_post_cache cache;
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,
+bool camu_client_init(struct camu_client *client, struct aki_event_loop *loop,
void (*callback)(void *, u8, void *), void *userdata);
+bool camu_client_login(struct camu_client *client, str *username, str *addr, s32 port);
+void camu_client_create_search(struct camu_client *client, str *provider, str *query);
+void camu_client_resume_search(struct camu_client *client, struct camu_search *search);
void camu_client_more_results(struct camu_client *client, struct camu_search *search);
+void camu_client_add(struct camu_client *client, str *unique_id, u32 index);
void camu_client_close(struct camu_client *client);
diff --git a/src/libclient/meson.build b/src/libclient/meson.build
index 99eabb2..a2f23b0 100644
--- a/src/libclient/meson.build
+++ b/src/libclient/meson.build
@@ -1,3 +1,3 @@
-libclient_src = ['client.c', 'search.c', 'resource_client.c']
+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
index 1069755..77c19c7 100644
--- a/src/libclient/resource_client.c
+++ b/src/libclient/resource_client.c
@@ -28,6 +28,7 @@ static void packet_callback(void *userdata, struct aki_packet_stream *stream,
aki_buffer_init(&buffer);
aki_packet_read_buffer(packet, &buffer);
client->callback(client->userdata, id, &buffer);
+ aki_packet_free(packet);
}
static void packet_sent_callback(void *userdata, struct aki_packet *packet)
@@ -42,41 +43,37 @@ static void signal_callback(void *userdata)
u32 size;
struct camu_resource_request request;
do {
- camu_queue_size(client->queue, size);
- if (!size) break;
- camu_queue_pop(client->queue, request);
+ camu_queue_try_pop(client->queue, size, request);
+ if (size == 0) break;
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_str(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)
+void camu_resource_client_init(struct camu_resource_client *client,
+ struct aki_event_loop *loop, void (*callback)(void *, u16, struct aki_buffer *), void *userdata)
{
+ client->loop = loop;
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);
+}
+
+
+void camu_resource_client_connect(struct camu_resource_client *client, str *addr, s32 port)
+{
+ aki_signal_start(&client->signal, client->loop);
+ aki_packet_stream_connect(&client->client, client->loop, addr, port);
client->connected = false;
while (!client->connected) {
- aki_event_loop_run_once(&client->loop);
+ 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,
diff --git a/src/libclient/resource_client.h b/src/libclient/resource_client.h
index 4cc1bbf..17ac31e 100644
--- a/src/libclient/resource_client.h
+++ b/src/libclient/resource_client.h
@@ -13,7 +13,7 @@ struct camu_resource_request {
};
struct camu_resource_client {
- struct aki_event_loop loop;
+ struct aki_event_loop *loop;
struct aki_packet_stream client;
bool connected;
struct aki_thread thread;
@@ -23,7 +23,8 @@ struct camu_resource_client {
void *userdata;
};
-void camu_resource_client_run(struct camu_resource_client *client,
- void (*callback)(void *, u16, struct aki_buffer *), void *userdata);
+void camu_resource_client_init(struct camu_resource_client *client,
+ struct aki_event_loop *loop, void (*callback)(void *, u16, struct aki_buffer *), void *userdata);
+void camu_resource_client_connect(struct camu_resource_client *client, str *addr, s32 port);
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
deleted file mode 100644
index f74f83d..0000000
--- a/src/libclient/search.c
+++ /dev/null
@@ -1,31 +0,0 @@
-#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
deleted file mode 100644
index b0d8554..0000000
--- a/src/libclient/search.h
+++ /dev/null
@@ -1,28 +0,0 @@
-#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/common.h b/src/libsink/common.h
new file mode 100644
index 0000000..1c701c8
--- /dev/null
+++ b/src/libsink/common.h
@@ -0,0 +1,6 @@
+#pragma once
+
+enum {
+ CAMU_SINK_CMD_BUFFER = 0,
+ CAMU_SINK_CMD_SET
+};
diff --git a/src/libsink/meson.build b/src/libsink/meson.build
index 5cbe36d..f212b0c 100644
--- a/src/libsink/meson.build
+++ b/src/libsink/meson.build
@@ -1,2 +1,2 @@
-libsink_src = ['sink.c']
+libsink_src = ['sink2.c']
libsink = declare_dependency(sources: libsink_src)
diff --git a/src/libsink/sink.c b/src/libsink/sink.c
index bae6c77..3692066 100644
--- a/src/libsink/sink.c
+++ b/src/libsink/sink.c
@@ -32,12 +32,12 @@ enum {
};
#define ENTRY_AUDIO_BUFFER_HELD(entry) \
- (al_atomic_bool_load(&entry->audio.buf.ref, AL_ATOMIC_RELAXED))
+ (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))
+ (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
@@ -160,7 +160,13 @@ static void handle_sink_cmd(struct camu_sink *sink, struct camu_sink_cmd *cmd)
}
case SEEK: {
struct camu_sink_entry *entry = (struct camu_sink_entry *)cmd->opaque;
- bmu_local_seek(&entry->runner, cmd->value.f);
+ switch (entry->type) {
+ case CAMU_SINK_LOCAL:
+ bmu_local_seek(&((struct camu_sink_local *)entry)->runner, cmd->value.f);
+ break;
+ default:
+ break;
+ }
break;
}
case CLOSE: {
@@ -176,9 +182,8 @@ static void queue_signal_callback(void *userdata)
u32 size;
struct camu_sink_cmd cmd;
do {
- camu_queue_size(sink->queue, size);
- if (!size) break;
- camu_queue_pop(sink->queue, cmd);
+ camu_queue_try_pop(sink->queue, size, cmd);
+ if (size == 0) break;
handle_sink_cmd(sink, &cmd);
} while (1);
}
@@ -227,10 +232,22 @@ static void audio_buffer_callback(void *userdata, u8 op)
aki_mutex_unlock(&entry->sink->mutex);
break;
case CAMU_BUFFER_STOP:
- bmu_local_stream_stop((struct bmu_local_stream *)entry->audio.stream);
+ switch (entry->type) {
+ case CAMU_SINK_LOCAL:
+ bmu_local_stream_stop((struct bmu_local_stream *)entry->audio.stream);
+ break;
+ default:
+ break;
+ }
break;
case CAMU_BUFFER_CONTINUE:
- bmu_local_stream_continue((struct bmu_local_stream *)entry->audio.stream);
+ switch (entry->type) {
+ case CAMU_SINK_LOCAL:
+ bmu_local_stream_continue((struct bmu_local_stream *)entry->audio.stream);
+ break;
+ default:
+ break;
+ }
break;
case CAMU_BUFFER_PAUSED:
queue_cmd(entry->sink, (struct camu_sink_cmd){
@@ -245,15 +262,20 @@ static void audio_buffer_callback(void *userdata, u8 op)
entry->audio.state = BUFFER_SET_OR_BUFFERED;
aki_mutex_unlock(&entry->sink->mutex);
if (ret != CAMU_SINK_BUFFERS_SWAPPED) {
+ // TODO: Can cause popping. Should run a timer
+ // and if any action would queue_cmd(AUDIO_START) before the
+ // the timer, don't queue the stop.
queue_cmd(entry->sink, (struct camu_sink_cmd){
.op = STOP,
.value.i = CAMU_SINK_AUDIO,
.opaque = entry->sink
});
+#ifndef CAMU_SINK_NO_VIDEO
queue_cmd(entry->sink, (struct camu_sink_cmd){
.op = SET_BUFFERED,
.value.i = CAMU_SINK_VIDEO
});
+#endif
}
break;
}
@@ -295,10 +317,22 @@ static void video_buffer_callback(void *userdata, u8 op)
aki_mutex_unlock(&entry->sink->mutex);
break;
case CAMU_BUFFER_STOP:
- bmu_local_stream_stop((struct bmu_local_stream *)entry->video.stream);
+ switch (entry->type) {
+ case CAMU_SINK_LOCAL:
+ bmu_local_stream_stop((struct bmu_local_stream *)entry->video.stream);
+ break;
+ default:
+ break;
+ }
break;
case CAMU_BUFFER_CONTINUE:
- bmu_local_stream_continue((struct bmu_local_stream *)entry->video.stream);
+ switch (entry->type) {
+ case CAMU_SINK_LOCAL:
+ bmu_local_stream_continue((struct bmu_local_stream *)entry->video.stream);
+ break;
+ default:
+ break;
+ }
break;
case CAMU_BUFFER_EOF:
queue_cmd(entry->sink, (struct camu_sink_cmd){
@@ -409,14 +443,32 @@ static void client_callback(void *userdata, u8 op, struct bmu_client_stream *str
break;
}
case BIMU_CLIENT_CLOSED: {
- bmu_local_close(&entry->runner);
+ switch (entry->type) {
+ case CAMU_SINK_LOCAL:
+ bmu_local_close(&((struct camu_sink_local *)entry)->runner);
+ break;
+ default:
+ break;
+ }
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);
+ switch (entry->type) {
+ case CAMU_SINK_LOCAL:
+ cch_entry_free(&((struct camu_sink_local *)entry)->entry);
+ break;
+ default:
+ break;
+ }
al_str_free(&entry->unique_id);
- al_free(entry);
+ switch (entry->type) {
+ case CAMU_SINK_LOCAL:
+ al_free((struct camu_sink_local *)entry);
+ break;
+ default:
+ break;
+ }
al_log_debug("sink", "Entry closed.");
break;
}
@@ -533,92 +585,109 @@ 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);
+ switch (entry->type) {
+ case CAMU_SINK_LOCAL:
+ bmu_local_stop(&((struct camu_sink_local *)entry)->runner);
+ break;
+ default:
+ break;
+ }
}
- aki_mutex_unlock(&sink->mutex);
al_array_free(sink->entries);
+ aki_mutex_unlock(&sink->mutex);
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)
+// Local bimu band-aid compat.
+
+static struct camu_sink_local *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 (struct camu_sink_local *)entry;
}
}
return NULL;
}
+static void camu_sink_buffer_internal(struct camu_sink *sink, struct camu_sink_entry *entry, str *unique_id)
+{
+ 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
+
+ al_array_push(sink->entries, entry);
+}
+
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;
+ bool repeat = true;
+ struct camu_sink_local *local = entry_from_unique_id(sink, unique_id);
+ if (!local) {
+ local = al_alloc_object(struct camu_sink_local);
+ local->e.type = CAMU_SINK_LOCAL;
+ repeat = false;
}
- entry->state = ENTRY_LOADED;
- if (!add_entry) {
+ local->e.state = ENTRY_LOADED;
+ if (repeat) {
queue_cmd(sink, (struct camu_sink_cmd){
.op = SEEK,
.value.f = 0,
- .opaque = entry
+ .opaque = &local->e
});
aki_mutex_unlock(&sink->mutex);
return true;
}
- entry->entry = centry;
- if (!bmu_local_init(&entry->runner, centry)) {
- al_free(entry);
+ local->entry = centry;
+ if (!bmu_local_init(&local->runner, centry)) {
+ al_free(local);
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;
+ camu_sink_buffer_internal(sink, &local->e, unique_id);
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);
+ local->runner.callback = client_callback;
+ local->runner.userdata = &local->e;
+ bmu_local_prepare_clients(&local->runner, renderer);
+ bmu_local_run(&local->runner);
aki_mutex_unlock(&sink->mutex);
return true;
}
-static void maybe_cleanup_entries(struct camu_sink *sink)
+static void maybe_cleanup_local_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);
+ bmu_local_stop(&((struct camu_sink_local *)entry)->runner);
al_array_remove_at_iter(sink->entries, i);
}
}
@@ -627,12 +696,12 @@ static void maybe_cleanup_entries(struct camu_sink *sink)
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);
+ struct camu_sink_local *local = entry_from_unique_id(sink, unique_id);
struct camu_sink_entry *previous = sink->current;
- sink->current = entry;
- add_audio_if_set_and_buffered(entry);
+ sink->current = &local->e;
+ add_audio_if_set_and_buffered(&local->e);
#ifndef CAMU_SINK_NO_VIDEO
- add_video_if_set_and_buffered(entry);
+ add_video_if_set_and_buffered(&local->e);
#endif
if (previous) {
if (!camu_clock_is_paused(&previous->clock)) {
@@ -640,24 +709,24 @@ void camu_sink_local_set(struct camu_sink *sink, str *unique_id)
}
remove_entry_buffers(sink, previous);
}
- maybe_cleanup_entries(sink);
+ maybe_cleanup_local_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);
+ struct camu_sink_local *local = entry_from_unique_id(sink, unique_id);
+ sink->current = &local->e;
+ add_audio_if_set_and_buffered(&local->e);
#ifndef CAMU_SINK_NO_VIDEO
- add_video_if_set_and_buffered(entry);
+ add_video_if_set_and_buffered(&local->e);
#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 && entry->state == ENTRY_LOADED) entry->state = ENTRY_DISREGUARDED;
+ struct camu_sink_local *local = entry_from_unique_id(sink, unique_id);
+ if (local && local->e.state == ENTRY_LOADED) local->e.state = ENTRY_DISREGUARDED;
aki_mutex_unlock(&sink->mutex);
}
diff --git a/src/libsink/sink.h b/src/libsink/sink.h
index 92c18d9..efef97b 100644
--- a/src/libsink/sink.h
+++ b/src/libsink/sink.h
@@ -27,20 +27,24 @@ enum {
};
enum {
+ CAMU_SINK_REMOTE = 0,
+ CAMU_SINK_LOCAL
+};
+
+enum {
CAMU_SINK_ADD_BUFFER = 0,
CAMU_SINK_REMOVE_BUFFER,
CAMU_SINK_SWAP_BUFFER,
- CAMU_SINK_SET_BUFFERED,
CAMU_SINK_START,
CAMU_SINK_STOP,
+ CAMU_SINK_ENTRY_BUFFERED,
CAMU_SINK_EXIT
};
struct camu_sink_entry {
+ u8 type;
u8 state;
str unique_id;
- struct cch_entry *entry;
- struct bmu_local runner;
struct camu_clock clock;
struct {
u8 state;
@@ -57,6 +61,12 @@ struct camu_sink_entry {
struct camu_sink *sink;
};
+struct camu_sink_local {
+ struct camu_sink_entry e;
+ struct cch_entry *entry;
+ struct bmu_local runner;
+};
+
struct camu_sink_cmd {
u8 op;
union { s64 i; f64 f; } value;
diff --git a/src/libsink/sink2.c b/src/libsink/sink2.c
new file mode 100644
index 0000000..e9b51f2
--- /dev/null
+++ b/src/libsink/sink2.c
@@ -0,0 +1,688 @@
+#include <al/log.h>
+
+#include "../tree/common.h"
+
+#include "sink2.h"
+
+enum {
+ SINK_EMPTY = 0,
+ SINK_PAUSED,
+ SINK_PLAYING
+};
+
+enum {
+ ENTRY_LOADED = 0,
+ ENTRY_DISREGUARDED
+};
+
+enum {
+ BUFFER_INIT = 0,
+ BUFFER_QUEUED,
+ BUFFER_CONFIGURED,
+ BUFFER_SET_OR_BUFFERED,
+ BUFFER_ADDED,
+};
+
+enum {
+ // TODO: Add and remove buffer can be done from
+ // any thread. Does this always need to be the case?
+ // Is it beneficial for this to be the case?
+ ADD_BUFFER = 0,
+ REMOVE_BUFFER,
+ START,
+ STOP,
+ TOGGLE_PAUSE,
+ SEEK,
+ ENTRY_BUFFERED, // Currently set entry is buffered.
+ CLOSE,
+ // Internal.
+ CORK,
+ UNCORK
+};
+
+#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
+
+#define ENTRY_VIDEO_EMPTY(entry) \
+ (entry->video.state == BUFFER_INIT || entry->video.state == BUFFER_QUEUED)
+#define ENTRY_AUDIO_EMPTY(entry) \
+ (entry->audio.state == BUFFER_INIT || entry->audio.state == BUFFER_QUEUED)
+
+static void handle_sink_cmd(struct camu_sink *sink, struct camu_sink_cmd *cmd)
+{
+ switch (cmd->op) {
+ case ADD_BUFFER: {
+ struct camu_sink_entry *entry = (struct camu_sink_entry *)cmd->opaque;
+ switch (cmd->value.i) {
+ case CAMU_SINK_AUDIO:
+ sink->callback(sink->userdata, CAMU_SINK_ADD_BUFFER, CAMU_SINK_AUDIO, &entry->audio.buf);
+ break;
+#ifndef CAMU_SINK_NO_VIDEO
+ case CAMU_SINK_VIDEO:
+ sink->callback(sink->userdata, CAMU_SINK_ADD_BUFFER, CAMU_SINK_VIDEO, &entry->video.buf);
+ break;
+#endif
+ }
+ break;
+ }
+ case REMOVE_BUFFER: {
+ struct camu_sink_entry *entry = (struct camu_sink_entry *)cmd->opaque;
+ switch (cmd->value.i) {
+ case CAMU_SINK_AUDIO:
+ sink->callback(sink->userdata, CAMU_SINK_REMOVE_BUFFER, CAMU_SINK_AUDIO, &entry->audio.buf);
+ break;
+#ifndef CAMU_SINK_NO_VIDEO
+ case CAMU_SINK_VIDEO:
+ sink->callback(sink->userdata, CAMU_SINK_REMOVE_BUFFER, CAMU_SINK_VIDEO, &entry->video.buf);
+ break;
+#endif
+ }
+ break;
+ }
+ case START: {
+ 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: {
+ 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;
+ (void)entry;
+ break;
+ }
+ case ENTRY_BUFFERED: {
+ switch (cmd->value.i) {
+ case CAMU_SINK_AUDIO:
+ sink->callback(sink->userdata, CAMU_SINK_SET_BUFFERED, CAMU_SINK_AUDIO, NULL);
+ break;
+#ifndef CAMU_SINK_NO_VIDEO
+ case CAMU_SINK_VIDEO:
+ sink->callback(sink->userdata, CAMU_SINK_SET_BUFFERED, CAMU_SINK_VIDEO, NULL);
+ break;
+#endif
+ }
+ break;
+ }
+ case CLOSE: {
+ sink->callback(sink->userdata, CAMU_SINK_EXIT, 0, NULL);
+ return;
+ }
+ case CORK: {
+ struct camu_sink_entry *entry = (struct camu_sink_entry *)cmd->opaque;
+ bmu_client_cork(&entry->client, true);
+ break;
+ }
+ case UNCORK: {
+ struct camu_sink_entry *entry = (struct camu_sink_entry *)cmd->opaque;
+ bmu_client_cork(&entry->client, false);
+ 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_try_pop(sink->queue, size, cmd);
+ if (size == 0) break;
+ 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 = ENTRY_BUFFERED,
+ .value.i = CAMU_SINK_AUDIO
+ });
+ queue_cmd(entry->sink, (struct camu_sink_cmd){
+ .op = START,
+ .value.i = CAMU_SINK_AUDIO
+ });
+ if (ENTRY_VIDEO_EMPTY(entry) || entry->video.state == BUFFER_ADDED) {
+ camu_clock_resume(&entry->clock);
+ }
+ 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_CORK:
+ queue_cmd(entry->sink, (struct camu_sink_cmd){
+ .op = CORK,
+ .opaque = entry
+ });
+ break;
+ case CAMU_BUFFER_UNCORK:
+ queue_cmd(entry->sink, (struct camu_sink_cmd){
+ .op = UNCORK,
+ .opaque = entry
+ });
+ break;
+ case CAMU_BUFFER_PAUSED:
+ queue_cmd(entry->sink, (struct camu_sink_cmd){
+ .op = STOP,
+ .value.i = CAMU_SINK_AUDIO
+ });
+ 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) {
+ // TODO: Can cause popping. Should run a timer
+ // and if any action would queue_cmd(AUDIO_START) before the
+ // the timer, don't queue the stop.
+ queue_cmd(entry->sink, (struct camu_sink_cmd){
+ .op = STOP,
+ .value.i = CAMU_SINK_AUDIO
+ });
+#ifndef CAMU_SINK_NO_VIDEO
+ queue_cmd(entry->sink, (struct camu_sink_cmd){
+ .op = ENTRY_BUFFERED,
+ .value.i = CAMU_SINK_VIDEO
+ });
+#endif
+ }
+ 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 = ENTRY_BUFFERED,
+ .value.i = CAMU_SINK_VIDEO
+ });
+ queue_cmd(entry->sink, (struct camu_sink_cmd){
+ .op = START,
+ .value.i = CAMU_SINK_VIDEO
+ });
+ if (ENTRY_AUDIO_EMPTY(entry) || 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_CORK:
+ queue_cmd(entry->sink, (struct camu_sink_cmd){
+ .op = CORK,
+ .opaque = entry
+ });
+ break;
+ case CAMU_BUFFER_UNCORK:
+ queue_cmd(entry->sink, (struct camu_sink_cmd){
+ .op = UNCORK,
+ .opaque = entry
+ });
+ break;
+ case CAMU_BUFFER_EOF:
+ queue_cmd(entry->sink, (struct camu_sink_cmd){
+ .op = STOP,
+ .value.i = CAMU_SINK_VIDEO
+ });
+ 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) {
+ // A lot of logic here assumes that no data will be sent
+ // until all active streams are configured. This is extremely important,
+ // if it does not hold true many confusing errors will arise.
+ case BIMU_CLIENT_CONFIGURE: {
+ switch (stream->type) {
+ case BIMU_STREAM_AUDIO:
+ entry->audio.stream = stream;
+ camu_audio_buffer_configure(&entry->audio.buf, &stream->stream);
+ aki_mutex_lock(&entry->sink->mutex);
+ if (entry->audio.state == BUFFER_QUEUED) {
+ entry->audio.state = BUFFER_SET_OR_BUFFERED;
+ } else {
+ entry->audio.state = BUFFER_CONFIGURED;
+ }
+ aki_mutex_unlock(&entry->sink->mutex);
+ break;
+#ifndef CAMU_SINK_NO_VIDEO
+ case BIMU_STREAM_VIDEO:
+ entry->video.stream = stream;
+ camu_video_buffer_configure(&entry->video.buf, &stream->stream);
+ aki_mutex_lock(&entry->sink->mutex);
+ if (entry->video.state == BUFFER_QUEUED) {
+ entry->video.state = BUFFER_SET_OR_BUFFERED;
+ } else {
+ entry->video.state = BUFFER_CONFIGURED;
+ }
+ aki_mutex_unlock(&entry->sink->mutex);
+ 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;
+ }
+ break;
+ }
+ case BIMU_CLIENT_SEEK: {
+ // Must ensure no more data from the previous
+ // stream position is sent during or after this call to seek.
+ f64 seek_pos = *(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_pos);
+ 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: {
+ // close/free client.
+ camu_audio_buffer_free(&entry->audio.buf);
+#ifndef CAMU_SINK_NO_VIDEO
+ camu_video_buffer_free(&entry->video.buf);
+#endif
+ 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;
+}
+
+static struct camu_sink_entry *entry_from_node_id(struct camu_sink *sink, u16 node_id)
+{
+ struct camu_sink_entry *entry;
+ al_array_foreach(sink->entries, i, entry) {
+ if (entry->client.node_id == node_id) return entry;
+ }
+ return NULL;
+}
+
+static bool buffer_command_callback(void *userdata, struct aki_rpc_connection *conn,
+ struct aki_packet *packet, struct aki_packet *rpacket)
+{
+ struct camu_sink *sink = (struct camu_sink *)userdata;
+ (void)conn;
+ (void)rpacket;
+
+ str addr;
+ aki_packet_read_str(packet, &addr);
+ s32 port = aki_packet_read_s32(packet);
+ u16 node_id = aki_packet_read_u16(packet);
+
+ struct camu_sink_entry *entry = al_alloc_object(struct camu_sink_entry);
+ entry->sink = sink;
+ al_array_push(sink->entries, entry);
+
+ entry->state = ENTRY_LOADED;
+ 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->client.callback = client_callback;
+ entry->client.userdata = entry;
+ bmu_client_connect(&entry->client, sink->loop, &addr, port, node_id);
+
+ aki_packet_free(packet);
+ return false;
+}
+
+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;
+ }
+}
+
+static bool set_command_callback(void *userdata, struct aki_rpc_connection *conn,
+ struct aki_packet *packet, struct aki_packet *rpacket)
+{
+ struct camu_sink *sink = (struct camu_sink *)userdata;
+ (void)conn;
+ (void)rpacket;
+
+ u16 node_id = aki_packet_read_u16(packet);
+ aki_mutex_lock(&sink->mutex);
+ struct camu_sink_entry *entry = entry_from_node_id(sink, node_id);
+ struct camu_sink_entry *previous = sink->current;
+ sink->current = entry;
+ if (entry->audio.state == BUFFER_INIT) {
+ entry->audio.state = BUFFER_QUEUED;
+ } else {
+ add_audio_if_set_and_buffered(entry);
+ }
+#ifndef CAMU_SINK_NO_VIDEO
+ if (entry->video.state == BUFFER_INIT) {
+ entry->video.state = BUFFER_QUEUED;
+ } else {
+ add_video_if_set_and_buffered(entry);
+ }
+#endif
+ if (previous) {
+ camu_clock_pause(&previous->clock);
+ remove_entry_buffers(sink, previous);
+ }
+ aki_mutex_unlock(&sink->mutex);
+
+ aki_packet_free(packet);
+ return false;
+}
+
+static struct aki_rpc_command commands[] = {
+ { .op = CAMU_SINK_CMD_BUFFER, .callback = buffer_command_callback, .userdata = NULL },
+ { .op = CAMU_SINK_CMD_SET, .callback = set_command_callback, .userdata = NULL }
+};
+
+static void connection_callback(void *userdata, struct aki_rpc_connection *conn)
+{
+ struct camu_sink *sink = (struct camu_sink *)userdata;
+ sink->conn = conn;
+ struct aki_packet *packet = aki_rpc_get_packet(&sink->client, TREE_CMD_IDENTIFY);
+ aki_packet_write_u8(packet, TREE_SINK);
+ aki_rpc_connection_command(sink->conn, packet, NULL, NULL);
+}
+
+static void connection_closed_callback(void *userdata, struct aki_rpc_connection *conn)
+{
+ (void)userdata;
+ (void)conn;
+}
+
+bool camu_sink_connect(struct camu_sink *sink, str *addr, s32 port)
+{
+ aki_rpc_init(&sink->client, AKI_SOCKET_TCP, connection_callback,
+ connection_closed_callback, sink);
+ for (u32 i = 0; i < AL_ARRAY_SIZE(commands); i++) {
+ commands[i].userdata = sink;
+ al_assert(sink->callback);
+ aki_rpc_add_command(&sink->client, &commands[i]);
+ }
+ return aki_rpc_connect(&sink->client, sink->loop, addr, port);
+}
+
+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);
+}
+
+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
+ });
+ 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) {
+ }
+ */
+ al_array_free(sink->entries);
+ aki_mutex_unlock(&sink->mutex);
+ aki_signal_stop(&sink->signal);
+ camu_queue_free(sink->queue);
+ aki_mutex_destroy(&sink->mutex);
+}
diff --git a/src/libsink/sink2.h b/src/libsink/sink2.h
new file mode 100644
index 0000000..95c280f
--- /dev/null
+++ b/src/libsink/sink2.h
@@ -0,0 +1,106 @@
+#pragma once
+
+//#define CAMU_SINK_NO_VIDEO
+
+#include <al/str.h>
+#include <al/array.h>
+#include <aki/rpc2.h>
+#include <aki/signal.h>
+#include <aki/timer.h>
+
+#include "../util/queue.h"
+
+#include "../buffer/clock.h"
+#include "../buffer/audio.h"
+#ifndef CAMU_SINK_NO_VIDEO
+#include "../buffer/video.h"
+#endif
+
+#include "../bimu/client.h"
+
+#include "common.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_SET_BUFFERED,
+ CAMU_SINK_START,
+ CAMU_SINK_STOP,
+ CAMU_SINK_EXIT
+};
+
+struct camu_sink_entry {
+ u8 state;
+ struct bmu_client client;
+ 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_rpc client;
+ struct aki_rpc_connection *conn;
+ 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);
diff --git a/src/mixer/audio_miniaudio.c b/src/mixer/audio_miniaudio.c
index 1388fc4..38cb91d 100644
--- a/src/mixer/audio_miniaudio.c
+++ b/src/mixer/audio_miniaudio.c
@@ -179,12 +179,12 @@ struct camu_audio_miniaudio audio_plugin_miniaudio = {
.stop = audio_miniaudio_stop,
.free = audio_miniaudio_free
},
- .log = { },
- .context_config = { },
- .context = { },
+ .log = { 0 },
+ .context_config = { 0 },
+ .context = { 0 },
.infos = NULL,
.info_count = 0,
.selected_device = NULL,
- .config = { },
- .device = { }
+ .config = { 0 },
+ .device = { 0 }
};
diff --git a/src/mixer/meson.build b/src/mixer/meson.build
index 4053210..53389eb 100644
--- a/src/mixer/meson.build
+++ b/src/mixer/meson.build
@@ -9,4 +9,17 @@ if host_machine.system() != 'windows'
mixer_deps += [libpulse]
endif
-mixer = declare_dependency(sources: mixer_src, dependencies: mixer_deps)
+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'
+]
+
+mixer = declare_dependency(sources: mixer_src, dependencies: mixer_deps,
+ compile_args: miniaudio_args)
diff --git a/src/mixer/mixer.c b/src/mixer/mixer.c
index 8cfc2cc..646f056 100644
--- a/src/mixer/mixer.c
+++ b/src/mixer/mixer.c
@@ -60,6 +60,7 @@ void camu_mixer_pick_format(struct camu_mixer *mixer, struct camu_lav_resample_f
f64 camu_mixer_get_latency(struct camu_mixer *mixer)
{
+ if (!mixer->audio) return 0;
return mixer->audio->get_latency(mixer->audio) / 1000000.0;
}
diff --git a/src/render/meson.build b/src/render/meson.build
index cc11eea..e9c093a 100644
--- a/src/render/meson.build
+++ b/src/render/meson.build
@@ -23,7 +23,7 @@ libplacebo = dependency('libplacebo',
'd3d11=disabled',
'gl-proc-addr=disabled',
'vk-proc-addr=disabled',
- 'lcms=disabled',
+ 'lcms=enabled',
'dovi=disabled',
'libdovi=disabled'
], required: false, allow_fallback: true)
diff --git a/src/render/queue_libplacebo.c b/src/render/queue_libplacebo.c
index ee64920..327397f 100644
--- a/src/render/queue_libplacebo.c
+++ b/src/render/queue_libplacebo.c
@@ -1,5 +1,9 @@
#include <al/lib.h>
#include <al/log.h>
+#ifdef HAVE_FFMPEG
+#define PL_LIBAV_IMPLEMENTATION 1
+#include <libplacebo/utils/libav.h>
+#endif
#include "../buffer/video.h"
diff --git a/src/render/queue_libplacebo.h b/src/render/queue_libplacebo.h
index 7da99ca..829ef87 100644
--- a/src/render/queue_libplacebo.h
+++ b/src/render/queue_libplacebo.h
@@ -4,9 +4,6 @@
#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;
diff --git a/src/render/renderer.h b/src/render/renderer.h
index 61965ed..64dc691 100644
--- a/src/render/renderer.h
+++ b/src/render/renderer.h
@@ -18,7 +18,6 @@ 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 *),
diff --git a/src/render/renderer_libplacebo.c b/src/render/renderer_libplacebo.c
index 019a5ce..148574b 100644
--- a/src/render/renderer_libplacebo.c
+++ b/src/render/renderer_libplacebo.c
@@ -1,6 +1,9 @@
#include <al/lib.h>
#include <al/log.h>
#include <aki/file.h>
+#ifdef HAVE_FFMPEG
+#include <libplacebo/utils/libav.h>
+#endif
#include "../screen/screen.h"
#include "../util/color_palette.h"
@@ -16,8 +19,14 @@ static void renderer_lp_resize(struct camu_renderer *renderer, s32 *width, s32 *
if (lr->swapchain) pl_swapchain_resize(lr->swapchain, width, height);
}
+static void log_callback(void *userdata, enum pl_log_level level, const char *msg)
+{
+ (void)userdata;
+ (void)level;
+ al_log_debug("render_libplacebo", msg);
+}
+
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 *),
@@ -38,29 +47,32 @@ static bool renderer_lp_create_renderer(struct camu_renderer *renderer, s32 *wid
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,
+ .log_cb = log_callback,
+ .log_priv = NULL,
+ .log_level = PL_LOG_INFO
));
#if defined CAMU_RENDERER_VULKAN
VkResult err;
u32 num;
+ const char *const *extensions = vk_get_extensions(&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),
+ .extensions = extensions,
.num_extensions = num,
));
if (!lr->vk_inst) {
al_log_error("render_libplacebo", "Failed to create vulkan instance.");
+ goto err;
}
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.");
+ goto err;
}
lr->vk = pl_vulkan_create(lr->logger, pl_vulkan_params(
@@ -72,6 +84,7 @@ static bool renderer_lp_create_renderer(struct camu_renderer *renderer, s32 *wid
if (!lr->vk) {
al_log_error("render_libplacebo", "Failed to create vulkan device.");
+ goto err;
}
lr->swapchain = pl_vulkan_create_swapchain(lr->vk, pl_vulkan_swapchain_params(
@@ -81,6 +94,7 @@ static bool renderer_lp_create_renderer(struct camu_renderer *renderer, s32 *wid
if (!lr->swapchain) {
al_log_error("render_libplacebo", "Failed to create vulkan swapchain.");
+ goto err;
}
lr->gpu = lr->vk->gpu;
@@ -98,6 +112,7 @@ static bool renderer_lp_create_renderer(struct camu_renderer *renderer, s32 *wid
if (!lr->gl) {
al_log_error("render_libplacebo", "Failed to create GL device.");
+ goto err;
}
lr->swapchain = pl_opengl_create_swapchain(lr->gl, pl_opengl_swapchain_params(
@@ -107,6 +122,7 @@ static bool renderer_lp_create_renderer(struct camu_renderer *renderer, s32 *wid
if (!lr->swapchain) {
al_log_error("render_libplacebo", "Failed to create GL swapchain.");
+ goto err;
}
lr->gpu = lr->gl->gpu;
@@ -114,32 +130,24 @@ static bool renderer_lp_create_renderer(struct camu_renderer *renderer, s32 *wid
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);
- }
+ lr->r.resize(&lr->r, width, height);
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;
+ //lr->params.upscaler = &pl_filter_bicubic;
+ //lr->params.upscaler = &pl_filter_nearest;
+
// Clear manually so we can draw multiple images per frame.
lr->params.skip_target_clearing = true;
+ // Breaks zoom/pan.
lr->params.skip_caching_single_frame = true;
+ // Prioritize a more consistant image (should be configurable).
+ lr->params.correct_subpixel_offsets = true;
#if 0
struct aki_file file;
- if (aki_file_open(&file, al_str_c(""))) {
+ if (aki_file_open(&file, al_str_c(""), 0)) {
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);
@@ -157,6 +165,9 @@ static bool renderer_lp_create_renderer(struct camu_renderer *renderer, s32 *wid
#endif
return true;
+err:
+ lr->r.free((struct camu_renderer **)&lr);
+ return false;
}
static struct camu_frame_queue *renderer_lp_create_queue(struct camu_renderer *renderer)
@@ -166,9 +177,9 @@ static struct camu_frame_queue *renderer_lp_create_queue(struct camu_renderer *r
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.interpolation_threshold = 0.01f;
lq->params.radius = pl_frame_mix_radius(&lr->params);
- lq->params.timeout = 0; // vsync
+ lq->params.timeout = 0uL; // vsync
return queue;
}
@@ -211,7 +222,7 @@ static void renderer_lp_render(struct camu_renderer *renderer, struct camu_scree
pl_swapchain_swap_buffers(lr->swapchain);
}
-static void renderer_lp_free(struct camu_renderer **renderer)
+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);
diff --git a/src/render/renderer_libplacebo.h b/src/render/renderer_libplacebo.h
index 661faff..80f4688 100644
--- a/src/render/renderer_libplacebo.h
+++ b/src/render/renderer_libplacebo.h
@@ -1,7 +1,6 @@
#pragma once
#include <libplacebo/common.h>
-#include <libplacebo/opengl.h>
#include <libplacebo/renderer.h>
#include <libplacebo/shaders/lut.h>
#include <libplacebo/colorspace.h>
@@ -10,13 +9,10 @@
#if defined CAMU_RENDERER_VULKAN
#include <libplacebo/vulkan.h>
+#elif defined CAMU_RENDERER_OPENGL
+#include <libplacebo/opengl.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
@@ -31,8 +27,6 @@ struct camu_renderer_lp {
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
index 7cbbbb9..e4ae239 100644
--- a/src/render/renderer_tiger.c
+++ b/src/render/renderer_tiger.c
@@ -96,7 +96,6 @@ static void renderer_tiger_resize(struct camu_renderer *renderer, s32 *width, s3
}
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 *),
@@ -132,14 +131,6 @@ static bool renderer_tiger_create_renderer(struct camu_renderer *renderer, s32 *
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);
diff --git a/src/screen/meson.build b/src/screen/meson.build
index cd78988..d0bbd50 100644
--- a/src/screen/meson.build
+++ b/src/screen/meson.build
@@ -1,4 +1,10 @@
screen_src = ['screen.c', 'view.c']
-sekihi = subproject('sekihi').get_variable('sekihi')
-screen = declare_dependency(sources: screen_src,
- dependencies: [sekihi])
+sekihi = subproject('sekihi',
+ default_options: [
+ 'api=opengl',
+ 'window=wayland',
+ 'poll=inline',
+ 'event-buffer=false',
+ 'pause=true'
+ ]).get_variable('sekihi')
+screen = declare_dependency(sources: screen_src, dependencies: [sekihi])
diff --git a/src/screen/screen.c b/src/screen/screen.c
index 82c7b12..dd4bdc6 100644
--- a/src/screen/screen.c
+++ b/src/screen/screen.c
@@ -3,18 +3,6 @@
#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;
@@ -51,31 +39,30 @@ static void resize_callback(void *userdata, s32 width, s32 height)
}
}
-static void pointer_pos_callback(void *userdata, f64 x, f64 y)
+static bool 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);
+ bool queue_refresh = false;
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 dx = x - scr->last_mouse_x;
+ f64 dy = y - scr->last_mouse_y;
+ if (camu_view_pan_simple(view, scr->width, scr->height, dx, dy)) {
+ view->mode = CAMU_VIEW_DETACHED;
+ queue_refresh = true;
}
- 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;
+ return queue_refresh;
}
-static void mouse_button_callback(void *userdata, u8 state, u8 button)
+static bool mouse_button_callback(void *userdata, u8 state, u8 button)
{
struct camu_screen *scr = (struct camu_screen *)userdata;
switch (button) {
@@ -106,55 +93,30 @@ static void mouse_button_callback(void *userdata, u8 state, u8 button)
default:
break;
}
+ return false;
}
-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;
- }
-}
+#if defined SEKIHI_WINDOW_WAYLAND
+#define SCROLL_MULTIPLIER 87.5
+#else
+#define SCROLL_MULTIPLIER 5.0
+#endif
-static void scroll_callback(void *userdata, f64 y)
+static bool 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;
+ if (view && scr->flags & CAMU_SCREEN_ZOOM_PAN_SIMPLE) {
+ y = -y / SCROLL_MULTIPLIER;
+ if (camu_view_zoom_simple(view, scr->width, scr->height, scr->last_mouse_x, scr->last_mouse_y, y)) {
+ view->mode = CAMU_VIEW_DETACHED;
+ return true;
+ }
}
+ return false;
}
-static void key_callback(void *userdata, u8 state, u8 button)
+static bool key_callback(void *userdata, u8 state, u8 button)
{
struct camu_screen *scr = (struct camu_screen *)userdata;
switch (state) {
@@ -183,9 +145,11 @@ static void key_callback(void *userdata, u8 state, u8 button)
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);
+ if (view) {
+ view->mode = CAMU_DEFAULT_VIEW;
+ camu_view_calculate(view, scr->width, scr->height);
+ return true;
+ }
break;
}
case 0x1d:
@@ -204,6 +168,7 @@ static void key_callback(void *userdata, u8 state, u8 button)
default:
break;
}
+ return false;
}
static void key_immediate_callback(void *userdata, u8 state, u8 button)
@@ -251,7 +216,8 @@ bool camu_screen_init(struct camu_screen *scr)
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)) {
+ s32 flags = SEKIHI_WINDOW_VSYNC;
+ if (!scr->window->create_window(scr->window, CAMU_SCREEN_WIDTH, CAMU_SCREEN_HEIGHT, NULL, name, flags)) {
return false;
}
scr->width = scr->window->width;
@@ -263,14 +229,13 @@ bool camu_screen_create_renderer(struct camu_screen *scr, struct camu_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_loader_load,
scr->window->gl_make_current,
scr->window->gl_release_current,
scr->window->gl_swap_buffers,
@@ -367,28 +332,30 @@ void camu_screen_run_queue(struct camu_screen *scr)
}
#endif
-void camu_screen_set_state(struct camu_screen *scr, u8 state)
+void camu_screen_set_state(struct camu_screen *scr, s32 state)
{
al_atomic_s32_store(&scr->state, state, AL_ATOMIC_RELAXED);
}
-bool camu_screen_tick(struct camu_screen *scr)
+bool camu_screen_poll(struct camu_screen *scr, bool block)
{
-#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);
+ scr->window->poll(scr->window, block);
+ scr->window->process_events(scr->window);
+#if defined SEKIHI_EVENT_BUFFER
+ skh_window_read_events(scr->window);
#endif
+ return scr->window->should_refresh(scr->window);
+}
+
+bool camu_screen_tick(struct camu_screen *scr)
+{
+ bool paused = al_atomic_s32_load(&scr->state, AL_ATOMIC_RELAXED) == CAMU_SCREEN_PAUSED;
+ return camu_screen_poll(scr, paused) || !paused;
}
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)
@@ -399,4 +366,5 @@ void camu_screen_close(struct camu_screen *scr)
aki_mutex_destroy(&scr->mutex);
#endif
al_array_free(scr->videos);
+ scr->window->free(&scr->window);
}
diff --git a/src/screen/screen.h b/src/screen/screen.h
index 798fc8e..0ca5779 100644
--- a/src/screen/screen.h
+++ b/src/screen/screen.h
@@ -19,9 +19,9 @@
#define CAMU_SCREEN_HEIGHT 700
enum {
- CAMU_SCREEN_ZOOM_PAN_SIMPLE = 1,
+ CAMU_SCREEN_MODIFIER = 1,
CAMU_SCREEN_DRAGGING = 1 << 1,
- CAMU_SCREEN_MODIFIER = 1 << 2,
+ CAMU_SCREEN_ZOOM_PAN_SIMPLE = 1 << 2,
};
enum {
@@ -72,7 +72,8 @@ void camu_screen_remove_buffer(struct camu_screen *scr, struct camu_video_buffer
#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);
+void camu_screen_set_state(struct camu_screen *scr, s32 state);
+bool camu_screen_poll(struct camu_screen *scr, bool block);
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
index cf23ed4..4cf6007 100644
--- a/src/screen/view.c
+++ b/src/screen/view.c
@@ -1,5 +1,15 @@
#include "view.h"
+#define ZOOM_MAX 100.0
+#define ZOOM_MIN 0.02
+
+#define PAN_BOUNDS 35.0
+
+#define X_IN_BOUNDS(view, w, x) \
+ ((x + PAN_BOUNDS <= w) && (PAN_BOUNDS - x <= view->width * view->zoom))
+#define Y_IN_BOUNDS(view, h, y) \
+ ((y + PAN_BOUNDS <= h) && (PAN_BOUNDS - y <= view->height * view->zoom))
+
void camu_view_calculate(struct camu_view *view, s32 window_width, s32 window_height)
{
f64 ratio = window_width / (f64)window_height;
@@ -26,3 +36,48 @@ void camu_view_calculate(struct camu_view *view, s32 window_width, s32 window_he
view->x_offset = ((window_width - (view->width * view->zoom)) / 2.0);
view->y_offset = ((window_height - (view->height * view->zoom)) / 2.0);
}
+
+bool camu_view_pan_simple(struct camu_view *view, s32 window_width, s32 window_height, f64 dx, f64 dy)
+{
+ f64 nx = view->x_offset + dx;
+ f64 ny = view->y_offset + dy;
+ bool view_changed = false;
+ if (X_IN_BOUNDS(view, window_width, nx)) {
+ view->x_offset = nx;
+ view_changed = true;
+ }
+ if (Y_IN_BOUNDS(view, window_height, ny)) {
+ view->y_offset = ny;
+ view_changed = true;
+ }
+ return view_changed;
+}
+
+bool camu_view_zoom_simple(struct camu_view *view, s32 window_width, s32 window_height, f64 x, f64 y, f64 v)
+{
+ x -= view->x_offset;
+ y -= view->y_offset;
+ x /= view->zoom;
+ y /= view->zoom;
+
+ f64 prev_width = window_width * view->zoom;
+ f64 prev_height = window_height * view->zoom;
+
+ f64 x_ratio = x / (f64)window_width;
+ f64 y_ratio = y / (f64)window_height;
+
+ f64 zoom = view->zoom + (v * view->zoom);
+ if (zoom > ZOOM_MAX || zoom < ZOOM_MIN) return false;
+
+ x = view->x_offset - x_ratio * ((window_width * zoom) - prev_width);
+ y = view->y_offset - y_ratio * ((window_height * zoom) - prev_height);
+ if (!(X_IN_BOUNDS(view, window_width, x) && Y_IN_BOUNDS(view, window_height, y))) {
+ return false;
+ }
+
+ view->x_offset = x;
+ view->y_offset = y;
+ view->zoom = zoom;
+
+ return true;
+}
diff --git a/src/screen/view.h b/src/screen/view.h
index 0546062..e9bec81 100644
--- a/src/screen/view.h
+++ b/src/screen/view.h
@@ -9,6 +9,8 @@ enum {
CAMU_VIEW_DETACHED
};
+#define CAMU_DEFAULT_VIEW CAMU_VIEW_AUTOFIT
+
struct camu_view {
u8 mode;
s32 width;
@@ -21,3 +23,5 @@ struct camu_view {
};
void camu_view_calculate(struct camu_view *view, s32 window_width, s32 window_height);
+bool camu_view_pan_simple(struct camu_view *view, s32 window_width, s32 window_height, f64 dx, f64 dy);
+bool camu_view_zoom_simple(struct camu_view *view, s32 window_width, s32 window_height, f64 x, f64 y, f64 v);
diff --git a/src/shoki/cpy/lib.pxd b/src/shoki/cpy/lib.pxd
deleted file mode 100644
index 5749141..0000000
--- a/src/shoki/cpy/lib.pxd
+++ /dev/null
@@ -1,13 +0,0 @@
-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
deleted file mode 100644
index 0b0a97e..0000000
--- a/src/shoki/cpy/post.pxd
+++ /dev/null
@@ -1,58 +0,0 @@
-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
deleted file mode 100644
index 1bebb55..0000000
--- a/src/shoki/cpy/result.pxd
+++ /dev/null
@@ -1,8 +0,0 @@
-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
deleted file mode 100644
index 1b491f1..0000000
--- a/src/shoki/cpy/setup.py
+++ /dev/null
@@ -1,6 +0,0 @@
-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
deleted file mode 100644
index 6d1dd56..0000000
--- a/src/shoki/cpy/shoki.pyx
+++ /dev/null
@@ -1,132 +0,0 @@
-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
deleted file mode 100644
index a12ced4..0000000
--- a/src/shoki/cpy/str.pxd
+++ /dev/null
@@ -1,11 +0,0 @@
-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
deleted file mode 100644
index fec35a5..0000000
--- a/src/shoki/cpy/types.pxd
+++ /dev/null
@@ -1,11 +0,0 @@
-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
deleted file mode 100644
index 557d497..0000000
--- a/src/shoki/meson.build
+++ /dev/null
@@ -1,14 +0,0 @@
-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
deleted file mode 100644
index e69de29..0000000
--- a/src/shoki/py/__init__.py
+++ /dev/null
diff --git a/src/shoki/py/client.py b/src/shoki/py/client.py
deleted file mode 100644
index 3986060..0000000
--- a/src/shoki/py/client.py
+++ /dev/null
@@ -1,51 +0,0 @@
-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
deleted file mode 100644
index 237d658..0000000
--- a/src/shoki/py/config.def.py
+++ /dev/null
@@ -1,19 +0,0 @@
-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
deleted file mode 100644
index c36c54e..0000000
--- a/src/shoki/py/log.py
+++ /dev/null
@@ -1,30 +0,0 @@
-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
deleted file mode 100644
index d489298..0000000
--- a/src/shoki/py/post.py
+++ /dev/null
@@ -1,70 +0,0 @@
-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
deleted file mode 100644
index a719d01..0000000
--- a/src/shoki/py/provider.py
+++ /dev/null
@@ -1,39 +0,0 @@
-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
deleted file mode 100644
index e69de29..0000000
--- a/src/shoki/py/providers/__init__.py
+++ /dev/null
diff --git a/src/shoki/py/providers/fanbox.py b/src/shoki/py/providers/fanbox.py
deleted file mode 100644
index 7739be3..0000000
--- a/src/shoki/py/providers/fanbox.py
+++ /dev/null
@@ -1,130 +0,0 @@
-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
deleted file mode 100644
index 0146ba3..0000000
--- a/src/shoki/py/providers/instagram.py
+++ /dev/null
@@ -1,96 +0,0 @@
-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
deleted file mode 100644
index 9276d61..0000000
--- a/src/shoki/py/providers/pixiv.py
+++ /dev/null
@@ -1,113 +0,0 @@
-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
deleted file mode 100644
index b536f28..0000000
--- a/src/shoki/py/providers/searx.py
+++ /dev/null
@@ -1,93 +0,0 @@
-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
deleted file mode 100644
index 4770dda..0000000
--- a/src/shoki/py/providers/twitter.py
+++ /dev/null
@@ -1,424 +0,0 @@
-import json
-import config
-import http.cookiejar
-from datetime import timezone
-
-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 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
deleted file mode 100644
index 3cbf0c8..0000000
--- a/src/shoki/py/providers/youtube.py
+++ /dev/null
@@ -1,122 +0,0 @@
-import log
-from provider import Provider
-from search import Search
-from post import Post, PostType, Image, Video
-from query_parser import QueryParser
-from yt_dlp import YoutubeDL
-
-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 = YoutubeDL(ydl_opts)
-
-def get_playback_url(data, video=True):
- if 'entries' in data:
- if len(data['entries']) == 0:
- return None
- data = data['entries'][0]
-
- if 'formats' not in data:
- if 'url' in data:
- return data['url']
- return None
-
- # Filter out hls temporarily.
- data['formats'] = list(filter(lambda f: not f['protocol'].startswith('m3u8'), data['formats']))
-
- if len(data['formats']) == 0:
- return None
-
- url = data['formats'][0]['url']
-
- # audio_ext?
- 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:
- url = get_playback_url(res)
- if not url:
- return False
- media = [Video(url=url)]
- 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
deleted file mode 100644
index ee23bef..0000000
--- a/src/shoki/py/query_parser.py
+++ /dev/null
@@ -1,39 +0,0 @@
-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
deleted file mode 100644
index 4134851..0000000
--- a/src/shoki/py/search.py
+++ /dev/null
@@ -1,35 +0,0 @@
-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
deleted file mode 100644
index 26ba9ee..0000000
--- a/src/shoki/src/packet_ext.c
+++ /dev/null
@@ -1,91 +0,0 @@
-#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
deleted file mode 100644
index ff0fda1..0000000
--- a/src/shoki/src/packet_ext.h
+++ /dev/null
@@ -1,8 +0,0 @@
-#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
deleted file mode 100644
index 22fb7ef..0000000
--- a/src/shoki/src/post.c
+++ /dev/null
@@ -1,55 +0,0 @@
-#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
deleted file mode 100644
index 6814b0f..0000000
--- a/src/shoki/src/post.h
+++ /dev/null
@@ -1,78 +0,0 @@
-#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
deleted file mode 100644
index a8f02eb..0000000
--- a/src/shoki/src/post_cache.c
+++ /dev/null
@@ -1,30 +0,0 @@
-#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
deleted file mode 100644
index 80cd0b7..0000000
--- a/src/shoki/src/post_cache.h
+++ /dev/null
@@ -1,13 +0,0 @@
-#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
deleted file mode 100644
index b4b2157..0000000
--- a/src/shoki/src/search.c
+++ /dev/null
@@ -1,99 +0,0 @@
-#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
deleted file mode 100644
index 97e40ae..0000000
--- a/src/shoki/src/search.h
+++ /dev/null
@@ -1,30 +0,0 @@
-#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/common.h
index 1b1187e..4e3ec3a 100644
--- a/src/tree/commands.h
+++ b/src/tree/common.h
@@ -1,5 +1,9 @@
#pragma once
+#define TREE_PORT 4356
+#define TREE_RESOURCE_PORT 4357
+#define TREE_STREAM_PORT 4358
+
enum {
TREE_NODE = 0,
TREE_CLIENT,
@@ -8,6 +12,8 @@ enum {
enum {
TREE_CMD_IDENTIFY = 0,
+ TREE_CMD_STATUS,
TREE_CMD_SEARCH,
+ TREE_CMD_RESUME_SEARCH,
TREE_CMD_ADD
};
diff --git a/src/tree/list.c b/src/tree/list.c
new file mode 100644
index 0000000..016c0de
--- /dev/null
+++ b/src/tree/list.c
@@ -0,0 +1,112 @@
+#ifdef AKIYO_HAS_CURL
+#include "../cache/handlers/http.h"
+#endif
+
+#include "../libsink/common.h"
+
+#include "list.h"
+#include "tree.h"
+
+void tree_list_init(struct tree_list *list, struct tree_server *tree, str *name)
+{
+ al_str_clone(&list->name, name);
+ list->set = -1;
+ list->current = 0;
+ al_array_init(list->entries);
+ al_array_init(list->sinks);
+ list->tree = tree;
+}
+
+void tree_list_add_sink(struct tree_list *list, struct tree_sink *sink)
+{
+ al_array_push(list->sinks, sink);
+}
+
+void tree_list_remove_sink(struct tree_list *list, struct tree_sink *sink)
+{
+ struct tree_sink *rsink;
+ al_array_foreach(list->sinks, i, rsink) {
+ if (rsink == sink) {
+ al_array_remove_at_iter(list->sinks, i);
+ break;
+ }
+ }
+}
+
+bool tree_list_add(struct tree_list *list, str *unique_id, u32 index)
+{
+ struct tree_server *tree = list->tree;
+ struct sho_post *post = sho_post_cache_get(&tree->resources.cache, unique_id);
+ if (!post) return false;
+
+ struct tree_list_entry *entry = al_alloc_object(struct tree_list_entry);
+
+ entry->post = post;
+ entry->index = index;
+
+ entry->entry = cch_handler_http_create(&al_array_at(entry->post->media, entry->index).url);
+ if (!entry->entry) {
+ al_free(entry);
+ return false;
+ }
+ entry->entry->handler->maybe_spawn_worker(entry->entry->handler, 0);
+
+ entry->node_id = bmu_server_create_node(&tree->streams.server, entry->entry);
+
+ entry->buffer_requested = false;
+
+ al_array_push(list->entries, entry);
+
+ tree_list_pump(list);
+
+ return true;
+}
+
+void tree_list_skip(struct tree_list *list, s32 n)
+{
+ s32 size = (s32)list->entries.size;
+ if (list->current + n < 0 || list->current + n >= size) {
+ return;
+ }
+ list->current += n;
+ tree_list_pump(list);
+}
+
+static void send_buffer_cmd(struct tree_list *list, struct tree_list_entry *entry)
+{
+ struct aki_packet *packet;
+ struct tree_sink *sink;
+ al_array_foreach(list->sinks, i, sink) {
+ packet = aki_rpc_get_packet(&list->tree->server, CAMU_SINK_CMD_BUFFER);
+ aki_packet_write_str(packet, al_str_c("127.0.0.1"));
+ aki_packet_write_s32(packet, TREE_STREAM_PORT);
+ aki_packet_write_u16(packet, entry->node_id);
+ aki_rpc_connection_command(sink->conn, packet, NULL, NULL);
+ }
+}
+
+static void send_set_cmd(struct tree_list *list, struct tree_list_entry *entry)
+{
+ struct aki_packet *packet;
+ struct tree_sink *sink;
+ al_array_foreach(list->sinks, i, sink) {
+ packet = aki_rpc_get_packet(&list->tree->server, CAMU_SINK_CMD_SET);
+ aki_packet_write_u16(packet, entry->node_id);
+ aki_rpc_connection_command(sink->conn, packet, NULL, NULL);
+ }
+}
+
+void tree_list_pump(struct tree_list *list)
+{
+ s32 size = (s32)list->entries.size;
+ if (size <= list->current) return;
+ if (list->set != list->current) {
+ struct tree_list_entry *entry = al_array_at(list->entries, list->current);
+ if (!entry->buffer_requested) {
+ send_buffer_cmd(list, entry);
+ entry->buffer_requested = true;
+ }
+ send_set_cmd(list, entry);
+ list->set = list->current;
+ }
+}
diff --git a/src/tree/list.h b/src/tree/list.h
new file mode 100644
index 0000000..ac7f8ee
--- /dev/null
+++ b/src/tree/list.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include <al/str.h>
+#include <al/array.h>
+
+#include "../cache/entry.h"
+
+struct tree_list_entry {
+ struct sho_post *post;
+ u32 index;
+ u16 node_id;
+ bool buffer_requested;
+ struct cch_entry *entry;
+};
+
+struct tree_sink;
+struct tree_list {
+ str name;
+ s32 set;
+ s32 current;
+ array(struct tree_list_entry *) entries;
+ array(struct tree_sink *) sinks;
+ struct tree_server *tree;
+};
+
+void tree_list_init(struct tree_list *list, struct tree_server *tree, str *name);
+void tree_list_add_sink(struct tree_list *list, struct tree_sink *sink);
+void tree_list_remove_sink(struct tree_list *list, struct tree_sink *sink);
+bool tree_list_add(struct tree_list *list, str *unique_id, u32 index);
+void tree_list_skip(struct tree_list *list, s32 n);
+void tree_list_pump(struct tree_list *list);
diff --git a/src/tree/meson.build b/src/tree/meson.build
index c7301f9..e9b8670 100644
--- a/src/tree/meson.build
+++ b/src/tree/meson.build
@@ -1,3 +1,7 @@
-tree_src = ['tree.c', 'resource_manager.c']
+tree_src = [
+ 'tree.c',
+ 'resource_manager.c',
+ 'list.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
index 0e95fbb..dd24310 100644
--- a/src/tree/resource_manager.c
+++ b/src/tree/resource_manager.c
@@ -18,7 +18,7 @@ static void packet_callback(void *userdata, struct aki_packet_stream *stream, st
request->stream = stream;
request->id = aki_packet_read_u16(packet);
str unique_id;
- aki_packet_read_string(packet, &unique_id);
+ aki_packet_read_str(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);
@@ -40,8 +40,8 @@ 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);
+ aki_packet_pool_return(&server->pool, packet);
}
static void flushed_callback(void *userdata)
@@ -70,6 +70,6 @@ bool tree_resource_server_init(struct tree_resource_server *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_pool_init(&server->pool, 50, loop, packet_pool_callback, server);
aki_packet_stream_listen(&server->server, loop, addr, port);
}
diff --git a/src/tree/tree.c b/src/tree/tree.c
index 0947be0..014397f 100644
--- a/src/tree/tree.c
+++ b/src/tree/tree.c
@@ -24,8 +24,12 @@ static void http_callback(void *userdata, struct aki_http_request *response, boo
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;
+ struct sho_post *post = sho_post_cache_get(&tree->resources.cache, &request->unique_id);
+ struct aki_packet *packet = request->packet;
+ if (!post) {
+ aki_packet_pool_return(&request->server->pool, packet);
+ return false;
+ }
str *url = NULL;
if (al_str_eq(&post->author.unique_id, &request->unique_id)) {
url = &post->author.profile_image_url;
@@ -37,7 +41,10 @@ static bool request_resource(void *userdata, struct tree_resource_request *reque
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);
+ if (!aki_http_request(&request->request, AKI_HTTP_GET, &tree->loop, http_callback, request)) {
+ aki_packet_pool_return(&request->server->pool, packet);
+ return false;
+ }
return true;
}
@@ -66,6 +73,25 @@ static struct tree_user *get_user_by_connection(struct tree_server *tree, struct
return NULL;
}
+static void send_status(struct tree_server *server, struct tree_user *user, struct aki_rpc_connection *conn)
+{
+ struct aki_packet *packet = aki_rpc_get_packet(&server->server, TREE_CMD_STATUS);
+ aki_packet_write_u32(packet, user->lists.size);
+ struct tree_list *list;
+ al_array_foreach(user->lists, i, list) {
+ aki_packet_write_str(packet, &list->name);
+ }
+ aki_packet_write_u32(packet, user->search.searches.size);
+ struct sho_search *search;
+ al_array_foreach(user->search.searches, i, search) {
+ aki_packet_write_s32(packet, search->id);
+ aki_packet_write_str(packet, &search->module);
+ aki_packet_write_str(packet, &search->query);
+ aki_packet_write_s32(packet, search->page);
+ }
+ aki_rpc_connection_command(conn, packet, NULL, NULL);
+}
+
static bool identify_command_callback(void *userdata, struct aki_rpc_connection *conn,
struct aki_packet *packet, struct aki_packet *rpacket)
{
@@ -76,18 +102,21 @@ static bool identify_command_callback(void *userdata, struct aki_rpc_connection
struct tree_node *node = al_alloc_object(struct tree_node);
node->conn = conn;
al_array_push(tree->nodes, node);
+ al_log_info("tree", "New node.");
break;
}
case TREE_CLIENT: {
str username;
- aki_packet_read_string(packet, &username);
+ aki_packet_read_str(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;
+ client->user = user;
al_array_push(tree->clients, client);
al_array_push(user->clients, client);
+ send_status(tree, user, conn);
+ al_log_info("tree", "User \"%.*s\" logged in.", AL_STR_PRINTF(&user->username));
}
break;
}
@@ -95,6 +124,10 @@ static bool identify_command_callback(void *userdata, struct aki_rpc_connection
struct tree_sink *sink = al_alloc_object(struct tree_sink);
sink->conn = conn;
al_array_push(tree->sinks, sink);
+ struct tree_user *user = al_array_at(tree->users, 0);
+ struct tree_list *list = al_array_at(user->lists, 0);
+ tree_list_add_sink(list, sink);
+ al_log_info("tree", "New sink.");
break;
}
}
@@ -102,22 +135,26 @@ static bool identify_command_callback(void *userdata, struct aki_rpc_connection
return true;
}
-static struct tree_query *query_from_id(struct tree_user *user, s32 id)
+static bool search_command_callback(void *userdata, struct aki_rpc_connection *conn,
+ struct aki_packet *packet, struct aki_packet *rpacket)
{
- struct tree_query *query;
- al_array_foreach_ptr(user->queries, i, query) {
- if (query->search.id == id) {
- return query;
- }
+ 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;
}
- al_array_push(user->queries, (struct tree_query){});
- query = &al_array_last(user->queries);
- query->id = id;
- sho_search_init(&query->search);
- return query;
+ str module, query;
+ aki_packet_read_str(packet, &module);
+ aki_packet_read_str(packet, &query);
+ s32 id = sho_client_create_search(&user->search, &module, &query);
+ aki_packet_write_s32(rpacket, id);
+out:
+ aki_packet_free(packet);
+ return true;
}
-static bool search_command_callback(void *userdata, struct aki_rpc_connection *conn,
+static bool resume_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;
@@ -127,66 +164,134 @@ static bool search_command_callback(void *userdata, struct aki_rpc_connection *c
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.");
+ s32 req_page = aki_packet_read_s32(packet);
+ struct sho_search *search = sho_client_get_search(&user->search, id);
+ if (!search) {
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);
+ if (req_page >= 0) {
+ if (!sho_search_from_page(search, req_page)) {
+ aki_packet_write_s32(rpacket, -1);
+ goto out;
+ }
+ } else {
+ if (!sho_search_more_results(search)) {
+ aki_packet_write_s32(rpacket, -1);
+ goto out;
+ }
+ }
+ aki_packet_write_s32(rpacket, 0);
+ aki_packet_write_s32(rpacket, search->id);
+ aki_packet_write_s32(rpacket, search->page);
+ struct sho_result_page *page = &al_array_at(search->pages, search->page);
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);
+ aki_packet_write_str(rpacket, unique_id);
}
out:
aki_packet_free(packet);
return true;
}
+static bool add_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;
+ struct tree_user *user = get_user_by_connection(tree, conn);
+ if (!user) goto out;
+
+ str unique_id;
+ aki_packet_read_str(packet, &unique_id);
+ u32 index = aki_packet_read_u32(packet);
+
+ struct tree_list *list = al_array_at(user->lists, 0);
+ tree_list_add(list, &unique_id, index);
+ tree_list_skip(list, 1);
+
+out:
+ aki_packet_free(packet);
+ return false;
+}
+
static void connection_callback(void *userdata, struct aki_rpc_connection *conn)
{
(void)userdata;
(void)conn;
}
+static void cleanup_node(struct tree_node *node)
+{
+ al_free(node);
+}
+
+static void cleanup_client(struct tree_client *client)
+{
+ al_free(client);
+}
+
+static void cleanup_sink(struct tree_sink *sink)
+{
+ al_free(sink);
+}
+
static void connection_closed_callback(void *userdata, struct aki_rpc_connection *conn)
{
- (void)userdata;
- (void)conn;
+ struct tree_server *tree = (struct tree_server *)userdata;
+
+ struct tree_node *node;
+ al_array_foreach(tree->nodes, i, node) {
+ if (node->conn == conn) {
+ al_log_info("tree", "Node removed.");
+ cleanup_node(node);
+ al_array_remove_at_iter(tree->nodes, i);
+ break;
+ }
+ }
+
+ struct tree_client *client;
+ al_array_foreach(tree->clients, i, client) {
+ if (client->conn == conn) {
+ al_log_info("tree", "User \"%.*s\" logged out.", AL_STR_PRINTF(&client->user->username));
+ cleanup_client(client);
+ al_array_remove_at_iter(tree->clients, i);
+ break;
+ }
+ }
+
+ struct tree_sink *sink;
+ al_array_foreach(tree->sinks, i, sink) {
+ if (sink->conn == conn) {
+ al_log_info("tree", "Sink removed.");
+ cleanup_sink(sink);
+ al_array_remove_at_iter(tree->sinks, i);
+ struct tree_user *user = al_array_at(tree->users, 0);
+ struct tree_list *list = al_array_at(user->lists, 0);
+ tree_list_remove_sink(list, sink);
+ break;
+ }
+ }
}
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 },
+ { .op = TREE_CMD_RESUME_SEARCH, .callback = resume_search_command_callback, .userdata = NULL },
+ { .op = TREE_CMD_ADD, .callback = add_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)) {
+ if (!aki_file_open(&file, &dir->path, 0)) {
return false;
}
str s;
@@ -195,10 +300,10 @@ static bool open_user(struct tree_server *tree, struct aki_dir_entry *dir)
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);
+ sho_client_init(&user->search, &tree->resources.cache);
al_array_init(user->lists);
- struct tree_list default_list;
- al_str_from(&default_list.name, "default");
+ struct tree_list *default_list = al_alloc_object(struct tree_list);
+ tree_list_init(default_list, tree, al_str_c("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));
@@ -232,25 +337,15 @@ static bool open_db(struct tree_server *tree, str *path)
return true;
}
-static bool cap_callback(void *userdata, u8 op, str *name, str *unique_id, void *opaque)
+static void sigint_handler(s32 signum)
{
- (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;
+ (void)signum;
+ // explode.
+ exit(EXIT_SUCCESS);
}
+static struct tree_server tree = { 0 };
+
s32 main(void)
{
aki_common_init();
@@ -266,7 +361,7 @@ s32 main(void)
return EXIT_FAILURE;
}
- sho_post_cache_init(&tree.cache);
+ sho_post_cache_init(&tree.resources.cache);
bool py_init = sho_python_init();
@@ -280,13 +375,11 @@ s32 main(void)
}
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);
+ tree_resource_server_init(&tree.resources.server, request_resource, &tree);
+ tree_resource_server_listen(&tree.resources.server, &tree.loop, al_str_c("0.0.0.0"), TREE_RESOURCE_PORT);
- bmu_server_init(&tree.stream_server);
- bmu_server_listen(&tree.stream_server, &tree.loop, al_str_c("0.0.0.0"), TREE_STREAM_PORT);
+ bmu_server_init(&tree.streams.server);
+ bmu_server_listen(&tree.streams.server, &tree.loop, al_str_c("0.0.0.0"), TREE_STREAM_PORT);
aki_event_loop_run(&tree.loop);
diff --git a/src/tree/tree.h b/src/tree/tree.h
index 06f98b1..478306a 100644
--- a/src/tree/tree.h
+++ b/src/tree/tree.h
@@ -1,47 +1,35 @@
#pragma once
#include <aki/rpc2.h>
+#include <sho/post.h>
+#include <sho/post_cache.h>
+#include <sho/search.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 "common.h"
#include "resource_manager.h"
+#include "list.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_user *user;
};
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;
+ struct sho_client search;
+ array(struct tree_list *) lists;
array(struct tree_client *) clients;
};
@@ -52,8 +40,11 @@ struct tree_server {
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;
+ struct {
+ struct sho_post_cache cache;
+ struct tree_resource_server server;
+ } resources;
+ struct {
+ struct bmu_server server;
+ } streams;
};
diff --git a/src/util/color_palette.c b/src/util/color_palette.c
index e55c9e1..53c2c68 100644
--- a/src/util/color_palette.c
+++ b/src/util/color_palette.c
@@ -32,7 +32,7 @@ static char *normal_colors[16] = {
bool camu_color_palette_init(str *path)
{
struct aki_file file;
- if (!aki_file_open(&file, path, false)) {
+ if (!aki_file_open(&file, path, 0)) {
return false;
}
str s;
diff --git a/src/util/queue.h b/src/util/queue.h
index 032bb55..4f184ba 100644
--- a/src/util/queue.h
+++ b/src/util/queue.h
@@ -36,6 +36,16 @@
aki_mutex_unlock(&(q).mutex); \
} while (0)
+#define camu_queue_try_pop(q, s, r) \
+ do { \
+ aki_mutex_lock(&(q).mutex); \
+ s = (q).a.size; \
+ if (s > 0) { \
+ 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); \