summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore10
-rw-r--r--flake.lock12
-rw-r--r--flake.nix10
-rwxr-xr-xscripts/run_server.sh3
-rw-r--r--scripts/screenrc9
-rw-r--r--src/buffer/audio.c84
-rw-r--r--src/buffer/audio.h10
-rw-r--r--src/buffer/frame_queue.h4
-rw-r--r--src/buffer/video.c88
-rw-r--r--src/buffer/video.h14
-rw-r--r--src/codec/codec.h18
-rw-r--r--src/codec/ffmpeg/decoder.c9
-rw-r--r--src/codec/ffmpeg/decoder.h4
-rw-r--r--src/codec/ffmpeg/demuxer.c35
-rw-r--r--src/codec/ffmpeg/packet_ext.c39
-rw-r--r--src/codec/ffmpeg/packet_ext.h1
-rw-r--r--src/codec/ffmpeg/scaler.c18
-rw-r--r--src/codec/ffmpeg/scaler.h2
-rw-r--r--src/fruits/cmsrv/cmsrv.c6
-rw-r--r--src/fruits/cmsrv/ui.c10
-rw-r--r--src/fruits/cmv/cmv.c4
-rw-r--r--src/fruits/ctv/ctv.c5
-rw-r--r--src/fruits/droid/ctv/gradle/libs.versions.toml2
-rw-r--r--src/liana/client.c64
-rw-r--r--src/liana/handler.h1
-rw-r--r--src/liana/handlers/cdio_server.c8
-rw-r--r--src/liana/handlers/codec_client.c35
-rw-r--r--src/liana/handlers/codec_server.c4
-rw-r--r--src/liana/list.c6
-rw-r--r--src/liana/vcr.c98
-rw-r--r--src/liana/vcr.h12
-rw-r--r--src/libsink/common.h2
-rw-r--r--src/libsink/sink.c193
-rw-r--r--src/libsink/sink.h1
-rw-r--r--src/mixer/audio_miniaudio.c15
-rw-r--r--src/mixer/audio_null.c2
-rw-r--r--src/render/meson.build7
-rw-r--r--src/render/queue_libplacebo.c150
-rw-r--r--src/render/queue_libplacebo.h12
-rw-r--r--src/render/renderer.h2
-rw-r--r--src/render/renderer_libplacebo.c34
-rw-r--r--src/render/renderer_libplacebo.h3
-rw-r--r--subprojects/fontconfig.wrap7
-rw-r--r--subprojects/freetype2.wrap4
-rw-r--r--subprojects/fribidi.wrap4
-rw-r--r--subprojects/harfbuzz.wrap4
-rw-r--r--subprojects/libass.wrap1
-rw-r--r--subprojects/packagefiles/libass/meson.build21
-rw-r--r--subprojects/packagefiles/notcurses/notcurses_tmux.diff12
49 files changed, 789 insertions, 310 deletions
diff --git a/.gitignore b/.gitignore
index b0e2071..dda3f04 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,10 +13,16 @@ subprojects/BLAKE3/
subprojects/c89atomic.wrap
subprojects/c89atomic/
subprojects/ffmpeg-*/
+subprojects/fontconfig/
+subprojects/freetype2/
+subprojects/fribidi/
subprojects/glfm.wrap
subprojects/glfm/
subprojects/glfw3.wrap
subprojects/glfw3/
+subprojects/gperf.wrap
+subprojects/gperf/
+subprojects/harfbuzz/
subprojects/jansson.wrap
subprojects/jansson/
subprojects/libakiyo
@@ -33,6 +39,10 @@ subprojects/libdvdread/
subprojects/libev-4.33/
subprojects/libev.wrap
subprojects/libplacebo/
+subprojects/libpng-*/
+subprojects/libpng.wrap
+subprojects/libxml2-*/
+subprojects/libxml2.wrap
subprojects/linmath.h/
subprojects/miniaudio/
subprojects/packagecache/
diff --git a/flake.lock b/flake.lock
index 18a86a5..09983e1 100644
--- a/flake.lock
+++ b/flake.lock
@@ -5,11 +5,11 @@
"systems": "systems"
},
"locked": {
- "lastModified": 1726560853,
- "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
- "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
@@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1731139594,
- "narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
+ "lastModified": 1731676054,
+ "narHash": "sha256-OZiZ3m8SCMfh3B6bfGC/Bm4x3qc1m2SVEAlkV6iY7Yg=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
+ "rev": "5e4fbfb6b3de1aa2872b76d49fafc942626e2add",
"type": "github"
},
"original": {
diff --git a/flake.nix b/flake.nix
index 2166ae8..ed4e79b 100644
--- a/flake.nix
+++ b/flake.nix
@@ -13,6 +13,13 @@
patches = [];
});
})
+ (self: super: {
+ notcurses = super.notcurses.overrideAttrs (old: {
+ patches = [
+ ./subprojects/packagefiles/notcurses/notcurses_tmux.diff
+ ];
+ });
+ })
];
pkgs = import nixpkgs {
inherit system;
@@ -97,7 +104,8 @@
ruff-lsp
perf-tools
valgrind
- ((python3.override { enableOptimizations = true; reproducibleBuild = false; }).withPackages (python-pkgs: [
+ kcachegrind
+ ((python3.override {}).withPackages (python-pkgs: [
python-pkgs.setuptools
python-pkgs.pip
python-pkgs.cython
diff --git a/scripts/run_server.sh b/scripts/run_server.sh
index df6571a..fde98ea 100755
--- a/scripts/run_server.sh
+++ b/scripts/run_server.sh
@@ -2,7 +2,8 @@
export CURL_CA_BUNDLE=/etc/ssl/certs/ca-bundle.crt
export PYTHONDONTWRITEBYTECODE=1
export PYTHONPATH=$HOME/c/camu/src/portal/vendor/vendor
-./src/fruits/cmsrv/cmsrv $@
+screen -c ../scripts/screenrc
+#./src/fruits/cmsrv/cmsrv $@
#valgrind --log-file=./server-valgrind.log ./src/fruits/cmsrv/cmsrv $@
#cpulimit -l 1 ./src/fruits/cmsrv/cmsrv $@
#gdb -ex run --args ./src/fruits/cmsrv/cmsrv $@
diff --git a/scripts/screenrc b/scripts/screenrc
new file mode 100644
index 0000000..9c84722
--- /dev/null
+++ b/scripts/screenrc
@@ -0,0 +1,9 @@
+escape ^[^[
+bind ^[ colon "quit\015"
+mousetrack on
+defmousetrack on
+screen -t 'cmsrv' ./src/fruits/cmsrv/cmsrv
+split
+focus
+screen -t 'cmsrv_debug'
+stuff 'gdb -p $(pgrep cmsrv) -ex cont '
diff --git a/src/buffer/audio.c b/src/buffer/audio.c
index 3abae42..5058154 100644
--- a/src/buffer/audio.c
+++ b/src/buffer/audio.c
@@ -9,9 +9,9 @@
#include "common_internal.h"
#include "volume.h"
-#define BUFFER_SIZE (9 * 1000000L)
-#define BUFFER_MARK_MIN (4.3 * 1000000L) // Must be a most half of the buffer size.
-#define BUFFER_MARK_BUFFERED (4.0 * 1000000L)
+#define BUFFER_SIZE 9.0
+#define BUFFER_MARK_MIN 4.3 // Must be a most half of the buffer size.
+#define BUFFER_MARK_BUFFERED 4.0
#ifdef CAMU_AUDIO_BUFFER_FADE
#define FADE_STEP(fmt) (1.f / (fmt)->sample_rate)
@@ -27,7 +27,7 @@ enum {
static void reset_buffer_state(struct camu_audio_buffer *buf)
{
- buf->pts = -1.0;
+ al_atomic_store(f64)(&buf->pts, -1.0, AL_ATOMIC_RELAXED);
buf->pause = PAUSE_PAUSED;
al_atomic_store(s32)(&buf->unpause, 0, AL_ATOMIC_RELAXED);
al_atomic_store(s32)(&buf->volume.set, 0, AL_ATOMIC_RELAXED);
@@ -40,11 +40,9 @@ static void reset_buffer_state(struct camu_audio_buffer *buf)
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)
+bool camu_audio_buffer_init(struct camu_audio_buffer *buf, struct camu_clock *clock)
{
buf->clock = clock;
- buf->mixer = mixer;
reset_buffer_state(buf);
buf->ignore_desync = false;
buf->latency = 0.0;
@@ -54,10 +52,11 @@ bool camu_audio_buffer_init(struct camu_audio_buffer *buf, struct camu_clock *cl
return true;
}
-bool camu_audio_buffer_configure(struct camu_audio_buffer *buf, struct camu_codec_stream *stream)
+bool camu_audio_buffer_configure(struct camu_audio_buffer *buf, struct camu_codec_stream *stream,
+ struct camu_mixer *mixer)
{
camu_audio_format_copy(&buf->fmt.in, &stream->audio.fmt);
- camu_mixer_pick_format(buf->mixer, &buf->fmt);
+ camu_mixer_pick_format(mixer, &buf->fmt);
buf->fmt.resampler_needed = !camu_resampler_format_matches(&buf->fmt);
if (buf->fmt.resampler_needed) {
#ifdef CAMU_HAVE_FFMPEG
@@ -76,12 +75,12 @@ bool camu_audio_buffer_configure(struct camu_audio_buffer *buf, struct camu_code
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_audio_format_usec_to_bytes(&buf->fmt.req, BUFFER_SIZE);
+ buf->size = camu_audio_format_sec_to_bytes(&buf->fmt.req, BUFFER_SIZE);
buf->data = (u8 *)al_malloc(buf->size);
al_ring_buffer_init(&buf->rb, buf->data, buf->size);
- 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);
+ buf->mark.min = camu_audio_format_sec_to_bytes(&buf->fmt.req, BUFFER_MARK_MIN);
+ buf->mark.buffered = camu_audio_format_sec_to_bytes(&buf->fmt.req, BUFFER_MARK_BUFFERED);
camu_peak_buffer_init(&buf->peak, 1024 * 16);
buf->stream = stream;
@@ -144,6 +143,16 @@ static bool push_internal(struct camu_audio_buffer *buf, u8 *data, s32 sample_co
return true;
}
+static bool frame_is_late(struct camu_audio_buffer *buf, f64 pts, f64 duration)
+{
+ f64 base = al_atomic_load(f64)(&buf->pts, AL_ATOMIC_ACQUIRE);
+ if (base == -1.0) {
+ base = camu_clock_get_base_pts(buf->clock);
+ al_atomic_store(f64)(&buf->pts, base, AL_ATOMIC_RELEASE);
+ }
+ return pts + duration < base;
+}
+
#ifdef CAMU_HAVE_FFMPEG
static void push_av_frame_internal(struct camu_audio_buffer *buf, AVFrame *frame)
{
@@ -151,8 +160,7 @@ static void push_av_frame_internal(struct camu_audio_buffer *buf, AVFrame *frame
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, sample_count);
- if (pts + duration >= camu_clock_get_base_pts(buf->clock)) {
- if (buf->pts == -1.0) buf->pts = pts;
+ if (!frame_is_late(buf, pts, duration)) {
u8 **data = frame->data;
if (buf->fmt.resampler_needed) {
sample_count = buf->resamp->convert(buf->resamp, (const u8 **)data, sample_count);
@@ -173,9 +181,8 @@ void camu_audio_buffer_push(struct camu_audio_buffer *buf, struct camu_codec_fra
s32 sample_count = frame->audio.sample_count;
f64 pts = frame->pts;
f64 duration = camu_audio_format_samples_to_sec(&buf->fmt.in, sample_count);
- if (pts + duration >= camu_clock_get_base_pts(buf->clock)) {
- if (buf->pts == -1.0) buf->pts = pts;
- u8 *store[AV_NUM_DATA_POINTERS] = { 0 };
+ if (!frame_is_late(buf, pts, duration)) {
+ u8 *store[CAMU_NUM_DATA_POINTERS] = { 0 };
store[0] = frame->data;
u8 **data = store;
if (buf->fmt.resampler_needed) {
@@ -201,17 +208,6 @@ void camu_audio_buffer_unpause(struct camu_audio_buffer *buf)
al_atomic_add(s32)(&buf->unpause, 1, AL_ATOMIC_RELAXED);
}
-// Not thread-safe, must be called while the buffer is not being read from or written to.
-void camu_audio_buffer_reset(struct camu_audio_buffer *buf)
-{
- 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)
{
@@ -233,18 +229,29 @@ void camu_audio_buffer_flush(struct camu_audio_buffer *buf)
al_log_debug("audio_buffer", "Flush requested.");
}
+// Not thread-safe, must be called while the buffer is not being read from or written to.
+void camu_audio_buffer_reset(struct camu_audio_buffer *buf)
+{
+ 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);
+ }
+}
+
+static void increment_pts(struct camu_audio_buffer *buf, f64 amount, f64 *base)
+{
+ *base += amount;
+ al_atomic_store(f64)(&buf->pts, *base, AL_ATOMIC_RELEASE);
+}
+
#define NOT_PAUSED(pause) (pause != PAUSE_PAUSED)
// 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)
{
- u8 flow = al_atomic_load(u8)(&buf->flow, AL_ATOMIC_ACQUIRE);
- if (flow == SIGNALED || camu_clock_is_ended(buf->clock)) {
- buf->callback(buf->userdata, CAMU_BUFFER_EOF);
- return 0;
- }
-
if (al_atomic_load(s32)(&buf->volume.set, AL_ATOMIC_ACQUIRE) > 0) {
buf->volume.user = al_atomic_load(f32)(&buf->volume.queued, AL_ATOMIC_RELAXED);
#ifdef CAMU_AUDIO_BUFFER_FADE
@@ -287,6 +294,7 @@ size_t camu_audio_buffer_read(struct camu_audio_buffer *buf, u8 *data, size_t re
al_atomic_sub(s32)(&buf->unpause, 1, AL_ATOMIC_RELEASE);
}
+ f64 base = al_atomic_load(f64)(&buf->pts, AL_ATOMIC_ACQUIRE);
f64 pts = camu_clock_get_pts(buf->clock, buf->latency);
size_t ret, signal = req;
size_t have = al_ring_buffer_occupied(&buf->rb);
@@ -296,7 +304,7 @@ size_t camu_audio_buffer_read(struct camu_audio_buffer *buf, u8 *data, size_t re
else have -= buf->fade.offset;
if (buf->pause != PAUSE_FADING && buf->pause != PAUSE_PLAYING) {
#endif
- pts -= buf->pts;
+ pts -= base;
// 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)) {
@@ -306,7 +314,7 @@ size_t camu_audio_buffer_read(struct camu_audio_buffer *buf, u8 *data, size_t re
al_log_info("audio_buffer", "Skipping %fs of audio (%zu bytes).", pts, ret);
ret = al_ring_buffer_discard(&buf->rb, ret);
have -= ret;
- buf->pts += camu_audio_format_bytes_to_sec(&buf->fmt.req, ret);
+ increment_pts(buf, camu_audio_format_bytes_to_sec(&buf->fmt.req, ret), &base);
// Could go on to underrun.
} else if (pts < 0.0) { // Delay.
pts = -pts;
@@ -325,6 +333,7 @@ size_t camu_audio_buffer_read(struct camu_audio_buffer *buf, u8 *data, size_t re
}
#endif
+ u8 flow = al_atomic_load(u8)(&buf->flow, AL_ATOMIC_ACQUIRE);
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.
@@ -375,7 +384,7 @@ size_t camu_audio_buffer_read(struct camu_audio_buffer *buf, u8 *data, size_t re
// Read as much as we determined we can.
ret = al_ring_buffer_read(&buf->rb, data, req);
al_assert(ret == req);
- buf->pts += camu_audio_format_bytes_to_sec(&buf->fmt.req, ret);
+ increment_pts(buf, camu_audio_format_bytes_to_sec(&buf->fmt.req, ret), &base);
#ifdef CAMU_AUDIO_BUFFER_FADE
}
f32 step = 0.f;
@@ -386,6 +395,7 @@ size_t camu_audio_buffer_read(struct camu_audio_buffer *buf, u8 *data, size_t re
}
if (step != 0.f || buf->fade.volume != 1.f) {
if (buf->volume.user > 1.f) {
+ // Only adjust step if it's an increase.
step *= buf->volume.user;
}
buf->fade.volume = apply_volume(data, req, &buf->fmt.req, buf->fade.volume, buf->volume.user, step);
diff --git a/src/buffer/audio.h b/src/buffer/audio.h
index 1d49448..b4d7594 100644
--- a/src/buffer/audio.h
+++ b/src/buffer/audio.h
@@ -13,9 +13,8 @@
struct camu_audio_buffer {
struct camu_codec_stream *stream;
- struct camu_mixer *mixer;
- f64 pts;
+ atomic(f64) pts;
u8 pause;
atomic(s32) unpause;
struct camu_clock *clock;
@@ -57,13 +56,14 @@ struct camu_audio_buffer {
void *userdata;
};
-bool camu_audio_buffer_init(struct camu_audio_buffer *buf, struct camu_clock *clock, struct camu_mixer *mixer);
-bool camu_audio_buffer_configure(struct camu_audio_buffer *buf, struct camu_codec_stream *stream);
+bool camu_audio_buffer_init(struct camu_audio_buffer *buf, struct camu_clock *clock);
+bool camu_audio_buffer_configure(struct camu_audio_buffer *buf, struct camu_codec_stream *stream,
+ struct camu_mixer *mixer);
void camu_audio_buffer_set_volume(struct camu_audio_buffer *buf, f32 volume);
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);
+void camu_audio_buffer_reset(struct camu_audio_buffer *buf);
size_t camu_audio_buffer_read(struct camu_audio_buffer *buf, u8 *data, size_t req);
void camu_audio_buffer_free(struct camu_audio_buffer *buf);
diff --git a/src/buffer/frame_queue.h b/src/buffer/frame_queue.h
index 0175668..f670ded 100644
--- a/src/buffer/frame_queue.h
+++ b/src/buffer/frame_queue.h
@@ -5,6 +5,7 @@
#endif
#include "../codec/codec.h"
+#include "../buffer/video.h"
enum {
CAMU_QUEUE_OK = 0,
@@ -14,9 +15,12 @@ enum {
};
struct camu_frame_queue {
+ struct camu_video_buffer *buf;
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);
+ void (*configure_subtitles)(struct camu_frame_queue *, s32, s32, AVCodecParameters *);
+ void (*push_subtitle)(struct camu_frame_queue *, AVPacket *);
#endif
void (*flush)(struct camu_frame_queue *);
s32 (*count)(struct camu_frame_queue *);
diff --git a/src/buffer/video.c b/src/buffer/video.c
index a8b35c6..b528141 100644
--- a/src/buffer/video.c
+++ b/src/buffer/video.c
@@ -15,14 +15,15 @@
#define BUFFER_MARK_HIGH ((1.0 / 30.0) * 12)
#define BUFFER_MARK_RESET (BUFFER_MARK_HIGH * 2.0)
-bool camu_video_buffer_init(struct camu_video_buffer *buf, struct camu_clock *clock, struct camu_renderer *renderer)
+bool camu_video_buffer_init(struct camu_video_buffer *buf, struct camu_clock *clock)
{
buf->clock = clock;
buf->latency = 0.0;
- buf->pts = -1.0;
+ al_atomic_store(f64)(&buf->pts, -1.0, AL_ATOMIC_RELAXED);
// Defaulting single_frame to true can simplify non-configured buffers in sink.
buf->single_frame = true;
- buf->queue = renderer->create_queue(renderer);
+ buf->avg_frame_duration = 0.0;
+ buf->queue = NULL;
buf->buffered = false;
al_atomic_store(u8)(&buf->flow, FLOWING, AL_ATOMIC_RELAXED);
#ifdef CAMU_SCREEN_THREADED
@@ -32,27 +33,23 @@ bool camu_video_buffer_init(struct camu_video_buffer *buf, struct camu_clock *cl
return true;
}
-bool camu_video_buffer_configure(struct camu_video_buffer *buf, struct camu_codec_stream *stream)
+bool camu_video_buffer_configure(struct camu_video_buffer *buf, struct camu_codec_stream *stream,
+ struct camu_renderer *renderer)
{
buf->stream = stream;
+ buf->queue = renderer->create_queue(renderer);
+ buf->queue->buf = buf;
struct camu_video_format *fmt = &buf->stream->video.fmt;
switch (stream->mode) {
case CAMU_NORMAL: {
camu_video_format_copy(&buf->fmt.in, fmt);
-
- buf->single_frame = true;
- buf->avg_frame_duration = 0.0;
-
const char *format_name = camu_pixel_format_name(fmt->format);
- al_log_info("video_buffer", "Stream: %s (%dx%d) %s.",
- format_name, fmt->width, fmt->height, "IMAGE");
-
+ al_log_info("video_buffer", "Stream: %s (%dx%d) %s.", format_name, fmt->width, fmt->height, "IMAGE");
break;
}
#ifdef CAMU_HAVE_FFMPEG
case CAMU_FFMPEG_COMPAT: {
AVCodecParameters *codecpar = stream->av.stream->codecpar;
- s64 duration = stream->av.stream->duration;
AVRational frame_rate = stream->av.stream->avg_frame_rate;
fmt->width = codecpar->width;
@@ -60,7 +57,7 @@ bool camu_video_buffer_configure(struct camu_video_buffer *buf, struct camu_code
fmt->format = codecpar->format;
camu_video_format_copy(&buf->fmt.in, fmt);
- buf->single_frame = duration == 0 || frame_rate.den == 0;
+ buf->single_frame = stream->duration == 0 || frame_rate.den == 0;
if (buf->single_frame) frame_rate = (AVRational){ 0, 1 };
buf->avg_frame_duration = buf->single_frame ? 0.0 : av_q2d(av_inv_q(frame_rate));
@@ -129,25 +126,33 @@ static void after_push_internal(struct camu_video_buffer *buf)
static bool push_av_frame_internal(struct camu_video_buffer *buf, AVFrame *frame)
{
f64 pts = 0.0;
+ f64 base = al_atomic_load(f64)(&buf->pts, AL_ATOMIC_ACQUIRE);
if (!buf->single_frame) {
AVStream *stream = buf->stream->av.stream;
pts = frame->best_effort_timestamp * av_q2d(stream->time_base);
- if (pts + buf->avg_frame_duration < camu_clock_get_base_pts(buf->clock)) {
+ // If buf->pts = -1.0, this is the first frame pushed either on
+ // a new stream or after a reset(). Meaning, we can safely set buf->pts
+ // from this thread.
+ if (base == -1.0) {
+ base = camu_clock_get_base_pts(buf->clock);
+ al_atomic_store(f64)(&buf->pts, base, AL_ATOMIC_RELEASE);
+ }
+ if (pts + buf->avg_frame_duration < base) {
av_frame_free(&frame);
return false;
}
+ } else if (base == -1.0) {
+ al_atomic_store(f64)(&buf->pts, 0.0, AL_ATOMIC_RELEASE);
}
- if (buf->pts == -1.0) buf->pts = pts;
#ifdef CAMU_VIDEO_BUFFER_FORCE_SCALER
if (buf->fmt.scaler_needed) {
if (!buf->scale->scale(buf->scale, (const u8 **)frame->data, frame->linesize)) {
return false;
}
av_frame_free(&frame);
- frame = av_frame_clone(buf->scale->get_frame(buf->scale));
+ frame = av_frame_clone(buf->scale->get_frame(buf->scale)->av.frame);
}
#endif
- if (frame) frame->opaque = buf;
buf->queue->push_av_frame(buf->queue, frame, pts);
return true;
}
@@ -161,9 +166,14 @@ void camu_video_buffer_push(struct camu_video_buffer *buf, struct camu_codec_fra
return;
}
switch (frame->mode) {
- case CAMU_NORMAL:
+ case CAMU_NORMAL: {
+ f64 base = al_atomic_load(f64)(&buf->pts, AL_ATOMIC_ACQUIRE);
+ if (base == -1.0) {
+ al_atomic_store(f64)(&buf->pts, 0.0, AL_ATOMIC_RELEASE);
+ }
buf->queue->push(buf->queue, frame, 0.0);
break;
+ }
#ifdef CAMU_HAVE_FFMPEG
case CAMU_FFMPEG_COMPAT: {
bool pushed = push_av_frame_internal(buf, frame->av.frame);
@@ -176,15 +186,19 @@ void camu_video_buffer_push(struct camu_video_buffer *buf, struct camu_codec_fra
after_push_internal(buf);
}
-// Not thread-safe, must be called while the buffer is not being read from or written to.
-void camu_video_buffer_reset(struct camu_video_buffer *buf)
+#ifdef CAMU_HAVE_FFMPEG
+void camu_video_buffer_configure_subtitles(struct camu_video_buffer *buf, AVCodecParameters *codecpar)
{
- buf->pts = -1.0;
- buf->queue->reset(buf->queue);
- buf->buffered = false;
- al_atomic_store(u8)(&buf->flow, FLOWING, AL_ATOMIC_RELAXED);
+ struct camu_video_format *fmt = &buf->stream->video.fmt;
+ buf->queue->configure_subtitles(buf->queue, fmt->width, fmt->height, codecpar);
}
+void camu_video_buffer_push_subtitle(struct camu_video_buffer *buf, AVPacket *pkt)
+{
+ buf->queue->push_subtitle(buf->queue, pkt);
+}
+#endif
+
// flush() always comes from the same thread as push().
void camu_video_buffer_flush(struct camu_video_buffer *buf)
{
@@ -198,18 +212,28 @@ void camu_video_buffer_flush(struct camu_video_buffer *buf)
al_atomic_store(u8)(&buf->flow, FLUSHED, AL_ATOMIC_RELAXED);
}
+// Not thread-safe, must be called while the buffer is not being read from or written to.
+void camu_video_buffer_reset(struct camu_video_buffer *buf)
+{
+ al_atomic_store(f64)(&buf->pts, -1.0, AL_ATOMIC_RELAXED);
+ if (buf->queue) buf->queue->reset(buf->queue);
+ buf->buffered = false;
+ al_atomic_store(u8)(&buf->flow, FLOWING, AL_ATOMIC_RELAXED);
+}
+
bool camu_video_buffer_read(struct camu_video_buffer *buf, void *out)
{
- u8 flow = al_atomic_load(u8)(&buf->flow, AL_ATOMIC_ACQUIRE);
+ f64 base = 0.0;
if (!buf->single_frame) {
- if (flow == SIGNALED || camu_clock_is_ended(buf->clock)) {
- buf->callback(buf->userdata, CAMU_BUFFER_EOF);
- return false;
- }
+ base = al_atomic_load(f64)(&buf->pts, AL_ATOMIC_ACQUIRE);
f64 pts = camu_clock_get_pts(buf->clock, buf->latency);
- if (pts > buf->pts) buf->pts = pts;
+ if (pts > base) {
+ base = pts;
+ al_atomic_store(f64)(&buf->pts, base, AL_ATOMIC_RELEASE);
+ }
}
- u8 ret = buf->queue->read(buf->queue, buf->pts, out);
+ u8 ret = buf->queue->read(buf->queue, base, out);
+ u8 flow = al_atomic_load(u8)(&buf->flow, AL_ATOMIC_ACQUIRE);
if (flow == FLUSHED && (ret == CAMU_QUEUE_EOF || (buf->single_frame && ret == CAMU_QUEUE_OK))) {
buf->callback(buf->userdata, CAMU_BUFFER_EOF);
al_atomic_store(u8)(&buf->flow, SIGNALED, AL_ATOMIC_RELEASE);
@@ -228,5 +252,5 @@ void camu_video_buffer_free(struct camu_video_buffer *buf)
#ifdef CAMU_VIDEO_BUFFER_FORCE_SCALER
if (buf->fmt.scaler_needed) buf->scale->free(&buf->scale);
#endif
- buf->queue->free(&buf->queue);
+ if (buf->queue) buf->queue->free(&buf->queue);
}
diff --git a/src/buffer/video.h b/src/buffer/video.h
index 0b48361..fd7ec9f 100644
--- a/src/buffer/video.h
+++ b/src/buffer/video.h
@@ -6,7 +6,6 @@
#include "../codec/codec.h"
#include "../render/renderer.h"
-#include "../screen/screen.h"
#include "../screen/view.h"
#include "clock.h"
@@ -17,7 +16,7 @@ struct camu_video_buffer {
struct camu_clock *clock;
f64 latency;
- f64 pts;
+ atomic(f64) pts;
bool single_frame;
f64 avg_frame_duration;
@@ -42,12 +41,17 @@ struct camu_video_buffer {
void *userdata;
};
-bool camu_video_buffer_init(struct camu_video_buffer *buf, struct camu_clock *clock, struct camu_renderer *renderer);
-bool camu_video_buffer_configure(struct camu_video_buffer *buf, struct camu_codec_stream *stream);
+bool camu_video_buffer_init(struct camu_video_buffer *buf, struct camu_clock *clock);
+bool camu_video_buffer_configure(struct camu_video_buffer *buf, struct camu_codec_stream *stream,
+ struct camu_renderer *renderer);
void camu_video_buffer_set_latency(struct camu_video_buffer *buf, s32 frames);
bool camu_video_buffer_is_single_frame(struct camu_video_buffer *buf);
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);
+#ifdef CAMU_HAVE_FFMPEG
+void camu_video_buffer_configure_subtitles(struct camu_video_buffer *buf, AVCodecParameters *codecpar);
+void camu_video_buffer_push_subtitle(struct camu_video_buffer *buf, AVPacket *pkt);
+#endif
void camu_video_buffer_flush(struct camu_video_buffer *buf);
+void camu_video_buffer_reset(struct camu_video_buffer *buf);
bool camu_video_buffer_read(struct camu_video_buffer *buf, void *out);
void camu_video_buffer_free(struct camu_video_buffer *buf);
diff --git a/src/codec/codec.h b/src/codec/codec.h
index 6fd42e9..4b93673 100644
--- a/src/codec/codec.h
+++ b/src/codec/codec.h
@@ -29,7 +29,8 @@ 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
+ CAMU_STREAM_SUBTITLE = AVMEDIA_TYPE_SUBTITLE,
+ CAMU_STREAM_ATTACHMENT = AVMEDIA_TYPE_ATTACHMENT
};
enum {
CAMU_SAMPLE_FORMAT_NONE = AV_SAMPLE_FMT_NONE,
@@ -52,6 +53,7 @@ enum {
CAMU_PIXEL_FORMAT_GREYA = AV_PIX_FMT_GRAY8A,
CAMU_PIXEL_FORMAT_GREY = AV_PIX_FMT_GRAY8
};
+#define CAMU_NUM_DATA_POINTERS AV_NUM_DATA_POINTERS
#else
enum {
CAMU_OK = 0,
@@ -66,7 +68,8 @@ enum {
CAMU_STREAM_UNKNOWN = 0,
CAMU_STREAM_AUDIO,
CAMU_STREAM_VIDEO,
- CAMU_STREAM_SUBTITLE
+ CAMU_STREAM_SUBTITLE,
+ CAMU_STREAM_ATTACHMENT
};
enum {
CAMU_SAMPLE_FORMAT_NONE = -1,
@@ -89,6 +92,7 @@ enum {
CAMU_PIXEL_FORMAT_GREYA,
CAMU_PIXEL_FORMAT_GREY
};
+#define CAMU_NUM_DATA_POINTERS 8
#endif
enum {
@@ -204,7 +208,7 @@ struct camu_scaler_format {
struct camu_scaler {
bool (*init)(struct camu_scaler *, struct camu_scaler_format *);
bool (*scale)(struct camu_scaler *, const u8 **in_slice, s32 *in_strides);
- AVFrame *(*get_frame)(struct camu_scaler *);
+ struct camu_codec_frame *(*get_frame)(struct camu_scaler *);
void (*free)(struct camu_scaler **);
};
@@ -276,7 +280,7 @@ static inline f64 camu_audio_format_samples_to_sec(struct camu_audio_format *fmt
return samples / (f64)fmt->sample_rate;
}
-static inline size_t camu_audio_format_usec_to_bytes(struct camu_audio_format *fmt, size_t usec)
+static inline size_t camu_audio_format_usec_to_bytes(struct camu_audio_format *fmt, u64 usec)
{
// Keep order of operations for precision.
size_t samples = usec * (fmt->sample_rate / 1000000.0);
@@ -289,6 +293,12 @@ static inline size_t camu_audio_format_sec_to_bytes(struct camu_audio_format *fm
return samples * camu_audio_format_bytes_per_sample(fmt) * fmt->channel_count;
}
+static inline u64 camu_audio_format_bytes_to_usec(struct camu_audio_format *fmt, size_t bytes)
+{
+ size_t samples = bytes / (camu_audio_format_bytes_per_sample(fmt) * fmt->channel_count);
+ return samples / (fmt->sample_rate / 1000000.0);
+}
+
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);
diff --git a/src/codec/ffmpeg/decoder.c b/src/codec/ffmpeg/decoder.c
index d3ab3b2..0fb60ac 100644
--- a/src/codec/ffmpeg/decoder.c
+++ b/src/codec/ffmpeg/decoder.c
@@ -37,10 +37,11 @@ static bool ff_decoder_init(struct camu_decoder *dec, struct camu_renderer *rend
}
s32 cpus = av_cpu_count();
- if (cpus > 4) cpus = 2;
+ if (cpus >= 8) cpus = 4;
av->codec_context->thread_count = cpus / 2;
// FF_THREAD_FRAME or FF_THREAD_SLICE.
- av->codec_context->thread_type = FF_THREAD_SLICE;
+ //av->codec_context->thread_type = FF_THREAD_SLICE;
+ av->codec_context->thread_type = FF_THREAD_FRAME;
al_log_debug("ff_decoder", "Using %i threads for decoder.", cpus);
#if !defined CAMU_SINK_NO_VIDEO && defined CAMU_RENDERER_VULKAN
@@ -61,8 +62,8 @@ static bool ff_decoder_init(struct camu_decoder *dec, struct camu_renderer *rend
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->duration = stream->av.stream->duration;
+ //av->duration = stream->duration;
+ //av->time_base = stream->av.stream->time_base;
//av->last_pts = 0;
//av->last_duration = 0;
//av->seek_pos = -1;
diff --git a/src/codec/ffmpeg/decoder.h b/src/codec/ffmpeg/decoder.h
index 88bbd4c..2e35bb1 100644
--- a/src/codec/ffmpeg/decoder.h
+++ b/src/codec/ffmpeg/decoder.h
@@ -8,8 +8,8 @@
struct camu_ff_decoder {
struct camu_decoder dec;
AVCodecContext *codec_context;
- AVRational time_base;
- u64 duration;
+ //AVRational time_base;
+ //u64 duration;
//s64 last_pts;
//s64 last_duration;
//s64 seek_pos;
diff --git a/src/codec/ffmpeg/demuxer.c b/src/codec/ffmpeg/demuxer.c
index 8ea645b..24c1ba6 100644
--- a/src/codec/ffmpeg/demuxer.c
+++ b/src/codec/ffmpeg/demuxer.c
@@ -77,28 +77,31 @@ 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);
- stream->duration = 0;
- } else if (stream->duration < 0) {
- al_log_warn("ff_demuxer", "Stream #%u has an invalid duration (%ld).", i, stream->duration);
- if (av->format_context->duration < 0) {
- stream->duration = 0;
+ u64 duration = 0;
+ if (type == AVMEDIA_TYPE_AUDIO || type == AVMEDIA_TYPE_VIDEO || type == AVMEDIA_TYPE_SUBTITLE) {
+ // 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);
} else {
- stream->duration = av_rescale_q(av->format_context->duration, AV_TIME_BASE_Q, stream->time_base);
+ if (stream->duration < 0) {
+ al_log_warn("ff_demuxer", "Stream #%u has an invalid duration (%ld).", i, stream->duration);
+ stream->duration = av->format_context->duration < 0 ? 0 :
+ av_rescale_q(av->format_context->duration, AV_TIME_BASE_Q, stream->time_base);
+ al_log_info("ff_demuxer", "Defaulting stream #%u to a duration of %.3fs.",
+ i, stream->duration * av_q2d(stream->time_base));
+ al_assert(stream->duration >= 0);
+ }
+ duration = (u64)av_rescale_q(stream->duration, stream->time_base, AV_TIME_BASE_Q);
+ if (duration > av->duration) av->duration = duration;
}
- 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 == AV_NOPTS_VALUE) stream->start_time = 0;
+ } else if (type != AVMEDIA_TYPE_ATTACHMENT) {
+ continue;
}
- 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_codec_stream){
.mode = CAMU_FFMPEG_COMPAT,
.type = type,
- .duration = (u64)duration,
+ .duration = duration,
.av.stream = stream
}));
}
diff --git a/src/codec/ffmpeg/packet_ext.c b/src/codec/ffmpeg/packet_ext.c
index c0e400d..16f5748 100644
--- a/src/codec/ffmpeg/packet_ext.c
+++ b/src/codec/ffmpeg/packet_ext.c
@@ -38,6 +38,23 @@ void aki_packet_write_av_codec_id(struct aki_packet *packet, enum AVCodecID code
AKI_PACKET_WRITE_TYPE(packet, enum AVCodecID, codec_id);
}
+void aki_packet_write_av_dictionary(struct aki_packet *packet, AVDictionary *dict)
+{
+ s32 count = av_dict_count(dict);
+ AKI_PACKET_WRITE_TYPE(packet, s32, count);
+ const AVDictionaryEntry *entry = NULL;
+ while ((entry = av_dict_iterate(dict, entry))) {
+ size_t len = al_strlen(entry->key);
+ al_assert(len <= UINT32_MAX);
+ AKI_PACKET_WRITE_TYPE(packet, u32, len);
+ AKI_PACKET_WRITE_DATA(packet, entry->key, len);
+ len = al_strlen(entry->value);
+ al_assert(len <= UINT32_MAX);
+ AKI_PACKET_WRITE_TYPE(packet, u32, len);
+ AKI_PACKET_WRITE_DATA(packet, entry->value, len);
+ }
+}
+
void aki_packet_write_av_stream(struct aki_packet *packet, AVStream *stream)
{
AKI_PACKET_WRITE_TYPE(packet, s32, stream->index);
@@ -46,6 +63,7 @@ void aki_packet_write_av_stream(struct aki_packet *packet, AVStream *stream)
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);
+ aki_packet_write_av_dictionary(packet, stream->metadata);
if (stream->avg_frame_rate.den == 0) {
// r_frame_rate.den == 0 handled on the client.
AKI_PACKET_WRITE_TYPE(packet, AVRational, stream->r_frame_rate);
@@ -118,6 +136,25 @@ enum AVCodecID aki_packet_read_av_codec_id(struct aki_packet *packet)
return id;
}
+void aki_packet_read_av_dictionary(struct aki_packet *packet, AVDictionary **dict)
+{
+ s32 count;
+ AKI_PACKET_READ_TYPE(packet, s32, count);
+ for (s32 i = 0; i < count; i++) {
+ char *key, *value;
+ u32 len;
+ AKI_PACKET_READ_TYPE(packet, u32, len);
+ AKI_PACKET_READ_DATA(packet, len, key);
+ // Key and value MUST be allocated with av_malloc functions
+ // just like all FFmpeg structures.
+ key = av_strndup(key, len);
+ AKI_PACKET_READ_TYPE(packet, u32, len);
+ AKI_PACKET_READ_DATA(packet, len, value);
+ value = av_strndup(value, len);
+ av_dict_set(dict, key, value, AV_DICT_DONT_STRDUP_KEY | AV_DICT_DONT_STRDUP_VAL);
+ }
+}
+
AVStream *aki_packet_read_av_stream(AVFormatContext *format_context, const AVCodec *codec, struct aki_packet *packet)
{
AVStream *stream = avformat_new_stream(format_context, codec);
@@ -127,6 +164,8 @@ AVStream *aki_packet_read_av_stream(AVFormatContext *format_context, const AVCod
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->metadata = NULL;
+ aki_packet_read_av_dictionary(packet, &stream->metadata);
AKI_PACKET_READ_TYPE(packet, AVRational, stream->avg_frame_rate);
return stream;
}
diff --git a/src/codec/ffmpeg/packet_ext.h b/src/codec/ffmpeg/packet_ext.h
index 36b6ed5..26a1392 100644
--- a/src/codec/ffmpeg/packet_ext.h
+++ b/src/codec/ffmpeg/packet_ext.h
@@ -6,6 +6,7 @@
#include <libavcodec/packet.h>
#include <libavcodec/codec_par.h>
#include <libavformat/avformat.h>
+#include <libavutil/dict.h>
void aki_packet_write_av_codec_parameters(struct aki_packet *packet, AVCodecParameters *codecpar);
void aki_packet_write_av_codec_id(struct aki_packet *packet, enum AVCodecID codec_id);
diff --git a/src/codec/ffmpeg/scaler.c b/src/codec/ffmpeg/scaler.c
index d233268..0089dd1 100644
--- a/src/codec/ffmpeg/scaler.c
+++ b/src/codec/ffmpeg/scaler.c
@@ -49,11 +49,12 @@ static bool ff_scaler_init(struct camu_scaler *scale, struct camu_scaler_format
return false;
}
- av->frame = alloc_picture(fmt->req.format, fmt->req.width, fmt->req.height);
- if (!av->frame) {
+ AVFrame *frame = alloc_picture(fmt->req.format, fmt->req.width, fmt->req.height);
+ if (!frame) {
al_log_error("ff_scaler", "Failed to allocate frame.");
return false;
}
+ av->frame.av.frame = frame;
return true;
}
@@ -61,8 +62,9 @@ static bool ff_scaler_init(struct camu_scaler *scale, struct camu_scaler_format
static bool ff_scaler_scale(struct camu_scaler *scale, const u8 **in_slice, s32 *in_strides)
{
struct camu_ff_scaler *av = (struct camu_ff_scaler *)scale;
- s32 ret = sws_scale(av->scaler_context, in_slice, in_strides, 0, av->fmt.in.height,
- av->frame->data, av->frame->linesize);
+ AVFrame *frame = av->frame.av.frame;
+ s32 ret = sws_scale(av->scaler_context, in_slice, in_strides, 0,
+ av->fmt.in.height, frame->data, frame->linesize);
if (ret != av->fmt.req.height) {
al_log_error("ff_scaler", "Failed to scale frame (%s).", av_err2str(ret));
return false;
@@ -70,17 +72,17 @@ static bool ff_scaler_scale(struct camu_scaler *scale, const u8 **in_slice, s32
return true;
}
-static AVFrame *ff_scaler_get_frame(struct camu_scaler *scale)
+static struct camu_codec_frame *ff_scaler_get_frame(struct camu_scaler *scale)
{
struct camu_ff_scaler *av = (struct camu_ff_scaler *)scale;
- return av->frame;
+ return &av->frame;
}
static void ff_scaler_free(struct camu_scaler **scale)
{
struct camu_ff_scaler *av = (struct camu_ff_scaler *)*scale;
- av_freep(&av->frame->data[0]);
- av_frame_free(&av->frame);
+ av_freep(&av->frame.av.frame->data[0]);
+ av_frame_free(&av->frame.av.frame);
sws_freeContext(av->scaler_context);
al_free(av);
*scale = NULL;
diff --git a/src/codec/ffmpeg/scaler.h b/src/codec/ffmpeg/scaler.h
index a4effb3..f01f3fd 100644
--- a/src/codec/ffmpeg/scaler.h
+++ b/src/codec/ffmpeg/scaler.h
@@ -9,7 +9,7 @@ struct camu_ff_scaler {
struct camu_scaler scale;
struct SwsContext *scaler_context;
struct camu_scaler_format fmt;
- AVFrame *frame;
+ struct camu_codec_frame frame;
};
struct camu_scaler *camu_ff_scaler_create(void);
diff --git a/src/fruits/cmsrv/cmsrv.c b/src/fruits/cmsrv/cmsrv.c
index c6f4546..511484d 100644
--- a/src/fruits/cmsrv/cmsrv.c
+++ b/src/fruits/cmsrv/cmsrv.c
@@ -127,7 +127,9 @@ s32 wmain(s32 argc, wchar_t **argv)
al_set_print(log_callback, &s);
+#ifdef CAMU_HAVE_FFMPEG
camu_ff_set_default_log_callback();
+#endif
aki_event_loop_init(&s.loop);
@@ -162,6 +164,10 @@ s32 wmain(s32 argc, wchar_t **argv)
cmsrv_ui_close(&s.ui);
+#ifdef CAMU_HAVE_FFMPEG
+ camu_ff_free_default_log_callback();
+#endif
+
aki_common_close();
return EXIT_SUCCESS;
diff --git a/src/fruits/cmsrv/ui.c b/src/fruits/cmsrv/ui.c
index 1a52641..e10db5e 100644
--- a/src/fruits/cmsrv/ui.c
+++ b/src/fruits/cmsrv/ui.c
@@ -1,8 +1,9 @@
#include <al/lib.h>
+#include <al/log.h>
#include "ui.h"
-#define LOG_RATIO 1.3
+#define LOG_RATIO 2.1
static s32 resize_cb(struct ncplane *p)
{
@@ -99,7 +100,10 @@ static void layout_lists(struct cmsrv_ui *ui, struct ncplane *parent)
static void erase_lists(struct cmsrv_ui *ui)
{
struct ncplane *n = ui->lists.n;
- if (n) ncplane_erase(n);
+ if (n) {
+ ncplane_erase(n);
+ notcurses_render(ui->nc);
+ }
}
static void putnwstr_maxwidth_yx(struct ncplane *n, u32 y, u32 x, u32 maxwidth, wstr *w)
@@ -118,7 +122,7 @@ static void render_lists(struct cmsrv_ui *ui)
u32 max_width = ncplane_dim_x(n);
u32 max_height = ncplane_dim_y(n);
u32 current_line = 0;
- s32 entries_per_list = 10;
+ s32 entries_per_list = max_height;
struct lia_list *list;
al_array_foreach(ui->server->lists, i, list) {
diff --git a/src/fruits/cmv/cmv.c b/src/fruits/cmv/cmv.c
index 96b6f75..0fe33ad 100644
--- a/src/fruits/cmv/cmv.c
+++ b/src/fruits/cmv/cmv.c
@@ -61,7 +61,9 @@ s32 wmain(s32 argc, wchar_t **argv)
signal(SIGINT, sigint_handler);
+#ifdef CAMU_HAVE_FFMPEG
camu_ff_set_default_log_callback();
+#endif
aki_event_loop_init(&c.loop);
@@ -132,7 +134,9 @@ s32 wmain(s32 argc, wchar_t **argv)
out:
aki_event_loop_destroy(&c.loop);
+#ifdef CAMU_HAVE_FFMPEG
camu_ff_free_default_log_callback();
+#endif
stl_global_close();
aki_common_close();
diff --git a/src/fruits/ctv/ctv.c b/src/fruits/ctv/ctv.c
index 75936eb..5f4a819 100644
--- a/src/fruits/ctv/ctv.c
+++ b/src/fruits/ctv/ctv.c
@@ -49,6 +49,11 @@ static void onSurfaceCreated(GLFMDisplay *display, s32 width, s32 height)
struct ctv *c = (struct ctv *)glfmGetUserData(display);
(void)width;
(void)height;
+ GLFMRenderingAPI api = glfmGetRenderingAPI(display);
+ al_log_info("window_glfm", "Created GLES context of version %s.",
+ api == GLFMRenderingAPIOpenGLES32 ? "3.2" :
+ api == GLFMRenderingAPIOpenGLES31 ? "3.1" :
+ api == GLFMRenderingAPIOpenGLES3 ? "3.0" : "2.0");
if (!c->created) {
c->scr.callback = screen_callback;
c->scr.userdata = c;
diff --git a/src/fruits/droid/ctv/gradle/libs.versions.toml b/src/fruits/droid/ctv/gradle/libs.versions.toml
index 0b1b89c..c474f95 100644
--- a/src/fruits/droid/ctv/gradle/libs.versions.toml
+++ b/src/fruits/droid/ctv/gradle/libs.versions.toml
@@ -1,5 +1,5 @@
[versions]
-agp = "8.7.1"
+agp = "8.7.2"
[libraries]
diff --git a/src/liana/client.c b/src/liana/client.c
index 527376a..0edc262 100644
--- a/src/liana/client.c
+++ b/src/liana/client.c
@@ -22,14 +22,15 @@ static void parse_info_packet(struct lia_client *client, struct aki_packet *pack
u32 count = aki_packet_read_u32(packet);
for (u32 i = 0; i < count; i++) {
u8 mode = aki_packet_read_u8(packet);
+ u8 type = aki_packet_read_u8(packet);
+ u64 duration = aki_packet_read_u64(packet);
+ s32 index = aki_packet_read_s32(packet);
+ al_assert(index < 32);
struct lia_vcr_track *track = NULL;
switch (mode) {
case CAMU_NORMAL: {
- client->mask |= 1 << 0;
+ client->mask |= 1 << index;
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);
@@ -44,49 +45,63 @@ static void parse_info_packet(struct lia_client *client, struct aki_packet *pack
fmt->height = aki_packet_read_s32(packet);
fmt->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:
+ switch (type) {
+ case CAMU_STREAM_AUDIO:
client->mask |= 1 << index;
break;
- case AVMEDIA_TYPE_VIDEO:
+ case CAMU_STREAM_VIDEO:
client->mask |= 1 << index;
- //continue;
break;
- case AVMEDIA_TYPE_SUBTITLE:
+ case CAMU_STREAM_SUBTITLE:
+ client->mask |= 1 << index;
+ break;
+ case CAMU_STREAM_ATTACHMENT:
+ // Assume we have all the data we need in the AVStream object.
+ break;
default:
continue;
}
+ enum AVCodecID codec_id = aki_packet_read_av_codec_id(packet);
+ const AVCodec *codec = avcodec_find_decoder(codec_id);
+ AVFormatContext *format_context = avformat_alloc_context();
+ AVStream *stream = aki_packet_read_av_stream(format_context, codec, packet);
+ if (type == CAMU_STREAM_SUBTITLE && codec_id != AV_CODEC_ID_ASS) {
+ client->mask &= ~(1 << index);
+ continue;
+ }
+ if (type == CAMU_STREAM_ATTACHMENT) {
+ struct camu_codec_stream attachment;
+ attachment.type = CAMU_STREAM_ATTACHMENT;
+ attachment.av.stream = stream;
+ client->callback(client->userdata, LIANA_CLIENT_CONFIGURE, &attachment, track);
+ avformat_free_context(format_context);
+ 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) {
+ if (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->index = index;
+ track->stream.mode = mode;
+ track->stream.type = type;
+ track->stream.duration = duration;
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)) {
track->client->free(&track->client);
#ifdef CAMU_HAVE_FFMPEG
@@ -107,8 +122,12 @@ static void info_packet_callback(void *userdata, struct aki_packet_stream *strea
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);
+ if (client->mask == 0 || lia_vcr_is_empty(&client->vcr)) {
+ client->reconnect = false;
+ aki_packet_stream_disconnect(&client->data);
+ return;
+ }
stream->packet_callback = data_packet_callback;
struct aki_packet *rpacket = aki_packet_create();
aki_packet_write_s32(rpacket, client->mask);
@@ -124,6 +143,7 @@ static void packet_sent_callback(void *userdata, struct aki_packet *packet)
static void connection_callback(void *userdata, struct aki_packet_stream *stream)
{
struct lia_client *client = (struct lia_client *)userdata;
+ lia_vcr_start(&client->vcr, client->loop);
stream->packet_sent_callback = packet_sent_callback;
struct aki_packet *packet = aki_packet_create();
aki_packet_write_u16(packet, client->id);
@@ -173,8 +193,6 @@ void lia_client_connect(struct lia_client *client, struct aki_event_loop *loop,
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)) {
diff --git a/src/liana/handler.h b/src/liana/handler.h
index f994069..84aa1de 100644
--- a/src/liana/handler.h
+++ b/src/liana/handler.h
@@ -26,6 +26,7 @@ enum {
enum {
LIANA_CLIENT_CONFIGURE = 0,
LIANA_CLIENT_DATA,
+ LIANA_CLIENT_SUBTITLE,
LIANA_CLIENT_REMOVE_BUFFERS,
LIANA_CLIENT_RESUME_AT,
LIANA_CLIENT_EOF,
diff --git a/src/liana/handlers/cdio_server.c b/src/liana/handlers/cdio_server.c
index d271829..641ed92 100644
--- a/src/liana/handlers/cdio_server.c
+++ b/src/liana/handlers/cdio_server.c
@@ -32,6 +32,8 @@ static void cdio_server_write_info(struct lia_server_handler *handler, struct ak
aki_packet_write_u32(packet, 1);
aki_packet_write_u8(packet, CAMU_NORMAL);
aki_packet_write_u8(packet, CAMU_STREAM_AUDIO);
+ aki_packet_write_u64(packet, cdio->handler.get_duration(&cdio->handler));
+ aki_packet_write_s32(packet, 0);
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);
@@ -48,9 +50,9 @@ static u64 cdio_server_get_duration(struct lia_server_handler *handler)
struct lia_cdio_server *cdio = (struct lia_cdio_server *)handler;
struct cch_chapter *first = &al_array_at(cdio->handle->entry->chapters, 0);
struct cch_chapter *last = &al_array_last(cdio->handle->entry->chapters);
- f64 seconds = camu_audio_format_bytes_to_sec(&cdio->fmt, (last->end - first->start) * CDIO_CD_FRAMESIZE_RAW);
- al_log_info("cdio", "Length: %.2fs.", seconds);
- return (u64)(seconds * 1000000.0);
+ u64 length = camu_audio_format_bytes_to_usec(&cdio->fmt, (last->end - first->start) * CDIO_CD_FRAMESIZE_RAW);
+ al_log_info("cdio", "Length: %.2fs.", length / 1000000.0);
+ return length;
}
static bool cdio_server_seek(struct lia_server_handler *handler, u64 pos)
diff --git a/src/liana/handlers/codec_client.c b/src/liana/handlers/codec_client.c
index d420c59..7c57d03 100644
--- a/src/liana/handlers/codec_client.c
+++ b/src/liana/handlers/codec_client.c
@@ -21,13 +21,15 @@ static bool codec_client_init(struct lia_client_handler *handler, struct camu_re
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;
+ if (stream->type == CAMU_STREAM_AUDIO || stream->type == CAMU_STREAM_VIDEO) {
+ codec->dec = camu_ff_decoder_create();
+ //codec->dec = camu_stbi_decoder_create();
+ //codec->dec = camu_spng_decoder_create();
+ //codec->dec = camu_wuffs_decoder_create();
+ if (!codec->dec->init(codec->dec, renderer, stream, data_callback, codec)) {
+ return false;
+ }
}
return true;
}
@@ -38,8 +40,6 @@ 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
@@ -56,7 +56,7 @@ static bool codec_client_handle_packet(struct lia_client_handler *handler, struc
{
struct lia_codec_client *codec = (struct lia_codec_client *)handler;
if (!packet) {
- struct lia_codec_client *codec = (struct lia_codec_client *)handler;
+ if (!codec->dec) return true;
s32 ret = codec->dec->push(codec->dec, NULL);
// Flush returns success.
ret = codec->dec->process(codec->dec);
@@ -68,6 +68,7 @@ static bool codec_client_handle_packet(struct lia_client_handler *handler, struc
u8 type = aki_packet_read_u8(packet);
switch (type) {
case CAMU_NORMAL: {
+ if (!codec->dec) return true;
struct aki_buffer buffer;
aki_packet_read_buffer(packet, &buffer);
success = push_packet(codec, &buffer);
@@ -75,12 +76,20 @@ static bool codec_client_handle_packet(struct lia_client_handler *handler, struc
}
#ifdef CAMU_HAVE_FFMPEG
case CAMU_FFMPEG_COMPAT: {
- success = push_av_packet(codec, aki_packet_read_av_packet(packet));
+ AVPacket *pkt = aki_packet_read_av_packet(packet);
+ if (!codec->dec) {
+ codec->handler.callback(codec->handler.userdata, LIANA_CLIENT_SUBTITLE, codec->handler.stream, pkt);
+ return true;
+ } else {
+ success = push_av_packet(codec, pkt);
+ }
+ av_packet_unref(pkt);
+ av_packet_free(&pkt);
break;
}
#endif
}
- // Forcing in EOF on errors is not necessary but should be a better experience client-side.
+ // Forcing in EOF on an error 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;
@@ -96,13 +105,13 @@ static bool codec_client_handle_packet(struct lia_client_handler *handler, struc
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);
+ if (codec->dec) 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);
+ if (codec->dec) codec->dec->free(&codec->dec);
al_free(codec);
*handler = NULL;
}
diff --git a/src/liana/handlers/codec_server.c b/src/liana/handlers/codec_server.c
index 58da2e6..94b18df 100644
--- a/src/liana/handlers/codec_server.c
+++ b/src/liana/handlers/codec_server.c
@@ -42,9 +42,11 @@ static void codec_server_write_info(struct lia_server_handler *handler, struct a
struct camu_codec_stream *stream;
al_array_foreach_ptr(codec->demux->streams, i, stream) {
aki_packet_write_u8(packet, stream->mode);
+ aki_packet_write_u8(packet, stream->type);
+ aki_packet_write_u64(packet, stream->duration);
+ aki_packet_write_s32(packet, i);
switch (stream->mode) {
case CAMU_NORMAL: {
- aki_packet_write_u8(packet, stream->type);
struct camu_video_format *fmt = &stream->video.fmt;
aki_packet_write_s32(packet, fmt->width);
aki_packet_write_s32(packet, fmt->height);
diff --git a/src/liana/list.c b/src/liana/list.c
index 65efc9d..09720b6 100644
--- a/src/liana/list.c
+++ b/src/liana/list.c
@@ -151,7 +151,7 @@ static bool handle_add(struct lia_list *list, struct lia_list_entry *entry)
sink->set = list->current;
sink->callback(sink->userdata, LIANA_SINK_SET, entry, list->current, &time);
}
- // meta playing
+ al_log_info("list", "Now playing: %ls.\n", AL_WSTR_PRINTF(&entry->name));
} else {
/*
if (list->queued == -1) {
@@ -309,7 +309,7 @@ static bool handle_skipto(struct lia_list *list, s32 sequence, s32 index)
sink->callback(sink->userdata, LIANA_SINK_SET, target, index, &time);
}
- // meta playing
+ al_log_info("list", "Now playing: %ls.\n", AL_WSTR_PRINTF(&target->name));
return true;
}
@@ -403,7 +403,7 @@ static void handle_end(struct lia_list *list, s32 sequence)
al_array_foreach(list->sinks, i, sink) {
sink->queued = -1;
}
- // meta playing
+ al_log_info("list", "Now playing: %ls.\n", AL_WSTR_PRINTF(&current->name));
} else if (next < size) {
struct lia_list_cmd *cmd = al_alloc_object(struct lia_list_cmd);
cmd->op = SKIPTO;
diff --git a/src/liana/vcr.c b/src/liana/vcr.c
index 213d5cb..c9e7975 100644
--- a/src/liana/vcr.c
+++ b/src/liana/vcr.c
@@ -3,9 +3,18 @@
#include "vcr.h"
#include "handler.h"
-#define VCR_BUFFER_INIT 256
-#define VCR_BUFFER_BUFFERED (VCR_BUFFER_INIT - 16)
-#define VCR_BUFFER_LOW (VCR_BUFFER_INIT - 64)
+#define VCR_BUFFER_BUFFERED MB(24)
+
+enum {
+ VCR_EXPAND_UNTOUCHED = 0,
+ VCR_EXPAND_GROWN,
+ VCR_EXPAND_COMPLETE
+};
+
+// Only track the buffered state of audio and video streams as we don't
+// expect any other type of stream to ever call cork().
+#define TRACK_IGNORE_BUFFERED(track) \
+ (!(track->stream.type == CAMU_STREAM_AUDIO || track->stream.type == CAMU_STREAM_VIDEO))
static void signal_callback(void *userdata)
{
@@ -13,14 +22,22 @@ static void signal_callback(void *userdata)
aki_packet_stream_cork(vcr->data, false);
}
+static void reset_metrics(struct lia_vcr *vcr)
+{
+ vcr->metric.current_frame = 0;
+ vcr->metric.last_report_ts = 0;
+}
+
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;
+ al_atomic_store(u64)(&vcr->count, 0, AL_ATOMIC_RELAXED);
vcr->mark.buffered = VCR_BUFFER_BUFFERED;
+ vcr->mark.low = 0;
+ vcr->expand = VCR_EXPAND_UNTOUCHED;
vcr->data = data;
aki_signal_init(&vcr->signal, signal_callback, vcr);
+ reset_metrics(vcr);
}
void lia_vcr_start(struct lia_vcr *vcr, struct aki_event_loop *loop)
@@ -45,9 +62,10 @@ static aki_thread_result AKI_THREADCALL vcr_track_thread(void *userdata)
// We were signaled to close, exit thread.
goto out;
}
+ u32 size = aki_packet_get_size(packet);
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) {
+ u64 buffer = al_atomic_sub(u64)(&vcr->count, size, AL_ATOMIC_RELAXED);
+ if (buffered && buffer <= vcr->mark.low) {
aki_signal_send(&vcr->signal);
}
}
@@ -90,12 +108,17 @@ 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);
+ al_atomic_store(u8)(&track->buffered, TRACK_IGNORE_BUFFERED(track), AL_ATOMIC_RELAXED);
+ aki_packet_cache_init(&track->cache, 256);
al_array_push(vcr->tracks, track);
al_atomic_store(s32)(&track->state, LIANA_STREAM_RUNNING, AL_ATOMIC_RELAXED);
}
+bool lia_vcr_is_empty(struct lia_vcr *vcr)
+{
+ return vcr->tracks.size == 0;
+}
+
static struct lia_vcr_track *get_track_from_index(struct lia_vcr *vcr, s32 index)
{
struct lia_vcr_track *track;
@@ -105,7 +128,7 @@ static struct lia_vcr_track *get_track_from_index(struct lia_vcr *vcr, s32 index
return NULL;
}
-static void cork_if_buffered(struct lia_vcr *vcr)
+static void cork_if_buffered(struct lia_vcr *vcr, u64 buffer)
{
u8 buffered = 1;
struct lia_vcr_track *track;
@@ -113,6 +136,15 @@ static void cork_if_buffered(struct lia_vcr *vcr)
buffered &= al_atomic_load(u8)(&track->buffered, AL_ATOMIC_RELAXED);
}
if (buffered) {
+ if (vcr->expand == VCR_EXPAND_UNTOUCHED) {
+ vcr->mark.buffered = buffer * 2;
+ vcr->expand = VCR_EXPAND_GROWN;
+ al_log_info("vcr", "Expanded buffer to size %.2fMB.", vcr->mark.buffered / (f32)MB(1));
+ return;
+ } else if (vcr->expand == VCR_EXPAND_GROWN) {
+ vcr->mark.low = vcr->mark.buffered - MB(2);
+ vcr->expand = VCR_EXPAND_COMPLETE;
+ }
aki_packet_stream_cork(vcr->data, true);
al_array_foreach(vcr->tracks, i, track) {
aki_packet_cache_flush(&track->cache);
@@ -120,7 +152,24 @@ static void cork_if_buffered(struct lia_vcr *vcr)
}
}
-bool lia_vcr_push_packet(struct lia_vcr *vcr, struct aki_packet *packet)
+static void update_metrics(struct lia_vcr *vcr, u32 size)
+{
+ vcr->metric.current_frame += size;
+ u64 now = aki_get_timestamp();
+ if (!vcr->metric.last_report_ts) {
+ vcr->metric.last_report_ts = now;
+ return;
+ }
+ u64 diff;
+ if ((diff = now - vcr->metric.last_report_ts) > 1000000Lu) {
+ f32 kbps = (vcr->metric.current_frame / 125.f) / (diff / 1000000.f);
+ al_log_info("vcr", "Receiving packets at %.2fkbps.", kbps);
+ vcr->metric.current_frame = 0;
+ vcr->metric.last_report_ts = now;
+ }
+}
+
+void lia_vcr_push_packet(struct lia_vcr *vcr, struct aki_packet *packet)
{
struct lia_vcr_track *track;
u8 op = aki_packet_read_u8(packet);
@@ -130,34 +179,37 @@ bool lia_vcr_push_packet(struct lia_vcr *vcr, struct aki_packet *packet)
if (!track) {
al_log_warn("liana", "Received data from errored or unknown track.");
aki_packet_free(packet);
- return false;
+ return;
}
if (!track->running) {
aki_thread_create(&track->thread, vcr_track_thread, track);
track->running = true;
}
- s32 count;
+ u32 size = aki_packet_get_size(packet);
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;
- cork_if_buffered(vcr);
+ return;
+ }
+ u64 buffer;
+ if ((buffer = al_atomic_add(u64)(&vcr->count, size, AL_ATOMIC_RELAXED)) >= vcr->mark.buffered) {
+ cork_if_buffered(vcr, buffer);
}
+ update_metrics(vcr, size);
break;
case LIANA_PACKET_EOF:
al_array_foreach(vcr->tracks, i, track) {
aki_packet_cache_send_packet(&track->cache, NULL);
}
aki_packet_free(packet);
+ aki_signal_stop(&vcr->signal);
break;
case LIANA_PACKET_ERROR:
al_log_warn("liana", "Unhandled error packet.");
aki_packet_free(packet);
break;
default:
- al_assert_and_return(false);
+ al_assert(false);
}
- return true;
}
void lia_vcr_cork(struct lia_vcr_track *track)
@@ -206,11 +258,15 @@ void lia_vcr_flush(struct lia_vcr *vcr)
vcr_track_close_internal(track);
track->client->flush(track->client);
aki_packet_cache_enable(&track->cache);
- al_atomic_store(u8)(&track->buffered, 0, AL_ATOMIC_RELAXED);
+ al_atomic_store(u8)(&track->buffered, TRACK_IGNORE_BUFFERED(track), AL_ATOMIC_RELAXED);
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);
+ aki_signal_stop(&vcr->signal);
+ al_atomic_store(u64)(&vcr->count, 0, AL_ATOMIC_RELAXED);
+ if (vcr->expand == VCR_EXPAND_COMPLETE) {
+ vcr->mark.low = 0;
+ vcr->expand = VCR_EXPAND_GROWN;
+ }
}
void lia_vcr_close_all(struct lia_vcr *vcr)
diff --git a/src/liana/vcr.h b/src/liana/vcr.h
index f471c4e..fc7fb32 100644
--- a/src/liana/vcr.h
+++ b/src/liana/vcr.h
@@ -29,16 +29,22 @@ struct lia_vcr_track {
struct lia_vcr {
array(struct lia_vcr_track *) tracks;
- atomic(s32) count;
- struct { s32 low, buffered; } mark;
+ atomic(u64) count;
+ struct { u64 buffered, low; } mark;
+ u8 expand;
struct aki_packet_stream *data;
struct aki_signal signal;
+ struct {
+ u64 current_frame;
+ u64 last_report_ts;
+ } metric;
};
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);
+bool lia_vcr_is_empty(struct lia_vcr *vcr);
+void 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);
diff --git a/src/libsink/common.h b/src/libsink/common.h
index 0bb3ded..708b1bb 100644
--- a/src/libsink/common.h
+++ b/src/libsink/common.h
@@ -1,6 +1,6 @@
#pragma once
-#define CAMU_SINK_LOCAL
+//#define CAMU_SINK_LOCAL
enum {
CAMU_SINK_SET = 0,
diff --git a/src/libsink/sink.c b/src/libsink/sink.c
index 538515a..2caa973 100644
--- a/src/libsink/sink.c
+++ b/src/libsink/sink.c
@@ -10,6 +10,8 @@
#include "sink.h"
#include "common.h"
+#include "../render/renderer_libplacebo.h"
+
enum {
SINK_EMPTY = 0,
SINK_PAUSED,
@@ -36,7 +38,7 @@ enum {
CLOSE
};
-#define ENTRY_MAX_AGE 7
+#define ENTRY_MAX_AGE 5
#define BUFFER_EMPTY(buf) ((buf)->state == BUFFER_INIT || (buf)->state == BUFFER_QUEUED)
@@ -47,6 +49,13 @@ enum {
#endif
#define AUDIO_READY_OR_EMPTY(entry) (BUFFER_EMPTY(&(entry)->audio) || (entry)->audio.state == BUFFER_ADDED)
+#ifdef CAMU_SINK_NO_VIDEO
+#define VIDEO_REMOVED_OR_EMPTY(entry) true
+#else
+#define VIDEO_REMOVED_OR_EMPTY(entry) (BUFFER_EMPTY(&(entry)->video) || (entry)->video.state != BUFFER_ADDED)
+#endif
+#define AUDIO_REMOVED_OR_EMPTY(entry) (BUFFER_EMPTY(&(entry)->audio) || (entry)->audio.state != BUFFER_ADDED)
+
#if defined CAMU_SCREEN_THREADED && defined CAMU_MIXER_THREADED
#define BLOCKING_SLEEP(delay) aki_thread_sleep(delay)
#else
@@ -379,6 +388,36 @@ void add_video_if_set_and_buffered(struct camu_sink_entry *entry)
}
#endif
+static void switch_to(struct camu_sink *sink, struct camu_sink_entry *entry)
+{
+ if (sink->current) {
+ if (camu_clock_is_ended(&entry->clock)) {
+ remove_entry_buffers(sink, sink->current);
+ } else {
+ maybe_add_to_previous(sink, sink->current, entry);
+ }
+ }
+ set_or_queue_entry(entry);
+ sink->current = entry;
+}
+
+static bool end_entry_and_advance_queue(struct camu_sink *sink, struct camu_sink_entry *entry)
+{
+ al_log_info("sink", "Entry ended.");
+ camu_clock_end(&entry->clock);
+ if (sink->target) {
+ switch_to(sink, sink->target);
+ sink->target = NULL;
+ }
+ if (sink->queued) {
+ set_or_queue_entry(sink->queued);
+ sink->current = sink->queued;
+ sink->queued = NULL;
+ return true;
+ }
+ return false;
+}
+
static void audio_buffer_callback(void *userdata, u8 op)
{
struct camu_sink_entry *entry = (struct camu_sink_entry *)userdata;
@@ -386,9 +425,7 @@ static void audio_buffer_callback(void *userdata, u8 op)
switch (op) {
case CAMU_BUFFER_BUFFERED:
aki_mutex_lock(&sink->mutex);
- if (!entry->ended) {
- add_audio_if_set_and_buffered(entry);
- }
+ add_audio_if_set_and_buffered(entry);
aki_mutex_unlock(&sink->mutex);
break;
case CAMU_BUFFER_CORK:
@@ -410,20 +447,23 @@ static void audio_buffer_callback(void *userdata, u8 op)
case CAMU_BUFFER_EOF: {
lia_vcr_cork(entry->audio.track);
aki_mutex_lock(&sink->mutex);
+ al_log_info("sink", "Audio EOF.");
remove_entry_audio_buffer(sink, entry);
- camu_clock_end(&entry->clock);
- // EOF on the audio buffer unconditionally
- // advances the queue.
- if (sink->queued) {
- set_or_queue_entry(sink->queued);
- sink->current = sink->queued;
- sink->queued = NULL;
+ bool run_queue = VIDEO_REMOVED_OR_EMPTY(entry);
+#ifndef CAMU_SINK_NO_VIDEO
+ bool single_frame = camu_video_buffer_is_single_frame(&entry->video.buf);
+ run_queue = run_queue || single_frame;
+#endif
+ if (run_queue) {
+ end_entry_and_advance_queue(sink, entry);
}
aki_mutex_unlock(&sink->mutex);
- queue_cmd(sink, (struct camu_sink_cmd){
- .op = END,
- .value.i = entry->sequence
- });
+ if (run_queue) {
+ queue_cmd(sink, (struct camu_sink_cmd){
+ .op = END,
+ .value.i = entry->sequence
+ });
+ }
break;
}
}
@@ -436,11 +476,8 @@ static void video_buffer_callback(void *userdata, u8 op)
struct camu_sink *sink = entry->sink;
switch (op) {
case CAMU_BUFFER_BUFFERED: {
- bool single_frame = camu_video_buffer_is_single_frame(&entry->video.buf);
aki_mutex_lock(&sink->mutex);
- if (!entry->ended || single_frame) {
- add_video_if_set_and_buffered(entry);
- }
+ add_video_if_set_and_buffered(entry);
aki_mutex_unlock(&sink->mutex);
break;
}
@@ -455,19 +492,13 @@ static void video_buffer_callback(void *userdata, u8 op)
bool single_frame = camu_video_buffer_is_single_frame(&entry->video.buf);
bool swapped = false;
aki_mutex_lock(&sink->mutex);
+ al_log_info("sink", "Video EOF.");
if (!single_frame) {
remove_entry_video_buffer(sink, entry);
}
- // If this entry has no audio, advance the queue.
- bool run_queue = !single_frame && BUFFER_EMPTY(&entry->audio);
+ bool run_queue = !single_frame && AUDIO_REMOVED_OR_EMPTY(entry);
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;
- }
+ swapped = end_entry_and_advance_queue(sink, entry);
}
aki_mutex_unlock(&sink->mutex);
if (!swapped) {
@@ -528,7 +559,7 @@ static void client_callback(void *userdata, u8 op, struct camu_codec_stream *str
switch (stream->type) {
case CAMU_STREAM_AUDIO:
entry->audio.track = (struct lia_vcr_track *)opaque;
- if (!camu_audio_buffer_configure(&entry->audio.buf, stream)) {
+ if (!camu_audio_buffer_configure(&entry->audio.buf, stream, sink->audio.mixer)) {
lia_client_disconnect(&entry->client);
}
aki_mutex_lock(&sink->mutex);
@@ -545,7 +576,7 @@ static void client_callback(void *userdata, u8 op, struct camu_codec_stream *str
#ifndef CAMU_SINK_NO_VIDEO
case CAMU_STREAM_VIDEO:
entry->video.track = (struct lia_vcr_track *)opaque;
- if (!camu_video_buffer_configure(&entry->video.buf, stream)) {
+ if (!camu_video_buffer_configure(&entry->video.buf, stream, sink->video.renderer)) {
lia_client_disconnect(&entry->client);
}
aki_mutex_lock(&sink->mutex);
@@ -559,6 +590,20 @@ static void client_callback(void *userdata, u8 op, struct camu_codec_stream *str
}
aki_mutex_unlock(&sink->mutex);
break;
+ case CAMU_STREAM_SUBTITLE:
+#ifdef CAMU_HAVE_FFMPEG
+ camu_video_buffer_configure_subtitles(&entry->video.buf, stream->av.stream->codecpar);
+#endif
+ break;
+ case CAMU_STREAM_ATTACHMENT: {
+#ifdef CAMU_HAVE_FFMPEG
+ struct camu_renderer_lp *lp = (struct camu_renderer_lp *)sink->video.renderer;
+ AVDictionaryEntry *entry = av_dict_get(stream->av.stream->metadata, "filename", NULL, 0);
+ AVCodecParameters *codecpar = stream->av.stream->codecpar;
+ ass_add_font(lp->ass, entry->value, (const char *)codecpar->extradata, codecpar->extradata_size);
+#endif
+ break;
+ }
#endif
}
break;
@@ -580,6 +625,18 @@ static void client_callback(void *userdata, u8 op, struct camu_codec_stream *str
}
break;
}
+ case LIANA_CLIENT_SUBTITLE: {
+ switch (stream->type) {
+ case CAMU_STREAM_SUBTITLE: {
+#ifdef CAMU_HAVE_FFMPEG
+ AVPacket *pkt = (AVPacket *)opaque;
+ camu_video_buffer_push_subtitle(&entry->video.buf, pkt);
+#endif
+ break;
+ }
+ }
+ break;
+ }
case LIANA_CLIENT_REMOVE_BUFFERS: {
aki_mutex_lock(&sink->mutex);
if (entry->audio.state == BUFFER_ADDED) {
@@ -588,7 +645,8 @@ static void client_callback(void *userdata, u8 op, struct camu_codec_stream *str
}
#ifndef CAMU_SINK_NO_VIDEO
bool reconnect = *(bool *)opaque;
- bool keep_video = reconnect && camu_video_buffer_is_single_frame(&entry->video.buf);
+ bool single_frame = camu_video_buffer_is_single_frame(&entry->video.buf);
+ bool keep_video = reconnect && single_frame;
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;
@@ -599,7 +657,7 @@ static void client_callback(void *userdata, u8 op, struct camu_codec_stream *str
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);
+ if (reconnect && !single_frame) camu_video_buffer_reset(&entry->video.buf);
#else
while (entry_buffers_held(entry)) {
BLOCKING_SLEEP(AKI_TS_FROM_USEC(2000));
@@ -634,8 +692,21 @@ static void client_callback(void *userdata, u8 op, struct camu_codec_stream *str
}
case LIANA_CLIENT_CLOSED: {
aki_mutex_lock(&sink->mutex);
- if (entry == sink->current) sink->current = NULL;
- if (entry == sink->queued) sink->queued = NULL;
+ if (entry == sink->target) {
+ if (sink->current) {
+ remove_entry_buffers(sink, sink->current);
+ sink->current = NULL;
+ }
+ sink->target = NULL;
+ } else if (entry == sink->current) {
+ sink->current = NULL;
+ if (sink->target) {
+ switch_to(sink, sink->target);
+ sink->target = NULL;
+ }
+ } else if (entry == sink->queued) {
+ sink->queued = NULL;
+ }
lia_client_free(&entry->client);
camu_audio_buffer_free(&entry->audio.buf);
#ifndef CAMU_SINK_NO_VIDEO
@@ -647,13 +718,13 @@ static void client_callback(void *userdata, u8 op, struct camu_codec_stream *str
if (rentry == entry) {
al_array_remove_at(sink->entries, i);
removed = true;
- al_log_debug("sink", "Entry closed by disconnect.");
+ al_log_info("sink", "Entry closed by disconnect.");
break;
}
}
al_free(entry);
if (!removed) {
- al_log_debug("sink", "Entry closed by cleanup.");
+ al_log_info("sink", "Entry closed by cleanup.");
}
aki_mutex_unlock(&sink->mutex);
break;
@@ -743,20 +814,6 @@ static void maybe_cleanup_old_entries(struct camu_sink *sink)
}
}
-static void switch_to(struct camu_sink *sink, struct camu_sink_entry *entry)
-{
- if (entry->ended || camu_clock_is_ended(&entry->clock)) {
- if (sink->current) {
- remove_entry_buffers(sink, sink->current);
- }
- } else {
- if (sink->current) {
- maybe_add_to_previous(sink, sink->current, entry);
- }
- }
- set_or_queue_entry(entry);
- sink->current = entry;
-}
static void clock_callback(void *userdata, u8 op)
{
@@ -790,13 +847,13 @@ static struct camu_sink_entry *get_entry_from_id(struct camu_sink *sink, u16 id,
entry->audio.state = BUFFER_INIT;
entry->audio.armed = false;
- camu_audio_buffer_init(&entry->audio.buf, &entry->clock, sink->audio.mixer);
+ camu_audio_buffer_init(&entry->audio.buf, &entry->clock);
entry->audio.buf.callback = audio_buffer_callback;
entry->audio.buf.userdata = entry;
#ifndef CAMU_SINK_NO_VIDEO
entry->video.state = BUFFER_INIT;
- camu_video_buffer_init(&entry->video.buf, &entry->clock, sink->video.renderer);
+ camu_video_buffer_init(&entry->video.buf, &entry->clock);
entry->video.buf.callback = video_buffer_callback;
entry->video.buf.userdata = entry;
#endif
@@ -842,6 +899,7 @@ static bool set_command_callback(void *userdata, struct aki_rpc_connection *conn
u64 seek_pos = aki_packet_read_u64(packet);
u8 pause = aki_packet_read_u8(packet);
bool ended = aki_packet_read_bool(packet);
+ (void)ended;
bool created;
struct camu_sink_entry *entry = get_entry_from_id(sink, node_id, &created);
@@ -851,9 +909,6 @@ static bool set_command_callback(void *userdata, struct aki_rpc_connection *conn
if (created) {
camu_clock_set(&entry->clock, seek_pos / 1000000.0);
- if (ended) {
- camu_clock_end(&entry->clock);
- }
struct camu_renderer *renderer = NULL;
#ifndef CAMU_SINK_NO_VIDEO
renderer = sink->video.renderer;
@@ -861,8 +916,6 @@ static bool set_command_callback(void *userdata, struct aki_rpc_connection *conn
lia_client_connect(&entry->client, sink->loop, sink->type, &addr, port, node_id, seek_pos, renderer);
}
- entry->ended = ended;
-
if (op == LIANA_SINK_BUFFER) {
goto out;
} else if (op == LIANA_SINK_BUFFER_AND_QUEUE) {
@@ -890,27 +943,25 @@ static bool set_command_callback(void *userdata, struct aki_rpc_connection *conn
al_log_warn("sink", "Ignoring target on NONE.");
sink->target = NULL;
}
- if (sink->current) {
- maybe_add_to_previous(sink, sink->current, entry);
+ if (entry == sink->current) {
+ break;
}
- set_or_queue_entry(entry);
- sink->current = entry;
+ switch_to(sink, entry);
break;
case LIANA_PAUSE_RESUME:
if (sink->target) {
al_log_warn("sink", "Ignoring target on RESUME.");
sink->target = NULL;
}
+ camu_clock_resume(&entry->clock, at);
if (entry == sink->current) {
camu_audio_buffer_unpause(&entry->audio.buf);
- } else if (sink->current) {
- maybe_add_to_previous(sink, sink->current, entry);
+ break;
}
- camu_clock_resume(&entry->clock, at);
- set_or_queue_entry(entry);
- sink->current = entry;
+ switch_to(sink, entry);
break;
case LIANA_PAUSE_PAUSE: {
+ // For target to be set that must mean that current is set and armed to pause.
struct camu_sink_entry *prev_target = sink->target;
if (prev_target) {
if (prev_target == entry) {
@@ -919,9 +970,8 @@ static bool set_command_callback(void *userdata, struct aki_rpc_connection *conn
sink->target = entry;
}
camu_clock_pause(&prev_target->clock, at);
- } else if (sink->current) {
- if (camu_clock_is_ended(&sink->current->clock)) {
- // Server thought we weren't done, be we are.
+ } else {
+ if (!sink->current || camu_clock_is_ended(&sink->current->clock)) {
switch_to(sink, entry);
} else {
sink->target = entry;
@@ -941,8 +991,8 @@ static bool set_command_callback(void *userdata, struct aki_rpc_connection *conn
camu_audio_buffer_unpause(&entry->audio.buf);
}
camu_clock_pause(&prev_target->clock, at);
- } else if (sink->current) {
- if (camu_clock_is_ended(&sink->current->clock)) {
+ } else {
+ if (!sink->current || camu_clock_is_ended(&sink->current->clock)) {
switch_to(sink, entry);
} else {
sink->target = entry;
@@ -1024,7 +1074,6 @@ static bool seek_command_callback(void *userdata, struct aki_rpc_connection *con
if (!current) goto out;
if (current->sequence == sequence) {
- current->ended = false;
#ifdef CAMU_SINK_LOCAL
(void)at;
lia_client_seek(&current->client, pos, 0);
diff --git a/src/libsink/sink.h b/src/libsink/sink.h
index 5feadf5..18ef134 100644
--- a/src/libsink/sink.h
+++ b/src/libsink/sink.h
@@ -42,7 +42,6 @@ struct camu_sink_entry {
u16 lru;
struct camu_clock clock;
struct lia_client client;
- bool ended;
struct {
u8 state;
bool armed;
diff --git a/src/mixer/audio_miniaudio.c b/src/mixer/audio_miniaudio.c
index 4cb67de..beda5f1 100644
--- a/src/mixer/audio_miniaudio.c
+++ b/src/mixer/audio_miniaudio.c
@@ -75,10 +75,16 @@ static bool audio_miniaudio_init(struct camu_audio *audio, str *name)
ma_log_register_callback(&ma->log, ma_log_callback_init(miniaudio_log_callback, NULL));
ma->context_config = ma_context_config_init();
ma->context_config.pLog = &ma->log;
+#ifdef MA_ENABLE_ALSA
ma->context_config.alsa.useVerboseDeviceEnumeration = MA_TRUE;
+#endif
+#ifdef MA_ENABLE_PULSEAUDIO
char *c_str = al_str_to_c_str(name);
ma->context_config.pulse.pApplicationName = c_str;
ma->context_config.pulse.tryAutoSpawn = MA_TRUE;
+#else
+ (void)name;
+#endif
s32 ret = ma_context_init(backends, AL_ARRAY_SIZE(backends), &ma->context_config, &ma->context);
if (ret != MA_SUCCESS) {
al_log_error("audio_miniaudio", "Failed to initialize context (%d).", ret);
@@ -171,8 +177,15 @@ static bool audio_miniaudio_configure_stream(struct camu_audio *audio, void *opa
ma->config.noPreSilencedOutputBuffer = MA_TRUE;
ma->config.noFixedSizedCallback = MA_TRUE;
ma->config.noClip = MA_TRUE;
+#ifdef MA_ENABLE_WASAPI
ma->config.wasapi.noAutoConvertSRC = MA_FALSE;
ma->config.wasapi.noDefaultQualitySRC = MA_FALSE;
+#endif
+#ifdef MA_ENABLE_AAUDIO
+ ma->config.aaudio.usage = ma_aaudio_usage_media;
+ ma->config.aaudio.contentType = ma_aaudio_content_type_music;
+ ma->config.aaudio.allowedCapturePolicy = ma_aaudio_allow_capture_by_all;
+#endif
s32 ret = ma_device_init(&ma->context, &ma->config, &ma->device);
if (ret != MA_SUCCESS) {
al_log_error("audio_miniaudio", "Failed to initialize device (%d).", ret);
@@ -214,7 +227,9 @@ static void audio_miniaudio_free(struct camu_audio **audio)
struct camu_audio_miniaudio *ma = (struct camu_audio_miniaudio *)*audio;
ma_device_uninit(&ma->device);
ma_context_uninit(&ma->context);
+#ifdef MA_ENABLE_PULSEAUDIO
al_free((void *)ma->context_config.pulse.pApplicationName);
+#endif
al_free(ma);
*audio = NULL;
}
diff --git a/src/mixer/audio_null.c b/src/mixer/audio_null.c
index ac2846a..a6eeea3 100644
--- a/src/mixer/audio_null.c
+++ b/src/mixer/audio_null.c
@@ -14,7 +14,9 @@ static void audio_null_pick_format(struct camu_audio *audio, struct camu_resampl
(void)na;
fmt->req.format = fmt->in.format;
fmt->req.channel_count = fmt->in.channel_count;
+#ifdef CAMU_HAVE_FFMPEG
av_channel_layout_default(&fmt->req.channel_layout, fmt->in.channel_count);
+#endif
fmt->req.sample_rate = fmt->in.sample_rate;
}
diff --git a/src/render/meson.build b/src/render/meson.build
index 74d323e..c2bcfd7 100644
--- a/src/render/meson.build
+++ b/src/render/meson.build
@@ -59,4 +59,11 @@ elif get_option('renderer') == 'libplacebo'
render_args += ['-DCAMU_RENDERER_LIBPLACEBO']
endif
+libass_opts = []
+if not is_windows
+ libass_opts += ['fontconfig=enabled']
+endif
+libass = dependency('libass', default_options: libass_opts)
+render_deps += [libass]
+
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 bd5ed7b..a95afc4 100644
--- a/src/render/queue_libplacebo.c
+++ b/src/render/queue_libplacebo.c
@@ -101,12 +101,84 @@ static void queue_lp_push(struct camu_frame_queue *queue, struct camu_codec_fram
}
#ifdef CAMU_HAVE_FFMPEG
+static struct camu_overlay_lp *create_subtitle_overlay(pl_gpu gpu, ASS_Image *ass_frame)
+{
+ struct camu_overlay_lp *overlay = al_alloc_object(struct camu_overlay_lp);
+ for (; ass_frame; ass_frame = ass_frame->next) {
+ pl_tex tex = pl_tex_create(gpu, pl_tex_params(
+ .w = ass_frame->w,
+ .h = ass_frame->h,
+ .format = pl_find_named_fmt(gpu, "r8"),
+ .sampleable = true,
+ .host_writable = true
+ ));
+
+ bool ok = pl_tex_upload(gpu, pl_tex_transfer_params(
+ .tex = tex,
+ .row_pitch = ass_frame->stride,
+ .ptr = ass_frame->bitmap
+ ));
+ if (!ok) {
+ al_log_error("frame_queue_libplacebo", "Failed to upload subtitle bitmap.");
+ continue;
+ }
+
+ struct pl_overlay *current = NULL;
+ if (overlay->num >= overlay->alloc) {
+ s32 old_alloc = overlay->alloc;
+ overlay->alloc = old_alloc ? al_next_power_of_two(overlay->alloc + 1) : 8;
+ if (!overlay->overlays) {
+ overlay->overlays = al_malloc(sizeof(struct pl_overlay) * overlay->alloc);
+ } else {
+ overlay->overlays = al_realloc(overlay->overlays, sizeof(struct pl_overlay) * overlay->alloc);
+ }
+ for (s32 i = old_alloc; i < overlay->alloc; i++) {
+ current = (struct pl_overlay *)&overlay->overlays[i];
+ current->parts = al_malloc(sizeof(struct pl_overlay_part) * 1);
+ current->num_parts = 0;
+ }
+ }
+
+ // https://github.com/mpv-player/mpv/blob/70aaba71d6e3071a732069a1d222d1eb4293faf2/video/out/vo_gpu_next.c#L289
+ // https://github.com/mpv-player/mpv/blob/70aaba71d6e3071a732069a1d222d1eb4293faf2/sub/ass_mp.c#L195
+ current = (struct pl_overlay *)&overlay->overlays[overlay->num++];
+ struct pl_overlay_part *part = (struct pl_overlay_part *)&current->parts[0];
+ part->src = (pl_rect2df){ 0, 0, ass_frame->w, ass_frame->h };
+ part->dst = (pl_rect2df){
+ ass_frame->dst_x,
+ ass_frame->dst_y,
+ ass_frame->dst_x + ass_frame->w,
+ ass_frame->dst_y + ass_frame->h
+ };
+ u32 c = ass_frame->color;
+ part->color[0] = (c >> 24) / 255.0;
+ part->color[1] = ((c >> 16) & 0xFF) / 255.0;
+ part->color[2] = ((c >> 8) & 0xFF) / 255.0;
+ part->color[3] = 1.0 - (c & 0xFF) / 255.0;
+
+ current->tex = tex;
+ //pl_color_space_from_avframe(&current->color, frame);
+ current->color = *pl_color_space(
+ .primaries = PL_COLOR_PRIM_BT_709,
+ .transfer = PL_COLOR_TRC_SRGB
+ );
+ current->mode = PL_OVERLAY_MONOCHROME;
+ current->repr = pl_color_repr_unknown;
+ //current->repr.sys = pl_system_from_av(frame->colorspace);
+ //current->repr.levels = pl_levels_from_av(frame->color_range);
+ current->repr.alpha = PL_ALPHA_INDEPENDENT;
+ current->coords = PL_OVERLAY_COORDS_SRC_FRAME;
+ current->num_parts = 1;
+ }
+ return overlay;
+}
+
static bool map_av_frame(pl_gpu gpu, pl_tex *tex, const struct pl_source_frame *src,
struct pl_frame *out_frame)
{
AVFrame *frame = src->frame_data;
- struct camu_video_buffer *buf = frame->opaque;
- AVStream *stream = buf->stream->av.stream;
+ struct camu_frame_queue_lp *lq = (struct camu_frame_queue_lp *)frame->opaque;
+ AVStream *stream = lq->q.buf->stream->av.stream;
bool ok = pl_map_avframe_ex(gpu, out_frame, pl_avframe_params(
.frame = frame,
@@ -114,6 +186,27 @@ static bool map_av_frame(pl_gpu gpu, pl_tex *tex, const struct pl_source_frame *
.map_dovi = false
));
+ s64 now = av_rescale_q(frame->best_effort_timestamp, stream->time_base, (AVRational){ 1, 1000 });
+ s32 change;
+ ASS_Image *ass_frame = ass_render_frame(lq->ass_renderer, lq->ass_track, now, &change);
+ if (ass_frame) {
+ struct camu_overlay_lp *overlay = NULL;
+ if (lq->overlays.size > 0) {
+ overlay = al_array_last(lq->overlays);
+ }
+ if (!overlay || change > 0) {
+ overlay = create_subtitle_overlay(gpu, ass_frame);
+ overlay->lq = lq;
+ al_array_push(lq->overlays, overlay);
+ }
+ overlay->ref++;
+ out_frame->overlays = overlay->overlays;
+ out_frame->num_overlays = overlay->num;
+ ((struct pl_source_frame *)src)->frame_data = overlay;
+ } else {
+ ((struct pl_source_frame *)src)->frame_data = NULL;
+ }
+
av_frame_free(&frame);
if (!ok) {
@@ -128,7 +221,27 @@ static bool map_av_frame(pl_gpu gpu, pl_tex *tex, const struct pl_source_frame *
static void unmap_av_frame(pl_gpu gpu, struct pl_frame *frame, const struct pl_source_frame *src)
{
- (void)src;
+ if (src->frame_data) {
+ struct camu_overlay_lp *overlay = (struct camu_overlay_lp *)src->frame_data;
+ struct camu_frame_queue_lp *lq = overlay->lq;
+ if (!--overlay->ref) {
+ for (s32 i = 0; i < overlay->alloc; i++) {
+ struct pl_overlay *current = &overlay->overlays[i];
+ if (current->num_parts > 0) {
+ pl_tex_destroy(gpu, &current->tex);
+ }
+ al_free((struct pl_overlay_part *)current->parts);
+ }
+ struct camu_overlay_lp *roverlay;
+ al_array_foreach(lq->overlays, i, roverlay) {
+ if (roverlay == overlay) {
+ al_array_remove_at(lq->overlays, i);
+ break;
+ }
+ }
+ al_free(overlay);
+ }
+ }
pl_unmap_avframe(gpu, frame);
}
@@ -142,10 +255,10 @@ static void discard_av_frame(const struct pl_source_frame *src)
static void queue_lp_push_av_frame(struct camu_frame_queue *queue, AVFrame *frame, f64 pts)
{
struct camu_frame_queue_lp *lq = (struct camu_frame_queue_lp *)queue;
- struct camu_video_buffer *buf = frame->opaque;
+ frame->opaque = lq;
pl_queue_push(lq->queue, &(struct pl_source_frame){
.pts = pts,
- .duration = buf->avg_frame_duration,
+ .duration = lq->q.buf->avg_frame_duration,
.map = map_av_frame,
.unmap = unmap_av_frame,
.discard = discard_av_frame,
@@ -153,6 +266,22 @@ static void queue_lp_push_av_frame(struct camu_frame_queue *queue, AVFrame *fram
.first_field = pl_field_from_avframe(frame)
});
}
+
+static void queue_lp_configure_subtitiles(struct camu_frame_queue *queue, s32 width, s32 height, AVCodecParameters *codecpar)
+{
+ struct camu_frame_queue_lp *lq = (struct camu_frame_queue_lp *)queue;
+ ass_set_frame_size(lq->ass_renderer, width, height);
+ ass_set_storage_size(lq->ass_renderer, width, height);
+ ass_set_fonts(lq->ass_renderer, NULL, NULL, ASS_FONTPROVIDER_AUTODETECT, NULL, 0);
+ ass_process_codec_private(lq->ass_track, (const char *)codecpar->extradata,
+ codecpar->extradata_size);
+}
+
+static void queue_lp_push_subtitle(struct camu_frame_queue *queue, AVPacket *pkt)
+{
+ struct camu_frame_queue_lp *lq = (struct camu_frame_queue_lp *)queue;
+ ass_process_chunk(lq->ass_track, (const char *)pkt->data, pkt->size, pkt->pts, pkt->duration);
+}
#endif
static void queue_lp_flush(struct camu_frame_queue *queue)
@@ -170,10 +299,11 @@ static s32 queue_lp_count(struct camu_frame_queue *queue)
static u8 queue_lp_read(struct camu_frame_queue *queue, f64 pts, void *out)
{
struct camu_frame_queue_lp *lq = (struct camu_frame_queue_lp *)queue;
+ struct pl_frame_mix *mix = (struct pl_frame_mix *)out;
lq->params.pts = pts;
- switch (pl_queue_update(lq->queue, (struct pl_frame_mix *)out, &lq->params)) {
+ switch (pl_queue_update(lq->queue, mix, &lq->params)) {
case PL_QUEUE_OK:
- return CAMU_QUEUE_OK;
+ break;
case PL_QUEUE_MORE:
return CAMU_QUEUE_MORE;
case PL_QUEUE_EOF:
@@ -182,6 +312,7 @@ static u8 queue_lp_read(struct camu_frame_queue *queue, f64 pts, void *out)
default:
return CAMU_QUEUE_ERR;
}
+ return CAMU_QUEUE_OK;
}
static void queue_lp_reset(struct camu_frame_queue *queue)
@@ -194,6 +325,8 @@ static void queue_lp_reset(struct camu_frame_queue *queue)
static void queue_lp_free(struct camu_frame_queue **queue)
{
struct camu_frame_queue_lp *lq = (struct camu_frame_queue_lp *)*queue;
+ ass_free_track(lq->ass_track);
+ ass_renderer_done(lq->ass_renderer);
pl_queue_destroy(&lq->queue);
al_free(lq);
*queue = NULL;
@@ -202,9 +335,12 @@ static void queue_lp_free(struct camu_frame_queue **queue)
struct camu_frame_queue *camu_frame_queue_lp_create(void)
{
struct camu_frame_queue_lp *lq = al_alloc_object(struct camu_frame_queue_lp);
+ al_array_init(lq->overlays);
lq->q.push = queue_lp_push;
#ifdef CAMU_HAVE_FFMPEG
lq->q.push_av_frame = queue_lp_push_av_frame;
+ lq->q.configure_subtitles = queue_lp_configure_subtitiles;
+ lq->q.push_subtitle = queue_lp_push_subtitle;
#endif
lq->q.flush = queue_lp_flush;
lq->q.count = queue_lp_count;
diff --git a/src/render/queue_libplacebo.h b/src/render/queue_libplacebo.h
index 829ef87..3b403e5 100644
--- a/src/render/queue_libplacebo.h
+++ b/src/render/queue_libplacebo.h
@@ -4,11 +4,23 @@
#include <libplacebo/utils/frame_queue.h>
#include <libplacebo/utils/upload.h>
+#include <libass/ass.h>
+
+struct camu_overlay_lp {
+ struct pl_overlay *overlays;
+ s32 alloc;
+ s32 num;
+ s16 ref;
+ struct camu_frame_queue_lp *lq;
+};
struct camu_frame_queue_lp {
struct camu_frame_queue q;
pl_queue queue;
struct pl_queue_params params;
+ ASS_Renderer *ass_renderer;
+ ASS_Track *ass_track;
+ array(struct camu_overlay_lp *) overlays;
};
struct camu_frame_queue *camu_frame_queue_lp_create(void);
diff --git a/src/render/renderer.h b/src/render/renderer.h
index dcd74c1..b304fdf 100644
--- a/src/render/renderer.h
+++ b/src/render/renderer.h
@@ -12,8 +12,8 @@
#define CAMU_RENDERER_VULKAN
#endif
-struct camu_screen;
struct camu_frame_queue;
+struct camu_screen;
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 ae341f3..480da0b 100644
--- a/src/render/renderer_libplacebo.c
+++ b/src/render/renderer_libplacebo.c
@@ -161,6 +161,8 @@ static bool renderer_lp_create_renderer(struct camu_renderer *renderer, s32 *wid
lr->r.opaque = &lr->gpu;
#endif
+ lr->ass = ass_library_init();
+
return true;
err:
lr->r.free((struct camu_renderer **)&lr);
@@ -177,6 +179,8 @@ static struct camu_frame_queue *renderer_lp_create_queue(struct camu_renderer *r
lq->params.interpolation_threshold = 0.01f;
lq->params.radius = pl_frame_mix_radius(&lr->params);
lq->params.timeout = 0uL; // vsync
+ lq->ass_renderer = ass_renderer_init(lr->ass);
+ lq->ass_track = ass_new_track(lr->ass);
return queue;
}
@@ -208,19 +212,22 @@ static void renderer_lp_render(struct camu_renderer *renderer, struct camu_scree
while (scr->videos.size > 0) {
bool any_eof = false;
al_array_foreach_ptr(scr->videos, i, video) {
- if (camu_video_buffer_read(video->buf, &mix) && mix.frames) {
- target.crop = mix.frames[0]->crop;
- target.crop.x1 *= video->view.zoom / video->view.stretch;
- target.crop.y1 *= video->view.zoom * video->view.stretch;
- target.crop.x0 += video->view.x_offset;
- target.crop.y0 += video->view.y_offset;
- target.crop.x1 += video->view.x_offset;
- target.crop.y1 += video->view.y_offset;
- 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);
+ if (camu_video_buffer_read(video->buf, &mix)) {
+ // QUEUE_MORE doesn't return a frame obviously but don't treat it like an EOF.
+ if (mix.frames) {
+ target.crop = mix.frames[0]->crop;
+ target.crop.x1 *= video->view.zoom / video->view.stretch;
+ target.crop.y1 *= video->view.zoom * video->view.stretch;
+ target.crop.x0 += video->view.x_offset;
+ target.crop.y0 += video->view.y_offset;
+ target.crop.x1 += video->view.x_offset;
+ target.crop.y1 += video->view.y_offset;
+ 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);
+ }
} else {
any_eof = true;
}
@@ -250,6 +257,7 @@ void renderer_lp_free(struct camu_renderer **renderer)
if (lr->gl) pl_opengl_destroy(&lr->gl);
#endif
if (lr->logger) pl_log_destroy(&lr->logger);
+ if (lr->ass) ass_library_done(lr->ass);
al_free(lr);
*renderer = NULL;
}
diff --git a/src/render/renderer_libplacebo.h b/src/render/renderer_libplacebo.h
index 80f4688..5b206c6 100644
--- a/src/render/renderer_libplacebo.h
+++ b/src/render/renderer_libplacebo.h
@@ -4,6 +4,7 @@
#include <libplacebo/renderer.h>
#include <libplacebo/shaders/lut.h>
#include <libplacebo/colorspace.h>
+#include <libass/ass.h>
#include "renderer.h"
@@ -27,6 +28,8 @@ struct camu_renderer_lp {
pl_swapchain swapchain;
pl_renderer renderer;
struct pl_render_params params;
+ ASS_Library *ass;
+ ASS_Renderer *ass_renderer;
};
struct camu_renderer *camu_renderer_lp_create(void);
diff --git a/subprojects/fontconfig.wrap b/subprojects/fontconfig.wrap
new file mode 100644
index 0000000..e562096
--- /dev/null
+++ b/subprojects/fontconfig.wrap
@@ -0,0 +1,7 @@
+[wrap-git]
+url = https://gitlab.freedesktop.org/fontconfig/fontconfig.git
+revision = main
+depth = 1
+
+[provide]
+fontconfig = fontconfig_dep
diff --git a/subprojects/freetype2.wrap b/subprojects/freetype2.wrap
new file mode 100644
index 0000000..19ee383
--- /dev/null
+++ b/subprojects/freetype2.wrap
@@ -0,0 +1,4 @@
+[wrap-git]
+url = https://github.com/freetype/freetype.git
+revision = master
+depth = 1
diff --git a/subprojects/fribidi.wrap b/subprojects/fribidi.wrap
new file mode 100644
index 0000000..9633214
--- /dev/null
+++ b/subprojects/fribidi.wrap
@@ -0,0 +1,4 @@
+[wrap-git]
+url = https://github.com/fribidi/fribidi.git
+revision = master
+depth = 1
diff --git a/subprojects/harfbuzz.wrap b/subprojects/harfbuzz.wrap
new file mode 100644
index 0000000..e58aed4
--- /dev/null
+++ b/subprojects/harfbuzz.wrap
@@ -0,0 +1,4 @@
+[wrap-git]
+url = https://github.com/harfbuzz/harfbuzz.git
+revision = main
+depth = 1
diff --git a/subprojects/libass.wrap b/subprojects/libass.wrap
index a253fd8..df69923 100644
--- a/subprojects/libass.wrap
+++ b/subprojects/libass.wrap
@@ -3,4 +3,3 @@ url = https://github.com/libass/libass.git
revision = 0.17.3
clone-recursive = true
depth = 1
-patch_directory = libass
diff --git a/subprojects/packagefiles/libass/meson.build b/subprojects/packagefiles/libass/meson.build
deleted file mode 100644
index 915a942..0000000
--- a/subprojects/packagefiles/libass/meson.build
+++ /dev/null
@@ -1,21 +0,0 @@
-project('libass', 'c', version: '0.17.3', meson_version : '>=0.57.0')
-
-ext_proj = import('unstable-external_project')
-
-compiler = meson.get_compiler('c')
-
-run_command('./autogen.sh', check: true)
-
-proj = ext_proj.add_project('configure',
- configure_options: [
- '--prefix=@PREFIX@',
- '--libdir=@PREFIX@/@LIBDIR@',
- '--includedir=@PREFIX@/@INCLUDEDIR@',
- ], verbose: true)
-
-ass = proj.dependency('ass')
-
-libass = declare_dependency(dependencies: ass,
- include_directories: include_directories('.'))
-
-meson.override_dependency('libass', libass)
diff --git a/subprojects/packagefiles/notcurses/notcurses_tmux.diff b/subprojects/packagefiles/notcurses/notcurses_tmux.diff
new file mode 100644
index 0000000..49ca85b
--- /dev/null
+++ b/subprojects/packagefiles/notcurses/notcurses_tmux.diff
@@ -0,0 +1,12 @@
+diff --git a/src/lib/in.c b/src/lib/in.c
+index b6d4c79ef..cb128678c 100644
+--- a/src/lib/in.c
++++ b/src/lib/in.c
+@@ -1823,6 +1823,7 @@ build_cflow_automaton(inputctx* ictx){
+ { "[?7c", da1_cb, }, // CSI ? 7 c ("VT131")
+ { "[?1;0c", da1_cb, }, // CSI ? 1 ; 0 c ("VT101 with No Options")
+ { "[?1;2c", da1_cb, }, // CSI ? 1 ; 2 c ("VT100 with Advanced Video Option")
++ { "[?1;2;\\Dc", da1_attrs_cb, },
+ { "[?4;6c", da1_cb, }, // CSI ? 4 ; 6 c ("VT132 with Advanced Video and Graphics")
+ // CSI ? 1 2 ; Ps c ("VT125")
+ // CSI ? 6 0 ; Ps c (kmscon)