diff options
231 files changed, 7701 insertions, 6305 deletions
diff --git a/.editorconfig b/.editorconfig index f0409bd..7d5465e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,8 +1,11 @@ root = true +[/.git/**] +max_line_length = unset + [*] -indent_style=space -indent_size=4 -tab_width=4 -end_of_line=lf -trim_trailing_whitespace=true +indent_style = space +indent_size = 4 +tab_width = 4 +end_of_line = lf +trim_trailing_whitespace = true @@ -1,49 +1,47 @@ **/__pycache__/ +.cache/ build*/ compile_commands.json -.cache/ -.clangd/ data/ doc/**.html env/bin/ old*/ -src/portal/vendor/* src/portal/cpy/portal.c src/portal/cpy/portal.h src/portal/py/config.py +src/portal/vendor/* subprojects/BLAKE3/ -subprojects/c89atomic/ subprojects/c89atomic.wrap +subprojects/c89atomic/ subprojects/ffmpeg-*/ -subprojects/glfw3/ subprojects/glfw3.wrap -subprojects/jansson/ +subprojects/glfw3/ subprojects/jansson.wrap +subprojects/jansson/ subprojects/libakiyo subprojects/libakiyo/ subprojects/libalabaster subprojects/libalabaster/ subprojects/libass/ subprojects/libcdio-paranoia/ -subprojects/libcurl/ subprojects/libcurl.wrap +subprojects/libcurl/ subprojects/libdvdcss/ subprojects/libdvdnav/ subprojects/libdvdread/ subprojects/libev-4.33/ subprojects/libev.wrap -subprojects/liblfds.wrap -subprojects/liblfds7.1.1/ subprojects/libplacebo/ subprojects/linmath.h/ subprojects/miniaudio/ subprojects/packagecache/ -subprojects/sekihi subprojects/shaderc/ subprojects/stb/ -subprojects/vulkan/ +subprojects/stela +subprojects/stela/ subprojects/vulkan.wrap +subprojects/vulkan/ subprojects/wayland-protocols.wrap -subprojects/wlr-protocols/ subprojects/wlr-protocols.wrap +subprojects/wlr-protocols/ subprojects/wuffs/ diff --git a/cross/i686-w64-mingw32.txt b/cross/i686-w64-mingw32.txt new file mode 100644 index 0000000..3e2392f --- /dev/null +++ b/cross/i686-w64-mingw32.txt @@ -0,0 +1,17 @@ +[binaries] +c = 'i686-w64-mingw32-gcc' +cpp = 'i686-w64-mingw32-g++' +ar = 'i686-w64-mingw32-gcc-ar' +strip = 'i686-w64-mingw32-strip' +exe_wrapper = 'wine' + +[properties] +nm = 'i686-w64-mingw32-gcc-nm' +ar = 'i686-w64-mingw32-gcc-ar' +needs_exe_wrapper = true + +[host_machine] +system = 'windows' +cpu_family = 'x86' +cpu = 'x86' +endian = 'little' diff --git a/cross/x86_64-w64-mingw32.txt b/cross/x86_64-w64-mingw32.txt index 5aeeadd..55fc1e6 100644 --- a/cross/x86_64-w64-mingw32.txt +++ b/cross/x86_64-w64-mingw32.txt @@ -10,6 +10,7 @@ exe_wrapper = 'wine64' [properties] nm = 'x86_64-w64-mingw32-gcc-nm' ar = 'x86_64-w64-mingw32-gcc-ar' +needs_exe_wrapper = true [host_machine] system = 'windows' diff --git a/doc/database.txt b/doc/database.txt index 3cf4d4b..0d2afa2 100644 --- a/doc/database.txt +++ b/doc/database.txt @@ -1,3 +1,30 @@ += CAMU Database (Archive) + +Types of Media for Consideration +-------------------------------- +- Music + - Singles + - Albums + - Music Videos + - Playlists (Single/Split file(s)) + - Video Game OST (ripped) + - CD + +- Video + - Movies/TV Shows + - Youtube Channels/Videos + - DVD/Blu-ray + +- Art + - Artists + - Twitter/Pixiv Profiles + +- Games + - Steam Games + - Roms + - Achievements + - Time Tracking + Database -------- * Tied to a single directory on the filesystem @@ -5,4 +32,14 @@ Database * Easily referenced external files. ** external:<BLAKE3 checksum> +Database-specific Types +----------------------- +Collection +~~~~~~~~~~ +A defined set of resources that may be stored either too loose or too bundled than desired. The exact parameters that qualify a set of resources to become a collection is up to the database owner. A general rule of thumb should be that the relationship of the resources in a collection should be externally defined and commonly agreed upon. You may also want to consider if that relationship is "official" or legally defined, whatever that would mean for your case. +Examples +^^^^^^^^ +* A music album where each file is a track. +* An art book part of a larger zip archive with multiple other resources/collections. + // vim: set syntax=asciidoc: diff --git a/doc/network_independent_audio_pause.kra b/doc/network_independent_audio_pause.kra Binary files differnew file mode 100644 index 0000000..c0e39de --- /dev/null +++ b/doc/network_independent_audio_pause.kra diff --git a/doc/network_independent_audio_pause.png b/doc/network_independent_audio_pause.png Binary files differnew file mode 100644 index 0000000..b5aa516 --- /dev/null +++ b/doc/network_independent_audio_pause.png diff --git a/env/wsl/flake.lock b/env/wsl/flake.lock index f1c225f..42c0437 100755 --- a/env/wsl/flake.lock +++ b/env/wsl/flake.lock @@ -21,11 +21,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1705309234, - "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", "type": "github" }, "original": { @@ -41,11 +41,11 @@ ] }, "locked": { - "lastModified": 1706798041, - "narHash": "sha256-BbvuF4CsVRBGRP8P+R+JUilojk0M60D7hzqE0bEvJBQ=", + "lastModified": 1723986931, + "narHash": "sha256-Fy+KEvDQ+Hc8lJAV3t6leXhZJ2ncU5/esxkgt3b8DEY=", "owner": "nix-community", "repo": "home-manager", - "rev": "4d53427bce7bf3d17e699252fd84dc7468afc46e", + "rev": "2598861031b78aadb4da7269df7ca9ddfc3e1671", "type": "github" }, "original": { @@ -62,11 +62,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1706867893, - "narHash": "sha256-c5bADvtL35S3vsJaXR5YWTXe08W0gSwOrTOXfpJB4Ac=", + "lastModified": 1723124175, + "narHash": "sha256-w+rlZdbj4kn70uPJl4WrPDFoGEy0A9oJqw684nvZ9dY=", "owner": "nix-community", "repo": "NixOS-WSL", - "rev": "bcae8dc73b931b7f0fc65f1f1ef93dc379dfd66b", + "rev": "be1a6b2e4ddc34b9b6a297e7df2f2a2ecee24690", "type": "github" }, "original": { @@ -78,27 +78,27 @@ }, "nixpkgs": { "locked": { - "lastModified": 1705774713, - "narHash": "sha256-j6ADaDH9XiumUzkTPlFyCBcoWYhO83lfgiSqEJF2zcs=", + "lastModified": 1722087241, + "narHash": "sha256-2ShmEaFi0kJVOEEu5gmlykN5dwjWYWYUJmlRTvZQRpU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "1b64fc1287991a9cce717a01c1973ef86cb1af0b", + "rev": "8c50662509100d53229d4be607f1a3a31157fa12", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-23.11", + "ref": "nixos-24.05", "repo": "nixpkgs", "type": "github" } }, "nixpkgs_2": { "locked": { - "lastModified": 1706732774, - "narHash": "sha256-hqJlyJk4MRpcItGYMF+3uHe8HvxNETWvlGtLuVpqLU0=", + "lastModified": 1723637854, + "narHash": "sha256-med8+5DSWa2UnOqtdICndjDAEjxr5D7zaIiK4pn0Q7c=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "b8b232ae7b8b144397fdb12d20f592e5e7c1a64d", + "rev": "c3aa7b8938b17aebd2deecf7be0636000d62a2b9", "type": "github" }, "original": { diff --git a/env/wsl/flake.nix b/env/wsl/flake.nix index 577b607..dc8c0b1 100755 --- a/env/wsl/flake.nix +++ b/env/wsl/flake.nix @@ -15,9 +15,9 @@ nixpkgs-patched = source-pkgs.applyPatches { name = "nixpkgs-patched"; src = nixpkgs; - patches = [ - ./noto_cjk_no_variable.diff - ]; + #patches = [ + # ./noto_cjk_no_variable.diff + #]; }; nixosSystem = import (nixpkgs-patched + "/nixos/lib/eval-config.nix"); in nixosSystem { diff --git a/env/wsl/wsl-dev-host.nix b/env/wsl/wsl-dev-host.nix index ef7fbef..afef6a8 100755 --- a/env/wsl/wsl-dev-host.nix +++ b/env/wsl/wsl-dev-host.nix @@ -4,7 +4,7 @@ let in { imports = [ ]; - system.stateVersion = "23.05"; + system.stateVersion = "24.11"; nix = { settings = { @@ -31,7 +31,6 @@ in { networking.hostName = "wsl-dev"; networking.firewall.enable = false; - sound.enable = true; hardware.pulseaudio.enable = true; xdg.portal = { @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", "type": "github" }, "original": { @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1716509168, - "narHash": "sha256-4zSIhSRRIoEBwjbPm3YiGtbd8HDWzFxJjw5DYSDy1n8=", + "lastModified": 1729256560, + "narHash": "sha256-/uilDXvCIEs3C9l73JTACm4quuHUsIHcns1c+cHUJwA=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "bfb7a882678e518398ce9a31a881538679f6f092", + "rev": "4c2fcb090b1f3e5b47eaa7bd33913b574a11e0a0", "type": "github" }, "original": { @@ -10,9 +10,7 @@ overlays = [ (self: super: { libplacebo = super.libplacebo.overrideAttrs (old: { - patches = [ - ./subprojects/packagefiles/libplacebo/stretch.diff - ]; + patches = []; }); }) ]; @@ -21,7 +19,7 @@ with pkgs; rec { packages.default = stdenv.mkDerivation { - pname = "camu"; + name = "camu"; version = "0.01"; src = self; outputs = [ "out" "dev" ]; @@ -38,13 +36,14 @@ meson cmake ant - zulu8 + jdk8 wayland-scanner ]; buildInputs = [ (curl.override { websocketSupport = true; }) jansson - ffmpeg_7-headless + libspng + ffmpeg-headless libcdio libcdio-paranoia libdvdcss @@ -68,8 +67,9 @@ xorg.libX11 wayland wayland-protocols + wlr-protocols egl-wayland - notcurses + (notcurses.override { multimediaSupport = false; qrcodegenSupport = false; }) libsodium ]; meta = with lib; { @@ -85,24 +85,32 @@ NIX_HARDENING_ENABLE = ""; nativeBuildInputs = packages.default.nativeBuildInputs ++ [ gdb - clang-tools_17 + clang-tools + #ruff + #ruff-lsp + pyright perf-tools valgrind - ((python312.override { enableOptimizations = true; reproducibleBuild = false; }).withPackages (python-pkgs: [ + ((python3.override { enableOptimizations = true; reproducibleBuild = false; }).withPackages (python-pkgs: [ python-pkgs.setuptools python-pkgs.pip python-pkgs.cython + python-pkgs.certifi python-pkgs.requests + python-pkgs.beautifulsoup4 + python-pkgs.python-creole ])) - nodePackages.pyright perl + sourceHighlight + asciidoc asciidoctor + pandoc ]; buildInputs = packages.default.buildInputs ++ []; shellHook = '' export NIX_SHELL="camu" unset SOURCE_DATE_EPOCH - exec ${pkgs.zsh}/bin/zsh + exec ${pkgs.zsh}/bin/zsh -l ''; }; @@ -113,7 +121,7 @@ shellHook = '' export NIX_SHELL="camu-wsl" unset SOURCE_DATE_EPOCH - exec ${pkgs.zsh}/bin/zsh -c kitty + exec ${pkgs.zsh}/bin/zsh -l -c kitty ''; }; @@ -133,12 +141,15 @@ gdb nasm pkg-config + autoconf + automake + autogen ninja meson cmake wineWowPackages.staging glslang - python312 + python3 ]; buildInputs = with pkgsCross.ucrt64; [ (zlib.override { shared = false; static = true; }) @@ -147,28 +158,29 @@ shellHook = '' export NIX_SHELL="camu-mingw64" unset SOURCE_DATE_EPOCH - exec ${pkgs.zsh}/bin/zsh + exec ${pkgs.zsh}/bin/zsh -l ''; }; - devShells.mingw64-pthreads = let - gcc = pkgsCross.mingwW64.buildPackages.wrapCC (pkgsCross.mingwW64.buildPackages.gcc-unwrapped.override ({ -# threadsCross = { -# model = "posix"; -# package = null; -# }; + devShells.mingw32 = let + gcc = pkgsCross.mingw32.buildPackages.wrapCC (pkgsCross.mingw32.buildPackages.gcc-unwrapped.override ({ + threadsCross = { + model = "win32"; + package = null; + }; })); - gccWin32Stdenv = overrideCC pkgsCross.mingwW64.stdenv gcc; + gccWin32Stdenv = overrideCC pkgsCross.mingw32.stdenv gcc; in mkShell.override { stdenv = gccWin32Stdenv; } { NIX_HARDENING_ENABLE = ""; - nativeBuildInputs = devShells.mingw64.nativeBuildInputs ++ []; - buildInputs = [ - (pkgsCross.mingwW64.python312.override { enableOptimizations = true; reproducibleBuild = false; }) + nativeBuildInputs = [] ++ devShells.mingw64.nativeBuildInputs; + buildInputs = with pkgsCross.mingw32; [ + (zlib.override { shared = false; static = true; }) + xxHash ]; shellHook = '' - export NIX_SHELL="camu-mingw64-pthreads" + export NIX_SHELL="camu-mingw32" unset SOURCE_DATE_EPOCH - exec ${pkgs.zsh}/bin/zsh + exec ${pkgs.zsh}/bin/zsh -l ''; }; } diff --git a/meson.build b/meson.build index bdfb134..abf383b 100644 --- a/meson.build +++ b/meson.build @@ -5,49 +5,62 @@ compiler = meson.get_compiler('c') cmake = import('cmake') is_debug = get_option('buildtype').startswith('debug') +is_minsize = get_option('buildtype') == 'minsize' is_windows = host_machine.system() == 'windows' alabaster = subproject('libalabaster') akiyo_opts = [] -if get_option('sink-only') - akiyo_opts += ['curl=disabled'] +if not get_option('sink-only') + akiyo_opts += ['json=enabled', 'curl=enabled'] endif akiyo = subproject('libakiyo', default_options: akiyo_opts) akiyo_has_curl = akiyo.get_variable('akiyo_has_curl') akiyo_has_mmap = akiyo.get_variable('akiyo_has_mmap') - common_deps = [alabaster.get_variable('alabaster'), akiyo.get_variable('akiyo')] -subdir('src/util') +sink_use_vulkan = false +stela = subproject('stela', default_options: [ + sink_use_vulkan ? 'api=vulkan' : 'api=opengl', + is_windows ? 'window=glfw' : 'window=wayland', + 'poll=inline', + 'event-buffer=false', + 'pause=true' +]).get_variable('stela') +subdir('src/util') common_deps += [util] subdir('src/cache') subdir('src/codec') -subdir('src/list') -subdir('src/shrub') +subdir('src/liana') + +if get_option('portal').enabled() + subdir('src/portal') +endif + +if get_option('server').enabled() + subdir('src/server') + if not meson.is_subproject() + subdir('src/fruits/cmsrv') + endif +endif + +if get_option('client').enabled() + subdir('src/libclient') +endif -sink_use_vulkan = false if get_option('sink').enabled() subdir('src/buffer') subdir('src/mixer') subdir('src/render') subdir('src/screen') subdir('src/libsink') + subdir('src/sink') if not meson.is_subproject() - subdir('src/fruits/sink') + subdir('src/fruits/cmv') endif endif -if get_option('server').enabled() or get_option('client').enabled() - subdir('src/portal') -endif - -if get_option('server').enabled() - subdir('src/server') -endif - -if get_option('client').enabled() - #subdir('src/libclient') - #subdir('src/fruits/cmc') +if get_option('tests') + subdir('tests') endif diff --git a/meson_options.txt b/meson_options.txt index 94fa980..f187f48 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,4 +1,8 @@ option('sink', type: 'feature', value: 'enabled') option('sink-only', type: 'boolean', value: true) +option('codecs', type: 'array', choices: ['ffmpeg', 'stb_image', 'spng', 'wuffs'], value: ['ffmpeg']) +option('renderer', type: 'combo', choices: ['libplacebo', 'tiger'], value: 'libplacebo') +option('portal', type: 'feature', value: 'disabled') option('server', type: 'feature', value: 'disabled') option('client', type: 'feature', value: 'disabled') +option('tests', type: 'boolean', value: false) diff --git a/pyrightconfig.json b/pyrightconfig.json index 5d93693..ace75e9 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -1,10 +1,7 @@ { - "reportUnusedVariable": "none", "extraPaths": [ "./src/portal/py", "./src/portal/vendor/vendor" ], - "pythonVersion": "3.12", - "pythonPlatform": "Linux", "executionEnvironments": [ { "root": "./src/portal/py" } ] } diff --git a/scripts/cmv.sh b/scripts/cmv.sh new file mode 100755 index 0000000..0bf2de5 --- /dev/null +++ b/scripts/cmv.sh @@ -0,0 +1,18 @@ +#! /usr/bin/env bash +ORIGIN="$PWD" +CMD=() +for dir in "$@" +do + if [[ $dir == http\://* || $dir == https\://* || $dir == cdda\://* ]]; then + EXP=$dir + else + EXP=$(realpath -s "`printf '%b' \"$dir\"`") + if [[ ! -f $EXP ]]; then + EXP=";"$dir + fi + fi + CMD+=("$EXP") +done +cd $HOME/c/camu/build/ +../scripts/run.sh "${CMD[@]}" +cd "$ORIGIN" diff --git a/scripts/cmv_add.sh b/scripts/cmv_add.sh index 700b21b..26f5547 100755 --- a/scripts/cmv_add.sh +++ b/scripts/cmv_add.sh @@ -2,7 +2,7 @@ CMD="" for dir in "$@" do - if [[ $dir == http\://* || $dir == https\://* ]]; then + if [[ $dir == http\://* || $dir == https\://* || $dir == cdda\://* ]]; then EXP=$dir else EXP=$(realpath -s "`printf '%b' \"$dir\"`") @@ -12,4 +12,4 @@ do fi CMD="$CMD""$EXP\n" done -echo -e "$CMD""X" | socat - UNIX-CONNECT:/tmp/camu_sock +echo -e "$CMD""X" | socat - UNIX-CONNECT:/tmp/cmv_sock diff --git a/scripts/line_count.sh b/scripts/line_count.sh index 19844b9..31e72ab 100755 --- a/scripts/line_count.sh +++ b/scripts/line_count.sh @@ -1,8 +1,9 @@ #! /usr/bin/env sh find ./src -type f \ - -not -path "./src/portal/vendor/*" \ - -not -path "./src/portal/patches/*" \ -not -path "./src/portal/cpy/portal.c" \ -not -path "./src/portal/cpy/portal.h" \ + -not -path "./src/portal/patches/*" \ + -not -path "./src/portal/tests/*" \ + -not -path "./src/portal/vendor/*" \ -not -path "*__pycache__*" \ - | xargs wc -l + -exec wc -l {} + diff --git a/scripts/run.sh b/scripts/run.sh index f2bbaa0..40f34d7 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -1,3 +1,6 @@ #! /usr/bin/env sh +export CURL_CA_BUNDLE=/etc/ssl/certs/ca-bundle.crt +export PYTHONDONTWRITEBYTECODE=1 +export PYTHONPATH=$HOME/c/camu/src/portal/vendor/vendor export mesa_glthread=true -./src/fruits/sink/sink "$@" +nixGLIntel ./src/fruits/cmv/cmv "$@" diff --git a/scripts/run_debug.sh b/scripts/run_debug.sh index 8b8b329..bd9a667 100755 --- a/scripts/run_debug.sh +++ b/scripts/run_debug.sh @@ -1,2 +1,2 @@ #! /usr/bin/env sh -gdb -ex run --args ./src/fruits/sink/sink "$@" +gdb -ex run --args ./src/fruits/cmv/cmv "$@" diff --git a/scripts/run_server.sh b/scripts/run_server.sh index 1c2cbbf..4e65e48 100755 --- a/scripts/run_server.sh +++ b/scripts/run_server.sh @@ -1,4 +1,7 @@ #! /usr/bin/env sh +export CURL_CA_BUNDLE=/etc/ssl/certs/ca-bundle.crt export PYTHONDONTWRITEBYTECODE=1 export PYTHONPATH=$HOME/c/camu/src/portal/vendor/vendor -rm -f /tmp/camu_sock && gdb -ex run --args ./src/server/server $@ +./src/fruits/cmsrv/cmsrv $@ +#gdb -ex run --args ./src/fruits/cmsrv/cmsrv $@ +#valgrind ./src/fruits/cmsrv/cmsrv $@ diff --git a/scripts/run_valgrind.sh b/scripts/run_valgrind.sh new file mode 100755 index 0000000..943d25b --- /dev/null +++ b/scripts/run_valgrind.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env sh +valgrind --leak-check=full ./src/fruits/cmv/cmv "$@" diff --git a/src/buffer/audio.c b/src/buffer/audio.c index 644be42..1bf42bf 100644 --- a/src/buffer/audio.c +++ b/src/buffer/audio.c @@ -1,169 +1,139 @@ #include <al/log.h> +#ifdef CAMU_HAVE_FFMPEG +#include "../codec/ffmpeg/resampler.h" +#endif + #include "audio.h" #include "common.h" #include "common_internal.h" +#include "volume.h" -#define BUFFER_USEC (12 * 1000000L) -#define BUFFER_WATERMARK_LOW (4 * 1000000L) // Must be a most half of the buffer size. -#define BUFFER_WATERMARK_HIGH (3 * 1000000L) +#define BUFFER_USEC (3 * 1000000L) +#define BUFFER_MARK_MIN (1.1 * 1000000L) // Must be a most half of the buffer size. +#define BUFFER_MARK_BUFFERED (1.0 * 1000000L) -#define DESYNC_PTS 0.022 +#define LARGE_DESYNC_PTS 0.322 #ifdef CAMU_AUDIO_BUFFER_FADE -#define FADE 1.0 -#define FADE_LENGTH(rate) (rate * 0.4) -#define FADE_STEP(rate) (FADE / FADE_LENGTH(rate)) +#define FADE_STEP(fmt) (1.f / (fmt)->sample_rate) #endif enum { - PAUSE_PRE = 0, - PAUSE_PAUSED, + PAUSE_PAUSED = 0, #ifdef CAMU_AUDIO_BUFFER_FADE PAUSE_FADING, #endif PAUSE_PLAYING }; -static bool setup_optimal_resampler(struct camu_audio_buffer *buf, struct camu_ff_resampler *resamp, - struct camu_ff_resample_fmt *fmt) +static void reset_buffer_state(struct camu_audio_buffer *buf) { - /* - switch ((enum AVSampleFormat)fmt->in_format) { - case AV_SAMPLE_FMT_S16P: - fmt->req_format = AV_SAMPLE_FMT_S16; - break; - case AV_SAMPLE_FMT_S32P: - fmt->req_format = AV_SAMPLE_FMT_S32; - break; - case AV_SAMPLE_FMT_DBL: - case AV_SAMPLE_FMT_DBLP: - case AV_SAMPLE_FMT_FLTP: - fmt->req_format = AV_SAMPLE_FMT_FLT; - break; - case AV_SAMPLE_FMT_S16: - case AV_SAMPLE_FMT_S32: - case AV_SAMPLE_FMT_FLT: - fmt->req_format = fmt->in_format; - break; - default: - al_log_warn("audio_buffer", "Unhandled sample format."); - break; - } - - fmt->req_format = fmt->in_format; - fmt->req_channel_count = fmt->in_channel_count; - fmt->req_sample_rate = fmt->in_sample_rate; - */ - - buf->fade_rate = buf->fmt.req_sample_rate / 10; - buf->bytes_per_sample = (s32)camu_ff_resample_fmt_bytes_per_sample(fmt); - - return camu_ff_resampler_init(resamp, fmt); + buf->pts = -1.0; + buf->pause = PAUSE_PAUSED; + buf->volume = 1.f; +#ifdef CAMU_AUDIO_BUFFER_FADE + buf->fade_offset = 0; +#endif + buf->buffered = false; + al_atomic_store(u8)(&buf->flow, FLOWING, AL_ATOMIC_RELAXED); + al_atomic_store(size_t)(&buf->uncork_at, 0, AL_ATOMIC_RELAXED); } bool camu_audio_buffer_init(struct camu_audio_buffer *buf, struct camu_clock *clock, struct camu_mixer *mixer) { - buf->mixer = mixer; buf->clock = clock; + buf->mixer = mixer; + reset_buffer_state(buf); buf->ignore_desync = false; - al_atomic_size_t_store(&buf->continue_mark, 0, AL_ATOMIC_RELAXED); - buf->buffered = false; - al_atomic_u8_store(&buf->flow, FLOWING, AL_ATOMIC_RELAXED); - buf->pts = -1.0; - buf->pause = PAUSE_PRE; -#ifdef CAMU_AUDIO_BUFFER_FADE - buf->fade_period = 0; - buf->fade_offset = 0; -#endif + buf->latency = 0.0; #ifdef CAMU_MIXER_THREADED - al_atomic_bool_store(&buf->ref, false, AL_ATOMIC_RELAXED); + al_atomic_store(u8)(&buf->ref, 0, AL_ATOMIC_RELAXED); #endif return true; } -bool camu_audio_buffer_configure(struct camu_audio_buffer *buf, struct camu_stream *stream) +bool camu_audio_buffer_configure(struct camu_audio_buffer *buf, struct camu_codec_stream *stream) { - buf->fmt.in_format = stream->av.stream->codecpar->format; - av_channel_layout_copy(&buf->fmt.in_channel_layout, &stream->av.stream->codecpar->ch_layout); - buf->fmt.in_channel_count = buf->fmt.in_channel_layout.nb_channels; - buf->fmt.in_sample_rate = stream->av.stream->codecpar->sample_rate; - + camu_audio_format_copy(&buf->fmt.in, &stream->audio.fmt); camu_mixer_pick_format(buf->mixer, &buf->fmt); - if (!setup_optimal_resampler(buf, &buf->resamp, &buf->fmt)) { + buf->fmt.resampler_needed = !camu_resampler_format_matches(&buf->fmt); + if (buf->fmt.resampler_needed) { +#ifdef CAMU_HAVE_FFMPEG + buf->resamp = camu_ff_resampler_create(); + if (!buf->resamp->init(buf->resamp, &buf->fmt)) { + return false; + } +#else return false; +#endif } - 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); + const char *in_format_name = camu_audio_format_name(buf->fmt.in.format); + const char *req_format_name = camu_audio_format_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); + in_format_name, buf->fmt.in.channel_count, buf->fmt.in.sample_rate, + req_format_name, buf->fmt.req.channel_count, buf->fmt.req.sample_rate); - buf->size = camu_ff_resample_fmt_usec_to_bytes(&buf->fmt, BUFFER_USEC); + buf->size = camu_audio_format_usec_to_bytes(&buf->fmt.req, BUFFER_USEC); buf->data = (u8 *)al_malloc(buf->size); al_ring_buffer_init(&buf->rb, buf->data, buf->size); - buf->watermark.low = camu_ff_resample_fmt_usec_to_bytes(&buf->fmt, BUFFER_WATERMARK_LOW); - buf->watermark.high = camu_ff_resample_fmt_usec_to_bytes(&buf->fmt, BUFFER_WATERMARK_HIGH); - camu_peak_buffer_init(&buf->peak); + buf->mark.min = camu_audio_format_usec_to_bytes(&buf->fmt.req, BUFFER_MARK_MIN); + buf->mark.buffered = camu_audio_format_usec_to_bytes(&buf->fmt.req, BUFFER_MARK_BUFFERED); + camu_peak_buffer_init(&buf->peak, 1024 * 16); buf->stream = stream; return true; } +void camu_audio_buffer_set_latency(struct camu_audio_buffer *buf, f64 latency) +{ + buf->latency = latency; +} + #ifdef _DEBUG_ -#define BUFFER_OCCUPIED_DEBUG(buf) \ - camu_ff_resample_fmt_bytes_to_sec(&buf->fmt, al_ring_buffer_occupied(&buf->rb)) +#define OCCUPIED_SECONDS_DEBUG(buf) \ + camu_audio_format_bytes_to_sec(&buf->fmt.req, al_ring_buffer_occupied(&buf->rb)) #else -#define BUFFER_OCCUPIED_DEBUG(buf) 0 +#define OCCUPIED_SECONDS_DEBUG(buf) 0 #endif -static bool push_internal(struct camu_audio_buffer *buf, u8 **data, s32 sample_count, f64 pts) +static bool push_internal(struct camu_audio_buffer *buf, u8 *data, s32 sample_count) { - if (buf->fmt.resampler_needed) { - sample_count = camu_ff_resampler_convert(&buf->resamp, (const u8 **)data, sample_count); - data = camu_ff_resampler_get_data(&buf->resamp); - } - if (sample_count <= 0) return false; - - f64 duration = camu_ff_resample_fmt_samples_to_sec(&buf->fmt, sample_count); - if (pts + duration < camu_clock_get_base_pts(buf->clock)) { - return false; - } - if (buf->pts == -1.0) buf->pts = pts; - - size_t size, have = camu_ff_resample_fmt_samples_to_bytes(&buf->fmt, (size_t)sample_count); size_t space = al_ring_buffer_space(&buf->rb); - if (!buf->buffered && buf->size - space > buf->watermark.high) { + if (!buf->buffered && buf->size - space > buf->mark.buffered) { + al_log_debug("audio_buffer", "Buffered (mark: %.2fs).", OCCUPIED_SECONDS_DEBUG(buf)); buf->callback(buf->userdata, CAMU_BUFFER_BUFFERED); buf->buffered = true; - al_log_debug("audio_buffer", "Buffered (watermark: %.2fs).", BUFFER_OCCUPIED_DEBUG(buf)); } - size_t peak = buf->peak.buf.size; - if (space < (have + peak)) { - camu_peak_buffer_push(&buf->peak, data[0], have); - if (peak >= buf->watermark.low) { - // Discard the peak buffer. If this happens the writer of this buffer - // is taking _way_ too long to stop. - camu_peak_buffer_flush(&buf->peak, &size); + + size_t have = camu_audio_format_samples_to_bytes(&buf->fmt.req, (size_t)sample_count); + size_t peak = camu_peak_buffer_size(&buf->peak); + if (space < have + peak) { + al_assert(have < buf->mark.min); + camu_peak_buffer_push(&buf->peak, data, have); + peak += have; + if (peak >= buf->mark.min) { + // If this happens the writer of this buffer is taking way too long to stop. + al_log_warn("audio_buffer", "Overrun likely, discarding peak buffer."); + camu_peak_buffer_flush(&buf->peak); + peak = 0; } - // 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 size. - al_atomic_size_t_store(&buf->continue_mark, buf->watermark.low + peak, AL_ATOMIC_RELAXED); + al_assert(peak <= buf->mark.min); + // We adjust the min mark by the peak buffer size just for consistency. + al_atomic_store(size_t)(&buf->uncork_at, buf->mark.min - peak, AL_ATOMIC_RELAXED); buf->callback(buf->userdata, CAMU_BUFFER_CORK); return false; } if (peak > 0) { - u8 *ptr = camu_peak_buffer_flush(&buf->peak, &size); - al_ring_buffer_write(&buf->rb, ptr, size); + al_ring_buffer_write(&buf->rb, camu_peak_buffer_flush(&buf->peak), peak); } - al_ring_buffer_write(&buf->rb, data[0], have); + al_ring_buffer_write(&buf->rb, data, have); return true; } @@ -171,23 +141,40 @@ static bool push_internal(struct camu_audio_buffer *buf, u8 **data, s32 sample_c #ifdef CAMU_HAVE_FFMPEG static void push_av_frame_internal(struct camu_audio_buffer *buf, AVFrame *frame) { - // It doesn't matter if there's a race here with flush(), as long - // as any pushed frame is from the same stream. - // A reset() must finish before any data is pushed. - if (al_atomic_u8_load(&buf->flow, AL_ATOMIC_RELAXED) == FLOWING) { - AVStream *stream = buf->stream->av.stream; - f64 pts = (frame->best_effort_timestamp - stream->start_time) * av_q2d(stream->time_base); - push_internal(buf, frame->data, frame->nb_samples, pts); + s32 sample_count = frame->nb_samples; + AVStream *stream = buf->stream->av.stream; + f64 pts = frame->best_effort_timestamp * av_q2d(stream->time_base); + f64 duration = camu_audio_format_samples_to_sec(&buf->fmt.in, frame->nb_samples); + if (pts + duration >= camu_clock_get_base_pts(buf->clock)) { + if (buf->pts == -1.0) buf->pts = pts; + u8 **data = frame->data; + if (buf->fmt.resampler_needed) { + sample_count = buf->resamp->convert(buf->resamp, (const u8 **)data, sample_count); + data = buf->resamp->get_data(buf->resamp); + } + if (sample_count > 0) push_internal(buf, data[0], sample_count); } av_frame_free(&frame); } #endif -void camu_audio_buffer_push(struct camu_audio_buffer *buf, struct camu_frame *frame) +// A reset() must finish before any data is pushed. +void camu_audio_buffer_push(struct camu_audio_buffer *buf, struct camu_codec_frame *frame) { - switch (frame->type) { - case CAMU_NORMAL: + al_assert(al_atomic_load(u8)(&buf->flow, AL_ATOMIC_RELAXED) == FLOWING); + switch (frame->mode) { + case CAMU_NORMAL: { + u8 *store[AV_NUM_DATA_POINTERS] = { 0 }; + store[0] = frame->data; + u8 **data = store; + s32 sample_count = frame->audio.sample_count; + if (buf->fmt.resampler_needed) { + sample_count = buf->resamp->convert(buf->resamp, (const u8 **)store, sample_count); + data = buf->resamp->get_data(buf->resamp); + } + push_internal(buf, data[0], sample_count); break; + } #ifdef CAMU_HAVE_FFMPEG case CAMU_FFMPEG_COMPAT: { push_av_frame_internal(buf, frame->av.frame); @@ -198,92 +185,56 @@ void camu_audio_buffer_push(struct camu_audio_buffer *buf, struct camu_frame *fr al_free(frame); } +// Not thread-safe, must be called while the buffer is not being read from or written to. void camu_audio_buffer_unpause(struct camu_audio_buffer *buf) { - buf->pause = PAUSE_PRE; + buf->pause = PAUSE_PAUSED; } -// Not thread-safe, must be called while the buffer is not being read from or written to. +// Not thread-safe. void camu_audio_buffer_reset(struct camu_audio_buffer *buf) { - al_atomic_size_t_store(&buf->continue_mark, 0, AL_ATOMIC_RELAXED); - buf->buffered = false; - al_atomic_u8_store(&buf->flow, FLOWING, AL_ATOMIC_RELAXED); - buf->pts = -1.0; - buf->pause = PAUSE_PRE; + reset_buffer_state(buf); al_ring_buffer_reset(&buf->rb); + camu_peak_buffer_flush(&buf->peak); + if (buf->fmt.resampler_needed) { + buf->resamp->flush(buf->resamp); + } } // flush() always comes from the same thread as push(). void camu_audio_buffer_flush(struct camu_audio_buffer *buf) { + if (buf->fmt.resampler_needed) { + s32 sample_count = buf->resamp->flush(buf->resamp); + if (sample_count > 0) { + u8 **data = buf->resamp->get_data(buf->resamp); + if (!push_internal(buf, data[0], sample_count)) { + al_log_debug("audio_buffer", "Buffer filled by resampler flush."); + } + } + } if (!buf->buffered) { + al_log_debug("audio_buffer", "Buffered (mark: %.2fs).", OCCUPIED_SECONDS_DEBUG(buf)); buf->callback(buf->userdata, CAMU_BUFFER_BUFFERED); buf->buffered = true; - al_log_debug("audio_buffer", "Buffered (watermark: %.2fs).", BUFFER_OCCUPIED_DEBUG(buf)); } - al_atomic_u8_store(&buf->flow, FLUSHED, AL_ATOMIC_RELAXED); + al_atomic_store(u8)(&buf->flow, FLUSHED, AL_ATOMIC_RELAXED); al_log_debug("audio_buffer", "Flush requested."); } -#ifdef CAMU_AUDIO_BUFFER_FADE -#define MAX_BYTES 8 -#define TYPED_CAST(f, type) (f64)(*((type *)f)) -#define TYPED_CLAMP(f, type, min, max, ret) *((type *)ret) = (type)AL_CLAMP(f, min, max) +#define NOT_PAUSED(pause) (pause != PAUSE_PAUSED) -// 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 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; - for (u32 i = 0; i < sample_count; i++) { - al_memcpy(ptr, data, buf->bytes_per_sample); - switch (buf->bytes_per_sample) { - case 2: - value = TYPED_CAST(ptr, s16); - break; - case 4: - value = TYPED_CAST(ptr, s32); - break; - } - value *= (buf->volume); - switch (buf->bytes_per_sample) { - case 2: - TYPED_CLAMP(value, s16, INT16_MIN, INT16_MAX, ptr); - break; - case 4: - TYPED_CLAMP(value, s32, INT32_MIN, INT32_MAX, ptr); - break; - } - al_memcpy(data, ptr, buf->bytes_per_sample); - if ((i + 1) % channel_count == 0) { - if (out) buf->volume = AL_CLAMP((buf->volume - FADE_STEP(buf->fade_rate)), 0.0, 1.0); - else buf->volume = AL_CLAMP((buf->volume + FADE_STEP(buf->fade_rate)), 0.0, 1.0); - if (buf->fade_period > 0) buf->fade_period--; - } - data += buf->bytes_per_sample; - } -} -#endif - -#define NOT_PAUSED(pause) (pause != PAUSE_PAUSED && pause != PAUSE_PRE) - -// PAUSE_PAUSED means the last read was silence, meaning it's safe to skip. -// The sole purpose of PAUSE_PRE is to avoid a fade in at the start of the stream. +// PAUSE_PAUSED signifies that the last read was silence. Meaning we can skip +// around in the buffer without worrying about pops. size_t camu_audio_buffer_read(struct camu_audio_buffer *buf, u8 *data, size_t req) { 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 = PAUSE_FADING. - buf->volume = FADE; - buf->fade_period = FADE_LENGTH(buf->fade_rate); - buf->fade_offset = 0; buf->pause = PAUSE_FADING; - } else if (buf->fade_period == 0) { // Fade out done. + buf->fade_offset = 0; + } else if (buf->volume == 0.f) { // Fade out done. al_memset(data, 0, req); if (NOT_PAUSED(buf->pause)) { buf->pause = PAUSE_PAUSED; @@ -292,12 +243,6 @@ 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_PAUSED) { - // Fade in. Signified by >0 fade_period and pause = PAUSE_PLAYING. - buf->volume = 1.0 - FADE; - buf->fade_period = FADE_LENGTH(buf->fade_rate); - buf->fade_offset = 0; - // We don't want to consider sync if reversing an active - // fade but we do if pause = PAUSE_PAUSED. if (buf->pause == PAUSE_FADING) buf->pause = PAUSE_PLAYING; #else al_memset(data, 0, req); @@ -308,66 +253,87 @@ size_t camu_audio_buffer_read(struct camu_audio_buffer *buf, u8 *data, size_t re return req; #endif } - f64 pts = camu_clock_get_pts(buf->clock, camu_mixer_get_latency(buf->mixer)); + u8 flow = al_atomic_load(u8)(&buf->flow, AL_ATOMIC_ACQUIRE); + if (flow == SIGNALED) { + buf->callback(buf->userdata, CAMU_BUFFER_EOF); + return 0; + } + f64 pts = camu_clock_get_pts(buf->clock, buf->latency); size_t ret, signal = req; - size_t size = al_ring_buffer_occupied(&buf->rb); - // Cut off fade if it's reaching too far. - if (size < buf->fade_offset) size = 0; - else size -= buf->fade_offset; + size_t have = al_ring_buffer_occupied(&buf->rb); #ifdef CAMU_AUDIO_BUFFER_FADE + // Cut off fade if it's reaching too far. + if (have < buf->fade_offset) have = 0; + else have -= buf->fade_offset; if (buf->pause != PAUSE_FADING && buf->pause != PAUSE_PLAYING) { #endif pts -= buf->pts; - // Possibly attempt syncing to the clock. For this to work the mixer must - // report a reasonably accurate value for latency. - if (UNLIKELY(!buf->ignore_desync && (buf->pause == PAUSE_PRE || buf->pause == PAUSE_PAUSED))) { - if (UNLIKELY(fabs(pts) >= 0.322 && buf->pause != PAUSE_PRE)) { + // Attempt syncing to the clock. + // For this to work the mixer must report a reasonably accurate value for latency. + if (UNLIKELY(!buf->ignore_desync && buf->pause == PAUSE_PAUSED)) { + if (UNLIKELY(fabs(pts) >= LARGE_DESYNC_PTS)) { al_log_warn("audio_buffer", "Abnormally large audio desync of %.5fs", pts); } - if (pts > 0.0) { - ret = camu_ff_resample_fmt_sec_to_bytes(&buf->fmt, pts); - ret = AL_MIN(ret, size); - al_log_debug("audio_buffer", "Skipping %.5fs of audio (%zu bytes).", pts, ret); + if (pts > 0.0) { // Skip. + ret = camu_audio_format_sec_to_bytes(&buf->fmt.req, pts); + ret = AL_MIN(ret, have); + al_log_debug("audio_buffer", "Skipping %fs of audio (%zu bytes).", pts, ret); ret = al_ring_buffer_discard(&buf->rb, ret); - size -= ret; - buf->pts += camu_ff_resample_fmt_bytes_to_sec(&buf->fmt, ret); + have -= ret; + buf->pts += camu_audio_format_bytes_to_sec(&buf->fmt.req, ret); // Could go on to underrun. - } else if (pts < 0.0) { + } else if (pts < 0.0) { // Delay. pts = -pts; - ret = camu_ff_resample_fmt_sec_to_bytes(&buf->fmt, pts); + ret = camu_audio_format_sec_to_bytes(&buf->fmt.req, pts); ret = AL_MIN(ret, req); - al_log_debug("audio_buffer", "Delaying audio by %.5fs (%zu bytes).", pts, ret); + al_log_debug("audio_buffer", "Delaying audio by %fs (%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 (req == 0) return signal; } } if (buf->pause != PAUSE_PLAYING) buf->pause = PAUSE_PLAYING; #ifdef CAMU_AUDIO_BUFFER_FADE } #endif - u8 flow = al_atomic_u8_load(&buf->flow, AL_ATOMIC_RELAXED); - if (size < req) { // We don't have enough data to fulfill our request. - if (flow == FLUSHED) { - // If flow = FLUSHED, we know we've recieved all the data - // this stream wants to send. Signal that we are done. - signal = size; - buf->callback(buf->userdata, CAMU_BUFFER_EOF); - al_atomic_u8_store(&buf->flow, SIGNALED, AL_ATOMIC_RELAXED); - al_log_debug("audio_buffer", "Flushed (signal: %zu).", signal); + if (have < req) { // We don't have enough data to fulfill our request. + if (flow == FLUSHED) { // Stream is flushed. + // Check peak buffer for any remaining data. + ret = camu_peak_buffer_size(&buf->peak); + if (ret > 0) { + // This doesn't break single reader, single writer because + // pushing data after a flush is not allowed. + al_ring_buffer_write(&buf->rb, camu_peak_buffer_flush(&buf->peak), ret); + have += ret; + } + // Only signal EOF if what, if any, data from the peak buffer + // still isn't enough for this request. + if (have < req) { + buf->callback(buf->userdata, CAMU_BUFFER_EOF); + flow = SIGNALED; + al_atomic_store(u8)(&buf->flow, SIGNALED, AL_ATOMIC_RELEASE); + signal = have; + al_log_debug("audio_buffer", "Flushed (signal: %zu).", signal); + } } else { - // Put silence into the remainder of the request buffer. - al_memset(data + size, 0, req - size); + // Silence remainder of the request. + al_memset(data + have, 0, req - have); if (flow == FLOWING) { - // Our stream hit an underrun because data wasn't coming in fast - // enough. It's also possible to underrun if this function takes too - // long which isn't checked here. That is unlikely though. - al_log_warn("audio_buffer", "Underrun (req: %zu, have: %zu).", req, size); + // We hit an underrun because data wasn't coming in fast enough. + // An underrun can also happen in the audio output if read() (this function) + // takes too long. That isn't checked here. + al_log_warn("audio_buffer", "Underrun (req: %zu, have: %zu).", req, have); + // If we have data by the next read(), try to skip ahead to maintain sync. + // This might exacerbate the underrun issue but an underrun is already + // unexpected behavior, trying to stay in sync comes first. + buf->pause = PAUSE_PAUSED; } - // If flow = SIGNALED, we are just waiting to be removed or reset. + // If flow = SIGNALED, we are waiting to be removed or reset. } - req = size; + // The amount of available data could've increased in the flow = FLUSHED case. + req = AL_MIN(req, have); } if (req > 0) { #ifdef CAMU_AUDIO_BUFFER_FADE @@ -378,29 +344,38 @@ size_t camu_audio_buffer_read(struct camu_audio_buffer *buf, u8 *data, size_t re buf->fade_offset += ret; } else { #endif - // Read as much as we can from the ring buffer. + // Read as much as we determined we can. ret = al_ring_buffer_read(&buf->rb, data, req); - buf->pts += camu_ff_resample_fmt_bytes_to_sec(&buf->fmt, ret); - if (flow == FLOWING) { - ret = al_atomic_size_t_load(&buf->continue_mark, AL_ATOMIC_RELAXED); - // Request to uncork if we are below the mark. - if (ret && ((buf->size - size) - req) >= ret) { - buf->callback(buf->userdata, CAMU_BUFFER_UNCORK); - } - } + al_assert(ret == req); + buf->pts += camu_audio_format_bytes_to_sec(&buf->fmt.req, ret); #ifdef CAMU_AUDIO_BUFFER_FADE } - if (buf->fade_period > 0) handle_fade(buf, data, req, buf->pause == PAUSE_FADING); + f32 step = 0.f; + if (buf->pause == PAUSE_FADING) { + step = -FADE_STEP(&buf->fmt.req); + } else if (buf->volume < 1.f) { + step = FADE_STEP(&buf->fmt.req); + } + if (step != 0.f || buf->volume != 1.f) { + buf->volume = apply_volume(data, req, &buf->fmt.req, buf->volume, step); + } #endif } - // Never return less than req, unless we hit the end of the stream. + // Check if we should request to uncork. + if (flow == FLOWING) { + ret = al_atomic_load(size_t)(&buf->uncork_at, AL_ATOMIC_RELAXED); + if (ret && have - req <= ret) { + buf->callback(buf->userdata, CAMU_BUFFER_UNCORK); + } + } + // To signal EOF, return less then req. return signal; } void camu_audio_buffer_free(struct camu_audio_buffer *buf) { if (buf->fmt.resampler_needed) { - camu_ff_resampler_close(&buf->resamp); + buf->resamp->free(&buf->resamp); } if (buf->data) al_free(buf->data); camu_peak_buffer_free(&buf->peak); diff --git a/src/buffer/audio.h b/src/buffer/audio.h index c4ba142..3adccc7 100644 --- a/src/buffer/audio.h +++ b/src/buffer/audio.h @@ -4,45 +4,44 @@ #include <al/ring_buffer.h> #include "../codec/codec.h" -#include "../codec/ffmpeg/resampler.h" #include "../mixer/mixer.h" #include "clock.h" #include "peak_buffer.h" -//#define CAMU_AUDIO_BUFFER_FADE +#define CAMU_AUDIO_BUFFER_FADE struct camu_audio_buffer { - struct camu_stream *stream; + struct camu_codec_stream *stream; struct camu_mixer *mixer; f64 pts; u8 pause; - bool ignore_desync; struct camu_clock *clock; + bool ignore_desync; + f64 latency; - f64 volume; - s32 fade_period; - size_t fade_offset; - s32 fade_rate; - - s32 bytes_per_sample; - struct camu_ff_resampler resamp; - struct camu_ff_resample_fmt fmt; + struct camu_resampler_format fmt; + struct camu_resampler *resamp; u8 *data; size_t size; struct al_ring_buffer rb; - struct { size_t low, high; } watermark; + struct { size_t min, buffered; } mark; bool buffered; + atomic(u8) flow; + struct camu_peak_buffer peak; - atomic_size_t continue_mark; + atomic(size_t) uncork_at; - atomic_u8 flow; + f32 volume; +#ifdef CAMU_AUDIO_BUFFER_FADE + size_t fade_offset; +#endif #ifdef CAMU_MIXER_THREADED - atomic_bool ref; + atomic(u8) ref; #endif void (*callback)(void *, u8); @@ -50,8 +49,9 @@ struct camu_audio_buffer { }; bool camu_audio_buffer_init(struct camu_audio_buffer *buf, struct camu_clock *clock, struct camu_mixer *mixer); -bool camu_audio_buffer_configure(struct camu_audio_buffer *buf, struct camu_stream *stream); -void camu_audio_buffer_push(struct camu_audio_buffer *buf, struct camu_frame *frame); +bool camu_audio_buffer_configure(struct camu_audio_buffer *buf, struct camu_codec_stream *stream); +void camu_audio_buffer_set_latency(struct camu_audio_buffer *buf, f64 latency); +void camu_audio_buffer_push(struct camu_audio_buffer *buf, struct camu_codec_frame *frame); void camu_audio_buffer_unpause(struct camu_audio_buffer *buf); void camu_audio_buffer_reset(struct camu_audio_buffer *buf); void camu_audio_buffer_flush(struct camu_audio_buffer *buf); diff --git a/src/buffer/clock.c b/src/buffer/clock.c index d26e4a8..c4a07a6 100644 --- a/src/buffer/clock.c +++ b/src/buffer/clock.c @@ -1,116 +1,117 @@ #include <al/lib.h> +#include <aki/thread.h> #include "clock.h" -void camu_clock_set(struct camu_clock *clock, struct shrb_seek_req *req) -{ - al_atomic_f64_store(&clock->tick, 0.0, AL_ATOMIC_RELAXED); - if (req->paused) { - clock->pause_at = req->paused_at / 1000000.0; - al_atomic_f64_store(&clock->base, clock->pause_at, AL_ATOMIC_RELAXED); - al_atomic_u64_store(&clock->start, 0Lu, AL_ATOMIC_RELAXED); - } else { - clock->pause_at = -1.0; - al_atomic_f64_store(&clock->base, req->base / 1000000.0, AL_ATOMIC_RELAXED); - al_atomic_u64_store(&clock->start, req->ts + req->delay, AL_ATOMIC_RELAXED); - clock->ts = req->ts; - } - clock->offset = 0L; - clock->delay = req->delay; -} +#define RUNNING 0.0 +#define PAUSED -DBL_MAX +#define ENDED DBL_MAX -f64 calc_tick_internal(struct camu_clock *clock) +void camu_clock_init(struct camu_clock *clock, void (*callback)(void *, u8), void *userdata) { - s64 diff = clock->delay == 0Lu ? 0L - : ((al_atomic_u64_load(&clock->start, AL_ATOMIC_ACQUIRE) + clock->offset) - - aki_get_timestamp()); - f64 tick = aki_get_tick() - al_atomic_f64_load(&clock->base, AL_ATOMIC_RELAXED); - //al_printf("DIFFERENCE: %f\n", diff / 1000000.0); - tick += diff / 1000000.0; - al_atomic_f64_store(&clock->tick, tick, AL_ATOMIC_RELAXED); - al_atomic_u64_store(&clock->start, 0Lu, AL_ATOMIC_RELEASE); - return tick; + clock->callback = callback; + clock->userdata = userdata; } -bool camu_clock_is_late(struct camu_clock *clock) +static f64 calc_tick_offset(f64 tick, u64 now, u64 target) { - s64 diff = (s64)(al_atomic_u64_load(&clock->start, AL_ATOMIC_RELAXED)) - aki_get_timestamp(); - return clock->delay > 0L && diff < 0L; + if (now > target) return tick - (now - target) / 1000000.0; + else return tick + (target - now) / 1000000.0; } -void camu_clock_ready(struct camu_clock *clock) +void camu_clock_set(struct camu_clock *clock, f64 base) { - if (clock->pause_at != -1.0) { - clock->pause_at = -1.0; - return; - } - camu_clock_resume(clock); + clock->base = base; + clock->offset = 0.0; + al_atomic_store(f64)(&clock->tick, -1.0, AL_ATOMIC_RELAXED); + al_atomic_store(f64)(&clock->pause, PAUSED, AL_ATOMIC_RELAXED); + clock->paused_at = 0.0; } -void camu_clock_offset(struct camu_clock *clock, s64 offset) +void camu_clock_offset(struct camu_clock *clock, f64 amount) { - clock->offset += offset; - //al_printf("OFFSET: %f\n", clock->offset / 1000000.0); + clock->offset += amount; } -void camu_clock_resume(struct camu_clock *clock) +void camu_clock_pause(struct camu_clock *clock, u64 target) { - f64 tick = al_atomic_f64_load(&clock->tick, AL_ATOMIC_ACQUIRE); - if (tick != 0.0) return; - u64 start = al_atomic_u64_load(&clock->start, AL_ATOMIC_RELAXED); - if (start != 0Lu) { - // No longer report paused, calculate tick on first call to get_pts(). - al_atomic_f64_store(&clock->tick, -1.0, AL_ATOMIC_RELEASE); + f64 pause = al_atomic_load(f64)(&clock->pause, AL_ATOMIC_ACQUIRE); + if (pause == ENDED) return; + al_assert(clock->paused_at == -1.0); + f64 tick = aki_get_tick(); + if (target > 0) { + tick = calc_tick_offset(tick, aki_get_timestamp(), target); + al_atomic_store(f64)(&clock->pause, tick, AL_ATOMIC_RELEASE); } else { - tick = al_atomic_f64_load(&clock->base, AL_ATOMIC_RELAXED); - al_atomic_f64_store(&clock->tick, aki_get_tick() - tick, AL_ATOMIC_RELEASE); + al_atomic_store(f64)(&clock->pause, PAUSED, AL_ATOMIC_RELEASE); } - clock->pause_at = -1.0; + clock->paused_at = tick; } -void camu_clock_arm_resume(struct camu_clock *clock, u64 ts) +void camu_clock_resume(struct camu_clock *clock, u64 target) { - al_atomic_u64_store(&clock->start, ts, AL_ATOMIC_RELAXED); - al_atomic_f64_store(&clock->tick, -1.0, AL_ATOMIC_RELAXED); - clock->pause_at = -1.0; + f64 pause = al_atomic_load(f64)(&clock->pause, AL_ATOMIC_ACQUIRE); + if (pause == ENDED) return; + al_assert(clock->paused_at != -1.0); + f64 tick = aki_get_tick(); + //if (pause != PAUSED && tick < pause) { + // tick = pause; + //} else if (target > 0) { + if (target > 0) { + tick = calc_tick_offset(tick, aki_get_timestamp(), target); + al_printf("%f\n", tick); + } + if (clock->paused_at == 0.0) { + if (target == 0) { + // target = 0 can never be synced. + al_atomic_store(f64)(&clock->tick, -1.0, AL_ATOMIC_RELAXED); + } else { + al_atomic_store(f64)(&clock->tick, tick, AL_ATOMIC_RELAXED); + } + } else { + clock->offset += tick - clock->paused_at; + } + al_atomic_store(f64)(&clock->pause, RUNNING, AL_ATOMIC_RELEASE); + clock->paused_at = -1.0; } -void camu_clock_pause(struct camu_clock *clock, f64 base) +bool camu_clock_is_paused(struct camu_clock *clock) { - f64 tick = al_atomic_f64_load(&clock->tick, AL_ATOMIC_ACQUIRE); - if (tick == 0.0) return; - if (base == -1.0) base = aki_get_tick() - tick; - al_atomic_f64_store(&clock->tick, 0.0, AL_ATOMIC_RELEASE); - clock->pause_at = base; - al_atomic_f64_store(&clock->base, clock->pause_at, AL_ATOMIC_RELAXED); + return al_atomic_load(f64)(&clock->pause, AL_ATOMIC_RELAXED) == PAUSED; } -void camu_clock_arm_pause(struct camu_clock *clock, f64 pts) +void camu_clock_end(struct camu_clock *clock) { - clock->pause_at = pts; + al_atomic_store(f64)(&clock->pause, ENDED, AL_ATOMIC_RELAXED); } -bool camu_clock_is_paused(struct camu_clock *clock) +bool camu_clock_is_ended(struct camu_clock *clock) { - f64 tick = al_atomic_f64_load(&clock->tick, AL_ATOMIC_RELAXED); - if (tick == 0.0) return true; - if (clock->pause_at >= 0.0 && aki_get_tick() - tick >= clock->pause_at) { - al_atomic_f64_store(&clock->base, clock->pause_at, AL_ATOMIC_RELAXED); - al_atomic_f64_store(&clock->tick, 0.0, AL_ATOMIC_RELAXED); - return true; - } - return false; + return al_atomic_load(f64)(&clock->pause, AL_ATOMIC_RELAXED) == ENDED; } f64 camu_clock_get_base_pts(struct camu_clock *clock) { - return al_atomic_f64_load(&clock->base, AL_ATOMIC_RELAXED); + return clock->base; } f64 camu_clock_get_pts(struct camu_clock *clock, f64 offset) { - f64 tick = al_atomic_f64_load(&clock->tick, AL_ATOMIC_RELAXED); - if (tick == -1.0) tick = calc_tick_internal(clock); - f64 pts = (aki_get_tick() - tick) + offset; - return pts; + f64 pause = al_atomic_load(f64)(&clock->pause, AL_ATOMIC_ACQUIRE); + if (pause == PAUSED || pause == ENDED) return -1.0; + f64 current = aki_get_tick(); + f64 tick = al_atomic_load(f64)(&clock->tick, AL_ATOMIC_RELAXED); + if (tick == -1.0) { + tick = al_atomic_compare_and_swap(f64)(&clock->tick, -1.0, current); + if (tick == -1.0) tick = current; + } + // pause > 0.0 means running or pause armed. + if (pause > 0.0 && current > pause) { + current = pause; + pause = al_atomic_compare_and_swap(f64)(&clock->pause, pause, PAUSED); + if (pause != PAUSED) { + clock->callback(clock->userdata, CAMU_CLOCK_PAUSED); + } + } + return clock->base + (current - (tick + clock->offset)) + offset; } diff --git a/src/buffer/clock.h b/src/buffer/clock.h index 6f505be..17edb0d 100644 --- a/src/buffer/clock.h +++ b/src/buffer/clock.h @@ -1,27 +1,51 @@ #pragma once -#include <aki/thread.h> +#include <al/types.h> #include <al/atomic.h> -#include "../shrub/handler.h" +// Synced: +// () = other clients only +// Pause: +// - User input -> start fade and request pause from server +// - Get response -> (start fade) arm pause or reseek if late +// Resume: +// - User input -> request resume from server +// - Get response -> arm resume, apply offset to queued +// Next/Prev: +// - User input -> start fade request next from server +// - Get response -> (start fade) arm pause, arm resume on next, then swap on pause +// +// Local: +// Pause: +// - User input -> start fade and pause immediately +// Reumse: +// - User input -> resume and fade in +// Next/Prev: +// - User input -> swap + +enum { + CAMU_CLOCK_PAUSED = 0 +}; struct camu_clock { - atomic_f64 base, tick; - atomic_u64 start; - u64 ts; - s64 offset; - u64 delay; - f64 pause_at; + f64 base; + f64 offset; + atomic(f64) tick; + atomic(f64) pause; + f64 paused_at; + void (*callback)(void *, u8); + void *userdata; }; -void camu_clock_set(struct camu_clock *clock, struct shrb_seek_req *req); -bool camu_clock_is_late(struct camu_clock *clock); -void camu_clock_ready(struct camu_clock *clock); -void camu_clock_offset(struct camu_clock *clock, s64 offset); -void camu_clock_resume(struct camu_clock *clock); -void camu_clock_arm_resume(struct camu_clock *clock, u64 ts); -void camu_clock_pause(struct camu_clock *clock, f64 base); -void camu_clock_arm_pause(struct camu_clock *clock, f64 pts); +void camu_clock_init(struct camu_clock *clock, void (*callback)(void *, u8), void *userdata); +void camu_clock_set(struct camu_clock *clock, f64 base); +void camu_clock_offset(struct camu_clock *clock, f64 amount); + +void camu_clock_pause(struct camu_clock *clock, u64 ts); +void camu_clock_resume(struct camu_clock *clock, u64 ts); bool camu_clock_is_paused(struct camu_clock *clock); +void camu_clock_end(struct camu_clock *clock); +bool camu_clock_is_ended(struct camu_clock *clock); + f64 camu_clock_get_base_pts(struct camu_clock *clock); f64 camu_clock_get_pts(struct camu_clock *clock, f64 offset); diff --git a/src/buffer/frame_queue.h b/src/buffer/frame_queue.h index 512b7f2..0175668 100644 --- a/src/buffer/frame_queue.h +++ b/src/buffer/frame_queue.h @@ -14,7 +14,7 @@ enum { }; struct camu_frame_queue { - void (*push)(struct camu_frame_queue *, struct camu_frame *, f64); + void (*push)(struct camu_frame_queue *, struct camu_codec_frame *, f64); #ifdef CAMU_HAVE_FFMPEG void (*push_av_frame)(struct camu_frame_queue *, AVFrame *, f64); #endif diff --git a/src/buffer/peak_buffer.c b/src/buffer/peak_buffer.c index 2856a78..005e77f 100644 --- a/src/buffer/peak_buffer.c +++ b/src/buffer/peak_buffer.c @@ -2,10 +2,10 @@ // https://github.com/MusicPlayerDaemon/MPD/blob/c71e586c530e1d066efde7c2ca40c363f74100c7/src/util/PeakBuffer.cxx -void camu_peak_buffer_init(struct camu_peak_buffer *buf) +void camu_peak_buffer_init(struct camu_peak_buffer *buf, size_t size) { aki_buffer_init(&buf->buf); - aki_buffer_ensure_space(&buf->buf, 128 * 1024); + aki_buffer_ensure_space(&buf->buf, size); } void camu_peak_buffer_push(struct camu_peak_buffer *buf, u8 *data, size_t size) @@ -13,9 +13,13 @@ void camu_peak_buffer_push(struct camu_peak_buffer *buf, u8 *data, size_t size) aki_buffer_append(&buf->buf, data, size); } -u8 *camu_peak_buffer_flush(struct camu_peak_buffer *buf, size_t *size) +size_t camu_peak_buffer_size(struct camu_peak_buffer *buf) +{ + return aki_buffer_get_size(&buf->buf); +} + +u8 *camu_peak_buffer_flush(struct camu_peak_buffer *buf) { - *size = aki_buffer_get_size(&buf->buf); aki_buffer_set_size(&buf->buf, 0); return aki_buffer_get_ptr(&buf->buf, 0); } diff --git a/src/buffer/peak_buffer.h b/src/buffer/peak_buffer.h index b8d9934..c518ade 100644 --- a/src/buffer/peak_buffer.h +++ b/src/buffer/peak_buffer.h @@ -7,7 +7,8 @@ struct camu_peak_buffer { struct aki_buffer buf; }; -void camu_peak_buffer_init(struct camu_peak_buffer *buf); +void camu_peak_buffer_init(struct camu_peak_buffer *buf, size_t size); void camu_peak_buffer_push(struct camu_peak_buffer *buf, u8 *data, size_t size); -u8 *camu_peak_buffer_flush(struct camu_peak_buffer *buf, size_t *size); +size_t camu_peak_buffer_size(struct camu_peak_buffer *buf); +u8 *camu_peak_buffer_flush(struct camu_peak_buffer *buf); void camu_peak_buffer_free(struct camu_peak_buffer *buf); diff --git a/src/buffer/video.c b/src/buffer/video.c index 3b9be20..b2381b2 100644 --- a/src/buffer/video.c +++ b/src/buffer/video.c @@ -4,35 +4,39 @@ #include "common.h" #include "common_internal.h" -//#define CAMU_VIDEO_BUFFER_FORCE_SCALER - #define BUFFER_WATERMARK_LOW 8 // frames. -#define BUFFER_WATERMARK_BUFFERED 8 -#define BUFFER_WATERMARK_HIGH 14 +#define BUFFER_WATERMARK_BUFFERED 5 +#define BUFFER_WATERMARK_HIGH 12 #define BUFFER_WATERMARK_RESET BUFFER_WATERMARK_HIGH + 10. -bool camu_video_buffer_init(struct camu_video_buffer *buf, struct camu_clock *clock, f64 offset, - struct camu_renderer *renderer) +bool camu_video_buffer_init(struct camu_video_buffer *buf, struct camu_clock *clock, struct camu_renderer *renderer) { buf->clock = clock; - buf->offset = offset; + buf->latency = 0.0; + buf->pts = -1.0; + // Defaulting single_frame to true should simplify non-configured buffers in sink. + buf->single_frame = true; buf->queue = renderer->create_queue(renderer); buf->buffered = false; - al_atomic_u8_store(&buf->flow, FLOWING, AL_ATOMIC_RELAXED); + al_atomic_store(u8)(&buf->flow, FLOWING, AL_ATOMIC_RELAXED); #ifdef CAMU_SCREEN_THREADED - al_atomic_bool_store(&buf->ref, false, AL_ATOMIC_RELAXED); + al_atomic_store(u8)(&buf->ref, 0, AL_ATOMIC_RELAXED); #endif buf->view.mode = CAMU_VIEW_NONE; return true; } -bool camu_video_buffer_configure(struct camu_video_buffer *buf, struct camu_stream *stream) +bool camu_video_buffer_configure(struct camu_video_buffer *buf, struct camu_codec_stream *stream) { buf->stream = stream; - switch (stream->type) { + switch (stream->mode) { case CAMU_NORMAL: { + s32 width = buf->stream->video.width; + s32 height = buf->stream->video.height; buf->single_frame = true; buf->avg_frame_duration = 0.0; + const char *format_name = camu_pixel_format_name(buf->stream->video.format); + al_log_info("video_buffer", "Stream: %s (%dx%d) %s.", format_name, width, height, "IMAGE"); break; } #ifdef CAMU_HAVE_FFMPEG @@ -44,8 +48,8 @@ bool camu_video_buffer_configure(struct camu_video_buffer *buf, struct camu_stre 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; - AVRational frame_rate = buf->single_frame ? (AVRational){ 0, 1 } : - stream->av.stream->avg_frame_rate; + 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, @@ -56,7 +60,7 @@ bool camu_video_buffer_configure(struct camu_video_buffer *buf, struct camu_stre buf->fmt.req_format = AV_PIX_FMT_RGBA; if (camu_ff_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, + al_log_info("video_buffer", "Scaling to: %s (%dx%d)", format_name, buf->fmt.req_width, buf->fmt.req_height); } else { return false; @@ -69,6 +73,11 @@ bool camu_video_buffer_configure(struct camu_video_buffer *buf, struct camu_stre return true; } +void camu_video_buffer_set_latency(struct camu_video_buffer *buf, f64 latency) +{ + buf->latency = latency; +} + bool camu_video_buffer_is_single_frame(struct camu_video_buffer *buf) { return buf->single_frame; @@ -78,12 +87,12 @@ static void after_push_internal(struct camu_video_buffer *buf) { s32 count = buf->queue->count(buf->queue); if (!buf->buffered && (buf->single_frame || count >= BUFFER_WATERMARK_BUFFERED)) { + al_log_debug("video_buffer", "Buffered (watermark: %d frames).", count); // Preserve order of: flush -> callback -> set flow, for single frames. if (buf->single_frame) buf->queue->flush(buf->queue); buf->callback(buf->userdata, CAMU_BUFFER_BUFFERED); buf->buffered = true; - al_log_debug("video_buffer", "Buffered (watermark: %d frames).", count); - if (buf->single_frame) al_atomic_u8_store(&buf->flow, FLUSHED, AL_ATOMIC_RELAXED); + if (buf->single_frame) al_atomic_store(u8)(&buf->flow, FLUSHED, AL_ATOMIC_RELAXED); } if (count >= BUFFER_WATERMARK_RESET) buf->queue->reset(buf->queue); else if (count >= BUFFER_WATERMARK_HIGH) buf->callback(buf->userdata, CAMU_BUFFER_CORK); @@ -95,12 +104,13 @@ static void push_av_frame_internal(struct camu_video_buffer *buf, AVFrame *frame 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); + pts = frame->best_effort_timestamp * av_q2d(stream->time_base); if (pts + buf->avg_frame_duration < camu_clock_get_base_pts(buf->clock)) { av_frame_free(&frame); return; } } + if (buf->pts == -1.0) buf->pts = pts; #ifdef CAMU_VIDEO_BUFFER_FORCE_SCALER if (buf->fmt.scaler_needed) { if (!camu_ff_scaler_scale(&buf->scale, (const u8 **)frame->data, frame->linesize)) { @@ -115,14 +125,14 @@ static void push_av_frame_internal(struct camu_video_buffer *buf, AVFrame *frame } #endif -void camu_video_buffer_push(struct camu_video_buffer *buf, struct camu_frame *frame) +void camu_video_buffer_push(struct camu_video_buffer *buf, struct camu_codec_frame *frame) { // A single frame will be sent again after a seek, discard it here (for now). if (buf->single_frame && buf->buffered) { - camu_frame_discard(frame); + camu_codec_frame_discard(frame); return; } - switch (frame->type) { + switch (frame->mode) { case CAMU_NORMAL: buf->queue->push(buf->queue, frame, 0.0); break; @@ -141,7 +151,7 @@ void camu_video_buffer_reset(struct camu_video_buffer *buf) { buf->queue->reset(buf->queue); buf->buffered = false; - al_atomic_u8_store(&buf->flow, FLOWING, AL_ATOMIC_RELAXED); + al_atomic_store(u8)(&buf->flow, FLOWING, AL_ATOMIC_RELAXED); } // flush() always comes from the same thread as push(). @@ -149,26 +159,28 @@ void camu_video_buffer_flush(struct camu_video_buffer *buf) { buf->queue->flush(buf->queue); if (!buf->buffered) { + al_log_debug("video_buffer", "Buffered (watermark: %d frames).", buf->queue->count(buf->queue)); buf->callback(buf->userdata, CAMU_BUFFER_BUFFERED); buf->buffered = true; - al_log_debug("video_buffer", "Buffered (watermark: %d frames).", buf->queue->count(buf->queue)); } - al_atomic_u8_store(&buf->flow, FLUSHED, AL_ATOMIC_RELAXED); + al_atomic_store(u8)(&buf->flow, FLUSHED, AL_ATOMIC_RELAXED); } bool camu_video_buffer_read(struct camu_video_buffer *buf, void *out) { - bool paused = buf->single_frame || camu_clock_is_paused(buf->clock); - f64 base = camu_clock_get_base_pts(buf->clock); - if (!paused) { - f64 pts = camu_clock_get_pts(buf->clock, buf->offset); - if (pts > base) base = pts; + if (!buf->single_frame) { + f64 pts = camu_clock_get_pts(buf->clock, buf->latency); + if (pts > buf->pts) buf->pts = pts; + } + u8 flow = al_atomic_load(u8)(&buf->flow, AL_ATOMIC_RELAXED); + if (flow == SIGNALED && !buf->single_frame) { + buf->callback(buf->userdata, CAMU_BUFFER_EOF); + return false; } - u8 flow = al_atomic_u8_load(&buf->flow, AL_ATOMIC_RELAXED); - u8 ret = buf->queue->read(buf->queue, base, out); + u8 ret = buf->queue->read(buf->queue, buf->pts, out); if (flow == FLUSHED && (ret == CAMU_QUEUE_EOF || (buf->single_frame && ret == CAMU_QUEUE_OK))) { buf->callback(buf->userdata, CAMU_BUFFER_EOF); - al_atomic_u8_store(&buf->flow, SIGNALED, AL_ATOMIC_RELAXED); + al_atomic_store(u8)(&buf->flow, SIGNALED, AL_ATOMIC_RELAXED); al_log_debug("video_buffer", "Flushed."); } else if (flow == FLOWING && buf->queue->count(buf->queue) <= BUFFER_WATERMARK_LOW) { buf->callback(buf->userdata, CAMU_BUFFER_UNCORK); diff --git a/src/buffer/video.h b/src/buffer/video.h index 5e18b33..0e8bfff 100644 --- a/src/buffer/video.h +++ b/src/buffer/video.h @@ -1,36 +1,43 @@ #pragma once +//#define CAMU_VIDEO_BUFFER_FORCE_SCALER + #include <al/types.h> #include "../codec/codec.h" -#include "../codec/ffmpeg/scaler.h" #include "../render/renderer.h" #include "../screen/screen.h" #include "../screen/view.h" +#ifdef CAMU_VIDEO_BUFFER_FORCE_SCALER +#endif +#include "../codec/ffmpeg/scaler.h" #include "clock.h" #include "frame_queue.h" struct camu_video_buffer { - struct camu_stream *stream; + struct camu_codec_stream *stream; struct camu_clock *clock; - f32 start_time; - f32 avg_frame_duration; - f64 offset; + f64 latency; + f64 pts; bool single_frame; + f32 avg_frame_duration; +#ifdef CAMU_VIDEO_BUFFER_FORCE_SCALER struct camu_ff_scaler scale; +#endif + // TODO: Terrible struct camu_ff_scale_fmt fmt; struct camu_frame_queue *queue; bool buffered; - atomic_u8 flow; + atomic(u8) flow; #ifdef CAMU_SCREEN_THREADED - atomic_bool ref; + atomic(u8) ref; #endif struct camu_view view; @@ -38,11 +45,11 @@ struct camu_video_buffer { void *userdata; }; -bool camu_video_buffer_init(struct camu_video_buffer *buf, struct camu_clock *clock, f64 offset, - struct camu_renderer *renderer); -bool camu_video_buffer_configure(struct camu_video_buffer *buf, struct camu_stream *stream); +bool camu_video_buffer_init(struct camu_video_buffer *buf, struct camu_clock *clock, struct camu_renderer *renderer); +bool camu_video_buffer_configure(struct camu_video_buffer *buf, struct camu_codec_stream *stream); +void camu_video_buffer_set_latency(struct camu_video_buffer *buf, f64 latency); bool camu_video_buffer_is_single_frame(struct camu_video_buffer *buf); -void camu_video_buffer_push(struct camu_video_buffer *buf, struct camu_frame *frame); +void camu_video_buffer_push(struct camu_video_buffer *buf, struct camu_codec_frame *frame); void camu_video_buffer_reset(struct camu_video_buffer *buf); void camu_video_buffer_flush(struct camu_video_buffer *buf); bool camu_video_buffer_read(struct camu_video_buffer *buf, void *out); diff --git a/src/buffer/volume.h b/src/buffer/volume.h new file mode 100644 index 0000000..f785eb9 --- /dev/null +++ b/src/buffer/volume.h @@ -0,0 +1,36 @@ +#include "../codec/codec.h" + +AL_UNUSED_FUNCTION_PUSH + +static inline f32 apply_audio_f32(f32 *data, size_t sample_count, s32 channel_count, f32 volume, f32 step) +{ + for (u32 i = 0; i < sample_count; i++) { + if ((i + 1) % channel_count == 0) { + volume = AL_CLAMP(volume + step, 0.f, 1.f); + } + data[i] *= volume; + } + return volume; +} + +static inline f32 apply_volume(u8 *data, size_t size, struct camu_audio_format *fmt, f32 volume, f32 step) +{ + size_t sample_count; + switch (camu_audio_format_bytes_per_sample(fmt)) { + case 4: + sample_count = size / 4; + if (fmt->format == CAMU_SAMPLE_FORMAT_S32) { + } else if (fmt->format == CAMU_SAMPLE_FORMAT_FLT) { + return apply_audio_f32((f32 *)data, sample_count, fmt->channel_count, volume, step); + } + break; + case 2: + sample_count = size / 2; + if (fmt->format == CAMU_SAMPLE_FORMAT_S16) { + } + break; + } + return volume; +} + +AL_UNUSED_FUNCTION_POP diff --git a/src/cache/backing.h b/src/cache/backing.h index 24aff44..d9d8bad 100644 --- a/src/cache/backing.h +++ b/src/cache/backing.h @@ -1,19 +1,26 @@ #pragma once #include <al/types.h> +#include <al/array.h> enum { - CCH_BACKING_READ = 0, - CCH_BACKING_MAPPED + CACHE_BACKING_READ = 0, + CACHE_BACKING_MAPPED +}; + +struct cch_range { + off_t start; + off_t end; }; struct cch_backing { u8 mode; + array(struct cch_range) available; + void (*lock)(struct cch_backing *); void (*write)(struct cch_backing *, u8 *, off_t, size_t *); void (*read)(struct cch_backing *, u8 *, off_t, size_t *); u8 *(*get_ptr)(struct cch_backing *, off_t, size_t *); void (*unlock)(struct cch_backing *); - void *(*get_mmap)(struct cch_backing *); off_t (*get_size_estimate)(struct cch_backing *); void (*resize)(struct cch_backing *, size_t); void (*free)(struct cch_backing **); diff --git a/src/cache/backings/file.c b/src/cache/backings/file.c index 4c5f344..153b1e4 100644 --- a/src/cache/backings/file.c +++ b/src/cache/backings/file.c @@ -1,6 +1,14 @@ +#include "../range.h" + #include "file.h" #include "file_common.c" +static void file_backing_lock(struct cch_backing *backing) +{ + struct cch_backing_file *file = (struct cch_backing_file *)backing; + aki_mutex_lock(&file->mutex); +} + static void file_backing_write(struct cch_backing *backing, u8 *buf, off_t index, size_t *size) { struct cch_backing_file *file = (struct cch_backing_file *)backing; @@ -10,10 +18,11 @@ static void file_backing_write(struct cch_backing *backing, u8 *buf, off_t index al_assert(file->u.pointer == index); } if (file->size >= 0 && index + ((off_t)*size) >= file->size) { - *size = file->size - index; + *size = AL_MAX(file->size - index, 0); } aki_file_write(&file->file, buf, *size); file->u.pointer += *size; + cch_backing_fill_range(&file->backing, index, *size); aki_mutex_unlock(&file->mutex); } @@ -26,13 +35,19 @@ static void file_backing_read(struct cch_backing *backing, u8 *buf, off_t index, al_assert(file->u.pointer == index); } if (file->size >= 0 && index + ((off_t)*size) >= file->size) { - *size = file->size - index; + *size = AL_MAX(file->size - index, 0); } aki_file_read(&file->file, buf, *size); file->u.pointer += *size; aki_mutex_unlock(&file->mutex); } +static void file_backing_unlock(struct cch_backing *backing) +{ + struct cch_backing_file *file = (struct cch_backing_file *)backing; + aki_mutex_unlock(&file->mutex); +} + static void file_backing_resize(struct cch_backing *backing, size_t size) { struct cch_backing_file *file = (struct cch_backing_file *)backing; @@ -49,6 +64,7 @@ static void file_backing_free(struct cch_backing **backing) { struct cch_backing_file *file = (struct cch_backing_file *)*backing; aki_file_close(&file->file); + al_array_free(file->backing.available); al_free(file); *backing = NULL; } @@ -56,15 +72,16 @@ static void file_backing_free(struct cch_backing **backing) struct cch_backing *cch_backing_file_create(str *path, size_t size) { struct cch_backing_file *file = al_alloc_object(struct cch_backing_file); - file->backing.mode = CCH_BACKING_READ; + file->backing.mode = CACHE_BACKING_READ; + file->backing.lock = file_backing_lock; file->backing.write = file_backing_write; file->backing.read = file_backing_read; file->backing.get_ptr = NULL; - file->backing.unlock = NULL; - file->backing.get_mmap = NULL; + file->backing.unlock = file_backing_unlock; file->backing.get_size_estimate = file_backing_get_size_estimate; file->backing.resize = file_backing_resize; file->backing.free = file_backing_free; + al_array_init(file->backing.available); if (!file_open_internal(file, path, size)) { al_free(file); return NULL; diff --git a/src/cache/backings/file.h b/src/cache/backings/file.h index 9794bbb..3f8437b 100644 --- a/src/cache/backings/file.h +++ b/src/cache/backings/file.h @@ -13,8 +13,8 @@ struct cch_backing_file { off_t size; off_t filesize; union { - void *map; // CCH_BACKING_MAPPED - off_t pointer; // CCH_BACKING_READ + void *map; // CACHE_BACKING_MAPPED + off_t pointer; // CACHE_BACKING_READ } u; struct aki_mutex mutex; }; diff --git a/src/cache/backings/file_common.c b/src/cache/backings/file_common.c index 232580a..35322fd 100644 --- a/src/cache/backings/file_common.c +++ b/src/cache/backings/file_common.c @@ -14,6 +14,7 @@ static bool file_open_internal(struct cch_backing_file *file, str *path, size_t file->filesize = aki_file_get_filesize(&file->file); if (!size) { file->size = file->filesize; + cch_backing_fill_range(&file->backing, 0, file->size); } else { file->size = -1; if (file->filesize < (off_t)size) { diff --git a/src/cache/backings/file_mapped.c b/src/cache/backings/file_mapped.c index 4277c21..6c6fc94 100644 --- a/src/cache/backings/file_mapped.c +++ b/src/cache/backings/file_mapped.c @@ -1,14 +1,23 @@ +#include "../range.h" + #include "file.h" #include "file_common.c" +static void file_backing_lock(struct cch_backing *backing) +{ + struct cch_backing_file *file = (struct cch_backing_file *)backing; + aki_mutex_lock(&file->mutex); +} + static void file_backing_write(struct cch_backing *backing, u8 *buf, off_t index, size_t *size) { struct cch_backing_file *file = (struct cch_backing_file *)backing; aki_mutex_lock(&file->mutex); - if (file->size >= 0 && index + ((off_t)*size) >= file->size) { - *size = file->size - index; + if (file->size >= 0 && index + (off_t)*size >= file->size) { + *size = AL_MAX(file->size - index, 0); } al_memcpy(file->u.map + index, buf, *size); + cch_backing_fill_range(&file->backing, index, *size); aki_mutex_unlock(&file->mutex); } @@ -16,8 +25,8 @@ static u8 *file_backing_get_ptr(struct cch_backing *backing, off_t index, size_t { struct cch_backing_file *file = (struct cch_backing_file *)backing; aki_mutex_lock(&file->mutex); - if (file->size >= 0 && index + ((off_t)*size) >= file->size) { - *size = file->size - index; + if (file->size >= 0 && index + (off_t)*size >= file->size) { + *size = AL_MAX(file->size - index, 0); } return (u8 *)(file->u.map + index); } @@ -28,24 +37,15 @@ static void file_backing_unlock(struct cch_backing *backing) aki_mutex_unlock(&file->mutex); } -static void *file_backing_get_mmap(struct cch_backing *backing) -{ - struct cch_backing_file *file = (struct cch_backing_file *)backing; - aki_mutex_lock(&file->mutex); - void *map = file->u.map; - aki_mutex_unlock(&file->mutex); - return map; -} - static void file_backing_resize(struct cch_backing *backing, size_t size) { struct cch_backing_file *file = (struct cch_backing_file *)backing; aki_mutex_lock(&file->mutex); file->size = size; if ((off_t)size > file->filesize) { - // TODO: Unmap, truncate, map? - file->u.map = aki_file_mremap(&file->file, size, file->u.map); + aki_file_munmap(&file->file, file->u.map); aki_file_truncate(&file->file, size); + file->u.map = aki_file_mmap(&file->file); file->filesize = size; } aki_mutex_unlock(&file->mutex); @@ -56,6 +56,7 @@ static void file_backing_free(struct cch_backing **backing) struct cch_backing_file *file = (struct cch_backing_file *)*backing; aki_file_munmap(&file->file, file->u.map); aki_file_close(&file->file); + al_array_free(file->backing.available); al_free(file); *backing = NULL; } @@ -63,20 +64,20 @@ static void file_backing_free(struct cch_backing **backing) struct cch_backing *cch_backing_file_create(str *path, size_t size) { struct cch_backing_file *file = al_alloc_object(struct cch_backing_file); - file->backing.mode = CCH_BACKING_MAPPED; + file->backing.mode = CACHE_BACKING_MAPPED; + file->backing.lock = file_backing_lock; file->backing.write = file_backing_write; file->backing.read = NULL; - file->backing.get_mmap = file_backing_get_mmap; file->backing.get_ptr = file_backing_get_ptr; file->backing.unlock = file_backing_unlock; file->backing.get_size_estimate = file_backing_get_size_estimate; file->backing.resize = file_backing_resize; file->backing.free = file_backing_free; - if (!file_open_internal(file, path, size)) { + al_array_init(file->backing.available); + if (!file_open_internal(file, path, size) || !(file->u.map = aki_file_mmap(&file->file))) { al_free(file); return NULL; } - file->u.map = aki_file_mmap(&file->file); aki_mutex_init(&file->mutex); return (struct cch_backing *)file; } diff --git a/src/cache/backings/memory.c b/src/cache/backings/memory.c index 194d66c..1dca8f9 100644 --- a/src/cache/backings/memory.c +++ b/src/cache/backings/memory.c @@ -1,22 +1,31 @@ +#include "../range.h" + #include "memory.h" static void ensure_alloced(struct cch_backing_memory *mem, size_t size) { if (size > mem->alloc) { - mem->data = al_realloc(mem->data, size); - mem->alloc = size; + mem->alloc = al_next_power_of_two(size); + mem->data = al_realloc(mem->data, mem->alloc); } } +static void memory_backing_lock(struct cch_backing *backing) +{ + struct cch_backing_memory *mem = (struct cch_backing_memory *)backing; + aki_mutex_lock(&mem->mutex); +} + static void memory_backing_write(struct cch_backing *backing, u8 *buf, off_t index, size_t *size) { struct cch_backing_memory *mem = (struct cch_backing_memory *)backing; aki_mutex_lock(&mem->mutex); - if (mem->size >= 0 && index + ((off_t)*size) >= mem->size) { - *size = mem->size - index; + if (mem->size >= 0 && index + (off_t)*size >= mem->size) { + *size = AL_MAX(mem->size - index, 0); } ensure_alloced(mem, index + *size); al_memcpy(mem->data + index, buf, *size); + cch_backing_fill_range(&mem->backing, index, *size); aki_mutex_unlock(&mem->mutex); } @@ -24,8 +33,8 @@ static u8 *memory_backing_get_ptr(struct cch_backing *backing, off_t index, size { struct cch_backing_memory *mem = (struct cch_backing_memory *)backing; aki_mutex_lock(&mem->mutex); - if (mem->size >= 0 && index + ((off_t)*size) >= mem->size) { - *size = mem->size - index; + if (mem->size >= 0 && index + (off_t)*size >= mem->size) { + *size = AL_MAX(mem->size - index, 0); } return (u8 *)(mem->data + index); } @@ -59,6 +68,7 @@ static void memory_backing_free(struct cch_backing **backing) struct cch_backing_memory *mem = (struct cch_backing_memory *)*backing; al_free(mem->data); aki_mutex_destroy(&mem->mutex); + al_array_free(mem->backing.available); al_free(mem); *backing = NULL; } @@ -66,9 +76,9 @@ static void memory_backing_free(struct cch_backing **backing) struct cch_backing *cch_backing_memory_create(size_t size) { struct cch_backing_memory *mem = al_alloc_object(struct cch_backing_memory); - mem->backing.mode = CCH_BACKING_MAPPED; + mem->backing.mode = CACHE_BACKING_MAPPED; + mem->backing.lock = memory_backing_lock; mem->backing.write = memory_backing_write; - mem->backing.get_mmap = NULL; mem->backing.get_ptr = memory_backing_get_ptr; mem->backing.unlock = memory_backing_unlock; mem->backing.get_size_estimate = memory_backing_get_size_estimate; @@ -78,5 +88,6 @@ struct cch_backing *cch_backing_memory_create(size_t size) mem->alloc = size; mem->size = -1; aki_mutex_init(&mem->mutex); + al_array_init(mem->backing.available); return (struct cch_backing *)mem; } diff --git a/src/cache/entry.c b/src/cache/entry.c index 04e8f92..8068397 100644 --- a/src/cache/entry.c +++ b/src/cache/entry.c @@ -16,13 +16,26 @@ bool cch_entry_get_handle(struct cch_entry *entry, struct cch_handle *handle) return true; } +str *cch_entry_get_liana(struct cch_entry *entry) +{ + return &entry->handler->liana; +} + void cch_entry_set_size(struct cch_entry *entry, off_t filesize) { - return entry->backing->resize(entry->backing, filesize); + if (filesize == 0) { + // The backing will keep size = -1 internally. + entry->unknown_size = true; + return; + } + entry->backing->resize(entry->backing, filesize); } off_t cch_entry_get_size(struct cch_entry *entry) { + if (entry->unknown_size) { + return 0; + } return entry->backing->get_size_estimate(entry->backing); } diff --git a/src/cache/entry.h b/src/cache/entry.h index 305fb1f..3196809 100644 --- a/src/cache/entry.h +++ b/src/cache/entry.h @@ -6,15 +6,24 @@ #include "backing.h" #include "handler.h" +struct cch_chapter { + off_t start; + off_t end; +}; + struct cch_entry { u32 hash; s32 ref_count; + bool unknown_size; + array(struct cch_chapter) chapters; + struct cch_chapter *chapter; struct cch_backing *backing; struct cch_handler *handler; struct aki_mutex mutex; }; bool cch_entry_get_handle(struct cch_entry *entry, struct cch_handle *handle); +str *cch_entry_get_liana(struct cch_entry *entry); void cch_entry_set_size(struct cch_entry *entry, off_t filesize); off_t cch_entry_get_size(struct cch_entry *entry); void cch_entry_return_handle(struct cch_entry *entry, struct cch_handle *handle); diff --git a/src/cache/handle.c b/src/cache/handle.c index 9dfaf70..5bc868c 100644 --- a/src/cache/handle.c +++ b/src/cache/handle.c @@ -6,26 +6,32 @@ #include "handle.h" #include "entry.h" +#include "threaded_waits.h" -static bool wait_for_size(struct cch_handle *handle, struct cch_handler *handler) +static off_t wait_for_size(struct cch_handle *handle, struct cch_handler *handler) { - handle->wait.id = al_rand_u16(); handle->wait.end = -1; - return handler->wait_for_range(handler, &handle->wait); + if (!handler->wait_for_range(handler, &handle->wait)) { + return -1; + } + return cch_entry_get_size(handler->entry); } s32 cch_handle_read(struct cch_handle *handle, u8 *buf, s32 size) { struct cch_handler *handler = handle->entry->handler; - wait_for_size(handle, handler); + // filesize < 0: Size is yet to be evaluated and we can wait on it. + // filesize = 0: Size is explicitly unknown. off_t filesize = cch_entry_get_size(handle->entry); - if (filesize >= 0) { + if (size < 0) { + filesize = wait_for_size(handle, handler); + } + if (filesize > 0) { if (handle->pointer >= filesize) return CAMU_ERR_EOF; if (handle->pointer + size >= filesize) { size = filesize - handle->pointer; } } - handle->wait.id = al_rand_u16(); handle->wait.start = handle->pointer; handle->wait.end = handle->pointer + size; if (!handler->wait_for_range(handler, &handle->wait)) { @@ -33,25 +39,27 @@ s32 cch_handle_read(struct cch_handle *handle, u8 *buf, s32 size) } struct cch_backing *backing = handle->entry->backing; size_t available = (size_t)size; - if (backing->mode == CCH_BACKING_MAPPED) { + if (backing->mode == CACHE_BACKING_MAPPED) { u8 *ptr = backing->get_ptr(backing, handle->pointer, &available); if (ptr && available > 0) { al_memcpy(buf, ptr, available); } backing->unlock(backing); - } else if (backing->mode == CCH_BACKING_READ) { + } else if (backing->mode == CACHE_BACKING_READ) { backing->read(backing, buf, handle->pointer, &available); } handle->pointer += available; - return (available > 0) ? (s32)available : CAMU_ERR_EOF; + return available > 0 ? (s32)available : CAMU_ERR_EOF; } off_t cch_handle_seek(struct cch_handle *handle, off_t offset, s32 whence) { struct cch_handler *handler = handle->entry->handler; - wait_for_size(handle, handler); off_t filesize = cch_entry_get_size(handle->entry); - if (filesize < 0) return -1; + if (filesize < 0) { + filesize = wait_for_size(handle, handler); + } + if (filesize <= 0) return -1; if (whence == CAMU_SEEK_SIZE) { al_log_debug("cache_handle", "seek_size"); return filesize; @@ -91,10 +99,5 @@ bool cch_handle_can_seek(struct cch_handle *handle) void cch_handle_disable(struct cch_handle *handle) { - aki_mutex_lock(&handle->wait.mutex); - handle->wait.disabled = true; - if (aki_cond_is_waiting(&handle->wait.cond)) { - aki_cond_signal(&handle->wait.cond); - } - aki_mutex_unlock(&handle->wait.mutex); + cch_threaded_wait_disable(&handle->wait); } diff --git a/src/cache/handle.h b/src/cache/handle.h index 55223aa..ae936f3 100644 --- a/src/cache/handle.h +++ b/src/cache/handle.h @@ -3,7 +3,7 @@ #include <al/types.h> #include <aki/thread.h> -#include "handler.h" +#include "wait.h" struct cch_entry; struct cch_handle { diff --git a/src/cache/handler.h b/src/cache/handler.h index 5a2861e..9dd1896 100644 --- a/src/cache/handler.h +++ b/src/cache/handler.h @@ -2,15 +2,10 @@ #include <al/types.h> #include <al/array.h> +#include <al/str.h> #include <aki/thread.h> -struct cch_handler_wait { - u16 id; - off_t start, end; - struct aki_cond cond; - struct aki_mutex mutex; - bool disabled; -}; +#include "handle.h" struct cch_handler { bool (*can_seek)(struct cch_handler *); @@ -18,6 +13,8 @@ struct cch_handler { bool (*wait_for_range)(struct cch_handler *, struct cch_handler_wait *); void (*free)(struct cch_handler **); struct cch_entry *entry; - struct cch_backing *backing; + str liana; + bool disabled; + struct aki_mutex mutex; array(struct cch_handler_wait *) waits; }; diff --git a/src/cache/handlers/cdio.c b/src/cache/handlers/cdio.c new file mode 100644 index 0000000..23f87b6 --- /dev/null +++ b/src/cache/handlers/cdio.c @@ -0,0 +1,206 @@ +#include <al/log.h> + +#include "../backings/file.h" +#include "../backings/memory.h" +#include "../threaded_waits.h" +#include "../range.h" + +#include "cdio.h" + +// 1 sector at a time to minimize skips +#define SECTORS_PER_STEP 1 +#define BYTES_PER_STEP (SECTORS_PER_STEP * CDIO_CD_FRAMESIZE_RAW) + +static void cdio_log_messages(struct cch_handler_cdio *cdio) +{ + char *error_messages = cdio_cddap_errors(cdio->drive); + char *messages = cdio_cddap_messages(cdio->drive); + if (error_messages) { + al_log_error("cdio", "\n%s", error_messages); + cdio_cddap_free_messages(error_messages); + } + if (messages) { + al_log_info("cdio", "\n%s", messages); + cdio_cddap_free_messages(messages); + } +} + +static bool handler_cdio_can_seek(struct cch_handler *handler) +{ + (void)handler; + return true; +} + +static aki_thread_result AKI_THREADCALL cd_read_thread(void *userdata) +{ + struct cch_handler_cdio *cdio = (struct cch_handler_cdio *)userdata; + while (1) { + if (!al_atomic_load(s32)(&cdio->running, AL_ATOMIC_RELAXED)) { + // We don't care about how complete the disc read was. + return 0; + } + // cdio_read() here can block for a very long time so we buffer each read + // before writing it to the backing. + long ret = cdio_cddap_read(cdio->drive, aki_buffer_get_ptr(&cdio->buffer, 0), cdio->sector, SECTORS_PER_STEP); + cdio_log_messages(cdio); + al_assert(ret <= SECTORS_PER_STEP); + off_t pointer = cdio->sector * CDIO_CD_FRAMESIZE_RAW; + size_t size; + u8 *ptr = cdio->backing->get_ptr(cdio->backing, pointer, &size); + if (size < BYTES_PER_STEP) { + al_log_error("cdio", "Tried to write over expected size during normal operation."); + cdio->backing->unlock(cdio->backing); + break; + } + if (ret == SECTORS_PER_STEP) { + al_memcpy(ptr, aki_buffer_get_ptr(&cdio->buffer, 0), BYTES_PER_STEP); + } else if (ret == -10) { + al_log_error("cdio", "I/O error, skipping sector."); + al_memset(ptr, 0, BYTES_PER_STEP); + ret = SECTORS_PER_STEP; + } else if (ret < 0) { + al_log_error("cdio", "Fatal error (%d).", ret); + cdio->backing->unlock(cdio->backing); + break; + } + cch_backing_fill_range(cdio->backing, pointer, BYTES_PER_STEP); + cdio->sector += ret; + cdio->backing->unlock(cdio->backing); + cch_threaded_waits_evaluate(&cdio->handler, cdio->backing); + if (ret < SECTORS_PER_STEP || cdio->sector > cdio->end) { + al_log_debug("cdio", "Normal CD EOF."); + break; + } + } + if (cdio->sector <= cdio->end) { + al_log_warn("cdio", "Disc read cut short (sectors: %zd, end: %zd).", cdio->sector, cdio->end); + // We didn't read the full disc, disable waiters. + cch_threaded_waits_disable_all(&cdio->handler); + } + return 0; +} + +static void handler_cdio_maybe_spawn_worker(struct cch_handler *handler, size_t index) +{ + struct cch_handler_cdio *cdio = (struct cch_handler_cdio *)handler; + if (cdio->start == (lsn_t)index) return; + if (al_atomic_load(s32)(&cdio->running, AL_ATOMIC_ACQUIRE)) { + al_atomic_store(s32)(&cdio->running, 0, AL_ATOMIC_RELEASE); + aki_thread_join(&cdio->thread); + } + cdio->start = index; + cdio->sector = cdio->start; + al_atomic_store(s32)(&cdio->running, 1, AL_ATOMIC_RELEASE); + aki_thread_create(&cdio->thread, cd_read_thread, cdio); +} + +static bool handler_cdio_wait_for_range(struct cch_handler *handler, struct cch_handler_wait *wait) +{ + struct cch_handler_cdio *cdio = (struct cch_handler_cdio *)handler; + return cch_threaded_wait_for_range(&cdio->handler, cdio->backing, wait); +} + +static void handler_cdio_free(struct cch_handler **handler) +{ + struct cch_handler_cdio *cdio = (struct cch_handler_cdio *)*handler; + cch_threaded_waits_disable_all(&cdio->handler); + if (al_atomic_load(s32)(&cdio->running, AL_ATOMIC_RELAXED)) { + al_atomic_store(s32)(&cdio->running, 0, AL_ATOMIC_RELAXED); + aki_thread_join(&cdio->thread); + } + if (cdio->drive) { + cdio_log_messages(cdio); + cdio_cddap_close(cdio->drive); + } + cch_threaded_waits_close(&cdio->handler); + al_str_free(&cdio->handler.liana); + al_free(cdio); + *handler = NULL; +} + +static bool open_cd_drive(struct cch_handler_cdio *cdio) +{ + cdrom_drive_t *drive = NULL; + char **cd_drives; + CdIo_t *cdio_handle = NULL; + + cd_drives = cdio_get_devices_with_cap(NULL, CDIO_FS_AUDIO, false); + if (cd_drives) { + // Use first drive. + cdio_handle = cdio_open(*cd_drives, DRIVER_UNKNOWN); + drive = cdio_cddap_identify_cdio(cdio_handle, CDDA_MESSAGE_LOGIT, NULL); + } else { + al_log_error("cdio", "No compatible disc found."); + return false; + } + cdio_free_device_list(cd_drives); + + cdio_cddap_verbose_set(drive, CDDA_MESSAGE_LOGIT, CDDA_MESSAGE_LOGIT); + + s32 ret = cdio_cddap_open(drive); + if (ret != 0) { + al_log_error("cdio", "Unable to open disc (%d).", ret); + return false; + } + + ret = cdio_cddap_speed_set(drive, 4); + if (ret != 0) { + al_log_warn("cdio", "Failed to set drive speed (%d).", ret); + } + + // Not sure what this is based on, but it can seemingly just be wrong. + track_t tracks = cdio_cddap_tracks(drive); + al_log_info("cdio", "CD with %d tracks loaded.", tracks); + al_array_reserve(cdio->handler.entry->chapters, tracks); + + lsn_t start = cdio_cddap_disc_firstsector(drive); + lsn_t end = cdio_cddap_disc_lastsector(drive); + lsn_t offset = start; + s32 track = CDIO_INVALID_TRACK; + while (offset < end) { + track = cdio_cddap_sector_gettrack(drive, offset); + al_assert(track != CDIO_INVALID_TRACK); + lsn_t track_start = cdio_cddap_track_firstsector(drive, track); + lsn_t track_end = cdio_cddap_track_lastsector(drive, track); + al_array_push(cdio->handler.entry->chapters, ((struct cch_chapter){ track_start, track_end })); + offset = track_end + 1; + } + // end is a valid index so add one framesize. + cch_entry_set_size(cdio->handler.entry, ((end - start) + 1) * CDIO_CD_FRAMESIZE_RAW); + cdio->start = -1; + cdio->end = end; + + cdio->drive = drive; + + cdio_log_messages(cdio); + + return true; +} + +struct cch_entry *cch_handler_cdio_create(void) +{ + struct cch_backing *backing = cch_backing_file_create(al_str_c("/tmp/camu_cd_data"), 4096); + if (!backing) return NULL; + struct cch_handler_cdio *cdio = al_alloc_object(struct cch_handler_cdio); + cdio->backing = backing; + struct cch_entry *entry = al_alloc_object(struct cch_entry); + entry->backing = backing; + entry->handler = (struct cch_handler *)cdio; + entry->ref_count = 0; + entry->unknown_size = false; + entry->hash = 0; + aki_mutex_init(&entry->mutex); + entry->handler->can_seek = handler_cdio_can_seek; + entry->handler->maybe_spawn_worker = handler_cdio_maybe_spawn_worker; + entry->handler->wait_for_range = handler_cdio_wait_for_range; + entry->handler->free = handler_cdio_free; + entry->handler->entry = entry; + al_str_from(&entry->handler->liana, "cdio"); + if (!open_cd_drive(cdio)) return NULL; + al_atomic_store(s32)(&cdio->running, 0, AL_ATOMIC_RELAXED); + aki_buffer_init(&cdio->buffer); + aki_buffer_ensure_space(&cdio->buffer, BYTES_PER_STEP); + aki_buffer_set_size(&cdio->buffer, BYTES_PER_STEP); + cch_threaded_waits_init(&cdio->handler); + return entry; +} diff --git a/src/cache/handlers/cdio.h b/src/cache/handlers/cdio.h new file mode 100644 index 0000000..953c988 --- /dev/null +++ b/src/cache/handlers/cdio.h @@ -0,0 +1,21 @@ +#include <al/atomic.h> +#include <aki/common.h> +#include <cdio/paranoia/cdda.h> +#include <cdio/cd_types.h> + +#include "../handler.h" +#include "../entry.h" + +struct cch_handler_cdio { + struct cch_handler handler; + struct cch_backing *backing; + cdrom_drive_t *drive; + lsn_t start; + lsn_t end; + lsn_t sector; + atomic(s32) running; + struct aki_thread thread; + struct aki_buffer buffer; +}; + +struct cch_entry *cch_handler_cdio_create(void); diff --git a/src/cache/handlers/file.c b/src/cache/handlers/file.c index ec6a146..03e2814 100644 --- a/src/cache/handlers/file.c +++ b/src/cache/handlers/file.c @@ -25,6 +25,7 @@ static bool handler_file_wait_for_range(struct cch_handler *handler, struct cch_ static void handler_file_free(struct cch_handler **handler) { struct cch_handler_file *file = (struct cch_handler_file *)*handler; + al_str_free(&file->handler.liana); al_free(file); *handler = NULL; } @@ -33,17 +34,20 @@ struct cch_entry *cch_handler_file_create(str *path) { struct cch_backing *backing = cch_backing_file_create(path, 0); if (!backing) return NULL; + struct cch_handler_file *file = al_alloc_object(struct cch_handler_file); + file->backing = backing; struct cch_entry *entry = al_alloc_object(struct cch_entry); entry->backing = backing; + entry->handler = (struct cch_handler *)file; entry->ref_count = 0; + entry->unknown_size = false; entry->hash = 0; - entry->handler = (struct cch_handler *)al_alloc_object(struct cch_handler_file); aki_mutex_init(&entry->mutex); entry->handler->can_seek = handler_file_can_seek; entry->handler->maybe_spawn_worker = handler_file_maybe_spawn_worker; entry->handler->wait_for_range = handler_file_wait_for_range; entry->handler->free = handler_file_free; entry->handler->entry = entry; - entry->handler->backing = entry->backing; + al_str_from(&entry->handler->liana, "codec"); return entry; } diff --git a/src/cache/handlers/file.h b/src/cache/handlers/file.h index 52e5c94..85e1c7f 100644 --- a/src/cache/handlers/file.h +++ b/src/cache/handlers/file.h @@ -7,6 +7,7 @@ struct cch_handler_file { struct cch_handler handler; + struct cch_backing *backing; }; struct cch_entry *cch_handler_file_create(str *path); diff --git a/src/cache/handlers/http.c b/src/cache/handlers/http.c index 789b247..71ae186 100644 --- a/src/cache/handlers/http.c +++ b/src/cache/handlers/http.c @@ -1,8 +1,10 @@ #include <al/random.h> #include <al/log.h> -#include "../backings/file.h" #include "../backings/memory.h" +#include "../backings/file.h" +#include "../threaded_waits.h" +#include "../wait.h" #include "http.h" @@ -14,21 +16,6 @@ static bool handler_http_can_seek(struct cch_handler *handler) return false; } -static void disable_waiters_internal(struct cch_handler_http *http) -{ - aki_mutex_lock(&http->mutex); - // Don't allow any further waits. - http->disabled = true; - struct cch_handler_wait *wait; - al_array_foreach(http->handler.waits, i, wait) { - aki_mutex_lock(&wait->mutex); - wait->disabled = true; - aki_cond_signal(&wait->cond); - aki_mutex_unlock(&wait->mutex); - } - aki_mutex_unlock(&http->mutex); -} - static size_t http_callback(void *userdata, u8 op, u8 *buf, s64 int0) { struct cch_handler_http *http = (struct cch_handler_http *)userdata; @@ -39,8 +26,7 @@ static size_t http_callback(void *userdata, u8 op, u8 *buf, s64 int0) break; } case AKI_HTTP_ERROR: { - al_log_debug("cache_handler_http", "Error."); - disable_waiters_internal(http); + cch_threaded_waits_disable_all(&http->handler); break; } case AKI_HTTP_RESPONSE_CODE: { @@ -54,37 +40,18 @@ static size_t http_callback(void *userdata, u8 op, u8 *buf, s64 int0) case AKI_HTTP_CONTENT_LENGTH: { off_t content_length = (off_t)int0; al_log_debug("cache_handler_http", "Content-Length: %lld.", content_length); - aki_mutex_lock(&http->mutex); cch_entry_set_size(http->handler.entry, content_length); - struct cch_handler_wait *wait; - al_array_foreach(http->handler.waits, i, wait) { - if (wait->end < 0) { - aki_mutex_lock(&wait->mutex); - aki_cond_signal(&wait->cond); - al_array_remove_at_iter(http->handler.waits, i); - aki_mutex_unlock(&wait->mutex); - } - } - aki_mutex_unlock(&http->mutex); + cch_threaded_waits_signal_any(&http->handler); break; } case AKI_HTTP_WRITE: { - http->handler.backing->write(http->handler.backing, buf, http->pointer, &ret); - aki_mutex_lock(&http->mutex); - http->pointer += ret; - struct cch_handler_wait *wait; - al_array_foreach(http->handler.waits, i, wait) { - aki_mutex_lock(&wait->mutex); - // Assume that if we get data before a length, we won't get a length. - // So, signal the waiter (wait_for_range returns false). - if (wait->end < 0) wait->disabled = true; - if (wait->disabled || http->pointer >= wait->end) { - aki_cond_signal(&wait->cond); - al_array_remove_at_iter(http->handler.waits, i); - } - aki_mutex_unlock(&wait->mutex); + // If we got data before a Content-Length, assume the size is unknown. + if (cch_entry_get_size(http->handler.entry) < 0) { + cch_entry_set_size(http->handler.entry, 0); } - aki_mutex_unlock(&http->mutex); + http->backing->write(http->backing, buf, http->pointer, &ret); + http->pointer += ret; + cch_threaded_waits_evaluate(&http->handler, http->backing); break; } default: @@ -93,123 +60,63 @@ static size_t http_callback(void *userdata, u8 op, u8 *buf, s64 int0) return ret; } -static void signal_callback(void *userdata) -{ - struct cch_handler_http *http = (struct cch_handler_http *)userdata; - u32 size; - s32 index; - do { - camu_queue_try_pop(http->queue, size, index); - if (size == 0) break; - if (index == -1) { - aki_event_loop_break(&http->loop); - break; - } - al_array_push(http->requests, (struct aki_http){0}); - struct aki_http *request = &al_array_last(http->requests); - aki_http_init(request); - aki_http_set_url(request, &http->url); - aki_http_set_user_agent(request, USER_AGENT); - aki_http_request_stream(request, AKI_HTTP_GET, &http->loop, http_callback, http); - } while (1); -} - static void handler_http_maybe_spawn_worker(struct cch_handler *handler, size_t index) { struct cch_handler_http *http = (struct cch_handler_http *)handler; - camu_queue_push(http->queue, index); - aki_signal_send(&http->signal); + (void)index; + al_array_push(http->requests, (struct aki_http){ 0 }); + struct aki_http *request = &al_array_last(http->requests); + aki_http_init(request); + aki_http_set_url(request, &http->url); + aki_http_set_user_agent(request, USER_AGENT); + aki_http_request_stream(request, AKI_HTTP_GET, http->loop, http_callback, http); } static bool handler_http_wait_for_range(struct cch_handler *handler, struct cch_handler_wait *wait) { struct cch_handler_http *http = (struct cch_handler_http *)handler; - aki_mutex_lock(&http->mutex); - bool canceled = http->disabled; - if (!canceled && (cch_entry_get_size(http->handler.entry) < 0 || http->pointer < wait->end)) { - aki_mutex_lock(&wait->mutex); - if (!wait->disabled) { - al_array_push(http->handler.waits, wait); - aki_mutex_unlock(&http->mutex); - aki_cond_wait(&wait->cond, &wait->mutex); - } else { - aki_mutex_unlock(&wait->mutex); - aki_mutex_unlock(&http->mutex); - return false; - } - aki_mutex_unlock(&wait->mutex); - aki_mutex_lock(&http->mutex); - if (wait->disabled) { - u16 id = wait->id; - al_array_foreach(http->handler.waits, i, wait) { - if (wait->id == id) { - al_array_remove_at_iter(http->handler.waits, i); - break; - } - } - canceled = true; - } - } - aki_mutex_unlock(&http->mutex); - return !canceled; + return cch_threaded_wait_for_range(&http->handler, http->backing, wait); } static void handler_http_free(struct cch_handler **handler) { struct cch_handler_http *http = (struct cch_handler_http *)*handler; - disable_waiters_internal(http); - camu_queue_push(http->queue, -1); - aki_signal_send(&http->signal); - aki_thread_join(&http->thread); - aki_mutex_destroy(&http->mutex); + cch_threaded_waits_disable_all(&http->handler); struct aki_http *request; al_array_foreach_ptr(http->requests, i, request) { aki_http_close(request); } + cch_threaded_waits_close(&http->handler); al_array_free(http->requests); - camu_queue_free(http->queue); al_str_free(&http->url); - al_array_free(http->handler.waits); + al_str_free(&http->handler.liana); al_free(http); *handler = NULL; } -static aki_thread_result AKI_THREADCALL event_loop_thread(void *userdata) -{ - struct cch_handler_http *http = (struct cch_handler_http *)userdata; - aki_event_loop_run(&http->loop); - aki_event_loop_destroy(&http->loop); - return 0; -} - -struct cch_entry *cch_handler_http_create(str *url) +struct cch_entry *cch_handler_http_create(str *url, struct aki_event_loop *loop) { - //struct cch_backing *backing = cch_backing_file_create(al_str_c("/tmp/camu_http_data"), 1024 * 128); struct cch_backing *backing = cch_backing_memory_create(1024 * 128); if (!backing) return NULL; + struct cch_handler_http *http = al_alloc_object(struct cch_handler_http); + http->backing = backing; struct cch_entry *entry = al_alloc_object(struct cch_entry); entry->backing = backing; + entry->handler = (struct cch_handler *)http; entry->ref_count = 0; + entry->unknown_size = false; entry->hash = 0; - entry->handler = (struct cch_handler *)al_alloc_object(struct cch_handler_http); aki_mutex_init(&entry->mutex); entry->handler->can_seek = handler_http_can_seek; entry->handler->maybe_spawn_worker = handler_http_maybe_spawn_worker; entry->handler->wait_for_range = handler_http_wait_for_range; entry->handler->free = handler_http_free; entry->handler->entry = entry; - entry->handler->backing = entry->backing; - al_array_init(entry->handler->waits); - struct cch_handler_http *http = (struct cch_handler_http *)entry->handler; + al_str_from(&entry->handler->liana, "codec"); + http->loop = loop; al_str_clone(&http->url, url); http->pointer = 0; - http->disabled = false; al_array_init(http->requests); - camu_queue_init(http->queue); - aki_event_loop_init(&http->loop); - aki_signal_init(&http->signal, signal_callback, http); - aki_signal_start(&http->signal, &http->loop); - aki_mutex_init(&http->mutex); - aki_thread_create(&http->thread, event_loop_thread, http); + cch_threaded_waits_init(&http->handler); return entry; } diff --git a/src/cache/handlers/http.h b/src/cache/handlers/http.h index e8fb2e0..5f9df86 100644 --- a/src/cache/handlers/http.h +++ b/src/cache/handlers/http.h @@ -4,22 +4,16 @@ #include <aki/thread.h> #include <aki/signal.h> -#include "../../util/queue.h" - #include "../handler.h" #include "../entry.h" struct cch_handler_http { struct cch_handler handler; - off_t pointer; + struct cch_backing *backing; str url; - bool disabled; + off_t pointer; array(struct aki_http) requests; - queue(s32) queue; - struct aki_event_loop loop; - struct aki_signal signal; - struct aki_mutex mutex; - struct aki_thread thread; + struct aki_event_loop *loop; }; -struct cch_entry *cch_handler_http_create(str *url); +struct cch_entry *cch_handler_http_create(str *url, struct aki_event_loop *loop); diff --git a/src/cache/meson.build b/src/cache/meson.build index 099b2a6..8684ccd 100644 --- a/src/cache/meson.build +++ b/src/cache/meson.build @@ -1,7 +1,9 @@ cache_src = [ 'entry.c', 'handle.c', + 'threaded_waits.c', 'handlers/file.c', + 'handlers/cdio.c', 'backings/memory.c' ] diff --git a/src/cache/range.h b/src/cache/range.h new file mode 100644 index 0000000..598b655 --- /dev/null +++ b/src/cache/range.h @@ -0,0 +1,23 @@ +#include <al/types.h> + +#include "backing.h" + +AL_UNUSED_FUNCTION_PUSH + +// TODO: cch_backing_remove_range. +// +// TODO: report overlap, don't unlock in backing_write() and handle fill in handler + +static void cch_backing_fill_range(struct cch_backing *backing, off_t index, off_t size) +{ + struct cch_range *range; + al_array_foreach_ptr(backing->available, i, range) { + if (index >= range->start && index <= range->end) { + range->end = AL_MAX(index + size, range->end); + return; + } + } + al_array_push(backing->available, ((struct cch_range){ index, index + size })); +} + +AL_UNUSED_FUNCTION_POP diff --git a/src/cache/threaded_waits.c b/src/cache/threaded_waits.c new file mode 100644 index 0000000..a93fe85 --- /dev/null +++ b/src/cache/threaded_waits.c @@ -0,0 +1,126 @@ +#include "threaded_waits.h" +#include "entry.h" + +void cch_threaded_waits_init(struct cch_handler *handler) +{ + handler->disabled = false; + aki_mutex_init(&handler->mutex); + al_array_init(handler->waits); +} + +static bool wait_range_satisfied(struct cch_backing *backing, struct cch_handler_wait *wait) +{ + if (wait->end < 0) return true; + struct cch_range *range; + al_array_foreach_ptr(backing->available, i, range) { + if (wait->start >= range->start && range->end >= wait->end) { + return true; + } + } + return false; +} + +bool cch_threaded_wait_for_range(struct cch_handler *handler, struct cch_backing *backing, + struct cch_handler_wait *wait) +{ + aki_mutex_lock(&handler->mutex); + bool canceled = handler->disabled; + off_t size = cch_entry_get_size(handler->entry); + backing->lock(backing); + bool satisfied = size >= 0 && wait_range_satisfied(backing, wait); + backing->unlock(backing); + if (!canceled && !satisfied) { + aki_mutex_lock(&wait->mutex); + if (!wait->disabled) { + al_array_push(handler->waits, wait); + aki_mutex_unlock(&handler->mutex); + aki_cond_wait(&wait->cond, &wait->mutex); + } else { + aki_mutex_unlock(&wait->mutex); + aki_mutex_unlock(&handler->mutex); + return false; + } + aki_mutex_unlock(&wait->mutex); + aki_mutex_lock(&handler->mutex); + if (wait->disabled) { + struct cch_handler_wait *rwait; + al_array_foreach(handler->waits, i, rwait) { + if (rwait == wait) { + al_array_remove_at(handler->waits, i); + break; + } + } + canceled = true; + } + } + aki_mutex_unlock(&handler->mutex); + return !canceled; +} + +void cch_threaded_waits_signal_any(struct cch_handler *handler) +{ + aki_mutex_lock(&handler->mutex); + struct cch_handler_wait *wait; + al_array_foreach_rev(handler->waits, i, wait) { + if (wait->end < 0) { + al_array_remove_at(handler->waits, i); + aki_mutex_lock(&wait->mutex); + aki_cond_signal(&wait->cond); + aki_mutex_unlock(&wait->mutex); + } + } + aki_mutex_unlock(&handler->mutex); +} + +void cch_threaded_wait_disable(struct cch_handler_wait *wait) +{ + aki_mutex_lock(&wait->mutex); + wait->disabled = true; + // cond_is_waiting() could be false if the handler is disabled. + if (aki_cond_is_waiting(&wait->cond)) { + aki_cond_signal(&wait->cond); + } + aki_mutex_unlock(&wait->mutex); +} + +void cch_threaded_waits_disable_all(struct cch_handler *handler) +{ + aki_mutex_lock(&handler->mutex); + if (handler->disabled) { + aki_mutex_unlock(&handler->mutex); + return; + } + // Disallow any further waits. + handler->disabled = true; + struct cch_handler_wait *wait; + al_array_foreach(handler->waits, i, wait) { + aki_mutex_lock(&wait->mutex); + wait->disabled = true; + aki_cond_signal(&wait->cond); + aki_mutex_unlock(&wait->mutex); + } + aki_mutex_unlock(&handler->mutex); +} + +void cch_threaded_waits_evaluate(struct cch_handler *handler, struct cch_backing *backing) +{ + aki_mutex_lock(&handler->mutex); + backing->lock(backing); + struct cch_handler_wait *wait; + al_array_foreach_rev(handler->waits, i, wait) { + aki_mutex_lock(&wait->mutex); + if (wait_range_satisfied(backing, wait)) { + aki_cond_signal(&wait->cond); + al_array_remove_at(handler->waits, i); + } + aki_mutex_unlock(&wait->mutex); + } + backing->unlock(backing); + aki_mutex_unlock(&handler->mutex); +} + +void cch_threaded_waits_close(struct cch_handler *handler) +{ + aki_mutex_destroy(&handler->mutex); + al_array_free(handler->waits); +} diff --git a/src/cache/threaded_waits.h b/src/cache/threaded_waits.h new file mode 100644 index 0000000..edbee03 --- /dev/null +++ b/src/cache/threaded_waits.h @@ -0,0 +1,15 @@ +#pragma once + +#include <al/types.h> + +#include "handler.h" +#include "backing.h" + +void cch_threaded_waits_init(struct cch_handler *handler); +bool cch_threaded_wait_for_range(struct cch_handler *handler, struct cch_backing *backing, + struct cch_handler_wait *wait); +void cch_threaded_waits_signal_any(struct cch_handler *handler); +void cch_threaded_wait_disable(struct cch_handler_wait *wait); +void cch_threaded_waits_disable_all(struct cch_handler *handler); +void cch_threaded_waits_evaluate(struct cch_handler *handler, struct cch_backing *backing); +void cch_threaded_waits_close(struct cch_handler *handler); diff --git a/src/cache/wait.h b/src/cache/wait.h new file mode 100644 index 0000000..8654362 --- /dev/null +++ b/src/cache/wait.h @@ -0,0 +1,10 @@ +#pragma once + +#include <aki/thread.h> + +struct cch_handler_wait { + off_t start, end; + struct aki_cond cond; + struct aki_mutex mutex; + bool disabled; +}; diff --git a/src/codec/codec.h b/src/codec/codec.h index fd847b8..9528522 100644 --- a/src/codec/codec.h +++ b/src/codec/codec.h @@ -3,38 +3,94 @@ #include <al/types.h> #include <al/array.h> #include <aki/common.h> - -#include "../cache/handle.h" - #ifdef CAMU_HAVE_FFMPEG #include <libavutil/pixdesc.h> #include <libavformat/avformat.h> #endif +#include "../cache/handle.h" + +#ifndef CAMU_SINK_NO_VIDEO +#include "../render/renderer.h" +#else +struct camu_renderer; +#endif + #ifdef CAMU_HAVE_FFMPEG enum { CAMU_OK = 0, + CAMU_ERR_ERROR = 1, CAMU_ERR_EOF = AVERROR_EOF, CAMU_ERR_AGAIN = AVERROR(EAGAIN) }; enum { CAMU_SEEK_SIZE = AVSEEK_SIZE }; +enum { + CAMU_STREAM_UNKNOWN = AVMEDIA_TYPE_UNKNOWN, + CAMU_STREAM_AUDIO = AVMEDIA_TYPE_AUDIO, + CAMU_STREAM_VIDEO = AVMEDIA_TYPE_VIDEO, + CAMU_STREAM_SUBTITLE = AVMEDIA_TYPE_SUBTITLE +}; +enum { + CAMU_SAMPLE_FORMAT_NONE = AV_SAMPLE_FMT_NONE, + CAMU_SAMPLE_FORMAT_U8 = AV_SAMPLE_FMT_U8, + CAMU_SAMPLE_FORMAT_S16 = AV_SAMPLE_FMT_S16, + CAMU_SAMPLE_FORMAT_S32 = AV_SAMPLE_FMT_S32, + CAMU_SAMPLE_FORMAT_FLT = AV_SAMPLE_FMT_FLT, + CAMU_SAMPLE_FORMAT_DBL = AV_SAMPLE_FMT_DBL, + CAMU_SAMPLE_FORMAT_U8P = AV_SAMPLE_FMT_U8P, + CAMU_SAMPLE_FORMAT_S16P = AV_SAMPLE_FMT_S16P, + CAMU_SAMPLE_FORMAT_S32P = AV_SAMPLE_FMT_S32P, + CAMU_SAMPLE_FORMAT_FLTP = AV_SAMPLE_FMT_FLTP, + CAMU_SAMPLE_FORMAT_DBLP = AV_SAMPLE_FMT_DBLP, + CAMU_SAMPLE_FORMAT_S64 = AV_SAMPLE_FMT_S64, + CAMU_SAMPLE_FORMAT_S64P = AV_SAMPLE_FMT_S64P +}; +enum { + CAMU_PIXEL_FORMAT_RGBA = AV_PIX_FMT_RGBA, + CAMU_PIXEL_FORMAT_RGB = AV_PIX_FMT_RGB8, + CAMU_PIXEL_FORMAT_GREYA = AV_PIX_FMT_GRAY8A, + CAMU_PIXEL_FORMAT_GREY = AV_PIX_FMT_GRAY8 +}; #else enum { CAMU_OK = 0, + CAMU_ERR_ERROR = 1, CAMU_ERR_EOF = -1, CAMU_ERR_AGAIN = -2 }; enum { CAMU_SEEK_SIZE = 0x10000 }; -#endif - enum { - CAMU_PIXEL_FMT_RGB = 0, - CAMU_PIXEL_FMT_RGBA + CAMU_STREAM_UNKNOWN = 0, + CAMU_STREAM_AUDIO, + CAMU_STREAM_VIDEO, + CAMU_STREAM_SUBTITLE }; +enum { + CAMU_SAMPLE_FORMAT_NONE = -1, + CAMU_SAMPLE_FORMAT_U8, + CAMU_SAMPLE_FORMAT_S16, + CAMU_SAMPLE_FORMAT_S32, + CAMU_SAMPLE_FORMAT_FLT, + CAMU_SAMPLE_FORMAT_DBL, + CAMU_SAMPLE_FORMAT_U8P, + CAMU_SAMPLE_FORMAT_S16P, + CAMU_SAMPLE_FORMAT_S32P, + CAMU_SAMPLE_FORMAT_FLTP, + CAMU_SAMPLE_FORMAT_DBLP, + CAMU_SAMPLE_FORMAT_S64, + CAMU_SAMPLE_FORMAT_S64P +}; +enum { + CAMU_PIXEL_FORMAT_RGBA = 0, + CAMU_PIXEL_FORMAT_RGB, + CAMU_PIXEL_FORMAT_GREYA, + CAMU_PIXEL_FORMAT_GREY +}; +#endif enum { CAMU_FRAME_DEVICE_ALLOCATED = 1 @@ -47,21 +103,27 @@ enum { #endif }; -struct camu_icc_profile { - u8 *data; - size_t size; +struct camu_audio_format { + s32 format; + s32 sample_rate; +#ifdef CAMU_HAVE_FFMPEG + AVChannelLayout channel_layout; +#endif + s32 channel_count; }; -struct camu_stream { +struct camu_codec_stream { + u8 mode; u8 type; - s64 duration; + u64 duration; struct { - u8 fmt; s32 width; s32 height; - struct camu_icc_profile icc; + s32 format; } video; - //struct { } audio; + struct { + struct camu_audio_format fmt; + } audio; #ifdef CAMU_HAVE_FFMPEG struct { AVStream *stream; @@ -70,56 +132,182 @@ struct camu_stream { #endif }; -struct camu_packet { - u8 type; +struct camu_codec_packet { + u8 mode; struct aki_buffer *buffer; #ifdef CAMU_HAVE_FFMPEG struct { AVPacket *pkt; } av; #endif }; -struct camu_frame { - u8 type; - u8 *data; +struct camu_codec_frame { + u8 mode; u8 flags; - u8 fmt; - s32 width; - s32 height; + u8 *data; + s32 format; f64 pts; + struct { + s32 width; + s32 height; + } video; + struct { + u32 sample_count; + } audio; #ifdef CAMU_HAVE_FFMPEG struct { AVFrame *frame; } av; #endif - void *opaque; }; struct camu_demuxer { - u8 type; + u8 mode; bool (*init)(struct camu_demuxer *, struct cch_handle *); - s32 (*get_packet)(struct camu_demuxer *, struct camu_packet *); + s32 (*get_packet)(struct camu_demuxer *, struct camu_codec_packet *); u64 (*get_duration)(struct camu_demuxer *); bool (*seek)(struct camu_demuxer *, u64); void (*free)(struct camu_demuxer **); - array(struct camu_stream) streams; - array(s32) subscribed; + array(struct camu_codec_stream) streams; + s32 subscribed; }; -struct camu_renderer; struct camu_decoder { - bool (*init)(struct camu_decoder *, struct camu_renderer *, struct camu_stream *); - void (*set_callback)(struct camu_decoder *, void (*callback)(void *, struct camu_frame *), void *); - s32 (*push)(struct camu_decoder *, struct camu_packet *); + bool (*init)(struct camu_decoder *, struct camu_renderer *, struct camu_codec_stream *, + void (*callback)(void *, struct camu_codec_frame *), void *); + s32 (*push)(struct camu_decoder *, struct camu_codec_packet *); s32 (*process)(struct camu_decoder *); void (*flush)(struct camu_decoder *); void (*free)(struct camu_decoder **); - struct camu_stream *stream; }; -static inline void camu_frame_discard(struct camu_frame *frame) +struct camu_resampler_format { + bool resampler_needed; + struct camu_audio_format in; + struct camu_audio_format req; +}; + +struct camu_resampler { + bool (*init)(struct camu_resampler *, struct camu_resampler_format *); + s32 (*convert)(struct camu_resampler *, const u8 **, s32); + s32 (*flush)(struct camu_resampler *); + u8 **(*get_data)(struct camu_resampler *); + void (*free)(struct camu_resampler **); +}; + +AL_UNUSED_FUNCTION_PUSH + +static inline bool camu_resampler_format_matches(struct camu_resampler_format *fmt) { + return fmt->in.format == fmt->req.format + && fmt->in.sample_rate == fmt->req.sample_rate #ifdef CAMU_HAVE_FFMPEG - if (frame->type == CAMU_FFMPEG_COMPAT) { - av_frame_free(&frame->av.frame); + && av_channel_layout_compare(&fmt->in.channel_layout, &fmt->req.channel_layout) == 0 +#endif + && fmt->in.channel_count == fmt->req.channel_count; +} + +static inline void camu_audio_format_copy(struct camu_audio_format *dest, struct camu_audio_format *src) +{ + dest->format = src->format; + dest->sample_rate = src->sample_rate; +#ifdef CAMU_HAVE_FFMPEG + av_channel_layout_copy(&dest->channel_layout, &src->channel_layout); +#endif + dest->channel_count = src->channel_count; +} + +static const char *camu_audio_format_name(s32 format) +{ +#ifdef CAMU_HAVE_FFMPEG + return av_get_sample_fmt_name(format); +#else + (void)format; + return "(undefined)"; +#endif +} + +static inline s32 camu_audio_format_bytes_per_sample(struct camu_audio_format *fmt) +{ +#if CAMU_HAVE_FFMPEG + return av_get_bytes_per_sample(fmt->format); +#else + switch (fmt->format) { + case CAMU_SAMPLE_FORMAT_U8: + case CAMU_SAMPLE_FORMAT_U8P: return 1; + case CAMU_SAMPLE_FORMAT_S16: + case CAMU_SAMPLE_FORMAT_S16P: return 2; + case CAMU_SAMPLE_FORMAT_S32: + case CAMU_SAMPLE_FORMAT_S32P: + case CAMU_SAMPLE_FORMAT_FLT: + case CAMU_SAMPLE_FORMAT_FLTP: return 4; + case CAMU_SAMPLE_FORMAT_S64: + case CAMU_SAMPLE_FORMAT_S64P: + case CAMU_SAMPLE_FORMAT_DBL: + case CAMU_SAMPLE_FORMAT_DBLP: return 8; + default: + al_assert(false); + return -1; } #endif +} + +static inline size_t camu_audio_format_samples_to_bytes(struct camu_audio_format *fmt, size_t samples) +{ + s32 bytes_per_sample = camu_audio_format_bytes_per_sample(fmt); + return samples * bytes_per_sample * fmt->channel_count; +} + +static inline f64 camu_audio_format_samples_to_sec(struct camu_audio_format *fmt, size_t samples) +{ + return samples / (f64)fmt->sample_rate; +} + +static inline size_t camu_audio_format_usec_to_bytes(struct camu_audio_format *fmt, size_t usec) +{ + // Keep order of operations for precision. + size_t samples = usec * (fmt->sample_rate / 1000000.0); + return samples * camu_audio_format_bytes_per_sample(fmt) * fmt->channel_count; +} + +static inline size_t camu_audio_format_sec_to_bytes(struct camu_audio_format *fmt, f64 sec) +{ + size_t samples = sec * fmt->sample_rate; + return samples * camu_audio_format_bytes_per_sample(fmt) * fmt->channel_count; +} + +static inline f64 camu_audio_format_bytes_to_sec(struct camu_audio_format *fmt, size_t bytes) +{ + size_t samples = bytes / (camu_audio_format_bytes_per_sample(fmt) * fmt->channel_count); + return samples / (f64)fmt->sample_rate; +} + +static const char *camu_pixel_format_name(s32 format) +{ + switch (format) { + case CAMU_PIXEL_FORMAT_RGBA: return "rgba"; + case CAMU_PIXEL_FORMAT_RGB: return "rgb"; + case CAMU_PIXEL_FORMAT_GREYA: return "grey + alpha"; + case CAMU_PIXEL_FORMAT_GREY: return "grey"; + } + return "(unknown)"; +} + +static inline s32 camu_pixel_format_from_channels(s32 channels) +{ + switch (channels) { + case 4: return CAMU_PIXEL_FORMAT_RGBA; + case 3: return CAMU_PIXEL_FORMAT_RGB; + case 2: return CAMU_PIXEL_FORMAT_GREYA; + case 1: return CAMU_PIXEL_FORMAT_GREY; + default: al_assert(false); return CAMU_PIXEL_FORMAT_GREY; + } +} + +static inline void camu_codec_frame_discard(struct camu_codec_frame *frame) +{ +#ifdef CAMU_HAVE_FFMPEG + if (frame->mode == CAMU_FFMPEG_COMPAT) av_frame_free(&frame->av.frame); +#endif + if (frame->mode == CAMU_NORMAL && frame->data) al_free(frame->data); al_free(frame); } + +AL_UNUSED_FUNCTION_POP diff --git a/src/codec/common.h b/src/codec/common.h index adf503d..98665d7 100644 --- a/src/codec/common.h +++ b/src/codec/common.h @@ -13,17 +13,7 @@ static inline void *camu_page_alloc(size_t size) #endif } -static inline void *camu_page_realloc(void *ptr, size_t old_size, size_t size) +static inline void *camu_page_realloc(void *ptr, size_t size) { -#ifdef AL_HAVE_POSIX_MEMALIGN - void *old_ptr = ptr; - al_posix_memalign(&ptr, al_page_size, size); - if (old_ptr) { - al_memcpy(ptr, old_ptr, old_size); - al_free(old_ptr); - } - return ptr; -#else return al_realloc(ptr, size); -#endif } diff --git a/src/codec/ffmpeg/common.c b/src/codec/ffmpeg/common.c index 57caff7..b7a232e 100644 --- a/src/codec/ffmpeg/common.c +++ b/src/codec/ffmpeg/common.c @@ -1,6 +1,38 @@ +#include <al/lib.h> +#include <al/log.h> +#include <libavutil/log.h> + #include "common.h" void camu_ff_set_log_callback(void (*callback)(void *, int, const char *, va_list)) { av_log_set_callback(callback); } + +static char *av_log_buf = NULL; +static s32 av_log_pos = 0; + +static void av_log_callback(void *userdata, int level, const char *fmt, va_list args) +{ + (void)userdata; + al_assert(av_log_buf); + if (level < AV_LOG_DEBUG) { + av_log_pos += al_vsnprintf(&av_log_buf[av_log_pos], 512, fmt, args); + if (av_log_buf[av_log_pos - 1] == '\n' || av_log_pos >= 512) { + al_log_info("ff_log", av_log_buf); + av_log_pos = 0; + } + } +} + +void camu_ff_set_default_log_callback() +{ + av_log_buf = (char *)al_malloc(1024); + camu_ff_set_log_callback(av_log_callback); +} + + +void camu_ff_free_default_log_callback() +{ + al_free(av_log_buf); +} diff --git a/src/codec/ffmpeg/common.h b/src/codec/ffmpeg/common.h index ded7b35..229ffb0 100644 --- a/src/codec/ffmpeg/common.h +++ b/src/codec/ffmpeg/common.h @@ -1,6 +1,10 @@ #pragma once #include <al/types.h> -#include <libavutil/log.h> +#include <al/log.h> +// userdata, level, fmt, args void camu_ff_set_log_callback(void (*callback)(void *, int, const char *, va_list)); + +void camu_ff_set_default_log_callback(); +void camu_ff_free_default_log_callback(); diff --git a/src/codec/ffmpeg/decoder.c b/src/codec/ffmpeg/decoder.c index 7071bea..d231b2f 100644 --- a/src/codec/ffmpeg/decoder.c +++ b/src/codec/ffmpeg/decoder.c @@ -1,8 +1,6 @@ -#include <libavutil/cpu.h> #include <al/lib.h> #include <al/log.h> - -//#include "../../render/renderer.h" +#include <libavutil/cpu.h> #include "decoder.h" @@ -11,7 +9,8 @@ static void close_internal(struct camu_ff_decoder *av) if (av->codec_context) avcodec_free_context(&av->codec_context); } -static bool ff_decoder_init(struct camu_decoder *dec, struct camu_renderer *renderer, struct camu_stream *stream) +static bool ff_decoder_init(struct camu_decoder *dec, struct camu_renderer *renderer, struct camu_codec_stream *stream, + void (*callback)(void *, struct camu_codec_frame *), void *userdata) { struct camu_ff_decoder *av = (struct camu_ff_decoder *)dec; @@ -37,49 +36,45 @@ static bool ff_decoder_init(struct camu_decoder *dec, struct camu_renderer *rend /* s32 cpus = av_cpu_count(); - cpus = (cpus > 16) ? 4 : cpus / 4; - if (cpus == 0) cpus = 1; + if (cpus > 4) cpus = 2; av->codec_context->thread_count = cpus; + av->codec_context->thread_type = FF_THREAD_SLICE; al_log_debug("ff_decoder", "Using %i threads for decoder.", cpus); */ - (void)renderer; - /* - if (renderer && renderer->get_buffer2 && codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { +#ifndef CAMU_SINK_NO_VIDEO + if (codecpar->codec_type == AVMEDIA_TYPE_VIDEO && renderer && renderer->get_buffer2) { av->codec_context->get_buffer2 = renderer->get_buffer2; av->codec_context->opaque = renderer->opaque; } - */ +#else + (void)renderer; +#endif if (avcodec_open2(av->codec_context, codec, NULL) < 0) { al_log_error("ff_decoder", "Failed to open codec (%s).", codec->name); goto err; } - if (codec->long_name) { - al_log_info("ff_decoder", "Codec: %s (%s) %ldkbps.", codec->name, codec->long_name, codecpar->bit_rate / 1000); - } else { - al_log_info("ff_decoder", "Codec: %s %ldkbps.", codec->name, codecpar->bit_rate / 1000); - } + const char *long_name = codec->long_name ? codec->long_name : codec->name; + s64 kbps = codecpar->bit_rate > 0 ? codecpar->bit_rate / 1000L : 0L; + al_log_info("ff_decoder", "Codec: %s (%s) %ldkbps.", codec->name, long_name, kbps); av->time_base = stream->av.stream->time_base; - av->last_pts = 0; - av->last_duration = 0; + av->duration = stream->av.stream->duration; + //av->last_pts = 0; + //av->last_duration = 0; //av->seek_pos = -1; + av->callback = callback; + av->userdata = userdata; + return true; err: close_internal(av); return false; } -static void ff_decoder_set_callback(struct camu_decoder *dec, void (*callback)(void *, struct camu_frame *), void *userdata) -{ - struct camu_ff_decoder *av = (struct camu_ff_decoder *)dec; - av->callback = callback; - av->userdata = userdata; -} - static s32 send_packet(struct camu_ff_decoder *av, AVPacket *pkt) { /* @@ -92,16 +87,16 @@ static s32 send_packet(struct camu_ff_decoder *av, AVPacket *pkt) s32 ret = avcodec_send_packet(av->codec_context, pkt); if (ret < 0 && ret != AVERROR(EAGAIN) && ret != AVERROR_EOF) { - al_log_error("ff_decoder", "Error sending packet to decoder: (%s).", av_err2str(ret)); + al_log_error("ff_decoder", "Error sending packet to the decoder: (%s).", av_err2str(ret)); } return ret; } -static s32 ff_decoder_push(struct camu_decoder *dec, struct camu_packet *packet) +static s32 ff_decoder_push(struct camu_decoder *dec, struct camu_codec_packet *packet) { struct camu_ff_decoder *av = (struct camu_ff_decoder *)dec; - return send_packet(av, (packet) ? packet->av.pkt : NULL); + return send_packet(av, packet ? packet->av.pkt : NULL); } static void ff_decoder_flush(struct camu_decoder *dec) @@ -114,19 +109,19 @@ static s32 receive_frames(struct camu_ff_decoder *av) { s32 ret; do { - struct camu_frame *frame = al_alloc_object(struct camu_frame); - frame->type = CAMU_FFMPEG_COMPAT; + struct camu_codec_frame *frame = al_alloc_object(struct camu_codec_frame); + frame->mode = CAMU_FFMPEG_COMPAT; frame->av.frame = av_frame_alloc(); ret = avcodec_receive_frame(av->codec_context, frame->av.frame); if (ret < 0) { av_frame_free(&frame->av.frame); al_free(frame); + // Checking for EAGAIN should prevent an infinite loop. if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break; - al_log_error("ff_decoder", "Error receiving packet from decoder: (%s).", av_err2str(ret)); + al_log_error("ff_decoder", "Error receiving packet from the decoder: (%s).", av_err2str(ret)); continue; } - // Track pts and duration of the previous frame so we can handle multiple frames - // in a single packet. + // Track pts and duration of the previous frame so we can handle multiple frames in a single packet. //if (av->codec_context->codec_type == AVMEDIA_TYPE_AUDIO) { // if (frame->av.frame->best_effort_timestamp < 0) { // frame->av.frame->best_effort_timestamp = av->last_pts + av->last_duration; @@ -158,7 +153,6 @@ struct camu_decoder *camu_ff_decoder_create(void) { struct camu_ff_decoder *av = al_alloc_object(struct camu_ff_decoder); av->dec.init = ff_decoder_init; - av->dec.set_callback = ff_decoder_set_callback; av->dec.push = ff_decoder_push; av->dec.process = ff_decoder_process; av->dec.flush = ff_decoder_flush; diff --git a/src/codec/ffmpeg/decoder.h b/src/codec/ffmpeg/decoder.h index 45cca00..88bbd4c 100644 --- a/src/codec/ffmpeg/decoder.h +++ b/src/codec/ffmpeg/decoder.h @@ -1,7 +1,6 @@ #pragma once #include <al/types.h> - #include <libavcodec/avcodec.h> #include "../codec.h" @@ -10,10 +9,11 @@ struct camu_ff_decoder { struct camu_decoder dec; AVCodecContext *codec_context; AVRational time_base; - s64 last_pts; - s64 last_duration; + u64 duration; + //s64 last_pts; + //s64 last_duration; //s64 seek_pos; - void (*callback)(void *, struct camu_frame *); + void (*callback)(void *, struct camu_codec_frame *); void *userdata; }; diff --git a/src/codec/ffmpeg/demuxer.c b/src/codec/ffmpeg/demuxer.c index 9b26596..7ff0dca 100644 --- a/src/codec/ffmpeg/demuxer.c +++ b/src/codec/ffmpeg/demuxer.c @@ -3,7 +3,7 @@ #include "demuxer.h" #include "avio.h" -#define BUF_SIZE 16384 +#define DEMUX_BUF_SIZE (4096 * 4) static void close_internal(struct camu_ff_demuxer *av) { @@ -28,16 +28,16 @@ static bool ff_demuxer_init(struct camu_demuxer *demux, struct cch_handle *handl goto err; } - u8 *buf = (u8 *)av_malloc(BUF_SIZE); - av->io_context = avio_alloc_context(buf, BUF_SIZE, 0, handle, camu_avio_read, NULL, camu_avio_seek); + u8 *buf = (u8 *)av_malloc(DEMUX_BUF_SIZE); + av->io_context = avio_alloc_context(buf, DEMUX_BUF_SIZE, 0, handle, camu_avio_read, NULL, camu_avio_seek); if (!av->io_context) { al_log_error("ff_demuxer", "Failed to create custom io context."); goto err; } if (!cch_handle_can_seek(handle)) { - // This should always be unset if we know we cannot seek. - // But, should also never be unset if we *do* want to seek because it + // AVIO_SEEKABLE_NORMAL should always be unset if we know we cannot seek. + // However, it should also never be unset if we *do* want to seek because it // disables long seeks (Not tested in a long time). av->io_context->seekable &= ~AVIO_SEEKABLE_NORMAL; } else { @@ -47,12 +47,19 @@ static bool ff_demuxer_init(struct camu_demuxer *demux, struct cch_handle *handl av->format_context->pb = av->io_context; av->format_context->flags |= AVFMT_FLAG_CUSTOM_IO; - if (avformat_open_input(&av->format_context, "", 0, 0) < 0) { + AVDictionary *opts = NULL; + av_dict_set(&opts, "analyzeduration", "10000000", 0); + av_dict_set(&opts, "probesize", "20M", 0); + + if (avformat_open_input(&av->format_context, "file:", NULL, &opts) < 0) { al_log_error("ff_demuxer", "Failed to open input."); + av_dict_free(&opts); goto err; } - if (avformat_find_stream_info(av->format_context, 0) < 0) { + av_dict_free(&opts); + + if (avformat_find_stream_info(av->format_context, NULL) < 0) { al_log_error("ff_demuxer", "Failed to find stream info."); goto err; } @@ -70,6 +77,7 @@ static bool ff_demuxer_init(struct camu_demuxer *demux, struct cch_handle *handl 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) type = AVMEDIA_TYPE_UNKNOWN; // Try to detect attached images. if (type == AVMEDIA_TYPE_VIDEO && GUESS_STREAM_IS_IMAGE(stream)) { al_log_info("ff_demuxer", "Guessing that stream #%u is an image.", i); @@ -83,12 +91,14 @@ static bool ff_demuxer_init(struct camu_demuxer *demux, struct cch_handle *handl } al_log_info("ff_demuxer", "Setting stream #%u to a duration of %.3fs.", i, stream->duration * av_q2d(stream->time_base)); } - if (stream->start_time < 0) stream->start_time = 0; + if (stream->start_time == AV_NOPTS_VALUE) stream->start_time = 0; s64 duration = av_rescale_q(stream->duration, stream->time_base, AV_TIME_BASE_Q); al_assert(duration >= 0); if ((u64)duration > av->duration) av->duration = (u64)duration; - al_array_push(av->demux.streams, ((struct camu_stream){ - .type = CAMU_FFMPEG_COMPAT, + al_array_push(av->demux.streams, ((struct camu_codec_stream){ + .mode = CAMU_FFMPEG_COMPAT, + .type = type, + .duration = (u64)duration, .av.stream = stream })); } @@ -106,30 +116,15 @@ err: return false; } -/* -static AVStream *av_stream_at_index(struct camu_ff_demuxer *av, s32 index) -{ - struct camu_stream *stream; - al_array_foreach_ptr(av->demux.streams, i, stream) { - if (stream->av.stream->index == index) return stream->av.stream; - } - return NULL; -} -*/ - 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; + return ((demux->subscribed >> index) & 1); } -static s32 ff_demuxer_get_packet(struct camu_demuxer *demux, struct camu_packet *packet) +static s32 ff_demuxer_get_packet(struct camu_demuxer *demux, struct camu_codec_packet *packet) { struct camu_ff_demuxer *av = (struct camu_ff_demuxer *)demux; - packet->type = CAMU_FFMPEG_COMPAT; + packet->mode = CAMU_FFMPEG_COMPAT; s32 ret = 0; do { ret = av_read_frame(av->format_context, packet->av.pkt); @@ -160,12 +155,13 @@ static bool ff_demuxer_seek(struct camu_demuxer *demux, u64 pos) { struct camu_ff_demuxer *av = (struct camu_ff_demuxer *)demux; if (pos > av->duration) pos = av->duration; + al_assert(pos <= INT64_MAX); s64 ts = av_rescale_q((s64)pos, (AVRational){ 1, 1000000 }, AV_TIME_BASE_Q); if (av->eof) { avformat_flush(av->format_context); av->eof = false; } - if (avformat_seek_file(av->format_context, -1, INT64_MIN, ts, ts, AVSEEK_FLAG_BACKWARD) < 0) { + if (avformat_seek_file(av->format_context, -1, INT64_MIN, ts, ts, 0) < 0) { return false; } av->seeked = true; @@ -184,9 +180,8 @@ static void ff_demuxer_free(struct camu_demuxer **demux) struct camu_demuxer *camu_ff_demuxer_create(void) { struct camu_ff_demuxer *av = al_alloc_object(struct camu_ff_demuxer); - av->demux.type = CAMU_FFMPEG_COMPAT; + av->demux.mode = CAMU_FFMPEG_COMPAT; al_array_init(av->demux.streams); - al_array_init(av->demux.subscribed); av->demux.init = ff_demuxer_init; av->demux.get_packet = ff_demuxer_get_packet; av->demux.get_duration = ff_demuxer_get_duration; diff --git a/src/codec/ffmpeg/demuxer.h b/src/codec/ffmpeg/demuxer.h index 8327534..68ad03e 100644 --- a/src/codec/ffmpeg/demuxer.h +++ b/src/codec/ffmpeg/demuxer.h @@ -3,7 +3,6 @@ #include <al/types.h> #include <al/lib.h> #include <al/array.h> - #include <libavformat/avformat.h> #include "../codec.h" diff --git a/src/codec/ffmpeg/encoder.c b/src/codec/ffmpeg/encoder.c index 4617baf..83a8b55 100644 --- a/src/codec/ffmpeg/encoder.c +++ b/src/codec/ffmpeg/encoder.c @@ -1,3 +1,5 @@ +#include <al/lib.h> +#include <al/log.h> #include <libavcodec/avcodec.h> #include <libavcodec/codec.h> #include <libavformat/avformat.h> @@ -6,13 +8,12 @@ #include <libavutil/error.h> #include <libavutil/frame.h> #include <libavutil/samplefmt.h> -#include <al/lib.h> -#include <al/log.h> #include "encoder.h" #define BITRATE 256000 -#define FRAME_SIZE 20 +#define FRAME_DURATION 20 +#define SAMPLES_PER_FRAME (48000 / 1000.0 * FRAME_DURATION) #define CHANNELS 2 static void free_frame_data(struct camu_ff_encoder *enc) @@ -20,15 +21,6 @@ static void free_frame_data(struct camu_ff_encoder *enc) if (enc->frame && enc->frame->nb_samples > 0) av_freep(&enc->frame->data[0]); } -static void alloc_frame_data(struct camu_ff_encoder *enc, s32 frame_size) -{ - if (enc->frame->nb_samples >= frame_size) return; - free_frame_data(enc); - s32 nb_channels = enc->frame->ch_layout.nb_channels; - av_samples_alloc(enc->frame->data, NULL, nb_channels, frame_size, enc->frame->format, 0); - enc->frame->nb_samples = frame_size; -} - bool camu_ff_encoder_init(struct camu_ff_encoder *enc, char *encoder_name, char *ext) { al_memset(enc, 0, sizeof(struct camu_ff_encoder)); @@ -51,7 +43,7 @@ bool camu_ff_encoder_init(struct camu_ff_encoder *enc, char *encoder_name, char av_channel_layout_default(&enc->codec_context->ch_layout, CHANNELS); enc->codec_context->sample_fmt = AV_SAMPLE_FMT_FLT; enc->codec_context->bit_rate = BITRATE; - enc->codec_context->frame_size = FRAME_SIZE; + enc->codec_context->frame_size = SAMPLES_PER_FRAME; enc->codec_context->time_base = (AVRational){ 1, enc->codec_context->sample_rate }; enc->codec_context->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL; @@ -77,23 +69,17 @@ bool camu_ff_encoder_init(struct camu_ff_encoder *enc, char *encoder_name, char AVCodecContext *out_codec = enc->codec_context; - av_channel_layout_copy(&enc->fmt.req_channel_layout, &out_codec->ch_layout); - enc->fmt.req_sample_rate = out_codec->sample_rate; - enc->fmt.req_format = out_codec->sample_fmt; - - enc->audio_fifo = av_audio_fifo_alloc(enc->fmt.req_format, - enc->fmt.req_channel_layout.nb_channels, 1); - - if (!enc->audio_fifo) { - al_log_error("ff_encoder", "Failed to alloc fifo."); - goto err; - } + enc->fmt.req.format = out_codec->sample_fmt; + enc->fmt.req.sample_rate = out_codec->sample_rate; + av_channel_layout_copy(&enc->fmt.req.channel_layout, &out_codec->ch_layout); + enc->fmt.req.channel_count = out_codec->ch_layout.nb_channels; enc->frame = av_frame_alloc(); - enc->frame->nb_samples = 0; - enc->frame->sample_rate = enc->fmt.req_sample_rate; - av_channel_layout_copy(&enc->frame->ch_layout, &enc->fmt.req_channel_layout); - enc->frame->format = enc->fmt.req_format; + enc->frame->format = enc->fmt.req.format; + enc->frame->sample_rate = enc->fmt.req.sample_rate; + av_channel_layout_copy(&enc->frame->ch_layout, &enc->fmt.req.channel_layout); + av_samples_alloc(enc->frame->data, NULL, enc->frame->ch_layout.nb_channels, SAMPLES_PER_FRAME, enc->frame->format, 1); + enc->frame->nb_samples = SAMPLES_PER_FRAME; enc->frame->pts = 0; return true; @@ -104,93 +90,31 @@ err: void camu_ff_encoder_push(struct camu_ff_encoder *enc, u8 **data, s32 sample_count) { - s32 alloc_size = av_audio_fifo_size(enc->audio_fifo) + sample_count; - if (av_audio_fifo_space(enc->audio_fifo) < alloc_size) { - if (av_audio_fifo_realloc(enc->audio_fifo, alloc_size) < 0) { - al_assert(false); - } + al_assert(sample_count == SAMPLES_PER_FRAME * CHANNELS); + enc->frame->data[0] = data[0]; + s32 ret = avcodec_send_frame(enc->codec_context, enc->frame); + if (ret < 0 && ret != AVERROR_EOF) { + al_log_error("ff_encoder", "Error sending frame to the encoder: (%s).", av_err2str(ret)); + return; } - av_audio_fifo_write(enc->audio_fifo, (void **)data, sample_count); -} - -static bool receive_packets(struct camu_ff_encoder *enc) -{ - s32 ret; AVPacket pkt = { 0 }; - - do { - ret = avcodec_receive_packet(enc->codec_context, &pkt); - if (ret < 0) break; - enc->callback(enc->userdata, &pkt); - } while (1); - - if (ret != AVERROR(EAGAIN) && ret != AVERROR_EOF) { - al_log_error("ff_encoder", "Error receiving packet from encoder: (%s).", av_err2str(ret)); - return false; - } - - return true; -} - -static s32 send_frame(struct camu_ff_encoder *enc, AVFrame *in_frame) -{ - s32 ret = avcodec_send_frame(enc->codec_context, in_frame); + ret = avcodec_receive_packet(enc->codec_context, &pkt); if (ret < 0 && ret != AVERROR_EOF) { - al_log_error("ff_encoder", "Error sending frame to encoder: (%s).", av_err2str(ret)); + al_log_error("ff_encoder", "Error receiving packet from the encoder: (%s).", av_err2str(ret)); + return; } - return ret; -} - -static s32 read_frame(struct camu_ff_encoder *enc, s32 frame_size) -{ - alloc_frame_data(enc, frame_size); - return av_audio_fifo_read(enc->audio_fifo, (void **)enc->frame->data, frame_size); -} - -bool camu_ff_encoder_process(struct camu_ff_encoder *enc, bool flush) -{ - s32 samples_available = av_audio_fifo_size(enc->audio_fifo); - if (samples_available == 0 && flush) { - // Flush encoder. - send_frame(enc, NULL); - receive_packets(enc); - // This should guarantee that there are not packets left. - return false; - } - - s32 frame_size = enc->codec_context->frame_size; - if (samples_available == 0 || (samples_available < frame_size && !flush)) { - return false; - } - - s32 ret; - do { - if (samples_available < frame_size) frame_size = samples_available; - read_frame(enc, frame_size); - samples_available -= frame_size; - do { - ret = send_frame(enc, enc->frame); - if (ret < 0) break; - if (!receive_packets(enc)) return false; - } while (ret == AVERROR(EAGAIN)); - if (ret < 0) break; - enc->frame->pts += frame_size; - } while (samples_available > 0); - - // ret < 0 means encoder errored while sending frame. - return ret == 0; + enc->callback(enc->userdata, &pkt); + enc->frame->pts += SAMPLES_PER_FRAME; } void camu_ff_encoder_reset(struct camu_ff_encoder *enc) { avcodec_flush_buffers(enc->codec_context); - av_audio_fifo_reset(enc->audio_fifo); } void camu_ff_encoder_close(struct camu_ff_encoder *enc) { if (enc->codec_context) avcodec_free_context(&enc->codec_context); - if (enc->audio_fifo) av_audio_fifo_free(enc->audio_fifo); free_frame_data(enc); if (enc->frame) av_frame_free(&enc->frame); } diff --git a/src/codec/ffmpeg/encoder.h b/src/codec/ffmpeg/encoder.h index 9bfe8ea..c0d953c 100644 --- a/src/codec/ffmpeg/encoder.h +++ b/src/codec/ffmpeg/encoder.h @@ -5,16 +5,14 @@ #include <libavutil/audio_fifo.h> #include <libavutil/rational.h> -#include "resampler.h" +#include "../codec.h" struct camu_ff_encoder { AVCodecContext *codec_context; const AVOutputFormat *output_format; - struct camu_ff_resample_fmt fmt; - + struct camu_resampler_format fmt; AVFrame *frame; - AVAudioFifo *audio_fifo; void (*callback)(void *, AVPacket *); void *userdata; @@ -22,6 +20,5 @@ struct camu_ff_encoder { bool camu_ff_encoder_init(struct camu_ff_encoder *enc, char *encoder_name, char *ext); void camu_ff_encoder_push(struct camu_ff_encoder *enc, u8 **data, s32 sample_count); -bool camu_ff_encoder_process(struct camu_ff_encoder *enc, bool flush); void camu_ff_encoder_reset(struct camu_ff_encoder *enc); void camu_ff_encoder_close(struct camu_ff_encoder *enc); diff --git a/src/codec/ffmpeg/packet_ext.c b/src/codec/ffmpeg/packet_ext.c index 8fe4c39..c0e400d 100644 --- a/src/codec/ffmpeg/packet_ext.c +++ b/src/codec/ffmpeg/packet_ext.c @@ -16,6 +16,7 @@ void aki_packet_write_av_codec_parameters(struct aki_packet *packet, AVCodecPara AKI_PACKET_WRITE_TYPE(packet, s32, codecpar->width); AKI_PACKET_WRITE_TYPE(packet, s32, codecpar->height); AKI_PACKET_WRITE_TYPE(packet, AVRational, codecpar->sample_aspect_ratio); + AKI_PACKET_WRITE_TYPE(packet, AVRational, codecpar->framerate); AKI_PACKET_WRITE_TYPE(packet, enum AVFieldOrder, codecpar->field_order); AKI_PACKET_WRITE_TYPE(packet, enum AVColorRange, codecpar->color_range); AKI_PACKET_WRITE_TYPE(packet, enum AVColorPrimaries, codecpar->color_primaries); @@ -23,18 +24,13 @@ void aki_packet_write_av_codec_parameters(struct aki_packet *packet, AVCodecPara AKI_PACKET_WRITE_TYPE(packet, enum AVColorSpace, codecpar->color_space); AKI_PACKET_WRITE_TYPE(packet, enum AVChromaLocation, codecpar->chroma_location); AKI_PACKET_WRITE_TYPE(packet, s32, codecpar->video_delay); + AKI_PACKET_WRITE_TYPE(packet, AVChannelLayout, codecpar->ch_layout); AKI_PACKET_WRITE_TYPE(packet, s32, codecpar->sample_rate); AKI_PACKET_WRITE_TYPE(packet, s32, codecpar->block_align); AKI_PACKET_WRITE_TYPE(packet, s32, codecpar->frame_size); AKI_PACKET_WRITE_TYPE(packet, s32, codecpar->initial_padding); AKI_PACKET_WRITE_TYPE(packet, s32, codecpar->trailing_padding); AKI_PACKET_WRITE_TYPE(packet, s32, codecpar->seek_preroll); - AKI_PACKET_WRITE_TYPE(packet, AVChannelLayout, codecpar->ch_layout); -} - -void aki_packet_write_av_rational(struct aki_packet *packet, AVRational rational) -{ - AKI_PACKET_WRITE_TYPE(packet, AVRational, rational); } void aki_packet_write_av_codec_id(struct aki_packet *packet, enum AVCodecID codec_id) @@ -46,15 +42,15 @@ void aki_packet_write_av_stream(struct aki_packet *packet, AVStream *stream) { AKI_PACKET_WRITE_TYPE(packet, s32, stream->index); aki_packet_write_av_codec_parameters(packet, stream->codecpar); - aki_packet_write_av_rational(packet, stream->time_base); + AKI_PACKET_WRITE_TYPE(packet, AVRational, stream->time_base); AKI_PACKET_WRITE_TYPE(packet, s64, stream->duration); AKI_PACKET_WRITE_TYPE(packet, s64, stream->start_time); AKI_PACKET_WRITE_TYPE(packet, s64, stream->nb_frames); if (stream->avg_frame_rate.den == 0) { // r_frame_rate.den == 0 handled on the client. - aki_packet_write_av_rational(packet, stream->r_frame_rate); + AKI_PACKET_WRITE_TYPE(packet, AVRational, stream->r_frame_rate); } else { - aki_packet_write_av_rational(packet, stream->avg_frame_rate); + AKI_PACKET_WRITE_TYPE(packet, AVRational, stream->avg_frame_rate); } } @@ -68,7 +64,9 @@ void aki_packet_write_av_packet(struct aki_packet *packet, AVPacket *pkt) AKI_PACKET_WRITE_TYPE(packet, s32, pkt->flags); AKI_PACKET_WRITE_TYPE(packet, s32, pkt->side_data_elems); for (s32 i = 0; i < pkt->side_data_elems; i++) { - AKI_PACKET_WRITE_TYPE(packet, size_t, pkt->side_data[i].size); + // Possibly upcast size_t. + s64 size = (s64)pkt->side_data[i].size; + AKI_PACKET_WRITE_TYPE(packet, s64, size); AKI_PACKET_WRITE_DATA(packet, pkt->side_data[i].data, pkt->side_data[i].size); AKI_PACKET_WRITE_TYPE(packet, enum AVPacketSideDataType, pkt->side_data[i].type); } @@ -96,6 +94,7 @@ void aki_packet_read_av_codec_parameters(struct aki_packet *packet, AVCodecParam AKI_PACKET_READ_TYPE(packet, s32, codecpar->width); AKI_PACKET_READ_TYPE(packet, s32, codecpar->height); AKI_PACKET_READ_TYPE(packet, AVRational, codecpar->sample_aspect_ratio); + AKI_PACKET_READ_TYPE(packet, AVRational, codecpar->framerate); AKI_PACKET_READ_TYPE(packet, enum AVFieldOrder, codecpar->field_order); AKI_PACKET_READ_TYPE(packet, enum AVColorRange, codecpar->color_range); AKI_PACKET_READ_TYPE(packet, enum AVColorPrimaries, codecpar->color_primaries); @@ -103,20 +102,13 @@ void aki_packet_read_av_codec_parameters(struct aki_packet *packet, AVCodecParam AKI_PACKET_READ_TYPE(packet, enum AVColorSpace, codecpar->color_space); AKI_PACKET_READ_TYPE(packet, enum AVChromaLocation, codecpar->chroma_location); AKI_PACKET_READ_TYPE(packet, s32, codecpar->video_delay); + AKI_PACKET_READ_TYPE(packet, AVChannelLayout, codecpar->ch_layout); AKI_PACKET_READ_TYPE(packet, s32, codecpar->sample_rate); AKI_PACKET_READ_TYPE(packet, s32, codecpar->block_align); AKI_PACKET_READ_TYPE(packet, s32, codecpar->frame_size); AKI_PACKET_READ_TYPE(packet, s32, codecpar->initial_padding); AKI_PACKET_READ_TYPE(packet, s32, codecpar->trailing_padding); AKI_PACKET_READ_TYPE(packet, s32, codecpar->seek_preroll); - AKI_PACKET_READ_TYPE(packet, AVChannelLayout, codecpar->ch_layout); -} - -AVRational aki_packet_read_av_rational(struct aki_packet *packet) -{ - AVRational r; - AKI_PACKET_READ_TYPE(packet, AVRational, r); - return r; } enum AVCodecID aki_packet_read_av_codec_id(struct aki_packet *packet) @@ -131,11 +123,11 @@ AVStream *aki_packet_read_av_stream(AVFormatContext *format_context, const AVCod AVStream *stream = avformat_new_stream(format_context, codec); AKI_PACKET_READ_TYPE(packet, s32, stream->index); aki_packet_read_av_codec_parameters(packet, stream->codecpar); - stream->time_base = aki_packet_read_av_rational(packet); + AKI_PACKET_READ_TYPE(packet, AVRational, stream->time_base); AKI_PACKET_READ_TYPE(packet, s64, stream->duration); AKI_PACKET_READ_TYPE(packet, s64, stream->start_time); AKI_PACKET_READ_TYPE(packet, s64, stream->nb_frames); - stream->avg_frame_rate = aki_packet_read_av_rational(packet); + AKI_PACKET_READ_TYPE(packet, AVRational, stream->avg_frame_rate); return stream; } @@ -152,10 +144,12 @@ AVPacket *aki_packet_read_av_packet(struct aki_packet *packet) AKI_PACKET_READ_TYPE(packet, s32, side_data_elems); pkt->side_data_elems = 0; for (s32 i = 0; i < side_data_elems; i++) { - size_t size; u8 *data; enum AVPacketSideDataType type; - AKI_PACKET_READ_TYPE(packet, s32, size); + s64 storage_for_size; + AKI_PACKET_READ_TYPE(packet, s64, storage_for_size); + al_assert((sizeof(size_t) > 4 || storage_for_size <= INT32_MAX)); + size_t size = (size_t)storage_for_size; AKI_PACKET_READ_DATA(packet, size, data); AKI_PACKET_READ_TYPE(packet, enum AVPacketSideDataType, type); u8 *side_data = av_packet_new_side_data(pkt, type, size); diff --git a/src/codec/ffmpeg/packet_ext.h b/src/codec/ffmpeg/packet_ext.h index ff6e337..36b6ed5 100644 --- a/src/codec/ffmpeg/packet_ext.h +++ b/src/codec/ffmpeg/packet_ext.h @@ -8,13 +8,11 @@ #include <libavformat/avformat.h> void aki_packet_write_av_codec_parameters(struct aki_packet *packet, AVCodecParameters *codecpar); -void aki_packet_write_av_rational(struct aki_packet *packet, AVRational rational); void aki_packet_write_av_codec_id(struct aki_packet *packet, enum AVCodecID codec_id); void aki_packet_write_av_stream(struct aki_packet *packet, AVStream *stream); void aki_packet_write_av_packet(struct aki_packet *packet, AVPacket *pkt); void aki_packet_read_av_codec_parameters(struct aki_packet *packet, AVCodecParameters *codecpar); -AVRational aki_packet_read_av_rational(struct aki_packet *packet); enum AVCodecID aki_packet_read_av_codec_id(struct aki_packet *packet); AVStream *aki_packet_read_av_stream(AVFormatContext *format_context, const AVCodec *codec, struct aki_packet *packet); AVPacket *aki_packet_read_av_packet(struct aki_packet *packet); diff --git a/src/codec/ffmpeg/resampler.c b/src/codec/ffmpeg/resampler.c index d4b281a..00418e6 100644 --- a/src/codec/ffmpeg/resampler.c +++ b/src/codec/ffmpeg/resampler.c @@ -1,161 +1,108 @@ +#include <al/lib.h> +#include <al/log.h> #include <libavutil/channel_layout.h> #include <libavutil/frame.h> -#include <libavutil/opt.h> #include <libavutil/samplefmt.h> +#include <libavutil/opt.h> #include <libswresample/swresample.h> -#include <al/lib.h> -#include <al/log.h> #include "resampler.h" -static bool in_req_matches(struct camu_ff_resample_fmt *fmt) +static bool ff_resampler_init(struct camu_resampler *resamp, struct camu_resampler_format *fmt) { - return (fmt->in_format == fmt->req_format) && - (av_channel_layout_compare(&fmt->in_channel_layout, &fmt->req_channel_layout) == 0) && - (fmt->in_sample_rate == fmt->req_sample_rate); -} - -bool camu_ff_resampler_init(struct camu_ff_resampler *resamp, struct camu_ff_resample_fmt *fmt) -{ - al_memset(resamp, 0, sizeof(struct camu_ff_resampler)); - - fmt->resampler_needed = !in_req_matches(fmt); - if (!fmt->resampler_needed) return true; + struct camu_ff_resampler *av = (struct camu_ff_resampler *)resamp; + al_assert(fmt->resampler_needed); - camu_ff_resample_fmt_copy(&resamp->fmt, fmt); - - SwrContext *ctx = NULL; - s32 ret = swr_alloc_set_opts2(&ctx, - &fmt->req_channel_layout, - fmt->req_format, - fmt->req_sample_rate, - &fmt->in_channel_layout, - fmt->in_format, - fmt->in_sample_rate, - 0, - NULL); + camu_audio_format_copy(&av->fmt, &fmt->req); + s32 ret = swr_alloc_set_opts2(&av->resample_context, + &fmt->req.channel_layout, fmt->req.format, fmt->req.sample_rate, + &fmt->in.channel_layout, fmt->in.format, fmt->in.sample_rate, 0, NULL); if (ret != 0) { al_log_error("lav_resampler", "Failed to configure resampler context."); return false; } +#ifndef _WIN32 // Slower but more accurate resampler. - //av_opt_set_int(ctx, "resampler", SWR_ENGINE_SOXR, 0); + av_opt_set_int(av->resample_context, "resampler", SWR_ENGINE_SOXR, 0); +#endif - if (swr_init(ctx) != 0) { - swr_free(&ctx); - al_log_error("lav_resampler", "Failed to open resampler context."); + if (swr_init(av->resample_context) != 0) { + swr_free(&av->resample_context); + al_log_error("lav_resampler", "Failed to init resampler context."); return false; } - resamp->resample_context = ctx; - return true; } -s32 camu_ff_resampler_sample_count(struct camu_ff_resampler *resamp, s32 in_samples) -{ - return swr_get_out_samples(resamp->resample_context, in_samples); - //return av_rescale_rnd(swr_get_delay(resamp->resample_context, resamp->fmt.req_sample_rate) + - // in_samples, resamp->fmt.in_sample_rate, resamp->fmt.req_sample_rate, AV_ROUND_UP); -} - -static inline void free_sample_data(struct camu_ff_resampler *resamp) +static inline void free_sample_data(struct camu_ff_resampler *av) { - if (resamp->data[0]) av_freep(&resamp->data[0]); + if (av->data[0]) av_freep(&av->data[0]); } -static inline void alloc_sample_data(struct camu_ff_resampler *resamp, s32 sample_count) +static inline void alloc_sample_data(struct camu_ff_resampler *av, s32 sample_count) { - if (resamp->sample_count >= sample_count) return; - free_sample_data(resamp); - s32 nb_channels = resamp->fmt.req_channel_layout.nb_channels; - av_samples_alloc(resamp->data, NULL, nb_channels, sample_count, resamp->fmt.req_format, 32); - resamp->sample_count = sample_count; + if (av->sample_count >= sample_count) return; + free_sample_data(av); + s32 nb_channels = av->fmt.channel_layout.nb_channels; + av_samples_alloc(av->data, NULL, nb_channels, sample_count, av->fmt.format, 32); + av->sample_count = sample_count; } -s32 camu_ff_resampler_convert(struct camu_ff_resampler *resamp, const u8 **in_data, s32 in_samples) +static s32 ff_resampler_convert(struct camu_resampler *resamp, const u8 **in_data, s32 in_samples) { + struct camu_ff_resampler *av = (struct camu_ff_resampler *)resamp; al_assert(in_samples > 0); - s32 max_resample_count = camu_ff_resampler_sample_count(resamp, in_samples); - alloc_sample_data(resamp, max_resample_count); - s32 resample_count = swr_convert(resamp->resample_context, resamp->data, - max_resample_count, in_data, in_samples); + s32 out_resample_count = swr_get_out_samples(av->resample_context, in_samples); + alloc_sample_data(av, out_resample_count); + s32 resample_count = swr_convert(av->resample_context, av->data, + out_resample_count, in_data, in_samples); if (resample_count < 0) { - al_log_error("lav_resampler", "Failed to convert samples (%i).", resample_count); + al_log_error("lav_resampler", "Failed to convert samples (%d).", resample_count); } return resample_count; } -u8 **camu_ff_resampler_get_data(struct camu_ff_resampler *resamp) +static s32 ff_resampler_flush(struct camu_resampler *resamp) { - return (u8 **)resamp->data; -} - -void camu_ff_resampler_close(struct camu_ff_resampler *resamp) -{ - if (resamp->resample_context) { - swr_close(resamp->resample_context); - swr_free(&resamp->resample_context); + struct camu_ff_resampler *av = (struct camu_ff_resampler *)resamp; + s32 resample_count = swr_convert(av->resample_context, av->data, + av->sample_count, NULL, 0); + if (swr_init(av->resample_context) != 0) { + swr_free(&av->resample_context); + al_log_error("lav_resampler", "Failed to re-init resampler context after flush."); + return -1; } - free_sample_data(resamp); -} - -void camu_ff_resample_fmt_copy(struct camu_ff_resample_fmt *dest, struct camu_ff_resample_fmt *src) -{ - dest->resampler_needed = src->resampler_needed; - dest->in_format = src->in_format; - av_channel_layout_copy(&dest->in_channel_layout, &src->in_channel_layout); - dest->in_sample_rate = src->in_sample_rate; - dest->req_format = src->req_format; - av_channel_layout_copy(&dest->req_channel_layout, &src->req_channel_layout); - dest->req_sample_rate = src->req_sample_rate; -} - -size_t camu_ff_resample_fmt_bytes_per_sample(struct camu_ff_resample_fmt *fmt) -{ - return (size_t)av_get_bytes_per_sample(fmt->req_format); -} - -size_t camu_ff_resample_fmt_samples_to_bytes(struct camu_ff_resample_fmt *fmt, size_t samples) -{ - s32 size; - s32 ret = av_samples_get_buffer_size(&size, fmt->req_channel_layout.nb_channels, (s32)samples, fmt->req_format, 1); - al_assert(ret >= 0); - return (size_t)size; -} - -size_t camu_ff_resample_fmt_samples_to_usec(struct camu_ff_resample_fmt *fmt, size_t samples) -{ - return samples / (fmt->req_sample_rate / 1000000.0); -} - -f64 camu_ff_resample_fmt_samples_to_sec(struct camu_ff_resample_fmt *fmt, size_t samples) -{ - return samples / (f64)fmt->req_sample_rate; -} - -size_t camu_ff_resample_fmt_usec_to_bytes(struct camu_ff_resample_fmt *fmt, size_t usec) -{ - size_t samples = usec * (fmt->req_sample_rate / 1000000.0); - return samples * (camu_ff_resample_fmt_bytes_per_sample(fmt) * fmt->req_channel_layout.nb_channels); + return resample_count; } -size_t camu_ff_resample_fmt_sec_to_bytes(struct camu_ff_resample_fmt *fmt, f64 sec) +static u8 **ff_resampler_get_data(struct camu_resampler *resamp) { - size_t samples = sec * fmt->req_sample_rate; - return samples * (camu_ff_resample_fmt_bytes_per_sample(fmt) * fmt->req_channel_layout.nb_channels); + struct camu_ff_resampler *av = (struct camu_ff_resampler *)resamp; + return (u8 **)av->data; } -size_t camu_ff_resample_fmt_bytes_to_usec(struct camu_ff_resample_fmt *fmt, size_t bytes) +static void ff_resampler_free(struct camu_resampler **resamp) { - size_t samples = bytes / (camu_ff_resample_fmt_bytes_per_sample(fmt) * fmt->req_channel_layout.nb_channels); - return samples / (fmt->req_sample_rate / 1000000.0); + struct camu_ff_resampler *av = (struct camu_ff_resampler *)*resamp; + if (av->resample_context) { + swr_close(av->resample_context); + swr_free(&av->resample_context); + } + free_sample_data(av); + al_free(av); + *resamp = NULL; } -f64 camu_ff_resample_fmt_bytes_to_sec(struct camu_ff_resample_fmt *fmt, size_t bytes) +struct camu_resampler *camu_ff_resampler_create(void) { - size_t samples = bytes / (camu_ff_resample_fmt_bytes_per_sample(fmt) * fmt->req_channel_layout.nb_channels); - return samples / ((f64)fmt->req_sample_rate); + struct camu_ff_resampler *av = al_alloc_object(struct camu_ff_resampler); + av->resamp.init = ff_resampler_init; + av->resamp.convert = ff_resampler_convert; + av->resamp.flush = ff_resampler_flush; + av->resamp.get_data = ff_resampler_get_data; + av->resamp.free = ff_resampler_free; + return (struct camu_resampler *)av; } diff --git a/src/codec/ffmpeg/resampler.h b/src/codec/ffmpeg/resampler.h index 4f7b07d..35e1158 100644 --- a/src/codec/ffmpeg/resampler.h +++ b/src/codec/ffmpeg/resampler.h @@ -1,40 +1,16 @@ #pragma once -#include <libswresample/swresample.h> -#include <libavutil/opt.h> #include <al/types.h> +#include <libswresample/swresample.h> -struct camu_ff_resample_fmt { - bool resampler_needed; - enum AVSampleFormat in_format; - enum AVSampleFormat req_format; - s32 in_channel_count; - s32 req_channel_count; - AVChannelLayout in_channel_layout; - AVChannelLayout req_channel_layout; - s32 in_sample_rate; - s32 req_sample_rate; -}; +#include "../codec.h" struct camu_ff_resampler { + struct camu_resampler resamp; SwrContext *resample_context; - struct camu_ff_resample_fmt fmt; + struct camu_audio_format fmt; u8 *data[AV_NUM_DATA_POINTERS]; s32 sample_count; }; -bool camu_ff_resampler_init(struct camu_ff_resampler *resamp, struct camu_ff_resample_fmt *fmt); -s32 camu_ff_resampler_sample_count(struct camu_ff_resampler *resamp, s32 in_samples); -s32 camu_ff_resampler_convert(struct camu_ff_resampler *resamp, const u8 **in_data, s32 in_samples); -u8 **camu_ff_resampler_get_data(struct camu_ff_resampler *resamp); -void camu_ff_resampler_close(struct camu_ff_resampler *resamp); - -void camu_ff_resample_fmt_copy(struct camu_ff_resample_fmt *dest, struct camu_ff_resample_fmt *src); -size_t camu_ff_resample_fmt_bytes_per_sample(struct camu_ff_resample_fmt *fmt); -size_t camu_ff_resample_fmt_samples_to_bytes(struct camu_ff_resample_fmt *fmt, size_t samples); -size_t camu_ff_resample_fmt_samples_to_usec(struct camu_ff_resample_fmt *fmt, size_t samples); -f64 camu_ff_resample_fmt_samples_to_sec(struct camu_ff_resample_fmt *fmt, size_t samples); -size_t camu_ff_resample_fmt_usec_to_bytes(struct camu_ff_resample_fmt *fmt, size_t usec); -size_t camu_ff_resample_fmt_sec_to_bytes(struct camu_ff_resample_fmt *fmt, f64 sec); -size_t camu_ff_resample_fmt_bytes_to_usec(struct camu_ff_resample_fmt *fmt, size_t bytes); -f64 camu_ff_resample_fmt_bytes_to_sec(struct camu_ff_resample_fmt *fmt, size_t bytes); +struct camu_resampler *camu_ff_resampler_create(void); diff --git a/src/codec/meson.build b/src/codec/meson.build index f3b604f..7a01040 100644 --- a/src/codec/meson.build +++ b/src/codec/meson.build @@ -1,71 +1,77 @@ -ffmpeg_src = [ - 'ffmpeg/common.c', - 'ffmpeg/packet_ext.c' -] -ffmpeg_server_src = [ - ffmpeg_src, - 'ffmpeg/demuxer.c', - 'ffmpeg/encoder.c', - 'ffmpeg/avio.c' -] -ffmpeg_client_src = [ - ffmpeg_src, - 'ffmpeg/decoder.c', - 'ffmpeg/resampler.c' -] -ffmpeg_inc = [] -ffmpeg_args = [] -ffmpeg_server_deps = [] -ffmpeg_client_deps = [] -ffmpeg_client_use_scaler = false +codec_deps = [] -if ffmpeg_client_use_scaler - ffmpeg_client_src += ['ffmpeg/scaler.c'] +if get_option('codecs').contains('ffmpeg') + ffmpeg_src = [ + 'ffmpeg/common.c', + 'ffmpeg/packet_ext.c', + 'ffmpeg/decoder.c', + 'ffmpeg/resampler.c', + 'ffmpeg/scaler.c', + 'ffmpeg/encoder.c', + ] + ffmpeg_inc = [] + ffmpeg_deps = [] + ffmpeg_args = [] +if not get_option('sink-only') + ffmpeg_src += [ + 'ffmpeg/demuxer.c', + 'ffmpeg/avio.c' + ] endif -libavutil = dependency('libavutil', required: false) -libavformat = dependency('libavformat', required: false) -libavcodec = dependency('libavcodec', required: false) -libswresample = dependency('libswresample', required: false) -libswscale = dependency('libswscale', required: false) -if not (libavutil.found() and libavformat.found() and libavcodec.found() and libswresample.found() and libswscale.found()) - ffmpeg_proj = subproject('ffmpeg', required: false) - if ffmpeg_proj.found() - libavutil = ffmpeg_proj.get_variable('avutil') - libavformat = ffmpeg_proj.get_variable('avformat') - libavcodec = ffmpeg_proj.get_variable('avcodec') - libswresample = ffmpeg_proj.get_variable('swresample') - libswscale = ffmpeg_proj.get_variable('swscale') - # Only used to set include dirs for ffmpeg. - ffmpeg_inc_dep = ffmpeg_proj.get_variable('ffmpeg_inc_dep') - ffmpeg_server_deps += [ffmpeg_inc_dep] - ffmpeg_client_deps += [ffmpeg_inc_dep, dependency('zlib')] - soxr = compiler.find_library('soxr', required: false) - if soxr.found() - ffmpeg_client_deps += [soxr] + libavutil = dependency('libavutil', required: false) + libavformat = dependency('libavformat', required: false) + libavcodec = dependency('libavcodec', required: false) + libswresample = dependency('libswresample', required: false) + libswscale = dependency('libswscale', required: false) + if not (libavutil.found() and libavformat.found() and libavcodec.found() and libswresample.found() and libswscale.found()) + ffmpeg_proj = subproject('ffmpeg', required: false) + if ffmpeg_proj.found() + libavutil = ffmpeg_proj.get_variable('avutil') + libavformat = ffmpeg_proj.get_variable('avformat') + libavcodec = ffmpeg_proj.get_variable('avcodec') + libswresample = ffmpeg_proj.get_variable('swresample') + libswscale = ffmpeg_proj.get_variable('swscale') + # Only used to set include dirs for ffmpeg. + ffmpeg_inc_dep = ffmpeg_proj.get_variable('ffmpeg_inc_dep') + # Extra static dependencies. + zlib = dependency('zlib') + bcrypt = compiler.find_library('bcrypt') + ffmpeg_deps += [ffmpeg_inc_dep, zlib, bcrypt] + soxr = compiler.find_library('soxr', required: false) + if soxr.found() + ffmpeg_deps += [soxr] + endif endif - ffmpeg_client_deps += [compiler.find_library('bcrypt')] endif -endif -if libavutil.found() and libavformat.found() and libavcodec.found() and libswresample.found() and libswscale.found() - ffmpeg_args += ['-DCAMU_HAVE_FFMPEG'] - ffmpeg_server_deps += [libavutil, libavformat, libavcodec] - ffmpeg_client_deps += [libavutil, libavformat, libavcodec, libswresample] - if ffmpeg_client_use_scaler - ffmpeg_client_deps += [libswscale] + if libavutil.found() and libavformat.found() and libavcodec.found() and libswresample.found() and libswscale.found() + ffmpeg_deps += [libavutil, libavformat, libavcodec, libswresample, libswscale] + ffmpeg_args += ['-DCAMU_HAVE_FFMPEG'] + ffmpeg_codec = declare_dependency(sources: ffmpeg_src, dependencies: ffmpeg_deps, + include_directories: ffmpeg_inc, compile_args: ffmpeg_args) + codec_deps += [ffmpeg_codec] endif - ffmpeg_server = declare_dependency(sources: ffmpeg_server_src, - dependencies: ffmpeg_server_deps, include_directories: ffmpeg_inc, compile_args: ffmpeg_args) - ffmpeg_client = declare_dependency(sources: ffmpeg_client_src, - dependencies: ffmpeg_client_deps, include_directories: ffmpeg_inc, compile_args: ffmpeg_args) endif -#stb_image = declare_dependency(sources: ['stb_image/impl.c'], -# dependencies: subproject('stb').get_variable('stb')) +if get_option('codecs').contains('stb_image') + stb = subproject('stb').get_variable('stb') + stb_image_codec = declare_dependency(sources: ['stb_image/impl.c'], dependencies: stb) + codec_deps += [stb_image_codec] +endif + +if get_option('codecs').contains('spng') + spng = dependency('spng', required: false) + if spng.found() + spng_codec = declare_dependency(sources: ['spng/impl.c'], dependencies: spng) + codec_deps += [spng_codec] + endif +endif -#wuffs = declare_dependency(sources: ['wuffs/impl.c'], -# dependencies: subproject('wuffs').get_variable('wuffs')) +if get_option('codecs').contains('wuffs') + wuffs = subproject('wuffs').get_variable('wuffs') + wuffs_codec = declare_dependency(sources: ['wuffs/impl.c'], dependencies: wuffs) + codec_deps += [wuffs_codec] +endif -#spng = declare_dependency(sources: ['spng/impl.c'], -# dependencies: dependency('spng')) +codecs = declare_dependency(dependencies: [stela, codec_deps]) diff --git a/src/codec/spng/decoder.h b/src/codec/spng/decoder.h index fee30c6..c027fce 100644 --- a/src/codec/spng/decoder.h +++ b/src/codec/spng/decoder.h @@ -4,7 +4,8 @@ struct camu_spng_decoder { struct camu_decoder dec; - void (*callback)(void *, struct camu_frame *); + struct camu_codec_stream *stream; + void (*callback)(void *, struct camu_codec_frame *); void (*userdata); }; diff --git a/src/codec/spng/impl.c b/src/codec/spng/impl.c index 9089e99..198d514 100644 --- a/src/codec/spng/impl.c +++ b/src/codec/spng/impl.c @@ -31,10 +31,12 @@ static bool spng_demuxer_init(struct camu_demuxer *demux, struct cch_handle *han struct spng_ihdr ihdr; spng_get_ihdr(ctx, &ihdr); - struct camu_stream stream; - stream.type = CAMU_NORMAL; + struct camu_codec_stream stream; + stream.mode = CAMU_NORMAL; + stream.type = CAMU_STREAM_VIDEO; stream.video.width = ihdr.width; stream.video.height = ihdr.height; + stream.video.format = CAMU_PIXEL_FORMAT_RGBA; al_array_push(spng->demux.streams, stream); @@ -43,14 +45,16 @@ static bool spng_demuxer_init(struct camu_demuxer *demux, struct cch_handle *han return true; } -static s32 spng_demuxer_get_packet(struct camu_demuxer *demux, struct camu_packet *packet) +static s32 spng_demuxer_get_packet(struct camu_demuxer *demux, struct camu_codec_packet *packet) { struct camu_spng_demuxer *spng = (struct camu_spng_demuxer *)demux; - if (spng->eof) return CAMU_ERR_EOF; - packet->type = CAMU_NORMAL; - packet->buffer = &spng->buffer; - spng->eof = true; - return CAMU_OK; + if (!spng->eof) { + packet->mode = CAMU_NORMAL; + packet->buffer = &spng->buffer; + spng->eof = true; + return CAMU_OK; + } + return CAMU_ERR_EOF; } static u64 spng_demuxer_get_duration(struct camu_demuxer *demux) @@ -69,31 +73,27 @@ static bool spng_demuxer_seek(struct camu_demuxer *demux, u64 pos) static void spng_demuxer_free(struct camu_demuxer **demux) { struct camu_spng_demuxer *spng = (struct camu_spng_demuxer *)*demux; + aki_buffer_free(&spng->buffer); + al_array_free(spng->demux.streams); al_free(spng); *demux = NULL; } -static bool spng_decoder_init(struct camu_decoder *dec, struct camu_renderer *renderer, struct camu_stream *stream) +static bool spng_decoder_init(struct camu_decoder *dec, struct camu_renderer *renderer, struct camu_codec_stream *stream, + void (*callback)(void *, struct camu_codec_frame *), void *userdata) { struct camu_spng_decoder *spng = (struct camu_spng_decoder *)dec; - (void)spng; - (void)stream; (void)renderer; - return true; -} - -static void spng_decoder_set_callback(struct camu_decoder *dec, void (*callback)(void *, struct camu_frame *), void *userdata) -{ - struct camu_spng_decoder *spng = (struct camu_spng_decoder *)dec; + spng->stream = stream; spng->callback = callback; spng->userdata = userdata; + return true; } -static s32 spng_decoder_push(struct camu_decoder *dec, struct camu_packet *packet) +static s32 spng_decoder_push(struct camu_decoder *dec, struct camu_codec_packet *packet) { - if (!packet) return CAMU_OK; - struct camu_spng_decoder *spng = (struct camu_spng_decoder *)dec; + if (!packet) return CAMU_OK; spng_ctx *ctx = spng_ctx_new(SPNG_CTX_IGNORE_ADLER32); spng_set_png_buffer(ctx, aki_buffer_get_ptr(packet->buffer, 0), packet->buffer->size); @@ -103,19 +103,17 @@ static s32 spng_decoder_push(struct camu_decoder *dec, struct camu_packet *packe size_t out_size; spng_decoded_image_size(ctx, SPNG_FMT_RGBA8, &out_size); - struct camu_frame *frame = al_alloc_object(struct camu_frame); - frame->type = CAMU_NORMAL; - frame->width = spng->dec.stream->video.width; - frame->height = spng->dec.stream->video.height; - frame->fmt = CAMU_PIXEL_FMT_RGBA; - frame->pts = 0.0; - frame->flags = 0; - + struct camu_codec_frame *frame = al_alloc_object(struct camu_codec_frame); + frame->mode = CAMU_NORMAL; frame->data = camu_page_alloc(out_size); spng_decode_chunks(ctx); spng_decode_image(ctx, frame->data, out_size, SPNG_FMT_RGBA8, 0); - spng_ctx_free(ctx); + frame->flags = 0; + frame->format = CAMU_PIXEL_FORMAT_RGBA; + frame->width = spng->stream->video.width; + frame->height = spng->stream->video.height; + frame->pts = 0.0; spng->callback(spng->userdata, frame); @@ -143,7 +141,7 @@ static void spng_decoder_free(struct camu_decoder **dec) struct camu_demuxer *camu_spng_demuxer_create(void) { struct camu_spng_demuxer *spng = al_alloc_object(struct camu_spng_demuxer); - spng->demux.type = CAMU_NORMAL; + spng->demux.mode = CAMU_NORMAL; al_array_init(spng->demux.streams); spng->demux.init = spng_demuxer_init; spng->demux.get_packet = spng_demuxer_get_packet; @@ -157,7 +155,6 @@ struct camu_decoder *camu_spng_decoder_create(void) { struct camu_spng_decoder *spng = al_alloc_object(struct camu_spng_decoder); spng->dec.init = spng_decoder_init; - spng->dec.set_callback = spng_decoder_set_callback; spng->dec.push = spng_decoder_push; spng->dec.process = spng_decoder_process; spng->dec.flush = spng_decoder_flush; diff --git a/src/codec/stb_image/decoder.h b/src/codec/stb_image/decoder.h index 7083a09..af50799 100644 --- a/src/codec/stb_image/decoder.h +++ b/src/codec/stb_image/decoder.h @@ -4,7 +4,8 @@ struct camu_stbi_decoder { struct camu_decoder dec; - void (*callback)(void *, struct camu_frame *); + struct camu_codec_stream *stream; + void (*callback)(void *, struct camu_codec_frame *); void (*userdata); }; diff --git a/src/codec/stb_image/impl.c b/src/codec/stb_image/impl.c index 6058ff2..98d9e2c 100644 --- a/src/codec/stb_image/impl.c +++ b/src/codec/stb_image/impl.c @@ -7,8 +7,10 @@ #define STBI_NO_FAILURE_STRINGS #define STBI_MALLOC camu_page_alloc #define STBI_FREE al_free -#define STBI_REALLOC al_realloc +#define STBI_REALLOC camu_page_realloc +AL_UNUSED_FUNCTION_PUSH #include <stb_image.h> +AL_UNUSED_FUNCTION_POP #include "../../cache/entry.h" @@ -28,27 +30,37 @@ static bool stbi_demuxer_init(struct camu_demuxer *demux, struct cch_handle *han aki_buffer_ensure_space(&stb->buffer, size); cch_handle_read(stb->handle, aki_buffer_get_ptr(&stb->buffer, 0), size); stb->buffer.size = size; + s32 w, h, channels; - stbi_info_from_memory(aki_buffer_get_ptr(&stb->buffer, 0), stb->buffer.size, &w, &h, &channels); + u8 *data = aki_buffer_get_ptr(&stb->buffer, 0); + if (!stbi_info_from_memory(data, stb->buffer.size, &w, &h, &channels)) { + goto err; + } - struct camu_stream stream = { 0 }; - stream.type = CAMU_NORMAL; + struct camu_codec_stream stream = { 0 }; + stream.mode = CAMU_NORMAL; + stream.type = CAMU_STREAM_VIDEO; stream.video.width = w; stream.video.height = h; + stream.video.format = camu_pixel_format_from_channels(channels); al_array_push(stb->demux.streams, stream); return true; +err: + return false; } -static s32 stbi_demuxer_get_packet(struct camu_demuxer *demux, struct camu_packet *packet) +static s32 stbi_demuxer_get_packet(struct camu_demuxer *demux, struct camu_codec_packet *packet) { struct camu_stbi_demuxer *stb = (struct camu_stbi_demuxer *)demux; - if (stb->eof) return CAMU_ERR_EOF; - packet->type = CAMU_NORMAL; - packet->buffer = &stb->buffer; - stb->eof = true; - return CAMU_OK; + if (!stb->eof) { + packet->mode = CAMU_NORMAL; + packet->buffer = &stb->buffer; + stb->eof = true; + return CAMU_OK; + } + return CAMU_ERR_EOF; } static u64 stbi_demuxer_get_duration(struct camu_demuxer *demux) @@ -67,44 +79,44 @@ static bool stbi_demuxer_seek(struct camu_demuxer *demux, u64 pos) static void stbi_demuxer_free(struct camu_demuxer **demux) { struct camu_stbi_demuxer *stb = (struct camu_stbi_demuxer *)*demux; + aki_buffer_free(&stb->buffer); + al_array_free(stb->demux.streams); al_free(stb); *demux = NULL; } -static bool stbi_decoder_init(struct camu_decoder *dec, struct camu_renderer *renderer, struct camu_stream *stream) -{ - struct camu_stbi_decoder *stb = (struct camu_stbi_decoder *)dec; - (void)stb; - (void)stream; - (void)renderer; - return true; -} -static void stbi_decoder_set_callback(struct camu_decoder *dec, void (*callback)(void *, struct camu_frame *), void *userdata) +static bool stbi_decoder_init(struct camu_decoder *dec, struct camu_renderer *renderer, struct camu_codec_stream *stream, + void (*callback)(void *, struct camu_codec_frame *), void *userdata) { struct camu_stbi_decoder *stb = (struct camu_stbi_decoder *)dec; + (void)renderer; + stb->stream = stream; stb->callback = callback; stb->userdata = userdata; + return true; } -static s32 stbi_decoder_push(struct camu_decoder *dec, struct camu_packet *packet) +static s32 stbi_decoder_push(struct camu_decoder *dec, struct camu_codec_packet *packet) { - if (!packet) return CAMU_OK; - struct camu_stbi_decoder *stb = (struct camu_stbi_decoder *)dec; + if (!packet) return CAMU_OK; s32 w, h, channels; - u8 *pixels = stbi_load_from_memory(aki_buffer_get_ptr(packet->buffer, 0), - packet->buffer->size, &w, &h, &channels, 4); + u8 *data = aki_buffer_get_ptr(packet->buffer, 0); + u8 *pixels = stbi_load_from_memory(data, packet->buffer->size, &w, &h, &channels, 0); + if (!pixels) return CAMU_ERR_ERROR; + al_assert(w == stb->stream->video.width && h == stb->stream->video.height); - struct camu_frame *frame = al_alloc_object(struct camu_frame); - frame->type = CAMU_NORMAL; + struct camu_codec_frame *frame = al_alloc_object(struct camu_codec_frame); + frame->mode = CAMU_NORMAL; + frame->flags = 0; frame->data = pixels; - frame->width = stb->dec.stream->video.width; - frame->height = stb->dec.stream->video.height; - frame->fmt = CAMU_PIXEL_FMT_RGBA; + frame->video.width = w; + frame->video.height = h; + frame->format = camu_pixel_format_from_channels(channels); + al_assert(frame->format == stb->stream->video.format); frame->pts = 0.0; - frame->flags = 0; stb->callback(stb->userdata, frame); @@ -132,7 +144,7 @@ static void stbi_decoder_free(struct camu_decoder **dec) struct camu_demuxer *camu_stbi_demuxer_create(void) { struct camu_stbi_demuxer *stb = al_alloc_object(struct camu_stbi_demuxer); - stb->demux.type = CAMU_NORMAL; + stb->demux.mode = CAMU_NORMAL; al_array_init(stb->demux.streams); stb->demux.init = stbi_demuxer_init; stb->demux.get_packet = stbi_demuxer_get_packet; @@ -146,7 +158,6 @@ struct camu_decoder *camu_stbi_decoder_create(void) { struct camu_stbi_decoder *stb = al_alloc_object(struct camu_stbi_decoder); stb->dec.init = stbi_decoder_init; - stb->dec.set_callback = stbi_decoder_set_callback; stb->dec.push = stbi_decoder_push; stb->dec.process = stbi_decoder_process; stb->dec.flush = stbi_decoder_flush; diff --git a/src/codec/wuffs/decoder.h b/src/codec/wuffs/decoder.h index 71f0290..bd79236 100644 --- a/src/codec/wuffs/decoder.h +++ b/src/codec/wuffs/decoder.h @@ -5,8 +5,9 @@ struct camu_wuffs_decoder { struct camu_decoder dec; + struct camu_codec_stream *stream; u8 file_type; - void (*callback)(void *, struct camu_frame *); + void (*callback)(void *, struct camu_codec_frame *); void (*userdata); }; diff --git a/src/codec/wuffs/impl.c b/src/codec/wuffs/impl.c index 26e9011..4d9ea7e 100644 --- a/src/codec/wuffs/impl.c +++ b/src/codec/wuffs/impl.c @@ -1,3 +1,4 @@ +#include <aki/common.h> #define WUFFS_IMPLEMENTATION #define WUFFS_CONFIG__STATIC_FUNCTIONS #define WUFFS_CONFIG__MODULES @@ -11,8 +12,6 @@ #define WUFFS_CONFIG__MODULE__IMAGE #include <wuffs-unsupported-snapshot.c> -#include <aki/common.h> - #include "../../cache/entry.h" #include "demuxer.h" @@ -32,44 +31,47 @@ static bool wuffs_demuxer_init(struct camu_demuxer *demux, struct cch_handle *ha cch_handle_read(wfs->handle, aki_buffer_get_ptr(&wfs->buffer, 0), size); wfs->buffer.size = size; - wuffs_png__decoder wf_dec = {0}; + wuffs_png__decoder wf_dec = { 0 }; wuffs_base__status status = wuffs_png__decoder__initialize(&wf_dec, sizeof(wf_dec), WUFFS_VERSION, 0); - (void)status; - //if (!wuffs_base__status__is_ok(&status)) goto err; + if (!wuffs_base__status__is_ok(&status)) goto err; wuffs_png__decoder__set_quirk(&wf_dec, WUFFS_BASE__QUIRK_IGNORE_CHECKSUM, true); - //wuffs_png__decoder__set_report_metadata(&wf_dec, WUFFS_BASE__FOURCC__ICCP, true); - - wuffs_base__io_buffer src = {0}; + wuffs_base__io_buffer src = { 0 }; src.data.ptr = aki_buffer_get_ptr(&wfs->buffer, 0); src.data.len = wfs->buffer.size; src.meta.wi = wfs->buffer.size; src.meta.closed = true; - wuffs_base__image_config ic = {0}; + wuffs_base__image_config ic = { 0 }; status = wuffs_png__decoder__decode_image_config(&wf_dec, &ic, &src); - //if (!wuffs_base__status__is_ok(&status)) goto err; - //if (!wuffs_base__image_config__is_valid(&ic)) goto err; + if (!wuffs_base__status__is_ok(&status)) goto err; + if (!wuffs_base__image_config__is_valid(&ic)) goto err; - struct camu_stream stream; - stream.type = CAMU_NORMAL; + struct camu_codec_stream stream; + stream.mode = CAMU_NORMAL; + stream.type = CAMU_STREAM_VIDEO; stream.video.width = wuffs_base__pixel_config__width(&ic.pixcfg); stream.video.height = wuffs_base__pixel_config__height(&ic.pixcfg); + stream.video.format = CAMU_PIXEL_FORMAT_RGBA; al_array_push(wfs->demux.streams, stream); return true; +err: + return false; } -static s32 wuffs_demuxer_get_packet(struct camu_demuxer *demux, struct camu_packet *packet) +static s32 wuffs_demuxer_get_packet(struct camu_demuxer *demux, struct camu_codec_packet *packet) { struct camu_wuffs_demuxer *wfs = (struct camu_wuffs_demuxer *)demux; - if (wfs->eof) return CAMU_ERR_EOF; - packet->type = CAMU_NORMAL; - packet->buffer = &wfs->buffer; - wfs->eof = true; - return CAMU_OK; + if (!wfs->eof) { + packet->mode = CAMU_NORMAL; + packet->buffer = &wfs->buffer; + wfs->eof = true; + return CAMU_OK; + } + return CAMU_ERR_EOF; } static u64 wuffs_demuxer_get_duration(struct camu_demuxer *demux) @@ -88,85 +90,74 @@ static bool wuffs_demuxer_seek(struct camu_demuxer *demux, u64 pos) static void wuffs_demuxer_free(struct camu_demuxer **demux) { struct camu_wuffs_demuxer *wfs = (struct camu_wuffs_demuxer *)*demux; + aki_buffer_free(&wfs->buffer); + al_array_free(wfs->demux.streams); al_free(wfs); *demux = NULL; } -static bool wuffs_decoder_init(struct camu_decoder *dec, struct camu_renderer *renderer, struct camu_stream *stream) +static bool wuffs_decoder_init(struct camu_decoder *dec, struct camu_renderer *renderer, struct camu_codec_stream *stream, + void (*callback)(void *, struct camu_codec_frame *), void *userdata) { struct camu_wuffs_decoder *wfs = (struct camu_wuffs_decoder *)dec; - (void)wfs; - (void)stream; (void)renderer; - return true; -} - -static void wuffs_decoder_set_callback(struct camu_decoder *dec, void (*callback)(void *, struct camu_frame *), void *userdata) -{ - struct camu_wuffs_decoder *wfs = (struct camu_wuffs_decoder *)dec; + wfs->stream = stream; wfs->callback = callback; wfs->userdata = userdata; + return true; } -static s32 wuffs_decoder_push(struct camu_decoder *dec, struct camu_packet *packet) +static s32 wuffs_decoder_push(struct camu_decoder *dec, struct camu_codec_packet *packet) { + struct camu_wuffs_decoder *wfs = (struct camu_wuffs_decoder *)dec; if (!packet) return CAMU_OK; - struct camu_wuffs_decoder *wfs = (struct camu_wuffs_decoder *)dec; + struct camu_codec_frame *frame = al_alloc_object(struct camu_codec_frame); + frame->mode = CAMU_NORMAL; + frame->data = NULL; + frame->flags = 0; + frame->width = wfs->stream->video.width; + frame->height = wfs->stream->video.height; + frame->format = CAMU_PIXEL_FMT_RGBA; - wuffs_png__decoder wf_dec = {0}; + wuffs_png__decoder wf_dec = { 0 }; wuffs_base__status status = wuffs_png__decoder__initialize(&wf_dec, sizeof(wf_dec), WUFFS_VERSION, 0); - //if (!wuffs_base__status__is_ok(&status)) goto err; + if (!wuffs_base__status__is_ok(&status)) goto err; wuffs_png__decoder__set_quirk(&wf_dec, WUFFS_BASE__QUIRK_IGNORE_CHECKSUM, true); - //wuffs_png__decoder__set_report_metadata(&wf_dec, WUFFS_BASE__FOURCC__ICCP, true); - - wuffs_base__io_buffer src = {0}; + wuffs_base__io_buffer src = { 0 }; src.data.ptr = aki_buffer_get_ptr(packet->buffer, 0); src.data.len = packet->buffer->size; src.meta.wi = packet->buffer->size; src.meta.closed = true; - //wf_dec.private_impl.p_decode_iccp[0] = 1; - - wuffs_base__image_config ic = {0}; + wuffs_base__image_config ic = { 0 }; status = wuffs_png__decoder__decode_image_config(&wf_dec, &ic, &src); - //if (!wuffs_base__status__is_ok(&status)) goto err; - //if (!wuffs_base__image_config__is_valid(&ic)) goto err; - - struct camu_frame *frame = al_alloc_object(struct camu_frame); - frame->type = CAMU_NORMAL; - frame->width = wfs->dec.stream->video.width; - frame->height = wfs->dec.stream->video.height; - frame->fmt = CAMU_PIXEL_FMT_RGBA; + if (!wuffs_base__status__is_ok(&status)) goto err; + if (!wuffs_base__image_config__is_valid(&ic)) goto err; u32 pixsub = WUFFS_BASE__PIXEL_SUBSAMPLING__NONE; u32 pixfmt = WUFFS_BASE__PIXEL_FORMAT__RGBA_NONPREMUL; wuffs_base__pixel_config__set(&ic.pixcfg, pixfmt, pixsub, frame->width, frame->height); - wuffs_base__pixel_buffer pb = {0}; + wuffs_base__pixel_buffer pb = { 0 }; size_t dst_size = frame->width * frame->height * 4; size_t wrk_size = wuffs_png__decoder__workbuf_len(&wf_dec).max_incl; - /* - void *device_ptr = dec->renderer->get_device_ptr(dec->renderer, dst_size + wrk_size); - wuffs_base__slice_u8 wrk = wuffs_base__make_slice_u8(device_ptr + dst_size, wrk_size); - status = wuffs_base__pixel_buffer__set_from_slice(&pb, &ic.pixcfg, - wuffs_base__make_slice_u8(device_ptr, dst_size)); - */ frame->data = camu_page_alloc(dst_size + wrk_size); - status = wuffs_base__pixel_buffer__set_from_slice(&pb, &ic.pixcfg, - wuffs_base__make_slice_u8(frame->data, dst_size)); - //if (!wuffs_base__status__is_ok(&status)) goto err; + status = wuffs_base__pixel_buffer__set_from_slice(&pb, &ic.pixcfg, wuffs_base__make_slice_u8(frame->data, dst_size)); + if (!wuffs_base__status__is_ok(&status)) goto err; wuffs_base__slice_u8 wrk = wuffs_base__make_slice_u8(frame->data + dst_size, wrk_size); status = wuffs_png__decoder__decode_frame(&wf_dec, &pb, &src, 0, wrk, NULL); - (void)status; - //if (!wuffs_base__status__is_ok(&status)) goto err; + if (!wuffs_base__status__is_ok(&status)) goto err; wfs->callback(wfs->userdata, frame); return CAMU_OK; +err: + camu_codec_frame_discard(frame); + return CAMU_ERR_ERROR; } static void wuffs_decoder_flush(struct camu_decoder *dec) @@ -190,7 +181,7 @@ static void wuffs_decoder_free(struct camu_decoder **dec) struct camu_demuxer *camu_wuffs_demuxer_create(void) { struct camu_wuffs_demuxer *wfs = al_alloc_object(struct camu_wuffs_demuxer); - wfs->demux.type = CAMU_NORMAL; + wfs->demux.mode = CAMU_NORMAL; al_array_init(wfs->demux.streams); wfs->demux.init = wuffs_demuxer_init; wfs->demux.get_packet = wuffs_demuxer_get_packet; @@ -204,7 +195,6 @@ struct camu_decoder *camu_wuffs_decoder_create(void) { struct camu_wuffs_decoder *wfs = al_alloc_object(struct camu_wuffs_decoder); wfs->dec.init = wuffs_decoder_init; - wfs->dec.set_callback = wuffs_decoder_set_callback; wfs->dec.push = wuffs_decoder_push; wfs->dec.process = wuffs_decoder_process; wfs->dec.flush = wuffs_decoder_flush; diff --git a/src/fruits/cmc/cmc.c b/src/fruits/cmc/cmc.c deleted file mode 100644 index 5bbdb0b..0000000 --- a/src/fruits/cmc/cmc.c +++ /dev/null @@ -1,128 +0,0 @@ -#include <al/lib.h> -#include <al/log.h> -#include <al/wstr.h> -#include <al/random.h> -#include <aki/common.h> -#include <aki/event_loop.h> - -#include "../../server/common.h" - -#include "cmc.h" - -static void client_callback(void *userdata, u8 op, void *opaque) -{ - struct cmc *c = (struct cmc *)userdata; - switch (op) { - case CAMU_CLIENT_STATE_UPDATED: { - switch (c->action) { - case CMC_ACTION_OPEN: { - cmc_ui_open(&c->ui, c); - break; - } - case CMC_ACTION_LIST: { - switch (c->type) { - case CMC_LIST: { - struct camu_list *list; - al_array_foreach(c->client.state.lists, i, list) { - al_printf("%.*s:\n", AL_STR_PRINTF(&list->name)); - str *unique_id; - al_array_foreach_ptr(list->entries, j, unique_id) { - if ((s32)j == list->current) { - al_printf(" > %.*s\n", AL_STR_PRINTF(unique_id)); - } else { - al_printf(" %.*s\n", AL_STR_PRINTF(unique_id)); - } - } - } - aki_event_loop_break(&c->loop); - break; - } - case CMC_SEARCH: { - struct camu_search *search; - al_array_foreach(c->client.state.searches, i, search) { - al_printf("%u %x %.*s\n", i, search->id, AL_STR_PRINTF(&search->query)); - } - aki_event_loop_break(&c->loop); - break; - } - } - break; - } - } - break; - } - case CAMU_CLIENT_SEARCH_CREATED: { - aki_event_loop_break(&c->loop); - break; - } - case CAMU_CLIENT_RESULTS: { - cmc_ui_handle_results(&c->ui.search, (struct camu_result_list *)opaque); - break; - } - case CAMU_CLIENT_END_OF_RESULTS: { - cmc_ui_handle_end_of_results(&c->ui.search, *((s32 *)opaque)); - break; - } - case CAMU_CLIENT_DISCONNECTED: - aki_event_loop_break(&c->loop); - break; - } -} - -static struct cmc c = { 0 }; - -s32 main(s32 argc, char *argv[]) -{ - if (argc < 2 || !aki_common_init()) return EXIT_FAILURE; - - str module; - str query; - - str *cmd = al_str_cr(argv[1]); - if (al_str_eq(cmd, al_str_c("search"))) { - c.type = CMC_SEARCH; - if (argc == 2) { - c.action = CMC_ACTION_LIST; - } else { - c.action = CMC_ACTION_CREATE; - al_str_from(&module, "twitter"); - al_str_from(&query, argv[2]); - } - } else if (al_str_eq(cmd, al_str_c("open"))) { - c.action = CMC_ACTION_OPEN; - } else if (al_str_eq(cmd, al_str_c("list"))) { - c.type = CMC_LIST; - if (argc == 2) { - c.action = CMC_ACTION_LIST; - } else { - c.action = CMC_ACTION_CREATE; - } - } else { - return EXIT_FAILURE; - } - - aki_event_loop_init(&c.loop); - - camu_client_init(&c.client, &c.loop, client_callback, &c); - if (!camu_client_login(&c.client, al_str_c("andrew"), CAMU_SERVER_IP, CAMU_PORT)) { - goto err; - } - - if (c.action == CMC_ACTION_CREATE) { - if (c.type == CMC_SEARCH) { - camu_client_create_search(&c.client, &module, &query); - } - } - - aki_event_loop_run(&c.loop); - - if (c.action == CMC_ACTION_OPEN && c.ui.nc) notcurses_stop(c.ui.nc); - - camu_client_close(&c.client); - - aki_common_close(); - - return EXIT_SUCCESS; -err: - return EXIT_FAILURE; -} diff --git a/src/fruits/cmc/cmc.h b/src/fruits/cmc/cmc.h deleted file mode 100644 index 625d256..0000000 --- a/src/fruits/cmc/cmc.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include <al/types.h> -#include <aki/event_loop.h> - -#include "../../libclient/client.h" - -#include "tui/ui.h" - -enum { - CMC_LIST = 0, - CMC_SEARCH -}; - -enum { - CMC_ACTION_OPEN = 0, - CMC_ACTION_LIST, - CMC_ACTION_CREATE, -}; - -struct cmc { - u8 type; - u8 action; - struct aki_event_loop loop; - struct camu_client client; - struct cmc_ui ui; -}; diff --git a/src/fruits/cmc/cmc.py b/src/fruits/cmc/cmc.py new file mode 100644 index 0000000..5c0a5b6 --- /dev/null +++ b/src/fruits/cmc/cmc.py @@ -0,0 +1,17 @@ +import cffi +import locale +from notcurses import notcurses + +ffi = cffi.FFI() + +print(ffi.string(notcurses.lib.notcurses_version())) + +class CMC: + def __init__(self): + pass + +c = CMC() + +locale.setlocale(locale.LC_ALL, "") +nc = notcurses.Notcurses() +nc.render() diff --git a/src/fruits/cmc/meson.build b/src/fruits/cmc/meson.build deleted file mode 100644 index 57ded9c..0000000 --- a/src/fruits/cmc/meson.build +++ /dev/null @@ -1,15 +0,0 @@ -cmc_src = [ - 'cmc.c', - 'tui/ui.c', - 'tui/resources.c', - 'tui/pager.c', - 'tui/view_twitter.c', - 'tui/pane_lists.c', - 'tui/pane_search.c' -] -cmc_deps = [common_deps, libclient, subproject('stb').get_variable('stb')] - -notcurses = dependency('notcurses') -cmc_deps += [notcurses] - -executable('cmc', cmc_src, dependencies: cmc_deps) diff --git a/src/fruits/cmc/tui/pager.c b/src/fruits/cmc/tui/pager.c deleted file mode 100644 index a672315..0000000 --- a/src/fruits/cmc/tui/pager.c +++ /dev/null @@ -1,88 +0,0 @@ -#include "pager.h" - -void cmc_pager_init(struct cmc_pager *pager, u32 page) -{ - pager->find_anchor = PAGE_NONE; - pager->anchor = 0; - pager->redraw = true; - pager->selected = 0; - al_array_init(pager->prev); - pager->max_page_num = page; - pager->min_page_num = page; - pager->end_of_page = -1; - pager->pending_requests = 0; - al_array_init(pager->pages); -} - -static bool any_pending_requests(struct cmc_view_page *page) -{ - struct cmc_view *view; - al_array_foreach(page->views, j, view) { - if (view->pending_requests > 0) { - return true; - } - } - return false; -} - -bool cmc_pager_maybe_process_pages(struct cmc_pager *pager, void (*func)(void *, struct cmc_view_page *), void *userdata) -{ - bool any_page_processed = false; - struct cmc_view_page *page; - al_array_foreach(pager->pages, i, page) { - if (page->processed) continue; - if (!any_pending_requests(page)) { - page->processed = true; - func(userdata, page); - any_page_processed = true; - } - } - return any_page_processed; -} - -struct cmc_view_page *cmc_get_page_from_num(struct cmc_pager *pager, u32 num) -{ - struct cmc_view_page *page; - al_array_foreach(pager->pages, i, page) { - if (page->num == num) return page; - } - return NULL; -} - -struct cmc_view_page *cmc_pager_page_containing_index(struct cmc_pager *pager, u32 index, u32 *offset) -{ - u32 count = 0, size = 0; - struct cmc_view_page *page = NULL; - for (u32 i = pager->min_page_num; i <= pager->max_page_num; i++) { - if (!(page = cmc_get_page_from_num(pager, i))) continue; - size = page->views.size; - count += size; - if (count > index) { - if (offset) *offset = size - (count - index); - return page; - } - } - return NULL; -} - -bool cmc_pager_iterate_to_height(struct cmc_pager *pager, u32 height, u32 anchor, u32 *count, - void (*func)(void *, struct cmc_view *, u32), void *userdata) -{ - u32 y = 0, offset; - struct cmc_view_page *page = cmc_pager_page_containing_index(pager, anchor, &offset); - if (!page) return false; - struct cmc_view *view; - do { - // If the page is not processed, return out of space as to not display a discontinuity. - if (!page->processed) return true; - for (; offset < page->views.size; offset++) { - view = al_array_at(page->views, offset); - // If this post would draw past the height, return out of space. - if (y + view->rows > height) return true; - if (func) func(userdata, view, y); - (*count)++; - y += view->rows + 1; - } - } while ((offset = 0, (page = cmc_get_page_from_num(pager, page->num + 1)))); - return false; -} diff --git a/src/fruits/cmc/tui/pager.h b/src/fruits/cmc/tui/pager.h deleted file mode 100644 index 6aeeb9c..0000000 --- a/src/fruits/cmc/tui/pager.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include <al/types.h> -#include <al/array.h> - -#include "view.h" - -enum { - PAGE_NONE = 0, - PAGE_UP, - PAGE_DOWN -}; - -struct cmc_pager { - u8 find_anchor; - s32 anchor; - bool redraw; - u32 count; - s32 selected; - array(struct cmc_view *) prev; - u32 max_page_num; - u32 min_page_num; - s32 end_of_page; - u32 pending_requests; - array(struct cmc_view_page *) pages; -}; - -void cmc_pager_init(struct cmc_pager *pager, u32 page); -bool cmc_pager_maybe_process_pages(struct cmc_pager *pager, void (*func)(void *, struct cmc_view_page *), void *userdata); -struct cmc_view_page *cmc_get_page_from_num(struct cmc_pager *pager, u32 num); -struct cmc_view_page *cmc_pager_page_containing_index(struct cmc_pager *pager, u32 index, u32 *offset); -bool cmc_pager_iterate_to_height(struct cmc_pager *pager, u32 height, u32 anchor, u32 *count, - void (*func)(void *, struct cmc_view *, u32), void *userdata); diff --git a/src/fruits/cmc/tui/pane_lists.c b/src/fruits/cmc/tui/pane_lists.c deleted file mode 100644 index 6367e57..0000000 --- a/src/fruits/cmc/tui/pane_lists.c +++ /dev/null @@ -1,93 +0,0 @@ -#include "../cmc.h" - -#include "pane_lists.h" -#include "ui.h" - -void cmc_pane_lists_init(struct cmc_pane_lists *pane, struct cmc_ui *ui) -{ - pane->offset = 0; - pane->selected = 0; - pane->list = al_array_at(ui->c->client.state.lists, 0); - pane->ui = ui; -} - -bool cmc_pane_lists_handle_input(struct cmc_pane_lists *pane, struct ncinput *input) -{ - if (!(input->evtype == NCTYPE_PRESS || input->evtype == NCTYPE_UNKNOWN)) return false; - s32 rows = pane->ui->rows - 1; - s32 size = (s32)pane->list->entries.size; - if (input->id == 'j') { - if (pane->selected + 1 < size) { - pane->selected++; - if (pane->selected - pane->offset >= rows) { - pane->offset += rows; - } - return true; - } - } else if (input->id == 'k') { - if (pane->selected - 1 >= 0) { - pane->selected--; - if (pane->selected - pane->offset < 0) { - pane->offset -= rows; - } - return true; - } - } else if (input->id == 'n') { - if (pane->offset + rows < size) { - pane->offset += rows; - pane->selected += rows; - if (pane->selected >= size) { - pane->selected = size - 1; - } - return true; - } - } else if (input->id == 'p') { - if (pane->offset - rows >= 0) { - pane->offset -= rows; - pane->selected -= rows; - return true; - } - } else if (input->id == NCKEY_RETURN) { - camu_client_skipto(&pane->ui->c->client, pane->selected); - } - return false; -} - -bool cmc_pane_lists_handle_input_overlay(struct cmc_pane_lists *pane, struct ncinput *input) -{ - (void)pane; - (void)input; - return false; -} - -void cmc_pane_lists_draw_overlay(struct cmc_pane_lists *pane) -{ - (void)pane; -} - -void cmc_pane_lists_refresh(struct cmc_pane_lists *pane) -{ - struct cmc_ui *ui = pane->ui; - - ncplane_cursor_move_yx(ui->p, 0, 0); - ncplane_erase(ui->p); - notcurses_refresh(ui->nc, NULL, NULL); - notcurses_render(ui->nc); - - str *unique_id; - u32 rows = AL_MIN(pane->list->entries.size - pane->offset, ui->rows - 1); - for (u32 i = pane->offset; i < pane->offset + rows; i++) { - unique_id = &al_array_at(pane->list->entries, i); - if ((s32)i == pane->selected) { - ncplane_set_fg_palindex(ui->p, 4); - } else { - ncplane_set_fg_default(ui->p); - } - u32 cols = AL_MIN(unique_id->len, ui->cols); - for (u32 j = 0; j < cols; j++) { - ncplane_putchar_yx(ui->p, i - pane->offset, j + 1, al_str_at(unique_id, j)); - } - } - - notcurses_render(ui->nc); -} diff --git a/src/fruits/cmc/tui/pane_lists.h b/src/fruits/cmc/tui/pane_lists.h deleted file mode 100644 index 323eb52..0000000 --- a/src/fruits/cmc/tui/pane_lists.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include <al/types.h> -#include <notcurses/notcurses.h> - -#include "../../libclient/client.h" - -struct cmc_ui; -struct cmc_pane_lists { - s32 offset; - s32 selected; - struct camu_list *list; - struct cmc_ui *ui; -}; - -void cmc_pane_lists_init(struct cmc_pane_lists *pane, struct cmc_ui *ui); -bool cmc_pane_lists_handle_input(struct cmc_pane_lists *pane, struct ncinput *input); -bool cmc_pane_lists_handle_input_overlay(struct cmc_pane_lists *pane, struct ncinput *input); -void cmc_pane_lists_draw_overlay(struct cmc_pane_lists *pane); -void cmc_pane_lists_refresh(struct cmc_pane_lists *pane); diff --git a/src/fruits/cmc/tui/pane_search.c b/src/fruits/cmc/tui/pane_search.c deleted file mode 100644 index 002a4d1..0000000 --- a/src/fruits/cmc/tui/pane_search.c +++ /dev/null @@ -1,335 +0,0 @@ -#include <al/lib.h> -#include <notcurses/nckeys.h> - -#include "../cmc.h" - -#include "pane_search.h" -#include "view_twitter.h" -#include "ui.h" - -static void request_page_internal(struct cmc *c, struct cmc_search *search, u32 num) -{ - search->pager.pending_requests++; - camu_client_get_page(&c->client, search->search, num); -} - -void cmc_pane_search_init(struct cmc_pane_search *pane, struct cmc_ui *ui) -{ - pane->ui = ui; - al_array_init(pane->searches); - pane->search = NULL; - struct camu_search *s; - al_array_foreach(pane->ui->c->client.state.searches, i, s) { - struct cmc_search *search = al_alloc_object(struct cmc_search); - search->search = s; - cmc_pager_init(&search->pager, s->page); - al_array_push(pane->searches, search); - } - if (pane->searches.size > 0) { - pane->search = al_array_at(pane->searches, 0); - } -} - -static struct cmc_view *get_view_by_visual_index(struct cmc_pager *pager, s32 index) -{ - u32 offset; - struct cmc_view_page *page = cmc_pager_page_containing_index(pager, pager->anchor + index, &offset); - if (!page || !page->processed) return NULL; - return al_array_at(page->views, offset); -} - -// Given an index of a post, request the page to the offset of the page containing that post. -static bool request_page_at_page_offset(struct cmc_pane_search *pane, struct cmc_pager *pager, u32 index, s32 offset) -{ - struct cmc_view_page *page = cmc_pager_page_containing_index(pager, index, NULL); - if (page) { - s32 req = (s32)page->num + offset; - if (req < 0) return false; - request_page_internal(pane->ui->c, pane->search, req); - } - return true; -} - -static void add_selected_from_index(struct cmc *c, struct cmc_pager *pager, u32 index) -{ - struct cmc_view *view = get_view_by_visual_index(pager, pager->selected); - if (view && view->post->media.size > index) { - camu_client_add(&c->client, &view->post->unique_id, index, true); - } -} - -#define ON_LAST_PAGE(pager) \ - (pager->end_of_page >= 0 && pager->anchor + pager->count > (u32)pager->end_of_page) - -bool cmc_pane_search_handle_input(struct cmc_pane_search *pane, struct ncinput *input) -{ - struct cmc_pager *pager = &pane->search->pager; - if (!(input->evtype == NCTYPE_PRESS || input->evtype == NCTYPE_UNKNOWN)) return false; - if (input->id == 'j') { - if (pager->selected + 1 >= (s32)pager->count) { - if (pager->pending_requests > 0 || pager->find_anchor != PAGE_NONE) return false; - if (ON_LAST_PAGE(pager)) return false; - if (pager->count == 0) pager->count = 1; - u32 index = pager->anchor + pager->count; - pager->find_anchor = PAGE_DOWN; - struct cmc_view_page *page = cmc_pager_page_containing_index(pager, index, NULL); - if (!page) { - request_page_at_page_offset(pane, pager, index - 1, 1); - return false; - } - } else { - pager->selected++; - } - return true; - } else if (input->id == 'k') { - if (pager->selected - 1 < 0) { - if (pager->pending_requests > 0 || pager->find_anchor != PAGE_NONE) return false; - if (pager->anchor - 1 < 0 && !request_page_at_page_offset(pane, pager, 0, -1)) { - // The reason to do this here instead of letting it resolve in - // refresh_pane_pager() is so we can return early if - // we are at the top of the page. - return false; - } - pager->anchor--; - pager->find_anchor = PAGE_UP; - // Wait for the requested previous page to come in. - if (pager->anchor < 0) return false; - } else { - pager->selected--; - } - return true; - } else if (input->id == NCKEY_RETURN || input->id == '1') { - add_selected_from_index(pane->ui->c, pager, 0); - } else if (input->id == '2') { - add_selected_from_index(pane->ui->c, pager, 1); - } else if (input->id == '3') { - add_selected_from_index(pane->ui->c, pager, 2); - } else if (input->id == '4') { - add_selected_from_index(pane->ui->c, pager, 3); - } - return false; -} - -bool cmc_pane_search_handle_input_overlay(struct cmc_pane_search *pane, struct ncinput *input) -{ - if (!(input->evtype == NCTYPE_PRESS || input->evtype == NCTYPE_UNKNOWN)) return false; - struct cmc_ui *ui = pane->ui; - struct cmc_pager *pager = &pane->search->pager; - if (input->id == NCKEY_TAB) { - pager->redraw = true; - } else if (input->id == 'j') { - if (ui->overlay.selected + 1 < (s32)pane->searches.size) { - ui->overlay.selected++; - return true; - } - } else if (input->id == 'k') { - if (ui->overlay.selected - 1 >= 0) { - ui->overlay.selected--; - return true; - } - } else if (input->id == NCKEY_RETURN) { - cmc_ui_toggle_overlay(ui); - struct cmc_view *view; - al_array_foreach(pager->prev, j, view) { - ncplane_erase(view->container); - ncplane_reparent_family(view->container, view->container); - } - pager->prev.size = 0; - pane->search = al_array_at(pane->searches, ui->overlay.selected); - pager = &pane->search->pager; - pager->redraw = true; - return true; - } - return false; -} - -void cmc_pane_search_maybe_draw_overlay(struct cmc_pane_search *pane) -{ - struct cmc_ui *ui = pane->ui; - - if (!ui->overlay.active) return; - - ncplane_cursor_move_yx(ui->overlay.p, 0, 0); - ncplane_erase(ui->overlay.p); - - u64 color = 0; - u32 background = 0; - ncchannel_set_rgb8(&background, 255, 255, 255); - ncchannel_set_alpha(&background, 255); - ncchannels_set_fchannel(&color, background); - - ncchannel_set_rgb8(&background, 124, 12, 53); - ncchannels_set_bchannel(&color, background); - ncchannels_set_fchannel(&color, background); - ncplane_gradient(ui->overlay.p, 0, 0, ui->overlay.rows, ui->overlay.cols, "", 0, color, color, color, color); - - ncplane_set_bg_rgb8(ui->overlay.p, 124, 12, 53); - - struct cmc_search *search; - al_array_foreach(ui->search.searches, i, search) { - if ((s32)i == ui->overlay.selected) { - ncplane_set_fg_palindex(ui->overlay.p, 4); - } else { - ncplane_set_fg_default(ui->overlay.p); - } - str *query = &search->search->query; - for (u32 j = 0; j < query->len; j++) { - ncplane_putchar_yx(ui->overlay.p, i, j + 1, al_str_at(query, j)); - } - } -} - -static void draw_selection_line(struct cmc_view *view, u32 y, u32 rows) -{ - ncplane_move_yx(view->container, y, 0); - wchar_t box = L'█'; - u32 k = ((struct cmc_view_twitter *)view)->repost ? 1 : 0; - for (; k < rows - 1; k++) { - ncplane_cursor_move_yx(view->container, k, 0); - ncplane_putwc(view->container, box); - } -} - -void cmc_pane_search_refresh(struct cmc_pane_search *pane) -{ - struct cmc_pager *pager = &pane->search->pager; - if (pager->pending_requests > 0) return; - - u32 page = pane->search->search->page; - if (!cmc_get_page_from_num(pager, page)) { - request_page_internal(pane->ui->c, pane->search, page); - return; - } - - if (pager->find_anchor == PAGE_UP) { - u32 count; - // If find_anchor = PAGE_UP, search->anchor is index of the requested view. - s32 anchor = pager->anchor; - while (anchor >= 0) { - count = 0; - cmc_pager_iterate_to_height(pager, pane->ui->rows, anchor, &count, NULL, NULL); - // TODO: Handle count == 0. - // Is the requested view still visible? - if ((s32)(anchor + count) - 1 < pager->anchor) break; - anchor--; - // Load more posts if needed. - if (anchor < 0 && request_page_at_page_offset(pane, pager, 0, -1)) return; - } - anchor++; // We want the anchor before the last attempt above. - pager->selected = abs(pager->anchor - anchor); - pager->anchor = anchor; - pager->redraw = true; - pager->find_anchor = PAGE_NONE; - } else if (pager->find_anchor == PAGE_DOWN) { - // When moving down we already know how many views are being displayed - // so just move the anchor 1 past that. - pager->selected = 0; - pager->anchor += pager->count; - pager->redraw = true; - pager->find_anchor = PAGE_NONE; - } - - cmc_pane_search_maybe_draw_overlay(pane); - - struct cmc_view *view; - bool out_of_space = true; - if (pager->redraw) { - al_array_foreach(pager->prev, j, view) { - ncplane_erase(view->container); - ncplane_reparent_family(view->container, view->container); - } - pager->prev.size = 0; - pager->count = 0; - out_of_space = cmc_pager_iterate_to_height(pager, pane->ui->rows, pager->anchor, &pager->count, cmc_draw_tweet_func, pane->ui); - } else { - al_array_foreach(pager->prev, j, view) { - ncplane_erase(view->container); - } - } - if ((view = get_view_by_visual_index(pager, pager->selected))) { - draw_selection_line(view, view->y, view->rows); - } - if (pager->redraw) { - pager->redraw = false; - notcurses_refresh(pane->ui->nc, NULL, NULL); - } - notcurses_render(pane->ui->nc); - if (pager->count > 0 && !out_of_space && pager->end_of_page == -1) { - request_page_at_page_offset(pane, pager, (pager->anchor + pager->count) - 1, 1); - } -} - -static struct cmc_search *get_search_from_id(struct cmc_pane_search *pane, s32 id) -{ - struct cmc_search *search; - al_array_foreach(pane->searches, i, search) { - if (search->search->id == id) return search; - } - return NULL; -} - -static void process_page_func(void *userdata, struct cmc_view_page *page) -{ - struct cmc_ui *ui = (struct cmc_ui *)userdata; - struct cmc_view *view; - al_array_foreach(page->views, i, view) { - view->rows = cmc_layout_tweet(ui, (struct cmc_view_twitter *)view, ui->cols); - cmc_draw_tweet((struct cmc_view_twitter *)view, view->container, view->rows, ui->cols); - } -} - -void cmc_ui_resources_callback(void *userdata) -{ - struct cmc_pane_search *pane = (struct cmc_pane_search *)userdata; - struct cmc_pager *pager = &pane->search->pager; - if (cmc_pager_maybe_process_pages(pager, process_page_func, pane->ui)) { - pager->redraw = true; - cmc_pane_search_refresh(pane); - } -} - -void cmc_ui_handle_results(struct cmc_pane_search *pane, struct camu_result_list *results) -{ - struct cmc_search *search = get_search_from_id(pane, results->search->id); - if (!search) return; - struct cmc_pager *pager = &search->pager; - pager->pending_requests--; - - struct cmc_view_page *page = al_alloc_object(struct cmc_view_page); - al_array_push(pager->pages, page); - page->num = results->num; - page->processed = false; - if (page->num > pager->max_page_num) pager->max_page_num = page->num; - if (page->num < pager->min_page_num) pager->min_page_num = page->num; - if (pager->find_anchor == PAGE_UP) { - pager->anchor += results->unique_ids.size; - if (pager->end_of_page >= 0) { - pager->end_of_page += results->unique_ids.size; - } - } - - str *unique_id; - struct cmc_view_twitter *view; - al_array_foreach_ptr(results->unique_ids, i, unique_id) { - view = cmc_create_view_twitter(pane->ui, unique_id); - al_array_push(page->views, &view->v); - } - - if (cmc_pager_maybe_process_pages(pager, process_page_func, pane->ui)) { - pager->redraw = true; - cmc_pane_search_refresh(pane); - } -} - -void cmc_ui_handle_end_of_results(struct cmc_pane_search *pane, s32 id) -{ - struct cmc_search *search = get_search_from_id(pane, id); - if (!search) return; - struct cmc_pager *pager = &search->pager; - pager->pending_requests--; - pager->end_of_page = (pager->anchor + pager->count) - 1; - if (cmc_pager_maybe_process_pages(pager, process_page_func, pane->ui)) { - pager->redraw = true; - cmc_pane_search_refresh(pane); - } -} diff --git a/src/fruits/cmc/tui/pane_search.h b/src/fruits/cmc/tui/pane_search.h deleted file mode 100644 index b7c5e2c..0000000 --- a/src/fruits/cmc/tui/pane_search.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include <notcurses/notcurses.h> - -#include "../../libclient/client.h" - -#include "pager.h" - -struct cmc_search { - struct camu_search *search; - struct cmc_pager pager; -}; - -struct cmc_ui; -struct cmc_pane_search { - array(struct cmc_search *) searches; - struct cmc_search *search; - struct cmc_ui *ui; -}; - -void cmc_pane_search_init(struct cmc_pane_search *pane, struct cmc_ui *ui); -bool cmc_pane_search_handle_input(struct cmc_pane_search *pane, struct ncinput *input); -bool cmc_pane_search_handle_input_overlay(struct cmc_pane_search *pane, struct ncinput *input); -void cmc_pane_search_draw_overlay(struct cmc_pane_search *pane); -void cmc_pane_search_refresh(struct cmc_pane_search *pane); - -void cmc_ui_resources_callback(void *userdata); -void cmc_ui_handle_results(struct cmc_pane_search *pane, struct camu_result_list *results); -void cmc_ui_handle_end_of_results(struct cmc_pane_search *pane, s32 id); diff --git a/src/fruits/cmc/tui/resources.c b/src/fruits/cmc/tui/resources.c deleted file mode 100644 index 19513bf..0000000 --- a/src/fruits/cmc/tui/resources.c +++ /dev/null @@ -1,85 +0,0 @@ -#include <al/log.h> -#include <al/random.h> -#define STB_IMAGE_IMPLEMENTATION -#define STB_IMAGE_STATIC -#define STBI_NO_STDIO -#define STBI_NO_FAILURE_STRINGS -#define STBI_MALLOC al_malloc -#define STBI_FREE al_free -#define STBI_REALLOC al_realloc -#include <stb_image.h> - -#include "resources.h" -#include "view.h" - -static void resource_client_callback(void *userdata, u16 id, struct aki_buffer *buffer) -{ - struct cmc_resources *resources = (struct cmc_resources *)userdata; - struct cmc_view_resource *resource; - al_array_foreach(resources->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, 0); - if (channels == 3) { - resource->v = ncvisual_from_rgb_packed(pixels, h, w * channels, w, 255); - } else if (channels == 4) { - resource->v = ncvisual_from_rgba(pixels, h, w * channels, w); - } else { - al_log_warn("cmc", "Image has unsupported channel count."); - } - stbi_image_free(pixels); - ncvisual_geom(resources->nc, resource->v, &resource->vopts, &resource->geom); - struct cmc_view *view; - al_array_foreach(resource->refs, j, view) { - view->pending_requests--; - } - al_array_free(resource->refs); - break; - } - } - resources->callback(resources->userdata); -} - -bool cmc_resources_connect(struct cmc_resources *resources, struct notcurses *nc, - struct aki_event_loop *loop, str *addr, s32 port) -{ - resources->nc = nc; - resources->pending_requests = 0; - camu_resource_client_init(&resources->client, loop, resource_client_callback, resources); - return camu_resource_client_connect(&resources->client, addr, port); -} - -void cmc_resources_set_callback(struct cmc_resources *resources, void (*callback)(void *), void *userdata) -{ - resources->callback = callback; - resources->userdata = userdata; -} - -static struct cmc_view_resource *create_resource(struct cmc_resources *resources, str *unique_id, u32 index) -{ - struct cmc_view_resource *resource = al_alloc_object(struct cmc_view_resource); - al_array_push(resources->cache, resource); - resource->vopts.blitter = NCBLIT_PIXEL; - resource->vopts.scaling = NCSCALE_SCALE; - resource->id = al_rand_u16(); - al_str_clone(&resource->unique_id, unique_id); - resource->index = index; - al_array_init(resource->refs); - camu_resource_request(&resources->client, unique_id, index, resource->id); - return resource; -} - -struct cmc_view_resource *cmc_get_resource_from_cache(struct cmc_resources *resources, struct cmc_view *view, - str *unique_id, u32 index) -{ - struct cmc_view_resource *resource; - al_array_foreach(resources->cache, i, resource) { - if (al_str_eq(&resource->unique_id, unique_id) && resource->index == index) { - return resource; - } - } - resource = create_resource(resources, unique_id, index); - view->pending_requests++; - al_array_push(resource->refs, view); - return resource; -} diff --git a/src/fruits/cmc/tui/resources.h b/src/fruits/cmc/tui/resources.h deleted file mode 100644 index f12d861..0000000 --- a/src/fruits/cmc/tui/resources.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include <notcurses/notcurses.h> - -#include "../../libclient/resource_client.h" - -struct cmc_view; -struct cmc_view_resource { - u16 id; - str unique_id; - u32 index; - array(struct cmc_view *) refs; - struct ncvisual *v; - struct ncvgeom geom; - struct ncvisual_options vopts; -}; - -struct cmc_resources { - struct camu_resource_client client; - u32 pending_requests; - struct notcurses *nc; - array(struct cmc_view_resource *) cache; - void (*callback)(void *); - void *userdata; -}; - -bool cmc_resources_connect(struct cmc_resources *resources, struct notcurses *nc, - struct aki_event_loop *loop, str *addr, s32 port); -void cmc_resources_set_callback(struct cmc_resources *resources, void (*callback)(void *), void *userdata); -struct cmc_view_resource *cmc_get_resource_from_cache(struct cmc_resources *resources, struct cmc_view *view, - str *unique_id, u32 index); diff --git a/src/fruits/cmc/tui/ui.c b/src/fruits/cmc/tui/ui.c deleted file mode 100644 index 2c406bd..0000000 --- a/src/fruits/cmc/tui/ui.c +++ /dev/null @@ -1,144 +0,0 @@ -#include "../../server/common.h" - -#include "../cmc.h" - -#include "ui.h" - -/* -struct nccell ul, ur; nccell_init(&ul); nccell_init(&ur); -struct nccell ll, lr; nccell_init(&ll); nccell_init(&lr); -struct nccell hl, vl; nccell_init(&hl); nccell_init(&vl); -//nccell_set_channels(&hl, color); -//nccell_set_channels(&vl, color); - -ncplane_box(tui->overlay.p, &ul, &ur, &ll, &lr, &hl, &vl, tui->overlay.rows - 1, tui->overlay.cols - 1, 0); - -nccell_release(tui->overlay.p, &ul); nccell_release(tui->overlay.p, &ur); -nccell_release(tui->overlay.p, &ll); nccell_release(tui->overlay.p, &lr); -nccell_release(tui->overlay.p, &hl); nccell_release(tui->overlay.p, &vl); -*/ - -//ncplane_double_box(tui->overlay.p, NCSTYLE_NONE, color, tui->overlay.rows - 1, tui->overlay.cols - 1, 0); -//ncplane_rounded_box(tui->overlay.p, NCSTYLE_NONE, color, tui->overlay.rows - 1, tui->overlay.cols - 1, 0); - -void cmc_ui_toggle_overlay(struct cmc_ui *ui) -{ - if (ui->overlay.active) { - ncplane_reparent_family(ui->overlay.p, ui->overlay.p); - } else { - ncplane_reparent_family(ui->overlay.p, ui->p); - } - ui->overlay.active = !ui->overlay.active; -} - -static void refresh_internal(struct cmc_ui *ui) -{ - switch (ui->pane) { - case CMC_PANE_LIBRARY: - break; - case CMC_PANE_LISTS: - cmc_pane_lists_refresh(&ui->lists); - break; - case CMC_PANE_BROWSE: - break; - case CMC_PANE_SEARCH: - cmc_pane_search_refresh(&ui->search); - break; - } -} - -static void input_poll_callback(void *userdata, s32 revents) -{ - struct cmc_ui *ui = (struct cmc_ui *)userdata; - (void)revents; - struct ncinput input; - u32 ret; - bool do_refresh = false; - do { - ret = notcurses_get_nblock(ui->nc, &input); - if (ret == (u32)-1 || ret == 0) break; - if (input.evtype == NCTYPE_PRESS || input.evtype == NCTYPE_UNKNOWN) { - if (input.id == 'q') { - aki_event_loop_break(&ui->c->loop); - break; - } else if (input.id == NCKEY_TAB) { - cmc_ui_toggle_overlay(ui); - do_refresh = true; - } - switch (ui->pane) { - case CMC_PANE_LIBRARY: - break; - case CMC_PANE_LISTS: - if (ui->overlay.active) { - do_refresh |= cmc_pane_lists_handle_input_overlay(&ui->lists, &input); - } else { - do_refresh |= cmc_pane_lists_handle_input(&ui->lists, &input); - } - break; - case CMC_PANE_BROWSE: - break; - case CMC_PANE_SEARCH: - if (ui->overlay.active) { - do_refresh |= cmc_pane_search_handle_input_overlay(&ui->search, &input); - } else { - do_refresh |= cmc_pane_search_handle_input(&ui->search, &input); - } - break; - } - } - } while (1); - if (do_refresh) refresh_internal(ui); -} - -static s32 resize_cb(struct ncplane *p) -{ - struct cmc_ui *ui = (struct cmc_ui *)ncplane_userptr(p); - notcurses_stddim_yx(ui->nc, &ui->rows, &ui->cols); - ui->cols -= 4; - ui->rows -= 1; - struct ncplane_options nopts = { 0 }; - nopts.cols = ui->cols; - nopts.rows = ui->rows; - nopts.x = 3; - nopts.y = 1; - nopts.flags = 0; - ui->p = ncplane_create(p, &nopts); - ui->pane = CMC_PANE_SEARCH; - ui->overlay.cols = nopts.cols = 35; - ui->overlay.rows = nopts.rows = 20; - nopts.x = (ui->cols / 2) - (ui->overlay.cols / 2); - nopts.y = (ui->rows / 2) - (ui->overlay.rows / 2); - ui->overlay.p = ncpile_create(ui->nc, &nopts); - return 0; -} - -bool cmc_ui_open(struct cmc_ui *ui, struct cmc *c) -{ - if (!(ui->nc = notcurses_init(NULL, stdin))) return false; - - ui->c = c; - - cmc_pane_lists_init(&ui->lists, ui); - cmc_pane_search_init(&ui->search, ui); - - //if (!cmc_resources_connect(&ui->resources, ui->nc, &ui->c->loop, CAMU_SERVER_IP, CAMU_RESOURCE_PORT)) { - // return false; - //} - cmc_resources_set_callback(&ui->resources, cmc_ui_resources_callback, &ui->search); - - aki_poll_init(&ui->input, input_poll_callback, ui); - aki_poll_set(&ui->input, notcurses_inputready_fd(ui->nc), AKI_POLL_READ); - aki_poll_start(&ui->input, &c->loop); - - ui->overlay.active = false; - ui->overlay.selected = 0; - - struct ncplane *stdplane = notcurses_stdplane(ui->nc); - ncplane_set_resizecb(stdplane, resize_cb); - ncplane_set_userptr(stdplane, ui); - resize_cb(stdplane); - - refresh_internal(ui); - - return true; -} diff --git a/src/fruits/cmc/tui/ui.h b/src/fruits/cmc/tui/ui.h deleted file mode 100644 index 76ea353..0000000 --- a/src/fruits/cmc/tui/ui.h +++ /dev/null @@ -1,40 +0,0 @@ -#pragma once - -#include <aki/event_loop.h> -#include <notcurses/notcurses.h> - -#include "resources.h" -#include "pane_lists.h" -#include "pane_search.h" - -enum { - CMC_PANE_LIBRARY = 0, - CMC_PANE_LISTS, - CMC_PANE_BROWSE, - CMC_PANE_SEARCH -}; - -struct cmc; -struct cmc_ui { - struct notcurses *nc; - u32 cols; - u32 rows; - struct ncplane *p; - u8 pane; - struct { - struct ncplane *p; - u32 cols; - u32 rows; - bool active; - s32 selected; - } overlay; - struct aki_poll input; - struct cmc_resources resources; - struct cmc_pane_lists lists; - struct cmc_pane_search search; - struct cmc *c; -}; - -bool cmc_ui_open(struct cmc_ui *ui, struct cmc *c); -void cmc_ui_toggle_overlay(struct cmc_ui *ui); -void cmc_ui_maybe_draw_overlay(struct cmc_ui *ui); diff --git a/src/fruits/cmc/tui/view.h b/src/fruits/cmc/tui/view.h deleted file mode 100644 index 3c521c3..0000000 --- a/src/fruits/cmc/tui/view.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include <al/types.h> -#include <sho/post.h> - -enum { - CMC_VIEW_NONE = 0, - CMC_VIEW_SINGLE, - CMC_VIEW_PARTITION -}; - -struct cmc_view { - struct sho_post *post; - struct ncplane *container; - u32 pending_requests; - u32 rows; - u32 y; -}; - -struct cmc_view_page { - u32 num; - bool processed; - array(struct cmc_view *) views; -}; diff --git a/src/fruits/cmc/tui/view_twitter.c b/src/fruits/cmc/tui/view_twitter.c deleted file mode 100644 index b1070a4..0000000 --- a/src/fruits/cmc/tui/view_twitter.c +++ /dev/null @@ -1,370 +0,0 @@ -#include "../cmc.h" - -#include "view_twitter.h" - -#define TEXT_OFFSET 10 - -#define PROFILE_SIZE 5 -#define PREVIEW_OFFSET_WIDTH 26 - -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; -} - -u32 cmc_layout_tweet(struct cmc_ui *ui, struct cmc_view_twitter *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); - view->text.y = repost ? 1 : 0; - view->text.height += view->text.y; - rows += view->text.height; - - if (view->preview.mode == CMC_VIEW_NONE) { - return rows; - } - - view->preview.width = view->text.width - 1; - view->preview.x = TEXT_OFFSET; - u32 partition_width = view->preview.width / 2; - - struct ncplane_options opts = { 0 }; - opts.cols = view->preview.width; - opts.rows = ncplane_dim_y(notcurses_stdplane(ui->nc)) / 3; - opts.x = 0; - opts.y = 0; - struct ncplane *inter = ncplane_create(notcurses_stdplane(ui->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(ui->nc, resource->v, &resource->vopts, &resource->geom); - // rcellx == width case very untested. - left += resource->geom.rcelly + (s32)((resource->geom.rcellx != view->preview.width) ? 1 : -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(ui->nc, resource->v, &resource->vopts, &resource->geom); - left += resource->geom.rcelly + 1; - - resource = view->preview.resources[1]; - resource->vopts.n = inter; - ncvisual_geom(ui->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(ui->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(ui->nc, resource->v, &resource->vopts, &resource->geom); - right += resource->geom.rcelly + 1; - } - - break; - } - } - - ncplane_destroy(inter); - - view->preview.height = AL_MAX(right, left); - - return rows + view->preview.height; -} - -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 >= 1000000000) { - al_sprintf(out, "%.1fB", number / 1000000000.f); - } else 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); - } -} - -static void draw_tweet_resources(struct cmc_ui *ui, struct cmc_view_twitter *view) -{ - 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(ui->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(ui->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) + 3); - resource->vopts.n = view->preview.p[1]; - ncvisual_blit(ui->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(ui->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[1]->geom.rcelly + view->text.height + 2, (view->preview.x + view->preview.resources[2]->geom.rcellx) + 3); - resource->vopts.n = view->preview.p[3]; - ncvisual_blit(ui->nc, resource->v, &resource->vopts); - } - - break; - } - } - - 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(ui->nc, resource->v, &resource->vopts); - } -} - -void cmc_draw_tweet_func(void *userdata, struct cmc_view *view, u32 y) -{ - struct cmc_ui *ui = (struct cmc_ui *)userdata; - struct cmc_search *search = ui->search.search; - ncplane_reparent_family(view->container, ui->p); - ncplane_move_yx(view->container, y, 0); - draw_tweet_resources(ui, (struct cmc_view_twitter *)view); - view->y = y; - al_array_push(search->pager.prev, view); -} - -static char format_buf[32]; - -void cmc_draw_tweet(struct cmc_view_twitter *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); - - 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 = 0; - - if (post->comments.set) { - 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"❤"; - 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 + 2, format_buf); - } -} - -static void get_post_and_maybe_repost(struct cmc_ui *ui, str *unique_id, struct sho_post **post, struct sho_post **repost) -{ - struct sho_post_cache *cache = &ui->c->client.cache; - *post = sho_post_cache_get(cache, unique_id); - *repost = NULL; - if ((*post)->type == SHOKI_POST_REPOST) { - *repost = *post; - *post = sho_post_cache_get(cache, &(*post)->post.unique_id); - } -} - -static void strip_link_from_tweet(struct sho_post *post) -{ - wstr *check = al_wstr_c(L"https://t.co/"); - s32 index = al_wstr_rfind(&post->text, L':'); - if (index < 5) return; - index -= 5; // [h]ttps:// (if matched). - if (al_wstr_cmp(&post->text, check, index, check->len) == 0) { - al_wstr_at(&post->text, index) = L'\0'; - post->text.len = index; - } -} - -struct cmc_view_twitter *cmc_create_view_twitter(struct cmc_ui *ui, str *unique_id) -{ - struct cmc_view_twitter *view = al_alloc_object(struct cmc_view_twitter); - get_post_and_maybe_repost(ui, unique_id, &view->v.post, &view->repost); - - if (view->v.post->media.size > 0) { - strip_link_from_tweet(view->v.post); - } - - view->v.rows = 0; - view->v.y = 0; - view->v.pending_requests = 0; - - 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(ui->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); - - struct sho_post *post = view->v.post; - view->profile.resource = cmc_get_resource_from_cache(&ui->resources, &view->v, &post->author.unique_id, 0); - struct sho_post_media *media; - al_array_foreach_ptr(post->media, i, media) { - (void)media; - view->preview.resources[i] = cmc_get_resource_from_cache(&ui->resources, &view->v, &post->unique_id, i); - } - - switch (post->media.size) { - case 0: - view->preview.mode = CMC_VIEW_NONE; - break; - case 1: - view->preview.mode = CMC_VIEW_SINGLE; - break; - default: - view->preview.mode = CMC_VIEW_PARTITION; - break; - } - - return view; -} diff --git a/src/fruits/cmc/tui/view_twitter.h b/src/fruits/cmc/tui/view_twitter.h deleted file mode 100644 index 9792826..0000000 --- a/src/fruits/cmc/tui/view_twitter.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include "ui.h" -#include "view.h" - -struct cmc_view_twitter { - 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_view_twitter *cmc_create_view_twitter(struct cmc_ui *ui, str *unique_id); -void cmc_draw_tweet(struct cmc_view_twitter *view, struct ncplane *container, u32 rows, u32 width); -u32 cmc_layout_tweet(struct cmc_ui *ui, struct cmc_view_twitter *view, u32 width); -void cmc_draw_tweet_func(void *userdata, struct cmc_view *view, u32 y); diff --git a/src/fruits/cmsrv/cmsrv.c b/src/fruits/cmsrv/cmsrv.c new file mode 100644 index 0000000..08f67dd --- /dev/null +++ b/src/fruits/cmsrv/cmsrv.c @@ -0,0 +1,196 @@ +#define CAMU_LOCAL_SOCKET + +#include <al/log.h> +#include <aki/common.h> +#ifdef CAMU_LOCAL_SOCKET +#include <aki/line_processor.h> +#endif +#include <aki/timer.h> + +#include "../../server/server.h" +#include "../../server/list.h" +#include "../../server/common.h" +#include "../../cache/handlers/cdio.h" +#include "../../codec/ffmpeg/common.h" +#ifdef CAMU_LOCAL_SOCKET +#include "../../server/local_compat.h" +#endif + +#include "ui.h" + +struct cmsrv { + struct aki_event_loop loop; + struct aki_signal quit_signal; + struct camu_server server; +#ifdef CAMU_LOCAL_SOCKET + struct { + struct aki_socket sock; + struct aki_line_processor cli; + struct camu_local_compat compat; + } local; +#endif + struct cmsrv_ui ui; + struct aki_poll input_poll; + struct aki_timer render_timer; +}; + +#ifdef CAMU_LOCAL_SOCKET +static u8 server_line_callback(void *userdata, str *line) +{ + struct cmsrv *s = (struct cmsrv *)userdata; + struct camu_list *list = al_array_at(s->server.lists, 0); + if (al_str_eq(line, al_str_c(";PAUSE"))) { + lia_list_toggle_pause(&list->impl, LIANA_SEQUENCE_ANY, -1.0); + } else if (al_str_eq(line, al_str_c(";NEXT"))) { + lia_list_skip(&list->impl, LIANA_SEQUENCE_ANY, 1); + } else if (al_str_eq(line, al_str_c(";PREV"))) { + lia_list_skip(&list->impl, LIANA_SEQUENCE_ANY, -1); + } else if (al_str_eq(line, al_str_c(";SHUFFLE"))) { + lia_list_shuffle(&list->impl); + } else if (al_str_eq(line, al_str_c(";SORT"))) { + lia_list_sort(&list->impl); + } else if (al_str_eq(line, al_str_c(";REVERSE"))) { + lia_list_reverse(&list->impl); + } else if (al_str_eq(line, al_str_c(";CLEAR"))) { + lia_list_clear(&list->impl); + } else { + struct aki_packet *packet = aki_packet_create(); + aki_packet_write_str(packet, line); + camu_server_local_add(&s->server, packet); + } + return AKI_LINE_PROCESSOR_CONTINUE; +} +#endif + +static void meta_callback(void *userdata, u8 op, struct lia_list_entry *entry) +{ + struct cmsrv *s = (struct cmsrv *)userdata; + (void)s; + switch (op) { + case LIANA_META_PLAYING: { + struct camu_server_resource *resource = (struct camu_server_resource *)entry->opaque; +#ifdef CAMU_HAVE_PORTAL + if (resource->post) { + al_log_info("server", "now playing: %.*ls.", AL_WSTR_PRINTF(&resource->post->title)); + } else { +#endif + al_log_info("server", "now playing: %.*s.", AL_STR_PRINTF(&resource->unique_id)); +#ifdef CAMU_HAVE_PORTAL + } +#endif + break; + } + } +} + +static void render_timer_callback(void *userdata, struct aki_timer *timer) +{ + struct cmsrv *s = (struct cmsrv *)userdata; + (void)timer; + cmsrv_ui_render(&s->ui); + aki_timer_again(&s->render_timer); +} + +static void input_poll_callback(void *userdata, s32 revents) +{ + struct cmsrv *s = (struct cmsrv *)userdata; + (void)revents; + struct ncinput input; + do { + if (!cmsrv_ui_read_input(&s->ui, &input)) break; + if (input.evtype == NCTYPE_PRESS || input.evtype == NCTYPE_UNKNOWN) { + switch (input.id) { + case 'q': + aki_event_loop_break_one(&s->loop); + break; + case '1': + cmsrv_ui_set_pane(&s->ui, CMSRV_UI_LISTS); + break; + case '2': + cmsrv_ui_set_pane(&s->ui, CMSRV_UI_RESOURCES); + break; + case '3': + cmsrv_ui_set_pane(&s->ui, CMSRV_UI_NODES); + break; + } + } + } while (1); +} + +static void quit_signal_callback(void *userdata) +{ + struct cmsrv *s = (struct cmsrv *)userdata; + aki_event_loop_break_one(&s->loop); +} + +static s32 log_callback(void *userdata, char *message) +{ + struct cmsrv *s = (struct cmsrv *)userdata; + cmsrv_ui_push_message(&s->ui, message); + return al_strlen(message); +} + +static struct cmsrv s = { 0 }; + +static void sigint_handler(s32 signum) +{ + (void)signum; + aki_signal_send(&s.quit_signal); +} + +#ifndef _WIN32 +s32 main(s32 argc, char *argv[]) +#else +s32 wmain(s32 argc, wchar_t **argv) +#endif +{ + (void)argc; + (void)argv; + if (!aki_common_init() || !cmsrv_ui_init(&s.ui, &s.server)) return EXIT_FAILURE; + + signal(SIGINT, sigint_handler); + + al_set_print(log_callback, &s); + + camu_ff_set_default_log_callback(); + + aki_event_loop_init(&s.loop); + + aki_signal_init(&s.quit_signal, quit_signal_callback, &s); + aki_signal_start(&s.quit_signal, &s.loop); + + if (!camu_server_init(&s.server, CAMU_LOCAL_TYPE, &s.loop)) return EXIT_FAILURE; + camu_server_listen(&s.server, CAMU_LOCAL_ADDR, CAMU_PORT); + struct camu_list *list = al_array_at(s.server.lists, 0); + list->impl.callback = meta_callback; + list->impl.userdata = &s; + +#ifdef CAMU_LOCAL_SOCKET + s.local.sock.type = AKI_SOCKET_UNIX; + aki_socket_init(&s.local.sock); + aki_socket_set_blocking(&s.local.sock, false); + s.local.cli.callback = server_line_callback; + s.local.cli.userdata = &s; + aki_line_processor_init(&s.local.cli, al_str_c("\n")); + aki_line_processor_open_socket(&s.local.cli, &s.local.sock); + if (aki_socket_bind(&s.local.sock, CAMU_UNIX_LOCAL, 0) && aki_socket_listen(&s.local.sock)) { + aki_line_processor_run(&s.local.cli, &s.loop); + } +#endif + + aki_poll_init(&s.input_poll, input_poll_callback, &s); + aki_poll_set(&s.input_poll, cmsrv_ui_get_input_fd(&s.ui), AKI_POLL_READ); + aki_poll_start(&s.input_poll, &s.loop); + + aki_timer_init(&s.render_timer, &s.loop, render_timer_callback, &s); + aki_timer_set_repeat(&s.render_timer, AKI_TS_FROM_USEC(100000)); + aki_timer_again(&s.render_timer); + + aki_event_loop_run(&s.loop); + + cmsrv_ui_close(&s.ui); + + aki_common_close(); + + return EXIT_SUCCESS; +} diff --git a/src/fruits/cmsrv/meson.build b/src/fruits/cmsrv/meson.build new file mode 100644 index 0000000..ee8eb91 --- /dev/null +++ b/src/fruits/cmsrv/meson.build @@ -0,0 +1,19 @@ +cmsrv_src = ['cmsrv.c'] +cmsrv_deps = [server] +cmsrv_args = [] + +if get_option('portal').enabled() + cmsrv_deps += [portal] +endif + +use_tui = true +if use_tui + cmsrv_src += ['ui.c'] + cmsrv_deps += [dependency('notcurses-core')] +endif + +if is_windows and meson.is_cross_build() + cmsrv_args += ['-static', '-static-libgcc', '-static-libstdc++', '-municode', '-mwindows'] +endif + +executable('cmsrv', cmsrv_src, dependencies: cmsrv_deps, link_args: cmsrv_args) diff --git a/src/fruits/cmsrv/ui.c b/src/fruits/cmsrv/ui.c new file mode 100644 index 0000000..48f50ca --- /dev/null +++ b/src/fruits/cmsrv/ui.c @@ -0,0 +1,210 @@ +#include <al/lib.h> + +#include "../../server/list.h" + +#include "ui.h" + +#define LOG_RATIO 1.3 + +static s32 resize_cb(struct ncplane *p) +{ + struct cmsrv_ui *ui = (struct cmsrv_ui *)ncplane_userptr(p); + ui->pending_layout = true; + return 0; +} + +bool cmsrv_ui_init(struct cmsrv_ui *ui, struct camu_server *server) +{ + if (!(ui->nc = notcurses_core_init(NULL, stdin))) { + return false; + } + ui->server = server; + notcurses_stddim_yx(ui->nc, &ui->term_rows, &ui->term_cols); + struct ncplane *stdplane = notcurses_stdplane(ui->nc); + ncplane_set_resizecb(stdplane, resize_cb); + ncplane_set_userptr(stdplane, ui); + ui->pending_layout = true; + al_array_init(ui->log.messages); + return true; +} + +s32 cmsrv_ui_get_input_fd(struct cmsrv_ui *ui) +{ + return notcurses_inputready_fd(ui->nc); +} + +bool cmsrv_ui_read_input(struct cmsrv_ui *ui, struct ncinput *input) +{ + al_memset(input, 0, sizeof(struct ncinput)); + u32 ret = notcurses_get_nblock(ui->nc, input); + return !(ret == (u32)-1 || ret == 0); +} + +void cmsrv_ui_set_pane(struct cmsrv_ui *ui, u8 pane) +{ + ui->pane = pane; +} + +void cmsrv_ui_push_message(struct cmsrv_ui *ui, char *message) +{ + al_array_push(ui->log.messages, message); +} + +static void layout_log(struct cmsrv_ui *ui, struct ncplane *parent) +{ + struct ncplane_options nopts = { + .rows = ui->term_rows / LOG_RATIO, + .cols = ui->term_cols, + .x = 0 + }; + nopts.y = ui->term_rows - nopts.rows; + ui->log.p = ncplane_create(parent, &nopts); +} + +static void erase_log(struct cmsrv_ui *ui) +{ + struct ncplane *p = ui->log.p; + ncplane_erase(p); +} + +static void render_log(struct cmsrv_ui *ui) +{ + struct ncplane *p = ui->log.p; + ncplane_erase(p); + + u32 max_width = ncplane_dim_x(p) - 2; + u32 max_height = ncplane_dim_y(p) - 2; + + u64 c = 0; + ncchannels_set_fg_default(&c); + ncplane_rounded_box(p, NCSTYLE_NONE, c, max_height + 1, max_width + 1, 0); + + u32 size = ui->log.messages.size; + u32 index = size > max_height ? size - max_height : 0; + for (u32 i = index; i < size; i++) { + ncplane_putnstr_yx(p, (i - index) + 1, 1, max_width, al_array_at(ui->log.messages, i)); + } +} + +static void layout_lists(struct cmsrv_ui *ui, struct ncplane *parent) +{ + struct ncplane_options nopts = { + .rows = ui->term_rows - (ui->term_rows / LOG_RATIO), + .cols = ui->term_cols, + .x = 0, + .y = 0 + }; + ui->lists.p = ncplane_create(parent, &nopts); +} + +static void erase_lists(struct cmsrv_ui *ui) +{ + struct ncplane *p = ui->lists.p; + ncplane_erase(p); +} + +static void render_lists(struct cmsrv_ui *ui) +{ + struct ncplane *p = ui->lists.p; + + u32 max_width = ncplane_dim_x(p); + u32 max_height = ncplane_dim_y(p); + u32 current_line = 0; + s32 entries_per_list = 10; + + struct camu_list *list; + al_array_foreach(ui->server->lists, i, list) { + if (current_line++ >= max_height) break; + char *c_str = al_str_to_c_str(&list->name); + ncplane_putnstr_yx(p, i, 0, max_width, c_str); + al_free(c_str); + struct lia_list *impl = &list->impl; + s32 current = (s32)impl->current; + s32 index = AL_MAX(current - (entries_per_list / 2), 0); + s32 size = (s32)impl->entries.size; + s32 end = AL_MIN(index + entries_per_list, size); + for (s32 j = index; j < end; j++) { + struct lia_list_entry *entry = al_array_at(impl->entries, j); + c_str = al_str_to_c_str(&entry->name); + u32 y = i + (j - index) + 1; + if (j == current) { + ncplane_putchar_yx(p, y, 1, '>'); + ncplane_putnstr_yx(p, y, 3, max_width - 3, c_str); + } else { + ncplane_putnstr_yx(p, y, 1, max_width - 1, c_str); + } + al_free(c_str); + if (++current_line >= max_height) break; + } + } +} + +static void layout_nodes(struct cmsrv_ui *ui, struct ncplane *parent) +{ + struct ncplane_options nopts = { + .rows = ui->term_rows - (ui->term_rows / LOG_RATIO), + .cols = ui->term_cols, + .x = 0, + .y = 0 + }; + ui->nodes.p = ncplane_create(parent, &nopts); +} + +static void erase_nodes(struct cmsrv_ui *ui) +{ + struct ncplane *p = ui->nodes.p; + ncplane_erase(p); +} + +static void render_nodes(struct cmsrv_ui *ui) +{ + struct ncplane *p = ui->lists.p; + + u32 max_width = ncplane_dim_x(p); + u32 max_height = ncplane_dim_y(p); + (void)max_height; + + char strbuf[16]; + + struct lia_node *node; + al_array_foreach(ui->server->data.server.nodes, i, node) { + al_snprintf(strbuf, sizeof(strbuf), "%hu", node->id); + ncplane_putnstr_yx(p, i, 0, max_width, strbuf); + struct lia_node_connection *conn; + al_array_foreach(node->connections, j, conn) { + al_snprintf(strbuf, sizeof(strbuf), "%u", conn->pool.ready.size); + ncplane_putnstr_yx(p, i + j + 1, 2, max_width, strbuf); + } + } +} + +void cmsrv_ui_render(struct cmsrv_ui *ui) +{ + if (ui->pending_layout) { + ui->pending_layout = false; + struct ncplane *stdplane = notcurses_stdplane(ui->nc); + layout_log(ui, stdplane); + layout_lists(ui, stdplane); + layout_nodes(ui, stdplane); + } + erase_log(ui); + erase_lists(ui); + erase_nodes(ui); + render_log(ui); + switch (ui->pane) { + case CMSRV_UI_LISTS: + render_lists(ui); + break; + case CMSRV_UI_RESOURCES: + break; + case CMSRV_UI_NODES: + render_nodes(ui); + break; + } + notcurses_render(ui->nc); +} + +void cmsrv_ui_close(struct cmsrv_ui *ui) +{ + notcurses_stop(ui->nc); +} diff --git a/src/fruits/cmsrv/ui.h b/src/fruits/cmsrv/ui.h new file mode 100644 index 0000000..08ad956 --- /dev/null +++ b/src/fruits/cmsrv/ui.h @@ -0,0 +1,44 @@ +#include <notcurses/notcurses.h> +#include <al/types.h> +#include <al/array.h> + +#include "../../server/server.h" + +enum { + CMSRV_UI_LISTS = 0, + CMSRV_UI_RESOURCES, + CMSRV_UI_NODES +}; + +struct cmsrv_ui { + struct notcurses *nc; + u32 term_cols; + u32 term_rows; + bool pending_layout; + u8 pane; + struct { + struct ncplane *p; + array(char *) messages; + } log; + struct { + struct ncplane *p; + } lists; + struct { + } resources; + struct { + struct ncplane *p; + } nodes; + struct camu_server *server; +}; + +bool cmsrv_ui_init(struct cmsrv_ui *ui, struct camu_server *server); + +s32 cmsrv_ui_get_input_fd(struct cmsrv_ui *ui); +bool cmsrv_ui_read_input(struct cmsrv_ui *ui, struct ncinput *input); + +void cmsrv_ui_set_pane(struct cmsrv_ui *ui, u8 pane); + +void cmsrv_ui_push_message(struct cmsrv_ui *ui, char *message); +void cmsrv_ui_render(struct cmsrv_ui *ui); + +void cmsrv_ui_close(struct cmsrv_ui *ui); diff --git a/src/fruits/cmv/cmv.c b/src/fruits/cmv/cmv.c new file mode 100644 index 0000000..8eaa8b7 --- /dev/null +++ b/src/fruits/cmv/cmv.c @@ -0,0 +1,151 @@ +#include <al/log.h> +#include <aki/event_loop.h> +#include <aki/thread.h> + +#include "../../sink/desktop.h" +#include "../../server/common.h" +#include "../../codec/ffmpeg/common.h" +#ifndef CAMU_SINK_ONLY +#include "../../server/server.h" +#include "../../server/list.h" +#else +#include "../../server/common.c" +#endif + +struct cmv { + struct aki_event_loop loop; + struct camu_desktop desktop; +#ifndef CAMU_SINK_ONLY + struct camu_server server; +#endif +}; + +#ifndef CAMU_SINK_ONLY +static void meta_callback(void *userdata, u8 op, struct lia_list_entry *entry) +{ + struct cmv *c = (struct cmv *)userdata; + (void)c; + switch (op) { + case LIANA_META_PLAYING: { + struct camu_server_resource *resource = (struct camu_server_resource *)entry->opaque; + al_log_info("server", "now playing: %.*s.", AL_STR_PRINTF(&resource->unique_id)); + break; + } + } +} + +static void exit_callback(void *userdata, struct camu_desktop *desktop) +{ + struct cmv *c = (struct cmv *)userdata; + (void)desktop; + camu_server_close(&c->server); +} +#endif + +static aki_thread_result AKI_THREADCALL event_loop_thread(void *userdata) +{ + struct cmv *c = (struct cmv *)userdata; + aki_event_loop_run(&c->loop); + return 0; +} + +static struct cmv c = { 0 }; + +static void sigint_handler(int signum) +{ + (void)signum; + c.desktop.should_quit = 1; +} + +#define failure() do { \ + goto out; \ +} while (0) + +#ifndef _WIN32 +s32 main(s32 argc, char *argv[]) +#else +s32 wmain(s32 argc, wchar_t **argv) +#endif +{ + if (!aki_common_init() || !stl_global_init(false)) { + return EXIT_FAILURE; + } + + s32 ret = EXIT_FAILURE; + + signal(SIGINT, sigint_handler); + + camu_ff_set_default_log_callback(); + + aki_event_loop_init(&c.loop); + + u8 type; + str *addr; +#ifndef CAMU_SINK_ONLY + bool local = argc > 1; + if (local) { + type = AKI_SOCKET_UNIX; addr = CAMU_UNIX_LOCAL; + if (!camu_server_init(&c.server, type, &c.loop)) failure(); + if (!camu_server_listen(&c.server, addr, CAMU_PORT)) failure(); + struct camu_list *list = al_array_at(c.server.lists, 0); + list->impl.callback = meta_callback; + list->impl.userdata = &c; + } else { + type = CAMU_LOCAL_TYPE; + addr = CAMU_LOCAL_ADDR; + } +#else + type = AKI_SOCKET_TCP; addr = CAMU_SERVER_IP; +#endif + +#ifndef CAMU_SINK_ONLY + if (local) { + for (s32 i = 1; i < argc; i++) { +#ifndef _WIN32 + str arg = *al_str_cr(argv[i]); +#else + str arg; + al_wstr_to_str(al_wstr_cr(argv[i]), &arg); +#endif + struct aki_packet *packet = aki_packet_create(); + aki_packet_write_str(packet, &arg); + camu_server_local_add(&c.server, packet); + } + } +#else + (void)argc; + (void)argv; +#endif + + if (!camu_desktop_init(&c.desktop, "cmv")) failure(); +#ifndef CAMU_SINK_ONLY + if (local) { + c.desktop.exit_callback = exit_callback; + c.desktop.userdata = &c; + } +#endif + if (!camu_desktop_connect(&c.desktop, type, &c.loop, addr, CAMU_PORT)) failure(); + + struct aki_thread thread0; + aki_thread_create(&thread0, event_loop_thread, &c); + + while (camu_desktop_tick(&c.desktop)) {} + + camu_desktop_stop(&c.desktop); + aki_thread_join(&thread0); + camu_desktop_free(&c.desktop); +#ifndef CAMU_SINK_ONLY + camu_server_free(&c.server); +#endif + + ret = EXIT_SUCCESS; +out: + aki_event_loop_destroy(&c.loop); + + camu_ff_free_default_log_callback(); + + stl_global_close(); + aki_common_close(); + + return ret; +} diff --git a/src/fruits/cmv/meson.build b/src/fruits/cmv/meson.build new file mode 100644 index 0000000..ba571c3 --- /dev/null +++ b/src/fruits/cmv/meson.build @@ -0,0 +1,25 @@ +cmv_src = ['cmv.c'] +cmv_deps = [desktop] +cmv_args = [] +if get_option('sink-only') + cmv_args += ['-DCAMU_SINK_ONLY'] +else + cmv_deps += [server] +endif + +if get_option('portal').enabled() + cmv_deps += [portal] +endif + +use_tui = false +if use_tui + cmv_src += [] + cmv_deps += [dependency('notcurses')] +endif + +cmv_link_args = [] +if is_windows and meson.is_cross_build() + cmv_link_args += ['-static', '-static-libgcc', '-static-libstdc++', '-municode', '-mwindows'] +endif + +executable('cmv', cmv_src, dependencies: cmv_deps, c_args: cmv_args, link_args: cmv_link_args) diff --git a/src/fruits/sink/meson.build b/src/fruits/sink/meson.build deleted file mode 100644 index 1dd9140..0000000 --- a/src/fruits/sink/meson.build +++ /dev/null @@ -1,15 +0,0 @@ -sink_src = ['sink.c'] -sink_deps = [common_deps, buffer, render, screen, mixer, libsink] -sink_args = [] - -use_tui = false -if use_tui - sink_src += [] - sink_deps += [dependency('notcurses')] -endif - -if is_windows and meson.is_cross_build() - sink_args += ['-static', '-static-libgcc', '-static-libstdc++', '-municode', '-mwindows'] -endif - -executable('sink', sink_src, dependencies: sink_deps, link_args: sink_args) diff --git a/src/fruits/sink/sink.c b/src/fruits/sink/sink.c deleted file mode 100644 index 2b3d451..0000000 --- a/src/fruits/sink/sink.c +++ /dev/null @@ -1,208 +0,0 @@ -#include <al/log.h> -#include <aki/event_loop.h> - -#include "../../mixer/audio_miniaudio.h" - -#include "../../screen/screen.h" -#include "../../render/renderer_libplacebo.h" -//#include "../../render/renderer_tiger.h" - -#include "../../libsink/sink.h" -#include "../../server/common.h" - -struct cmv { - s32 quit; - struct camu_screen scr; - struct camu_renderer *renderer; - struct camu_mixer mixer; - struct aki_event_loop loop; - struct camu_sink sink; -}; - -static u8 sink_callback(void *userdata, u8 op, u8 type, void *opaque) -{ - struct cmv *c = (struct cmv *)userdata; - switch (op) { - case CAMU_SINK_ADD_BUFFER: - switch (type) { - case CAMU_SINK_AUDIO: { - struct camu_audio_buffer *buf = (struct camu_audio_buffer *)opaque; - camu_mixer_add_buffer(&c->mixer, buf); - al_log_debug("cmv", "Audio buffer added."); - break; - } - case CAMU_SINK_VIDEO: { - struct camu_video_buffer *buf = (struct camu_video_buffer *)opaque; - camu_screen_add_buffer(&c->scr, buf); - al_log_debug("cmv", "Video buffer added."); - break; - } - } - break; - case CAMU_SINK_REMOVE_BUFFER: - switch (type) { - case CAMU_SINK_AUDIO: { - struct camu_audio_buffer *buf = (struct camu_audio_buffer *)opaque; - camu_mixer_remove_buffer(&c->mixer, buf); - al_log_debug("cmv", "Audio buffer removed."); - break; - } - case CAMU_SINK_VIDEO: { - struct camu_video_buffer *buf = (struct camu_video_buffer *)opaque; - camu_screen_remove_buffer(&c->scr, buf); - al_log_debug("cmv", "Video buffer removed."); - break; - } - } - break; - case CAMU_SINK_SWAP_BUFFER: - switch (type) { - case CAMU_SINK_AUDIO: { - break; - } - } - break; - case CAMU_SINK_SET_BUFFERED: { - break; - } - case CAMU_SINK_START: - switch (type) { - case CAMU_SINK_AUDIO: { - camu_mixer_resume(&c->mixer); - al_log_debug("cmv", "Audio started."); - break; - } - case CAMU_SINK_VIDEO: { - camu_screen_set_state(&c->scr, CAMU_SCREEN_PLAYING); - camu_screen_wake(&c->scr); - al_log_debug("cmv", "Video started."); - break; - } - } - break; - case CAMU_SINK_STOP: - switch (type) { - case CAMU_SINK_AUDIO: { - camu_mixer_pause(&c->mixer); - al_log_debug("cmv", "Audio stopped."); - break; - } - case CAMU_SINK_VIDEO: { - camu_screen_set_state(&c->scr, CAMU_SCREEN_PAUSED); - al_log_debug("cmv", "Video stopped."); - break; - } - } - break; - case CAMU_SINK_EXIT: - camu_sink_close(&c->sink); - break; - } - return CAMU_SINK_OK; -} - -static void screen_callback(void *userdata, u8 op, f64 float0) -{ - struct cmv *c = (struct cmv *)userdata; - switch (op) { - case CAMU_SCREEN_NEXT: - camu_sink_skip(&c->sink, 1); - break; - case CAMU_SCREEN_PREVIOUS: - camu_sink_skip(&c->sink, -1); - break; - case CAMU_SCREEN_TOGGLE_PAUSE: - camu_sink_toggle_pause(&c->sink); - break; - case CAMU_SCREEN_SEEK: - camu_sink_seek(&c->sink, float0); - break; - case CAMU_SCREEN_RESEEK: - camu_sink_reseek(&c->sink); - break; - case CAMU_SCREEN_CLOSE: - c->quit = 1; - break; - } -} - -static aki_thread_result AKI_THREADCALL event_loop_thread(void *userdata) -{ - struct cmv *c = (struct cmv *)userdata; - 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; - if (camu_sink_connect(&c->sink, CAMU_SERVER_IP, CAMU_PORT)) { - 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; -} - -#ifndef _WIN32 -s32 main(s32 argc, char *argv[]) -#else -s32 wmain(s32 argc, wchar_t **argv) -#endif -{ - (void)argc; - (void)argv; - if (!aki_common_init()) return EXIT_FAILURE; - - signal(SIGINT, sigint_handler); - - c.scr.callback = screen_callback; - c.scr.userdata = &c; - 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(); - if (!camu_screen_create_renderer(&c.scr, c.renderer)) { - goto err; - } - c.renderer->render(c.renderer, &c.scr); - - if (!camu_mixer_init(&c.mixer, (struct camu_audio *)&audio_plugin_miniaudio)) { - goto err; - } - - struct aki_thread thread0; - aki_thread_create(&thread0, event_loop_thread, &c); - - c.quit = 0; - - while (!c.quit) { - if (camu_screen_tick(&c.scr)) { - c.renderer->render(c.renderer, &c.scr); - } - } - - camu_screen_set_state(&c.scr, CAMU_SCREEN_CLOSED); - - camu_sink_stop(&c.sink); - - aki_thread_join(&thread0); - - camu_sink_free(&c.sink); - - camu_mixer_close(&c.mixer); - c.renderer->free(&c.renderer); - camu_screen_close(&c.scr); - - aki_common_close(); - - return EXIT_SUCCESS; -err: - aki_common_close(); - return EXIT_FAILURE; -} diff --git a/src/liana/client.c b/src/liana/client.c new file mode 100644 index 0000000..d556453 --- /dev/null +++ b/src/liana/client.c @@ -0,0 +1,199 @@ +#include "../server/common.h" +#ifdef CAMU_HAVE_FFMPEG +#include "../codec/ffmpeg/packet_ext.h" +#endif + +#include "client.h" +#include "handlers.h" + +static void data_packet_callback(void *userdata, struct aki_packet_stream *stream, struct aki_packet *packet) +{ + struct lia_client *client = (struct lia_client *)userdata; + (void)stream; + lia_vcr_push_packet(&client->vcr, packet); +} + +static void parse_info_packet(struct lia_client *client, struct aki_packet *packet) +{ + str liana; + aki_packet_read_str(packet, &liana); + client->duration = aki_packet_read_u64(packet); + u32 count = aki_packet_read_u32(packet); + for (u32 i = 0; i < count; i++) { + u8 mode = aki_packet_read_u8(packet); + struct lia_vcr_track *track = NULL; + switch (mode) { + case CAMU_NORMAL: { + client->mask |= 1 << 0; + track = al_alloc_object(struct lia_vcr_track); + track->stream.mode = mode; + u8 type = aki_packet_read_u8(packet); + track->stream.type = type; + if (type == CAMU_STREAM_AUDIO) { + struct camu_audio_format *fmt = &track->stream.audio.fmt; + fmt->format = aki_packet_read_s32(packet); + fmt->sample_rate = aki_packet_read_s32(packet); + fmt->channel_count = aki_packet_read_s32(packet); +#ifdef CAMU_HAVE_FFMPEG + av_channel_layout_default(&fmt->channel_layout, fmt->channel_count); +#endif + } else if (type == CAMU_STREAM_VIDEO) { + track->stream.video.width = aki_packet_read_s32(packet); + track->stream.video.height = aki_packet_read_s32(packet); + track->stream.video.format = aki_packet_read_s32(packet); + } + track->index = 0; + break; + } +#ifdef CAMU_HAVE_FFMPEG + case CAMU_FFMPEG_COMPAT: { + const AVCodec *codec = avcodec_find_decoder(aki_packet_read_av_codec_id(packet)); + AVFormatContext *format_context = avformat_alloc_context(); + AVStream *stream = aki_packet_read_av_stream(format_context, codec, packet); + s32 index = stream->index; + al_assert(index < 32); + switch (stream->codecpar->codec_type) { + case AVMEDIA_TYPE_AUDIO: + client->mask |= 1 << index; + break; + case AVMEDIA_TYPE_VIDEO: + client->mask |= 1 << index; + break; + case AVMEDIA_TYPE_SUBTITLE: + default: + continue; + } + track = al_alloc_object(struct lia_vcr_track); + track->stream.mode = CAMU_FFMPEG_COMPAT; + track->stream.type = stream->codecpar->codec_type; + track->stream.av.format_context = format_context; + track->stream.av.stream = stream; + if (track->stream.type == CAMU_STREAM_AUDIO) { + struct camu_audio_format *fmt = &track->stream.audio.fmt; + fmt->format = stream->codecpar->format; + fmt->sample_rate = stream->codecpar->sample_rate; + av_channel_layout_copy(&fmt->channel_layout, &stream->codecpar->ch_layout); + fmt->channel_count = stream->codecpar->ch_layout.nb_channels; + } + track->index = index; + break; + } +#endif + } + track->client = lia_handler_by_name(&liana)->create_client_handler(); + track->client->callback = client->callback; + track->client->userdata = client->userdata; + track->stream.mode = mode; + if (track->client->init(track->client, client->renderer, &track->stream)) { + client->callback(client->userdata, LIANA_CLIENT_CONFIGURE, track->client->stream, track); + } + lia_vcr_add_track(&client->vcr, track); + } +} + +static void info_packet_callback(void *userdata, struct aki_packet_stream *stream, struct aki_packet *packet) +{ + struct lia_client *client = (struct lia_client *)userdata; + client->connection_id = aki_packet_read_u16(packet); + parse_info_packet(client, packet); + al_assert(client->mask != 0); + aki_packet_free(packet); + stream->packet_callback = data_packet_callback; + struct aki_packet *rpacket = aki_packet_create(); + aki_packet_write_s32(rpacket, client->mask); + aki_packet_stream_send_packet(stream, rpacket); +} + +static void packet_sent_callback(void *userdata, struct aki_packet *packet) +{ + (void)userdata; + aki_packet_free(packet); +} + +static void connection_callback(void *userdata, struct aki_packet_stream *stream) +{ + struct lia_client *client = (struct lia_client *)userdata; + stream->packet_sent_callback = packet_sent_callback; + struct aki_packet *packet = aki_packet_create(); + aki_packet_write_u16(packet, client->id); + aki_packet_write_u16(packet, 0); + aki_packet_write_s32(packet, client->mask); + aki_packet_write_u64(packet, client->pos); + if (client->mask == 0) { + stream->packet_callback = info_packet_callback; + } else { + stream->packet_callback = data_packet_callback; + } + aki_packet_stream_send_packet(stream, packet); +} + +static void connection_closed_callback(void *userdata, struct aki_packet_stream *stream) +{ + struct lia_client *client = (struct lia_client *)userdata; + if (client->reconnect) { + lia_vcr_flush(&client->vcr); + } else { + lia_vcr_close_all(&client->vcr); + } + // REMOVE_BUFFERS can possibly run the event loop while waiting. + // This should be accounted for in the client code here to not cause + // any unexpected behavior. + client->callback(client->userdata, LIANA_CLIENT_REMOVE_BUFFERS, NULL, &client->reconnect); + if (client->reconnect) { + client->reconnect = false; + aki_packet_stream_reconnect(stream, &client->addr, client->port); + } else { + client->callback(client->userdata, LIANA_CLIENT_CLOSED, NULL, NULL); + } +} + +void lia_client_connect(struct lia_client *client, struct aki_event_loop *loop, u8 type, + str *addr, u16 port, u16 id, u64 pos, struct camu_renderer *renderer) +{ + client->loop = loop; + client->id = id; + client->pos = pos; + client->mask = 0; + client->reconnect = false; + lia_vcr_init(&client->vcr, &client->data); + // TODO: Starting and stopping of vcr could be more clear. + lia_vcr_start(&client->vcr, client->loop); + al_str_clone(&client->addr, addr); + client->port = port; + if (!aki_packet_stream_init(&client->data, type, connection_callback, connection_closed_callback, client)) { + connection_closed_callback(client, &client->data); + } + aki_packet_stream_set_multiplex(&client->data, CAMU_MULTIPLEX_LIANA); + client->renderer = renderer; + aki_packet_stream_connect(&client->data, client->loop, &client->addr, client->port); +} + +void lia_client_set_renderer(struct lia_client *client, struct camu_renderer *renderer) +{ + client->renderer = renderer; +} + +void lia_client_seek(struct lia_client *client, u64 pos) +{ + client->reconnect = true; + client->pos = pos; + aki_packet_stream_disconnect(&client->data); +} + +void lia_client_reseek(struct lia_client *client) +{ + (void)client; +} + +void lia_client_disconnect(struct lia_client *client) +{ + client->reconnect = false; + aki_packet_stream_disconnect(&client->data); +} + +void lia_client_free(struct lia_client *client) +{ + lia_vcr_free(&client->vcr); + aki_packet_stream_free(&client->data); + al_str_free(&client->addr); +} diff --git a/src/liana/client.h b/src/liana/client.h new file mode 100644 index 0000000..5c16cd8 --- /dev/null +++ b/src/liana/client.h @@ -0,0 +1,31 @@ +#pragma once + +#include <aki/packet_stream.h> + +#include "../codec/codec.h" + +#include "vcr.h" + +struct lia_client { + struct aki_event_loop *loop; + u16 id; + s32 mask; + u64 pos; + bool reconnect; + str addr; + u16 port; + u16 connection_id; + struct aki_packet_stream data; + u64 duration; + struct lia_vcr vcr; + struct camu_renderer *renderer; + void (*callback)(void *, u8, struct camu_codec_stream *stream, void *); + void *userdata; +}; + +void lia_client_connect(struct lia_client *client, struct aki_event_loop *loop, u8 type, + str *addr, u16 port, u16 id, u64 pos, struct camu_renderer *renderer); +void lia_client_seek(struct lia_client *client, u64 pos); +void lia_client_reseek(struct lia_client *client); +void lia_client_disconnect(struct lia_client *client); +void lia_client_free(struct lia_client *client); diff --git a/src/liana/common.h b/src/liana/common.h new file mode 100644 index 0000000..6f70f09 --- /dev/null +++ b/src/liana/common.h @@ -0,0 +1 @@ +#pragma once diff --git a/src/liana/handler.h b/src/liana/handler.h new file mode 100644 index 0000000..6c23ac5 --- /dev/null +++ b/src/liana/handler.h @@ -0,0 +1,58 @@ +#pragma once + +#include <aki/packet_pool.h> + +#include "../codec/codec.h" +#include "../cache/handle.h" + +struct lia_server_handler { + bool (*init)(struct lia_server_handler *, struct cch_handle *); + void (*write_info)(struct lia_server_handler *, struct aki_packet *); + void (*subscribe)(struct lia_server_handler *, s32); + u64 (*get_duration)(struct lia_server_handler *); + bool (*seek)(struct lia_server_handler *, u64); + void (*step)(struct lia_server_handler *); + void (*write_packet)(struct lia_server_handler *, struct aki_packet *); + void (*free)(struct lia_server_handler **); + s32 status; +}; + +enum { + LIANA_PACKET_DATA = 0, + LIANA_PACKET_EOF, + LIANA_PACKET_ERROR +}; + +enum { + LIANA_CLIENT_CONFIGURE = 0, + LIANA_CLIENT_SET, + LIANA_CLIENT_PAUSE, + LIANA_CLIENT_RESUME, + LIANA_CLIENT_DATA, + LIANA_CLIENT_REMOVE_BUFFERS, + LIANA_CLIENT_EOF, + LIANA_CLIENT_CLOSED +}; + +struct lia_resume_req { + u64 start; + u64 offset; +}; + +struct lia_seek_req { + u64 base; + u64 ts; + u64 delay; + bool paused; + u64 paused_at; +}; + +struct lia_client_handler { + bool (*init)(struct lia_client_handler *, struct camu_renderer *, struct camu_codec_stream *); + bool (*handle_packet)(struct lia_client_handler *, struct aki_packet *); + void (*flush)(struct lia_client_handler *); + void (*free)(struct lia_client_handler **); + struct camu_codec_stream *stream; + void (*callback)(void *, u8, struct camu_codec_stream *stream, void *); + void *userdata; +}; diff --git a/src/liana/handlers.c b/src/liana/handlers.c new file mode 100644 index 0000000..e67250a --- /dev/null +++ b/src/liana/handlers.c @@ -0,0 +1,37 @@ +#include "handlers.h" + +#include "handlers/codec.h" +#include "handlers/cdio.h" + +struct lia_handler_entry liana_handlers[] = { + { + .name = al_str_c("codec"), +#ifdef LIANA_SERVER + .create_server_handler = lia_codec_server_create, +#endif +#ifdef LIANA_CLIENT + .create_client_handler = lia_codec_client_create +#endif + }, +#ifdef LIANA_HAVE_CDIO + { + .name = al_str_c("cdio"), +#ifdef LIANA_SERVER + .create_server_handler = lia_cdio_server_create, +#endif +#ifdef LIANA_CLIENT + .create_client_handler = lia_cdio_client_create +#endif + } +#endif +}; + +struct lia_handler_entry *lia_handler_by_name(str *name) +{ + for (u32 i = 0; i < AL_ARRAY_SIZE(liana_handlers); i++) { + if (al_str_eq(liana_handlers[i].name, name)) { + return &liana_handlers[i]; + } + } + return NULL; +} diff --git a/src/liana/handlers.h b/src/liana/handlers.h new file mode 100644 index 0000000..0193f18 --- /dev/null +++ b/src/liana/handlers.h @@ -0,0 +1,13 @@ +#pragma once + +#include "handler.h" + +struct lia_handler_entry { + str *name; + struct lia_server_handler *(*create_server_handler)(void); + struct lia_client_handler *(*create_client_handler)(void); +}; + +extern struct lia_handler_entry liana_handlers[]; + +struct lia_handler_entry *lia_handler_by_name(str *name); diff --git a/src/liana/handlers/cdio.h b/src/liana/handlers/cdio.h new file mode 100644 index 0000000..2727616 --- /dev/null +++ b/src/liana/handlers/cdio.h @@ -0,0 +1,18 @@ +#pragma once + +#include "../handler.h" + +struct lia_cdio_server { + struct lia_server_handler handler; + struct cch_handle *handle; + struct camu_audio_format fmt; + f64 pts; + struct aki_buffer buffer; +}; + +struct lia_cdio_client { + struct lia_client_handler handler; +}; + +struct lia_server_handler *lia_cdio_server_create(); +struct lia_client_handler *lia_cdio_client_create(void); diff --git a/src/liana/handlers/cdio_client.c b/src/liana/handlers/cdio_client.c new file mode 100644 index 0000000..93b1a27 --- /dev/null +++ b/src/liana/handlers/cdio_client.c @@ -0,0 +1,49 @@ +#include "cdio.h" + +static bool cdio_client_init(struct lia_client_handler *handler, struct camu_renderer *renderer, + struct camu_codec_stream *stream) +{ + struct lia_cdio_client *cdio = (struct lia_cdio_client *)handler; + (void)renderer; + cdio->handler.stream = stream; + return true; +} + +static bool cdio_client_handle_packet(struct lia_client_handler *handler, struct aki_packet *packet) +{ + struct lia_cdio_client *cdio = (struct lia_cdio_client *)handler; + if (!packet) { + cdio->handler.callback(cdio->handler.userdata, LIANA_CLIENT_EOF, cdio->handler.stream, NULL); + return true; + } + struct camu_codec_frame *frame = al_alloc_object(struct camu_codec_frame); + frame->mode = CAMU_NORMAL; + frame->pts = aki_packet_read_f64(packet); + frame->audio.sample_count = aki_packet_read_s32(packet); + struct aki_buffer buffer; + aki_packet_read_buffer(packet, &buffer); + frame->data = al_malloc(buffer.size); + al_memcpy(frame->data, aki_buffer_get_ptr(&buffer, 0), buffer.size); + cdio->handler.callback(cdio->handler.userdata, LIANA_CLIENT_DATA, cdio->handler.stream, frame); + return true; +} + +static void cdio_client_flush(struct lia_client_handler *handler) +{ + (void)handler; +} + +static void cdio_client_free(struct lia_client_handler **handler) +{ + (void)handler; +} + +struct lia_client_handler *lia_cdio_client_create(void) +{ + struct lia_cdio_client *cdio = al_alloc_object(struct lia_cdio_client); + cdio->handler.init = cdio_client_init; + cdio->handler.handle_packet = cdio_client_handle_packet; + cdio->handler.flush = cdio_client_flush; + cdio->handler.free = cdio_client_free; + return (struct lia_client_handler *)cdio; +} diff --git a/src/liana/handlers/cdio_server.c b/src/liana/handlers/cdio_server.c new file mode 100644 index 0000000..1eaf6f3 --- /dev/null +++ b/src/liana/handlers/cdio_server.c @@ -0,0 +1,106 @@ +#include <al/log.h> +#include <cdio/paranoia/cdda.h> + +#include "../../cache/entry.h" +#include "../../cache/handlers/cdio.h" + +#include "cdio.h" + +#define SECTORS_PER_PACKET 8 + +static bool cdio_server_init(struct lia_server_handler *handler, struct cch_handle *handle) +{ + struct lia_cdio_server *cdio = (struct lia_cdio_server *)handler; + cdio->handle = handle; + + cdio->fmt.format = CAMU_SAMPLE_FORMAT_S16; + cdio->fmt.sample_rate = 44100; + cdio->fmt.channel_count = 2; + cdio->pts = 0.0; + aki_buffer_init(&cdio->buffer); + + if (handle->entry->chapter) { + cch_handle_seek(handle, handle->entry->chapter->start * CDIO_CD_FRAMESIZE_RAW, SEEK_SET); + } + + return true; +} + +static void cdio_server_write_info(struct lia_server_handler *handler, struct aki_packet *packet) +{ + struct lia_cdio_server *cdio = (struct lia_cdio_server *)handler; + aki_packet_write_u64(packet, 0); + aki_packet_write_u32(packet, 1); + aki_packet_write_u8(packet, CAMU_NORMAL); + aki_packet_write_u8(packet, CAMU_STREAM_AUDIO); + aki_packet_write_s32(packet, cdio->fmt.format); + aki_packet_write_s32(packet, cdio->fmt.sample_rate); + aki_packet_write_s32(packet, cdio->fmt.channel_count); +} + +static void cdio_server_subscribe(struct lia_server_handler *handler, s32 mask) +{ + (void)handler; + (void)mask; +} + +static u64 cdio_server_get_duration(struct lia_server_handler *handler) +{ + (void)handler; + return 0; +} + +static bool cdio_server_seek(struct lia_server_handler *handler, u64 pos) +{ + struct lia_cdio_server *cdio = (struct lia_cdio_server *)handler; + return cch_handle_seek(cdio->handle, pos, SEEK_SET); +} + +static void cdio_server_step(struct lia_server_handler *handler) +{ + struct lia_cdio_server *cdio = (struct lia_cdio_server *)handler; + (void)cdio; +} + +static void cdio_server_write_packet(struct lia_server_handler *handler, struct aki_packet *packet) +{ + struct lia_cdio_server *cdio = (struct lia_cdio_server *)handler; + off_t size = CDIO_CD_FRAMESIZE_RAW * SECTORS_PER_PACKET; + aki_buffer_ensure_space(&cdio->buffer, size); + s32 ret = cch_handle_read(cdio->handle, aki_buffer_get_ptr(&cdio->buffer, 0), size); + if (ret == CAMU_ERR_EOF) { + cdio->handler.status = CAMU_ERR_EOF; + aki_packet_write_u8(packet, LIANA_PACKET_EOF); + return; + } + cdio->handler.status = CAMU_OK; + aki_packet_write_u8(packet, LIANA_PACKET_DATA); + aki_packet_write_s32(packet, 0); + aki_packet_write_f64(packet, cdio->pts); + s32 sample_count = ret / (camu_audio_format_bytes_per_sample(&cdio->fmt) * cdio->fmt.channel_count); + cdio->pts += camu_audio_format_samples_to_sec(&cdio->fmt, sample_count); + aki_packet_write_s32(packet, sample_count); + aki_buffer_set_size(&cdio->buffer, ret); + aki_packet_write_buffer(packet, &cdio->buffer); +} + +static void cdio_server_free(struct lia_server_handler **handler) +{ + struct lia_cdio_server *cdio = (struct lia_cdio_server *)*handler; + al_free(cdio); + *handler = NULL; +} + +struct lia_server_handler *lia_cdio_server_create() +{ + struct lia_cdio_server *cdio = al_alloc_object(struct lia_cdio_server); + cdio->handler.init = cdio_server_init; + cdio->handler.write_info = cdio_server_write_info; + cdio->handler.subscribe = cdio_server_subscribe; + cdio->handler.get_duration = cdio_server_get_duration; + cdio->handler.seek = cdio_server_seek; + cdio->handler.step = cdio_server_step; + cdio->handler.write_packet = cdio_server_write_packet; + cdio->handler.free = cdio_server_free; + return (struct lia_server_handler *)cdio; +} diff --git a/src/liana/handlers/codec.h b/src/liana/handlers/codec.h new file mode 100644 index 0000000..8c5528f --- /dev/null +++ b/src/liana/handlers/codec.h @@ -0,0 +1,19 @@ +#pragma once + +#include "../../codec/codec.h" + +#include "../handler.h" + +struct lia_codec_server { + struct lia_server_handler handler; + struct camu_demuxer *demux; + struct camu_codec_packet packet; +}; + +struct lia_codec_client { + struct lia_client_handler handler; + struct camu_decoder *dec; +}; + +struct lia_server_handler *lia_codec_server_create(void); +struct lia_client_handler *lia_codec_client_create(void); diff --git a/src/liana/handlers/codec_client.c b/src/liana/handlers/codec_client.c new file mode 100644 index 0000000..050cc6f --- /dev/null +++ b/src/liana/handlers/codec_client.c @@ -0,0 +1,116 @@ +#include "../../codec/codec.h" +#include "../../codec/ffmpeg/decoder.h" +#include "../../codec/ffmpeg/packet_ext.h" +#include "../../codec/stb_image/decoder.h" +#include "../../codec/spng/decoder.h" +#include "../../codec/wuffs/decoder.h" + +#include "../vcr.h" + +#include "codec.h" + +static void data_callback(void *userdata, struct camu_codec_frame *frame) +{ + struct lia_codec_client *codec = (struct lia_codec_client *)userdata; + codec->handler.callback(codec->handler.userdata, LIANA_CLIENT_DATA, codec->handler.stream, frame); +} + +static bool codec_client_init(struct lia_client_handler *handler, struct camu_renderer *renderer, + struct camu_codec_stream *stream) +{ + struct lia_codec_client *codec = (struct lia_codec_client *)handler; + codec->dec = camu_ff_decoder_create(); + //codec->dec = camu_stbi_decoder_create(); + //codec->dec = camu_spng_decoder_create(); + //codec->dec = camu_wuffs_decoder_create(); + codec->handler.stream = stream; + if (!codec->dec->init(codec->dec, renderer, stream, data_callback, codec)) { + return false; + } + return true; +} + +#ifdef CAMU_HAVE_FFMPEG +static bool push_av_packet(struct lia_codec_client *codec, AVPacket *pkt) +{ + struct camu_codec_packet packet; + packet.av.pkt = pkt; + s32 ret = codec->dec->push(codec->dec, &packet); + av_packet_unref(pkt); + av_packet_free(&pkt); + return ret == CAMU_OK; +} +#endif + +static bool push_packet(struct lia_codec_client *codec, struct aki_buffer *buffer) +{ + struct camu_codec_packet packet; + packet.buffer = buffer; + s32 ret = codec->dec->push(codec->dec, &packet); + return ret == CAMU_OK; +} + +static bool codec_client_handle_packet(struct lia_client_handler *handler, struct aki_packet *packet) +{ + struct lia_codec_client *codec = (struct lia_codec_client *)handler; + if (!packet) { + struct lia_codec_client *codec = (struct lia_codec_client *)handler; + s32 ret = codec->dec->push(codec->dec, NULL); + // Flush returns success. + ret = codec->dec->process(codec->dec); + // process() could still error. + codec->handler.callback(codec->handler.userdata, LIANA_CLIENT_EOF, codec->handler.stream, NULL); + return ret == CAMU_ERR_EOF; + } + bool success = false; + u8 type = aki_packet_read_u8(packet); + switch (type) { + case CAMU_NORMAL: { + struct aki_buffer buffer; + aki_packet_read_buffer(packet, &buffer); + success = push_packet(codec, &buffer); + break; + } +#ifdef CAMU_HAVE_FFMPEG + case CAMU_FFMPEG_COMPAT: { + success = push_av_packet(codec, aki_packet_read_av_packet(packet)); + break; + } +#endif + } + // Forcing in EOF on errors is not necessary but should be a better experience client-side. + if (!success) { + codec->handler.callback(codec->handler.userdata, LIANA_CLIENT_EOF, codec->handler.stream, NULL); + return false; + } + s32 ret = codec->dec->process(codec->dec); + if (!(ret == CAMU_ERR_AGAIN || ret == CAMU_ERR_EOF)) { + codec->handler.callback(codec->handler.userdata, LIANA_CLIENT_EOF, codec->handler.stream, NULL); + return false; + } + return true; +} + +static void codec_client_flush(struct lia_client_handler *handler) +{ + struct lia_codec_client *codec = (struct lia_codec_client *)handler; + codec->dec->flush(codec->dec); +} + +static void codec_client_free(struct lia_client_handler **handler) +{ + struct lia_codec_client *codec = (struct lia_codec_client *)*handler; + codec->dec->free(&codec->dec); + al_free(codec); + *handler = NULL; +} + +struct lia_client_handler *lia_codec_client_create(void) +{ + struct lia_codec_client *codec = al_alloc_object(struct lia_codec_client); + codec->handler.init = codec_client_init; + codec->handler.handle_packet = codec_client_handle_packet; + codec->handler.flush = codec_client_flush; + codec->handler.free = codec_client_free; + return (struct lia_client_handler *)codec; +} diff --git a/src/liana/handlers/codec_server.c b/src/liana/handlers/codec_server.c new file mode 100644 index 0000000..07fafdf --- /dev/null +++ b/src/liana/handlers/codec_server.c @@ -0,0 +1,143 @@ +#include "../../codec/codec.h" +#include "../../codec/ffmpeg/demuxer.h" +#include "../../codec/ffmpeg/packet_ext.h" +#include "../../codec/stb_image/demuxer.h" +#include "../../codec/spng/demuxer.h" +#include "../../codec/wuffs/demuxer.h" + +#include "../handler.h" + +#include "codec.h" + +static bool codec_server_init(struct lia_server_handler *handler, struct cch_handle *handle) +{ + struct lia_codec_server *codec = (struct lia_codec_server *)handler; + codec->demux = camu_ff_demuxer_create(); + //codec->demux = camu_stbi_demuxer_create(); + //codec->demux = camu_spng_demuxer_create(); + //codec->demux = camu_wuffs_demuxer_create(); + if (!codec->demux->init(codec->demux, handle)) { + return false; + } + switch (codec->demux->mode) { + case CAMU_NORMAL: + break; +#ifdef CAMU_HAVE_FFMPEG + case CAMU_FFMPEG_COMPAT: + codec->packet.av.pkt = av_packet_alloc(); + break; +#endif + } + return true; +} + +static void codec_server_write_info(struct lia_server_handler *handler, struct aki_packet *packet) +{ + struct lia_codec_server *codec = (struct lia_codec_server *)handler; + u64 duration = codec->demux->get_duration(codec->demux); + aki_packet_write_u64(packet, duration); + aki_packet_write_u32(packet, codec->demux->streams.size); + struct camu_codec_stream *stream; + al_array_foreach_ptr(codec->demux->streams, i, stream) { + aki_packet_write_u8(packet, stream->mode); + switch (stream->mode) { + case CAMU_NORMAL: + aki_packet_write_u8(packet, stream->type); + aki_packet_write_s32(packet, stream->video.width); + aki_packet_write_s32(packet, stream->video.height); + aki_packet_write_s32(packet, stream->video.format); + break; +#ifdef CAMU_HAVE_FFMPEG + case CAMU_FFMPEG_COMPAT: + aki_packet_write_av_codec_id(packet, stream->av.stream->codecpar->codec_id); + aki_packet_write_av_stream(packet, stream->av.stream); + break; +#endif + } + } +} + +static void codec_server_subscribe(struct lia_server_handler *handler, s32 mask) +{ + struct lia_codec_server *codec = (struct lia_codec_server *)handler; + codec->demux->subscribed = mask; +} + +static u64 codec_server_get_duration(struct lia_server_handler *handler) +{ + struct lia_codec_server *codec = (struct lia_codec_server *)handler; + return codec->demux->get_duration(codec->demux); +} + +static bool codec_server_seek(struct lia_server_handler *handler, u64 pos) +{ + struct lia_codec_server *codec = (struct lia_codec_server *)handler; + return codec->demux->seek(codec->demux, pos); +} + +static void codec_server_step(struct lia_server_handler *handler) +{ + struct lia_codec_server *codec = (struct lia_codec_server *)handler; + codec->handler.status = codec->demux->get_packet(codec->demux, &codec->packet); +} + +static void codec_server_write_packet(struct lia_server_handler *handler, struct aki_packet *packet) +{ + struct lia_codec_server *codec = (struct lia_codec_server *)handler; + if (codec->handler.status == CAMU_OK) { + aki_packet_write_u8(packet, LIANA_PACKET_DATA); + switch (codec->packet.mode) { + case CAMU_NORMAL: { + aki_packet_write_s32(packet, 0); + aki_packet_write_u8(packet, codec->packet.mode); + aki_packet_write_buffer(packet, codec->packet.buffer); + break; + } +#ifdef CAMU_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, codec->packet.mode); + aki_packet_write_av_packet(packet, pkt); + av_packet_unref(pkt); + break; + } +#endif + } + } else if (codec->handler.status == CAMU_ERR_EOF) { + aki_packet_write_u8(packet, LIANA_PACKET_EOF); + } else { + aki_packet_write_u8(packet, LIANA_PACKET_ERROR); + } +} + +static void codec_server_free(struct lia_server_handler **handler) +{ + struct lia_codec_server *codec = (struct lia_codec_server *)*handler; + switch (codec->demux->mode) { + case CAMU_NORMAL: + break; +#ifdef CAMU_HAVE_FFMPEG + case CAMU_FFMPEG_COMPAT: + av_packet_free(&codec->packet.av.pkt); + break; +#endif + } + codec->demux->free(&codec->demux); + al_free(codec); + *handler = NULL; +} + +struct lia_server_handler *lia_codec_server_create(void) +{ + struct lia_codec_server *codec = al_alloc_object(struct lia_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; + codec->handler.write_packet = codec_server_write_packet; + codec->handler.free = codec_server_free; + return (struct lia_server_handler *)codec; +} diff --git a/src/liana/handlers/dvd.h b/src/liana/handlers/dvd.h new file mode 100644 index 0000000..788c30e --- /dev/null +++ b/src/liana/handlers/dvd.h @@ -0,0 +1,14 @@ +#pragma once + +#include "../handler.h" + +struct lia_dvd_server { + struct lia_server_handler handler; +}; + +struct lia_dvd_client { + struct lia_client_handler handler; +}; + +struct lia_server_handler *lia_dvd_server_create(void); +struct lia_client_handler *lia_dvd_client_create(void); diff --git a/src/shrub/handlers/dvd_server.c b/src/liana/handlers/dvd_server.c index ea3a9d9..a487de4 100644 --- a/src/shrub/handlers/dvd_server.c +++ b/src/liana/handlers/dvd_server.c @@ -1,21 +1,21 @@ #include "dvd.h" -static bool dvd_server_init(struct bmu_server_handler *handler, struct cch_handle *handle, - struct aki_packet_pool *pool) +static bool dvd_server_init(struct lia_server_handler *handler, struct cch_handle *handle) { - struct bmu_dvd_server *dvd = (struct bmu_dvd_server *)handler; + struct lia_dvd_server *dvd = (struct lia_dvd_server *)handler; return true; } -struct bmu_server_handler *bmu_dvd_server_create(void) +struct lia_server_handler *lia_dvd_server_create(void) { - struct bmu_dvd_server *dvd = al_alloc_object(struct bmu_dvd_server); + struct lia_dvd_server *dvd = al_alloc_object(struct lia_dvd_server); dvd->handler.init = dvd_server_init; // dvd->handler.write_info = dvd_server_write_info; // dvd->handler.subscribe = dvd_server_subscribe; // dvd->handler.get_duration = dvd_server_get_duration; // dvd->handler.seek = dvd_server_seek; // dvd->handler.step = dvd_server_step; +// dvd->handler.write_packet = dvd_server_write_packet; // dvd->handler.free = dvd_server_free; - return (struct bmu_server_handler *)dvd; + return (struct lia_server_handler *)dvd; } diff --git a/src/liana/list.c b/src/liana/list.c new file mode 100644 index 0000000..37a144f --- /dev/null +++ b/src/liana/list.c @@ -0,0 +1,413 @@ +#include <aki/thread.h> +#include <al/random.h> +#include <al/lib.h> +#include <al/log.h> + +#include "../libsink/common.h" + +#include "list.h" +#include "list_cmp.h" + +void lia_list_init(struct lia_list *list) +{ + list->current = -1; + list->previous = -1; + list->queued = -1; + list->idle = true; + al_array_init(list->entries); + al_array_init(list->sinks); +} + +/* +static void buffer_ahead(struct lia_list *list) +{ + s32 size = (s32)list->entries.size; + if (list->queued >= 0 && list->queued + 1 < size) { + s32 ahead = list->queued + 1; + for (s32 i = ahead; i < AL_MIN(ahead + LIANA_BUFFER_AHEAD, size); i++) { + struct lia_list_entry *buffered = al_array_at(list->entries, i); + struct lia_list_sink *sink; + al_array_foreach(list->sinks, i, sink) { + sink->callback(sink->userdata, CAMU_SINK_BUFFER, buffered, i, LIANA_TIMESTAMP_INVALID); + } + } + } +} +*/ + +static void unset_all(struct lia_list *list) +{ + struct lia_list_sink *sink; + al_array_foreach(list->sinks, i, sink) { + sink->set = -1; + sink->queued = -1; + } +} + +void lia_list_add_sink(struct lia_list *list, void (*callback)(void *, u8, struct lia_list_entry *, s32, struct lia_timing *), void *userdata) +{ + struct lia_list_sink *sink = al_alloc_object(struct lia_list_sink); + sink->callback = callback; + sink->userdata = userdata; + al_array_push(list->sinks, sink); + if (list->current >= 0) { + sink->set = list->current; + struct lia_list_entry *current = al_array_at(list->entries, list->current); + u64 now = aki_get_timestamp(); + u8 pause; + u64 at = LIANA_TIMESTAMP_INVALID; + u64 seek_pos = current->offset; + if (current->paused_at == LIANA_TIMESTAMP_INVALID) { + now += LIANA_BASE_DELAY; + if (now > current->start) { + at = now; + seek_pos += now - current->start; + } else { + at = current->start; + } + pause = LIANA_PAUSE_RESUME; + } else { + pause = LIANA_PAUSE_NONE; + } + struct lia_timing time = { + .at = at, + .seek_pos = seek_pos, + .pause = pause + }; + sink->callback(sink->userdata, CAMU_SINK_SET, current, list->current, &time); + } else { + sink->set = -1; + } + sink->queued = -1; +} + +void lia_list_unset(struct lia_list *list) +{ + unset_all(list); + list->current = list->entries.size - 1; + list->previous = -1; + list->queued = -1; + list->idle = true; + struct lia_list_sink *sink; + al_array_foreach(list->sinks, i, sink) { + sink->callback(sink->userdata, CAMU_SINK_CLEAR, NULL, -1, NULL); + } +} + +void lia_list_remove_sink(struct lia_list *list, void *userdata) +{ + struct lia_list_sink *sink; + al_array_foreach(list->sinks, i, sink) { + if (sink->userdata == userdata) { + al_array_remove_at(list->sinks, i); + al_free(sink); + break; + } + } +} + +void lia_list_add(struct lia_list *list, void *opaque, u64 duration, str *name) +{ + struct lia_list_entry *entry = al_alloc_object(struct lia_list_entry); + entry->opaque = opaque; + entry->paused_at = LIANA_TIMESTAMP_INVALID; + entry->held = false; + entry->offset = 0; + entry->duration = duration; + al_str_clone(&entry->name, name); + al_array_push(list->entries, entry); + if (list->idle) { + list->current++; + list->idle = false; + entry->start = aki_get_timestamp() + LIANA_BASE_DELAY; + struct lia_timing time = { + .at = entry->start, + .seek_pos = entry->offset, + .pause = LIANA_PAUSE_RESUME + }; + struct lia_list_sink *sink; + al_array_foreach(list->sinks, i, sink) { + al_assert(sink->set == -1); + sink->set = list->current; + sink->callback(sink->userdata, CAMU_SINK_SET, entry, list->current, &time); + } + if (list->callback) list->callback(list->userdata, LIANA_META_PLAYING, entry); + } else { + /* + if (list->queued == -1) { + struct lia_list_entry *current = al_array_at(list->entries, list->current); + list->queued = list->current + 1; + entry->start = current->start + (current->duration - current->offset); + struct lia_timing time = { + .at = entry->start, + .seek_pos = entry->offset, + .pause = LIANA_PAUSE_RESUME + }; + struct lia_list_sink *sink; + al_array_foreach(list->sinks, i, sink) { + sink->queued = list->queued; + sink->callback(sink->userdata, CAMU_SINK_BUFFER_AND_QUEUE, entry, list->queued, &time); + } + } else { + */ + entry->start = LIANA_TIMESTAMP_INVALID; + //} + if (list->callback) list->callback(list->userdata, LIANA_META_QUEUED, entry); + } +} + +static struct lia_list_entry *get_entry_from_sequence(struct lia_list *list, s32 sequence) +{ + s32 size = (s32)list->entries.size; + if (sequence < 0 || sequence >= size) return NULL; + return al_array_at(list->entries, sequence); +} + +static bool assume_done(struct lia_list_entry *entry, u64 at) +{ + if (entry->duration == 0) return true; + if (entry->paused_at == LIANA_TIMESTAMP_INVALID && entry->start != LIANA_TIMESTAMP_INVALID) { + if (entry->offset > entry->duration) { + return true; + } + if (at < entry->start) return false; + if (at - entry->start >= entry->duration - entry->offset) { + return true; + } + } + return false; +} + +void lia_list_skipto(struct lia_list *list, s32 sequence, s32 index) +{ + if (sequence == LIANA_SEQUENCE_ANY) sequence = list->current; + if (index == list->current) return; + + struct lia_list_entry *current = get_entry_from_sequence(list, sequence); + struct lia_list_entry *target = get_entry_from_sequence(list, index); + if (!current || !target) return; + + u64 now = aki_get_timestamp(); + u64 at = now + LIANA_BASE_DELAY; + u8 pause; + + // TODO: + // - Think about what is means for an entry to be done. never put into a pause state? + // - clock_end()?? + // - Sink needs to handle case where entry gets queued but the list already expects it to be playing + // - It's possible to know if sink->current is done during a set command, synchronously. + // So, check that when queueing an entry. + // - Can clock be ended during a queue command in any other case? + // - In the simplest case of our only operation being skip, how could client's become desynced? + // - Then with toggle pause + // - Is it safe to assert paused state on the client. + // - Do queued + // - Do seek + + //if (current->start != LIANA_TIMESTAMP_INVALID && current->start > ts - LIANA_BASE_PING) { + + // This should only happen if `start` has never been set. + if (target->start == LIANA_TIMESTAMP_INVALID && target->paused_at == LIANA_TIMESTAMP_INVALID) { + al_log_info("dd", "Hello"); + target->start = at; + } + + if (target->held) { + target->paused_at = LIANA_TIMESTAMP_INVALID; + target->held = false; + } + + al_assert(!current->held); + + bool done = assume_done(current, now); + if (done || current->paused_at == LIANA_TIMESTAMP_INVALID) { + if (!done) { + current->paused_at = at; + current->offset += current->paused_at - current->start; + current->start = LIANA_TIMESTAMP_INVALID; + current->held = true; + } + if (assume_done(target, now) || target->paused_at != LIANA_TIMESTAMP_INVALID) { + pause = !done ? LIANA_PAUSE_PAUSE : LIANA_PAUSE_NONE; + } else { //if (target->paused_at == LIANA_TIMESTAMP_INVALID) { + target->start = at; + pause = !done ? LIANA_PAUSE_BOTH : LIANA_PAUSE_RESUME; + } + al_log_info("dd", "%d: not paused, done: %d, pause: %d", sequence, done, pause); + } else { + if (assume_done(target, now) || target->paused_at != LIANA_TIMESTAMP_INVALID) { + pause = LIANA_PAUSE_NONE; + } else { //if (target->paused_at == LIANA_TIMESTAMP_INVALID) { + target->start = at; + pause = LIANA_PAUSE_RESUME; + } + al_log_info("dd", "%d: paused, pause: %d", sequence, pause); + } + + list->current = index; + list->previous = sequence; + list->idle = false; + + struct lia_timing time = { + .at = at, + .seek_pos = target->offset, + .pause = pause + }; + + struct lia_list_sink *sink; + al_array_foreach(list->sinks, i, sink) { + sink->set = index; + sink->callback(sink->userdata, CAMU_SINK_SET, target, index, &time); + } + + list->callback(list->userdata, LIANA_META_PLAYING, target); +} + +void lia_list_skip(struct lia_list *list, s32 sequence, s32 n) +{ + if (sequence == LIANA_SEQUENCE_ANY) sequence = list->current; + lia_list_skipto(list, sequence, sequence + n); +} + +void lia_list_toggle_pause(struct lia_list *list, s32 sequence, f64 pts) +{ + if (sequence == LIANA_SEQUENCE_ANY) sequence = list->current; + struct lia_list_entry *current = get_entry_from_sequence(list, sequence); + if (!current) return; + + u64 now = aki_get_timestamp(); + u8 pause = current->paused_at == LIANA_TIMESTAMP_INVALID ? LIANA_PAUSE_PAUSE : LIANA_PAUSE_RESUME; + u64 at; + switch (pause) { + case LIANA_PAUSE_PAUSE: + al_assert(pts != -1.0); + current->paused_at = now + LIANA_PAUSE_DELAY; + current->offset += current->paused_at - current->start; + current->start = LIANA_TIMESTAMP_INVALID; + at = current->paused_at; + break; + case LIANA_PAUSE_RESUME: + current->paused_at = LIANA_TIMESTAMP_INVALID; + current->start = now + LIANA_PAUSE_DELAY; + at = current->start; + break; + } + + struct lia_timing time = { + .at = at, + .seek_pos = LIANA_TIMESTAMP_INVALID, + .pause = pause + }; + + struct lia_list_sink *sink; + al_array_foreach(list->sinks, i, sink) { + sink->callback(sink->userdata, CAMU_SINK_PAUSE, current, sequence, &time); + } +} + +void lia_list_seek(struct lia_list *list, s32 sequence, f64 percent) +{ + if (sequence == LIANA_SEQUENCE_ANY) sequence = list->current; + list->idle = false; + struct lia_list_entry *current = get_entry_from_sequence(list, sequence); + u64 pos = (u64)(current->duration * percent); + u64 now = aki_get_timestamp(); + current->offset = pos; + current->start = now + LIANA_BASE_DELAY; + struct lia_timing time = { + .at = current->start, + .seek_pos = current->offset, + .pause = LIANA_PAUSE_NONE + }; + struct lia_list_sink *sink; + al_array_foreach(list->sinks, i, sink) { + sink->callback(sink->userdata, CAMU_SINK_SEEK, current, sequence, &time); + } +} + +void lia_list_end(struct lia_list *list, s32 sequence) +{ + al_assert(sequence != LIANA_SEQUENCE_ANY); + if (sequence != list->current) return; + s32 size = (s32)list->entries.size; + s32 next = sequence + 1; + list->previous = sequence; + struct lia_list_entry *current = al_array_at(list->entries, list->current); + //current->offset = current->duration; + if (list->queued >= 0) { + list->current = list->queued; + list->queued = -1; + struct lia_list_sink *sink; + al_array_foreach(list->sinks, i, sink) { + sink->queued = -1; + } + if (list->callback) { + list->callback(list->userdata, LIANA_META_PLAYING, al_array_at(list->entries, list->current)); + } + } else if (next < size) { + lia_list_skipto(list, sequence, next); + } else { + list->idle = true; + struct lia_list_sink *sink; + al_array_foreach(list->sinks, i, sink) { + sink->set = -1; + } + } +} + +void lia_list_reverse(struct lia_list *list) +{ + u32 size = list->entries.size; + for (u32 i = 0; i < size; i++) { + u32 tail = size - (i + 1); + if (tail <= i) break; + AL_SWAP(al_array_at(list->entries, i), al_array_at(list->entries, tail), struct lia_list_entry *); + } + unset_all(list); +} + +void lia_list_sort(struct lia_list *list) +{ + al_array_sort(list->entries, struct lia_list_entry *, camu_db_compare); + unset_all(list); +} + +void lia_list_shuffle(struct lia_list *list) +{ + u32 size = list->entries.size; + if (size == 0) return; + for (u32 i = 0; i < size - 1; i++) { + u32 j = i + al_rand() / (AL_RAND_MAX / (size - i) + 1); + AL_SWAP(al_array_at(list->entries, i), al_array_at(list->entries, j), struct lia_list_entry *); + } + unset_all(list); +} + +void lia_list_clear(struct lia_list *list) +{ + struct lia_list_entry *entry; + al_array_foreach(list->entries, i, entry) { + al_str_free(&entry->name); + al_free(entry); + } + list->entries.size = 0; + unset_all(list); + list->current = -1; + list->previous = -1; + list->idle = true; +} + +void lia_list_free(struct lia_list *list) +{ + struct lia_list_entry *entry; + al_array_foreach(list->entries, i, entry) { + al_str_free(&entry->name); + al_free(entry); + } + al_array_free(list->entries); + struct lia_list_sink *sink; + al_array_foreach(list->sinks, i, sink) { + al_free(sink); + } + al_array_free(list->sinks); +} diff --git a/src/liana/list.h b/src/liana/list.h new file mode 100644 index 0000000..8adea17 --- /dev/null +++ b/src/liana/list.h @@ -0,0 +1,107 @@ +#pragma once + +#include <al/types.h> +#include <al/str.h> +#include <al/array.h> + +#define LIANA_SEQUENCE_ANY -1 +#define LIANA_TIMESTAMP_INVALID UINT64_MAX + +#define LIANA_BASE_PING 200000Lu // 100ms +#define LIANA_BASE_DELAY 450000Lu // 250ms +#define LIANA_PAUSE_DELAY LIANA_BASE_PING +#define LIANA_DELAY_IGNORE 0Lu + +#define LIANA_BUFFER_AHEAD 2 + +enum { + LIANA_ENTRY_DURATION = 0, + LIANA_ENTRY_ID, + LIANA_ENTRY_SKIP +}; + +enum { + LIANA_META_PLAYING = 0, + LIANA_META_QUEUED +}; + +// Pause: +// - Set `paused_at` to now() + PAUSE_DELAY. +// - Increment `offset` by how long the entry will have been +// playing when paused at the requested timestamp (`paused_at` - `start`). +// Resume: +// - Unset `paused_at` +// - Set `start` to now() + PAUSE_DELAY. +// Next/Prev: +// Current Playing, Target Playing: +// - Set `current->held_at` to now() + PAUSE_DELAY. +// - Increment `current->offset` by the same logic as pause. +// - +// Current Playing, Target Paused: +// Current Paused, Target Playing: +// Current Paused, Target Paused: +// Seek: + +// NOTE: keep global per list max time until all sinks _should_ by buffered +// possibly use that instead of LIANA_PAUSE_DELAY. + +enum { + LIANA_PAUSE_NONE = 0, + LIANA_PAUSE_RESUME, + LIANA_PAUSE_PAUSE, + LIANA_PAUSE_BOTH +}; + +struct lia_timing { + u64 at; + u64 seek_pos; + u8 pause; +}; + +struct lia_list_entry { + void *opaque; + u64 start; + u64 paused_at; + bool held; + u64 offset; + u64 duration; + str name; +}; + +struct lia_list_sink { + s32 set; + s32 queued; + void (*callback)(void *, u8, struct lia_list_entry *, s32, struct lia_timing *); + void *userdata; +}; + +struct lia_list { + s32 current; + s32 previous; + s32 queued; + bool idle; + array(struct lia_list_entry *) entries; + array(struct lia_list_sink *) sinks; + void (*callback)(void *, u8, struct lia_list_entry *); + void *userdata; +}; + +void lia_list_init(struct lia_list *list); + +void lia_list_add_sink(struct lia_list *list, void (*callback)(void *, u8, struct lia_list_entry *, s32, struct lia_timing *), void *userdata); +void lia_list_remove_sink(struct lia_list *list, void *userdata); + +void lia_list_add(struct lia_list *list, void *opaque, u64 duration, str *name); +void lia_list_unset(struct lia_list *list); +void lia_list_skipto(struct lia_list *list, s32 sequence, s32 i); +void lia_list_skip(struct lia_list *list, s32 sequence, s32 n); +void lia_list_toggle_pause(struct lia_list *list, s32 sequence, f64 pts); +void lia_list_seek(struct lia_list *list, s32 sequence, f64 percent); +void lia_list_end(struct lia_list *list, s32 sequence); + +void lia_list_reverse(struct lia_list *list); +void lia_list_sort(struct lia_list *list); +void lia_list_shuffle(struct lia_list *list); +void lia_list_clear(struct lia_list *list); + +void lia_list_free(struct lia_list *list); diff --git a/src/liana/list_cmp.h b/src/liana/list_cmp.h new file mode 100644 index 0000000..1f77b46 --- /dev/null +++ b/src/liana/list_cmp.h @@ -0,0 +1,53 @@ +#pragma once + +#include <al/str.h> + +#include "list.h" + +static void camu_db_num_from_path(str *path, s64 *id, s64 *index) +{ + s32 index0 = al_str_rfind(path, '/'); + if (index0 < 0) return; + + str s = *al_str_substr(path, index0 + 1, path->len); + index0 = al_str_find(&s, '_'); + if (index0 < 0) return; + s = *al_str_substr(&s, index0 + 1, s.len); + index0 = al_str_find(&s, '_'); + if (index0 < 0) return; + s = *al_str_substr(&s, index0 + 1, s.len); + index0 = al_str_find(&s, '_'); + if (index0 < 0) return; + s = *al_str_substr(&s, 0, index0); + + s64 num = al_str_to_long(&s, 10); + if (num == INT64_MIN || num == INT64_MAX) return; + *id = num; + + index0 = al_str_rfind(path, '.'); + s32 index1 = al_str_rfind(path, 'a'); + if (index0 < 0 || index1 < 0) return; + s = *al_str_substr(path, index1 + 1, index0); + + num = al_str_to_long(&s, 10); + if (num == INT64_MIN || num == INT64_MAX) return; + *index = num; +} + +static s32 camu_db_compare(const void *a, const void *b) +{ + struct lia_list_entry *aa = *((struct lia_list_entry **)a); + struct lia_list_entry *bb = *((struct lia_list_entry **)b); + s64 anum = -1, aindex = -1; + s64 bnum = -1, bindex = -1; + camu_db_num_from_path(&aa->name, &anum, &aindex); + camu_db_num_from_path(&bb->name, &bnum, &bindex); + if (anum == bnum) { + if (aindex > bindex) return 1; + else if (aindex < bindex) return -1; + } else { + if (anum > bnum) return -1; + else if (anum < bnum) return 1; + } + return 0; +} diff --git a/src/liana/meson.build b/src/liana/meson.build new file mode 100644 index 0000000..0859e99 --- /dev/null +++ b/src/liana/meson.build @@ -0,0 +1,36 @@ +liana_server_src = [ + 'list.c', + 'server.c', + 'handlers/codec_server.c', + 'handlers.c' +] +liana_client_src = [ + 'client.c', + 'vcr.c', + 'handlers/codec_client.c', + 'handlers.c' +] +liana_deps = [codecs] +liana_args = [] + +libcdio_paranoia = dependency('libcdio_paranoia', required: false, allow_fallback: true) +libcdio_cdda = dependency('libcdio_cdda', required: false, allow_fallback: true) +if libcdio_paranoia.found() and libcdio_cdda.found() + liana_server_src += ['handlers/cdio_server.c'] + liana_client_src += ['handlers/cdio_client.c'] + liana_deps += [libcdio_paranoia, libcdio_cdda] + liana_args += ['-DLIANA_HAVE_CDIO'] +endif + +#libdvdcss = dependency('libdvdcss', required: false, allow_fallback: true) +#libdvdread = dependency('libdvdread', required: false, allow_fallback: true) +#libdvdnav = dependency('libdvdnav', required: false, allow_fallback: true) +#if libdvdcss.found() and libdvdread.found() and libdvdnav.found() +# liana_server_src += ['handlers/dvd_server.c'] +# liana_server_deps += [libdvdcss] +#endif + +liana_server = declare_dependency(sources: liana_server_src, dependencies: liana_deps, + compile_args: [liana_args, '-DLIANA_SERVER']) +liana_client = declare_dependency(sources: liana_client_src, dependencies: liana_deps, + compile_args: [liana_args, '-DLIANA_CLIENT']) diff --git a/src/liana/server.c b/src/liana/server.c new file mode 100644 index 0000000..1fc96fb --- /dev/null +++ b/src/liana/server.c @@ -0,0 +1,345 @@ +#include <al/random.h> + +#include "server.h" +#include "handler.h" +#include "handlers.h" +#include "list.h" + +bool lia_server_init(struct lia_server *server, struct aki_event_loop *loop) +{ + server->loop = loop; + al_array_init(server->nodes); + al_array_init(server->zombies); + return true; +} + +static void remove_zombie(struct lia_server *server, struct aki_packet_stream *stream) +{ + struct aki_packet_stream *zombie; + al_array_foreach(server->zombies, i, zombie) { + if (zombie == stream) { + al_array_remove_at(server->zombies, i); + break; + } + } +} + +static void data_packet_sent_callback(void *userdata, struct aki_packet *packet) +{ + struct lia_node_connection *conn = (struct lia_node_connection *)userdata; + aki_packet_pool_return(&conn->pool, packet); +} + +static u8 packet_pool_callback(void *userdata, struct aki_packet *packet) +{ + struct lia_node_connection *conn = (struct lia_node_connection *)userdata; + if (!aki_packet_stream_send_packet(conn->stream, packet)) { + return AKI_PACKET_POOL_RETURN; + } + return AKI_PACKET_POOL_KEEP; +} + +static aki_thread_result AKI_THREADCALL handler_thread(void *userdata) +{ + struct lia_node_connection *conn = (struct lia_node_connection *)userdata; + do { + struct aki_packet *packet = aki_packet_pool_get(&conn->pool); + if (!packet) break; + conn->handler->step(conn->handler); + conn->handler->write_packet(conn->handler, packet); + aki_packet_pool_submit(&conn->pool, packet); + if (conn->handler->status != CAMU_OK) break; + } while (1); + aki_packet_pool_flush(&conn->pool); + return 0; +} + +static void discard_packet_callback(void *userdata, struct aki_packet_stream *stream, struct aki_packet *packet) +{ + (void)userdata; + (void)stream; + aki_packet_free(packet); + // We should never be here. Although, we also shouldn't assert because + // any erroneous connection can bring us here. + al_assert(false); +} + +static void close_connection_internal(struct lia_node_connection *conn) +{ + al_assert(conn->handler); + conn->handler->free(&conn->handler); + cch_entry_return_handle(conn->node->entry, &conn->handle); + aki_packet_stream_free(conn->stream); + al_free(conn->stream); + conn->stream = NULL; +} + +static void data_connection_closed_callback(void *userdata, struct aki_packet_stream *stream) +{ + struct lia_node_connection *conn = (struct lia_node_connection *)userdata; + (void)stream; + aki_packet_pool_disable(&conn->pool); + cch_handle_disable(&conn->handle); + // This is joining handler_thread(), we will never be here if init_thread() blocks or fails. + aki_thread_join(&conn->thread); + close_connection_internal(conn); + aki_packet_pool_free(&conn->pool); +} + +static void subscribe_packet_callback(void *userdata, struct aki_packet_stream *stream, struct aki_packet *packet) +{ + struct lia_node_connection *conn = (struct lia_node_connection *)userdata; + s32 mask = aki_packet_read_s32(packet); + conn->handler->subscribe(conn->handler, mask); + stream->packet_callback = discard_packet_callback; + stream->packet_sent_callback = data_packet_sent_callback; + stream->connection_closed_callback = data_connection_closed_callback; + aki_thread_create(&conn->thread, handler_thread, conn); + aki_packet_free(packet); +} + +static void subscribe_packet_sent_callback(void *userdata, struct aki_packet *packet) +{ + (void)userdata; + aki_packet_free(packet); +} + +static void subscribe_connection_closed_callback(void *userdata, struct aki_packet_stream *stream) +{ + struct lia_node_connection *conn = (struct lia_node_connection *)userdata; + (void)stream; + close_connection_internal(conn); + aki_packet_pool_free(&conn->pool); +} + +static void handle_connection(struct lia_node_connection *conn, struct aki_packet *packet) +{ + struct aki_packet_stream *stream = conn->stream; + + s32 mask = aki_packet_read_s32(packet); + u64 seek_pos = aki_packet_read_u64(packet); + + // Besides being wasteful, seeking to 0 on a new stream can skip data. + if (seek_pos > 0) conn->handler->seek(conn->handler, seek_pos); + + if (mask == 0) { + struct aki_packet *rpacket = aki_packet_create(); + aki_packet_write_u16(rpacket, conn->id); + aki_packet_write_str(rpacket, cch_entry_get_liana(conn->node->entry)); + conn->handler->write_info(conn->handler, rpacket); + stream->packet_callback = subscribe_packet_callback; + stream->packet_sent_callback = subscribe_packet_sent_callback; + stream->connection_closed_callback = subscribe_connection_closed_callback; + aki_packet_stream_send_packet(stream, rpacket); + } else { + conn->handler->subscribe(conn->handler, mask); + stream->packet_callback = discard_packet_callback; + stream->packet_sent_callback = data_packet_sent_callback; + stream->connection_closed_callback = data_connection_closed_callback; + aki_thread_create(&conn->thread, handler_thread, conn); + } +} + +static void connection_closed_callback(void *userdata, struct aki_packet_stream *stream) +{ + struct lia_server *server = (struct lia_server *)userdata; + // This connection might no longer be a zombie, but that's fine. + remove_zombie(server, stream); + aki_packet_stream_free(stream); + al_free(stream); +} + +static void signal_callback(void *userdata) +{ + struct lia_node_connection *conn = (struct lia_node_connection *)userdata; + struct lia_node *node = conn->node; + struct lia_server *server = node->server; + aki_signal_stop(&conn->signal); + aki_thread_join(&conn->thread); + struct aki_packet *packet = conn->packet; + if (!packet) { + // Connection was closed before init was done. + close_connection_internal(conn); + return; + } + if (!conn->errored) { + conn->id = al_inc_u16(); + aki_packet_pool_init(&conn->pool, 48, server->loop, packet_pool_callback, conn); + al_array_push(node->connections, conn); + handle_connection(conn, packet); + } else { + conn->handler->free(&conn->handler); + cch_entry_return_handle(node->entry, &conn->handle); + struct aki_packet_stream *stream = conn->stream; + al_free(conn); + conn = NULL; + // This connection is now nothing but a packet stream. + stream->userdata = server; + stream->connection_closed_callback = connection_closed_callback; + aki_packet_stream_disconnect(stream); + } + aki_packet_free(packet); +} + +static aki_thread_result AKI_THREADCALL init_thread(void *userdata) +{ + struct lia_node_connection *conn = (struct lia_node_connection *)userdata; + if (!conn->handler->init(conn->handler, &conn->handle)) { + conn->errored = true; + } + aki_signal_send(&conn->signal); + return 0; +} + +static struct lia_node_connection *get_connection_from_id(struct lia_node *node, u16 id) +{ + struct lia_node_connection *conn = NULL; + al_array_foreach(node->connections, i, conn) { + if (conn->id == id) return conn; + } + return NULL; +} + +static struct lia_node *get_node_from_id(struct lia_server *server, u16 id) +{ + struct lia_node *node; + al_array_foreach(server->nodes, i, node) { + if (node->id == id) return node; + } + return NULL; +} + +static void pre_init_connection_closed_callback(void *userdata, struct aki_packet_stream *stream) +{ + struct lia_node_connection *conn = (struct lia_node_connection *)userdata; + (void)stream; + aki_packet_free(conn->packet); + // Checked in signal_callback and will signal to cleanup the connection. + conn->packet = NULL; +} + +static void packet_callback(void *userdata, struct aki_packet_stream *stream, struct aki_packet *packet) +{ + struct lia_server *server = (struct lia_server *)userdata; + + // We got a packet, this connection is no longer a zombie. + remove_zombie(server, stream); + + u16 node_id = aki_packet_read_u16(packet); + u16 connection_id = aki_packet_read_u16(packet); + + struct lia_node *node = get_node_from_id(server, node_id); + struct lia_node_connection *conn = NULL; + if (connection_id == 0) { + conn = al_alloc_object(struct lia_node_connection); + stream->userdata = conn; + conn->node = node; + conn->stream = stream; + conn->packet = packet; + aki_signal_init(&conn->signal, signal_callback, conn); + aki_signal_start(&conn->signal, server->loop); + cch_entry_get_handle(node->entry, &conn->handle); + conn->handler = lia_handler_by_name(cch_entry_get_liana(node->entry))->create_server_handler(); + conn->errored = false; + stream->packet_callback = discard_packet_callback; + stream->connection_closed_callback = pre_init_connection_closed_callback; + aki_thread_create(&conn->thread, init_thread, conn); + } else { + // This is completely unused and connections never get removed from node->connection. + if ((conn = get_connection_from_id(node, connection_id))) { + handle_connection(conn, packet); + } else { + aki_packet_stream_disconnect(stream); + } + aki_packet_free(packet); + } +} + +static void packet_sent_callback(void *userdata, struct aki_packet *packet) +{ + (void)userdata; + aki_packet_free(packet); +} + +static void connection_callback(void *userdata, struct aki_packet_stream *stream) +{ + struct lia_server *server = (struct lia_server *)userdata; + stream->packet_callback = packet_callback; + stream->packet_sent_callback = packet_sent_callback; + al_array_push(server->zombies, stream); +} + +void lia_server_add_socket(struct lia_server *server, struct aki_socket *sock) +{ + struct aki_packet_stream *stream = al_alloc_object(struct aki_packet_stream); + stream->connection_callback = connection_callback; + stream->connection_closed_callback = connection_closed_callback; + stream->userdata = server; + aki_packet_stream_from_socket(stream, server->loop, sock); +} + +struct lia_node *lia_server_create_node(struct lia_server *server, struct cch_entry *entry) +{ + struct lia_node *node = al_alloc_object(struct lia_node); + node->id = al_inc_u16(); + node->entry = entry; + al_array_init(node->connections); + node->server = server; + al_array_push(server->nodes, node); + return node; +} + +u64 lia_node_get_duration(struct lia_node *node) +{ + (void)node; +#if 0 + return 0; +#else + struct cch_handle handle; + cch_entry_get_handle(node->entry, &handle); + struct lia_server_handler *handler = lia_handler_by_name(cch_entry_get_liana(node->entry))->create_server_handler(); + if (!handler->init(handler, &handle)) { + return LIANA_TIMESTAMP_INVALID; + } + u64 duration = handler->get_duration(handler); + handler->free(&handler); + cch_entry_return_handle(node->entry, &handle); + return duration; +#endif +} + +void lia_server_close(struct lia_server *server) +{ + struct lia_node *node; + al_array_foreach(server->nodes, i, node) { + struct lia_node_connection *connection; + al_array_foreach(node->connections, j, connection) { + if (connection->stream) { + aki_packet_stream_disconnect(connection->stream); + } + } + } + struct aki_packet_stream *zombie; + al_array_foreach_rev(server->zombies, i, zombie) { + aki_packet_stream_disconnect(zombie); + } +} + +void lia_server_free(struct lia_server *server) +{ + struct lia_node *node; + al_array_foreach(server->nodes, i, node) { + struct lia_node_connection *connection; + al_array_foreach(node->connections, j, connection) { + al_free(connection); + } + al_array_free(node->connections); + al_free(node); + } + al_array_free(server->nodes); + struct aki_packet_stream *zombie; + al_array_foreach(server->zombies, i, zombie) { + al_free(zombie); + } + al_array_free(server->zombies); +} diff --git a/src/liana/server.h b/src/liana/server.h new file mode 100644 index 0000000..3c4c294 --- /dev/null +++ b/src/liana/server.h @@ -0,0 +1,42 @@ +#pragma once + +#include <aki/event_loop.h> +#include <aki/packet_stream.h> +#include <aki/packet_pool.h> +#include <aki/socket.h> +#include <aki/signal.h> + +#include "../cache/entry.h" + +struct lia_node_connection { + u16 id; + struct aki_packet *packet; + struct aki_packet_stream *stream; + struct lia_server_handler *handler; + bool errored; + struct cch_handle handle; + struct aki_thread thread; + struct aki_signal signal; + struct aki_packet_pool pool; + struct lia_node *node; +}; + +struct lia_node { + u16 id; + struct cch_entry *entry; + array(struct lia_node_connection *) connections; + struct lia_server *server; +}; + +struct lia_server { + struct aki_event_loop *loop; + array(struct lia_node *) nodes; + array(struct aki_packet_stream *) zombies; +}; + +bool lia_server_init(struct lia_server *server, struct aki_event_loop *loop); +void lia_server_add_socket(struct lia_server *server, struct aki_socket *sock); +struct lia_node *lia_server_create_node(struct lia_server *server, struct cch_entry *entry); +u64 lia_node_get_duration(struct lia_node *node); +void lia_server_close(struct lia_server *server); +void lia_server_free(struct lia_server *server); diff --git a/src/liana/vcr.c b/src/liana/vcr.c new file mode 100644 index 0000000..fad8b78 --- /dev/null +++ b/src/liana/vcr.c @@ -0,0 +1,232 @@ +#include <al/log.h> + +#include "vcr.h" +#include "handler.h" + +#define VCR_BUFFER_INIT 64 +#define VCR_BUFFER_BUFFERED (VCR_BUFFER_INIT - 8) +#define VCR_BUFFER_LOW (VCR_BUFFER_INIT - 16) + +static void signal_callback(void *userdata) +{ + struct lia_vcr *vcr = (struct lia_vcr *)userdata; + aki_packet_stream_cork(vcr->data, false); +} + +void lia_vcr_init(struct lia_vcr *vcr, struct aki_packet_stream *data) +{ + al_array_init(vcr->tracks); + al_atomic_store(s32)(&vcr->count, 0, AL_ATOMIC_RELAXED); + vcr->mark.low = VCR_BUFFER_LOW; + vcr->mark.buffered = VCR_BUFFER_BUFFERED; + vcr->data = data; + aki_signal_init(&vcr->signal, signal_callback, vcr); +} + +void lia_vcr_start(struct lia_vcr *vcr, struct aki_event_loop *loop) +{ + aki_signal_start(&vcr->signal, loop); +} + +static aki_thread_result AKI_THREADCALL vcr_track_thread(void *userdata) +{ + struct lia_vcr_track *track = (struct lia_vcr_track *)userdata; + struct lia_vcr *vcr = track->vcr; + u32 packets; + struct aki_packet *packet = NULL; + while (aki_packet_cache_wait(&track->cache, &packets)) { + s32 state = 0; + for (u32 i = 0; i < packets; i++) { + packet = aki_packet_cache_pop(&track->cache); + state = 0; + if (packet) { + state = al_atomic_load(s32)(&track->state, AL_ATOMIC_RELAXED); + if (state == LIANA_STREAM_CLOSED) { + // We were signaled to close, exit thread. + goto out; + } + u8 buffered = al_atomic_load(u8)(&track->buffered, AL_ATOMIC_RELAXED); + s32 count = al_atomic_sub(s32)(&vcr->count, 1, AL_ATOMIC_RELAXED); + if (count < vcr->mark.low && buffered) { + aki_signal_send(&vcr->signal); + } + } + if (!track->client->handle_packet(track->client, packet)) { + // Handler error, exit. + goto out; + } + if (packet) { + aki_packet_free(packet); + packet = NULL; + if (state == LIANA_STREAM_STOPPED) { + // Unlock here to accumulate packets while waiting. + aki_packet_cache_unlock(&track->cache); + aki_mutex_lock(&track->mutex); + aki_cond_wait(&track->cond, &track->mutex); + aki_mutex_unlock(&track->mutex); + break; + } + } else { + // NULL packet, exit thread. + goto out; + } + } + if (state != LIANA_STREAM_STOPPED) { + // If state = STOPPED, we already unlocked. + aki_packet_cache_unlock(&track->cache); + } + } + // packet_cache_wait() returned false. + return 0; +out: + // packet_cache_wait() returned true and we are jumping out of the loop. + if (packet) aki_packet_free(packet); + aki_packet_cache_unlock(&track->cache); + return 0; +} + +void lia_vcr_add_track(struct lia_vcr *vcr, struct lia_vcr_track *track) +{ + track->vcr = vcr; + aki_cond_init(&track->cond); + aki_mutex_init(&track->mutex); + al_atomic_store(u8)(&track->buffered, 0, AL_ATOMIC_RELAXED); + aki_packet_cache_init(&track->cache, VCR_BUFFER_INIT - 1); + al_array_push(vcr->tracks, track); + al_atomic_store(s32)(&track->state, LIANA_STREAM_RUNNING, AL_ATOMIC_RELAXED); +} + +static struct lia_vcr_track *get_track_from_index(struct lia_vcr *vcr, s32 index) +{ + struct lia_vcr_track *track; + al_array_foreach(vcr->tracks, i, track) { + if (track->index == index) return track; + } + return NULL; +} + +static void cork_if_buffered(struct lia_vcr *vcr) +{ + u8 buffered = 1; + struct lia_vcr_track *track; + al_array_foreach(vcr->tracks, i, track) { + buffered &= al_atomic_load(u8)(&track->buffered, AL_ATOMIC_RELAXED); + } + if (buffered) aki_packet_stream_cork(vcr->data, true); +} + +bool lia_vcr_push_packet(struct lia_vcr *vcr, struct aki_packet *packet) +{ + struct lia_vcr_track *track; + s32 count; + u8 op = aki_packet_read_u8(packet); + switch (op) { + case LIANA_PACKET_DATA: + track = get_track_from_index(vcr, aki_packet_read_s32(packet)); + al_assert(track); + if (!track->running) { + aki_thread_create(&track->thread, vcr_track_thread, track); + track->running = true; + } + if (!aki_packet_cache_send_packet(&track->cache, packet)) { + aki_packet_free(packet); + } else if ((count = al_atomic_add(s32)(&vcr->count, 1, AL_ATOMIC_RELAXED)) > vcr->mark.buffered) { + vcr->mark.buffered = count; + vcr->mark.low = vcr->mark.buffered - 16; + cork_if_buffered(vcr); + } + break; + case LIANA_PACKET_EOF: + al_array_foreach(vcr->tracks, i, track) { + aki_packet_cache_send_packet(&track->cache, NULL); + } + aki_packet_free(packet); + break; + case LIANA_PACKET_ERROR: + al_log_warn("liana", "Unhandled error packet."); + aki_packet_free(packet); + break; + default: + al_assert_and_return(false); + } + return true; +} + +void lia_vcr_cork(struct lia_vcr_track *track) +{ + al_atomic_store(s32)(&track->state, LIANA_STREAM_STOPPED, AL_ATOMIC_RELAXED); + al_atomic_store(u8)(&track->buffered, 1, AL_ATOMIC_RELAXED); +} + +void lia_vcr_uncork(struct lia_vcr_track *track) +{ + if (al_atomic_load(s32)(&track->state, AL_ATOMIC_RELAXED) != LIANA_STREAM_STOPPED) { + // This can be reached during normal operation. Whether or not that should + // be allowed is up for consideration. + return; + } + al_atomic_store(s32)(&track->state, LIANA_STREAM_RUNNING, AL_ATOMIC_RELAXED); + aki_mutex_lock(&track->mutex); + if (aki_cond_is_waiting(&track->cond)) { + aki_cond_signal(&track->cond); + } + aki_mutex_unlock(&track->mutex); +} + +static void vcr_track_close_internal(struct lia_vcr_track *track) +{ + al_atomic_store(s32)(&track->state, LIANA_STREAM_CLOSED, AL_ATOMIC_RELAXED); + aki_packet_cache_disable(&track->cache); + aki_mutex_lock(&track->mutex); + if (aki_cond_is_waiting(&track->cond)) { + aki_cond_signal(&track->cond); + } + aki_mutex_unlock(&track->mutex); + if (track->running) { + aki_thread_join(&track->thread); + track->running = false; + } + struct aki_packet *packet; + while ((packet = aki_packet_cache_pop(&track->cache))) { + aki_packet_free(packet); + } +} + +void lia_vcr_flush(struct lia_vcr *vcr) +{ + struct lia_vcr_track *track; + al_array_foreach(vcr->tracks, i, track) { + vcr_track_close_internal(track); + track->client->flush(track->client); + aki_packet_cache_enable(&track->cache); + track->running = false; + al_atomic_store(s32)(&track->state, LIANA_STREAM_RUNNING, AL_ATOMIC_RELAXED); + } + aki_signal_send(&vcr->signal); + al_atomic_store(s32)(&vcr->count, 0, AL_ATOMIC_RELAXED); +} + +void lia_vcr_close_all(struct lia_vcr *vcr) +{ + struct lia_vcr_track *track; + al_array_foreach(vcr->tracks, i, track) { + vcr_track_close_internal(track); + } + aki_signal_stop(&vcr->signal); +} + +void lia_vcr_free(struct lia_vcr *vcr) +{ + struct lia_vcr_track *track; + al_array_foreach(vcr->tracks, i, track) { + aki_packet_cache_free(&track->cache); + track->client->free(&track->client); +#if CAMU_HAVE_FFMPEG + if (track->stream.mode == CAMU_FFMPEG_COMPAT) { + avformat_free_context(track->stream.av.format_context); + } +#endif + al_free(track); + } + al_array_free(vcr->tracks); +} diff --git a/src/liana/vcr.h b/src/liana/vcr.h new file mode 100644 index 0000000..f471c4e --- /dev/null +++ b/src/liana/vcr.h @@ -0,0 +1,46 @@ +#pragma once + +#include <al/atomic.h> +#include <aki/packet_cache.h> +#include <aki/packet_stream.h> +#include <aki/signal.h> + +#include "../codec/codec.h" + +enum { + LIANA_STREAM_RUNNING = 0, + LIANA_STREAM_STOPPED, + LIANA_STREAM_CLOSED +}; + +struct lia_vcr_track { + s32 index; + struct camu_codec_stream stream; + struct lia_client_handler *client; + atomic(s32) state; + atomic(u8) buffered; + struct aki_packet_cache cache; + struct aki_cond cond; + struct aki_mutex mutex; + bool running; + struct aki_thread thread; + struct lia_vcr *vcr; +}; + +struct lia_vcr { + array(struct lia_vcr_track *) tracks; + atomic(s32) count; + struct { s32 low, buffered; } mark; + struct aki_packet_stream *data; + struct aki_signal signal; +}; + +void lia_vcr_init(struct lia_vcr *vcr, struct aki_packet_stream *data); +void lia_vcr_start(struct lia_vcr *vcr, struct aki_event_loop *loop); +void lia_vcr_add_track(struct lia_vcr *vcr, struct lia_vcr_track *track); +bool lia_vcr_push_packet(struct lia_vcr *vcr, struct aki_packet *packet); +void lia_vcr_cork(struct lia_vcr_track *track); +void lia_vcr_uncork(struct lia_vcr_track *track); +void lia_vcr_flush(struct lia_vcr *vcr); +void lia_vcr_close_all(struct lia_vcr *vcr); +void lia_vcr_free(struct lia_vcr *vcr); diff --git a/src/libclient/client.c b/src/libclient/client.c index cd0bdf5..58264e0 100644 --- a/src/libclient/client.c +++ b/src/libclient/client.c @@ -1,247 +1,58 @@ -#include <al/log.h> +#include "client.h" #include "../server/common.h" -#include "client.h" - -static void identifed_callback(void *userdata, struct aki_packet *packet) -{ - struct camu_client *client = (struct camu_client *)userdata; - client->callback(client->userdata, CAMU_CLIENT_CONNECTED, NULL); - aki_packet_free(packet); -} +static struct aki_rpc_command commands[] = { }; static void connection_callback(void *userdata, struct aki_rpc_connection *conn) { struct camu_client *client = (struct camu_client *)userdata; client->conn = conn; - struct aki_packet *packet = aki_rpc_get_packet(&client->client, CAMU_SRV_IDENTIFY); + struct aki_packet *packet = aki_rpc_get_packet(&client->client, CAMU_SERVER_IDENTIFY); aki_packet_write_u8(packet, CAMU_CLIENT); aki_packet_write_str(packet, &client->username); - aki_rpc_connection_command(client->conn, packet, identifed_callback, client); + aki_rpc_connection_command(client->conn, packet, NULL, NULL); } static void connection_closed_callback(void *userdata, struct aki_rpc_connection *conn) { struct camu_client *client = (struct camu_client *)userdata; - (void)conn; - client->callback(client->userdata, CAMU_CLIENT_DISCONNECTED, NULL); + al_assert(client->conn == conn); + client->conn = NULL; } -static u8 packet_pool_callback(void *userdata, struct aki_packet *packet) -{ - struct camu_client *client = (struct camu_client *)userdata; - aki_rpc_connection_command(client->conn, packet, packet->userdata, client); - return AKI_PACKET_POOL_NOP; -} - -static bool update_state_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; - (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_init(list->entries); - u32 entries = aki_packet_read_u32(packet); - for (u32 j = 0; j < entries; j++) { - aki_packet_read_str(packet, &s); - str unique_id; - al_str_clone(&unique_id, &s); - al_array_push(list->entries, unique_id); - } - list->current = aki_packet_read_s32(packet); - 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->module, &s); - aki_packet_read_str(packet, &s); - al_str_clone(&search->query, &s); - search->page = aki_packet_read_u32(packet); - al_array_push(client->state.searches, search); - } - - client->callback(client->userdata, CAMU_CLIENT_STATE_UPDATED, NULL); - - return false; -} - -static struct aki_rpc_command commands[] = { - { .op = CAMU_CONN_STATE, .callback = update_state_command_callback, .userdata = NULL } -}; - -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, struct aki_event_loop *loop, + u8 type, str *addr, u16 port, str *username) { client->loop = loop; - client->callback = callback; - client->userdata = userdata; - 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; - } + al_str_clone(&client->username, username); + aki_rpc_init(&client->client, client->loop, connection_callback, connection_closed_callback, client); 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)) { + if (!aki_rpc_prepare_client(&client->client, type, CAMU_MULTIPLEX_RPC)) { return false; } - while (!client->conn) { - aki_event_loop_run_once(client->loop); - } - // TODO: Timeout. + aki_rpc_connect(&client->client, addr, port); return true; } -static void create_search_callback(void *userdata, struct aki_packet *packet) -{ - struct camu_client *client = (struct camu_client *)userdata; - 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 *module, str *query) -{ - /* - struct aki_packet *packet = aki_rpc_get_packet(&client->client, TREE_CMD_CREATE_SEARCH); - aki_packet_write_str(packet, module); - aki_packet_write_str(packet, query); - packet->userdata = create_search_callback; - aki_packet_pool_submit(&client->pool, packet); - */ -} - -static struct camu_search *get_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; - - // We still get the requested id even if there was an error. - s32 id = aki_packet_read_s32(packet); - - s32 ok = aki_packet_read_s32(packet); - if (ok != 0) { - client->callback(client->userdata, CAMU_CLIENT_END_OF_RESULTS, &id); - return; - } - - struct camu_search *search = get_search_from_id(client, id); - if (!search) return; - - u32 page_num = aki_packet_read_u32(packet); - struct camu_result_list *results; - al_array_foreach(search->lists, i, results) { - if (results->num == page_num) { - al_log_warn("client", "Requested page was already loaded."); - return; - } - } - - results = al_alloc_object(struct camu_result_list); - al_array_push(search->lists, results); - results->num = page_num; - al_array_init(results->unique_ids); - results->search = search; - - 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->cache, &post); - } - - str s; - str unique_id; - size = aki_packet_read_u32(packet); - for (u32 i = 0; i < size; i++) { - aki_packet_read_str(packet, &s); - al_str_clone(&unique_id, &s); - al_array_push(results->unique_ids, unique_id); - } - - client->callback(client->userdata, CAMU_CLIENT_RESULTS, results); -} - -void camu_client_get_page(struct camu_client *client, struct camu_search *search, u32 num) -{ - /* - struct aki_packet *packet = aki_rpc_get_packet(&client->client, TREE_CMD_GET_PAGE); - aki_packet_write_s32(packet, search->id); - aki_packet_write_u32(packet, num); - packet->userdata = results_callback; - aki_packet_pool_submit(&client->pool, packet); - */ -} - -void camu_client_add(struct camu_client *client, str *unique_id, u32 index, bool set) -{ - /* - struct aki_packet *packet = aki_rpc_get_packet(&client->client, TREE_CMD_LIST_ACTION); - aki_packet_write_u8(packet, TREE_LIST_ADD); - aki_packet_write_str(packet, unique_id); - aki_packet_write_u32(packet, index); - aki_packet_write_u8(packet, set); - packet->userdata = NULL; - aki_packet_pool_submit(&client->pool, packet); - */ -} - -void camu_client_skip(struct camu_client *client, s32 n) -{ - /* - struct aki_packet *packet = aki_rpc_get_packet(&client->client, TREE_CMD_LIST_ACTION); - aki_packet_write_u8(packet, TREE_LIST_SKIP); - aki_packet_write_s32(packet, n); - packet->userdata = NULL; - aki_packet_pool_submit(&client->pool, packet); - */ -} - -void camu_client_skipto(struct camu_client *client, s32 i) +void camu_client_create_list(struct camu_client *client, str *name, + void (*callback)(void *, struct aki_packet *), void *userdata) { - /* - struct aki_packet *packet = aki_rpc_get_packet(&client->client, TREE_CMD_LIST_ACTION); - aki_packet_write_u8(packet, TREE_LIST_SKIPTO); - aki_packet_write_s32(packet, i); - packet->userdata = NULL; - aki_packet_pool_submit(&client->pool, packet); - */ + struct aki_packet *packet = aki_rpc_get_packet(&client->client, CAMU_SERVER_CLIENT_COMMAND); + aki_packet_write_u8(packet, CAMU_CLIENT_CREATE_LIST); + aki_packet_write_str(packet, name); + aki_rpc_connection_command(client->conn, packet, callback, userdata); } -void camu_client_close(struct camu_client *client) +void camu_client_enable_sink(struct camu_client *client, str *list, str *sink, + void (*callback)(void *, struct aki_packet *), void *userdata) { - aki_packet_pool_free(&client->pool); - al_str_free(&client->username); - al_array_free(client->state.lists); + struct aki_packet *packet = aki_rpc_get_packet(&client->client, CAMU_SERVER_CLIENT_COMMAND); + aki_packet_write_u8(packet, CAMU_CLIENT_ENABLE_SINK); + aki_packet_write_str(packet, list); + aki_packet_write_str(packet, sink); + aki_rpc_connection_command(client->conn, packet, callback, userdata); } diff --git a/src/libclient/client.h b/src/libclient/client.h index 98276f2..a56be26 100644 --- a/src/libclient/client.h +++ b/src/libclient/client.h @@ -1,62 +1,18 @@ #pragma once #include <aki/rpc2.h> -#include <aki/thread.h> -#include <aki/packet_pool.h> -#include <sho/post.h> -#include <sho/post_cache.h> - -enum { - CAMU_CLIENT_CONNECTED = 0, - CAMU_CLIENT_STATE_UPDATED, - CAMU_CLIENT_SEARCH_CREATED, - CAMU_CLIENT_RESULTS, - CAMU_CLIENT_END_OF_RESULTS, - CAMU_CLIENT_DISCONNECTED -}; - -struct camu_list { - str name; - s32 current; - array(str) entries; -}; - -struct camu_result_list { - u32 num; - array(str) unique_ids; - struct camu_search *search; -}; - -struct camu_search { - s32 id; - str module; - str query; - u32 page; - array(struct camu_result_list *) lists; -}; struct camu_client { 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 client; struct aki_rpc_connection *conn; - struct aki_packet_pool pool; - void (*callback)(void *, u8, void *); - void *userdata; }; -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 *module, str *query); -void camu_client_create_browse(struct camu_client *client); -void camu_client_get_page(struct camu_client *client, struct camu_search *search, u32 num); -void camu_client_add(struct camu_client *client, str *unique_id, u32 index, bool set); -void camu_client_skip(struct camu_client *client, s32 n); -void camu_client_skipto(struct camu_client *client, s32 i); -void camu_client_close(struct camu_client *client); +bool camu_client_login(struct camu_client *client, struct aki_event_loop *loop, + u8 type, str *addr, u16 port, str *username); + +void camu_client_create_list(struct camu_client *client, str *name, + void (*callback)(void *, struct aki_packet *), void *userdata); +void camu_client_enable_sink(struct camu_client *client, str *list, str *sink, + void (*callback)(void *, struct aki_packet *), void *userdata); diff --git a/src/libclient/meson.build b/src/libclient/meson.build index dff4014..197f0ff 100644 --- a/src/libclient/meson.build +++ b/src/libclient/meson.build @@ -1,3 +1,3 @@ -libclient_src = ['client.c', 'resource_client.c'] -libclient_deps = [portal] +libclient_src = ['client.c'] +libclient_deps = [common_deps] libclient = declare_dependency(sources: libclient_src, dependencies: libclient_deps) diff --git a/src/libclient/resource_client.c b/src/libclient/resource_client.c deleted file mode 100644 index a016fa6..0000000 --- a/src/libclient/resource_client.c +++ /dev/null @@ -1,87 +0,0 @@ -#include <al/random.h> - -#include "resource_client.h" - -static void connection_callback(void *userdata, struct aki_packet_stream *stream) -{ - struct camu_resource_client *client = (struct camu_resource_client *)userdata; - (void)stream; - client->connected = true; -} - -static void connection_closed_callback(void *userdata, struct aki_packet_stream *stream) -{ - struct camu_resource_client *client = (struct camu_resource_client *)userdata; - (void)stream; - client->connected = true; -} - -static void packet_callback(void *userdata, struct aki_packet_stream *stream, - struct aki_packet *packet) -{ - struct camu_resource_client *client = (struct camu_resource_client *)userdata; - (void)stream; - u16 id = aki_packet_read_u16(packet); - struct aki_buffer buffer; - aki_buffer_init(&buffer); - aki_packet_read_buffer(packet, &buffer); - client->callback(client->userdata, id, &buffer); - aki_packet_free(packet); -} - -static void packet_sent_callback(void *userdata, struct aki_packet *packet) -{ - (void)userdata; - aki_packet_free(packet); -} - -static void signal_callback(void *userdata) -{ - struct camu_resource_client *client = (struct camu_resource_client *)userdata; - u32 size; - struct camu_resource_request request; - do { - camu_queue_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_str(packet, &request.unique_id); - aki_packet_write_u32(packet, request.index); - aki_packet_stream_send_packet(&client->client, packet); - al_str_free(&request.unique_id); - } while (1); -} - -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_signal_init(&client->signal, signal_callback, client); - aki_packet_stream_init(&client->client, AKI_SOCKET_TCP, connection_callback, connection_closed_callback, - packet_callback, packet_sent_callback, client); -} - -bool 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); - } - // TODO: Timeout. - return true; -} - -void camu_resource_request(struct camu_resource_client *client, str *unique_id, u32 index, u16 id) -{ - struct camu_resource_request request; - request.id = id; - al_str_clone(&request.unique_id, unique_id); - request.index = index; - camu_queue_push(client->queue, request); - aki_signal_send(&client->signal); -} diff --git a/src/libclient/resource_client.h b/src/libclient/resource_client.h deleted file mode 100644 index 755e661..0000000 --- a/src/libclient/resource_client.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include <aki/packet_stream.h> -#include <aki/thread.h> -#include <aki/signal.h> - -#include "../util/queue.h" - -struct camu_resource_request { - u16 id; - str unique_id; - u32 index; -}; - -struct camu_resource_client { - struct aki_event_loop *loop; - struct aki_packet_stream client; - bool connected; - struct aki_thread thread; - struct aki_signal signal; - queue(struct camu_resource_request) queue; - void (*callback)(void *, u16, struct aki_buffer *); - void *userdata; -}; - -void camu_resource_client_init(struct camu_resource_client *client, - struct aki_event_loop *loop, void (*callback)(void *, u16, struct aki_buffer *), void *userdata); -bool 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/libsink/common.h b/src/libsink/common.h index a5ad25c..deae228 100644 --- a/src/libsink/common.h +++ b/src/libsink/common.h @@ -1,6 +1,12 @@ #pragma once +#define CAMU_SINK_LOCAL 0 + enum { CAMU_SINK_SET = 0, - CAMU_SINK_QUEUE + CAMU_SINK_BUFFER, + CAMU_SINK_BUFFER_AND_QUEUE, + CAMU_SINK_CLEAR, + CAMU_SINK_PAUSE, + CAMU_SINK_SEEK }; diff --git a/src/libsink/meson.build b/src/libsink/meson.build index 83cfdd7..adc06d2 100644 --- a/src/libsink/meson.build +++ b/src/libsink/meson.build @@ -1,3 +1,3 @@ libsink_src = ['sink.c'] -libsink_deps = [shrub_client] +libsink_deps = [liana_client] libsink = declare_dependency(sources: libsink_src, dependencies: libsink_deps) diff --git a/src/libsink/sink.c b/src/libsink/sink.c index c6727ab..859b717 100644 --- a/src/libsink/sink.c +++ b/src/libsink/sink.c @@ -2,13 +2,11 @@ #include "../server/common.h" -#include "../shrub/common.h" -#include "../shrub/handler.h" +#include "../liana/list.h" +#include "../liana/handler.h" #include "../buffer/common.h" -#include "../list/list.h" - #include "sink.h" #include "common.h" @@ -23,7 +21,8 @@ enum { BUFFER_QUEUED, BUFFER_CONFIGURED, BUFFER_SET_OR_BUFFERED, - BUFFER_ADDED + BUFFER_ADDED, + BUFFER_REMOVED }; enum { @@ -32,21 +31,13 @@ enum { TOGGLE_PAUSE, SEEK, SKIP, - FINISHED, + SHUFFLE, + END, RESEEK, - SET_BUFFERED, // Currently set entry is buffered. CLOSE }; -#define ENTRY_AUDIO_BUFFER_HELD(entry) \ - (al_atomic_bool_load(&(entry)->audio.buf.ref, AL_ATOMIC_RELAXED)) -#ifdef CAMU_SINK_NO_VIDEO -#define ENTRY_BUFFERS_HELD(entry) ENTRY_AUDIO_BUFFER_HELD(entry) -#else -#define ENTRY_VIDEO_BUFFER_HELD(entry) \ - (al_atomic_bool_load(&(entry)->video.buf.ref, AL_ATOMIC_RELAXED)) -#define ENTRY_BUFFERS_HELD(entry) (ENTRY_AUDIO_BUFFER_HELD(entry) || ENTRY_VIDEO_BUFFER_HELD(entry)) -#endif +#define ENTRY_MAX_AGE 7 #ifdef CAMU_SINK_NO_VIDEO #define ENTRY_VIDEO_READY_OR_EMPTY(entry) true @@ -57,18 +48,67 @@ enum { #define ENTRY_AUDIO_READY_OR_EMPTY(entry) \ (entry->audio.state == BUFFER_INIT || entry->audio.state == BUFFER_QUEUED || entry->audio.state == BUFFER_ADDED) +#if defined CAMU_SCREEN_THREADED && defined CAMU_MIXER_THREADED +#define BLOCKING_SLEEP(delay) aki_thread_sleep(delay) +#else +#define BLOCKING_SLEEP(delay) aki_event_loop_sleep(sink->loop, delay) +#endif + +static bool entry_audio_buffer_held(struct camu_sink_entry *entry) +{ +#ifdef CAMU_MIXER_THREADED + return al_atomic_load(u8)(&(entry)->audio.buf.ref, AL_ATOMIC_RELAXED) == 1; +#else + (void)entry; + return false; +#endif +} + +#ifndef CAMU_SINK_NO_VIDEO +static bool entry_video_buffer_held(struct camu_sink_entry *entry) +{ +#ifdef CAMU_SCREEN_THREADED + return al_atomic_load(u8)(&(entry)->video.buf.ref, AL_ATOMIC_RELAXED) == 1; +#else + (void)entry; + return false; +#endif +} +#endif + +static bool entry_buffers_held(struct camu_sink_entry *entry) +{ +#ifndef CAMU_SINK_NO_VIDEO + return entry_audio_buffer_held(entry) || entry_video_buffer_held(entry); +#else + return entry_audio_buffer_held(entry); +#endif +} + +static void remove_entry_audio_buffer(struct camu_sink *sink, struct camu_sink_entry *entry) +{ + 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; + } +} + +#ifndef CAMU_SINK_NO_VIDEO +static void remove_entry_video_buffer(struct camu_sink *sink, struct camu_sink_entry *entry) +{ + 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 + 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; - } + remove_entry_video_buffer(sink, entry); #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; - } + remove_entry_audio_buffer(sink, entry); } static void add_audio_if_set_and_buffered(struct camu_sink_entry *entry); @@ -76,21 +116,57 @@ static void add_audio_if_set_and_buffered(struct camu_sink_entry *entry); static void add_video_if_set_and_buffered(struct camu_sink_entry *entry); #endif -static void swap_buffers_internal(struct camu_sink *sink) +static void set_or_queue_entry(struct camu_sink_entry *entry) { - remove_entry_buffers(sink, sink->current); - sink->current = sink->queued; - sink->queued = NULL; - add_audio_if_set_and_buffered(sink->current); + if (entry->audio.state == BUFFER_INIT) { + entry->audio.state = BUFFER_QUEUED; + } else { + add_audio_if_set_and_buffered(entry); + } #ifndef CAMU_SINK_NO_VIDEO - add_video_if_set_and_buffered(sink->current); + if (entry->video.state == BUFFER_INIT) { + entry->video.state = BUFFER_QUEUED; + } else { + add_video_if_set_and_buffered(entry); + } #endif } +#if CAMU_SINK_LOCAL +static void sink_local_pause(struct camu_sink *sink, struct camu_sink_entry *entry) +{ + if (camu_clock_is_paused(&entry->clock)) { + camu_clock_resume(&entry->clock, 0); + bool no_audio = entry->audio.state == BUFFER_INIT || entry->audio.state == BUFFER_QUEUED; + if (!no_audio && 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 + bool single_frame = camu_video_buffer_is_single_frame(&entry->video.buf); + if (!single_frame && 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, 0); +#ifndef CAMU_SINK_NO_VIDEO + bool single_frame = camu_video_buffer_is_single_frame(&entry->video.buf); + if (!single_frame && sink->video.state == SINK_PLAYING) { + sink->callback(sink->userdata, CAMU_SINK_STOP, CAMU_SINK_VIDEO, NULL); + sink->video.state = SINK_PAUSED; + } +#endif + } +} +#endif + static void handle_sink_cmd(struct camu_sink *sink, struct camu_sink_cmd *cmd) { switch (cmd->op) { case START: { + aki_mutex_lock(&sink->mutex); switch (cmd->value.i) { case CAMU_SINK_AUDIO: if (sink->audio.state == SINK_PAUSED) { @@ -107,9 +183,11 @@ static void handle_sink_cmd(struct camu_sink *sink, struct camu_sink_cmd *cmd) break; #endif } + aki_mutex_unlock(&sink->mutex); break; } case STOP: { + aki_mutex_lock(&sink->mutex); switch (cmd->value.i) { case CAMU_SINK_AUDIO: if (sink->audio.state == SINK_PLAYING) { @@ -126,90 +204,69 @@ static void handle_sink_cmd(struct camu_sink *sink, struct camu_sink_cmd *cmd) break; #endif } + aki_mutex_unlock(&sink->mutex); break; } case SKIP: { + if (!sink->conn) return; struct aki_packet *packet = aki_rpc_get_packet(&sink->client, CAMU_SERVER_LIST_ACTION); - aki_packet_write_u8(packet, CAMU_SKIP); - s32 sequence = sink->current ? sink->current->sequence : CAMU_SEQUENCE_INVALID; + aki_packet_write_str(packet, al_str_c("default")); + aki_packet_write_u8(packet, CAMU_LIST_SKIP); + //s32 sequence = sink->current ? sink->current->sequence : LIANA_SEQUENCE_ANY; + s32 sequence = LIANA_SEQUENCE_ANY; aki_packet_write_s32(packet, sequence); aki_packet_write_s32(packet, cmd->value.i); aki_rpc_connection_command(sink->conn, packet, NULL, NULL); break; } + case SHUFFLE: { + if (!sink->conn) return; + struct aki_packet *packet = aki_rpc_get_packet(&sink->client, CAMU_SERVER_LIST_ACTION); + aki_packet_write_str(packet, al_str_c("default")); + aki_packet_write_u8(packet, CAMU_LIST_SHUFFLE); + aki_rpc_connection_command(sink->conn, packet, NULL, NULL); + break; + } case TOGGLE_PAUSE: { - struct camu_sink_entry *entry = (struct camu_sink_entry *)cmd->opaque; -#ifdef CAMU_SINK_LOCAL_PAUSE - 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, -1.0); -#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 - } +#if CAMU_SINK_LOCAL + sink_local_pause(sink, (struct camu_sink_entry *)cmd->opaque); #else - f64 base = camu_clock_get_base_pts(&entry->clock); - if (!camu_clock_is_paused(&entry->clock)) { - f64 pts = camu_clock_get_pts(&entry->clock, 0.0); - if (pts > base) base = pts; - } + if (!sink->conn) return; struct aki_packet *packet = aki_rpc_get_packet(&sink->client, CAMU_SERVER_LIST_ACTION); - aki_packet_write_u8(packet, CAMU_TOGGLE_PAUSE); - s32 sequence = sink->current ? sink->current->sequence : CAMU_SEQUENCE_INVALID; + aki_packet_write_str(packet, al_str_c("default")); + aki_packet_write_u8(packet, CAMU_LIST_TOGGLE_PAUSE); + s32 sequence = sink->current ? sink->current->sequence : LIANA_SEQUENCE_ANY; aki_packet_write_s32(packet, sequence); - aki_packet_write_u64(packet, base * 1000000.0); + aki_packet_write_f64(packet, cmd->value.f); aki_rpc_connection_command(sink->conn, packet, NULL, NULL); #endif break; } case SEEK: { + if (!sink->conn) return; struct aki_packet *packet = aki_rpc_get_packet(&sink->client, CAMU_SERVER_LIST_ACTION); - aki_packet_write_u8(packet, CAMU_SEEK); - s32 sequence = sink->current ? sink->current->sequence : CAMU_SEQUENCE_INVALID; + aki_packet_write_str(packet, al_str_c("default")); + aki_packet_write_u8(packet, CAMU_LIST_SEEK); + s32 sequence = sink->current ? sink->current->sequence : LIANA_SEQUENCE_ANY; aki_packet_write_s32(packet, sequence); aki_packet_write_f64(packet, cmd->value.f); aki_rpc_connection_command(sink->conn, packet, NULL, NULL); break; } - case FINISHED: { + case END: { + if (!sink->conn) return; struct aki_packet *packet = aki_rpc_get_packet(&sink->client, CAMU_SERVER_LIST_ACTION); - aki_packet_write_u8(packet, CAMU_FINISHED); + aki_packet_write_str(packet, al_str_c("default")); + aki_packet_write_u8(packet, CAMU_LIST_END); aki_packet_write_s32(packet, cmd->value.i); aki_rpc_connection_command(sink->conn, packet, NULL, NULL); break; } case RESEEK: { struct camu_sink_entry *entry = (struct camu_sink_entry *)cmd->opaque; - shrb_client_reseek(&entry->client); + lia_client_reseek(&entry->client); break; } -// case SET_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: { aki_signal_stop(&sink->signal); sink->callback(sink->userdata, CAMU_SINK_EXIT, 0, NULL); @@ -250,20 +307,15 @@ void add_audio_if_set_and_buffered(struct camu_sink_entry *entry) u8 state = entry->audio.state; if (state == BUFFER_SET_OR_BUFFERED) { bool can_resume = ENTRY_VIDEO_READY_OR_EMPTY(entry); - queue_cmd(entry->sink, (struct camu_sink_cmd){ - .op = START, - .value.i = CAMU_SINK_AUDIO - }); camu_audio_buffer_unpause(&entry->audio.buf); entry->sink->callback(entry->sink->userdata, CAMU_SINK_ADD_BUFFER, CAMU_SINK_AUDIO, &entry->audio.buf); if (can_resume) { - camu_clock_ready(&entry->clock); maybe_remove_previous(entry->sink); } - //queue_cmd(entry->sink, (struct camu_sink_cmd){ - // .op = SET_BUFFERED, - // .value.i = CAMU_SINK_AUDIO - //}); + queue_cmd(entry->sink, (struct camu_sink_cmd){ + .op = START, + .value.i = CAMU_SINK_AUDIO + }); state = BUFFER_ADDED; } else if (state == BUFFER_CONFIGURED) { state = BUFFER_SET_OR_BUFFERED; @@ -277,19 +329,15 @@ void add_video_if_set_and_buffered(struct camu_sink_entry *entry) u8 state = entry->video.state; if (state == BUFFER_SET_OR_BUFFERED) { bool can_resume = ENTRY_AUDIO_READY_OR_EMPTY(entry); - queue_cmd(entry->sink, (struct camu_sink_cmd){ - .op = START, - .value.i = CAMU_SINK_VIDEO - }); entry->sink->callback(entry->sink->userdata, CAMU_SINK_ADD_BUFFER, CAMU_SINK_VIDEO, &entry->video.buf); if (can_resume) { - camu_clock_ready(&entry->clock); maybe_remove_previous(entry->sink); } - //queue_cmd(entry->sink, (struct camu_sink_cmd){ - // .op = SET_BUFFERED, - // .value.i = CAMU_SINK_VIDEO - //}); + bool single_frame = camu_video_buffer_is_single_frame(&entry->video.buf); + queue_cmd(entry->sink, (struct camu_sink_cmd){ + .op = single_frame ? STOP : START, + .value.i = CAMU_SINK_VIDEO + }); state = BUFFER_ADDED; } else if (state == BUFFER_CONFIGURED) { state = BUFFER_SET_OR_BUFFERED; @@ -309,10 +357,10 @@ static void audio_buffer_callback(void *userdata, u8 op) aki_mutex_unlock(&sink->mutex); break; case CAMU_BUFFER_CORK: - shrb_vcr_stream_cork(entry->audio.stream); + lia_vcr_cork(entry->audio.track); break; case CAMU_BUFFER_UNCORK: - shrb_vcr_stream_uncork(entry->audio.stream); + lia_vcr_uncork(entry->audio.track); break; case CAMU_BUFFER_PAUSED: aki_mutex_lock(&sink->mutex); @@ -325,28 +373,18 @@ static void audio_buffer_callback(void *userdata, u8 op) aki_mutex_unlock(&sink->mutex); break; case CAMU_BUFFER_EOF: { - bool swapped = false; + lia_vcr_cork(entry->audio.track); aki_mutex_lock(&sink->mutex); + camu_clock_end(&entry->clock); + remove_entry_audio_buffer(sink, entry); if (sink->queued) { - al_log_info("sink", "Swapping audio buffers (gapless)."); - swap_buffers_internal(sink); - swapped = true; + set_or_queue_entry(sink->queued); + sink->current = sink->queued; + sink->queued = NULL; } aki_mutex_unlock(&sink->mutex); - if (!swapped) { - 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 = SET_BUFFERED, - // .value.i = CAMU_SINK_VIDEO - //}); -#endif - } queue_cmd(sink, (struct camu_sink_cmd){ - .op = FINISHED, + .op = END, .value.i = entry->sequence }); break; @@ -366,22 +404,39 @@ static void video_buffer_callback(void *userdata, u8 op) aki_mutex_unlock(&sink->mutex); break; case CAMU_BUFFER_CORK: - shrb_vcr_stream_cork(entry->video.stream); + lia_vcr_cork(entry->video.track); break; case CAMU_BUFFER_UNCORK: - shrb_vcr_stream_uncork(entry->video.stream); + lia_vcr_uncork(entry->video.track); break; case CAMU_BUFFER_EOF: { - queue_cmd(sink, (struct camu_sink_cmd){ - .op = STOP, - .value.i = CAMU_SINK_VIDEO - }); + lia_vcr_cork(entry->video.track); + bool single_frame = camu_video_buffer_is_single_frame(&entry->video.buf); + bool swapped = false; aki_mutex_lock(&sink->mutex); - bool no_audio = entry->audio.state == BUFFER_INIT; + if (!single_frame) { + remove_entry_video_buffer(sink, entry); + } + bool run_queue = !single_frame && (entry->audio.state == BUFFER_INIT || entry->audio.state == BUFFER_QUEUED); + if (run_queue) { + camu_clock_end(&entry->clock); + if (sink->queued) { + set_or_queue_entry(sink->queued); + sink->current = sink->queued; + sink->queued = NULL; + swapped = true; + } + } aki_mutex_unlock(&sink->mutex); - if (no_audio) { + if (!swapped) { queue_cmd(sink, (struct camu_sink_cmd){ - .op = FINISHED, + .op = STOP, + .value.i = CAMU_SINK_VIDEO + }); + } + if (run_queue) { + queue_cmd(sink, (struct camu_sink_cmd){ + .op = END, .value.i = entry->sequence }); } @@ -391,124 +446,114 @@ static void video_buffer_callback(void *userdata, u8 op) } #endif -static void client_callback(void *userdata, u8 op, struct shrb_vcr_stream *stream, void *opaque) +static void evaluate_latency(struct camu_sink *sink, struct camu_sink_entry *entry) +{ + // Entry has both audio and video configured. +#ifndef CAMU_SINK_NO_VIDEO + if (entry->audio.state != BUFFER_INIT && entry->audio.state != BUFFER_QUEUED && + entry->video.state != BUFFER_INIT && entry->video.state != BUFFER_QUEUED) { + camu_video_buffer_set_latency(&entry->video.buf, -camu_mixer_get_latency(sink->audio.mixer)); + } +#else + (void)sink; + (void)entry; +#endif +} + +static void client_callback(void *userdata, u8 op, struct camu_codec_stream *stream, void *opaque) { struct camu_sink_entry *entry = (struct camu_sink_entry *)userdata; struct camu_sink *sink = entry->sink; switch (op) { - case SHRUB_CLIENT_CONFIGURE: { + case LIANA_CLIENT_CONFIGURE: { // No data can be sent until all selected streams are configured. switch (stream->type) { - case SHRUB_STREAM_AUDIO: - entry->audio.stream = stream; - camu_audio_buffer_configure(&entry->audio.buf, &stream->stream); - aki_mutex_lock(&entry->sink->mutex); + case CAMU_STREAM_AUDIO: + entry->audio.track = (struct lia_vcr_track *)opaque; + if (!camu_audio_buffer_configure(&entry->audio.buf, stream)) { + lia_client_disconnect(&entry->client); + } + aki_mutex_lock(&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); + evaluate_latency(sink, entry); + aki_mutex_unlock(&sink->mutex); break; #ifndef CAMU_SINK_NO_VIDEO - case SHRUB_STREAM_VIDEO: - entry->video.stream = stream; - camu_video_buffer_configure(&entry->video.buf, &stream->stream); - aki_mutex_lock(&entry->sink->mutex); + case CAMU_STREAM_VIDEO: + entry->video.track = (struct lia_vcr_track *)opaque; + if (!camu_video_buffer_configure(&entry->video.buf, stream)) { + lia_client_disconnect(&entry->client); + } + aki_mutex_lock(&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); + evaluate_latency(sink, entry); + aki_mutex_unlock(&sink->mutex); break; #endif } break; } - case SHRUB_CLIENT_SET: { - struct shrb_seek_req *req = (struct shrb_seek_req *)opaque; - req->delay = SHRUB_DELAY_IGNORE; - camu_clock_set(&entry->clock, req); - if (req->paused) req->base = req->paused_at; - break; - } - case SHRUB_CLIENT_PAUSE: { - f64 pts = *(f64 *)opaque; - aki_mutex_lock(&entry->sink->mutex); - camu_clock_arm_pause(&entry->clock, pts); - entry->audio.armed = false; - aki_mutex_unlock(&entry->sink->mutex); - break; - } - case SHRUB_CLIENT_RESUME: { - aki_mutex_lock(&entry->sink->mutex); - struct shrb_resume_req *req = (struct shrb_resume_req *)opaque; - if (sink->queued) { - camu_clock_offset(&sink->queued->clock, req->offset); - } - camu_clock_arm_resume(&entry->clock, req->start); - if (sink->audio.state == SINK_PAUSED) { - sink->callback(sink->userdata, CAMU_SINK_START, CAMU_SINK_AUDIO, NULL); - sink->audio.state = SINK_PLAYING; - } else { - entry->audio.armed = true; - } - aki_mutex_unlock(&entry->sink->mutex); - break; - } - case SHRUB_CLIENT_DATA: { - struct camu_frame *frame = (struct camu_frame *)opaque; + case LIANA_CLIENT_DATA: { + struct camu_codec_frame *frame = (struct camu_codec_frame *)opaque; switch (stream->type) { - case SHRUB_STREAM_AUDIO: + case CAMU_STREAM_AUDIO: camu_audio_buffer_push(&entry->audio.buf, frame); break; #ifndef CAMU_SINK_NO_VIDEO - case SHRUB_STREAM_VIDEO: + case CAMU_STREAM_VIDEO: camu_video_buffer_push(&entry->video.buf, frame); break; #endif default: - camu_frame_discard(frame); + camu_codec_frame_discard(frame); break; } break; } - case SHRUB_CLIENT_REMOVE_BUFFERS: { - aki_mutex_lock(&entry->sink->mutex); + case LIANA_CLIENT_REMOVE_BUFFERS: { + aki_mutex_lock(&sink->mutex); if (entry->audio.state == BUFFER_ADDED) { - entry->sink->callback(entry->sink->userdata, CAMU_SINK_REMOVE_BUFFER, CAMU_SINK_AUDIO, &entry->audio.buf); + sink->callback(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); + bool reconnect = *(bool *)opaque; + bool keep_video = reconnect && camu_video_buffer_is_single_frame(&entry->video.buf); + if (!keep_video && 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 - 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)); } + aki_mutex_unlock(&sink->mutex); #ifndef CAMU_SINK_NO_VIDEO - camu_video_buffer_reset(&entry->video.buf); + while (keep_video ? entry_audio_buffer_held(entry) : entry_buffers_held(entry)) { + BLOCKING_SLEEP(AKI_TS_FROM_USEC(2000)); + } + if (!keep_video) camu_video_buffer_reset(&entry->video.buf); +#else + while (entry_buffers_held(entry)) { + BLOCKING_SLEEP(AKI_TS_FROM_USEC(2000)); } #endif camu_audio_buffer_reset(&entry->audio.buf); break; } - case SHRUB_CLIENT_EOF: { + case LIANA_CLIENT_EOF: { switch (stream->type) { - case SHRUB_STREAM_AUDIO: { + case CAMU_STREAM_AUDIO: { camu_audio_buffer_flush(&entry->audio.buf); break; } #ifndef CAMU_SINK_NO_VIDEO - case SHRUB_STREAM_VIDEO: { + case CAMU_STREAM_VIDEO: { bool single_frame = camu_video_buffer_is_single_frame(&entry->video.buf); if (!single_frame) { camu_video_buffer_flush(&entry->video.buf); @@ -519,19 +564,47 @@ static void client_callback(void *userdata, u8 op, struct shrb_vcr_stream *strea } break; } - case SHRUB_CLIENT_CLOSED: { - shrb_client_free(&entry->client); + case LIANA_CLIENT_CLOSED: { + aki_mutex_lock(&sink->mutex); + if (entry == sink->current) sink->current = NULL; + if (entry == sink->queued) sink->queued = NULL; + lia_client_free(&entry->client); camu_audio_buffer_free(&entry->audio.buf); #ifndef CAMU_SINK_NO_VIDEO camu_video_buffer_free(&entry->video.buf); #endif + bool removed = false; + struct camu_sink_entry *rentry; + al_array_foreach(sink->entries, i, rentry) { + if (rentry == entry) { + al_array_remove_at(sink->entries, i); + removed = true; + al_log_debug("sink", "Entry closed by disconnect."); + break; + } + } al_free(entry); - al_log_debug("sink", "Entry closed."); + if (!removed) { + al_log_debug("sink", "Entry closed by cleanup."); + } + aki_mutex_unlock(&sink->mutex); break; } } } +static void mixer_callback(void *userdata, u8 op) +{ + struct camu_sink *sink = (struct camu_sink *)userdata; + if (op == CAMU_MIXER_EMPTY) { + al_log_debug("sink", "Mixer empty."); + queue_cmd(sink, (struct camu_sink_cmd){ + .op = STOP, + .value.i = CAMU_SINK_AUDIO + }); + } +} + bool camu_sink_init(struct camu_sink *sink, struct aki_event_loop *loop, struct camu_mixer *mixer #ifndef CAMU_SINK_NO_VIDEO @@ -548,6 +621,9 @@ bool camu_sink_init(struct camu_sink *sink, struct aki_event_loop *loop, sink->current = NULL; al_array_init(sink->previous); al_array_init(sink->entries); + sink->lru = 0; + mixer->callback = mixer_callback; + mixer->userdata = sink; sink->audio.mixer = mixer; sink->audio.state = SINK_PAUSED; #ifndef CAMU_SINK_NO_VIDEO @@ -557,25 +633,82 @@ bool camu_sink_init(struct camu_sink *sink, struct aki_event_loop *loop, return true; } -static struct camu_sink_entry *entry_from_node_id(struct camu_sink *sink, u16 node_id) +static s32 lru_compare(const void *a, const void *b) { + struct camu_sink_entry *aa = *((struct camu_sink_entry **)a); + struct camu_sink_entry *bb = *((struct camu_sink_entry **)b); + if (aa->lru > bb->lru) return -1; + else if (aa->lru < bb->lru) return 1; + return 0; +} + +static void maybe_cleanup_old_entries(struct camu_sink *sink) +{ + al_array_sort(sink->entries, struct camu_sink_entry *, lru_compare); + // We check size <= MAX_AGE in the loop because sink->lru + // is not indicative of the amount of entries we have loaded. + // There are various reasons for this but the most obvious is + // that it's incremented for buffer and queue operations. + // + // Handle sink->lru wrapping. + // 0 65532 65533 65534 65535 + // 0 1 65533 65534 65535 + // 0 1 2 65534 65535 + // 0 1 2 3 65535 + // 0 1 2 3 4 struct camu_sink_entry *entry; - al_array_foreach(sink->entries, i, entry) { - if (entry->client.node_id == node_id) return entry; + al_array_foreach_rev(sink->entries, i, entry) { + if (sink->entries.size <= ENTRY_MAX_AGE) return; + if (entry->lru > sink->lru && (UINT16_MAX - (entry->lru - 1)) + sink->lru >= ENTRY_MAX_AGE) { + al_array_remove_at(sink->entries, i); + lia_client_disconnect(&entry->client); + } + } + if (sink->lru >= ENTRY_MAX_AGE) { + al_array_foreach_rev(sink->entries, i, entry) { + if (sink->entries.size <= ENTRY_MAX_AGE) return; + if (sink->lru - entry->lru >= ENTRY_MAX_AGE) { + al_array_remove_at(sink->entries, i); + lia_client_disconnect(&entry->client); + } + } + } +} + +static void clock_callback(void *userdata, u8 op) +{ + struct camu_sink_entry *entry = (struct camu_sink_entry *)userdata; + struct camu_sink *sink = entry->sink; + if (op == CAMU_CLOCK_PAUSED) { + aki_mutex_lock(&sink->mutex); + if (sink->target) { + if (sink->current) { + al_array_push(sink->previous, sink->current); + } + set_or_queue_entry(sink->target); + sink->current = sink->target; + sink->target = NULL; + } + aki_mutex_unlock(&sink->mutex); } - return NULL; } -static struct camu_sink_entry *ensure_entry_buffered_internal(struct camu_sink *sink, - str *addr, s32 port, u32 node_id) +static struct camu_sink_entry *get_entry_from_id(struct camu_sink *sink, u16 id, bool *created) { - struct camu_sink_entry *entry = entry_from_node_id(sink, node_id); - if (entry) return entry; + struct camu_sink_entry *entry; + al_array_foreach(sink->entries, i, entry) { + if (entry->id == id) { + *created = false; + return entry; + } + } entry = al_alloc_object(struct camu_sink_entry); - al_array_push(sink->entries, entry); + entry->id = id; entry->sink = sink; + camu_clock_init(&entry->clock, clock_callback, entry); + entry->audio.state = BUFFER_INIT; entry->audio.armed = false; camu_audio_buffer_init(&entry->audio.buf, &entry->clock, sink->audio.mixer); @@ -584,15 +717,17 @@ static struct camu_sink_entry *ensure_entry_buffered_internal(struct camu_sink * #ifndef CAMU_SINK_NO_VIDEO entry->video.state = BUFFER_INIT; - camu_video_buffer_init(&entry->video.buf, &entry->clock, 0.0, sink->video.renderer); + camu_video_buffer_init(&entry->video.buf, &entry->clock, sink->video.renderer); entry->video.buf.callback = video_buffer_callback; entry->video.buf.userdata = entry; #endif entry->client.callback = client_callback; entry->client.userdata = entry; - // Assume we can error out at any point after connect is called (even from within connect() itself). - shrb_client_connect(&entry->client, sink->loop, addr, port, node_id); + + al_array_push(sink->entries, entry); + + *created = true; return entry; } @@ -604,100 +739,237 @@ static bool set_command_callback(void *userdata, struct aki_rpc_connection *conn (void)conn; (void)rpacket; + aki_mutex_lock(&sink->mutex); + + u8 op = aki_packet_read_u8(packet); +#if CAMU_SINK_LOCAL + if (op == CAMU_SINK_CLEAR) { + if (sink->current) { + remove_entry_buffers(sink, sink->current); + sink->current = NULL; + } + goto out; + } +#else + if (op == CAMU_SINK_CLEAR) { al_assert(false); } // Unimplemented. +#endif + str addr; aki_packet_read_str(packet, &addr); - s32 port = aki_packet_read_s32(packet); + u16 port = aki_packet_read_u16(packet); u16 node_id = aki_packet_read_u16(packet); s32 sequence = aki_packet_read_s32(packet); - u64 start = aki_packet_read_u64(packet); - u8 paused = aki_packet_read_u8(packet); - u64 paused_at = aki_packet_read_u64(packet); + u64 at = aki_packet_read_u64(packet); + u64 seek_pos = aki_packet_read_u64(packet); + u8 pause = aki_packet_read_u8(packet); - aki_mutex_lock(&sink->mutex); - struct camu_sink_entry *entry = ensure_entry_buffered_internal(sink, &addr, port, node_id); + bool created; + struct camu_sink_entry *entry = get_entry_from_id(sink, node_id, &created); entry->sequence = sequence; - if (entry == sink->current) goto out; - if (!paused) { - // This will only have an effect if the clock is already set. - camu_clock_arm_resume(&entry->clock, start + SHRUB_BASE_DELAY); + sink->lru = al_u16_inc_wrap(sink->lru); + entry->lru = sink->lru; + + if (created) { + camu_clock_set(&entry->clock, seek_pos / 1000000.0); + struct camu_renderer *renderer = NULL; +#ifndef CAMU_SINK_NO_VIDEO + renderer = sink->video.renderer; +#endif + lia_client_connect(&entry->client, sink->loop, sink->type, &addr, port, node_id, seek_pos, renderer); + } + + if (op == CAMU_SINK_BUFFER) { + goto out; + } else if (op == CAMU_SINK_BUFFER_AND_QUEUE) { + sink->queued = entry; + goto out; } + +#if CAMU_SINK_LOCAL + (void)at; + (void)pause; if (sink->current) { - camu_clock_pause(&sink->current->clock, paused_at / 1000000.0); + if (!camu_clock_is_paused(&sink->current->clock)) { + camu_clock_pause(&sink->current->clock, 0); + } al_array_push(sink->previous, sink->current); } + set_or_queue_entry(entry); sink->current = entry; - if (sink->queued) sink->queued = NULL; - if (entry->audio.state == BUFFER_INIT) { - entry->audio.state = BUFFER_QUEUED; - } else { - add_audio_if_set_and_buffered(entry); + // This will resume a user paused stream. + camu_clock_resume(&entry->clock, 0); +#else + switch (pause) { + case LIANA_PAUSE_NONE: + if (sink->current) { + al_array_push(sink->previous, sink->current); + } + set_or_queue_entry(entry); + sink->current = entry; + break; + case LIANA_PAUSE_RESUME: + camu_clock_resume(&entry->clock, at); + if (sink->current) { + al_array_push(sink->previous, sink->current); + } + set_or_queue_entry(entry); + sink->current = entry; + break; + case LIANA_PAUSE_PAUSE: + if (sink->current) { + if (camu_clock_is_ended(&sink->current->clock)) { + // Server thought we weren't done, be we are. + al_array_push(sink->previous, sink->current); + set_or_queue_entry(entry); + sink->current = entry; + } else { + al_printf("ay\n"); + sink->target = entry; + camu_clock_pause(&sink->current->clock, at); + } + } + break; + case LIANA_PAUSE_BOTH: { + camu_clock_resume(&entry->clock, at); + struct camu_sink_entry *prev_target = sink->target; + if (prev_target) { + if (prev_target == entry) { + sink->target = NULL; + } else { + sink->target = entry; + } + camu_clock_pause(&prev_target->clock, at); + } else if (sink->current) { + if (camu_clock_is_ended(&sink->current->clock)) { + al_array_push(sink->previous, sink->current); + set_or_queue_entry(entry); + sink->current = entry; + } else { + sink->target = entry; + camu_clock_pause(&sink->current->clock, at); + } + } + break; } -#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 + out: aki_mutex_unlock(&sink->mutex); + if (op != CAMU_SINK_BUFFER) { + maybe_cleanup_old_entries(sink); + } + aki_packet_free(packet); return false; } -static bool queue_command_callback(void *userdata, struct aki_rpc_connection *conn, +static bool pause_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); + aki_mutex_lock(&sink->mutex); + struct camu_sink_entry *current = sink->current; + if (!current) goto out; +#if CAMU_SINK_LOCAL + sink_local_pause(sink, current); +#else s32 sequence = aki_packet_read_s32(packet); + u64 at = aki_packet_read_u64(packet); + u8 pause = aki_packet_read_u8(packet); + if (current->sequence == sequence) { + if (pause == LIANA_PAUSE_PAUSE) { + camu_clock_pause(&sink->current->clock, at); + current->audio.armed = false; + } else { + camu_clock_resume(&sink->current->clock, at); + if (sink->audio.state == SINK_PAUSED) { + sink->callback(sink->userdata, CAMU_SINK_START, CAMU_SINK_AUDIO, NULL); + sink->audio.state = SINK_PLAYING; + } else { + current->audio.armed = true; + } + } + } +#endif +out: + aki_mutex_unlock(&sink->mutex); + + aki_packet_free(packet); + return false; +} + +static bool seek_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; + + s32 sequence = aki_packet_read_s32(packet); + u64 at = aki_packet_read_u64(packet); + u64 pos = aki_packet_read_u64(packet); aki_mutex_lock(&sink->mutex); - sink->queued = ensure_entry_buffered_internal(sink, &addr, port, node_id); - sink->queued->sequence = sequence; + struct camu_sink_entry *current = sink->current; aki_mutex_unlock(&sink->mutex); + if (current->sequence == sequence) { + // TODO: Thread-safety here. + lia_client_seek(¤t->client, pos); + camu_clock_set(¤t->clock, pos / 1000000.0); + camu_clock_resume(¤t->clock, at); + } aki_packet_free(packet); - return false; } static struct aki_rpc_command commands[] = { { .op = CAMU_SINK_SET, .callback = set_command_callback, .userdata = NULL }, - { .op = CAMU_SINK_QUEUE, .callback = queue_command_callback, .userdata = NULL } + { .op = CAMU_SINK_PAUSE, .callback = pause_command_callback, .userdata = NULL }, + { .op = CAMU_SINK_SEEK, .callback = seek_command_callback, .userdata = NULL } }; +static void idd_callback(void *userdata, struct aki_packet *packet) +{ + struct camu_sink *sink = (struct camu_sink *)userdata; + (void)sink; + aki_packet_free(packet); +} + 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, CAMU_SERVER_IDENTIFY); aki_packet_write_u8(packet, CAMU_SINK); - aki_rpc_connection_command(sink->conn, packet, NULL, NULL); + aki_packet_write_str(packet, &sink->name); + aki_rpc_connection_command(sink->conn, packet, idd_callback, sink); } static void connection_closed_callback(void *userdata, struct aki_rpc_connection *conn) { struct camu_sink *sink = (struct camu_sink *)userdata; - (void)sink; - (void)conn; + if (sink->conn) { + al_assert(sink->conn == conn); + sink->conn = NULL; + } } -bool camu_sink_connect(struct camu_sink *sink, str *addr, s32 port) +bool camu_sink_connect(struct camu_sink *sink, u8 type, str *addr, u16 port, str *name) { + al_str_clone(&sink->name, name); + sink->type = type; aki_rpc_init(&sink->client, sink->loop, 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]); } - if (!aki_rpc_prepare_client(&sink->client, AKI_SOCKET_TCP, CAMU_MULTIPLEX_RPC)) { + if (!aki_rpc_prepare_client(&sink->client, sink->type, CAMU_MULTIPLEX_RPC)) { return false; } aki_rpc_connect(&sink->client, addr, port); @@ -715,14 +987,6 @@ void camu_sink_return_current(struct camu_sink *sink) aki_mutex_unlock(&sink->mutex); } -void camu_sink_add(struct camu_sink *sink, str *line) -{ - struct aki_packet *packet = aki_rpc_get_packet(&sink->client, CAMU_SERVER_LIST_ACTION); - aki_packet_write_u8(packet, CAMU_ADD); - aki_packet_write_str(packet, line); - aki_rpc_connection_command(sink->conn, packet, NULL, NULL); -} - void camu_sink_skip(struct camu_sink *sink, s32 n) { queue_cmd(sink, (struct camu_sink_cmd){ @@ -731,14 +995,26 @@ void camu_sink_skip(struct camu_sink *sink, s32 n) }); } +void camu_sink_shuffle(struct camu_sink *sink) +{ + queue_cmd(sink, (struct camu_sink_cmd){ + .op = SHUFFLE + }); +} + void camu_sink_toggle_pause(struct camu_sink *sink) { aki_mutex_lock(&sink->mutex); struct camu_sink_entry *current = sink->current; aki_mutex_unlock(&sink->mutex); + f64 pts = -1.0; + if (!camu_clock_is_paused(¤t->clock)) { + pts = camu_clock_get_pts(¤t->clock, 0.0); + } if (current) { queue_cmd(sink, (struct camu_sink_cmd){ .op = TOGGLE_PAUSE, + .value.f = pts, .opaque = current }); } @@ -788,19 +1064,20 @@ void camu_sink_stop(struct camu_sink *sink) void camu_sink_close(struct camu_sink *sink) { - // TODO: Make sure no commands can come in after this. - aki_rpc_conn_disconnect(sink->client.conn); - aki_mutex_lock(&sink->mutex); + if (sink->conn) aki_rpc_conn_disconnect(sink->conn); struct camu_sink_entry *entry; - al_array_foreach(sink->entries, i, entry) { - shrb_client_close(&entry->client); + al_array_foreach_rev(sink->entries, i, entry) { + al_array_remove_at(sink->entries, i); + lia_client_disconnect(&entry->client); } - sink->entries.size = 0; - aki_mutex_unlock(&sink->mutex); } void camu_sink_free(struct camu_sink *sink) { + struct camu_sink_entry *entry; + al_array_foreach(sink->entries, i, entry) { + al_free(entry); + } al_array_free(sink->entries); aki_rpc_free(&sink->client); camu_queue_free(sink->queue); diff --git a/src/libsink/sink.h b/src/libsink/sink.h index b789c9a..b9249cd 100644 --- a/src/libsink/sink.h +++ b/src/libsink/sink.h @@ -1,7 +1,5 @@ #pragma once -//#define CAMU_SINK_LOCAL_PAUSE - #include <al/str.h> #include <al/array.h> #include <aki/rpc2.h> @@ -15,7 +13,7 @@ #include "../buffer/video.h" #endif -#include "../shrub/client.h" +#include "../liana/client.h" enum { CAMU_SINK_AUDIO = 0, @@ -28,27 +26,32 @@ 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 }; +enum { + CAMU_SINK_OK = 0 +}; + struct camu_sink_entry { + u16 id; s32 sequence; + u16 lru; struct camu_clock clock; - struct shrb_client client; + struct lia_client client; struct { u8 state; bool armed; struct camu_audio_buffer buf; - struct shrb_vcr_stream *stream; + struct lia_vcr_track *track; } audio; #ifndef CAMU_SINK_NO_VIDEO struct { u8 state; struct camu_video_buffer buf; - struct shrb_vcr_stream *stream; + struct lia_vcr_track *track; } video; #endif struct camu_sink *sink; @@ -60,22 +63,21 @@ struct camu_sink_cmd { void *opaque; }; -enum { - CAMU_SINK_OK = 0, - CAMU_SINK_BUFFERS_SWAPPED = 1 -}; - struct camu_sink { struct aki_event_loop *loop; + str name; + u8 type; 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 *queued; struct camu_sink_entry *current; + struct camu_sink_entry *queued; + struct camu_sink_entry *target; array(struct camu_sink_entry *) previous; array(struct camu_sink_entry *) entries; + u16 lru; struct { u8 state; struct camu_mixer *mixer; @@ -96,11 +98,11 @@ bool camu_sink_init(struct camu_sink *sink, struct aki_event_loop *loop, , struct camu_renderer *renderer #endif ); -bool camu_sink_connect(struct camu_sink *sink, str *addr, s32 port); +bool camu_sink_connect(struct camu_sink *sink, u8 type, str *addr, u16 port, str *name); struct camu_sink_entry *camu_sink_get_current(struct camu_sink *sink); void camu_sink_return_current(struct camu_sink *sink); -void camu_sink_add(struct camu_sink *sink, str *line); void camu_sink_skip(struct camu_sink *sink, s32 n); +void camu_sink_shuffle(struct camu_sink *sink); void camu_sink_toggle_pause(struct camu_sink *sink); void camu_sink_seek(struct camu_sink *sink, f64 pos); void camu_sink_reseek(struct camu_sink *sink); diff --git a/src/list/list.c b/src/list/list.c deleted file mode 100644 index d5be3dc..0000000 --- a/src/list/list.c +++ /dev/null @@ -1,257 +0,0 @@ -#include <al/random.h> -#include <aki/thread.h> - -#include "../libsink/common.h" -#include "../shrub/handler.h" - -#include "list.h" - -void camu_list_init(struct camu_list *list, str *name) -{ - al_str_clone(&list->name, name); - list->current = -1; - list->prev = -1; - list->idle = true; - list->sequence = 0; - al_array_init(list->entries); - al_array_init(list->sinks); -} - -static void camu_list_pump(struct camu_list *list) -{ - s32 size = (s32)list->entries.size; - if (list->current == -1 || size <= list->current) return; - struct camu_list_entry *entry = al_array_at(list->entries, list->current); - struct camu_list_entry *upcoming = NULL; - if (list->current + 1 < size) { - upcoming = al_array_at(list->entries, list->current + 1); - } - struct camu_list_entry *prev = NULL; - if (list->prev >= 0) { - prev = al_array_at(list->entries, list->prev); - list->prev = -1; - } - struct camu_list_sink *sink; - al_array_foreach(list->sinks, i, sink) { - if (sink->set != list->current) { - void *prev_opaque = NULL; - if (prev) prev_opaque = prev->opaque; - sink->callback(sink->userdata, CAMU_SINK_SET, entry->opaque, prev_opaque, entry->sequence); - sink->set = list->current; - } - if (upcoming) { - sink->callback(sink->userdata, CAMU_SINK_QUEUE, upcoming->opaque, entry->opaque, upcoming->sequence); - } - } -} - -void camu_list_add_sink(struct camu_list *list, void (*callback)(void *, u8, void *, void *, s32), - void *userdata) -{ - struct camu_list_sink *sink = al_alloc_object(struct camu_list_sink); - sink->set = -1; - sink->callback = callback; - sink->userdata = userdata; - al_array_push(list->sinks, sink); - camu_list_pump(list); -} - -void camu_list_remove_sink(struct camu_list *list, void *userdata) -{ - struct camu_list_sink *sink; - al_array_foreach(list->sinks, i, sink) { - if (sink->userdata == userdata) { - al_array_remove_at_iter(list->sinks, i); - break; - } - } -} - -void camu_list_add(struct camu_list *list, void *opaque, void (*callback)(void *, u8, void *), - u64 duration, bool set) -{ - s32 size = (s32)list->entries.size; - struct camu_list_entry *entry = al_alloc_object(struct camu_list_entry); - entry->sequence = list->sequence++; - entry->opaque = opaque; - entry->callback = callback; - entry->duration = duration; - entry->start = aki_get_timestamp(); - entry->paused = false; - if (!list->idle) { - struct camu_list_entry *prev = al_array_at(list->entries, size - 1); - u64 end = prev->start + prev->duration; - if (end > entry->start) { - entry->start += end - entry->start; - } - } - al_array_push(list->entries, entry); - if (set) list->current = size; - else if (list->idle) { - list->current++; - list->idle = false; - } - camu_list_pump(list); - entry->callback(entry->opaque, CAMU_LIST_IMPULSE, &entry->start); -} - -static s32 index_of_entry_from_sequence(struct camu_list *list, s32 sequence) -{ - if (sequence != CAMU_SEQUENCE_INVALID) { - struct camu_list_entry *entry; - al_array_foreach(list->entries, i, entry) { - if (entry->sequence == sequence) return i; - } - } - return CAMU_SEQUENCE_INVALID; -} - -void camu_list_skip(struct camu_list *list, s32 sequence, s32 n) -{ - s32 size = (s32)list->entries.size; - s32 index = index_of_entry_from_sequence(list, sequence); - if (index == CAMU_SEQUENCE_INVALID) index = list->current; - if (index + n < 0 || index + n >= size || index + n == list->current) return; - list->prev = list->current; - struct camu_list_entry *prev = al_array_at(list->entries, list->prev); - prev->callback(prev->opaque, CAMU_LIST_PAUSE, NULL); - list->current = index + n; - struct camu_list_entry *entry = al_array_at(list->entries, list->current); - entry->start = aki_get_timestamp(); - entry->callback(entry->opaque, CAMU_LIST_IMPULSE, &entry->start); - camu_list_pump(list); -} - -void camu_list_skipto(struct camu_list *list, s32 i) -{ - s32 size = (s32)list->entries.size; - if (i < 0 || i >= size) return; - list->current = i; - camu_list_pump(list); -} - -void camu_list_toggle_pause(struct camu_list *list, s32 sequence, u64 pos) -{ - s32 index = index_of_entry_from_sequence(list, sequence); - if (index == CAMU_SEQUENCE_INVALID) return; - struct camu_list_entry *entry = al_array_at(list->entries, index); - if (entry->paused) { - u64 ts = aki_get_timestamp(); - struct shrb_resume_req req = { - .start = ts, - .offset = ts - entry->start - }; - entry->start = ts; - entry->callback(entry->opaque, CAMU_LIST_USER_RESUME, &req); - entry->paused = false; - } else { - entry->start = aki_get_timestamp(); - entry->callback(entry->opaque, CAMU_LIST_USER_PAUSE, &pos); - entry->paused = true; - } -} - -void camu_list_seek(struct camu_list *list, s32 sequence, f64 percent) -{ - s32 index = index_of_entry_from_sequence(list, sequence); - if (index == CAMU_SEQUENCE_INVALID) return; - list->idle = false; - struct camu_list_entry *entry = al_array_at(list->entries, index); - entry->start = aki_get_timestamp(); - entry->callback(entry->opaque, CAMU_LIST_IMPULSE, &entry->start); - u64 pos = percent * entry->duration; - entry->callback(entry->opaque, CAMU_LIST_SEEK, &pos); -} - -void camu_list_finished(struct camu_list *list, s32 sequence) -{ - s32 size = (s32)list->entries.size; - s32 index = index_of_entry_from_sequence(list, sequence); - if (index == CAMU_SEQUENCE_INVALID) return; - if (index + 1 >= size) { - list->idle = true; - return; - } - list->current = index + 1; - camu_list_pump(list); -} - -void camu_list_shuffle(struct camu_list *list) -{ - u32 size = list->entries.size; - if (size == 0) return; - for (u32 i = 0; i < size - 1; i++) { - u32 j = i + al_rand() / (AL_RAND_MAX / (size - i) + 1); - struct camu_list_entry *tmp = al_array_at(list->entries, j); - al_array_at(list->entries, j) = al_array_at(list->entries, i); - al_array_at(list->entries, i) = tmp; - } - struct camu_list_sink *sink; - al_array_foreach(list->sinks, i, sink) { - sink->set = -1; - } - camu_list_pump(list); -} - -static s64 camu_db_num_from_path(str *path) -{ - s32 slash = al_str_rfind(path, '/'); - if (slash < 0) return -1; - str s = *al_str_substr(path, slash + 1, path->len); - s32 underscore = al_str_find(&s, '_'); - if (underscore < 0) return -1; - s = *al_str_substr(&s, underscore + 1, s.len); - underscore = al_str_find(&s, '_'); - if (underscore < 0) return -1; - s = *al_str_substr(&s, underscore + 1, s.len); - underscore = al_str_find(&s, '_'); - if (underscore < 0) return -1; - s = *al_str_substr(&s, 0, underscore); - s64 num = al_str_to_long(&s, 10); - if (num == INT64_MIN || num == INT64_MAX) { - return -1; - } - return num; -} - -static s32 camu_db_compare(void *a, void *b) -{ - struct camu_list_entry *aa = *((struct camu_list_entry **)a); - struct camu_list_entry *bb = *((struct camu_list_entry **)b); - str *astr = NULL, *bstr = NULL; - aa->callback(aa->opaque, CAMU_LIST_ENTRY_ID, &astr); - bb->callback(bb->opaque, CAMU_LIST_ENTRY_ID, &bstr); - s64 anum = camu_db_num_from_path(astr); - s64 bnum = camu_db_num_from_path(bstr); - if (anum > bnum) { - return -1; - } else if (anum < bnum) { - return 1; - } - return 0; -} - -void camu_list_sort(struct camu_list *list) -{ - al_array_sort(list->entries, struct camu_list_entry *, camu_db_compare); - struct camu_list_sink *sink; - al_array_foreach(list->sinks, i, sink) { - sink->set = -1; - } - camu_list_pump(list); -} - -void camu_list_reverse(struct camu_list *list) -{ - u32 size = list->entries.size; - for (u32 i = 0; i < size; i++) { - u32 tail = size - (i + 1); - if (tail <= i) break; - AL_SWAP(al_array_at(list->entries, i), al_array_at(list->entries, tail), struct camu_list_entry *); - } - struct camu_list_sink *sink; - al_array_foreach(list->sinks, i, sink) { - sink->set = -1; - } - camu_list_pump(list); -} diff --git a/src/list/list.h b/src/list/list.h deleted file mode 100644 index 4873265..0000000 --- a/src/list/list.h +++ /dev/null @@ -1,57 +0,0 @@ -#pragma once - -#include <al/str.h> -#include <al/array.h> - -#define CAMU_SEQUENCE_INVALID INT32_MAX - -// TODO: Improve naming between this and server LIST_ACTION's -enum { - CAMU_LIST_ENTRY_ID = 0, - CAMU_LIST_IMPULSE, - CAMU_LIST_USER_PAUSE, - CAMU_LIST_USER_RESUME, - CAMU_LIST_PAUSE, // Pause at current position. - CAMU_LIST_SEEK -}; - -struct camu_list_entry { - s32 sequence; - void *opaque; - void (*callback)(void *, u8, void *); - u64 duration; - u64 start; - bool paused; -}; - -struct camu_list_sink { - s32 set; - bool (*action)(void *, u8, void *); - void (*callback)(void *, u8, void *, void *, s32); - void *userdata; -}; - -struct camu_list { - str name; - s32 current; - s32 prev; - bool idle; - s32 sequence; // Holds the next entry sequence. - array(struct camu_list_entry *) entries; - array(struct camu_list_sink *) sinks; -}; - -void camu_list_init(struct camu_list *list, str *name); -void camu_list_add_sink(struct camu_list *list, void (*callback)(void *, u8, void *, void *, s32), - void *userdata); -void camu_list_remove_sink(struct camu_list *list, void *userdata); -void camu_list_add(struct camu_list *list, void *opaque, void (*callback)(void *, u8, void *), - u64 duration, bool set); -void camu_list_skip(struct camu_list *list, s32 sequence, s32 n); -void camu_list_skipto(struct camu_list *list, s32 i); -void camu_list_toggle_pause(struct camu_list *list, s32 sequence, u64 pos); -void camu_list_seek(struct camu_list *list, s32 sequence, f64 percent); -void camu_list_finished(struct camu_list *list, s32 sequence); -void camu_list_shuffle(struct camu_list *list); -void camu_list_sort(struct camu_list *list); -void camu_list_reverse(struct camu_list *list); diff --git a/src/list/meson.build b/src/list/meson.build deleted file mode 100644 index c61ac39..0000000 --- a/src/list/meson.build +++ /dev/null @@ -1,5 +0,0 @@ -list_src = [ - 'list.c' -] -list_deps = [common_deps] -list = declare_dependency(sources: list_src, dependencies: list_deps) diff --git a/src/mixer/audio.h b/src/mixer/audio.h index 6624896..6e52f1a 100644 --- a/src/mixer/audio.h +++ b/src/mixer/audio.h @@ -2,12 +2,12 @@ #include <al/str.h> -#include "../codec/ffmpeg/resampler.h" +#include "../codec/codec.h" struct camu_audio { bool (*init)(struct camu_audio *, str *); - void (*pick_format)(struct camu_audio *, struct camu_ff_resample_fmt *); + void (*pick_format)(struct camu_audio *, struct camu_resampler_format *); bool (*configure_stream)(struct camu_audio *, void *); u64 (*get_latency)(struct camu_audio *); diff --git a/src/mixer/audio_miniaudio.c b/src/mixer/audio_miniaudio.c index 26c01b2..fb4db23 100644 --- a/src/mixer/audio_miniaudio.c +++ b/src/mixer/audio_miniaudio.c @@ -1,13 +1,26 @@ #include <al/log.h> - +#if defined(__clang__) || defined(__GNUC__) +_Pragma("GCC diagnostic push") \ +_Pragma("GCC diagnostic ignored \"-Wunused-function\"") +_Pragma("GCC diagnostic ignored \"-Wincompatible-pointer-types\"") +_Pragma("GCC diagnostic ignored \"-Wunknown-pragmas\"") +#elif _MSC_VER +#pragma warning(push, 0) +#endif #define MINIAUDIO_IMPLEMENTATION #include "audio_miniaudio.h" +#if defined(__clang__) || defined(__GNUC__) +_Pragma("GCC diagnostic pop") +#elif _MSC_VER +#pragma warning(pop) +#endif static ma_backend backends[] = { #ifndef _WIN32 ma_backend_pulseaudio, #else ma_backend_wasapi +// ma_backend_dsound #endif }; @@ -84,32 +97,34 @@ static bool audio_miniaudio_init(struct camu_audio *audio, str *name) return true; } -static enum AVSampleFormat av_sample_fmt_from_miniaudio(s32 fmt) +static s32 camu_sample_format_from_miniaudio(s32 fmt) { switch (fmt) { case ma_format_u8: - return AV_SAMPLE_FMT_U8; + return CAMU_SAMPLE_FORMAT_U8; case ma_format_s16: - return AV_SAMPLE_FMT_S16; + return CAMU_SAMPLE_FORMAT_S16; case ma_format_s24: case ma_format_s32: - return AV_SAMPLE_FMT_S32; + return CAMU_SAMPLE_FORMAT_S32; case ma_format_f32: - return AV_SAMPLE_FMT_FLT; + return CAMU_SAMPLE_FORMAT_FLT; case ma_format_unknown: default: - al_assert(false); - return AV_SAMPLE_FMT_NONE; + al_assert_and_return(false); } } -static void audio_miniaudio_pick_format(struct camu_audio *audio, struct camu_ff_resample_fmt *fmt) +static void audio_miniaudio_pick_format(struct camu_audio *audio, struct camu_resampler_format *fmt) { struct camu_audio_miniaudio *ma = (struct camu_audio_miniaudio *)audio; - fmt->req_format = av_sample_fmt_from_miniaudio(ma->device.playback.internalFormat); - fmt->req_channel_count = ma->device.playback.internalChannels; - av_channel_layout_default(&fmt->req_channel_layout, fmt->req_channel_count); - fmt->req_sample_rate = ma->device.playback.internalSampleRate; + fmt->req.format = camu_sample_format_from_miniaudio(ma->device.playback.format); + fmt->req.sample_rate = ma->device.playback.internalSampleRate; + u32 channels = ma->device.playback.channels; +#ifdef CAMU_HAVE_FFMPEG + av_channel_layout_default(&fmt->req.channel_layout, channels); +#endif + fmt->req.channel_count = channels; } static void data_callback(ma_device *device, void *output, const void *input, u32 frame_count) @@ -134,8 +149,9 @@ static bool audio_miniaudio_configure_stream(struct camu_audio *audio, void *opa ma->config.performanceProfile = ma_performance_profile_conservative; ma->config.periods = DEFAULT_PERIODS; ma->config.periodSizeInMilliseconds = DEFAULT_PERIOD_SIZE_IN_MILLISECONDS; - ma->config.playback.format = ma_format_unknown; - ma->config.playback.channels = 0; + //ma->config.playback.format = ma_format_unknown; + ma->config.playback.format = ma_format_f32; + ma->config.playback.channels = 2; ma->config.sampleRate = 0; ma->config.dataCallback = data_callback; ma->config.pUserData = ma; @@ -149,8 +165,8 @@ static bool audio_miniaudio_configure_stream(struct camu_audio *audio, void *opa al_log_error("audio_miniaudio", "Failed to initialize device (%d).", ret); return false; } - enum AVSampleFormat avfmt = av_sample_fmt_from_miniaudio(ma->device.playback.internalFormat); - const char *format_name = av_get_sample_fmt_name(avfmt); + s32 format = camu_sample_format_from_miniaudio(ma->device.playback.internalFormat); + const char *format_name = camu_audio_format_name(format); al_log_info("audio_miniaudio", "Output: %s (%dch) %dHz.", format_name, ma->device.playback.internalChannels, ma->device.playback.internalSampleRate); return true; @@ -187,11 +203,14 @@ static void audio_miniaudio_free(struct camu_audio **audio) ma_device_uninit(&ma->device); ma_context_uninit(&ma->context); al_free((void *)ma->context_config.pulse.pApplicationName); + al_free(ma); *audio = NULL; } -struct camu_audio_miniaudio audio_plugin_miniaudio = { - .a = { +struct camu_audio *camu_audio_miniaudio_create(void) +{ + struct camu_audio_miniaudio *ma = al_alloc_object(struct camu_audio_miniaudio); + ma->a = (struct camu_audio){ .init = audio_miniaudio_init, .pick_format = audio_miniaudio_pick_format, .configure_stream = audio_miniaudio_configure_stream, @@ -201,13 +220,6 @@ struct camu_audio_miniaudio audio_plugin_miniaudio = { .free = audio_miniaudio_free, .data_callback = NULL, .userdata = NULL - }, - .log = { }, - .context_config = { 0 }, - .context = { }, - .infos = NULL, - .info_count = 0, - .selected_device = NULL, - .config = { 0 }, - .device = { 0 } -}; + }; + return (struct camu_audio *)ma; +} diff --git a/src/mixer/audio_miniaudio.h b/src/mixer/audio_miniaudio.h index 3d7f010..d93c69c 100644 --- a/src/mixer/audio_miniaudio.h +++ b/src/mixer/audio_miniaudio.h @@ -1,5 +1,9 @@ #pragma once +// Include <aki/thread.h> here to ensure OS related headers +// get included the way we want. +// Specifically, on Windows we want to include <winsock2.h> before <windows.h>. +#include <aki/thread.h> #include <miniaudio.h> #include "audio.h" @@ -16,4 +20,4 @@ struct camu_audio_miniaudio { ma_device device; }; -extern struct camu_audio_miniaudio audio_plugin_miniaudio; +struct camu_audio *camu_audio_miniaudio_create(void); diff --git a/src/mixer/audio_null.c b/src/mixer/audio_null.c new file mode 100644 index 0000000..ac2846a --- /dev/null +++ b/src/mixer/audio_null.c @@ -0,0 +1,67 @@ +#include "audio_null.h" + +static bool audio_null_init(struct camu_audio *audio, str *name) +{ + struct camu_audio_null *na = (struct camu_audio_null *)audio; + (void)na; + (void)name; + return true; +} + +static void audio_null_pick_format(struct camu_audio *audio, struct camu_resampler_format *fmt) +{ + struct camu_audio_null *na = (struct camu_audio_null *)audio; + (void)na; + fmt->req.format = fmt->in.format; + fmt->req.channel_count = fmt->in.channel_count; + av_channel_layout_default(&fmt->req.channel_layout, fmt->in.channel_count); + fmt->req.sample_rate = fmt->in.sample_rate; +} + +static bool audio_null_configure_stream(struct camu_audio *audio, void *opaque) +{ + struct camu_audio_null *na = (struct camu_audio_null *)audio; + (void)na; + (void)opaque; + return true; +} + +static u64 audio_null_get_latency(struct camu_audio *audio) +{ + struct camu_audio_null *na = (struct camu_audio_null *)audio; + (void)na; + return 0; +} + +static void audio_null_start(struct camu_audio *audio) +{ + struct camu_audio_null *na = (struct camu_audio_null *)audio; + (void)na; +} + +static void audio_null_stop(struct camu_audio *audio) +{ + struct camu_audio_null *na = (struct camu_audio_null *)audio; + (void)na; +} + +static void audio_null_free(struct camu_audio **audio) +{ + struct camu_audio_null *na = (struct camu_audio_null *)*audio; + (void)na; + *audio = NULL; +} + +struct camu_audio_null audio_plugin_null = { + .a = { + .init = audio_null_init, + .pick_format = audio_null_pick_format, + .configure_stream = audio_null_configure_stream, + .get_latency = audio_null_get_latency, + .start = audio_null_start, + .stop = audio_null_stop, + .free = audio_null_free, + .data_callback = NULL, + .userdata = NULL + } +}; diff --git a/src/mixer/audio_null.h b/src/mixer/audio_null.h new file mode 100644 index 0000000..b69cded --- /dev/null +++ b/src/mixer/audio_null.h @@ -0,0 +1,7 @@ +#include "audio.h" + +struct camu_audio_null { + struct camu_audio a; +}; + +extern struct camu_audio_null audio_plugin_null; diff --git a/src/mixer/meson.build b/src/mixer/meson.build index 71c003b..ea9356c 100644 --- a/src/mixer/meson.build +++ b/src/mixer/meson.build @@ -1,27 +1,32 @@ mixer_src = ['mixer.c'] mixer_deps = [common_deps] +mixer_args = [] -miniaudio = subproject('miniaudio').get_variable('miniaudio') -mixer_src += ['audio_miniaudio.c'] -mixer_deps += [miniaudio] - -miniaudio_args = [ - '-DMA_ENABLE_ONLY_SPECIFIC_BACKENDS', - '-DMA_NO_DECODING', - '-DMA_NO_ENCODING', - '-DMA_NO_RESOURCE_MANAGER', - '-DMA_NO_NODE_GRAPH', - '-DMA_NO_ENGINE', - '-DMA_NO_GENERATION', -] - -if not is_windows - libpulse = dependency('libpulse') - mixer_deps += [libpulse] - miniaudio_args += ['-DMA_ENABLE_PULSEAUDIO', '-DMA_NO_RUNTIME_LINKING'] +use_miniaudio = true +if use_miniaudio + miniaudio = subproject('miniaudio').get_variable('miniaudio') + mixer_src += ['audio_miniaudio.c'] + mixer_deps += [miniaudio] + mixer_args += [ + '-DMA_ENABLE_ONLY_SPECIFIC_BACKENDS', + '-DMA_NO_DECODING', + '-DMA_NO_ENCODING', + '-DMA_NO_RESOURCE_MANAGER', + '-DMA_NO_NODE_GRAPH', + '-DMA_NO_ENGINE', + '-DMA_NO_GENERATION', + ] + if not is_windows + libpulse = dependency('libpulse') + mixer_deps += [libpulse] + mixer_args += ['-DMA_ENABLE_PULSEAUDIO', '-DMA_NO_RUNTIME_LINKING'] + else + mixer_args += ['-DMA_ENABLE_WASAPI'] + #mixer_args += ['-DMA_ENABLE_DSOUND'] + endif else - miniaudio_args += ['-DMA_ENABLE_WASAPI'] + mixer_src += ['audio_null.c'] endif mixer = declare_dependency(sources: mixer_src, dependencies: mixer_deps, - compile_args: miniaudio_args) + compile_args: mixer_args) diff --git a/src/mixer/mixer.c b/src/mixer/mixer.c index 5caee1b..987bc8b 100644 --- a/src/mixer/mixer.c +++ b/src/mixer/mixer.c @@ -3,29 +3,43 @@ #include "mixer.h" #include "audio.h" +// Should an initial read inside of start() be marked as silence or not. +#define MIXER_WANT_INITIAL_SILENCE 1 + +// Number of silent frames to append before signaling MIXER_EMPTY. +#define MIXER_TRAILING_SILENCE 100 + static s32 data_callback(void *userdata, u8 *data, s32 frame_count, bool *silence) { struct camu_mixer *mixer = (struct camu_mixer *)userdata; - size_t req = camu_ff_resample_fmt_samples_to_bytes(&mixer->fmt, (size_t)frame_count); + size_t req = camu_audio_format_samples_to_bytes(&mixer->fmt.req, (size_t)frame_count); if (UNLIKELY(mixer->paused)) { al_memset(data, 0, req); - *silence = true; + *silence = MIXER_WANT_INITIAL_SILENCE; } else { #ifdef CAMU_MIXER_THREADED - if (al_atomic_bool_load(&mixer->queued, AL_ATOMIC_RELAXED)) { + if (al_atomic_load(u8)(&mixer->queued, AL_ATOMIC_RELAXED)) { camu_mixer_run_queue(mixer); } #endif + if (UNLIKELY(mixer->empty_after > 0)) { + if (--mixer->empty_after == 0) { + mixer->callback(mixer->userdata, CAMU_MIXER_EMPTY); + } + *silence = false; + } else { + *silence = true; + } do { - if (mixer->buffers.size == 0) { + if (al_array_is_empty(mixer->buffers)) { al_memset(data, 0, req); - *silence = true; } else { - struct camu_audio_buffer *buf = al_array_last(mixer->buffers); size_t signal; + struct camu_audio_buffer *buf = al_array_last(mixer->buffers); + *silence = false; if ((signal = camu_audio_buffer_read(buf, data, req)) < req) { #ifdef CAMU_MIXER_THREADED - if (al_atomic_bool_load(&mixer->queued, AL_ATOMIC_RELAXED)) { + if (al_atomic_load(u8)(&mixer->queued, AL_ATOMIC_RELAXED)) { camu_mixer_run_queue(mixer); } #endif @@ -33,7 +47,6 @@ static s32 data_callback(void *userdata, u8 *data, s32 frame_count, bool *silenc req = req - signal; continue; } - *silence = false; } break; } while (1); @@ -46,23 +59,20 @@ bool camu_mixer_init(struct camu_mixer *mixer, struct camu_audio *audio) mixer->audio = audio; mixer->audio->data_callback = data_callback; mixer->audio->userdata = mixer; - mixer->audio->init(mixer->audio, al_str_c("camu")); - if (!mixer->audio->configure_stream(mixer->audio, NULL)) { - return false; - } - camu_mixer_pick_format(mixer, &mixer->fmt); + mixer->audio->init(mixer->audio, al_str_c("cmv")); mixer->paused = true; + mixer->empty_after = 0; al_array_init(mixer->buffers); #ifdef CAMU_MIXER_THREADED al_array_init(mixer->add_queue); al_array_init(mixer->rem_queue); - al_atomic_bool_store(&mixer->queued, false, AL_ATOMIC_RELAXED); + al_atomic_store(u8)(&mixer->queued, 0, AL_ATOMIC_RELAXED); aki_mutex_init(&mixer->mutex); #endif return true; } -void camu_mixer_pick_format(struct camu_mixer *mixer, struct camu_ff_resample_fmt *fmt) +void camu_mixer_pick_format(struct camu_mixer *mixer, struct camu_resampler_format *fmt) { mixer->audio->pick_format(mixer->audio, fmt); } @@ -75,7 +85,7 @@ f64 camu_mixer_get_latency(struct camu_mixer *mixer) static void add_buffer_internal(struct camu_mixer *mixer, struct camu_audio_buffer *buf) { #ifdef CAMU_MIXER_THREADED - al_atomic_bool_store(&buf->ref, true, AL_ATOMIC_RELAXED); + al_atomic_store(u8)(&buf->ref, 1, AL_ATOMIC_RELAXED); #endif al_array_push(mixer->buffers, buf); } @@ -86,26 +96,14 @@ static void remove_buffer_internal(struct camu_mixer *mixer, struct camu_audio_b al_array_foreach(mixer->buffers, i, cbuf) { if (cbuf == buf) { #ifdef CAMU_MIXER_THREADED - al_atomic_bool_store(&buf->ref, false, AL_ATOMIC_RELAXED); + al_atomic_store(u8)(&buf->ref, 0, AL_ATOMIC_RELAXED); #endif - al_array_remove_at_iter(mixer->buffers, i); + al_array_remove_at(mixer->buffers, i); break; } } } -void camu_mixer_add_buffer(struct camu_mixer *mixer, struct camu_audio_buffer *buf) -{ -#ifdef CAMU_MIXER_THREADED - aki_mutex_lock(&mixer->mutex); - al_array_push(mixer->add_queue, buf); - al_atomic_bool_store(&mixer->queued, true, AL_ATOMIC_RELAXED); - aki_mutex_unlock(&mixer->mutex); -#else - add_buffer_internal(mixer, buf); -#endif -} - #ifdef CAMU_MIXER_THREADED static void run_queue_internal(struct camu_mixer *mixer) { @@ -119,22 +117,43 @@ static void run_queue_internal(struct camu_mixer *mixer) remove_buffer_internal(mixer, buf); } mixer->rem_queue.size = 0; - al_atomic_bool_store(&mixer->queued, false, AL_ATOMIC_RELAXED); + al_atomic_store(u8)(&mixer->queued, 0, AL_ATOMIC_RELAXED); + if (al_array_is_empty(mixer->buffers)) { + mixer->empty_after = MIXER_TRAILING_SILENCE; + } else { + mixer->empty_after = 0; + } } #endif +void camu_mixer_add_buffer(struct camu_mixer *mixer, struct camu_audio_buffer *buf) +{ +#ifdef CAMU_MIXER_THREADED + aki_mutex_lock(&mixer->mutex); + al_array_push(mixer->add_queue, buf); + al_atomic_store(u8)(&mixer->queued, 1, AL_ATOMIC_RELAXED); + aki_mutex_unlock(&mixer->mutex); +#else + add_buffer_internal(mixer, buf); + mixer->empty_after = 0; +#endif +} + void camu_mixer_remove_buffer(struct camu_mixer *mixer, struct camu_audio_buffer *buf) { #ifdef CAMU_MIXER_THREADED aki_mutex_lock(&mixer->mutex); al_array_push(mixer->rem_queue, buf); - al_atomic_bool_store(&mixer->queued, true, AL_ATOMIC_RELAXED); + al_atomic_store(u8)(&mixer->queued, 1, AL_ATOMIC_RELAXED); if (mixer->paused) { run_queue_internal(mixer); } aki_mutex_unlock(&mixer->mutex); #else remove_buffer_internal(mixer, buf); + if (al_array_is_empty(mixer->buffers)) { + mixer->empty_after = MIXER_TRAILING_SILENCE; + } #endif } @@ -167,17 +186,11 @@ void camu_mixer_resume(struct camu_mixer *mixer) aki_mutex_lock(&mixer->mutex); #endif if (mixer->paused) { -//#ifdef CAMU_MIXER_THREADED -// bool queued = al_atomic_bool_load(&mixer->queued, AL_ATOMIC_RELAXED); -// // Disable queue when starting audio in case start() directly calls data_callback(). -// // Locking here is necessary to 100% know the state of the audio backend at -// // any time. This allows us to act on the value of paused, like in remove_buffer(). -// if (queued) al_atomic_bool_store(&mixer->queued, false, AL_ATOMIC_RELAXED); -//#endif + // start() can internally call data_callback once before returning. + // In that call mixer->paused will still be true. So, we have a special case to + // immediately return silence and avoid any possible locking. + // Outputs can treat that silence as part of the stream with MIXER_WANT_INITIAL_SILENCE. mixer->audio->start(mixer->audio); -//#ifdef CAMU_MIXER_THREADED -// if (queued) al_atomic_bool_store(&mixer->queued, true, AL_ATOMIC_RELAXED); -//#endif mixer->paused = false; } #ifdef CAMU_MIXER_THREADED @@ -185,6 +198,18 @@ void camu_mixer_resume(struct camu_mixer *mixer) #endif } +void camu_mixer_clear(struct camu_mixer *mixer) +{ +#ifdef CAMU_MIXER_THREADED + run_queue_internal(mixer); + struct camu_audio_buffer *buf; + al_array_foreach(mixer->buffers, i, buf) { + al_atomic_store(u8)(&buf->ref, 0, AL_ATOMIC_RELAXED); + } +#endif + mixer->buffers.size = 0; +} + void camu_mixer_close(struct camu_mixer *mixer) { al_array_free(mixer->buffers); diff --git a/src/mixer/mixer.h b/src/mixer/mixer.h index 113db62..729657f 100644 --- a/src/mixer/mixer.h +++ b/src/mixer/mixer.h @@ -9,23 +9,30 @@ #include <aki/thread.h> #endif -#include "../codec/ffmpeg/resampler.h" +#include "../codec/codec.h" + +enum { + CAMU_MIXER_EMPTY = 0 +}; struct camu_mixer { struct camu_audio *audio; + struct camu_resampler_format fmt; bool paused; - struct camu_ff_resample_fmt fmt; + u8 empty_after; array(struct camu_audio_buffer *) buffers; #ifdef CAMU_MIXER_THREADED array(struct camu_audio_buffer *) add_queue; array(struct camu_audio_buffer *) rem_queue; - atomic_bool queued; + atomic(u8) queued; struct aki_mutex mutex; #endif + void (*callback)(void *, u8); + void *userdata; }; bool camu_mixer_init(struct camu_mixer *mixer, struct camu_audio *audio); -void camu_mixer_pick_format(struct camu_mixer *mixer, struct camu_ff_resample_fmt *fmt); +void camu_mixer_pick_format(struct camu_mixer *mixer, struct camu_resampler_format *fmt); f64 camu_mixer_get_latency(struct camu_mixer *mixer); void camu_mixer_add_buffer(struct camu_mixer *mixer, struct camu_audio_buffer *buf); void camu_mixer_remove_buffer(struct camu_mixer *mixer, struct camu_audio_buffer *buf); @@ -34,4 +41,5 @@ void camu_mixer_run_queue(struct camu_mixer *mixer); #endif void camu_mixer_pause(struct camu_mixer *mixer); void camu_mixer_resume(struct camu_mixer *mixer); +void camu_mixer_clear(struct camu_mixer *mixer); void camu_mixer_close(struct camu_mixer *mixer); diff --git a/src/portal/cpy/build.sh b/src/portal/cpy/build.sh index 45a47f5..15ff4f5 100755 --- a/src/portal/cpy/build.sh +++ b/src/portal/cpy/build.sh @@ -1,3 +1,3 @@ #! /usr/bin/env sh export PYTHONDONTWRITEBYTECODE=1 -python3.12 setup.py build_ext -i +python3 setup.py build_ext -i diff --git a/src/portal/cpy/portal.pyx b/src/portal/cpy/portal.pyx index 3ddde80..ec33ac6 100644 --- a/src/portal/cpy/portal.pyx +++ b/src/portal/cpy/portal.pyx @@ -24,7 +24,7 @@ def encode_str(str): def decode_str(str): return str.decode('UTF-8') -class LogOutputHook(object): +class OutputHook(object): def __init__(self): self.fd = sys.__stdout__ @@ -35,7 +35,7 @@ class LogOutputHook(object): if len(msg) > 0 and msg != '\n': log_info(encode_str(msg)) -hook = LogOutputHook() +hook = OutputHook() sys.stdout = hook sys.stderr = hook @@ -147,8 +147,8 @@ cdef public int add_post_internal(bridge.camu_search *search, int id, unsigned i bridge.camu_search_add_post(search, num, &cpost) return 0 py_str_to_str(&cpost.url, encode_str(py_post.url)) - for type, date in py_post.dates.items(): - post.camu_post_add_date(&cpost, type.value, date) + for date in py_post.dates: + post.camu_post_add_date(&cpost, date.type, date.range[0], date.range[1], date.meta) py_str_to_str(&cpost.author.unique_id, encode_str(py_post.author.unique_id)) py_str_to_wstr(&cpost.author.username, encode_str(py_post.author.username)) py_str_to_wstr(&cpost.author.display_name, encode_str(py_post.author.display_name)) diff --git a/src/portal/cpy/post.pxd b/src/portal/cpy/post.pxd index 3f66508..aae9839 100644 --- a/src/portal/cpy/post.pxd +++ b/src/portal/cpy/post.pxd @@ -7,7 +7,9 @@ cdef extern from "../src/post.h": cdef struct camu_post_date: u8 type - f32 timestamp + f32 range_start + f32 range_end + u32 meta ctypedef struct camu_dates_array: u32 size @@ -57,5 +59,5 @@ cdef extern from "../src/post.h": camu_post_ref in_reply_to void camu_post_reset(camu_post *post) - void camu_post_add_date(camu_post *post, u8 type, f32 timestamp) + void camu_post_add_date(camu_post *post, u8 type, f32 range_start, f32 range_end, u32 meta) void camu_post_add_media(camu_post *post, u8 type, str *key, str *url, str *ext, str *thumbnail_url, str *thumbnail_ext) diff --git a/src/portal/meson.build b/src/portal/meson.build index 0df4366..c3bbeb4 100644 --- a/src/portal/meson.build +++ b/src/portal/meson.build @@ -5,8 +5,10 @@ portal_src = [ 'src/packet_ext.c' ] portal_deps = [] +portal_args = ['-DCAMU_HAVE_PORTAL'] python3_embed = import('python').find_installation('python3.12').dependency(embed: true) portal_deps += [python3_embed] -portal = declare_dependency(sources: portal_src, dependencies: portal_deps) +portal = declare_dependency(sources: portal_src, dependencies: portal_deps, + compile_args: portal_args) diff --git a/src/portal/patches/snscrape_auth.diff b/src/portal/patches/snscrape_auth.diff index 67962a9..efdc6e0 100644 --- a/src/portal/patches/snscrape_auth.diff +++ b/src/portal/patches/snscrape_auth.diff @@ -121,7 +121,7 @@ index d1719a8..43d9076 100644 r._snscrapeObj = obj if apiType is _TwitterAPIType.GRAPHQL and 'errors' in obj: msg = 'Twitter responded with an error: ' + ', '.join(f'{e["name"]}: {e["message"]}' for e in obj['errors']) -+ blockFor = 30.0 ++ blockFor = 10.0 + _logger.warn(f'Got errors waiting for {blockFor} seconds') + time.sleep(blockFor) + return False, msg diff --git a/src/portal/py/base.py b/src/portal/py/base.py index cc883af..7869521 100644 --- a/src/portal/py/base.py +++ b/src/portal/py/base.py @@ -1,21 +1,24 @@ import log import time +import json import httpx +import binascii import threading from enum import Enum +from blake3 import blake3 from collections import OrderedDict from typing import Optional, Self, Any from post import Post, User -USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; rv:122.0) Gecko/20100101 Firefox/122.0' +type ParsedJson = Any + +USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; rv:131.0) Gecko/20100101 Firefox/131.0' class Method(Enum): HEAD = "HEAD" GET = "GET" POST = "POST" -type ParsedJson = Any - class Search(): def __init__(self): self.pages: dict[int, list[str]] = {} @@ -59,8 +62,9 @@ class Module(): def __init__(self): self.headers: dict[str, str] = {} self.cookies: dict[str, str] = {} + self.mutex: threading.Lock = threading.Lock() + self.raw_responses: dict[str, str] = {} self.unique_id_map: OrderedDict = OrderedDict() - self.map_mutex: threading.Lock = threading.Lock() self.session: httpx.Client = httpx.Client(follow_redirects=True, timeout=20.0, http2=True) def init(self) -> bool: @@ -84,6 +88,7 @@ class Module(): response = None continue if response.status_code == 404 or response.status_code == 403: # Shortcut 404, 403 + log.warn(f'Returning None, {response.status_code}') return None if int(response.status_code / 100) != 2: # Non-200 response log.error(f'Non-200 status code ({response.status_code}), retrying in 5 seconds...') @@ -101,8 +106,16 @@ class Module(): break return response + def add_raw_response(self, data: ParsedJson) -> str: + with self.mutex: + dump = json.dumps(data) + hash = blake3(dump.encode('utf-8')).digest() + hash_str = binascii.hexlify(hash).decode('ascii') + self.raw_responses[hash_str] = dump + return hash_str + def add_to_map(self, unique_id: str, item: Post | User): - with self.map_mutex: + with self.mutex: self.unique_id_map[unique_id] = item if len(self.unique_id_map) > 10240: self.unique_id_map.popitem(last=False) @@ -111,7 +124,7 @@ class Module(): return None def get_item(self, unique_id: str) -> Optional[Post | User]: - with self.map_mutex: + with self.mutex: return self.unique_id_map.get(unique_id, None) def get_download(self, unique_id: str, key: str) -> Optional[dict[str, Any]]: diff --git a/src/portal/py/modules/__init__.py b/src/portal/py/modules/__init__.py index 994b25a..1a9e9ca 100644 --- a/src/portal/py/modules/__init__.py +++ b/src/portal/py/modules/__init__.py @@ -1,11 +1,11 @@ import config from modules.youtube import YoutubeModule -from modules.twitter import TwitterScrapeModule -from modules.pixiv_app import PixivAppModule -from modules.pixiv_web import PixivWebModule +#from modules.twitter import TwitterScrapeModule +#from modules.pixiv_app import PixivAppModule +#from modules.pixiv_web import PixivWebModule from modules.fanbox import FanboxModule -from modules.instagram import InstagramModule +#from modules.instagram import InstagramModule from modules.patreon import PatreonModule ALL_MODULES = { @@ -13,12 +13,12 @@ ALL_MODULES = { # 'twitter': (TwitterModule( # config.TWITTER_ACCESS_TOKEN, config.TWITTER_ACCESS_TOKEN_SECRET, # config.TWITTER_CONSUMER_TOKEN, config.TWITTER_CONSUMER_TOKEN_SECRET), []), - 'twitter': (TwitterScrapeModule(config.TWITTER_COOKIES_PATH), []), - 'pixiv_web': (PixivWebModule(config.PIXIV_SESSID, config.PIXIV_USERID), []), +# 'twitter': (TwitterScrapeModule(config.TWITTER_COOKIES_PATH), []), +# 'pixiv_web': (PixivWebModule(config.PIXIV_SESSID, config.PIXIV_USERID), []), # 'pixiv_app': (PixivAppModule(config.PIXIV_REFRESH_TOKEN), []), # 'fanbox': (FanboxModule(config.FANBOX_SESSID), []), # 'instagram': (InstagramModule( # config.INSTAGRAM_USER_AGENT, config.INSTAGRAM_SETTINGS_PATH, -# config.INSTAGRAM_SESSION_ID), []) - 'patreon': (PatreonModule(config.PATREON_SESSION_ID, config.PATREON_UUID, config.PATREON_USER_ID), []) +# config.INSTAGRAM_SESSION_ID), []), +# 'patreon': (PatreonModule(config.PATREON_SESSION_ID, config.PATREON_UUID, config.PATREON_USER_ID), []) } diff --git a/src/portal/py/modules/common.py b/src/portal/py/modules/common.py index 62c3d5d..3b87884 100644 --- a/src/portal/py/modules/common.py +++ b/src/portal/py/modules/common.py @@ -1,6 +1,6 @@ from datetime import datetime, timezone -def parse_pixiv_date(date_str: str) -> datetime: +def reformat_pixiv_date(date_str: str) -> datetime: rindex = date_str.rfind(':') date_str = date_str[:rindex] + date_str[rindex + 1:] return datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S%z") diff --git a/src/portal/py/modules/fanbox.py b/src/portal/py/modules/fanbox.py index 4323d6b..bf55b9d 100644 --- a/src/portal/py/modules/fanbox.py +++ b/src/portal/py/modules/fanbox.py @@ -3,9 +3,9 @@ import httpx from typing import Optional, Any from json.decoder import JSONDecodeError from base import USER_AGENT, Search, Module, Method, ParsedJson -from post import MediaUrl, PostType, DateType, Post, User, Image, File, Tag +from post import Url, PostType, DateType, Post, User, Image, File, Tag from query_parser import QueryParser -from modules.common import parse_pixiv_date, get_current_utc_time +from modules.common import reformat_pixiv_date, get_current_utc_time BASE_URL = "https://api.fanbox.cc" @@ -23,8 +23,8 @@ class FanboxBase(Search): return None return obj['body'] - def create_media_url(self, url: str) -> MediaUrl: - return MediaUrl(url, url.split('.')[-1]) + def create_media_url(self, url: str) -> Url: + return Url(url, url.split('.')[-1]) def create_post(self, data: ParsedJson) -> Optional[Post]: kwargs = {} @@ -33,12 +33,12 @@ class FanboxBase(Search): kwargs['unique_id'] = unique_id kwargs['raw_responses'] = {} kwargs['raw_responses']['api'] = json.dumps(data) - publish_date = parse_pixiv_date(data['publishedDatetime']).timestamp() + publish_date = reformat_pixiv_date(data['publishedDatetime']).timestamp() kwargs['dates'] = { DateType.CREATED: publish_date, DateType.RETRIEVED: get_current_utc_time().timestamp() } - update_date = parse_pixiv_date(data['updatedDatetime']).timestamp() + update_date = reformat_pixiv_date(data['updatedDatetime']).timestamp() if update_date != publish_date: kwargs['dates'][DateType.EDITED] = update_date user = User(unique_id=f'fanbox:u:{data['user']['userId']}', username=data['user']['name']) @@ -57,11 +57,11 @@ class FanboxBase(Search): if data['type'] == 'image': text = data['body']['text'] for i, value in enumerate(data['body']['images']): - media[str(i)] = Image(url=MediaUrl(value['originalUrl'], value['extension']), thumbnail_url=self.create_media_url(value['thumbnailUrl'])) + media[str(i)] = Image(url=Url(value['originalUrl'], value['extension']), thumbnail_url=self.create_media_url(value['thumbnailUrl'])) elif data['type'] == 'file': text = data['body']['text'] for i, value in enumerate(data['body']['files']): - media[str(i)] = File(url=MediaUrl(value['url'], value['extension']), name=value['name']) + media[str(i)] = File(url=Url(value['url'], value['extension']), name=value['name']) elif data['type'] == 'article': for block in data['body']['blocks']: if block['type'] == 'p': @@ -71,9 +71,9 @@ class FanboxBase(Search): elif block['type'] == 'url_embed': text += f'embed::{block['urlEmbedId']}[]' + '\n' for key, value in data['body']['imageMap'].items(): - media[key] = Image(url=MediaUrl(value['originalUrl'], value['extension']), thumbnail_url=self.create_media_url(value['thumbnailUrl'])) + media[key] = Image(url=Url(value['originalUrl'], value['extension']), thumbnail_url=self.create_media_url(value['thumbnailUrl'])) for key, value in data['body']['fileMap'].items(): - media[key] = File(url=MediaUrl(value['url'], value['extension']), name=value['name']) + media[key] = File(url=Url(value['url'], value['extension']), name=value['name']) kwargs['text'] = text kwargs['media'] = media post = Post(**kwargs) @@ -138,7 +138,7 @@ class FanboxUser(FanboxBase): self.errored = True return False self.pages[num] = [] - for item in obj['items']: + for item in obj: post = self.request_post(item['id']) if post: self.pages[num].append(post.unique_id) @@ -169,12 +169,20 @@ class FanboxModule(Module): self.parser.add_command('post', FanboxPost) self.parser.add_command('user', FanboxUser) self.parser.add_command('supporting', FanboxSupporting) - self.headers['Accept-Encoding'] = 'gzip, deflate, br' + self.headers['Accept-Encoding'] = 'gzip, deflate, br, zstd' self.headers['Accept-Language'] = 'en-US,en;q=0.5' + self.headers['Alt-Used'] = 'api.fanbox.cc' self.headers['Origin'] = 'https://www.fanbox.cc' self.headers['Referer'] = 'https://www.fanbox.cc/' self.headers['User-Agent'] = USER_AGENT self.cookies['FANBOXSESSID'] = sessid + self.cookies['privacy_policy_agreement'] = '7' + self.cookies['privacy_policy_notification'] = '0' + self.cookies['p_ab_id'] = '0' + self.cookies['p_ab_id_2'] = '6' + self.cookies['p_ab_d_id'] = '251960935' + self.cookies['cf_clearance'] = '' + self.cookies['__cf_bm'] = '' def search(self, query: str, *extra_args: Any) -> Optional[Search]: return self.parser.parse_query(query) diff --git a/src/portal/py/modules/instagram.py b/src/portal/py/modules/instagram.py index d107566..28f7def 100644 --- a/src/portal/py/modules/instagram.py +++ b/src/portal/py/modules/instagram.py @@ -1,10 +1,9 @@ import os import log -import email.utils from typing import Optional, Any from pathlib import Path from base import USER_AGENT, Module, Search -from post import MediaUrl, PostType, DateType, Post, User, Image, Video +from post import Url, PostType, DateType, Post, User, Image, Video from modules.common import get_current_utc_time from instagrapi import Client from instagrapi.exceptions import UserNotFound, LoginRequired, ChallengeRequired @@ -19,13 +18,13 @@ class InstagramSearch(Search): self.page: int = 0 self.cursor: Any = None - def create_media_url(self, url: str) -> MediaUrl: + def create_media_url(self, url: str) -> Url: question = url.find('?') if question >= 0: ext = url[:question].split('.')[-1] else: ext = url.split('.')[-1] - return MediaUrl(url, ext) + return Url(url, ext) def request_page(self, num: int) -> bool: if num >= self.page + self.REACH_LIMIT: @@ -34,6 +33,7 @@ class InstagramSearch(Search): for i in range(self.page, num + 1): try: media, self.cursor = self.module.cl.user_medias_paginated_v1(self.pk, 0, end_cursor=self.cursor) + #media, self.cursor = self.module.cl.user_clips_paginated_v1(self.pk, amount=12, end_cursor=self.cursor) except LoginRequired: self.errored = True return False @@ -47,10 +47,12 @@ class InstagramSearch(Search): kwargs = {} kwargs['type'] = PostType.POST unique_id = 'instagram:p:{}'.format(m.id) + #unique_id = 'instagram:r:{}'.format(m.id) kwargs['unique_id'] = unique_id kwargs['raw_responses'] = {} kwargs['raw_responses']['tile'] = m.json() kwargs['url'] = 'https://www.instagram.com/p/{}'.format(m.code) + #kwargs['url'] = 'https://www.instagram.com/reel/{}'.format(m.code) kwargs['dates'] = { DateType.CREATED: m.taken_at.timestamp(), DateType.RETRIEVED: get_current_utc_time().timestamp() diff --git a/src/portal/py/modules/patreon.py b/src/portal/py/modules/patreon.py index 40c3255..f33ec18 100644 --- a/src/portal/py/modules/patreon.py +++ b/src/portal/py/modules/patreon.py @@ -1,9 +1,12 @@ -import json +import log import httpx +import sys from json import JSONDecodeError from typing import Optional, Any +from datetime import datetime from base import USER_AGENT, Search, Module, Method, ParsedJson -from post import MediaUrl, PostType, DateType, Post, User, Media, Image, Animation, Tag, TagType +from modules.common import get_current_utc_time +from post import Date, DateType, DateMeta, Post, User, Url, Media, File from query_parser import QueryParser BASE_URL = 'https://www.patreon.com/api' @@ -26,7 +29,7 @@ class PatreonBase(Search): return None return obj - def search_user(self, query: str) -> Optional[User]: + def search_user(self, query: str) -> tuple[Optional[User], Optional[str]]: url = f'{BASE_URL}/search' params = { 'q': query, @@ -37,32 +40,76 @@ class PatreonBase(Search): } obj = self.check_api_response(self.module.do_request(Method.GET, url, params=params)) if not obj: - return None + return None, None for result in obj['data']: + username = result['attributes']['creator_name'] + display_name = result['attributes']['name'] + # Only accept exact matches. + if username.lower() != query.lower() and display_name.lower() != query.lower(): + continue prefix = result['id'].find('campaign_') if prefix >= 0: campaign = result['id'][prefix + len('campaign_'):] unique_id = 'patreon:c:' + campaign - user = User(unique_id=unique_id, username=result['attributes']['creator_name'], display_name=result['attributes']['name']) - return user - return None + user = User(unique_id=unique_id, username=username, display_name=display_name) + return user, campaign + return None, None class PatreonUser(PatreonBase): def __init__(self, userdata: Any, arg: str): super().__init__(userdata) self.username: str = arg self.user: Optional[User] = None - self.campaign: str + self.campaign: Optional[str] = None + self.cursor: Optional[str] = None + self.attachments: dict[str, Media] = {} + + # post_type + # image_file: https://www.patreon.com/posts/emma-again-29391136 + # text_only: https://www.patreon.com/posts/hello-update-29391184 + + def create_post(self, data: ParsedJson, hash: str) -> Optional[Post]: + kwargs = {} + unique_id = f'patreon:p:{data['id']}' + kwargs['unique_id'] = unique_id + kwargs['raw_responses'] = {} + kwargs['raw_responses']['api'] = hash + kwargs['url'] = data['attributes']['url'] + published_at = datetime.strptime(data['attributes']['published_at'], "%Y-%m-%dT%H:%M:%S.%f%z").timestamp() + current_time = get_current_utc_time().timestamp() + kwargs['dates'] = [ + Date(DateType.CREATED, (published_at, published_at), DateMeta.NONE), + Date(DateType.RETRIEVED, (current_time, current_time), DateMeta.NONE), + ] + kwargs['author'] = self.user + kwargs['title'] = data['attributes']['title'] + kwargs['likes'] = data['attributes']['like_count'] + kwargs['comments'] = data['attributes']['comment_count'] + kwargs['text'] = data['attributes']['content'] + post_type = data['attributes']['post_type'] + media = {} + #if 'access_rules' in data['relationships']: + # if not (len(data['relationships']['access_rules']['data']) == 1 and data['relationships']['access_rules']['data'][0]['id'] == '9816126'): + # return None + if 'media' in data['relationships']: + for m in data['relationships']['media']['data']: + media[m['id']] = self.attachments[m['id']] + if 'attachments' in data['relationships']: + for a in data['relationships']['attachments']['data']: + media[a['id']] = self.attachments[a['id']] + kwargs['media'] = media + post = Post(**kwargs) + self.module.add_to_map(unique_id, post) + return post def request_page(self, num: int) -> bool: if num >= self.page + self.REACH_LIMIT: return False if not self.user: - user = self.search_user(self.username) - if not user: + self.user, self.campaign = self.search_user(self.username) + if not self.user: self.errored = True return False - self.campaign = user.unique_id[len('patreon:c:'):] request_satisfied = False for i in range(self.page, num + 1): url = f'{BASE_URL}/posts' @@ -81,7 +128,7 @@ class PatreonUser(PatreonBase): 'filter[contains_exclusive_posts]': 'true', 'filter[is_draft]': 'false', 'filter[accessible_by_user_id]': self.module.user_id, - 'sort': '-published_at', + 'sort': 'published_at', 'json-api-version': '1.0', 'json-api-use-default-includes': 'false' } @@ -91,8 +138,27 @@ class PatreonUser(PatreonBase): if not obj: self.errored = True return False - print(json.dumps(obj, indent=4)) - return True + hash = self.module.add_raw_response(obj) + self.cursor = obj['meta']['pagination']['cursors']['next'] + self.pages[i] = [] + for inc in obj['included']: + if inc['type'] == 'attachment': + self.attachments[inc['id']] = File(url=Url(inc['attributes']['url']), name=inc['attributes']['name']) + elif inc['type'] == 'media': + self.attachments[inc['id']] = Media(url=Url(inc['attributes']['download_url']), thumbnail_url=Url(inc['attributes']['image_urls']['thumbnail'])) + else: + log.error(f'Unhandled inc type {inc['type']}.') + for entry in obj['data']: + if entry['type'] != 'post': + log.error(f'Unhandled entry type {entry['type']}.') + continue + post = self.create_post(entry, hash) + if post: + self.pages[i].append(post.unique_id) + self.page += 1 + if i == num: + request_satisfied = True + return request_satisfied class PatreonModule(Module): def __init__(self, session_id: str, uuid: str, user_id: str): @@ -100,7 +166,8 @@ class PatreonModule(Module): self.user_id: str = user_id self.parser: QueryParser = QueryParser(self, 'user') self.parser.add_command('user', PatreonUser) - self.headers['Accept-Encoding'] = 'gzip, deflate, br' + self.headers['User-Agent'] = USER_AGENT + self.headers['Accept-Encoding'] = 'gzip, deflate, br, zstd' self.headers['Accept-Language'] = 'en-US,en;q=0.5' self.headers['x-patreon-uuid'] = uuid self.cookies['patreon_locale_code'] = 'en-US' @@ -116,9 +183,4 @@ class PatreonModule(Module): post = self.unique_id_map[unique_id] if key not in post.media: return None - return { - 'urls': [post.media[key].url], - 'headers': { - 'User-Agent': USER_AGENT - } - } + return { 'urls': [post.media[key].url], 'headers': self.headers, 'cookies': self.cookies } diff --git a/src/portal/py/modules/pixiv_app.py b/src/portal/py/modules/pixiv_app.py index c84030d..ef5886d 100644 --- a/src/portal/py/modules/pixiv_app.py +++ b/src/portal/py/modules/pixiv_app.py @@ -1,10 +1,10 @@ import json from typing import Optional, Any from base import USER_AGENT, Search, Module, ParsedJson -from post import MediaUrl, PostType, DateType, User, Post, Image, Tag, TagType +from post import Url, PostType, DateType, User, Post, Image, Tag, TagType from query_parser import QueryParser -from pixivpy3 import * -from modules.common import get_current_utc_time, parse_pixiv_date +from pixivpy3 import AppPixivAPI +from modules.common import get_current_utc_time, reformat_pixiv_date class PixivAppBase(Search): def __init__(self, userdata: Any): @@ -33,8 +33,8 @@ class PixivAppBase(Search): self.page += 1 return request_satisfied - def create_media_url(self, url: str) -> MediaUrl: - return MediaUrl(url, url.split('.')[-1]) + def create_media_url(self, url: str) -> Url: + return Url(url, url.split('.')[-1]) def create_tag(self, data: ParsedJson) -> Tag: tag = Tag(type=TagType.GENERAL, name=data['name']) @@ -58,7 +58,7 @@ class PixivAppBase(Search): kwargs['raw_responses']['api'] = json.dumps(data) kwargs['url'] = 'https://www.pixiv.net/en/artworks/{}'.format(data['id']) kwargs['dates'] = { - DateType.CREATED: parse_pixiv_date(data['create_date']).timestamp(), + DateType.CREATED: reformat_pixiv_date(data['create_date']).timestamp(), DateType.RETRIEVED: get_current_utc_time().timestamp() } user = User(unique_id=f'pixiv:u:{data['user']['id']}', username=data['user']['account'], display_name=data['user']['name']) diff --git a/src/portal/py/modules/pixiv_web.py b/src/portal/py/modules/pixiv_web.py index be68b7e..7dbb8bb 100644 --- a/src/portal/py/modules/pixiv_web.py +++ b/src/portal/py/modules/pixiv_web.py @@ -5,9 +5,9 @@ import urllib.parse from typing import Optional, Any from json.decoder import JSONDecodeError from base import USER_AGENT, Search, Module, Method, ParsedJson -from post import MediaUrl, PostType, DateType, Post, User, Media, Image, Animation, Tag, TagType +from post import Url, PostType, DateType, Post, User, Media, Image, Animation, Tag, TagType from query_parser import QueryParser -from modules.common import parse_pixiv_date, get_current_utc_time +from modules.common import reformat_pixiv_date, get_current_utc_time BASE_URL = 'https://www.pixiv.net/ajax' LANG = 'en' @@ -30,8 +30,8 @@ class PixivWebBase(Search): return None return obj['body'] - def create_media_url(self, url: str) -> MediaUrl: - return MediaUrl(url, url.split('.')[-1]) + def create_media_url(self, url: str) -> Url: + return Url(url, url.split('.')[-1]) def parse_pages(self, data: ParsedJson) -> dict[str, Media]: media = {} @@ -45,7 +45,7 @@ class PixivWebBase(Search): animation.frames.append((frame['file'], frame['delay'])) return animation - def seek_profile_picture_url(self, user_illusts: ParsedJson) -> Optional[MediaUrl]: + def seek_profile_picture_url(self, user_illusts: ParsedJson) -> Optional[Url]: for illust in user_illusts.values(): if illust is None: continue @@ -71,14 +71,15 @@ class PixivWebBase(Search): kwargs['raw_responses'] = {} kwargs['raw_responses']['api'] = json.dumps(data) kwargs['url'] = f'https://www.pixiv.net/{LANG}/artworks/{data['illustId']}' - create_date = parse_pixiv_date(data['createDate']).timestamp() + create_date = reformat_pixiv_date(data['createDate']).timestamp() + current_date = get_current_utc_time().timestamp() kwargs['dates'] = { - DateType.CREATED: create_date, - DateType.RETRIEVED: get_current_utc_time().timestamp() + DateType.CREATED: (create_date, create_date, 0), + DateType.RETRIEVED: (current_date, current_date, 0) } - upload_date = parse_pixiv_date(data['uploadDate']).timestamp() + upload_date = reformat_pixiv_date(data['uploadDate']).timestamp() if upload_date != create_date: - kwargs['dates'][DateType.EDITED] = upload_date + kwargs['dates'][DateType.EDITED] = (upload_date, upload_date, 0) user = User(unique_id=f'pixiv:u:{data['userId']}', username=data['userAccount'], display_name=data['userName']) profile_picture_url = self.seek_profile_picture_url(data['userIllusts']) if profile_picture_url: @@ -242,10 +243,10 @@ class PixivWebModule(Module): self.parser.add_command('illust', PixivWebIllust) self.parser.add_command('user', PixivWebUser) self.parser.add_command('following', PixivWebFollowing) - self.headers['Accept-Encoding'] = 'gzip, deflate, br' # add 'zstd' when httpx update + self.headers['User-Agent'] = USER_AGENT + self.headers['Accept-Encoding'] = 'gzip, deflate, br, zstd' self.headers['Accept-Language'] = 'en-US,en;q=0.5' self.headers['Referer'] = 'https://www.pixiv.net/' - self.headers['User-Agent'] = USER_AGENT self.headers['x-user-id'] = user_id self.cookies['PHPSESSID'] = sessid diff --git a/src/portal/py/modules/twitter.py b/src/portal/py/modules/twitter.py index 2996d0e..386bc5b 100644 --- a/src/portal/py/modules/twitter.py +++ b/src/portal/py/modules/twitter.py @@ -3,7 +3,7 @@ import http.cookiejar from typing import Optional, Any, Iterator from datetime import timezone from base import USER_AGENT, Search, Module -from post import MediaUrl, PostType, DateType, PostRef, Post, User, Media, Image, Video +from post import Url, PostType, DateType, PostRef, Post, User, Media, Image, Video from query_parser import QueryParser from modules.common import get_current_utc_time @@ -26,7 +26,7 @@ class TwitterScrapeBase(Search): self.iterator: Iterator[Tweet | TweetRef | Tombstone] self.page: int = 0 - def create_media_url(self, url: str) -> MediaUrl: + def create_media_url(self, url: str) -> Url: format = url.find('format=') if format >= 0: ext = url[format + 7:format + 10] @@ -35,7 +35,7 @@ class TwitterScrapeBase(Search): if question >= 0: url = url[0:question] ext = url.split('.')[-1] - return MediaUrl(url, ext) + return Url(url, ext) def parse_media(self, data: list[twitter.Medium]) -> dict[str, Media]: media = {} diff --git a/src/portal/py/modules/youtube.py b/src/portal/py/modules/youtube.py index d14dfe8..f809ff6 100644 --- a/src/portal/py/modules/youtube.py +++ b/src/portal/py/modules/youtube.py @@ -1,7 +1,8 @@ import log +import json from typing import Optional, Any from base import Search, Module, ParsedJson -from post import MediaUrl, Post, PostType, Media, Video +from post import Url, Post, PostType, Media, Video from query_parser import QueryParser from yt_dlp import YoutubeDL @@ -25,65 +26,53 @@ ydl_opts = { 'quiet': False, 'logger': YDLLogger(), 'cachedir': False, -# 'cookiefile': '', + 'socket_timeout': 10 +# '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] +# https://github.com/yt-dlp/yt-dlp/issues/4103 - if 'formats' not in data: - if 'url' in data: - return data['url'] - return None +ydl = YoutubeDL(ydl_opts) - # Filter out hls temporarily. - data['formats'] = list(filter(lambda f: not f['protocol'].startswith('m3u8'), data['formats'])) +def get_playback_url(data, video=True) -> Optional[str]: + 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'] + result = data['formats'][-1]['url'] - # audio_ext? - has_audio = list(filter(lambda f: 'acodec' not in f or f['acodec'] != 'none', data['formats'])) + has_audio = list(filter(lambda f: + (f['acodec'] and f['acodec'] != 'none') or + (f['audio_ext'] and f['audio_ext'] != 'none') or + (f['abr']), 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 + data['formats'] = list(filter(lambda f: + (f['vcodec'] and f['vcodec'] != 'none') or + (f['video_ext'] and f['video_ext'] != 'none') or + (f['vbr']), has_audio)) 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 + data['formats'] = list(filter(lambda f: + (f['vcodec'] and f['vcodec'] == 'none') and + (f['video_ext'] and f['video_ext'] == 'none') and + (not f['vbr']), has_audio)) + + if len(data['formats']) > 0: + selection = max(data['formats'], key=lambda f: 0 if 'quality' not in f else f['quality']) + log.info(json.dumps(selection, indent=4)) + result = selection['url'] + else: + log.warn('Defaulting in Youtube get_playback_url().') - return url + return result class YoutubeBase(Search): def __init__(self, userdata: Any): super().__init__() self.module: YoutubeModule = userdata + self.guessed_index: int = 0 def get_info(self) -> Optional[ParsedJson]: return None @@ -92,11 +81,25 @@ class YoutubeBase(Search): info = self.get_info() if not info: return False - url = get_playback_url(info) + if 'entries' in info: + if len(info['entries']) == 0: + return False + if len(info['entries']) <= self.guessed_index: + self.guessed_index = 0 + info = info['entries'][self.guessed_index] + if 'formats' not in info and 'url' in info: + self.link = info['url'] + info = self.get_info() + if not info: + return False + if 'direct' in info and info['direct']: + url = info['url'] + else: + url = get_playback_url(info, video=False) if not url: return False - media: dict[str, Media] = { '0': Video(url=MediaUrl(url=url)) } unique_id = f'youtube:v:{info['id']}' + media: dict[str, Media] = { '0': Video(url=Url(url)) } self.module.add_to_map(unique_id, Post(type=PostType.POST, unique_id=unique_id, url='', title=info['title'], text='', media=media)) self.pages[num] = [unique_id] return True @@ -107,7 +110,13 @@ class YoutubeSearch(YoutubeBase): self.query: str = arg def get_info(self) -> Optional[ParsedJson]: - return ydl.extract_info(f'ytsearch1:{self.query}', download=False) + ydl.params['extract_flat'] = False + try: + info = ydl.extract_info(f'ytsearch1:{self.query}', download=False) + except Exception as e: + log.error(repr(e)) + return None + return info class YoutubeLink(YoutubeBase): def __init__(self, userdata: Any, arg: str): @@ -115,7 +124,24 @@ class YoutubeLink(YoutubeBase): self.link: str = arg def get_info(self) -> Optional[ParsedJson]: - return ydl.extract_info(self.link, download=False) + index = self.link.find('&index=') + if index >= 0: + sub = self.link[index + 7:] + end = sub.find('&') + if end >= 0: + sub = sub[:end] + try: + self.guessed_index = int(sub, 10) - 1 + except ValueError: + pass + log.info(f'Guessed index: {self.guessed_index}') + ydl.params['extract_flat'] = 'in_playlist' + try: + info = ydl.extract_info(self.link, download=False) + except Exception as e: + log.error(repr(e)) + return None + return info class YoutubeModule(Module): def __init__(self): diff --git a/src/portal/py/post.py b/src/portal/py/post.py index 8149e4c..76e82b9 100644 --- a/src/portal/py/post.py +++ b/src/portal/py/post.py @@ -1,22 +1,38 @@ import dataclasses from dataclasses import field from typing import Optional, Any -from enum import Enum +from enum import IntEnum, IntFlag from json import JSONEncoder -class PostType(int, Enum): +''' +List of Things: + - Social media posts + - Albums + - Loose files +''' + +class PostType(IntEnum): UNKNOWN = 0 POST = 1 REPOST = 2 PREVIEW = 3 TOMBSTONE = 4 -class DateType(int, Enum): - CREATED = 0 - EDITED = 1 - RETRIEVED = 2 +class DateType(IntEnum): + UNKNOWN = 0 + CREATED = 1 + EDITED = 2 + RETRIEVED = 3 + +class DateMeta(IntFlag): + NONE = 0 + ESTIMATE = 1 + GUESS = 1 << 1 + LOOSE_PRECISION = 1 << 2 + EDITED_AT_UNKNOWN_TIME = 1 << 3 + ASSUMED_TYPE = 1 << 4 -class MediaType(int, Enum): +class MediaType(IntEnum): UNKNOWN = 0 FILE = 1 AUDIO = 2 @@ -25,7 +41,7 @@ class MediaType(int, Enum): VIDEO_SPLIT = 5 ANIMATION = 6 -class TagType(int, Enum): +class TagType(IntEnum): UNKNOWN = 0 GENERAL = 1 ARTIST = 2 @@ -37,15 +53,33 @@ class TagType(int, Enum): def df(c: Any) -> Any: return field(default_factory=lambda: c) +def default_url_ext_guess(url): + # TODO: List of known extensions. + question = url.rfind('?') + if question >= 0: + guess = url[0:question].rsplit('.')[-1] + else: + guess = url.rsplit('.')[-1] + return guess + @dataclasses.dataclass -class MediaUrl(): - url: str = '' - ext: str = 'unknown' +class Url(): + url: str + ext: str + + def __init__(self, url: str, ext: str = 'unknown'): + self.url = url + ext_guess = default_url_ext_guess(url) + if ext_guess: + self.ext = ext_guess + else: + self.ext = ext @dataclasses.dataclass class Media(): type: MediaType = MediaType.UNKNOWN - url: MediaUrl = df(MediaUrl()) + url: Url = df(Url('')) + thumbnail_url: Url = df(Url('')) @dataclasses.dataclass class File(Media): @@ -59,24 +93,20 @@ class Audio(Media): @dataclasses.dataclass class Image(Media): type: MediaType = MediaType.IMAGE - thumbnail_url: MediaUrl = df(MediaUrl()) @dataclasses.dataclass class Video(Media): type: MediaType = MediaType.VIDEO - thumbnail_url: MediaUrl = df(MediaUrl()) @dataclasses.dataclass class VideoSplit(Media): type: MediaType = MediaType.VIDEO_SPLIT - audio_url: MediaUrl = df(MediaUrl()) - subtitle_url: MediaUrl = df(MediaUrl()) - thumbnail_url: MediaUrl = df(MediaUrl()) + audio_url: Url = df(Url('')) + subtitle_url: Url = df(Url('')) @dataclasses.dataclass class Animation(Media): type: MediaType = MediaType.ANIMATION - thumbnail_url: MediaUrl = df(MediaUrl()) frames: list[tuple[str, int]] = df([]) @dataclasses.dataclass @@ -85,31 +115,59 @@ class Tag(): name: str = '' alts: dict[str, str] = df({}) +''' +Date(DateType.CREATED, (1396411200.0, 1396756800.0), DateMeta.ESTIMATE) + - Post is estimated to have been created at some point between April 2nd and 6th 2014. + +Date(DateType.EDITED, (1396411200.0, 1550247862.0), DateMeta.EDITED_AT_UNKNOWN_TIME) + - We have no clue when the post was edited, but it had to have been between the time it was created and now. + - EDITED_AT_UNKNOWN_TIME should be used when either of CREATED and/or RETRIEVED are used as placeholders. + It can be combined with GUESS or ESTIMATE if, for example, there is some indication about the end of the range. + +Date(DateType.RETRIEVED, (1550247862.0, 1550247862.0), DateMeta.NONE) + - Time of processing this post. Will likely be slightly after it was actually retrived but that is negligible. + - RETRIEVED can still have a range and meta if applicable. + +Date(DateType.CREATED, (1483228800.0, 1546300800.0), DateMeta.LOOSE_PRECISION) + - Date is exactly between "01 Jan 2017 12:00:00 AM UTC" and "01 Jan 2019 12:00:00 AM UTC". + Combined with LOOSE_PRECISION, the application will assume a meaning of "between 2017 and 2019". + +Date(DateType.CREATED, (1145059200.0, 1145059200.0), DateMeta.ASSUMED_TYPE) + - The date of April 15th 2006 is associated with the resource. We are assuming that's the creation date. +''' +@dataclasses.dataclass +class Date(): + type: DateType = DateType.UNKNOWN + range: tuple[float, float] = (0.0, 0.0) + meta: DateMeta = DateMeta.NONE + note: str = '' + @dataclasses.dataclass class User(): unique_id: str = '' username: str = '' display_name: str = '' - profile_picture_url: MediaUrl = df(MediaUrl()) + profile_picture_url: Url = df(Url('')) @dataclasses.dataclass class PostRef(): unique_id: str = '' -# V4 Ideas: -# - Date estimate and range -# - Edited but unknown when -# - Generally an estimate/guess -# - Formated text/body - +''' +V4: + - Improved date. + - Ends when all current modules are ported and/or completed. +V5: + - Formatted body/text. +''' @dataclasses.dataclass class Post(): - version: int = 3 + version: int = 4 type: PostType = PostType.UNKNOWN unique_id: str = '' raw_responses: dict[str, str] = df({}) url: str = '' - dates: dict[DateType, float] = df({}) + dates: list[Date] = df([]) author: User = df(User()) title: str = '' text: str = '' @@ -128,8 +186,4 @@ class Post(): class PostEncoder(JSONEncoder): def default(self, o): - if isinstance(o, Enum): - return o.value - if dataclasses.is_dataclass(o): - return dataclasses.asdict(o) return o.__dict__ diff --git a/src/portal/py/test.py b/src/portal/py/test.py new file mode 100644 index 0000000..c5687b1 --- /dev/null +++ b/src/portal/py/test.py @@ -0,0 +1 @@ +import tests.test diff --git a/src/portal/py/tests/__init__.py b/src/portal/py/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/portal/py/tests/__init__.py diff --git a/src/portal/tests/archive_query.py b/src/portal/py/tests/archive_query.py index 9f69c1f..1097634 100644 --- a/src/portal/tests/archive_query.py +++ b/src/portal/py/tests/archive_query.py @@ -5,13 +5,13 @@ import json import threading import gzip from datetime import datetime, timezone -sys.path.append('../py') +sys.path.append('../') from base import Method from post import PostEncoder, PostType, DateType, User from modules import ALL_MODULES from logger import Logger -mode = 'instagram' +mode = 'fanbox' cmd = 'user' module = ALL_MODULES[mode][0] if not module.init(): @@ -66,8 +66,8 @@ class QueryDownloadThread(threading.Thread): else: with open(path, open_method) as f: f.write(content) - except: - self.log.write(f'ERROR: Writing file failed ({path}).') + except Exception as e: + self.log.write(f'ERROR: Writing file failed: {path} ({repr(e)}).') return False return True @@ -168,18 +168,19 @@ class QueryDownloadThread(threading.Thread): with queue_cond: queue_cond.notify() +#RUNTIME_PATH = "./run" RUNTIME_PATH = "/mnt/store/files/tmp/run" -LOG_FILE = f'{RUNTIME_PATH}/archive.log' -ARG_FILE = f'{RUNTIME_PATH}/completed_args.log' +LOG_FILE = f'{RUNTIME_PATH}/archive3.log' +ARG_FILE = f'{RUNTIME_PATH}/completed_args3.log' def read_completed_args(path): args = [] if os.path.isfile(path): with open(path, 'r') as f: - for l in f.read().splitlines(): - if l[0] == '-': + for line in f.read().splitlines(): + if line[0] == '-': continue - s = l.split(':') + s = line.split(':') if s[3] == 'c': args.append(f'{s[0]}:{s[1]}:{s[2]}') return args @@ -203,7 +204,7 @@ def download_args(): os.mkdir(output_dir) #args = module.search(f'following:{22781328}') - args = ['byjoshuajamal'] + args = ['anoh223'] threads = [] start = True diff --git a/src/portal/tests/logger.py b/src/portal/py/tests/logger.py index 80e22da..80e22da 100644 --- a/src/portal/tests/logger.py +++ b/src/portal/py/tests/logger.py diff --git a/src/portal/tests/old_twitter_api.py b/src/portal/py/tests/old_twitter_api.py index 40d0346..40d0346 100644 --- a/src/portal/tests/old_twitter_api.py +++ b/src/portal/py/tests/old_twitter_api.py diff --git a/src/portal/tests/rewrite_post.py b/src/portal/py/tests/rewrite_post.py index 3bb152b..3bb152b 100644 --- a/src/portal/tests/rewrite_post.py +++ b/src/portal/py/tests/rewrite_post.py diff --git a/src/portal/py/tests/test.py b/src/portal/py/tests/test.py new file mode 100644 index 0000000..bee7265 --- /dev/null +++ b/src/portal/py/tests/test.py @@ -0,0 +1,103 @@ +import json +import httpx +import os.path +from base import Method +from typing import Optional, Any +from post import DateType, DateMeta, Date, PostEncoder, Post, MediaType +from modules import ALL_MODULES +#from twitter.scraper import Scraper + +#scraper = Scraper(cookies = { +# 'ct0': '5a3c98b7b50a74dc556fa497f497932e8958b5b2ef7ba74440ce3f341bb4753f663d3fe7633222d62a52d4ce121673234b927d97eeef806129d42bbfba7ab4d4cfbd2749c20fe6f8da480d0a1e6629e1', +# 'auth_token': 'ea66b4c2ebd04e22de855dce6f1366221635bf2e' +#}) +# +#media = scraper.media([1557919548904419328], limit=10) +#print(media) + +#output = '/mnt/store/files/ext/patreon_tmp/FPSBlyck' +# +#module = ALL_MODULES['patreon'][0] +#module.init() +#search = module.search('FPSBlyck') +#page_num = 0 +#mark = False +#while not mark: +# page = search.get_page(page_num) +# if not page: +# break +# page_num += 1 +# for unique_id in page: +# if unique_id == 'patreon:p:29707872': +# print('hit mark') +# mark = True +# post = module.get_item(unique_id) +# print(json.dumps(post, indent=4, cls=PostEncoder)) +# for key, m in post.media.items(): +# if m.type == MediaType.FILE: +# path = f'{output}/{post.unique_id.replace(':', '_')}_{m.name}' +# if not os.path.isfile(path): +# dl = module.get_download(post.unique_id, key) +# retries = 3 +# r: Optional[httpx.Response] = None +# while retries >= 0: +# try: +# r = httpx.get(dl['urls'][0].url, headers=dl['headers'], cookies=dl['cookies'], follow_redirects=True) +# except: +# retries -= 1 +# else: +# break +# if r: +# with open(path, 'wb+') as f: +# f.write(r.content) +# else: +# print('Download failed') +# else: +# print('Skipping file') + +ALL_MODULES['youtube'][0].init() +search = ALL_MODULES['youtube'][0].search('yoyoyoy') +page = search.get_page(0) +print(page) +#print(json.dumps(page, indent=4, cls=PostEncoder)) + +#d = Date(DateType.EDITED, (0.0, 0.0), DateMeta.EDITED_AT_UNKNOWN_TIME) +#d = Date(DateType.EDITED, (0.0, 0.0), DateMeta.NONE) +#print(json.dumps(d, indent=4, cls=PostEncoder)) +#sys.exit(1) + +#module = ALL_MODULES['fanbox'][0] +#module = ALL_MODULES['pixiv_web'][0] +#module = ALL_MODULES['youtube'][0] +#module = ALL_MODULES['twitter'][0] +#module.init() + +#search = module.search('opium_00pium') +#search = module.search('search:ddd') +#page = search.get_page(0) +#for unique_id in page: +# post = module.get_item(unique_id) +# print(json.dumps(post, indent=4, cls=PostEncoder)) +# break + +#post = module.get_item(page[0]) +#for key in post.media.keys(): +# download_params = module.get_download(post.unique_id, key) +# for url in download_params['urls']: +# #response = module.do_request(Method.GET, url.url) +# r = httpx.get(url.url, headers=download_params['headers']) +# with open(f'test_{key}.{url.ext}', 'wb+') as f: +# f.write(r.content) + +#print(json.dumps(page, indent=4, cls=PostEncoder)) +#for i in range(0, len(post.media)): +# download_params = module.get_download(post.unique_id, i) +# r = httpx.get(download_params['url'], headers=download_params['headers']) +# if r.status_code != 200: +# print('error{}'.format(r.status_code)) +# else: +# with open('test{}.png'.format(i), 'wb+') as f: +# f.write(r.content) + +#print(json.dumps(page, indent=4, cls=PostEncoder)) +#print(page) diff --git a/src/portal/tests/unescape_json.py b/src/portal/py/tests/unescape_json.py index eaf5194..eaf5194 100644 --- a/src/portal/tests/unescape_json.py +++ b/src/portal/py/tests/unescape_json.py diff --git a/src/portal/requirements.txt b/src/portal/requirements.txt index d241387..f4982dc 100644 --- a/src/portal/requirements.txt +++ b/src/portal/requirements.txt @@ -1,2 +1,4 @@ -httpx[http2,brotli] +httpx[http2,brotli,zstd] +blake3 pillow +notcurses diff --git a/src/portal/scripts/create_vendor.sh b/src/portal/scripts/create_vendor.sh index f4d551e..6fdf9e0 100755 --- a/src/portal/scripts/create_vendor.sh +++ b/src/portal/scripts/create_vendor.sh @@ -4,7 +4,7 @@ python3 -m venv ./venv source ./venv/bin/activate cd vendor/ -rm -r vendor/ +rm -rf vendor/ pip3 install -r ../requirements.txt --upgrade --target=./vendor @@ -12,7 +12,11 @@ cd yt-dlp/ pip3 install . --upgrade --target=../vendor cd ../ -cd snscrape/ +#cd snscrape/ +#pip3 install . --upgrade --target=../vendor +#cd ../ + +cd twitter-api-client/ pip3 install . --upgrade --target=../vendor cd ../ @@ -24,5 +28,13 @@ cd instagrapi/ pip3 install . --upgrade --target=../vendor cd ../ +cd CyberDropDownloader/ +pip3 install . --upgrade --target=../vendor +cd ../ + +#cd gallery-dl/ +#pip3 install . --upgrade --target=../vendor +#cd ../ + cd ../ rm -r venv diff --git a/src/portal/scripts/run_py.sh b/src/portal/scripts/run_py.sh index ddbe869..4414f93 100755 --- a/src/portal/scripts/run_py.sh +++ b/src/portal/scripts/run_py.sh @@ -1,4 +1,5 @@ #! /usr/bin/env sh +export CURL_CA_BUNDLE=/etc/ssl/certs/ca-bundle.crt export PYTHONDONTWRITEBYTECODE=1 export PYTHONPATH=$HOME/c/camu/src/portal/vendor/vendor -python3 $@ +$@ diff --git a/src/portal/src/packet_ext.c b/src/portal/src/packet_ext.c index 580c307..e2909ef 100644 --- a/src/portal/src/packet_ext.c +++ b/src/portal/src/packet_ext.c @@ -16,7 +16,9 @@ void aki_packet_write_camu_post(struct aki_packet *packet, struct camu_post *pos struct camu_post_date *date; al_array_foreach_ptr(post->dates, i, date) { AKI_PACKET_WRITE_TYPE(packet, u8, date->type); - AKI_PACKET_WRITE_TYPE(packet, f32, date->timestamp); + AKI_PACKET_WRITE_TYPE(packet, f32, date->range_start); + AKI_PACKET_WRITE_TYPE(packet, f32, date->range_end); + AKI_PACKET_WRITE_TYPE(packet, u32, date->meta); } aki_packet_write_str(packet, &post->author.unique_id); aki_packet_write_wstr(packet, &post->author.username); @@ -64,7 +66,9 @@ void aki_packet_read_camu_post(struct aki_packet *packet, struct camu_post *post for (u32 i = 0; i < dates_size; i++) { struct camu_post_date date; AKI_PACKET_READ_TYPE(packet, u8, date.type); - AKI_PACKET_READ_TYPE(packet, f32, date.timestamp); + AKI_PACKET_READ_TYPE(packet, f32, date.range_start); + AKI_PACKET_READ_TYPE(packet, f32, date.range_end); + AKI_PACKET_READ_TYPE(packet, u32, date.meta); al_array_push(post->dates, date); } aki_packet_read_str(packet, &post->author.unique_id); diff --git a/src/portal/src/post.c b/src/portal/src/post.c index c5eb4b5..4cb0bcc 100644 --- a/src/portal/src/post.c +++ b/src/portal/src/post.c @@ -56,9 +56,9 @@ void camu_post_add_media(struct camu_post *post, u8 type, str *key, str *url, st })); } -void camu_post_add_date(struct camu_post *post, u8 type, f32 timestamp) +void camu_post_add_date(struct camu_post *post, u8 type, f32 range_start, f32 range_end, u32 meta) { al_array_push(post->dates, ((struct camu_post_date){ - .type = type, .timestamp = timestamp + .type = type, .range_start = range_start, .range_end = range_end, .meta = meta })); } diff --git a/src/portal/src/post.h b/src/portal/src/post.h index 11fa9b6..a946968 100644 --- a/src/portal/src/post.h +++ b/src/portal/src/post.h @@ -20,6 +20,13 @@ enum { }; enum { + CAMU_DATE_ESTIMATE = 1, + CAMU_DATE_GUESS = 1 << 1, + CAMU_DATE_EDITED_AT_UNKNOWN_TIME = 1 << 2, + CAMU_DATE_ASSUMED_TYPE = 1 << 3 +}; + +enum { CAMU_MEDIA_UNKNOWN = 0, CAMU_MEDIA_FILE, CAMU_MEDIA_AUDIO, @@ -46,7 +53,9 @@ typedef struct { struct camu_post_date { u8 type; - f32 timestamp; + f32 range_start; + f32 range_end; + u32 meta; }; typedef array(struct camu_post_date) camu_dates_array; @@ -98,5 +107,5 @@ void camu_post_reset(struct camu_post *post); void camu_post_clone(struct camu_post *dest, struct camu_post *src); // Python internal. -void camu_post_add_date(struct camu_post *post, u8 type, f32 timestamp); +void camu_post_add_date(struct camu_post *post, u8 type, f32 range_start, f32 range_end, u32 meta); void camu_post_add_media(struct camu_post *post, u8 type, str *key, str *url, str *ext, str *thumbnail_url, str *thumbnail_ext); diff --git a/src/portal/src/search.c b/src/portal/src/search.c index 7334c41..bcb0643 100644 --- a/src/portal/src/search.c +++ b/src/portal/src/search.c @@ -38,7 +38,7 @@ static struct camu_result_page *page_at_index(struct camu_search *search, u32 nu al_array_foreach_ptr(search->pages, i, page) { if (page->num == num) return page; } - al_array_push(search->pages, (struct camu_result_page){0}); + al_array_push(search->pages, (struct camu_result_page){ 0 }); page = &al_array_last(search->pages); page->num = num; al_array_init(page->posts); @@ -47,10 +47,10 @@ static struct camu_result_page *page_at_index(struct camu_search *search, u32 nu } -void camu_portal_init(struct camu_portal *portal, struct camu_post_cache *cache) +void camu_portal_init(struct camu_portal_bridge *bridge, struct camu_post_cache *cache) { - portal->cache = cache; - al_array_init(portal->searches); + bridge->cache = cache; + al_array_init(bridge->searches); } static void camu_search_init_internal(struct camu_search *search) @@ -59,7 +59,7 @@ static void camu_search_init_internal(struct camu_search *search) al_array_init(search->pages); } -s32 camu_portal_create_search(struct camu_portal *portal, str *module, str *query) +s32 camu_portal_create_search(struct camu_portal_bridge *bridge, str *module, str *query) { s32 id = portal_bridge_search(module, query); if (id >= 0) { @@ -68,31 +68,31 @@ s32 camu_portal_create_search(struct camu_portal *portal, str *module, str *quer search->id = id; al_str_clone(&search->module, module); al_str_clone(&search->query, query); - search->portal = portal; - al_array_push(portal->searches, search); + search->bridge = bridge; + al_array_push(bridge->searches, search); } al_log_info("portal", "New search %x (%.*s).", id, AL_STR_PRINTF(query)); return id; } -struct camu_search *camu_portal_get_search(struct camu_portal *portal, s32 id) +struct camu_search *camu_portal_get_search(struct camu_portal_bridge *bridge, s32 id) { struct camu_search *search; - al_array_foreach(portal->searches, i, search) { + al_array_foreach(bridge->searches, i, search) { if (search->id == id) return search; } return NULL; } -void camu_portal_discard_search(struct camu_portal *portal, s32 id) +void camu_portal_discard_search(struct camu_portal_bridge *bridge, s32 id) { - (void)portal; + (void)bridge; (void)id; } -void camu_portal_close(struct camu_portal *portal) +void camu_portal_close(struct camu_portal_bridge *bridge) { - (void)portal; + (void)bridge; } bool camu_search_get_page(struct camu_search *search, u32 num) @@ -105,11 +105,11 @@ bool camu_search_get_page(struct camu_search *search, u32 num) if (portal_bridge_get_page(search, search->id, num) == -1) { return false; } - struct camu_portal *portal = search->portal; - if (portal->cache) { + struct camu_portal_bridge *bridge = search->bridge; + if (bridge->cache) { struct camu_post *post; al_array_foreach_ptr(al_array_at(search->pages, num).posts, i, post) { - camu_post_cache_push(portal->cache, post); + camu_post_cache_push(bridge->cache, post); } } out: diff --git a/src/portal/src/search.h b/src/portal/src/search.h index 4962170..1f43ded 100644 --- a/src/portal/src/search.h +++ b/src/portal/src/search.h @@ -15,22 +15,22 @@ struct camu_search { str query; u32 page; array(struct camu_result_page) pages; - struct camu_portal *portal; + struct camu_portal_bridge *bridge; }; -struct camu_portal { - struct camu_post_cache *cache; +struct camu_portal_bridge { array(struct camu_search *) searches; + struct camu_post_cache *cache; }; bool camu_python_init(void); void camu_python_close(void); -void camu_portal_init(struct camu_portal *portal, struct camu_post_cache *cache); -s32 camu_portal_create_search(struct camu_portal *portal, str *module_str, str *search_str); -struct camu_search *camu_portal_get_search(struct camu_portal *portal, s32 id); -void camu_portal_discard_search(struct camu_portal *portal, s32 id); -void camu_portal_close(struct camu_portal *portal); +void camu_portal_init(struct camu_portal_bridge *bridge, struct camu_post_cache *cache); +s32 camu_portal_create_search(struct camu_portal_bridge *bridge, str *module_str, str *search_str); +struct camu_search *camu_portal_get_search(struct camu_portal_bridge *bridge, s32 id); +void camu_portal_discard_search(struct camu_portal_bridge *bridge, s32 id); +void camu_portal_close(struct camu_portal_bridge *bridge); bool camu_search_get_page(struct camu_search *search, u32 num); diff --git a/src/portal/tests/test.py b/src/portal/tests/test.py deleted file mode 100644 index a5139f6..0000000 --- a/src/portal/tests/test.py +++ /dev/null @@ -1,51 +0,0 @@ -import json -import httpx -import sys -sys.path.append('../py') -from base import Method -from post import DateType, PostEncoder, Media -from modules import ALL_MODULES - -#ALL_MODULES['instagram'][0].init() -#search = ALL_MODULES['instagram'][0].search('opium_00pium') -#page = search.get_page(0) -#print(page) -#print(json.dumps(page, indent=4, cls=PostEncoder)) - -d = Media() -print(d) -sys.exit(1) - -#module = ALL_MODULES['fanbox'][0] -#module = ALL_MODULES['pixiv_web'][0] -module = ALL_MODULES['instagram'][0] -#module = ALL_MODULES['twitter'][0] -module.init() - -search = module.search('opium_00pium') -page = search.get_page(1) -for unique_id in page: - post = module.get_item(unique_id) - print(json.dumps(post, indent=4, cls=PostEncoder)) - -#post = module.get_item(page[0]) -#for key in post.media.keys(): -# download_params = module.get_download(post.unique_id, key) -# for url in download_params['urls']: -# #response = module.do_request(Method.GET, url.url) -# r = httpx.get(url.url, headers=download_params['headers']) -# with open(f'test_{key}.{url.ext}', 'wb+') as f: -# f.write(r.content) - -#print(json.dumps(page, indent=4, cls=PostEncoder)) -#for i in range(0, len(post.media)): -# download_params = module.get_download(post.unique_id, i) -# r = httpx.get(download_params['url'], headers=download_params['headers']) -# if r.status_code != 200: -# print('error{}'.format(r.status_code)) -# else: -# with open('test{}.png'.format(i), 'wb+') as f: -# f.write(r.content) - -#print(json.dumps(page, indent=4, cls=PostEncoder)) -#print(page) diff --git a/src/render/meson.build b/src/render/meson.build index 20f68c3..b62a5a5 100644 --- a/src/render/meson.build +++ b/src/render/meson.build @@ -1,58 +1,62 @@ render_src = [] render_deps = [common_deps] +render_args = [] -#linmath = subproject('linmath.h').get_variable('linmath') -#libm = compiler.find_library('m', required: false) -#if libm.found() -# render_deps += [libm] -#endif -#render_src += ['renderer_tiger.c', 'queue_tiger.c'] -#render_deps += [linmath] - -libplacebo_opts = [ - 'warning_level=2', - 'demos=false', - 'tests=false', - 'bench=false', - 'fuzz=false', - 'glslang=disabled', - 'gl-proc-addr=disabled', - 'vk-proc-addr=disabled', - 'd3d11=disabled', - 'lcms=disabled', - 'dovi=disabled', - 'libdovi=disabled' -] - -if sink_use_vulkan - shaderc_opts = cmake.subproject_options() - shaderc_opts.add_cmake_defines({ 'CMAKE_BUILD_TYPE': is_debug ? 'Debug' : 'Release' }) - shaderc_opts.add_cmake_defines({ 'SHADERC_SKIP_INSTALL': false }) - shaderc_opts.add_cmake_defines({ 'SHADERC_SKIP_TESTS': true }) - shaderc_opts.add_cmake_defines({ 'SHADERC_SKIP_EXAMPLES': true }) - shaderc_opts.add_cmake_defines({ 'SHADERC_SKIP_COPYRIGHT_CHECK': true }) - shaderc_proj = cmake.subproject('shaderc', options: shaderc_opts) - shaderc = shaderc_proj.dependency('shaderc') - meson.override_dependency('shaderc', shaderc) - render_deps += [ - shaderc_proj.dependency('shaderc_util'), - shaderc_proj.dependency('glslang'), - shaderc_proj.dependency('MachineIndependent'), - shaderc_proj.dependency('OSDependent'), - shaderc_proj.dependency('GenericCodeGen'), - shaderc_proj.dependency('SPIRV'), - shaderc_proj.dependency('SPIRV-Tools-opt'), - shaderc_proj.dependency('SPIRV-Tools-static') +if get_option('renderer') == 'tiger' + render_src += ['renderer_tiger.c', 'queue_tiger.c'] + linmath_h = subproject('linmath.h').get_variable('linmath_h') + render_deps += [linmath_h] + libm = compiler.find_library('m', required: false) + if libm.found() + render_deps += [libm] + endif + render_args += ['-DCAMU_RENDERER_TIGER'] +elif get_option('renderer') == 'libplacebo' + libplacebo_opts = ['warning_level=2', 'demos=false', 'tests=false', 'bench=false', 'fuzz=false', + 'glslang=disabled', + 'gl-proc-addr=disabled', + 'vk-proc-addr=disabled', + 'd3d11=disabled', + 'lcms=disabled', + 'dovi=disabled', + 'libdovi=disabled' ] - libplacebo_opts += ['shaderc=enabled', 'vulkan=enabled', 'opengl=disabled'] -else - libplacebo_opts += ['shaderc=disabled', 'opengl=enabled', 'vulkan=disabled'] -endif -libplacebo = dependency('libplacebo', default_options: libplacebo_opts) -if libplacebo.found() + if sink_use_vulkan + shaderc = dependency('shaderc', required: false, allow_fallback: false) + if shaderc.found() + render_deps += [shaderc] + else + shaderc_opts = cmake.subproject_options() + reltype = is_minsize ? 'MinSizeRel' : 'Release' + shaderc_opts.add_cmake_defines({ 'CMAKE_BUILD_TYPE': is_debug ? 'Debug' : reltype }) + shaderc_opts.add_cmake_defines({ 'SHADERC_SKIP_INSTALL': true }) + shaderc_opts.add_cmake_defines({ 'SHADERC_SKIP_TESTS': true }) + shaderc_opts.add_cmake_defines({ 'SHADERC_SKIP_EXAMPLES': true }) + shaderc_opts.add_cmake_defines({ 'SHADERC_SKIP_COPYRIGHT_CHECK': true }) + shaderc_proj = cmake.subproject('shaderc', options: shaderc_opts) + shaderc = shaderc_proj.dependency('shaderc') + meson.override_dependency('shaderc', shaderc) + render_deps += [ + shaderc_proj.dependency('shaderc_util'), + shaderc_proj.dependency('glslang'), + shaderc_proj.dependency('MachineIndependent'), + shaderc_proj.dependency('OSDependent'), + shaderc_proj.dependency('GenericCodeGen'), + shaderc_proj.dependency('SPIRV'), + shaderc_proj.dependency('SPIRV-Tools-opt'), + shaderc_proj.dependency('SPIRV-Tools-static') + ] + endif + libplacebo_opts += ['shaderc=enabled', 'vulkan=enabled', 'opengl=disabled'] + else + libplacebo_opts += ['shaderc=disabled', 'opengl=enabled', 'vulkan=disabled'] + endif + + libplacebo = dependency('libplacebo', default_options: libplacebo_opts) render_src += ['renderer_libplacebo.c', 'queue_libplacebo.c'] render_deps += [libplacebo] + render_args += ['-DCAMU_RENDERER_LIBPLACEBO'] endif -render = declare_dependency(sources: render_src, dependencies: render_deps) +render = declare_dependency(sources: render_src, dependencies: render_deps, compile_args: render_args) diff --git a/src/render/queue_libplacebo.c b/src/render/queue_libplacebo.c index 6316fe2..bd5ed7b 100644 --- a/src/render/queue_libplacebo.c +++ b/src/render/queue_libplacebo.c @@ -12,49 +12,65 @@ static bool map_frame(pl_gpu gpu, pl_tex *tex, const struct pl_source_frame *src, struct pl_frame *out_frame) { - struct camu_frame *frame = src->frame_data; + struct camu_codec_frame *frame = src->frame_data; - u64 masks[4] = { 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000 }; - s32 pixel_stride = 4; - - /* - switch (frame->fmt) { - case CAMU_PIXEL_FMT_RGBA: + u64 masks[4] = { 0 }; + s32 pixel_stride; + switch (frame->format) { + case CAMU_PIXEL_FORMAT_RGBA: + masks[0] = 0x000000ff; + masks[1] = 0x0000ff00; + masks[2] = 0x00ff0000; + masks[3] = 0xff000000; + pixel_stride = 4; + break; + case CAMU_PIXEL_FORMAT_RGB: + masks[0] = 0x000000ff; + masks[1] = 0x0000ff00; + masks[2] = 0x00ff0000; + pixel_stride = 3; + break; + case CAMU_PIXEL_FORMAT_GREYA: + masks[0] = 0x000000ff; + masks[1] = 0x0000ff00; + pixel_stride = 2; + break; + case CAMU_PIXEL_FORMAT_GREY: + masks[0] = 0x000000ff; + pixel_stride = 1; break; default: al_log_error("frame_queue_libplacebo", "Unsuppored pixel format."); - return false; + al_assert_and_return(false); } - */ struct pl_plane_data data = { .type = PL_FMT_UNORM, - .width = frame->width, - .height = frame->height, + .width = frame->video.width, + .height = frame->video.height, .pixel_stride = pixel_stride, - .row_stride = frame->width * pixel_stride, + .row_stride = frame->video.width * pixel_stride, .pixels = frame->data }; - pl_plane_data_from_mask(&data, masks); struct pl_plane plane = { 0 }; bool ok = pl_upload_plane(gpu, &plane, tex, &data); - al_free(frame); + out_frame->num_planes = 1; + out_frame->planes[0] = plane; + out_frame->repr = pl_color_repr_unknown; + out_frame->color = pl_color_space_monitor; + out_frame->crop = (pl_rect2df){ 0, 0, frame->video.width, frame->video.height }; + //out_frame->profile = + + camu_codec_frame_discard(frame); if (!ok) { al_log_error("frame_queue_libplacebo", "Failed to upload texture."); return false; } - out_frame->num_planes = 1; - out_frame->planes[0] = plane; - out_frame->repr = pl_color_repr_unknown; - out_frame->color = pl_color_space_unknown; - out_frame->crop = (pl_rect2df){ 0, 0, frame->width, frame->height }; - //out_frame->profile = - return true; } @@ -67,11 +83,11 @@ static void unmap_frame(pl_gpu gpu, struct pl_frame *frame, const struct pl_sour static void discard_frame(const struct pl_source_frame *src) { - struct camu_frame *frame = src->frame_data; - al_free(frame); + struct camu_codec_frame *frame = src->frame_data; + camu_codec_frame_discard(frame); } -static void queue_lp_push(struct camu_frame_queue *queue, struct camu_frame *frame, f64 pts) +static void queue_lp_push(struct camu_frame_queue *queue, struct camu_codec_frame *frame, f64 pts) { struct camu_frame_queue_lp *lq = (struct camu_frame_queue_lp *)queue; pl_queue_push(lq->queue, &(struct pl_source_frame){ @@ -80,7 +96,7 @@ static void queue_lp_push(struct camu_frame_queue *queue, struct camu_frame *fra .map = map_frame, .unmap = unmap_frame, .discard = discard_frame, - .frame_data = frame, + .frame_data = frame }); } diff --git a/src/render/queue_tiger.c b/src/render/queue_tiger.c index 0e76502..b1c34ca 100644 --- a/src/render/queue_tiger.c +++ b/src/render/queue_tiger.c @@ -1,10 +1,9 @@ #include <al/lib.h> +#include <stl/gl.h> -#include "renderer_tiger.h" #include "queue_tiger.h" -static void upload_texture(struct camu_frame_queue_tiger *tq, s32 width, s32 height, - u8 *data, s32 linesize) +static void upload_texture(struct camu_frame_queue_tiger *tq, struct camu_codec_frame *frame) { glGenTextures(1, &tq->texture_id); glActiveTexture(GL_TEXTURE0); @@ -19,18 +18,28 @@ static void upload_texture(struct camu_frame_queue_tiger *tq, s32 width, s32 hei //if (linesize) { // glPixelStorei(GL_UNPACK_ROW_LENGTH, linesize); //} - (void)linesize; //glPixelStorei(GL_UNPACK_ROW_LENGTH, linesize); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); - //glGenerateMipmap(GL_TEXTURE_2D); + switch (frame->format) { + case CAMU_PIXEL_FORMAT_RGBA: + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, frame->width, frame->height, 0, + GL_RGBA, GL_UNSIGNED_BYTE, frame->data); + break; + case CAMU_PIXEL_FORMAT_RGB: + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, frame->width, frame->height, 0, + GL_RGB, GL_UNSIGNED_BYTE, frame->data); + break; + default: + al_assert(false); + } + glGenerateMipmap(GL_TEXTURE_2D); } -static void queue_tiger_push(struct camu_frame_queue *queue, struct camu_frame *frame, f64 pts) +static void queue_tiger_push(struct camu_frame_queue *queue, struct camu_codec_frame *frame, f64 pts) { struct camu_frame_queue_tiger *tq = (struct camu_frame_queue_tiger *)queue; (void)pts; - upload_texture(tq, frame->width, frame->height, frame->data, 0); + tq->queued_frame = frame; } #ifdef CAMU_HAVE_FFMPEG @@ -38,7 +47,6 @@ static void queue_tiger_push_av_frame(struct camu_frame_queue *queue, AVFrame *f { struct camu_frame_queue_tiger *tq = (struct camu_frame_queue_tiger *)queue; (void)pts; - upload_texture(tq, frame->width, frame->height, frame->data[0], frame->linesize[0]); } #endif @@ -58,6 +66,13 @@ static u8 queue_tiger_read(struct camu_frame_queue *queue, f64 pts, void *out) { struct camu_frame_queue_tiger *tq = (struct camu_frame_queue_tiger *)queue; (void)pts; + if (tq->queued_frame) { + struct camu_codec_frame *frame = tq->queued_frame; + upload_texture(tq, frame); + tq->queued_frame = NULL; + tq->uploaded = true; + } + if (!tq->uploaded) return CAMU_QUEUE_MORE; *((GLuint *)out) = tq->texture_id; return CAMU_QUEUE_OK; } @@ -79,6 +94,8 @@ static void queue_tiger_free(struct camu_frame_queue **queue) struct camu_frame_queue *camu_frame_queue_tiger_create(void) { struct camu_frame_queue_tiger *tq = al_alloc_object(struct camu_frame_queue_tiger); + tq->queued_frame = NULL; + tq->uploaded = false; tq->q.push = queue_tiger_push; #ifdef CAMU_HAVE_FFMPEG tq->q.push_av_frame = queue_tiger_push_av_frame; diff --git a/src/render/queue_tiger.h b/src/render/queue_tiger.h index db675df..a90d8c7 100644 --- a/src/render/queue_tiger.h +++ b/src/render/queue_tiger.h @@ -1,6 +1,6 @@ #pragma once -#include <skh/gl.h> +#include <stl/gl.h> #include "../buffer/frame_queue.h" @@ -8,6 +8,8 @@ struct camu_frame_queue_tiger { struct camu_frame_queue q; GLuint texture_id; struct camu_renderer_tiger *tr; + struct camu_codec_frame *queued_frame; + bool uploaded; }; struct camu_frame_queue *camu_frame_queue_tiger_create(void); diff --git a/src/render/renderer.h b/src/render/renderer.h index e60c3a5..5b1d342 100644 --- a/src/render/renderer.h +++ b/src/render/renderer.h @@ -1,20 +1,19 @@ #pragma once #include <al/types.h> -#include <skh/window.h> +#include <stl/window.h> #ifdef CAMU_HAVE_FFMPEG #include <libavcodec/avcodec.h> #endif -#include "../buffer/frame_queue.h" - -#if defined SEKIHI_API_OPENGL +#if defined STELA_API_OPENGL #define CAMU_RENDERER_OPENGL -#elif defined SEKIHI_API_VULKAN +#elif defined STELA_API_VULKAN #define CAMU_RENDERER_VULKAN #endif struct camu_screen; +struct camu_frame_queue; struct camu_renderer { bool (*create_renderer)( struct camu_renderer *, s32 *, s32 *, diff --git a/src/render/renderer_libplacebo.c b/src/render/renderer_libplacebo.c index 9cc0917..eeef565 100644 --- a/src/render/renderer_libplacebo.c +++ b/src/render/renderer_libplacebo.c @@ -130,18 +130,20 @@ static bool renderer_lp_create_renderer(struct camu_renderer *renderer, s32 *wid lr->renderer = pl_renderer_create(lr->logger, lr->gpu); - lr->r.resize(&lr->r, width, height); + pl_swapchain_resize(lr->swapchain, width, height); al_memset(&lr->params, 0, sizeof(struct pl_render_params)); - //lr->params = pl_render_default_params; - lr->params = pl_render_fast_params; - lr->params.upscaler = &pl_filter_box; + //lr->params = pl_render_fast_params; + //lr->params.upscaler = &pl_filter_nearest; + lr->params = pl_render_default_params; + //lr->params = pl_render_high_quality_params; + lr->params.deband_params = NULL; - // Clear manually so we can draw multiple images per frame. - lr->params.skip_target_clearing = true; - // Breaks zoom/pan. + // Don't let the cache treat different images of the same size as the same frame. lr->params.skip_caching_single_frame = true; - // Prioritize a more consistant image (should be configurable). + // Clear manually so we can draw multiple images per frame. + lr->params.border = PL_CLEAR_SKIP; + // Prioritize a more consistant image. lr->params.correct_subpixel_offsets = true; #if 0 @@ -159,8 +161,8 @@ static bool renderer_lp_create_renderer(struct camu_renderer *renderer, s32 *wid #endif #ifdef CAMU_HAVE_FFMPEG - //lr->r.get_buffer2 = pl_get_buffer2; - //lr->r.opaque = &lr->gpu; + lr->r.get_buffer2 = pl_get_buffer2; + lr->r.opaque = &lr->gpu; #endif return true; @@ -187,32 +189,23 @@ static void renderer_lp_render(struct camu_renderer *renderer, struct camu_scree struct camu_renderer_lp *lr = (struct camu_renderer_lp *)renderer; #ifdef CAMU_SCREEN_THREADED - if (al_atomic_bool_load(&scr->queued, AL_ATOMIC_RELAXED)) { + if (al_atomic_load(u8)(&scr->queued, AL_ATOMIC_RELAXED)) { camu_screen_run_queue(scr); } #endif - bool have_frame = false; struct pl_swapchain_frame frame; struct pl_frame target; struct pl_frame_mix mix; -#if 1 pl_swapchain_start_frame(lr->swapchain, &frame); pl_frame_from_swapchain(&target, &frame); pl_frame_clear_rgba(lr->gpu, &target, clear_color); - have_frame = true; -#endif - if (scr->videos.size > 0) { - struct camu_screen_video *video = &al_array_last(scr->videos); + struct camu_screen_video *video; + while (scr->videos.size > 0) { + video = &al_array_last(scr->videos); if (camu_video_buffer_read(video->buf, &mix) && mix.frames) { - if (!have_frame) { - pl_swapchain_start_frame(lr->swapchain, &frame); - pl_frame_from_swapchain(&target, &frame); - pl_frame_clear_rgba(lr->gpu, &target, clear_color); - have_frame = true; - } target.crop = mix.frames[0]->crop; target.crop.x1 *= video->view.zoom / video->view.stretch; target.crop.y1 *= video->view.zoom * video->view.stretch; @@ -221,14 +214,22 @@ static void renderer_lp_render(struct camu_renderer *renderer, struct camu_scree target.crop.x1 += video->view.x_offset; target.crop.y1 += video->view.y_offset; target.rotation = video->view.rotation; + //lr->params.color_adjustment = pl_color_adjustment( + // .saturation = 0.0 + //); pl_render_image_mix(lr->renderer, &mix, &target, &lr->params); + break; +#ifdef CAMU_SCREEN_THREADED + } else { + if (al_atomic_load(u8)(&scr->queued, AL_ATOMIC_RELAXED)) { + camu_screen_run_queue(scr); + } +#endif } } - if (have_frame) { - pl_swapchain_submit_frame(lr->swapchain); - pl_swapchain_swap_buffers(lr->swapchain); - } + pl_swapchain_submit_frame(lr->swapchain); + pl_swapchain_swap_buffers(lr->swapchain); } void renderer_lp_free(struct camu_renderer **renderer) diff --git a/src/render/renderer_tiger.c b/src/render/renderer_tiger.c index 049a3b6..7127891 100644 --- a/src/render/renderer_tiger.c +++ b/src/render/renderer_tiger.c @@ -90,9 +90,8 @@ static void renderer_tiger_resize(struct camu_renderer *renderer, s32 *width, s3 { struct camu_renderer_tiger *tr = (struct camu_renderer_tiger *)renderer; (void)tr; -#ifdef CAMU_RENDERER_OPENGL - glViewport(0, 0, *width, *height); -#endif + (void)width; + (void)height; } static bool renderer_tiger_create_renderer(struct camu_renderer *renderer, s32 *width, s32 *height, @@ -123,7 +122,7 @@ static bool renderer_tiger_create_renderer(struct camu_renderer *renderer, s32 * if (!tr->load_loader(tr->priv)) return false; - renderer_tiger_resize(renderer, width, height); + tr->r.resize(&tr->r, width, height); clear_color[0] = ((global_color_palette.colors[0] >> 16) & 0xff) / 255.f; clear_color[1] = ((global_color_palette.colors[0] >> 8) & 0xff) / 255.f; @@ -144,6 +143,10 @@ static bool renderer_tiger_create_renderer(struct camu_renderer *renderer, s32 * GLint pos = glGetAttribLocation(tr->program, "a_Position"); glVertexAttribPointer(pos, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (void *)0); + if (stl_gl_check_error()) { + al_log_error("render_tiger", "glVertexAttribPointer returned error, use GLES context."); + al_assert(false); + } glEnableVertexAttribArray(pos); GLint texcoord = glGetAttribLocation(tr->program, "a_TexCoord"); glVertexAttribPointer(texcoord, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (void *)12); @@ -208,7 +211,7 @@ static void set_uniform(struct camu_renderer_tiger *tr, const GLchar *name, } #ifdef CAMU_RENDERER_OPENGL -static mat4x4 model; +static mat4x4 model = { 0 }; static GLuint texture0 = 0; #endif @@ -217,6 +220,8 @@ static void renderer_tiger_render(struct camu_renderer *renderer, struct camu_sc #ifdef CAMU_RENDERER_OPENGL struct camu_renderer_tiger *tr = (struct camu_renderer_tiger *)renderer; + tr->make_current(tr->priv); + glClear(GL_COLOR_BUFFER_BIT); glUseProgram(tr->program); @@ -233,21 +238,26 @@ static void renderer_tiger_render(struct camu_renderer *renderer, struct camu_sc struct camu_screen_video *video = &al_array_last(scr->videos); GLuint texture_id; - camu_video_buffer_read(video->buf, &texture_id); - glBindTexture(GL_TEXTURE_2D, texture_id); + if (camu_video_buffer_read(video->buf, &texture_id)) { + glBindTexture(GL_TEXTURE_2D, texture_id); - mat4x4_ortho(model, 0.f, (f32)scr->width, (f32)scr->height, 0.f, -1.f, 1.f); - mat4x4_translate_in_place(model, (f32)video->view.x_offset, (f32)video->view.y_offset, 0.f); - mat4x4_scale_aniso(model, model, (f32)video->view.zoom, (f32)video->view.zoom, 1.f); - mat4x4_scale_aniso(model, model, (f32)video->view.width, (f32)video->view.height, 1.f); - set_uniform(tr, "g_ModelViewProjectionMatrix", TYPE_MAT4, model); - set_uniform(tr, "g_Texture0", TYPE_SAMPLER2D, &texture0); + mat4x4_identity(model); + mat4x4_ortho(model, 0.f, (f32)scr->width, (f32)scr->height, 0.f, -1.f, 1.f); + mat4x4_translate_in_place(model, (f32)video->view.x_offset, (f32)video->view.y_offset, 0.f); + mat4x4_scale_aniso(model, model, (f32)video->view.zoom, (f32)video->view.zoom, 1.f); + mat4x4_scale_aniso(model, model, (f32)video->view.width, (f32)video->view.height, 1.f); + set_uniform(tr, "g_ModelViewProjectionMatrix", TYPE_MAT4, model); + set_uniform(tr, "g_Texture0", TYPE_SAMPLER2D, &texture0); - glDrawArrays(GL_TRIANGLES, 0, 6); + glViewport(0, 0, scr->width, scr->height); + glDrawArrays(GL_TRIANGLES, 0, 6); + } } tr->swap_buffers(tr->priv); #endif + + tr->release_current(tr->priv); } static void *renderer_tiger_get_device_ptr(struct camu_renderer *renderer, size_t length) diff --git a/src/render/renderer_tiger.h b/src/render/renderer_tiger.h index 2979d91..5891c20 100644 --- a/src/render/renderer_tiger.h +++ b/src/render/renderer_tiger.h @@ -1,6 +1,6 @@ #pragma once -#include <skh/gl.h> +#include <stl/gl.h> #include "renderer.h" diff --git a/src/screen/meson.build b/src/screen/meson.build index 7f9738c..9c01566 100644 --- a/src/screen/meson.build +++ b/src/screen/meson.build @@ -1,10 +1,2 @@ screen_src = ['screen.c', 'view.c'] -sekihi = subproject('sekihi', - default_options: [ - sink_use_vulkan ? 'api=vulkan' : 'api=opengl', - 'window=glfw', - 'poll=inline', - 'event-buffer=false', - 'pause=true' - ]).get_variable('sekihi') -screen = declare_dependency(sources: screen_src, dependencies: [sekihi]) +screen = declare_dependency(sources: screen_src, dependencies: [stela]) diff --git a/src/screen/screen.c b/src/screen/screen.c index f92cd98..afc6b9b 100644 --- a/src/screen/screen.c +++ b/src/screen/screen.c @@ -1,5 +1,7 @@ -#include <aki/thread.h> #include <al/log.h> +#include <aki/thread.h> + +#include "../liana/list.h" #include "view.h" #include "screen.h" @@ -23,7 +25,7 @@ static struct camu_view *get_view_from_mouse_pos(struct camu_screen *scr) static void should_close_callback(void *userdata) { struct camu_screen *scr = (struct camu_screen *)userdata; - scr->callback(scr->userdata, CAMU_SCREEN_CLOSE, 0.0); + scr->callback(scr->userdata, CAMU_SCREEN_CLOSE, NULL); } static void resize_callback(void *userdata, s32 width, s32 height) @@ -62,8 +64,8 @@ static bool pointer_pos_callback(void *userdata, f64 x, f64 y) view->mode = CAMU_VIEW_DETACHED; queue_refresh = true; } + if (fabs(dx) + fabs(dy) > 3.0) scr->last_click_ts = 0; } - scr->last_click_ts = 0; } } scr->last_mouse_x = x; @@ -75,51 +77,54 @@ static bool mouse_button_callback(void *userdata, u8 state, u8 button) { struct camu_screen *scr = (struct camu_screen *)userdata; switch (button) { - case SEKIHI_MOUSE1: + case STELA_MOUSE1: switch (state) { - case SEKIHI_BUTTON_PRESSED: + case STELA_BUTTON_PRESSED: scr->flags |= CAMU_SCREEN_DRAGGING; scr->last_click_ts = aki_get_timestamp(); break; - case SEKIHI_BUTTON_RELEASED: + case STELA_BUTTON_RELEASED: scr->flags &= ~CAMU_SCREEN_DRAGGING; if (scr->last_click_ts && aki_get_timestamp() - scr->last_click_ts <= 300000) { if (scr->flags & CAMU_SCREEN_MODIFIER) { - scr->callback(scr->userdata, CAMU_SCREEN_SEEK, scr->last_mouse_x / scr->width); + f64 percent = scr->last_mouse_x / scr->width; + scr->callback(scr->userdata, CAMU_SCREEN_SEEK, &percent); } else { if (scr->last_mouse_x >= scr->width / 2.f) { - scr->callback(scr->userdata, CAMU_SCREEN_NEXT, 0.0); + scr->callback(scr->userdata, CAMU_SCREEN_NEXT, NULL); } else { - scr->callback(scr->userdata, CAMU_SCREEN_PREVIOUS, 0.0); + scr->callback(scr->userdata, CAMU_SCREEN_PREVIOUS, NULL); } } } break; } break; - case SEKIHI_MOUSE2: + case STELA_MOUSE2: switch (state) { - case SEKIHI_BUTTON_PRESSED: + case STELA_BUTTON_PRESSED: scr->flags |= CAMU_SCREEN_MODIFIER; break; - case SEKIHI_BUTTON_RELEASED: + case STELA_BUTTON_RELEASED: scr->flags &= ~CAMU_SCREEN_MODIFIER; break; } break; - case SEKIHI_MOUSE3: - scr->callback(scr->userdata, CAMU_SCREEN_CLOSE, 0.0); + case STELA_MOUSE3: { + f64 percent = scr->last_mouse_x / scr->width; + scr->callback(scr->userdata, CAMU_SCREEN_SEEK, &percent); break; + } default: break; } return false; } -#if defined SEKIHI_WINDOW_WAYLAND -#define SCROLL_MULTIPLIER 150.0 +#if defined STELA_WINDOW_WAYLAND +#define SCROLL_MULTIPLIER 155.0 #else -#define SCROLL_MULTIPLIER 25.0 +#define SCROLL_MULTIPLIER 13.0 #endif static bool scroll_callback(void *userdata, f64 y) @@ -127,11 +132,7 @@ 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) { - if (scr->flags & CAMU_SCREEN_MODIFIER) { - // Unimplemented. - view->rotation += y; - return true; - } else if (scr->flags & CAMU_SCREEN_ZOOM_PAN_SIMPLE) { + if (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; @@ -146,40 +147,35 @@ static bool key_callback(void *userdata, u8 state, u8 button) { struct camu_screen *scr = (struct camu_screen *)userdata; switch (state) { - case SEKIHI_BUTTON_PRESSED: + case STELA_BUTTON_PRESSED: //al_printf("%x\n", button); switch (button) { - case 0x1d: + case 0x1d: // control scr->flags |= CAMU_SCREEN_MODIFIER; break; - case 0x10: - case 0x18: - case 'q': - scr->callback(scr->userdata, CAMU_SCREEN_CLOSE, 0.0); + case 0x10: // q + scr->callback(scr->userdata, CAMU_SCREEN_CLOSE, NULL); + break; + case 0x31: // n + case 0x20: // d + case 0x6a: // right arrow + scr->callback(scr->userdata, CAMU_SCREEN_NEXT, NULL); break; - case 0x6a: - case 0x31: - case 0x4d: - case 'n': - scr->callback(scr->userdata, CAMU_SCREEN_NEXT, 0.0); + case 0x30: // b + case 0x1e: // a + case 0x69: // left arrow + scr->callback(scr->userdata, CAMU_SCREEN_PREVIOUS, NULL); break; - case 0x69: - case 0x30: - case 0x4b: - case 'b': - scr->callback(scr->userdata, CAMU_SCREEN_PREVIOUS, 0.0); + case 0x39: // spacebar + scr->callback(scr->userdata, CAMU_SCREEN_TOGGLE_PAUSE, NULL); break; - case 0x41: - case 0x39: - scr->callback(scr->userdata, CAMU_SCREEN_TOGGLE_PAUSE, 0.0); + case 0x1f: // s + scr->callback(scr->userdata, CAMU_SCREEN_SHUFFLE, NULL); break; - case 0x1f: - case 's': - scr->callback(scr->userdata, CAMU_SCREEN_RESEEK, 0.0); + case 0x12: // e + scr->callback(scr->userdata, CAMU_SCREEN_RESEEK, NULL); break; - case 0x13: - case 0x1b: - case 'r': { + case 0x13: { // r struct camu_view *view = get_view_from_mouse_pos(scr); if (view) { if (!(scr->flags & CAMU_SCREEN_MODIFIER)) { @@ -206,7 +202,7 @@ static bool key_callback(void *userdata, u8 state, u8 button) } break; } - case 0xd: { + case 0xd: { // + struct camu_view *view = get_view_from_mouse_pos(scr); if (view) { camu_view_zoom_simple(view, scr->width, scr->height, scr->width / 2.0, scr->height / 2.0, 0.05); @@ -214,7 +210,7 @@ static bool key_callback(void *userdata, u8 state, u8 button) } break; } - case 0xc: { + case 0xc: { // - struct camu_view *view = get_view_from_mouse_pos(scr); if (view) { camu_view_zoom_simple(view, scr->width, scr->height, scr->width / 2.0, scr->height / 2.0, -0.05); @@ -226,9 +222,9 @@ static bool key_callback(void *userdata, u8 state, u8 button) break; } break; - case SEKIHI_BUTTON_RELEASED: + case STELA_BUTTON_RELEASED: switch (button) { - case 0x1d: + case 0x1d: // control scr->flags &= ~CAMU_SCREEN_MODIFIER; break; } @@ -242,10 +238,9 @@ static void key_immediate_callback(void *userdata, u8 state, u8 button) { struct camu_screen *scr = (struct camu_screen *)userdata; switch (state) { - case SEKIHI_BUTTON_PRESSED: + case STELA_BUTTON_PRESSED: switch (button) { - case 0x29: - case 0x21: + case 0x21: // f scr->window->toggle_fullscreen(scr->window); break; } @@ -257,10 +252,10 @@ static void key_immediate_callback(void *userdata, u8 state, u8 button) bool camu_screen_init(struct camu_screen *scr) { - al_atomic_s32_store(&scr->state, CAMU_SCREEN_PAUSED, AL_ATOMIC_RELAXED); + al_atomic_store(s32)(&scr->state, CAMU_SCREEN_PAUSED, AL_ATOMIC_RELAXED); al_array_init(scr->videos); scr->renderer = NULL; - scr->window = skh_window_create(); + scr->window = stl_window_create(); scr->window->pointer_pos_callback = pointer_pos_callback; scr->window->scroll_callback = scroll_callback; scr->window->mouse_button_callback = mouse_button_callback; @@ -273,24 +268,37 @@ bool camu_screen_init(struct camu_screen *scr) #ifdef CAMU_SCREEN_THREADED al_array_init(scr->add_queue); al_array_init(scr->rem_queue); - al_atomic_bool_store(&scr->queued, false, AL_ATOMIC_RELAXED); + al_atomic_store(u8)(&scr->queued, 0, AL_ATOMIC_RELAXED); aki_mutex_init(&scr->mutex); #endif - scr->last_mouse_x = 0; - scr->last_mouse_y = 0; + scr->last_mouse_x = 0.0; + scr->last_mouse_y = 0.0; scr->flags = CAMU_SCREEN_ZOOM_PAN_SIMPLE; return true; } +#if defined STELA_EVENT_BUFFER +static aki_thread_result AKI_THREADCALL event_thread(void *userdata) +{ + struct camu_screen *scr = (struct camu_screen *)userdata; + while (scr->window->poll(scr->window, true) && al_atomic_load(s32)(&scr->state, AL_ATOMIC_RELAXED) != CAMU_SCREEN_CLOSED ) { + scr->window->process_events(scr->window); + } + return 0; +} +#endif + bool camu_screen_create_window(struct camu_screen *scr, const char *name) { - s32 flags = SEKIHI_WINDOW_VSYNC; - //flags |= SEKIHI_WINDOW_WALLPAPER; - if (!scr->window->create_window(scr->window, CAMU_SCREEN_WIDTH, CAMU_SCREEN_HEIGHT, "DP-1", name, flags)) { + s32 flags = STELA_WINDOW_VSYNC; + if (!scr->window->create_window(scr->window, CAMU_SCREEN_WIDTH, CAMU_SCREEN_HEIGHT, "", name, flags)) { return false; } scr->width = scr->window->width; scr->height = scr->window->height; +#if defined STELA_EVENT_BUFFER + aki_thread_create(&scr->thread, event_thread, scr); +#endif return true; } @@ -314,23 +322,30 @@ bool camu_screen_create_renderer(struct camu_screen *scr, struct camu_renderer * static void add_buffer_internal(struct camu_screen *scr, struct camu_video_buffer *buf) { - struct camu_stream *stream = buf->stream; struct camu_screen_video video = { - .buf = buf, - .view = { - .mode = CAMU_DEFAULT_VIEW, - .width = stream->video.width, - .height = stream->video.height, - .stretch = 1.0, - .rotation = CAMU_VIEW_ROTATION_0, - .zindex = 0 - } + .buf = buf }; -#ifdef CAMU_SCREEN_THREADED - al_atomic_bool_store(&buf->ref, true, AL_ATOMIC_RELAXED); + struct camu_codec_stream *stream = buf->stream; + if (buf->view.mode != CAMU_VIEW_NONE) { + video.view = buf->view; +#if 0 + } else if (!al_array_is_empty(scr->videos)) { + video.view = al_array_last(scr->videos).view; + video.view.width = stream->video.width; + video.view.height = stream->video.height; #endif - if (buf->view.mode != CAMU_VIEW_NONE) video.view = buf->view; + } else { + video.view.mode = CAMU_DEFAULT_VIEW; + video.view.width = stream->video.width; + video.view.height = stream->video.height; + video.view.stretch = 1.0; + video.view.rotation = CAMU_VIEW_ROTATION_0; + video.view.zindex = 0; + } camu_view_calculate(&video.view, scr->width, scr->height); +#ifdef CAMU_SCREEN_THREADED + al_atomic_store(u8)(&buf->ref, 1, AL_ATOMIC_RELAXED); +#endif al_array_push(scr->videos, video); } @@ -339,11 +354,11 @@ static void remove_buffer_internal(struct camu_screen *scr, struct camu_video_bu struct camu_screen_video *video; al_array_foreach_ptr(scr->videos, i, video) { if (video->buf == buf) { + buf->view = video->view; #ifdef CAMU_SCREEN_THREADED - al_atomic_bool_store(&buf->ref, false, AL_ATOMIC_RELAXED); + al_atomic_store(u8)(&buf->ref, 0, AL_ATOMIC_RELAXED); #endif - buf->view = video->view; - al_array_remove_at_iter(scr->videos, i); + al_array_remove_at(scr->videos, i); break; } } @@ -355,7 +370,7 @@ void camu_screen_add_buffer(struct camu_screen *scr, struct camu_video_buffer *b aki_mutex_lock(&scr->mutex); al_array_push(scr->add_queue, buf); al_array_reserve(scr->videos, scr->videos.size + scr->add_queue.size); - al_atomic_bool_store(&scr->queued, true, AL_ATOMIC_RELAXED); + al_atomic_store(u8)(&scr->queued, 1, AL_ATOMIC_RELAXED); aki_mutex_unlock(&scr->mutex); #else add_buffer_internal(scr, buf); @@ -382,11 +397,20 @@ void camu_screen_remove_buffer(struct camu_screen *scr, struct camu_video_buffer { #ifdef CAMU_SCREEN_THREADED aki_mutex_lock(&scr->mutex); - al_array_push(scr->rem_queue, buf); - al_atomic_bool_store(&scr->queued, true, AL_ATOMIC_RELAXED); - if (al_atomic_s32_load(&scr->state, AL_ATOMIC_RELAXED) == CAMU_SCREEN_CLOSED) { - run_queue_internal(scr); + struct camu_video_buffer *rbuf; + al_array_foreach(scr->add_queue, i, rbuf) { + if (rbuf == buf) { + al_array_remove_at(scr->add_queue, i); + bool queue_empty = scr->add_queue.size + scr->rem_queue.size == 0; + if (queue_empty) { + al_atomic_store(u8)(&scr->queued, 0, AL_ATOMIC_RELAXED); + } + aki_mutex_unlock(&scr->mutex); + return; + } } + al_array_push(scr->rem_queue, buf); + al_atomic_store(u8)(&scr->queued, 1, AL_ATOMIC_RELAXED); aki_mutex_unlock(&scr->mutex); #else remove_buffer_internal(scr, buf); @@ -399,24 +423,30 @@ void camu_screen_run_queue(struct camu_screen *scr) { aki_mutex_lock(&scr->mutex); run_queue_internal(scr); - al_atomic_bool_store(&scr->queued, false, AL_ATOMIC_RELAXED); + al_atomic_store(u8)(&scr->queued, 0, AL_ATOMIC_RELAXED); aki_mutex_unlock(&scr->mutex); } #endif void camu_screen_set_state(struct camu_screen *scr, s32 state) { - al_atomic_s32_store(&scr->state, state, AL_ATOMIC_RELAXED); + al_atomic_store(s32)(&scr->state, state, AL_ATOMIC_RELAXED); } +#define CAMU_SCREEN_POLL_HZ 576 + bool camu_screen_poll(struct camu_screen *scr, bool block) { +#if defined STELA_EVENT_BUFFER + if (block) { + aki_thread_sleep(AKI_TS_FROM_USEC(1000000/CAMU_SCREEN_POLL_HZ)); + } + stl_window_read_events(scr->window); +#else scr->window->poll(scr->window, block); scr->window->process_events(scr->window); -#if defined SEKIHI_EVENT_BUFFER - skh_window_read_events(scr->window); #endif -#if defined SEKIHI_PAUSE +#if defined STELA_PAUSE return scr->window->should_refresh(scr->window); #else return true; @@ -425,15 +455,29 @@ bool camu_screen_poll(struct camu_screen *scr, bool block) bool camu_screen_tick(struct camu_screen *scr) { - bool paused = al_atomic_s32_load(&scr->state, AL_ATOMIC_RELAXED) == CAMU_SCREEN_PAUSED; + bool paused = al_atomic_load(s32)(&scr->state, AL_ATOMIC_RELAXED) == CAMU_SCREEN_PAUSED; return camu_screen_poll(scr, paused) || !paused; } void camu_screen_wake(struct camu_screen *scr) { -#if defined SEKIHI_POLL_INLINE && defined SEKIHI_PAUSE +#if defined STELA_POLL_INLINE && defined STELA_PAUSE scr->window->wake(scr->window); +#else + (void)scr; +#endif +} + +void camu_screen_clear(struct camu_screen *scr) +{ +#ifdef CAMU_SCREEN_THREADED + run_queue_internal(scr); + struct camu_screen_video *video; + al_array_foreach_ptr(scr->videos, i, video) { + al_atomic_store(u8)(&video->buf->ref, 0, AL_ATOMIC_RELAXED); + } #endif + scr->videos.size = 0; } void camu_screen_close(struct camu_screen *scr) @@ -444,5 +488,9 @@ void camu_screen_close(struct camu_screen *scr) aki_mutex_destroy(&scr->mutex); #endif al_array_free(scr->videos); + al_atomic_store(s32)(&scr->state, CAMU_SCREEN_CLOSED, AL_ATOMIC_RELAXED); +#if defined STELA_EVENT_BUFFER + aki_thread_join(&scr->thread); +#endif scr->window->free(&scr->window); } diff --git a/src/screen/screen.h b/src/screen/screen.h index 67cd7ed..af4169e 100644 --- a/src/screen/screen.h +++ b/src/screen/screen.h @@ -7,11 +7,10 @@ #include <al/atomic.h> #include <aki/thread.h> #endif -#include <skh/window.h> +#include <stl/window.h> #include "../buffer/video.h" #include "../render/renderer.h" -#include "../codec/codec.h" #include "view.h" @@ -33,6 +32,7 @@ enum { enum { CAMU_SCREEN_NEXT = 0, CAMU_SCREEN_PREVIOUS, + CAMU_SCREEN_SHUFFLE, CAMU_SCREEN_TOGGLE_PAUSE, CAMU_SCREEN_SEEK, CAMU_SCREEN_RESEEK, @@ -45,9 +45,12 @@ struct camu_screen_video { }; struct camu_screen { - atomic_s32 state; + atomic(s32) state; u32 flags; - struct skh_window *window; + struct stl_window *window; +#if defined STELA_EVENT_BUFFER + struct aki_thread thread; +#endif struct camu_renderer *renderer; s32 width; s32 height; @@ -58,10 +61,10 @@ struct camu_screen { #ifdef CAMU_SCREEN_THREADED array(struct camu_video_buffer *) add_queue; array(struct camu_video_buffer *) rem_queue; - atomic_bool queued; + atomic(u8) queued; struct aki_mutex mutex; #endif - void (*callback)(void *, u8, f64); + void (*callback)(void *, u8, void *); void *userdata; }; @@ -77,4 +80,5 @@ 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_clear(struct camu_screen *scr); void camu_screen_close(struct camu_screen *scr); diff --git a/src/server/common.c b/src/server/common.c new file mode 100644 index 0000000..765418c --- /dev/null +++ b/src/server/common.c @@ -0,0 +1,10 @@ +#include "common.h" + +//const str *CAMU_DB_PATH = al_str_c("/mnt/store/files/camu_db"); +str *CAMU_DB_PATH = al_str_c("/home/andrew/c/camu/data/camu_db_test"); + +str *CAMU_SERVER_IP = al_str_c("108.52.160.112"); +//str *CAMU_SERVER_IP = al_str_c("127.0.0.1"); + +str *CAMU_UNIX_PATH = al_str_c("/tmp/camu_sock"); +str *CAMU_UNIX_LOCAL = al_str_c("/tmp/cmv_sock"); diff --git a/src/server/common.h b/src/server/common.h index 26d0edf..c31efcf 100644 --- a/src/server/common.h +++ b/src/server/common.h @@ -2,16 +2,20 @@ #include <al/str.h> -#define CAMU_DB_PATH al_str_c("/mnt/store/files/camu_db") -//#define CAMU_DB_PATH al_str_c("/home/andrew/c/camu/data/camu_db_test") +extern str *CAMU_DB_PATH; #define CAMU_PORT 14356 #define CAMU_MULTIPLEX_RPC 0x53 -#define CAMU_MULTIPLEX_SHRUB 0x85 +#define CAMU_MULTIPLEX_LIANA 0x85 -//#define CAMU_SERVER_IP al_str_c("127.0.0.1") -//#define CAMU_SERVER_IP al_str_c("192.168.1.192") -#define CAMU_SERVER_IP al_str_c("108.52.160.112") +extern str *CAMU_SERVER_IP; +extern str *CAMU_UNIX_PATH; +extern str *CAMU_UNIX_LOCAL; + +//#define CAMU_LOCAL_TYPE AKI_SOCKET_TCP +//#define CAMU_LOCAL_ADDR CAMU_SERVER_IP +#define CAMU_LOCAL_TYPE AKI_SOCKET_UNIX +#define CAMU_LOCAL_ADDR CAMU_UNIX_PATH enum { CAMU_NODE = 0, @@ -21,14 +25,33 @@ enum { enum { CAMU_SERVER_IDENTIFY = 0, + CAMU_SERVER_CLIENT_COMMAND, CAMU_SERVER_LIST_ACTION }; enum { - CAMU_ADD = 0, - CAMU_SKIP, - CAMU_SKIPTO, - CAMU_TOGGLE_PAUSE, - CAMU_SEEK, - CAMU_FINISHED + CAMU_CLIENT_CREATE_LIST = 0, + CAMU_CLIENT_ENABLE_SINK +}; + +enum { + CAMU_LIST_ADD = 0, + CAMU_LIST_SKIP, + CAMU_LIST_SKIPTO, + CAMU_LIST_SHUFFLE, + CAMU_LIST_TOGGLE_PAUSE, + CAMU_LIST_SEEK, + CAMU_LIST_UNSET, + CAMU_LIST_END }; + +AL_UNUSED_FUNCTION_PUSH + +static bool camu_is_url(str *s, u32 i) +{ + return al_str_cmp(s, al_str_c("https://"), i, 8) == 0 + || al_str_cmp(s, al_str_c("http://"), i, 7) == 0 + || al_str_cmp(s, al_str_c("cdda://"), i, 7) == 0; +} + +AL_UNUSED_FUNCTION_POP diff --git a/src/server/db.c b/src/server/db.c index 5042f2d..5c5e8fd 100644 --- a/src/server/db.c +++ b/src/server/db.c @@ -1,9 +1,11 @@ #include <al/log.h> +#include <aki/file.h> #include <jansson.h> +#include "list.h" #include "server.h" -static bool open_user(struct camu_server *srv, struct aki_dir_entry *dir) +static bool open_user(struct camu_server *server, struct aki_dir_entry *dir) { struct aki_file file; if (!aki_file_open(&file, &dir->path, 0)) { @@ -20,16 +22,14 @@ static bool open_user(struct camu_server *srv, struct aki_dir_entry *dir) } struct camu_user *user = al_alloc_object(struct camu_user); al_str_from(&user->name, json_string_value(json_object_get(root, "username"))); - al_array_init(user->lists); - struct camu_list *default_list = al_alloc_object(struct camu_list); - camu_list_init(default_list, al_str_c("default")); - al_array_push(user->lists, default_list); al_log_info("server", "Loaded user \"%.*s\"", AL_STR_PRINTF(&user->name)); - al_array_push(srv->users, user); + al_array_push(server->users, user); + json_decref(root); + al_str_free(&s); return true; } -bool camu_db_open(struct camu_server *srv, str *path) +bool camu_db_open(struct camu_server *server, str *path) { struct aki_dir camu_db; if (!aki_dir_open(&camu_db, path)) { @@ -43,8 +43,9 @@ bool camu_db_open(struct camu_server *srv, str *path) struct aki_dir_entry user; while (aki_dir_read(&users, &user)) { if (user.type == AKI_ENTRY_FILE) { - open_user(srv, &user); + open_user(server, &user); } + aki_dir_entry_free(&user); } aki_dir_close(&users); } @@ -54,3 +55,17 @@ bool camu_db_open(struct camu_server *srv, str *path) aki_dir_close(&camu_db); return true; } + +void camu_db_close(struct camu_server *server) +{ + struct camu_user *user; + al_array_foreach(server->users, i, user) { + al_str_free(&user->name); + } + al_array_free(server->users); + struct camu_list *list; + al_array_foreach(server->lists, i, list) { + camu_list_free(list); + } + al_array_free(server->lists); +} diff --git a/src/server/list.c b/src/server/list.c new file mode 100644 index 0000000..8ff123c --- /dev/null +++ b/src/server/list.c @@ -0,0 +1,13 @@ +#include "list.h" + +void camu_list_init(struct camu_list *list, str *name) +{ + al_str_clone(&list->name, name); + lia_list_init(&list->impl); +} + +void camu_list_free(struct camu_list *list) +{ + lia_list_free(&list->impl); + al_str_free(&list->name); +} diff --git a/src/server/list.h b/src/server/list.h new file mode 100644 index 0000000..f3e52e6 --- /dev/null +++ b/src/server/list.h @@ -0,0 +1,11 @@ +#pragma once + +#include "../liana/list.h" + +struct camu_list { + str name; + struct lia_list impl; +}; + +void camu_list_init(struct camu_list *list, str *name); +void camu_list_free(struct camu_list *list); diff --git a/src/server/local_compat.c b/src/server/local_compat.c new file mode 100644 index 0000000..9870ce5 --- /dev/null +++ b/src/server/local_compat.c @@ -0,0 +1,213 @@ +#include <al/log.h> + +#ifdef AKIYO_HAS_CURL +#include "../cache/handlers/http.h" +#endif +#include "../cache/handlers/file.h" +#include "../cache/handlers/cdio.h" + +#include "local_compat.h" +#include "common.h" +#include "server.h" + +#ifdef CAMU_HAVE_PORTAL +static bool uri_for_local(str *local, bool search, struct camu_portal_bridge *bridge, str *uri, struct camu_post **selected) +{ + if (search || camu_is_url(local, 0)) { + str module; + str query; + al_str_from(&module, ""); + al_str_from(&query, ""); + if (al_str_cmp(local, al_str_c("https://twitter.com"), 0, 19) == 0 || + al_str_cmp(local, al_str_c("https://x.com"), 0, 13) == 0) { + al_str_cat(&query, al_str_c("tweet:")); + al_str_cat(&query, local); + al_str_cat(&module, al_str_c("twitter")); + } else if (al_str_cmp(local, al_str_c("https://instagram.com"), 0, 21) == 0) { + s32 slash = al_str_rfind(local, '/'); + if (slash >= 0) { + al_str_cat(&query, al_str_substr(local, slash + 1, local->len)); + } + al_str_cat(&module, al_str_c("instagram")); + } else { + if (search) { + al_str_cat(&query, al_str_substr(local, 1, local->len)); + } else { + al_str_cat(&query, al_str_c("link:")); + al_str_cat(&query, local); + } + al_str_cat(&module, al_str_c("youtube")); + } + s32 id = camu_portal_create_search(bridge, &module, &query); + al_str_free(&module); + al_str_free(&query); + if (id < 0) return false; + struct camu_search *search = camu_portal_get_search(bridge, id); + if (!search || !camu_search_get_page(search, 0)) { + al_log_info("local_compat", "Search failed."); + return false; + } + struct camu_result_page *page = &al_array_at(search->pages, 0); + struct camu_post *post; + al_array_foreach_ptr(page->posts, i, post) { + struct camu_post_media *media; + al_array_foreach_ptr(post->media, j, media) { + if (media->url.len > 0) { + al_str_clone(uri, &media->url); + *selected = post; + break; + } + } + if (*selected) break; + } + camu_portal_discard_search(bridge, id); + } + return *selected != NULL; +} +#endif + +static void worker_signal_callback(void *userdata) +{ + struct camu_local_compat *compat = (struct camu_local_compat *)userdata; + u32 size; + struct camu_server_resource *resource; + do { + camu_queue_try_pop(compat->pending, size, resource); + if (size == 0) break; + struct cch_handler *handler = resource->entry->handler; + if (al_str_eq(&handler->liana, al_str_c("codec"))) { + handler->maybe_spawn_worker(handler, 0); + } + } while (1); +} + +static aki_thread_result AKI_THREADCALL queue_thread(void *userdata) +{ + struct camu_local_compat *compat = (struct camu_local_compat *)userdata; +#ifdef CAMU_HAVE_PORTAL + bool have_python = false; +#endif + u32 count; + while (aki_packet_cache_wait(&compat->queue, &count)) { + struct aki_packet *packet = aki_packet_cache_pop(&compat->queue); + if (!packet) { + aki_packet_cache_unlock(&compat->queue); + break; + } + str local; + aki_packet_read_str(packet, &local); + struct cch_entry *entry = NULL; + struct camu_post *post = NULL; + if (al_str_cmp(&local, al_str_c("cdda://"), 0, 7) == 0) { + entry = cch_handler_cdio_create(); + struct cch_chapter *chapter = &al_array_at(entry->chapters, 0); + if (local.len > 7) { + s64 index = al_str_to_long(al_str_substr(&local, 7, local.len), 10); + if (index != INT64_MIN && index != INT64_MAX && index > 0 && index <= entry->chapters.size) { + chapter = &al_array_at(entry->chapters, index - 1); + } + } + entry->chapter = chapter; + entry->handler->maybe_spawn_worker(entry->handler, chapter->start); + } else { +#ifdef CAMU_HAVE_PORTAL +#ifndef AKIYO_HAS_CURL +#error "Curl required to use portal" +#endif + bool is_search = al_str_at(&local, 0) == ';'; + if (is_search || camu_is_url(&local, 0)) { + if (!have_python) { + // Defer python init. + have_python = camu_python_init(); + } + if (have_python) { + str uri; + if (uri_for_local(&local, is_search, &compat->bridge, &uri, &post)) { + entry = cch_handler_http_create(&uri, compat->server->loop); + } + } + } else { + entry = cch_handler_file_create(&local); + } +#else +#ifdef AKIYO_HAS_CURL + if (camu_is_url(&local, 0)) { + entry = cch_handler_http_create(&local, compat->server->loop); + } else { +#endif + entry = cch_handler_file_create(&local); +#ifdef AKIYO_HAS_CURL + } +#endif +#endif + } + aki_packet_cache_unlock(&compat->queue); + if (entry) { + struct camu_server_resource *resource = al_alloc_object(struct camu_server_resource); + al_str_clone(&resource->unique_id, &local); + resource->post = post; + resource->entry = entry; + resource->node = lia_server_create_node(&compat->server->data.server, entry); + camu_queue_push(compat->pending, resource); + aki_signal_send(&compat->worker_signal); + resource->duration = lia_node_get_duration(resource->node); + al_array_push(compat->server->data.resources, resource); + camu_queue_push(compat->results, resource); + aki_signal_send(&compat->result_signal); + } else { + al_log_info("local_compat", "No resource could be created for: %.*s.", AL_STR_PRINTF(&local)); + } + aki_packet_free(packet); + } +#ifdef CAMU_HAVE_PORTAL + if (have_python) camu_python_close(); +#endif + return 0; +} + +static void result_signal_callback(void *userdata) +{ + struct camu_local_compat *compat = (struct camu_local_compat *)userdata; + u32 size; + struct camu_server_resource *resource; + do { + camu_queue_try_pop(compat->results, size, resource); + if (size == 0) break; + compat->callback(compat->userdata, resource); + } while (1); +} + +void camu_local_compat_run(struct camu_local_compat *compat, struct camu_server *server) +{ + compat->server = server; +#ifdef CAMU_HAVE_PORTAL + camu_post_cache_init(&compat->cache); + camu_portal_init(&compat->bridge, &compat->cache); +#endif + // Size 0 to flush on the first packet. + aki_packet_cache_init(&compat->queue, 0); + aki_signal_init(&compat->worker_signal, worker_signal_callback, compat); + aki_signal_start(&compat->worker_signal, compat->server->loop); + camu_queue_init(compat->pending); + aki_signal_init(&compat->result_signal, result_signal_callback, compat); + aki_signal_start(&compat->result_signal, compat->server->loop); + camu_queue_init(compat->results); + aki_thread_create(&compat->thread, queue_thread, compat); +} + +void camu_local_compat_send(struct camu_local_compat *compat, struct aki_packet *packet) +{ + aki_packet_cache_send_packet(&compat->queue, packet); +} + +void camu_local_compat_stop(struct camu_local_compat *compat) +{ + aki_packet_cache_disable(&compat->queue); + aki_thread_join(&compat->thread); + aki_signal_stop(&compat->result_signal); + camu_queue_free(compat->results); + struct aki_packet *packet; + while ((packet = aki_packet_cache_pop(&compat->queue))) { + aki_packet_free(packet); + } +} diff --git a/src/server/local_compat.h b/src/server/local_compat.h index cdb88ce..dcd2fc7 100644 --- a/src/server/local_compat.h +++ b/src/server/local_compat.h @@ -1,68 +1,31 @@ #pragma once -#include "../portal/src/search.h" +#include <al/str.h> +#include <aki/packet_cache.h> +#include <aki/signal.h> -#ifdef AKIYO_HAS_CURL -#include "../cache/handlers/http.h" +#ifdef CAMU_HAVE_PORTAL +#include "../portal/src/search.h" #endif -#include "../cache/handlers/file.h" +#include "../util/queue.h" -static struct cch_entry *entry_for_external_path(struct camu_portal *portal, str *path) -{ - struct cch_entry *entry = NULL; -#ifdef AKIYO_HAS_CURL - bool is_search = al_str_at(path, 0) == ';'; - if (is_search || al_str_cmp(path, al_str_c("https://"), 0, 8) == 0 || - al_str_cmp(path, al_str_c("http://"), 0, 7) == 0) { - str module; - str query; - al_str_from(&module, ""); - al_str_from(&query, ""); - if (al_str_cmp(path, al_str_c("https://twitter.com"), 0, 19) == 0 || - al_str_cmp(path, al_str_c("https://x.com"), 0, 13) == 0) { - al_str_cat(&query, al_str_c("tweet:")); - al_str_cat(&query, path); - al_str_cat(&module, al_str_c("twitter")); - } else if (al_str_cmp(path, al_str_c("https://instagram.com"), 0, 21) == 0) { - s32 slash = al_str_rfind(path, '/'); - if (slash >= 0) { - al_str_cat(&query, al_str_substr(path, slash + 1, path->len)); - } - al_str_cat(&module, al_str_c("instagram")); - } else { - if (is_search) { - al_str_cat(&query, al_str_substr(path, 1, path->len)); - } else { - al_str_cat(&query, al_str_c("link:")); - al_str_cat(&query, path); - } - al_str_cat(&module, al_str_c("youtube")); - } - s32 id = camu_portal_create_search(portal, &module, &query); - al_str_free(&module); - al_str_free(&query); - if (id < 0) return NULL; - struct camu_search *search = camu_portal_get_search(portal, id); - if (!search || !camu_search_get_page(search, 0)) { - return NULL; - } - struct camu_result_page *page = &al_array_at(search->pages, 0); - struct camu_post *post; - al_array_foreach_ptr(page->posts, i, post) { - struct camu_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; - } - camu_portal_discard_search(portal, id); - if (entry) entry->handler->maybe_spawn_worker(entry->handler, 0); - } else // { +struct camu_server_resource; +struct camu_local_compat { + struct aki_thread thread; +#ifdef CAMU_HAVE_PORTAL + struct camu_portal_bridge bridge; + struct camu_post_cache cache; #endif - entry = cch_handler_file_create(path); - // } - return entry; -} + struct aki_packet_cache queue; + struct aki_signal worker_signal; + queue(struct camu_server_resource *) pending; + struct aki_signal result_signal; + queue(struct camu_server_resource *) results; + struct camu_server *server; + void (*callback)(void *, struct camu_server_resource *); + void *userdata; +}; + +void camu_local_compat_run(struct camu_local_compat *compat, struct camu_server *server); +void camu_local_compat_send(struct camu_local_compat *compat, struct aki_packet *packet); +void camu_local_compat_stop(struct camu_local_compat *compat); diff --git a/src/server/meson.build b/src/server/meson.build index d10b131..0617c2c 100644 --- a/src/server/meson.build +++ b/src/server/meson.build @@ -1,6 +1,10 @@ server_src = [ 'server.c', - 'db.c' + 'common.c', + 'list.c', + 'user.c', + 'db.c', + 'local_compat.c' ] -server_deps = [common_deps, portal, cache, shrub_server, list] -executable('server', sources: server_src, dependencies: server_deps) +server_deps = [common_deps, cache, liana_server] +server = declare_dependency(sources: server_src, dependencies: server_deps) diff --git a/src/server/server.c b/src/server/server.c index 4e11d3e..6c991eb 100644 --- a/src/server/server.c +++ b/src/server/server.c @@ -1,18 +1,17 @@ #include <al/log.h> +#include <al/lib.h> #include "../libsink/common.h" #include "server.h" #include "common.h" +#include "list.h" #include "db.h" -#ifdef CAMU_LOCAL_SOCKET -#include "local_compat.h" -#endif -static struct camu_user *get_user_by_username(struct camu_server *tree, str *username) +static struct camu_user *get_user_by_username(struct camu_server *server, str *username) { struct camu_user *user; - al_array_foreach(tree->users, i, user) { + al_array_foreach(server->users, i, user) { if (al_str_eq(&user->name, username)) { return user; } @@ -20,278 +19,303 @@ static struct camu_user *get_user_by_username(struct camu_server *tree, str *use return NULL; } -static void list_callback(void *userdata, u8 op, void *opaque, void *prev_opaque, s32 sequence) +static void list_sink_callback(void *userdata, u8 op, struct lia_list_entry *entry, s32 sequence, struct lia_timing *timing) { - struct camu_srv_sink *sink = (struct camu_srv_sink *)userdata; - struct camu_srv_resource *resource = (struct camu_srv_resource *)opaque; - struct camu_srv_resource *prev = (struct camu_srv_resource *)prev_opaque; - struct aki_packet *packet = aki_rpc_get_packet(sink->conn->rpc, op); - aki_packet_write_str(packet, CAMU_SERVER_IP); - aki_packet_write_s32(packet, CAMU_PORT); - aki_packet_write_u16(packet, resource->node->id); - aki_packet_write_s32(packet, sequence); - aki_packet_write_u64(packet, resource->node->start); - aki_packet_write_u8(packet, resource->node->paused != SHRUB_NOT_PAUSED); - if (prev) { - aki_packet_write_u64(packet, prev->node->paused_at); - } else { - aki_packet_write_u64(packet, 0); + struct camu_server_sink *sink = (struct camu_server_sink *)userdata; + switch (op) { + case CAMU_SINK_CLEAR: { + struct aki_packet *packet = aki_rpc_get_packet(sink->conn->rpc, CAMU_SINK_SET); + aki_packet_write_u8(packet, op); + aki_rpc_connection_command(sink->conn, packet, NULL, NULL); + break; + } + case CAMU_SINK_SET: + case CAMU_SINK_BUFFER: + case CAMU_SINK_BUFFER_AND_QUEUE: { + struct camu_server_resource *resource = (struct camu_server_resource *)entry->opaque; + struct aki_packet *packet = aki_rpc_get_packet(sink->conn->rpc, CAMU_SINK_SET); + aki_packet_write_u8(packet, op); + aki_packet_write_str(packet, &sink->server->addr); + aki_packet_write_u16(packet, CAMU_PORT); + aki_packet_write_u16(packet, resource->node->id); + aki_packet_write_s32(packet, sequence); + aki_packet_write_u64(packet, timing->at); + aki_packet_write_u64(packet, timing->seek_pos); + aki_packet_write_u8(packet, timing->pause); + aki_rpc_connection_command(sink->conn, packet, NULL, NULL); + break; + } + case CAMU_SINK_PAUSE: { + struct aki_packet *packet = aki_rpc_get_packet(sink->conn->rpc, CAMU_SINK_PAUSE); + aki_packet_write_s32(packet, sequence); + aki_packet_write_u64(packet, timing->at); + aki_packet_write_u8(packet, timing->pause); + aki_rpc_connection_command(sink->conn, packet, NULL, NULL); + break; + } + case CAMU_SINK_SEEK: { + struct aki_packet *packet = aki_rpc_get_packet(sink->conn->rpc, CAMU_SINK_SEEK); + aki_packet_write_s32(packet, sequence); + aki_packet_write_u64(packet, timing->at); + aki_packet_write_u64(packet, timing->seek_pos); + aki_rpc_connection_command(sink->conn, packet, NULL, NULL); + break; } - aki_rpc_connection_command(sink->conn, packet, NULL, NULL); - if (op == CAMU_SINK_SET) { - al_printf("Now Playing: %.*s\n", AL_STR_PRINTF(&resource->unique_id)); } } static bool identify_command_callback(void *userdata, struct aki_rpc_connection *conn, struct aki_packet *packet, struct aki_packet *rpacket) { - struct camu_server *srv = (struct camu_server *)userdata; + struct camu_server *server = (struct camu_server *)userdata; (void)rpacket; - switch (aki_packet_read_u8(packet)) { + + u8 op = aki_packet_read_u8(packet); + switch (op) { case CAMU_NODE: { - struct camu_srv_node *node = al_alloc_object(struct camu_srv_node); + struct camu_server_node *node = al_alloc_object(struct camu_server_node); node->conn = conn; - al_array_push(srv->nodes, node); + al_array_push(server->nodes, node); al_log_info("server", "New node."); break; } case CAMU_CLIENT: { + struct camu_server_client *client = al_alloc_object(struct camu_server_client); + client->conn = conn; str username; aki_packet_read_str(packet, &username); - struct camu_user *user = get_user_by_username(srv, &username); - if (user) { - struct camu_srv_client *client = al_alloc_object(struct camu_srv_client); - client->conn = conn; - client->user = user; - al_array_push(srv->clients, client); - //send_current_state(tree, user, conn); - al_log_info("server", "User \"%.*s\" logged in.", AL_STR_PRINTF(&user->name)); - } else { - al_log_info("server", "User \"%.*s\" not found.", AL_STR_PRINTF(&user->name)); + struct camu_user *user = get_user_by_username(server, &username); + if (!user) { + user = al_alloc_object(struct camu_user); + al_str_clone(&user->name, &username); + al_array_push(server->users, user); } + client->user = user; + al_array_push(server->clients, client); + al_log_info("server", "User \"%.*s\" logged in.", AL_STR_PRINTF(&user->name)); break; } case CAMU_SINK: { - struct camu_srv_sink *sink = al_alloc_object(struct camu_srv_sink); + struct camu_server_sink *sink = al_alloc_object(struct camu_server_sink); sink->conn = conn; - al_array_push(srv->sinks, sink); - struct camu_user *user = al_array_at(srv->users, 0); - struct camu_list *list = al_array_at(user->lists, 0); - camu_list_add_sink(list, list_callback, sink); + str name; + aki_packet_read_str(packet, &name); + al_str_clone(&sink->name, &name); + sink->server = server; + al_array_push(server->sinks, sink); + struct camu_list *list = al_array_at(server->lists, 0); + lia_list_add_sink(&list->impl, list_sink_callback, sink); al_log_info("server", "New sink."); break; } } + aki_packet_free(packet); return true; } -static void list_entry_callback(void *entry, u8 op, void *opaque) +static struct camu_server_client *get_client_by_connection(struct camu_server *server, struct aki_rpc_connection *conn) { - struct camu_srv_resource *resource = (struct camu_srv_resource *)entry; - switch (op) { - case CAMU_LIST_ENTRY_ID: - *(str **)opaque = &resource->unique_id; - break; - case CAMU_LIST_IMPULSE: - shrb_node_set_start(resource->node, *(u64 *)opaque); - break; - case CAMU_LIST_USER_PAUSE: - shrb_node_user_pause(resource->node, *(u64 *)opaque); - break; - case CAMU_LIST_USER_RESUME: { - struct shrb_resume_req *req = (struct shrb_resume_req *)opaque; - shrb_node_user_resume(resource->node, req); - break; + struct camu_server_client *client; + al_array_foreach(server->clients, i, client) { + if (client->conn == conn) return client; } - case CAMU_LIST_PAUSE: - shrb_node_pause(resource->node); - break; - case CAMU_LIST_SEEK: - shrb_node_seek(resource->node, *(u64 *)opaque); - break; + return NULL; +} + +static struct camu_list *get_list_from_name(struct camu_server *server, str *name) +{ + struct camu_list *list; + al_array_foreach(server->lists, i, list) { + if (al_str_eq(&list->name, name)) return list; } + return NULL; } -#ifdef CAMU_LOCAL_SOCKET -static void list_add_local(struct camu_server *srv, struct camu_list *list, str *line) +static struct camu_server_sink *get_sink_from_name(struct camu_server *server, str *name) { - struct cch_entry *entry = entry_for_external_path(&srv->portal, line); - if (entry) { - struct shrb_node *node = shrb_server_create_node(&srv->resource, 0, entry); - if (node) { - struct camu_srv_resource *resource = al_alloc_object(struct camu_srv_resource); - al_array_push(srv->resources, resource); - al_str_clone(&resource->unique_id, line); - resource->entry = entry; - resource->node = node; - camu_list_add(list, resource, list_entry_callback, resource->node->duration, false); - } + struct camu_server_sink *sink; + al_array_foreach(server->sinks, i, sink) { + if (al_str_eq(&sink->name, name)) return sink; + } + return NULL; +} + +static bool client_command_command_callback(void *userdata, struct aki_rpc_connection *conn, + struct aki_packet *packet, struct aki_packet *rpacket) +{ + struct camu_server *server = (struct camu_server *)userdata; + (void)rpacket; + + struct camu_server_client *client = get_client_by_connection(server, conn); + if (!client) goto out; + + u8 op = aki_packet_read_u8(packet); + switch (op) { + case CAMU_CLIENT_CREATE_LIST: { + str name; + aki_packet_read_str(packet, &name); + struct camu_list *list = al_alloc_object(struct camu_list); + camu_list_init(list, &name); + al_array_push(server->lists, list); + break; + } + case CAMU_CLIENT_ENABLE_SINK: { + str name; + aki_packet_read_str(packet, &name); + struct camu_list *list = get_list_from_name(server, &name); + aki_packet_read_str(packet, &name); + struct camu_server_sink *sink = get_sink_from_name(server, &name); + lia_list_add_sink(&list->impl, list_sink_callback, sink); + break; + } } + +out: + aki_packet_free(packet); + return true; } -#endif static bool list_action_command_callback(void *userdata, struct aki_rpc_connection *conn, struct aki_packet *packet, struct aki_packet *rpacket) { - struct camu_server *srv = (struct camu_server *)userdata; + struct camu_server *server = (struct camu_server *)userdata; (void)conn; (void)rpacket; - struct camu_user *user = al_array_at(srv->users, 0); - struct camu_list *list = al_array_at(user->lists, 0); + str name; + aki_packet_read_str(packet, &name); - switch (aki_packet_read_u8(packet)) { - case CAMU_ADD: { - str line; - aki_packet_read_str(packet, &line); -#ifdef CAMU_LOCAL_SOCKET - list_add_local(srv, list, &line); -#endif - break; + struct camu_list *list = get_list_from_name(server, &name); + if (!list) goto out; + + u8 op = aki_packet_read_u8(packet); + switch (op) { + case CAMU_LIST_ADD: { + camu_local_compat_send(&server->compat, packet); + return false; } - case CAMU_SKIP: { + case CAMU_LIST_SKIP: { s32 sequence = aki_packet_read_s32(packet); s32 n = aki_packet_read_s32(packet); - camu_list_skip(list, sequence, n); + lia_list_skip(&list->impl, sequence, n); break; } - case CAMU_SKIPTO: { + case CAMU_LIST_SKIPTO: { + s32 sequence = aki_packet_read_s32(packet); s32 i = aki_packet_read_s32(packet); - camu_list_skipto(list, i); + lia_list_skipto(&list->impl, sequence, i); + break; + } + case CAMU_LIST_SHUFFLE: { + lia_list_shuffle(&list->impl); break; } - case CAMU_TOGGLE_PAUSE: { + case CAMU_LIST_TOGGLE_PAUSE: { s32 sequence = aki_packet_read_s32(packet); - u64 pos = aki_packet_read_u64(packet); - camu_list_toggle_pause(list, sequence, pos); + f64 pts = aki_packet_read_f64(packet); + lia_list_toggle_pause(&list->impl, sequence, pts); break; } - case CAMU_SEEK: { + case CAMU_LIST_SEEK: { s32 sequence = aki_packet_read_s32(packet); f64 percent = aki_packet_read_f64(packet); - camu_list_seek(list, sequence, percent); + lia_list_seek(&list->impl, sequence, percent); + break; + } + case CAMU_LIST_UNSET: { + lia_list_unset(&list->impl); break; } - case CAMU_FINISHED: { + case CAMU_LIST_END: { s32 sequence = aki_packet_read_s32(packet); - camu_list_finished(list, sequence); + lia_list_end(&list->impl, sequence); break; } } +out: aki_packet_free(packet); return false; } static struct aki_rpc_command commands[] = { { .op = CAMU_SERVER_IDENTIFY, .callback = identify_command_callback, .userdata = NULL }, + { .op = CAMU_SERVER_CLIENT_COMMAND, .callback = client_command_command_callback, .userdata = NULL }, { .op = CAMU_SERVER_LIST_ACTION, .callback = list_action_command_callback, .userdata = NULL } }; static void connection_callback(void *userdata, struct aki_rpc_connection *conn) { + // TODO: Cleanup zombie connections. (void)userdata; (void)conn; } -static void cleanup_node(struct camu_srv_node *node) +static void cleanup_node(struct camu_server_node *node) { al_free(node); } -static void cleanup_client(struct camu_srv_client *client) +static void cleanup_client(struct camu_server_client *client) { al_free(client); } -static void cleanup_sink(struct camu_srv_sink *sink) +static void cleanup_sink(struct camu_server_sink *sink) { + al_str_free(&sink->name); al_free(sink); } static void connection_closed_callback(void *userdata, struct aki_rpc_connection *conn) { - struct camu_server *srv = (struct camu_server *)userdata; + struct camu_server *server = (struct camu_server *)userdata; - struct camu_srv_node *node; - al_array_foreach(srv->nodes, i, node) { + struct camu_server_node *node; + al_array_foreach(server->nodes, i, node) { if (node->conn == conn) { - al_log_info("server", "Node removed."); cleanup_node(node); - al_array_remove_at_iter(srv->nodes, i); + al_array_remove_at(server->nodes, i); + al_log_info("server", "Node removed."); break; } } - struct camu_srv_client *client; - al_array_foreach(srv->clients, i, client) { + struct camu_server_client *client; + al_array_foreach(server->clients, i, client) { if (client->conn == conn) { - al_log_info("server", "User \"%.*s\" logged out.", AL_STR_PRINTF(&client->user->name)); cleanup_client(client); - al_array_remove_at_iter(srv->clients, i); + al_array_remove_at(server->clients, i); + al_log_info("server", "User \"%.*s\" logged out.", AL_STR_PRINTF(&client->user->name)); break; } } - struct camu_srv_sink *sink; - al_array_foreach(srv->sinks, i, sink) { + struct camu_server_sink *sink; + al_array_foreach(server->sinks, i, sink) { if (sink->conn == conn) { - al_log_info("server", "Sink removed."); - al_array_remove_at_iter(srv->sinks, i); - struct camu_user *user = al_array_at(srv->users, 0); - struct camu_list *list = al_array_at(user->lists, 0); - camu_list_remove_sink(list, sink); + al_array_remove_at(server->sinks, i); + struct camu_list *list; + al_array_foreach(server->lists, j, list) { + lia_list_remove_sink(&list->impl, sink); + } cleanup_sink(sink); + al_log_info("server", "Sink removed."); break; } } } - -#ifdef CAMU_LOCAL_SOCKET -static u8 server_line_callback(void *userdata, str *line) -{ - struct camu_server *srv = (struct camu_server *)userdata; - struct camu_user *user = al_array_at(srv->users, 0); - struct camu_list *list = al_array_at(user->lists, 0); - if (al_str_eq(line, al_str_c(";NEXT"))) { - camu_list_skip(list, CAMU_SEQUENCE_INVALID, 1); - } else if (al_str_eq(line, al_str_c(";PREV"))) { - camu_list_skip(list, CAMU_SEQUENCE_INVALID, -1); - } else if (al_str_eq(line, al_str_c(";SHUFFLE"))) { - camu_list_shuffle(list); - } else if (al_str_eq(line, al_str_c(";SORT"))) { - camu_list_sort(list); - } else if (al_str_eq(line, al_str_c(";REVERSE"))) { - camu_list_reverse(list); - } else if (al_str_eq(line, al_str_c(";CLEAR"))) { - } else { - list_add_local(srv, list, line); - } - return AKI_LINE_PROCESSOR_CONTINUE; -} -#endif - -static void sigint_handler(s32 signum) -{ - (void)signum; - // explode. - exit(EXIT_FAILURE); -} - -static struct camu_server srv = { 0 }; - -static bool multiplex_callback(void *userdata, u8 id, struct aki_socket *s) +static bool multiplex_callback(void *userdata, u8 id, struct aki_socket *sock) { - struct camu_server *srv = (struct camu_server *)userdata; + struct camu_server *server = (struct camu_server *)userdata; switch (id) { case CAMU_MULTIPLEX_RPC: - aki_rpc_add_socket(&srv->server, s); + aki_rpc_add_socket(&server->server, sock); return true; - case CAMU_MULTIPLEX_SHRUB: - shrb_server_add_socket(&srv->resource, s); + case CAMU_MULTIPLEX_LIANA: + lia_server_add_socket(&server->data.server, sock); return true; default: break; @@ -299,56 +323,71 @@ static bool multiplex_callback(void *userdata, u8 id, struct aki_socket *s) return false; } -s32 main(void) +static void local_compat_callback(void *userdata, struct camu_server_resource *resource) { - aki_common_init(); - - signal(SIGINT, sigint_handler); - - al_array_init(srv.nodes); - al_array_init(srv.clients); - al_array_init(srv.sinks); - al_array_init(srv.users); - - if (!camu_db_open(&srv, CAMU_DB_PATH)) return EXIT_FAILURE; - - camu_post_cache_init(&srv.cache); - - bool py_init = camu_python_init(); + struct camu_server *server = (struct camu_server *)userdata; + struct camu_list *list = al_array_last(server->lists); + if (resource) lia_list_add(&list->impl, resource, resource->duration, &resource->unique_id); +} - camu_portal_init(&srv.portal, &srv.cache); +bool camu_server_init(struct camu_server *server, u8 type, struct aki_event_loop *loop) +{ + server->loop = loop; + server->addr = al_str_zero(); + al_array_init(server->nodes); + al_array_init(server->clients); + al_array_init(server->sinks); + al_array_init(server->users); + al_array_init(server->lists); - aki_event_loop_init(&srv.loop); + struct camu_list *list = al_alloc_object(struct camu_list); + camu_list_init(list, al_str_c("default")); + al_array_push(server->lists, list); - aki_rpc_init(&srv.server, &srv.loop, connection_callback, connection_closed_callback, &srv); + aki_rpc_init(&server->server, server->loop, connection_callback, connection_closed_callback, server); for (u32 i = 0; i < AL_ARRAY_SIZE(commands); i++) { - commands[i].userdata = &srv; - aki_rpc_add_command(&srv.server, &commands[i]); + commands[i].userdata = server; + aki_rpc_add_command(&server->server, &commands[i]); } - shrb_server_init(&srv.resource, &srv.loop); + lia_server_init(&server->data.server, server->loop); - aki_multiplex_socket_init(&srv.multi, AKI_SOCKET_TCP, multiplex_callback, &srv); - aki_multiplex_socket_listen(&srv.multi, &srv.loop, al_str_c("0.0.0.0"), CAMU_PORT); + server->compat.callback = local_compat_callback; + server->compat.userdata = server; + camu_local_compat_run(&server->compat, server); -#ifdef CAMU_LOCAL_SOCKET - srv.socket.type = AKI_SOCKET_UNIX; - aki_socket_init(&srv.socket); - aki_socket_set_blocking(&srv.socket, false); - srv.pro.callback = server_line_callback; - srv.pro.userdata = &srv; - aki_line_processor_init(&srv.pro, al_str_c("\n")); - aki_line_processor_open_socket(&srv.pro, &srv.socket); - if (aki_socket_listen(&srv.socket, al_str_c("/tmp/camu_sock"), 0)) { - aki_line_processor_run(&srv.pro, &srv.loop); - } -#endif + return aki_multiplex_socket_init(&server->multi, type, multiplex_callback, server); +} - aki_event_loop_run(&srv.loop); +bool camu_server_listen(struct camu_server *server, str *addr, u16 port) +{ + al_str_clone(&server->addr, addr); + if (server->multi.sock.type == AKI_SOCKET_TCP) addr = NULL; // any + return aki_multiplex_socket_listen(&server->multi, server->loop, addr, port); +} - if (py_init) camu_python_close(); +void camu_server_close(struct camu_server *server) +{ + camu_local_compat_stop(&server->compat); + lia_server_close(&server->data.server); + aki_multiplex_socket_close(&server->multi); +} - aki_common_close(); +void camu_server_free(struct camu_server *server) +{ + // This needs to happen in flight. + // Too many things can block us before we get here. + struct camu_server_resource *resource; + al_array_foreach(server->data.resources, i, resource) { + cch_entry_free(&resource->entry); + } + al_array_free(server->data.resources); + lia_server_free(&server->data.server); + aki_rpc_free(&server->server); + al_str_free(&server->addr); +} - return EXIT_SUCCESS; +void camu_server_local_add(struct camu_server *server, struct aki_packet *packet) +{ + camu_local_compat_send(&server->compat, packet); } diff --git a/src/server/server.h b/src/server/server.h index 9565bf6..528f08a 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -1,56 +1,57 @@ #pragma once -#define CAMU_LOCAL_SOCKET - #include <aki/multiplex.h> #include <aki/rpc2.h> -#ifdef CAMU_LOCAL_SOCKET -#include <aki/line_processor.h> -#endif -#include "../portal/src/search.h" #include "../cache/entry.h" -#include "../shrub/server.h" -#include "../list/list.h" +#include "../liana/server.h" -struct camu_srv_node { - struct aki_rpc_connection *conn; -}; +#include "user.h" +#include "local_compat.h" -struct camu_user { - str name; - array(struct camu_list *) lists; +struct camu_server_node { + struct aki_rpc_connection *conn; }; -struct camu_srv_client { +struct camu_server_client { struct aki_rpc_connection *conn; struct camu_user *user; }; -struct camu_srv_sink { +struct camu_server_sink { struct aki_rpc_connection *conn; + str name; + struct camu_server *server; }; -struct camu_srv_resource { +struct camu_server_resource { str unique_id; + struct camu_post *post; struct cch_entry *entry; - struct shrb_node *node; + struct lia_node *node; + u64 duration; }; struct camu_server { - struct aki_event_loop loop; + struct aki_event_loop *loop; + str addr; struct aki_multiplex_socket multi; struct aki_rpc server; - array(struct camu_srv_node *) nodes; - array(struct camu_srv_client *) clients; - array(struct camu_srv_sink *) sinks; + array(struct camu_server_node *) nodes; + array(struct camu_server_client *) clients; + array(struct camu_server_sink *) sinks; array(struct camu_user *) users; - struct shrb_server resource; - array(struct camu_srv_resource *) resources; - struct camu_portal portal; - struct camu_post_cache cache; -#ifdef CAMU_LOCAL_SOCKET - struct aki_socket socket; - struct aki_line_processor pro; -#endif + array(struct camu_list *) lists; + struct camu_local_compat compat; + struct { + struct lia_server server; + array(struct camu_server_resource *) resources; + } data; }; + +bool camu_server_init(struct camu_server *server, u8 type, struct aki_event_loop *loop); +bool camu_server_listen(struct camu_server *server, str *addr, u16 port); +void camu_server_close(struct camu_server *server); +void camu_server_free(struct camu_server *server); + +void camu_server_local_add(struct camu_server *server, struct aki_packet *packet); diff --git a/src/server/user.c b/src/server/user.c new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/server/user.c diff --git a/src/server/user.h b/src/server/user.h new file mode 100644 index 0000000..9800c45 --- /dev/null +++ b/src/server/user.h @@ -0,0 +1,8 @@ +#pragma once + +#include <al/str.h> +#include <al/array.h> + +struct camu_user { + str name; +}; diff --git a/src/shrub/client.c b/src/shrub/client.c deleted file mode 100644 index abe2d23..0000000 --- a/src/shrub/client.c +++ /dev/null @@ -1,283 +0,0 @@ -#include <al/log.h> - -#include "../codec/ffmpeg/packet_ext.h" -#include "../server/common.h" - -#include "handlers/codec.h" - -#include "client.h" -#include "handler.h" -#include "common.h" - -static void send_reconnect_packet(struct shrb_client *client) -{ - struct aki_packet *packet = aki_packet_create(); - aki_packet_write_u8(packet, SHRUB_CONTROL_RECONNECT); - aki_packet_write_u32(packet, client->level); - aki_packet_stream_send_packet(&client->control, packet); -} - -static void cleanup_connection_internal(struct shrb_client *client) -{ - client->callback(client->userdata, SHRUB_CLIENT_CLOSED, NULL, NULL); -} - -static void packet_sent_callback(void *userdata, struct aki_packet *packet) -{ - (void)userdata; - aki_packet_free(packet); -} - -static void data_packet_callback(void *userdata, struct aki_packet_stream *stream, - struct aki_packet *packet) -{ - struct shrb_client *client = (struct shrb_client *)userdata; - (void)stream; - shrb_vcr_push_packet(&client->vcr, packet); -} - -static void data_connection_callback(void *userdata, struct aki_packet_stream *stream) -{ - struct shrb_client *client = (struct shrb_client *)userdata; - (void)stream; - client->data.packet_callback = data_packet_callback; - client->data.packet_sent_callback = packet_sent_callback; - struct aki_packet *packet = aki_packet_create(); - aki_packet_write_u8(packet, SHRUB_CONNECTION_DATA); - aki_packet_write_u16(packet, client->connection_id); - aki_packet_write_u16(packet, client->node_id); - aki_packet_write_u64(packet, client->seek_pos); - aki_packet_stream_send_packet(&client->data, packet); -} - -static void data_connection_closed_callback(void *userdata, struct aki_packet_stream *stream) -{ - struct shrb_client *client = (struct shrb_client *)userdata; - (void)stream; - if (client->closed) { - if (!client->control.connected) cleanup_connection_internal(client); - return; - } - client->callback(client->userdata, SHRUB_CLIENT_REMOVE_BUFFERS, NULL, NULL); - shrb_vcr_flush(&client->vcr); - send_reconnect_packet(client); -} - -void connect_data(struct shrb_client *client) -{ - if (!client->reconnect) client->reconnect = true; - else aki_packet_stream_free(&client->data); - aki_packet_stream_init(&client->data, AKI_SOCKET_TCP, data_connection_callback, - data_connection_closed_callback, client); - aki_packet_stream_set_multiplex(&client->data, CAMU_MULTIPLEX_SHRUB); - aki_packet_stream_connect(&client->data, client->loop, &client->addr, client->port); -} - -static void parse_info_packet(struct shrb_client *client, struct aki_packet *packet) -{ - client->duration = aki_packet_read_u64(packet); - u32 count = aki_packet_read_u32(packet); - for (u32 i = 0; i < count; i++) { - struct shrb_vcr_stream *stream = al_alloc_object(struct shrb_vcr_stream); - stream->client = shrb_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 = SHRUB_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 CAMU_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 = SHRUB_STREAM_AUDIO; - al_array_push(client->subscribed, stream->index); - break; - case AVMEDIA_TYPE_VIDEO: - stream->type = SHRUB_STREAM_VIDEO; - al_array_push(client->subscribed, stream->index); - break; - case AVMEDIA_TYPE_SUBTITLE: - default: - stream->type = SHRUB_STREAM_UNKNOWN; - break; - } - break; - } -#endif - } - if (!stream->client->init(stream->client, NULL, stream)) { - // TODO: Handle this. - } - shrb_vcr_add_stream(&client->vcr, stream); - } -} - -static void disconnect_data_internal(struct shrb_client *client) -{ - client->corked = false; - aki_packet_stream_disconnect(&client->data); -} - -static void send_subscribe_packet(struct shrb_client *client) -{ - struct aki_packet *packet = aki_packet_create(); - aki_packet_write_u8(packet, SHRUB_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 parse_start_packet(struct shrb_client *client, struct aki_packet *packet) -{ - u64 ts = aki_packet_read_u64(packet); - u64 delay = aki_packet_read_u64(packet); - u64 seek_pos = aki_packet_read_u64(packet); - u8 paused = aki_packet_read_u8(packet); - u64 paused_at = aki_packet_read_u64(packet); - struct shrb_seek_req req = { - .base = seek_pos, - .ts = ts, - .delay = delay, - .paused = paused, - .paused_at = paused_at - }; - client->callback(client->userdata, SHRUB_CLIENT_SET, NULL, &req); - client->seek_pos = req.base; -} - -static void control_packet_callback(void *userdata, struct aki_packet_stream *stream, - struct aki_packet *packet) -{ - struct shrb_client *client = (struct shrb_client *)userdata; - (void)stream; - - switch (aki_packet_read_u8(packet)) { - case SHRUB_CONTROL_INFO: - client->connection_id = aki_packet_read_u16(packet); - parse_info_packet(client, packet); - send_subscribe_packet(client); - break; - case SHRUB_CONTROL_START: - parse_start_packet(client, packet); - connect_data(client); - break; - case SHRUB_CONTROL_PAUSE: { - f64 pts = aki_packet_read_u64(packet) / 1000000.f; - client->callback(client->userdata, SHRUB_CLIENT_PAUSE, NULL, &pts); - break; - } - case SHRUB_CONTROL_RESUME: { - u64 start = aki_packet_read_u64(packet); - u64 offset = aki_packet_read_u64(packet); - struct shrb_resume_req req = { - .start = start, - .offset = offset - }; - client->callback(client->userdata, SHRUB_CLIENT_RESUME, NULL, &req); - break; - } - case SHRUB_CONTROL_SEEK: - client->level = 0; - disconnect_data_internal(client); - break; - default: - break; - } - - aki_packet_free(packet); -} - -static void control_connection_callback(void *userdata, struct aki_packet_stream *stream) -{ - struct shrb_client *client = (struct shrb_client *)userdata; - (void)stream; - client->control.packet_callback = control_packet_callback; - client->control.packet_sent_callback = packet_sent_callback; - struct aki_packet *packet = aki_packet_create(); - aki_packet_write_u8(packet, SHRUB_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) -{ - struct shrb_client *client = (struct shrb_client *)userdata; - (void)stream; - if (client->closed) { - if (!client->data.connected) cleanup_connection_internal(client); - return; - } - // TODO: Reconnect. -} - -bool shrb_client_connect(struct shrb_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->closed = false; - client->reconnect = false; - client->level = 0; - client->corked = false; - al_array_init(client->subscribed); - shrb_vcr_init(&client->vcr, client->loop, &client->data); - if (!aki_packet_stream_init(&client->control, AKI_SOCKET_TCP, control_connection_callback, - control_connection_closed_callback, client)) { - return false; - } - aki_packet_stream_set_nodelay(&client->control, 1); - aki_packet_stream_set_multiplex(&client->control, CAMU_MULTIPLEX_SHRUB); - aki_packet_stream_connect(&client->control, client->loop, &client->addr, client->port); - return true; -} - -void shrb_client_reseek(struct shrb_client *client) -{ - client->level++; - al_log_debug("bimu", "Attempting re-seek at level %i.", client->level); - disconnect_data_internal(client); -} - -void shrb_client_seek(struct shrb_client *client, f64 percent) -{ - u64 pos = client->duration * percent; - struct aki_packet *packet = aki_packet_create(); - aki_packet_write_u8(packet, SHRUB_CONTROL_SEEK); - aki_packet_write_u64(packet, pos); - aki_packet_stream_send_packet(&client->control, packet); -} - -void shrb_client_close(struct shrb_client *client) -{ - struct shrb_vcr_stream *stream; - al_array_foreach(client->vcr.streams, i, stream) { - shrb_vcr_stream_close(stream); - } - client->closed = true; - aki_packet_stream_disconnect(&client->control); - aki_packet_stream_disconnect(&client->data); -} - -void shrb_client_free(struct shrb_client *client) -{ - aki_packet_stream_free(&client->control); - aki_packet_stream_free(&client->data); - al_str_free(&client->addr); - al_array_free(client->subscribed); - shrb_vcr_free(&client->vcr); -} diff --git a/src/shrub/client.h b/src/shrub/client.h deleted file mode 100644 index 14f7da2..0000000 --- a/src/shrub/client.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include <al/types.h> -#include <aki/packet_stream.h> - -#include "vcr.h" - -struct shrb_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 closed; - bool reconnect; - u32 level; - bool corked; - u64 duration; - u64 seek_pos; - array(s32) subscribed; - struct shrb_vcr vcr; - void (*callback)(void *, u8, struct shrb_vcr_stream *stream, void *); - void *userdata; -}; - -bool shrb_client_connect(struct shrb_client *client, struct aki_event_loop *loop, str *addr, s32 port, u16 node_id); -void shrb_client_toggle_pause(struct shrb_client *client, f64 pts); -void shrb_client_reseek(struct shrb_client *client); -void shrb_client_seek(struct shrb_client *client, f64 percent); -void shrb_client_close(struct shrb_client *client); -void shrb_client_free(struct shrb_client *client); diff --git a/src/shrub/common.h b/src/shrub/common.h deleted file mode 100644 index 882eb49..0000000 --- a/src/shrub/common.h +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -enum { - SHRUB_STREAM_AUDIO = 0, - SHRUB_STREAM_VIDEO, - SHRUB_STREAM_UNKNOWN, -}; - -enum { - SHRUB_CONNECTION_CONTROL = 0, - SHRUB_CONNECTION_DATA -}; - -enum { - SHRUB_CONTROL_INFO = 0, - SHRUB_CONTROL_SUBSCRIBE, - SHRUB_CONTROL_RECONNECT, - SHRUB_CONTROL_START, - SHRUB_CONTROL_PAUSE, - SHRUB_CONTROL_RESUME, - SHRUB_CONTROL_SEEK -}; - -#define SHRUB_BASE_DELAY 500000Lu -#define SHRUB_PAUSE_DELAY 60000Lu -#define SHRUB_DELAY_IGNORE 0Lu diff --git a/src/shrub/handler.h b/src/shrub/handler.h deleted file mode 100644 index 7242130..0000000 --- a/src/shrub/handler.h +++ /dev/null @@ -1,59 +0,0 @@ -#pragma once - -#include <aki/packet_pool.h> - -#include "../codec/codec.h" -#include "../cache/handle.h" - -#include "vcr.h" - -struct shrb_server_handler { - bool (*init)(struct shrb_server_handler *, struct cch_handle *, struct aki_packet_pool *); - void (*write_info)(struct shrb_server_handler *, struct aki_packet *); - void (*subscribe)(struct shrb_server_handler *, s32, bool); - u64 (*get_duration)(struct shrb_server_handler *); - bool (*seek)(struct shrb_server_handler *, u64); - bool (*step)(struct shrb_server_handler *); - void (*free)(struct shrb_server_handler **); -}; - -enum { - SHRUB_PACKET_DATA = 0, - SHRUB_PACKET_EOF, - SHRUB_PACKET_ERROR -}; - -enum { - SHRUB_CLIENT_CONFIGURE = 0, - SHRUB_CLIENT_SET, - SHRUB_CLIENT_PAUSE, - SHRUB_CLIENT_RESUME, - SHRUB_CLIENT_DATA, - SHRUB_CLIENT_REMOVE_BUFFERS, - SHRUB_CLIENT_EOF, - SHRUB_CLIENT_CLOSED -}; - -struct shrb_resume_req { - u64 start; - u64 offset; -}; - -struct shrb_seek_req { - u64 base; - u64 ts; - u64 delay; - bool paused; - u64 paused_at; -}; - -struct shrb_client_stream; -struct shrb_client_handler { - bool (*init)(struct shrb_client_handler *, struct camu_renderer *, struct shrb_vcr_stream *); - void (*handle_packet)(struct shrb_client_handler *, struct aki_packet *); - void (*flush)(struct shrb_client_handler *); - void (*free)(struct shrb_client_handler **); - struct shrb_vcr_stream *stream; - void (*callback)(void *, u8, struct shrb_vcr_stream *stream, void *); - void *userdata; -}; diff --git a/src/shrub/handlers/cdio.h b/src/shrub/handlers/cdio.h deleted file mode 100644 index 0f614af..0000000 --- a/src/shrub/handlers/cdio.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include <cdio/paranoia/cdda.h> -#include <cdio/cd_types.h> - -#include "../handler.h" - -struct bmu_cdio_server { - struct bmu_server_handler handler; -}; - -struct bmu_cdio_client { - struct bmu_client_handler handler; -}; - -struct bmu_server_handler *bmu_cdio_server_create(void); -struct bmu_client_handler *bmu_cdio_client_create(void); diff --git a/src/shrub/handlers/cdio_server.c b/src/shrub/handlers/cdio_server.c deleted file mode 100644 index 151b94f..0000000 --- a/src/shrub/handlers/cdio_server.c +++ /dev/null @@ -1,21 +0,0 @@ -#include "cdio.h" - -static bool cdio_server_init(struct bmu_server_handler *handler, struct cch_handle *handle, - struct aki_packet_pool *pool) -{ - struct bmu_cdio_server *cdio = (struct bmu_cdio_server *)handler; - return true; -} - -struct bmu_server_handler *bmu_cdio_server_create(void) -{ - struct bmu_cdio_server *cdio = al_alloc_object(struct bmu_cdio_server); - cdio->handler.init = cdio_server_init; -// cdio->handler.write_info = cdio_server_write_info; -// cdio->handler.subscribe = cdio_server_subscribe; -// cdio->handler.get_duration = cdio_server_get_duration; -// cdio->handler.seek = cdio_server_seek; -// cdio->handler.step = cdio_server_step; -// cdio->handler.free = cdio_server_free; - return (struct bmu_server_handler *)cdio; -} diff --git a/src/shrub/handlers/codec.h b/src/shrub/handlers/codec.h deleted file mode 100644 index f17b17d..0000000 --- a/src/shrub/handlers/codec.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "../handler.h" - -#include "../../codec/codec.h" - -struct shrb_codec_server { - struct shrb_server_handler handler; - struct camu_demuxer *demux; - struct camu_packet packet; - struct aki_packet_pool *pool; -}; - -struct shrb_codec_client { - struct shrb_client_handler handler; - struct camu_decoder *dec; -}; - -struct shrb_server_handler *shrb_codec_server_create(void); -struct shrb_client_handler *shrb_codec_client_create(void); diff --git a/src/shrub/handlers/codec_client.c b/src/shrub/handlers/codec_client.c deleted file mode 100644 index f55a740..0000000 --- a/src/shrub/handlers/codec_client.c +++ /dev/null @@ -1,102 +0,0 @@ -#include "../../codec/codec.h" -#include "../../codec/ffmpeg/decoder.h" -#include "../../codec/ffmpeg/packet_ext.h" - -#include "../vcr.h" - -#include "codec.h" - -static void data_callback(void *userdata, struct camu_frame *frame) -{ - struct shrb_codec_client *codec = (struct shrb_codec_client *)userdata; - codec->handler.callback(codec->handler.userdata, SHRUB_CLIENT_DATA, codec->handler.stream, frame); -} - -static bool codec_client_init(struct shrb_client_handler *handler, struct camu_renderer *renderer, - struct shrb_vcr_stream *stream) -{ - struct shrb_codec_client *codec = (struct shrb_codec_client *)handler; - codec->dec = camu_ff_decoder_create(); - codec->handler.stream = stream; - if (!codec->dec->init(codec->dec, renderer, &stream->stream)) { - return false; - } - codec->dec->stream = &stream->stream; - codec->handler.callback(codec->handler.userdata, SHRUB_CLIENT_CONFIGURE, codec->handler.stream, NULL); - codec->dec->set_callback(codec->dec, data_callback, codec); - return true; -} - -#ifdef CAMU_HAVE_FFMPEG -static void push_av_packet(struct shrb_codec_client *codec, AVPacket *pkt) -{ - struct camu_packet packet; - packet.av.pkt = pkt; - s32 ret = codec->dec->push(codec->dec, &packet); - av_packet_unref(pkt); - av_packet_free(&pkt); - al_assert(ret == CAMU_OK); -} -#endif - -static void push_packet(struct shrb_codec_client *codec, struct aki_buffer *buffer) -{ - struct camu_packet packet; - packet.buffer = buffer; - s32 ret = codec->dec->push(codec->dec, &packet); - al_assert(ret == CAMU_OK); -} - -static void codec_client_handle_packet(struct shrb_client_handler *handler, struct aki_packet *packet) -{ - struct shrb_codec_client *codec = (struct shrb_codec_client *)handler; - if (!packet) { - struct shrb_codec_client *codec = (struct shrb_codec_client *)handler; - s32 ret = codec->dec->push(codec->dec, NULL); - // Flush returns success. - ret = codec->dec->process(codec->dec); - al_assert(ret == CAMU_ERR_EOF); - codec->handler.callback(codec->handler.userdata, SHRUB_CLIENT_EOF, codec->handler.stream, NULL); - return; - } - switch (aki_packet_read_u8(packet)) { - case CAMU_NORMAL: { - struct aki_buffer buffer; - aki_packet_read_buffer(packet, &buffer); - push_packet(codec, &buffer); - break; - } -#ifdef CAMU_HAVE_FFMPEG - case CAMU_FFMPEG_COMPAT: { - push_av_packet(codec, aki_packet_read_av_packet(packet)); - break; - } -#endif - } - s32 ret = codec->dec->process(codec->dec); - al_assert(ret == CAMU_ERR_AGAIN || ret == CAMU_ERR_EOF); -} - -static void codec_client_flush(struct shrb_client_handler *handler) -{ - struct shrb_codec_client *codec = (struct shrb_codec_client *)handler; - codec->dec->flush(codec->dec); -} - -static void codec_client_free(struct shrb_client_handler **handler) -{ - struct shrb_codec_client *codec = (struct shrb_codec_client *)*handler; - codec->dec->free(&codec->dec); - al_free(codec); - *handler = NULL; -} - -struct shrb_client_handler *shrb_codec_client_create(void) -{ - struct shrb_codec_client *codec = al_alloc_object(struct shrb_codec_client); - codec->handler.init = codec_client_init; - codec->handler.handle_packet = codec_client_handle_packet; - codec->handler.flush = codec_client_flush; - codec->handler.free = codec_client_free; - return (struct shrb_client_handler *)codec; -} diff --git a/src/shrub/handlers/codec_server.c b/src/shrub/handlers/codec_server.c deleted file mode 100644 index cab615a..0000000 --- a/src/shrub/handlers/codec_server.c +++ /dev/null @@ -1,140 +0,0 @@ -#include "../../codec/codec.h" -#include "../../codec/ffmpeg/demuxer.h" -#include "../../codec/ffmpeg/packet_ext.h" - -#include "../handler.h" - -#include "codec.h" - -static bool codec_server_init(struct shrb_server_handler *handler, struct cch_handle *handle, - struct aki_packet_pool *pool) -{ - struct shrb_codec_server *codec = (struct shrb_codec_server *)handler; - codec->demux = camu_ff_demuxer_create(); - if (!codec->demux->init(codec->demux, handle)) { - return false; - } - switch (codec->demux->type) { - case CAMU_NORMAL: - break; -#ifdef CAMU_HAVE_FFMPEG - case CAMU_FFMPEG_COMPAT: - codec->packet.av.pkt = av_packet_alloc(); - break; -#endif - } - codec->pool = pool; - return true; -} - -static void codec_server_write_info(struct shrb_server_handler *handler, struct aki_packet *packet) -{ - struct shrb_codec_server *codec = (struct shrb_codec_server *)handler; - aki_packet_write_u64(packet, codec->demux->get_duration(codec->demux)); - aki_packet_write_u32(packet, codec->demux->streams.size); - struct camu_stream *stream; - al_array_foreach_ptr(codec->demux->streams, i, stream) { - aki_packet_write_u8(packet, stream->type); - switch (stream->type) { - case CAMU_NORMAL: - aki_packet_write_s32(packet, stream->video.width); - aki_packet_write_s32(packet, stream->video.height); - break; -#ifdef CAMU_HAVE_FFMPEG - case CAMU_FFMPEG_COMPAT: - aki_packet_write_av_codec_id(packet, stream->av.stream->codecpar->codec_id); - aki_packet_write_av_stream(packet, stream->av.stream); - break; -#endif - } - } -} - -static void codec_server_subscribe(struct shrb_server_handler *handler, s32 index, bool subscribe) -{ - struct shrb_codec_server *codec = (struct shrb_codec_server *)handler; - if (subscribe) { - al_array_push(codec->demux->subscribed, index); - } -} - -static u64 codec_server_get_duration(struct shrb_server_handler *handler) -{ - struct shrb_codec_server *codec = (struct shrb_codec_server *)handler; - return codec->demux->get_duration(codec->demux); -} - -static bool codec_server_seek(struct shrb_server_handler *handler, u64 pos) -{ - struct shrb_codec_server *codec = (struct shrb_codec_server *)handler; - return codec->demux->seek(codec->demux, pos); -} - -static bool codec_server_step(struct shrb_server_handler *handler) -{ - struct shrb_codec_server *codec = (struct shrb_codec_server *)handler; - struct aki_packet *packet = aki_packet_pool_get(codec->pool); - if (!packet) return false; - s32 ret = codec->demux->get_packet(codec->demux, &codec->packet); - if (ret == CAMU_OK) { - aki_packet_write_u8(packet, 0); - switch (codec->packet.type) { - case CAMU_NORMAL: { - aki_packet_write_s32(packet, SHRUB_PACKET_DATA); - aki_packet_write_u8(packet, codec->packet.type); - aki_packet_write_buffer(packet, codec->packet.buffer); - break; - } -#ifdef CAMU_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, 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) { - aki_packet_write_u8(packet, SHRUB_PACKET_EOF); - aki_packet_pool_submit(codec->pool, packet); - return false; - } else { - aki_packet_write_u8(packet, SHRUB_PACKET_ERROR); - aki_packet_pool_submit(codec->pool, packet); - return false; - } - return true; -} - -static void codec_server_free(struct shrb_server_handler **handler) -{ - struct shrb_codec_server *codec = (struct shrb_codec_server *)*handler; - switch (codec->demux->type) { - case CAMU_NORMAL: - break; -#ifdef CAMU_HAVE_FFMPEG - case CAMU_FFMPEG_COMPAT: - av_packet_free(&codec->packet.av.pkt); - break; -#endif - } - codec->demux->free(&codec->demux); - al_free(codec); - *handler = NULL; -} - -struct shrb_server_handler *shrb_codec_server_create(void) -{ - struct shrb_codec_server *codec = al_alloc_object(struct shrb_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; - codec->handler.free = codec_server_free; - return (struct shrb_server_handler *)codec; -} diff --git a/src/shrub/handlers/dvd.h b/src/shrub/handlers/dvd.h deleted file mode 100644 index db9bfe9..0000000 --- a/src/shrub/handlers/dvd.h +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#include "../handler.h" - -struct bmu_dvd_server { - struct bmu_server_handler handler; -}; - -struct bmu_dvd_client { - struct bmu_client_handler handler; -}; - -struct bmu_server_handler *bmu_dvd_server_create(void); -struct bmu_client_handler *bmu_dvd_client_create(void); diff --git a/src/shrub/meson.build b/src/shrub/meson.build deleted file mode 100644 index a3fc77b..0000000 --- a/src/shrub/meson.build +++ /dev/null @@ -1,29 +0,0 @@ -shrub_server_src = [ - 'server.c', - 'handlers/codec_server.c' -] -shrub_client_src = [ - 'client.c', - 'vcr.c', - 'handlers/codec_client.c' -] -shrub_server_deps = [ffmpeg_server] -shrub_client_deps = [ffmpeg_client] - -#libcdio_paranoia = dependency('libcdio_paranoia', required: false, allow_fallback: true) -#libcdio_cdda = dependency('libcdio_cdda', required: false, allow_fallback: true) -#if libcdio_paranoia.found() and libcdio_cdda.found() -# shrub_server_src += ['handlers/cdio_server.c'] -# shrub_server_deps += [libcdio_paranoia, libcdio_cdda] -#endif -# -#libdvdcss = dependency('libdvdcss', required: false, allow_fallback: true) -#libdvdread = dependency('libdvdread', required: false, allow_fallback: true) -#libdvdnav = dependency('libdvdnav', required: false, allow_fallback: true) -#if libdvdcss.found() and libdvdread.found() and libdvdnav.found() -# shrub_server_src += ['handlers/dvd_server.c'] -# shrub_server_deps += [libdvdcss] -#endif - -shrub_server = declare_dependency(sources: shrub_server_src, dependencies: shrub_server_deps) -shrub_client = declare_dependency(sources: shrub_client_src, dependencies: shrub_client_deps) diff --git a/src/shrub/server.c b/src/shrub/server.c deleted file mode 100644 index 31d8787..0000000 --- a/src/shrub/server.c +++ /dev/null @@ -1,401 +0,0 @@ -#include <al/random.h> -#include <aki/common.h> - -#include "handlers/codec.h" - -#include "server.h" -#include "common.h" - -static void control_connection_closed_callback(void *userdata, struct aki_packet_stream *stream) -{ - struct shrb_node_connection *connection = (struct shrb_node_connection *)userdata; - (void)stream; - struct shrb_node_connection *rconnection; - al_array_foreach(connection->node->connections, i, rconnection) { - if (rconnection == connection) { - al_array_remove_at_iter(connection->node->connections, i); - break; - } - } - aki_packet_stream_free(connection->control); - connection->control = NULL; - //cch_entry_return_handle(connection->node->entry, &connection->handle); - //connection->handler->free(&connection->handler); - //aki_packet_pool_free(&connection->pool); -} - -static void data_connection_closed_callback(void *userdata, struct aki_packet_stream *stream) -{ - struct shrb_node_connection *connection = (struct shrb_node_connection *)userdata; - (void)stream; - connection->running = 0; - aki_packet_pool_disable(&connection->pool); - aki_thread_join(&connection->thread); - aki_packet_pool_enable(&connection->pool); - aki_packet_stream_free(connection->data); - connection->data = NULL; -} - -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 shrb_node_connection *connection = (struct shrb_node_connection *)userdata; - aki_packet_pool_return(&connection->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) -{ - struct shrb_node_connection *connection = (struct shrb_node_connection *)userdata; - if (connection->data->connected) { - aki_packet_stream_send_packet(connection->data, packet); - return AKI_PACKET_POOL_KEEP; - } - return AKI_PACKET_POOL_RETURN; -} - -static void send_resume_packet(struct shrb_node_connection *connection, u64 start, u64 offset) -{ - struct aki_packet *packet = aki_packet_create(); - aki_packet_write_u8(packet, SHRUB_CONTROL_RESUME); - aki_packet_write_u64(packet, start); - aki_packet_write_u64(packet, offset); - aki_packet_stream_send_packet(connection->control, packet); -} - -static void send_pause_packet(struct shrb_node_connection *connection, u64 seek_pos) -{ - struct aki_packet *packet = aki_packet_create(); - aki_packet_write_u8(packet, SHRUB_CONTROL_PAUSE); - aki_packet_write_u64(packet, seek_pos); - aki_packet_stream_send_packet(connection->control, packet); -} - -static struct shrb_node *get_node_by_id(struct shrb_server *server, u16 id) -{ - struct shrb_node *node; - al_array_foreach(server->nodes, i, node) { - if (node->id == id) return node; - } - return NULL; -} - -// Possible connection senarios: -// - initial connection and start -// - connected while playing -// - connected while paused -// - connected while queued -// - connected while queued after a paused stream -// Questions: -// - can the server estimate the position of the stream? I think yes - -static void send_start_packet(struct shrb_node_connection *connection) -{ - struct shrb_node *node = connection->node; - struct aki_packet *packet = aki_packet_create(); - aki_packet_write_u8(packet, SHRUB_CONTROL_START); - u64 seek_pos = node->seek_pos; - u64 delay = SHRUB_BASE_DELAY * powl(3, connection->level); - u64 start = node->start; - bool paused = node->paused != SHRUB_NOT_PAUSED; - if (!paused) { - s64 diff = aki_get_timestamp() - start; - if (diff > (s64)SHRUB_BASE_DELAY) { - start += diff; - seek_pos += diff; - } - } - // Start is not used if the node is paused. - aki_packet_write_u64(packet, start); - seek_pos += delay - SHRUB_BASE_DELAY; - aki_packet_write_u64(packet, delay); - aki_packet_write_u64(packet, seek_pos); - aki_packet_write_u8(packet, paused); - aki_packet_write_u64(packet, node->paused_at); - aki_packet_stream_send_packet(connection->control, packet); -} - -static void send_seek_packet(struct shrb_node_connection *connection, u64 pos) -{ - connection->seek_pos = pos; - struct aki_packet *packet = aki_packet_create(); - aki_packet_write_u8(packet, SHRUB_CONTROL_SEEK); - 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 shrb_node_connection *connection = (struct shrb_node_connection *)userdata; - (void)stream; - - switch (aki_packet_read_u8(packet)) { - case SHRUB_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 SHRUB_CONTROL_RECONNECT: { - connection->level = aki_packet_read_u32(packet); - send_start_packet(connection); - break; - } - case SHRUB_CONTROL_SEEK: { - u64 pos = aki_packet_read_u64(packet); - connection->node->seek_pos = pos; - connection->node->seeked = true; - //send_seek_packet(connection->node); - break; - } - } - aki_packet_free(packet); -} - -static struct shrb_node_connection *get_connection_by_id(struct shrb_node *node, u16 id) -{ - struct shrb_node_connection *connection; - al_array_foreach(node->connections, i, connection) { - if (connection->id == id) return connection; - } - return NULL; -} - -static bool send_control_info_packet(struct shrb_node_connection *connection) -{ - connection->handler = shrb_codec_server_create(); - if (!connection->handler->init(connection->handler, &connection->handle, &connection->pool)) { - return false; - } - struct aki_packet *packet = aki_packet_create(); - aki_packet_write_u8(packet, SHRUB_CONTROL_INFO); - aki_packet_write_u16(packet, connection->id); - connection->handler->write_info(connection->handler, packet); - aki_packet_stream_send_packet(connection->control, packet); - return true; -} - -static aki_thread_result AKI_THREADCALL handler_thread(void *userdata) -{ - struct shrb_node_connection *connection = (struct shrb_node_connection *)userdata; - while (connection->running && connection->handler->step(connection->handler)) {} - return 0; -} - -static void maybe_start_handler(struct shrb_node_connection *connection) -{ - al_assert(!connection->running); - // Don't seek to 0 unless node->seeked is set. - if (connection->seek_pos > 0 || connection->node->seeked) { - connection->handler->seek(connection->handler, connection->seek_pos); - } - connection->running = 1; - aki_thread_create(&connection->thread, handler_thread, connection); -} - -static void identify_packet_callback(void *userdata, struct aki_packet_stream *stream, struct aki_packet *packet) -{ - struct shrb_server *server = (struct shrb_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); - - // Before this point: - // - We get socket from accept - // - Create packet_stream - // - connection_closed_callback could have been called before this point - // - the socket exists only to wait for an identify 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 shrb_node *node = get_node_by_id(server, node_id); - al_assert(node); - - struct shrb_node_connection *connection = get_connection_by_id(node, connection_id); - if (!connection) { - al_assert(type == SHRUB_CONNECTION_CONTROL); - connection = al_alloc_object(struct shrb_node_connection); - al_array_push(node->connections, connection); - connection->id = node->connection_id++; - connection->node = node; - connection->control = NULL; - connection->data = NULL; - aki_packet_pool_init(&connection->pool, 16, server->loop, packet_pool_callback, connection); - cch_entry_get_handle(connection->node->entry, &connection->handle); - connection->level = 0; - connection->running = 0; - } - - switch (type) { - case SHRUB_CONNECTION_CONTROL: - aki_packet_stream_set_nodelay(stream, 1); - 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; - if (!send_control_info_packet(connection)) { - aki_packet_stream_disconnect(stream); - } - break; - case SHRUB_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? - // ^^^^ Literally any connection could call itself data, this needs to be handled better. - al_assert(!connection->data); - connection->seek_pos = aki_packet_read_u64(packet); - 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_callback(void *userdata, struct aki_packet_stream *stream) -{ - struct shrb_server *server = (struct shrb_server *)userdata; - stream->packet_callback = identify_packet_callback; - stream->packet_sent_callback = default_packet_sent_callback; - stream->userdata = server; -} - -bool shrb_server_init(struct shrb_server *server, struct aki_event_loop *loop) -{ - server->loop = loop; - al_array_init(server->nodes); - return true; -} - -static void connection_closed_callback(void *userdata, struct aki_packet_stream *stream) -{ - (void)userdata; - (void)stream; -} - -void shrb_server_add_socket(struct shrb_server *server, struct aki_socket *s) -{ - struct aki_packet_stream *stream = al_alloc_object(struct aki_packet_stream); - stream->connection_callback = connection_callback; - stream->connection_closed_callback = connection_closed_callback; - stream->userdata = server; - aki_packet_stream_from_socket(stream, server->loop, s); -} - -struct shrb_node *shrb_server_create_node(struct shrb_server *server, u16 prev, struct cch_entry *entry) -{ - struct shrb_node *node = al_alloc_object(struct shrb_node); - node->id = al_rand_u16(); - node->prev = prev; - node->entry = entry; - struct cch_handle handle; - if (!cch_entry_get_handle(node->entry, &handle)) { - al_free(node); - return NULL; - } - struct shrb_server_handler *handler = shrb_codec_server_create(); - if (!handler->init(handler, &handle, NULL)) { - al_free(node); - return NULL; - } - node->duration = handler->get_duration(handler); - handler->free(&handler); - cch_entry_return_handle(node->entry, &handle); - node->seeked = false; - node->start = 0; - node->paused = SHRUB_NOT_PAUSED; - node->paused_at = 0; - node->seek_pos = 0; - node->connection_id = 1; - al_array_init(node->connections); - node->server = server; - al_array_push(server->nodes, node); - return node; -} - -void shrb_node_set_start(struct shrb_node *node, u64 start) -{ - if (node->paused == SHRUB_PAUSED) { - node->paused = SHRUB_NOT_PAUSED; - } - node->start = start; -} - -void shrb_node_user_pause(struct shrb_node *node, u64 pos) -{ - al_assert(node->paused != SHRUB_USER_PAUSED); - pos += SHRUB_PAUSE_DELAY; - node->seek_pos = pos; - node->paused_at = pos; - struct shrb_node_connection *connection; - al_array_foreach(node->connections, i, connection) { - send_pause_packet(connection, node->seek_pos); - } - node->paused = SHRUB_USER_PAUSED; -} - -void shrb_node_user_resume(struct shrb_node *node, struct shrb_resume_req *req) -{ - al_assert(node->paused == SHRUB_USER_PAUSED); - node->start = req->start + SHRUB_PAUSE_DELAY; - struct shrb_node_connection *connection; - al_array_foreach(node->connections, i, connection) { - send_resume_packet(connection, node->start, req->offset); - } - node->start -= SHRUB_BASE_DELAY; - node->paused = SHRUB_NOT_PAUSED; -} - -void shrb_node_pause(struct shrb_node *node) -{ - return; - if (node->paused != SHRUB_NOT_PAUSED) return; - u64 start = node->start; - s64 pos = node->seek_pos + (aki_get_timestamp() - (start + SHRUB_BASE_DELAY)); - pos += SHRUB_PAUSE_DELAY; - al_assert(pos >= 0); - node->seek_pos = (u64)pos; - node->paused_at = pos; - node->paused = SHRUB_PAUSED; -} - -void shrb_node_seek(struct shrb_node *node, u64 pos) -{ - node->seek_pos = pos; - node->seeked = true; - struct shrb_node_connection *connection; - al_array_foreach(node->connections, i, connection) { - send_seek_packet(connection, node->seek_pos); - } -} - -void shrb_server_close(struct shrb_server *server) -{ - (void)server; -} diff --git a/src/shrub/server.h b/src/shrub/server.h deleted file mode 100644 index cb901c5..0000000 --- a/src/shrub/server.h +++ /dev/null @@ -1,62 +0,0 @@ -#pragma once - -#include <al/array.h> -#include <aki/multiplex.h> -#include <aki/packet_stream.h> -#include <aki/packet_pool.h> - -#include "../cache/entry.h" -#include "../cache/handle.h" - -#include "handler.h" - -struct shrb_node_connection { - u16 id; - struct shrb_node *node; - struct aki_packet_stream *control; - struct aki_packet_stream *data; - struct aki_packet_pool pool; - s32 running; - u32 level; - u64 seek_pos; - struct cch_handle handle; - struct shrb_server_handler *handler; - struct aki_thread thread; -}; - -enum { - SHRUB_NOT_PAUSED = 0, - SHRUB_PAUSED, - SHRUB_USER_PAUSED -}; - -struct shrb_node { - u16 id; - u16 prev; - struct cch_entry *entry; - u64 duration; - bool seeked; - u64 start; // Exact start of the stream minus BASE_DELAY. - u8 paused; - u64 paused_at; - u64 seek_pos; - u16 connection_id; - array(struct shrb_node_connection *) connections; - struct shrb_server *server; -}; - -struct shrb_server { - struct aki_event_loop *loop; - array(struct shrb_node *) nodes; -}; - -bool shrb_server_init(struct shrb_server *server, struct aki_event_loop *loop); -void shrb_server_add_socket(struct shrb_server *server, struct aki_socket *s); -struct shrb_node *shrb_server_create_node(struct shrb_server *server, u16 prev, struct cch_entry *entry); -void shrb_node_set_start(struct shrb_node *node, u64 start); -void shrb_node_user_pause(struct shrb_node *node, u64 pos); -void shrb_node_user_resume(struct shrb_node *node, struct shrb_resume_req *req); -void shrb_node_pause(struct shrb_node *node); -void shrb_node_resume(struct shrb_node *node); -void shrb_node_seek(struct shrb_node *node, u64 pos); -void shrb_server_close(struct shrb_server *server); diff --git a/src/shrub/vcr.c b/src/shrub/vcr.c deleted file mode 100644 index 7f2b058..0000000 --- a/src/shrub/vcr.c +++ /dev/null @@ -1,177 +0,0 @@ -#include <al/log.h> - -#include "vcr.h" -#include "handler.h" - -#define VCR_BUFFER_HIGH 1024 -#define VCR_BUFFER_BUFFERED (VCR_BUFFER_HIGH - 64) -#define VCR_BUFFER_LOW (VCR_BUFFER_HIGH - 256) - -static void signal_callback(void *userdata) -{ - struct shrb_vcr *vcr = (struct shrb_vcr *)userdata; - aki_packet_stream_cork(vcr->data, false); -} - -void shrb_vcr_init(struct shrb_vcr *vcr, struct aki_event_loop *loop, struct aki_packet_stream *data) -{ - al_array_init(vcr->streams); - al_atomic_u64_store(&vcr->count, 0, AL_ATOMIC_RELAXED); - vcr->data = data; - aki_signal_init(&vcr->signal, signal_callback, vcr); - aki_signal_start(&vcr->signal, loop); -} - -static aki_thread_result AKI_THREADCALL vcr_stream_thread(void *userdata) -{ - struct shrb_vcr_stream *stream = (struct shrb_vcr_stream *)userdata; - struct shrb_vcr *vcr = stream->vcr; - while (aki_packet_cache_wait(&stream->cache)) { - struct aki_packet *packet = aki_packet_cache_pop(&stream->cache); - if (!packet) { - stream->client->handle_packet(stream->client, NULL); - break; - } - if (al_atomic_u64_sub(&vcr->count, 1, AL_ATOMIC_RELAXED) < VCR_BUFFER_LOW && stream->buffered) { - aki_signal_send(&vcr->signal); - } - if (al_atomic_s32_load(&stream->state, AL_ATOMIC_RELAXED) == SHRUB_STREAM_STOPPED) { - aki_mutex_lock(&stream->mutex); - aki_cond_wait(&stream->cond, &stream->mutex); - aki_mutex_unlock(&stream->mutex); - } - if (al_atomic_s32_load(&stream->state, AL_ATOMIC_RELAXED) == SHRUB_STREAM_CLOSED) { - aki_packet_free(packet); - break; - } - stream->client->handle_packet(stream->client, packet); - aki_packet_free(packet); - } - return 0; -} - -void shrb_vcr_add_stream(struct shrb_vcr *vcr, struct shrb_vcr_stream *stream) -{ - stream->vcr = vcr; - aki_cond_init(&stream->cond); - aki_mutex_init(&stream->mutex); - aki_packet_cache_init(&stream->cache, VCR_BUFFER_HIGH); - stream->buffered = 0; - al_array_push(vcr->streams, stream); - al_atomic_s32_store(&stream->state, SHRUB_STREAM_RUNNING, AL_ATOMIC_RELAXED); -} - -static struct shrb_vcr_stream *get_stream_from_index(struct shrb_vcr *vcr, s32 index) -{ - struct shrb_vcr_stream *stream; - al_array_foreach(vcr->streams, i, stream) { - if (stream->index == index) return stream; - } - return NULL; -} - -bool shrb_vcr_push_packet(struct shrb_vcr *vcr, struct aki_packet *packet) -{ - struct shrb_vcr_stream *stream; - switch (aki_packet_read_u8(packet)) { - case SHRUB_PACKET_DATA: - stream = get_stream_from_index(vcr, aki_packet_read_s32(packet)); - al_assert(stream); - if (!stream->running) { - aki_thread_create(&stream->thread, vcr_stream_thread, stream); - stream->running = true; - } - if (!aki_packet_cache_send_packet(&stream->cache, packet)) { - aki_packet_free(packet); - } else if (al_atomic_u64_add(&vcr->count, 1, AL_ATOMIC_RELAXED) >= VCR_BUFFER_BUFFERED) { - aki_packet_stream_cork(vcr->data, true); - al_array_foreach(vcr->streams, i, stream) { - stream->buffered = 1; - } - } - break; - case SHRUB_PACKET_EOF: - al_array_foreach(vcr->streams, i, stream) { - aki_packet_cache_send_packet(&stream->cache, NULL); - } - aki_packet_free(packet); - break; - case SHRUB_PACKET_ERROR: - al_log_warn("bimu", "Unhandled error packet."); - aki_packet_free(packet); - break; - default: - al_assert(false); - } - return true; -} - -static void vcr_stream_close_internal(struct shrb_vcr_stream *stream) -{ - al_atomic_s32_store(&stream->state, SHRUB_STREAM_CLOSED, AL_ATOMIC_RELAXED); - aki_packet_cache_disable(&stream->cache); - aki_mutex_lock(&stream->mutex); - if (aki_cond_is_waiting(&stream->cond)) { - aki_cond_signal(&stream->cond); - } - aki_mutex_unlock(&stream->mutex); - if (stream->running) { - aki_thread_join(&stream->thread); - } - struct aki_packet *packet; - while ((packet = aki_packet_cache_pop(&stream->cache))) { - aki_packet_free(packet); - } -} - -void shrb_vcr_stream_close(struct shrb_vcr_stream *stream) -{ - vcr_stream_close_internal(stream); -} - -void shrb_vcr_flush(struct shrb_vcr *vcr) -{ - struct shrb_vcr_stream *stream; - al_array_foreach(vcr->streams, i, stream) { - vcr_stream_close_internal(stream); - stream->client->flush(stream->client); - aki_packet_cache_enable(&stream->cache); - stream->running = false; - al_atomic_s32_store(&stream->state, SHRUB_STREAM_RUNNING, AL_ATOMIC_RELAXED); - } - al_atomic_u64_store(&vcr->count, 0, AL_ATOMIC_RELAXED); -} - -void shrb_vcr_stream_cork(struct shrb_vcr_stream *stream) -{ - al_atomic_s32_store(&stream->state, SHRUB_STREAM_STOPPED, AL_ATOMIC_RELAXED); -} - -void shrb_vcr_stream_uncork(struct shrb_vcr_stream *stream) -{ - if (al_atomic_s32_load(&stream->state, AL_ATOMIC_RELAXED) == SHRUB_STREAM_RUNNING) { - // DEBUG: This will be reached during normal operation. - return; - } - al_atomic_s32_store(&stream->state, SHRUB_STREAM_RUNNING, AL_ATOMIC_RELAXED); - aki_mutex_lock(&stream->mutex); - if (aki_cond_is_waiting(&stream->cond)) { - aki_cond_signal(&stream->cond); - } - aki_mutex_unlock(&stream->mutex); -} - -void shrb_vcr_free(struct shrb_vcr *vcr) -{ - aki_signal_stop(&vcr->signal); - struct shrb_vcr_stream *stream; - al_array_foreach(vcr->streams, i, stream) { - aki_packet_cache_free(&stream->cache); - stream->client->free(&stream->client); - if (stream->stream.type == CAMU_FFMPEG_COMPAT) { - avformat_free_context(stream->stream.av.format_context); - } - al_free(stream); - } - al_array_free(vcr->streams); -} diff --git a/src/shrub/vcr.h b/src/shrub/vcr.h deleted file mode 100644 index 4fae2c0..0000000 --- a/src/shrub/vcr.h +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once - -#include <aki/packet_cache.h> -#include <al/atomic.h> -#include <aki/packet_stream.h> -#include <aki/signal.h> - -#include "../codec/codec.h" - -enum { - SHRUB_STREAM_RUNNING = 0, - SHRUB_STREAM_STOPPED, - SHRUB_STREAM_CLOSED -}; - -struct shrb_vcr_stream { - u8 type; - s32 index; - struct camu_stream stream; - struct shrb_client_handler *client; - atomic_s32 state; - struct aki_packet_cache cache; - s32 buffered; - struct aki_cond cond; - struct aki_mutex mutex; - bool running; - struct aki_thread thread; - struct shrb_vcr *vcr; -}; - -struct shrb_vcr { - array(struct shrb_vcr_stream *) streams; - atomic_u64 count; - struct aki_packet_stream *data; - struct aki_signal signal; -}; - -void shrb_vcr_init(struct shrb_vcr *vcr, struct aki_event_loop *loop, struct aki_packet_stream *data); -void shrb_vcr_add_stream(struct shrb_vcr *vcr, struct shrb_vcr_stream *stream); -bool shrb_vcr_push_packet(struct shrb_vcr *vcr, struct aki_packet *packet); -void shrb_vcr_stream_close(struct shrb_vcr_stream *stream); -void shrb_vcr_flush(struct shrb_vcr *vcr); -void shrb_vcr_stream_cork(struct shrb_vcr_stream *stream); -void shrb_vcr_stream_uncork(struct shrb_vcr_stream *stream); -void shrb_vcr_free(struct shrb_vcr *vcr); diff --git a/src/sink/desktop.c b/src/sink/desktop.c new file mode 100644 index 0000000..8e628fd --- /dev/null +++ b/src/sink/desktop.c @@ -0,0 +1,187 @@ +#include <al/log.h> + +#include "../mixer/audio_miniaudio.h" +//#include "../mixer/audio_null.h" +#if defined CAMU_RENDERER_TIGER +#include "../render/renderer_tiger.h" +#elif defined CAMU_RENDERER_LIBPLACEBO +#include "../render/renderer_libplacebo.h" +#endif + +#include "desktop.h" + +static u8 sink_callback(void *userdata, u8 op, u8 type, void *opaque) +{ + struct camu_desktop *c = (struct camu_desktop *)userdata; + switch (op) { + case CAMU_SINK_ADD_BUFFER: + switch (type) { + case CAMU_SINK_AUDIO: { + struct camu_audio_buffer *buf = (struct camu_audio_buffer *)opaque; + camu_mixer_add_buffer(&c->mixer, buf); + al_log_debug("camu_desktop", "Audio buffer added."); + break; + } + case CAMU_SINK_VIDEO: { + struct camu_video_buffer *buf = (struct camu_video_buffer *)opaque; + camu_screen_add_buffer(&c->scr, buf); + al_log_debug("camu_desktop", "Video buffer added."); + break; + } + } + break; + case CAMU_SINK_REMOVE_BUFFER: + switch (type) { + case CAMU_SINK_AUDIO: { + struct camu_audio_buffer *buf = (struct camu_audio_buffer *)opaque; + camu_mixer_remove_buffer(&c->mixer, buf); + al_log_debug("camu_desktop", "Audio buffer removed."); + break; + } + case CAMU_SINK_VIDEO: { + struct camu_video_buffer *buf = (struct camu_video_buffer *)opaque; + camu_screen_remove_buffer(&c->scr, buf); + al_log_debug("camu_desktop", "Video buffer removed."); + break; + } + } + break; + case CAMU_SINK_SWAP_BUFFER: + switch (type) { + case CAMU_SINK_AUDIO: { + break; + } + } + break; + case CAMU_SINK_START: + switch (type) { + case CAMU_SINK_AUDIO: { + camu_mixer_resume(&c->mixer); + al_log_info("camu_desktop", "Audio started."); + break; + } + case CAMU_SINK_VIDEO: { + camu_screen_set_state(&c->scr, CAMU_SCREEN_PLAYING); + camu_screen_wake(&c->scr); + al_log_info("camu_desktop", "Video started."); + break; + } + } + break; + case CAMU_SINK_STOP: + switch (type) { + case CAMU_SINK_AUDIO: { + camu_mixer_pause(&c->mixer); + al_log_info("camu_desktop", "Audio stopped."); + break; + } + case CAMU_SINK_VIDEO: { + camu_screen_set_state(&c->scr, CAMU_SCREEN_PAUSED); + al_log_info("camu_desktop", "Video stopped."); + break; + } + } + break; + case CAMU_SINK_EXIT: + camu_sink_close(&c->sink); + if (c->exit_callback) { + c->exit_callback(c->userdata, c); + } + break; + } + return CAMU_SINK_OK; +} + +static void screen_callback(void *userdata, u8 op, void *opaque) +{ + struct camu_desktop *c = (struct camu_desktop *)userdata; + switch (op) { + case CAMU_SCREEN_NEXT: + camu_sink_skip(&c->sink, 1); + break; + case CAMU_SCREEN_PREVIOUS: + camu_sink_skip(&c->sink, -1); + break; + case CAMU_SCREEN_SHUFFLE: + camu_sink_shuffle(&c->sink); + break; + case CAMU_SCREEN_TOGGLE_PAUSE: + camu_sink_toggle_pause(&c->sink); + break; + case CAMU_SCREEN_SEEK: { + f64 percent = *(f64 *)opaque; + camu_sink_seek(&c->sink, percent); + break; + } + case CAMU_SCREEN_RESEEK: + camu_sink_reseek(&c->sink); + break; + case CAMU_SCREEN_CLOSE: + c->should_quit = 1; + break; + } +} + +bool camu_desktop_init(struct camu_desktop *c, const char *name) +{ + c->scr.callback = screen_callback; + c->scr.userdata = c; + if (!camu_screen_init(&c->scr) || !camu_screen_create_window(&c->scr, name)) { + return false; + } +#if defined CAMU_RENDERER_TIGER + c->renderer = camu_renderer_tiger_create(); +#elif defined CAMU_RENDERER_LIBPLACEBO + c->renderer = camu_renderer_lp_create(); +#endif + if (!camu_screen_create_renderer(&c->scr, c->renderer)) { + return false; + } + c->renderer->render(c->renderer, &c->scr); + if (!camu_mixer_init(&c->mixer, camu_audio_miniaudio_create())) { + //if (!camu_mixer_init(&c->mixer, (struct camu_audio *)&audio_plugin_null)) { + return false; + } + if (!c->mixer.audio->configure_stream(c->mixer.audio, NULL)) { + return false; + } + camu_mixer_pick_format(&c->mixer, &c->mixer.fmt); + c->should_quit = 0; + c->exit_callback = NULL; + c->userdata = NULL; + return true; +} + +bool camu_desktop_connect(struct camu_desktop *c, u8 type, struct aki_event_loop *loop, str *addr, u16 port) +{ + c->loop = loop; + camu_sink_init(&c->sink, c->loop, &c->mixer, c->renderer); + c->sink.callback = sink_callback; + c->sink.userdata = c; + return camu_sink_connect(&c->sink, type, addr, port, al_str_c("desktop")); +} + +bool camu_desktop_tick(struct camu_desktop *c) +{ + if (!c->should_quit && camu_screen_tick(&c->scr)) { + c->renderer->render(c->renderer, &c->scr); + } + return !c->should_quit; +} + +void camu_desktop_stop(struct camu_desktop *c) +{ + camu_mixer_pause(&c->mixer); + camu_mixer_clear(&c->mixer); + camu_screen_set_state(&c->scr, CAMU_SCREEN_CLOSED); + camu_screen_clear(&c->scr); + camu_sink_stop(&c->sink); +} + +void camu_desktop_free(struct camu_desktop *c) +{ + camu_sink_free(&c->sink); + camu_mixer_close(&c->mixer); + c->renderer->free(&c->renderer); + camu_screen_close(&c->scr); +} diff --git a/src/sink/desktop.h b/src/sink/desktop.h new file mode 100644 index 0000000..efc1c34 --- /dev/null +++ b/src/sink/desktop.h @@ -0,0 +1,26 @@ +#pragma once + +#include <al/types.h> +#include <al/str.h> +#include <aki/event_loop.h> + +#include "../libsink/sink.h" + +#include "../screen/screen.h" + +struct camu_desktop { + struct aki_event_loop *loop; + s32 should_quit; + struct camu_screen scr; + struct camu_renderer *renderer; + struct camu_mixer mixer; + struct camu_sink sink; + void (*exit_callback)(void *, struct camu_desktop *); + void *userdata; +}; + +bool camu_desktop_init(struct camu_desktop *c, const char *name); +bool camu_desktop_connect(struct camu_desktop *c, u8 type, struct aki_event_loop *loop, str *addr, u16 port); +bool camu_desktop_tick(struct camu_desktop *c); +void camu_desktop_stop(struct camu_desktop *c); +void camu_desktop_free(struct camu_desktop *c); diff --git a/src/sink/meson.build b/src/sink/meson.build new file mode 100644 index 0000000..df80934 --- /dev/null +++ b/src/sink/meson.build @@ -0,0 +1,3 @@ +desktop_src = ['desktop.c'] +desktop_deps = [common_deps, buffer, render, screen, mixer, libsink] +desktop = declare_dependency(sources: desktop_src, dependencies: desktop_deps) diff --git a/src/util/color_palette.c b/src/util/color_palette.c index 53c2c68..3bc7f3e 100644 --- a/src/util/color_palette.c +++ b/src/util/color_palette.c @@ -1,9 +1,8 @@ -#include <aki/file.h> -#include <jansson.h> - #include "color_palette.h" -struct camu_color_palette global_color_palette = {0}; +#ifdef AKIYO_HAS_JSON +#include <aki/file.h> +#include <jansson.h> static char *special_colors[2] = { "background", @@ -28,9 +27,13 @@ static char *normal_colors[16] = { "color14", "color15" }; +#endif + +struct camu_color_palette global_color_palette = { 0 }; bool camu_color_palette_init(str *path) { +#ifdef AKIYO_HAS_JSON struct aki_file file; if (!aki_file_open(&file, path, 0)) { return false; @@ -59,4 +62,8 @@ bool camu_color_palette_init(str *path) global_color_palette.colors[i + 2] = (u32)al_str_to_long(al_str_w((char *)color, 1, 6), 16); } return true; +#else + (void)path; + return false; +#endif } diff --git a/src/util/meson.build b/src/util/meson.build index eff0fe6..1d62f55 100644 --- a/src/util/meson.build +++ b/src/util/meson.build @@ -1,6 +1,7 @@ util_src = ['color_palette.c', 'blocking_ring_buffer.c'] blake3_opts = cmake.subproject_options() -blake3_opts.add_cmake_defines({ 'CMAKE_BUILD_TYPE': is_debug ? 'Debug' : 'Release' }) +reltype = is_minsize ? 'MinSizeRel' : 'Release' +blake3_opts.add_cmake_defines({ 'CMAKE_BUILD_TYPE': is_debug ? 'Debug' : reltype }) blake3 = cmake.subproject('BLAKE3').dependency('blake3') util_deps = [blake3] util = declare_dependency(sources: util_src, dependencies: util_deps) diff --git a/subprojects/BLAKE3.wrap b/subprojects/BLAKE3.wrap index 0a26602..e8048cc 100644 --- a/subprojects/BLAKE3.wrap +++ b/subprojects/BLAKE3.wrap @@ -1,6 +1,6 @@ [wrap-git] url = https://github.com/BLAKE3-team/BLAKE3.git -revision = 8fc36186b84385d36d8339606e4d1ea6ff471965 +revision = 1.5.1 depth = 1 patch_directory = BLAKE3 method = cmake diff --git a/subprojects/ffmpeg.wrap b/subprojects/ffmpeg.wrap index 8a97530..779104a 100644 --- a/subprojects/ffmpeg.wrap +++ b/subprojects/ffmpeg.wrap @@ -1,14 +1,26 @@ [wrap-file] -directory = ffmpeg-7.0 +directory = ffmpeg-7.1 +#directory = ffmpeg-7.0.2 +#directory = ffmpeg-7.0.1 +#directory = ffmpeg-7.0 #directory = ffmpeg-6.1.1 #directory = ffmpeg-5.1.4 -source_url = https://www.ffmpeg.org/releases/ffmpeg-7.0.tar.gz +source_url = https://ffmpeg.org/releases/ffmpeg-7.1.tar.gz +#source_url = https://ffmpeg.org/releases/ffmpeg-7.0.2.tar.gz +#source_url = https://ffmpeg.org/releases/ffmpeg-7.0.1.tar.gz +#source_url = https://www.ffmpeg.org/releases/ffmpeg-7.0.tar.gz #source_url = https://www.ffmpeg.org/releases/ffmpeg-6.1.1.tar.gz #source_url = https://www.ffmpeg.org/releases/ffmpeg-5.1.4.tar.gz -source_filename = ffmpeg-7.0.tar.gz +source_filename = ffmpeg-7.1.tar.gz +#source_filename = ffmpeg-7.0.2.tar.gz +#source_filename = ffmpeg-7.0.1.tar.gz +#source_filename = ffmpeg-7.0.tar.gz #source_filename = ffmpeg-6.1.1.tar.gz #source_filename = ffmpeg-5.1.4.tar.gz -source_hash = 943a2a28044947c17a905c39075494b0da46ec0795224c2c61eff986518321eb +source_hash = 42a7dc0d1583885d1b8f6559fa7ce28f97acafea6803de6a8f73e3ba229348bd +#source_hash = 1233b3a93dd7517cc3c56b72a67f64041c044848d981e3deff4bebffa25f1054 +#source_hash = e273eae00b4f833595654c1ee2b2349240e0332f9dbf3c4cadd39df9819ff052 +#source_hash = 943a2a28044947c17a905c39075494b0da46ec0795224c2c61eff986518321eb #source_hash = b0aee531e4a91fed6bc031445dfc3ad8eb36de1fcb0fa2acdb589ecc9f8dc9cb #source_hash = ad1d83e0f022ba405928ea4985c9cab978c4698bb7937738a1085808c3eb78b4 patch_directory = ffmpeg diff --git a/subprojects/packagefiles/ffmpeg/meson.build b/subprojects/packagefiles/ffmpeg/meson.build index 3fb1539..8092783 100644 --- a/subprojects/packagefiles/ffmpeg/meson.build +++ b/subprojects/packagefiles/ffmpeg/meson.build @@ -1,4 +1,4 @@ -project('ffmpeg', 'c', version: '7.0', meson_version: '>=0.57.0') +project('ffmpeg', 'c', version: '7.0.1', meson_version: '>=0.57.0') compiler = meson.get_compiler('c') @@ -23,6 +23,8 @@ extra_options += ['--ar=' + meson.get_external_property('ar', 'ar')] if is_windows if is_64bit extra_options += ['--target-os=win64', '--arch=x86_64'] + else + extra_options += ['--target-os=win32', '--arch=x86'] endif if is_msvc extra_options += ['--toolchain=msvc'] @@ -61,6 +63,7 @@ proj = ext_proj.add_project('configure', '--incdir=@PREFIX@/@INCLUDEDIR@', '--enable-pic', '--enable-static', + '--disable-logging', '--disable-shared', '--disable-all', '--disable-autodetect', @@ -95,6 +98,8 @@ proj = ext_proj.add_project('configure', '--enable-avcodec', '--enable-swresample', '--enable-swscale', + '--disable-indevs', + '--disable-outdevs', '--enable-demuxer=' + demuxers, '--enable-decoder=' + decoders, '--enable-parser=' + parsers, diff --git a/subprojects/packagefiles/libplacebo/cache_ref_rect.diff b/subprojects/packagefiles/libplacebo/cache_ref_rect.diff new file mode 100644 index 0000000..647d637 --- /dev/null +++ b/subprojects/packagefiles/libplacebo/cache_ref_rect.diff @@ -0,0 +1,28 @@ +diff --git a/src/renderer.c b/src/renderer.c +index 7622523..54869b9 100644 +--- a/src/renderer.c ++++ b/src/renderer.c +@@ -31,6 +31,7 @@ struct cached_frame { + struct pl_color_space color; + struct pl_icc_profile profile; + pl_rect2df crop; ++ pl_rect2df ref_rect; + pl_tex tex; + int comps; + bool evict; // for garbage collection +@@ -3473,6 +3474,7 @@ retry: + can_reuse = f->tex->params.w == out_w && + f->tex->params.h == out_h && + pl_rect2d_eq(f->crop, img->crop) && ++ pl_rect2d_eq(f->ref_rect, pass.ref_rect) && + f->params_hash == par_info.hash && + pl_color_space_equal(&f->color, &target->color) && + pl_icc_profile_equal(&f->profile, &target->profile); +@@ -3568,6 +3570,7 @@ retry: + + f->params_hash = par_info.hash; + f->crop = img->crop; ++ f->ref_rect = pass.ref_rect; + f->color = inter_pass.img.color; + f->comps = inter_pass.img.comps; + f->profile = target->profile; diff --git a/subprojects/packagefiles/linmath.h/meson.build b/subprojects/packagefiles/linmath.h/meson.build index d3ab07d..19438fa 100644 --- a/subprojects/packagefiles/linmath.h/meson.build +++ b/subprojects/packagefiles/linmath.h/meson.build @@ -1,2 +1,2 @@ project('linmath.h', 'c', version: 'git-3eef828') -linmath = declare_dependency(include_directories: include_directories('.')) +linmath_h = declare_dependency(include_directories: include_directories('.')) diff --git a/subprojects/shaderc.wrap b/subprojects/shaderc.wrap index 7b9c266..fb5c1ee 100644 --- a/subprojects/shaderc.wrap +++ b/subprojects/shaderc.wrap @@ -1,5 +1,5 @@ [wrap-git] url = https://github.com/google/shaderc.git -revision = f59f0d11b80fd622383199c867137ededf89d43b +revision = 5d0f6ed6e46d3c0ccd23151c6a1486c2ec90c037 depth = 1 method = cmake diff --git a/subprojects/stb.wrap b/subprojects/stb.wrap index f8193df..d14fad7 100644 --- a/subprojects/stb.wrap +++ b/subprojects/stb.wrap @@ -1,5 +1,5 @@ [wrap-git] url = https://github.com/nothings/stb.git -revision = ae721c50eaf761660b4f90cc590453cdb0c2acd0 +revision = 013ac3beddff3dbffafd5177e7972067cd2b5083 depth = 1 patch_directory = stb diff --git a/subprojects/sekihi.wrap b/subprojects/stela.wrap index 160b2d3..7807a8c 100644 --- a/subprojects/sekihi.wrap +++ b/subprojects/stela.wrap @@ -1,4 +1,4 @@ [wrap-git] -url = https://git.akon.city/sekihi +url = https://git.akon.city/stela revision = master depth = 1 diff --git a/tests/action.h b/tests/action.h new file mode 100644 index 0000000..0a3d22d --- /dev/null +++ b/tests/action.h @@ -0,0 +1,14 @@ +#pragma once + +#include <al/types.h> + +enum { + CAMU_TEST_ACTION_ADD = 0, + CAMU_TEST_ACTION_SKIP, + CAMU_TEST_ACTION_SEEK +}; + +struct camu_test_action { + u8 action; + f32 roll; +}; diff --git a/tests/meson.build b/tests/meson.build new file mode 100644 index 0000000..97b0f60 --- /dev/null +++ b/tests/meson.build @@ -0,0 +1,3 @@ +#tests_src = ['runner.c', 'ui.c'] +#tests_deps = [common_deps, dependency('notcurses'), server, desktop] +#executable('runner', tests_src, dependencies: tests_deps) diff --git a/tests/runner.c b/tests/runner.c new file mode 100644 index 0000000..13bbaa1 --- /dev/null +++ b/tests/runner.c @@ -0,0 +1,288 @@ +#define DISABLE_TUI 0 + +#include <al/log.h> +#include <al/random.h> +#include <aki/common.h> +#include <aki/event_loop.h> +#include <aki/timer.h> + +#include "../src/server/server.h" +#include "../src/server/list.h" +#include "../src/server/common.h" +#include "../src/sink/desktop.h" +#include "../src/cache/handlers/file.h" +#include "../src/codec/ffmpeg/common.h" + +#include "ui.h" +#include "action.h" + +struct runner { + struct aki_event_loop loop; + struct camu_test_ui ui; + struct aki_poll input_poll; + struct aki_timer timer; + struct { + struct camu_server server; + } server; + struct { + u32 count; + atomic_s32 sinks_to_add; + array(struct camu_desktop *) pending_sinks; + struct aki_signal signal; + array(struct camu_desktop *) sinks; + atomic_bool cleanup_needed; + array(struct camu_desktop *) cleanup; + } sink; + struct aki_mutex mutex; +}; + +static void list_add_file(struct camu_server *server, struct lia_list *list, str *path) +{ + struct cch_entry *entry = cch_handler_file_create(path); + if (entry) { + struct camu_server_resource *resource = al_alloc_object(struct camu_server_resource); + resource->node = lia_server_create_node(&server->data.server, entry); + al_array_push(server->data.resources, resource); + al_str_clone(&resource->unique_id, path); + resource->entry = entry; + lia_list_add(list, resource, camu_server_entry_callback); + } +} + +static void exit_callback(void *userdata, struct camu_desktop *desktop) +{ + struct runner *rn = (struct runner *)userdata; + al_log_info("runner", "Sink exited."); + aki_mutex_lock(&rn->mutex); + al_array_push(rn->sink.cleanup, desktop); + al_atomic_bool_store(&rn->sink.cleanup_needed, true, AL_ATOMIC_RELAXED); + if (--rn->sink.count) { + aki_mutex_unlock(&rn->mutex); + return; + } + aki_mutex_unlock(&rn->mutex); + aki_signal_stop(&rn->sink.signal); + camu_server_close(&rn->server.server); +#if !DISABLE_TUI + aki_timer_disable(&rn->timer); + aki_timer_stop(&rn->timer); + aki_poll_stop(&rn->input_poll); +#endif +} + +static void signal_callback(void *userdata) +{ + struct runner *rn = (struct runner *)userdata; + aki_mutex_lock(&rn->mutex); + struct camu_desktop *desktop; + al_array_foreach(rn->sink.pending_sinks, i, desktop) { + if (!camu_desktop_connect(desktop, AKI_SOCKET_UNIX, &rn->loop, CAMU_UNIX_PATH, CAMU_PORT)) { + } + camu_test_ui_add_sink(&rn->ui, &desktop->sink); + } + rn->sink.pending_sinks.size = 0; + aki_mutex_unlock(&rn->mutex); +} + +static void add_sink(struct runner *rn) +{ + struct camu_desktop *desktop = al_alloc_object(struct camu_desktop); + if (!camu_desktop_init(desktop, "cmv_test")) { + } + desktop->exit_callback = exit_callback; + desktop->userdata = rn; + al_array_push(rn->sink.sinks, desktop); + aki_mutex_lock(&rn->mutex); + rn->sink.count++; + al_array_push(rn->sink.pending_sinks, desktop); + aki_mutex_unlock(&rn->mutex); + aki_signal_send(&rn->sink.signal); +} + +#if !DISABLE_TUI +static void input_poll_callback(void *userdata, s32 revents) +{ + struct runner *rn = (struct runner *)userdata; + (void)revents; + struct ncinput input; + u32 ret; + do { + al_memset(&input, 0, sizeof(struct ncinput)); + ret = notcurses_get_nblock(rn->ui.nc, &input); + if (ret == (u32)-1 || ret == 0) break; + if (input.evtype != NCTYPE_RELEASE) { + if (input.id == 'n') al_atomic_s32_add(&rn->sink.sinks_to_add, 1, AL_ATOMIC_RELAXED); + } + } while (1); +} + +static s32 log_callback(void *userdata, char *msg) +{ + struct runner *rn = (struct runner *)userdata; + camu_test_ui_push_message(&rn->ui, msg); + return al_strlen(msg); +} + +static void av_log_callback(void *userdata, int level, const char *fmt, va_list args) +{ + (void)userdata; + if (level < AV_LOG_DEBUG) al_logv("debug", "ff_debug", fmt, args); +} + +//static f32 chances[] = { 0.2f, 0.5f, 1.0f }; + +static void timer_callback(void *userdata, struct aki_timer *timer) +{ + struct runner *rn = (struct runner *)userdata; + (void)timer; + /* + f32 roll = al_rand() / (f32)AL_RAND_MAX; + struct camu_user *user = al_array_at(rn->server.server.users, 0); + struct lia_list *list = al_array_at(user->lists, 0); + //s32 sequence = al_array_at(list->entries, list->current)->sequence; + struct camu_test_action action = { .roll = roll }; + if (roll < chances[0]) { + action.action = CAMU_TEST_ACTION_SEEK; + s32 amount = (al_rand() - (RAND_MAX / 2)) % 3; + //lia_list_skip(list, sequence, amount); + } else if (roll < chances[1]) { + action.action = CAMU_TEST_ACTION_SKIP; + f64 percent = al_rand() / (f64)AL_RAND_MAX; + //lia_list_seek(list, sequence, percent); + } else if (roll < chances[2]) { + action.action = CAMU_TEST_ACTION_ADD; + } + camu_test_ui_push_action(&rn->ui, &action); + */ + camu_test_ui_draw(&rn->ui); + aki_timer_again(&rn->timer); +} +#endif + +static aki_thread_result AKI_THREADCALL event_loop_thread(void *userdata) +{ + struct runner *rn = (struct runner *)userdata; + aki_event_loop_run(&rn->loop); + aki_event_loop_destroy(&rn->loop); + return 0; +} + +static struct runner rn; + +#define failure() do { \ + goto out; \ +} while (0) + +#ifndef _WIN32 +s32 main(s32 argc, char *argv[]) +#else +s32 wmain(s32 argc, wchar_t **argv) +#endif +{ + (void)argc; + (void)argv; + if (!aki_common_init() || !stl_global_init()) { + return EXIT_FAILURE; + } + + s32 ret = EXIT_FAILURE; + +#if !DISABLE_TUI + al_set_print(log_callback, &rn); + camu_ff_set_log_callback(av_log_callback); + if (!camu_test_ui_init(&rn.ui)) failure(); +#endif + + aki_event_loop_init(&rn.loop); + + if (!camu_server_init(&rn.server.server, AKI_SOCKET_UNIX, &rn.loop)) failure(); + if (!camu_server_listen(&rn.server.server, CAMU_UNIX_PATH, CAMU_PORT)) failure(); + + struct camu_list *list = al_array_at(rn.server.server.lists, 0); + list_add_file(&rn.server.server, &list->impl, al_str_c("/home/andrew/dl/mp4/Biggest glow up of any ye song ever [fgbri3k71ljc1].mp4")); + list_add_file(&rn.server.server, &list->impl, al_str_c("/home/andrew/dl/mp4/This is FRIED [svlczpgt2m3d1].mp4")); + list_add_file(&rn.server.server, &list->impl, al_str_c("/home/andrew/pics/anime/41970900_p0.jpg")); + list_add_file(&rn.server.server, &list->impl, al_str_c("/home/andrew/c/camu/data/test_resources/2F49561C2BEE122D0E6FBE2B43A8F6AD_video_dashinit.mp4")); + +#if !DISABLE_TUI + camu_test_ui_add_list(&rn.ui, &list->impl); + + aki_poll_init(&rn.input_poll, input_poll_callback, &rn); + aki_poll_set(&rn.input_poll, notcurses_inputready_fd(rn.ui.nc), AKI_POLL_READ); + aki_poll_start(&rn.input_poll, &rn.loop); + + aki_timer_init(&rn.timer, &rn.loop, timer_callback, &rn); + aki_timer_set_repeat(&rn.timer, AKI_TS_FROM_USEC(333333)); + timer_callback(&rn, NULL); +#endif + + rn.sink.count = 0; + al_atomic_s32_store(&rn.sink.sinks_to_add, 0, AL_ATOMIC_RELAXED); + al_array_init(rn.sink.pending_sinks); + aki_signal_init(&rn.sink.signal, signal_callback, &rn); + aki_signal_start(&rn.sink.signal, &rn.loop); + al_array_init(rn.sink.sinks); + al_atomic_bool_store(&rn.sink.cleanup_needed, false, AL_ATOMIC_RELAXED); + al_array_init(rn.sink.cleanup); + + aki_mutex_init(&rn.mutex); + + struct aki_thread thread0; + aki_thread_create(&thread0, event_loop_thread, &rn); + + s32 sinks_to_add; + bool any_tick; + struct camu_desktop *desktop; + do { + sinks_to_add = al_atomic_s32_load(&rn.sink.sinks_to_add, AL_ATOMIC_RELAXED); + for (s32 i = 0; i < sinks_to_add; i++) { + add_sink(&rn); + al_atomic_s32_sub(&rn.sink.sinks_to_add, 1, AL_ATOMIC_RELAXED); + } + if (rn.sink.count == 0) { + any_tick = true; + aki_thread_sleep(AKI_TS_FROM_USEC(1000)); + continue; + } + // If any sink blocks this thread, things go awry and we + // will crash. I'm not sure why. In wayland this manifests + // as getting disconnected from the wayland display. + any_tick = false; + al_array_foreach(rn.sink.sinks, i, desktop) { + if (!camu_desktop_tick(desktop)) { + camu_desktop_stop(desktop); + al_array_remove_at_iter(rn.sink.sinks, i); + } else { + any_tick = true; + } + } + // This is a really bad race condition. It's quite likely that sink entries + // won't be done cleanup by the time we are calling desktop_free() here. + //if (al_atomic_bool_load(&rn.sink.cleanup_needed, AL_ATOMIC_RELAXED)) { + // aki_mutex_lock(&rn.mutex); + // al_array_foreach(rn.sink.cleanup, i, desktop) { + // camu_desktop_free(desktop); + // } + // rn.sink.cleanup.size = 0; + // al_atomic_bool_store(&rn.sink.cleanup_needed, false, AL_ATOMIC_RELAXED); + // aki_mutex_unlock(&rn.mutex); + //} + } while (any_tick); + + aki_thread_join(&thread0); + al_assert(rn.sink.sinks.size == 0); + al_array_foreach(rn.sink.cleanup, i, desktop) { + camu_desktop_free(desktop); + } + camu_server_free(&rn.server.server); + + ret = EXIT_SUCCESS; +out: +#if !DISABLE_TUI + camu_test_ui_close(&rn.ui); +#endif + stl_global_close(); + aki_common_close(); + + return ret; +} diff --git a/tests/ui.c b/tests/ui.c new file mode 100644 index 0000000..7998e93 --- /dev/null +++ b/tests/ui.c @@ -0,0 +1,220 @@ +#include "ui.h" + +static void layout_ui(struct camu_test_ui *ui) +{ + struct ncplane_options opts = { 0 }; + + if (ui->messages.p) ncplane_destroy(ui->messages.p); + opts.x = ui->term_cols / 2; + opts.y = 0; + opts.rows = ui->term_rows / 2; + opts.cols = ui->term_cols / 2; + ui->messages.p = ncplane_create(notcurses_stdplane(ui->nc), &opts); + + if (ui->actions.p) ncplane_destroy(ui->actions.p); + opts.y = ui->term_rows / 2; + opts.rows = ui->term_rows - opts.rows; + ui->actions.p = ncplane_create(notcurses_stdplane(ui->nc), &opts); + + if (ui->lists.p) ncplane_destroy(ui->lists.p); + opts.x = 0; + opts.y = 0; + opts.rows = ui->term_rows / 2; + ui->lists.p = ncplane_create(notcurses_stdplane(ui->nc), &opts); + + if (ui->sinks.p) ncplane_destroy(ui->sinks.p); + opts.y = ui->term_rows / 2; + opts.rows = ui->term_rows - opts.rows; + ui->sinks.p = ncplane_create(notcurses_stdplane(ui->nc), &opts); +} + +static int resize_cb(struct ncplane *p) +{ + struct camu_test_ui *ui = (struct camu_test_ui *)ncplane_userptr(p); + notcurses_stddim_yx(ui->nc, &ui->term_rows, &ui->term_cols); + layout_ui(ui); + camu_test_ui_draw(ui); + return 0; +} + +bool camu_test_ui_init(struct camu_test_ui *ui) +{ + if (!(ui->nc = notcurses_core_init(NULL, stdin))) { + return false; + } + + notcurses_stddim_yx(ui->nc, &ui->term_rows, &ui->term_cols); + ncplane_set_resizecb(notcurses_stdplane(ui->nc), resize_cb); + ncplane_set_userptr(notcurses_stdplane(ui->nc), ui); + + ui->messages.p = NULL; + al_array_init(ui->messages.a); + + ui->actions.p = NULL; + al_array_init(ui->actions.a); + + ui->lists.p = NULL; + al_array_init(ui->lists.a); + + ui->sinks.p = NULL; + al_array_init(ui->sinks.a); + + ui->buffer = al_malloc(256); + ui->buffer[0] = '\0'; + + layout_ui(ui); + + return true; +} + +void camu_test_ui_push_message(struct camu_test_ui *ui, char *msg) +{ + al_array_push(ui->messages.a, msg); +} + +void camu_test_ui_push_action(struct camu_test_ui *ui, struct camu_test_action *action) +{ + al_array_push(ui->actions.a, *action); +} + +void camu_test_ui_add_list(struct camu_test_ui *ui, struct lia_list *list) +{ + al_array_push(ui->lists.a, list); +} + +void camu_test_ui_add_sink(struct camu_test_ui *ui, struct camu_sink *sink) +{ + al_array_push(ui->sinks.a, sink); +} + +static void draw_list(struct ncplane *p, struct lia_list *list) +{ + for (u32 i = 0; i < list->entries.size; i++) { + struct lia_list_entry *entry = al_array_at(list->entries, i); + str *unique_id; + entry->callback(entry, LIANA_ENTRY_ID, &unique_id); + u32 len = AL_MIN(unique_id->len, ncplane_dim_x(p) - 2); + for (u32 j = 0; j < len; j++) { + ncplane_putchar_yx(p, i + 1, j + 1, al_str_at(unique_id, j)); + } + } +} + +static void print_formatted_time(f64 seconds, s32 *text, char *buffer) +{ + s64 minute = (s64)(seconds / 60.0); + s64 hour = minute / 60L; + minute -= hour * 60L; + if (hour > 0) { + *text += al_sprintf(buffer + *text, "%.2ld:", hour); + } + *text += al_sprintf(buffer + *text, "%.2ld:%06.3f", minute, fmod(seconds, 60.0)); +} + +static void draw_sink(struct camu_test_ui *ui, struct ncplane *p, struct camu_sink *sink, u32 y) +{ + u64 c = 0; + ncchannels_set_fg_rgb8(&c, 255, 255, 255); + ncplane_cursor_move_yx(p, y, 1); + ncplane_rounded_box(p, NCSTYLE_NONE, c, y + 2, ncplane_dim_x(p) - 3, 0); + struct camu_sink_entry *entry = camu_sink_get_current(sink); + if (entry) { + f64 pts = camu_clock_get_pts(&entry->clock, 0.0); + f64 duration = entry->client.duration / 1000000.0; + s32 text = 0; + ui->buffer[0] = '\0'; + text += al_sprintf(ui->buffer + text, "("); + print_formatted_time(pts, &text, ui->buffer); + text += al_sprintf(ui->buffer + text, "/"); + print_formatted_time(duration, &text, ui->buffer); + text += al_sprintf(ui->buffer + text, ")"); + f64 percent = (duration == 0.0 || pts > duration) ? 1.0 : pts / duration; + s32 parts = ((ncplane_dim_x(p) - 5) - text) * percent; + al_memset(ui->buffer + text, '-', parts); + ui->buffer[text + parts] = '\0'; + ncplane_putstr_yx(p, y + 1, 2, ui->buffer); + } + camu_sink_return_current(sink); +} + +void camu_test_ui_draw(struct camu_test_ui *ui) +{ + u64 c = 0; + ncchannels_set_fg_rgb8(&c, 255, 255, 255); + + // Log messages + struct ncplane *p = ui->messages.p; + ncplane_erase(p); + ncplane_rounded_box(p, NCSTYLE_NONE, c, ncplane_dim_y(p) - 1, ncplane_dim_x(p) - 1, 0); + u32 size = ui->messages.a.size; + u32 height = ncplane_dim_y(p) - 2; + u32 index = size > height ? size - height : 0; + for (u32 i = index; i < size; i++) { + char *msg = al_array_at(ui->messages.a, i); + u32 msg_width = AL_MIN(ncplane_dim_x(p) - 2, al_strlen(msg)); + for (u32 j = 0; j < msg_width; j++) { + ncplane_putchar_yx(p, (i - index) + 1, 1 + j, msg[j]); + } + } + + // Actions + p = ui->actions.p; + ncplane_erase(p); + ncplane_rounded_box(p, NCSTYLE_NONE, c, ncplane_dim_y(p) - 1, ncplane_dim_x(p) - 1, 0); + size = ui->actions.a.size; + height = ncplane_dim_y(p) - 2; + index = size > height ? size - height : 0; + for (u32 i = index; i < size; i++) { + struct camu_test_action *action = &al_array_at(ui->actions.a, i); + ui->buffer[0] = '\0'; + u32 text = al_sprintf(ui->buffer, "%f", action->roll); + ncplane_putstr_yx(p, (i - index) + 1, 1, ui->buffer); + switch (action->action) { + case CAMU_TEST_ACTION_ADD: + ncplane_putstr_yx(p, (i - index) + 1, text + 2, "ADD"); + break; + case CAMU_TEST_ACTION_SKIP: + ncplane_putstr_yx(p, (i - index) + 1, text + 2, "SKIP"); + break; + case CAMU_TEST_ACTION_SEEK: + ncplane_putstr_yx(p, (i - index) + 1, text + 2, "SEEK"); + break; + } + } + + // Lists + p = ui->lists.p; + ncplane_erase(p); + ncplane_rounded_box(p, NCSTYLE_NONE, c, ncplane_dim_y(p) - 1, ncplane_dim_x(p) - 1, 0); + struct lia_list *list; + al_array_foreach(ui->lists.a, i, list) { + draw_list(p, list); + } + + // Sinks + p = ui->sinks.p; + ncplane_erase(p); + ncplane_rounded_box(p, NCSTYLE_NONE, c, ncplane_dim_y(p) - 1, ncplane_dim_x(p) - 1, 0); + u32 y = 1; + struct camu_sink *sink; + al_array_foreach(ui->sinks.a, i, sink) { + draw_sink(ui, p, sink, y); + y += 3; + } + + notcurses_refresh(ui->nc, NULL, NULL); + notcurses_render(ui->nc); +} + +void camu_test_ui_close(struct camu_test_ui *ui) +{ + char *msg; + al_array_foreach(ui->messages.a, i, msg) { + al_free(msg); + } + al_array_free(ui->messages.a); + al_array_free(ui->actions.a); + al_array_free(ui->lists.a); + al_array_free(ui->sinks.a); + notcurses_stop(ui->nc); +} diff --git a/tests/ui.h b/tests/ui.h new file mode 100644 index 0000000..34c7ed1 --- /dev/null +++ b/tests/ui.h @@ -0,0 +1,40 @@ +#pragma once + +#include <al/array.h> +#include <notcurses/notcurses.h> + +#include "../src/liana/list.h" +#include "../src/libsink/sink.h" + +#include "action.h" + +struct camu_test_ui { + struct notcurses *nc; + u32 term_rows; + u32 term_cols; + struct { + struct ncplane *p; + array(char *) a; + } messages; + struct { + struct ncplane *p; + array(struct camu_test_action) a; + } actions; + struct { + struct ncplane *p; + array(struct lia_list *) a; + } lists; + struct { + struct ncplane *p; + array(struct camu_sink *) a; + } sinks; + char *buffer; +}; + +bool camu_test_ui_init(struct camu_test_ui *ui); +void camu_test_ui_push_message(struct camu_test_ui *ui, char *msg); +void camu_test_ui_push_action(struct camu_test_ui *ui, struct camu_test_action *action); +void camu_test_ui_add_list(struct camu_test_ui *ui, struct lia_list *list); +void camu_test_ui_add_sink(struct camu_test_ui *ui, struct camu_sink *sink); +void camu_test_ui_draw(struct camu_test_ui *ui); +void camu_test_ui_close(struct camu_test_ui *ui); |