Compare commits

...
Author SHA1 Message Date
Codex dd97d63021 record: prime live MediaCodec AAC without publishing calibration
Gate source PCM until measured delay determines the silent input boundary. Drop complete calibration packets before output, retain silent AAC preroll and shift video/source ends to the same epoch. Preserve the existing MP4 calibration/edit path and stable descriptors across output backpressure.

Strict C89 Windows suite passes; calibrated fixtures cover six delays, empty PCM, output backpressure and early cancellation. Native MediaCodec and network session activation still require acceptance.
2026-11-12 19:17:26 +01:00
Codex d7446f1609 record: define calibrated AAC live-stream source epochs
Calculate packet-aligned source input and coded origins from measured encoder delay, retaining two silent AAC frames before source audio and rounding the matching video epoch upward. Keep file edit semantics separate.

Check all delays 0-4096 at six rates. Independently encode/decode synthetic AAC-LC at 44.1/48 kHz mono/stereo, recover delay from the chirp, omit calibration packets and compare source PCM against continuous decoding. This is not MediaCodec device acceptance or dispatcher activation.
2026-11-12 19:13:23 +01:00
Codex baa6ff33c2 record: bridge MediaCodec packet borrows to shared RTMP output
Add a bounded I/O-worker output adapter with cold mux setup, serialized packet admission, transport backpressure, normal drain and cancellation retirement. Keep the MediaCodec descriptor live until both shared mux and RTMP transport borrows retire.

The controller must establish live AAC priming and source epochs separately; this adapter does not enable Android streaming or reuse MP4 calibration edits. Strict C89 Windows contract suite passes with the actual shared FLV mux and mocked transport.
2026-11-12 19:08:49 +01:00
Codex 9e333a55eb record: build shared FLV configuration from MediaCodec setup data
Create worker-side AVC/HEVC/AV1 mux configuration from bounded native CSD and optional AAC setup. Join separate AVC parameter sets only during cold setup, retain neither native format nor encoder metadata, and preserve microsecond packet clocks without rebasing. Reuse shared Annex B and AV1 mux validation, including codec-specific packet budgets and depth/color admission.

Add actual shared-mux fixtures for configuration copying, separate/combined AVC setup, AAC payload borrowing, microsecond timestamps, codec depth mismatch and early packet-budget refusal. This does not enable the unfinished Android network publisher.
2026-11-12 19:03:57 +01:00
Codex 5aaea68b34 record: retain MediaCodec output across consumer backpressure
Add a copied worker-owned output callback table, with the existing file mux as the default adapter. Retain one immutable native packet descriptor per encoder while output is BUSY; calibrate AAC only once for that packet and release the codec borrow only after output acceptance. Worker free waits for bounded output retirement before encoder destruction is allowed.

Exercise no-repump/no-early-release behavior, callback-table ownership, delayed cancellation retirement and calibrated coded/source clocks under BUSY. The strict-C89 native MinGW MediaCodec suite passes. Android network publisher integration remains unfinished.
2026-11-12 19:03:57 +01:00
Codex 28579236c9 record: expose bounded borrowed MediaCodec video configuration
Allow the owning worker to inspect native csd-0/csd-1 buffers before constructing shared stream output. Bound each buffer to 65535 bytes, clear outputs on refusal and preserve encoder state for optional/malformed metadata. No dequeue, release, allocation or encoded-payload copy occurs. Audio and stopped encoders cannot expose video configuration.

Cover separate CSD borrows, missing/malformed/oversized metadata, invalid arguments, audio refusal and stopped-state invalidation. Streaming remains disabled pending output/publisher and lifecycle integration.
2026-11-12 19:03:57 +01:00
22 changed files with 1277 additions and 49 deletions

No files matched your search

+1 -1
View File
@@ -3412,7 +3412,7 @@ ifeq ($(HAVE_RECORD_MEDIACODEC), 1)
DEFINES += -DHAVE_RECORD_MEDIACODEC
MEDIA_RECORD_OBJ := record/record_mediacodec_encoder.o \
record/record_audio_tap.o \
record/record_mux_mediacodec.o record/record_mediacodec_worker.o \
record/record_mux_mediacodec.o record/record_mediacodec_worker.o record/record_aac_stream_timing.o \
record/record_aac_probe.o \
record/record_mp4_audio_edit.o record/record_mediacodec_file_edit.o \
record/record_mediacodec_surface.o record/record_mediacodec_file.o \
+1
View File
@@ -1723,6 +1723,7 @@ RECORDING
#include "../record/record_mp4_audio_edit.c"
#include "../record/record_mediacodec_encoder.c"
#include "../record/record_mux_mediacodec.c"
#include "../record/record_aac_stream_timing.c"
#include "../record/record_mediacodec_worker.c"
#include "../record/record_mediacodec_surface.c"
#include "../record/record_mediacodec_file.c"
+22
View File
@@ -0,0 +1,22 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#include "record_aac_stream_timing.h"
#include <string.h>
bool record_aac_stream_timing(unsigned rate, unsigned measured_delay,
record_aac_stream_timing_t *timing)
{
unsigned decoded_epoch;
if (!timing)
return false;
memset(timing, 0, sizeof(*timing));
if (!rate || rate > 192000 || measured_delay > 4096)
return false;
decoded_epoch = RECORD_AAC_PROBE_FRAMES + RECORD_AAC_STREAM_GUARD
+ measured_delay + RECORD_AAC_STREAM_PREROLL;
decoded_epoch = (decoded_epoch + 1023) / 1024 * 1024;
timing->input_epoch = decoded_epoch - measured_delay;
timing->coded_origin = decoded_epoch - RECORD_AAC_STREAM_PREROLL;
timing->audio_epoch = RECORD_AAC_STREAM_PREROLL;
timing->video_epoch_us = (unsigned)(((uint64_t)timing->audio_epoch
* 1000000 + rate - 1) / rate);
return true;
}
+22
View File
@@ -0,0 +1,22 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef RECORD_AAC_STREAM_TIMING_H
#define RECORD_AAC_STREAM_TIMING_H
#include "record_aac_probe.h"
RETRO_BEGIN_DECLS
#define RECORD_AAC_STREAM_GUARD 4096u
#define RECORD_AAC_STREAM_PREROLL 2048u
typedef struct record_aac_stream_timing
{
unsigned input_epoch, coded_origin, audio_epoch, video_epoch_us;
} record_aac_stream_timing_t;
/* Cold worker policy after runtime calibration, not an encoder-delay guess.
* Feed the probe then GUARD silent input frames before awaiting calibration.
* Continue silence to input_epoch before accepting original source PCM.
* Discard complete coded AAC packets before coded_origin. Transmit the
* remaining silent preroll and offset video by video_epoch_us. Keep native
* AAC dependencies through this preroll; no MP4 edit or source truncation.
* Caller must independently validate native encoded/decoded continuity. */
bool record_aac_stream_timing(unsigned rate, unsigned measured_delay,
record_aac_stream_timing_t *timing);
RETRO_END_DECLS
#endif
+29
View File
@@ -466,6 +466,24 @@ void *record_mediacodec_encoder_format(record_mediacodec_encoder_t *s)
{
return s && !s->failed ? s->format : NULL;
}
bool record_mediacodec_encoder_video_config(record_mediacodec_encoder_t *s,
unsigned index, const void **data, size_t *bytes)
{
static const char *const keys[2] = {"csd-0", "csd-1"};
void *buffer = NULL;
size_t size = 0;
if (data)
*data = NULL;
if (bytes)
*bytes = 0;
if (!s || !data || !bytes || s->failed || s->audio || !s->format
|| index >= 2 || !s->get_buffer(s->format, keys[index], &buffer, &size)
|| !buffer || !size || size > 65535)
return false;
*data = buffer;
*bytes = size;
return true;
}
bool record_mediacodec_encoder_audio_config(record_mediacodec_encoder_t *s,
const void **data, size_t *bytes)
{
@@ -666,6 +684,17 @@ void *record_mediacodec_encoder_format(record_mediacodec_encoder_t *s)
(void)s;
return NULL;
}
bool record_mediacodec_encoder_video_config(record_mediacodec_encoder_t *s,
unsigned index, const void **data, size_t *bytes)
{
(void)s;
(void)index;
if (data)
*data = NULL;
if (bytes)
*bytes = 0;
return false;
}
bool record_mediacodec_encoder_audio_config(record_mediacodec_encoder_t *s,
const void **data, size_t *bytes)
{
+8
View File
@@ -51,6 +51,14 @@ void *record_mediacodec_encoder_surface(record_mediacodec_encoder_t *encoder);
int record_mediacodec_encoder_pump(
record_mediacodec_encoder_t *encoder, record_mediacodec_packet_t *packet);
void *record_mediacodec_encoder_format(record_mediacodec_encoder_t *encoder);
/* Worker-only video CSD buffer 0 or 1, at most 65535 bytes each.
* Native encoding is unchanged (AVC SPS/PPS are separate buffers; HEVC
* parameter sets share buffer 0). Borrowed until the next pump, stop or free.
* No allocation, packet dequeue/release or payload conversion. Missing or
* invalid metadata clears both outputs without poisoning the encoder. */
bool record_mediacodec_encoder_video_config(
record_mediacodec_encoder_t *encoder, unsigned index,
const void **data, size_t *bytes);
/* AAC configuration borrowed for the current output format. */
bool record_mediacodec_encoder_audio_config(
record_mediacodec_encoder_t *encoder, const void **data, size_t *bytes);
+65
View File
@@ -0,0 +1,65 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#include "record_mediacodec_stream_config.h"
#include <stdlib.h>
#include <string.h>
rflv_mux_t *record_mediacodec_stream_mux_new(record_mediacodec_encoder_t *video,
record_mediacodec_encoder_t *audio, const record_mediacodec_stream_config_t *config)
{
rflv_config_t mux_config;
rflv_mux_t *mux;
const void *first = NULL, *second = NULL;
size_t first_size = 0, second_size = 0;
uint8_t *joined = NULL;
if (!video || !config || (unsigned)config->codec > RECORD_MC_STREAM_AV1
|| (config->bit_depth != 8 && config->bit_depth != 10)
|| (config->codec == RECORD_MC_STREAM_AVC && config->bit_depth != 8)
|| !config->max_packet_size
|| config->max_packet_size > (config->codec == RECORD_MC_STREAM_HEVC
? 0xfffff7u : 0xfffffau))
return NULL;
memset(&mux_config, 0, sizeof(mux_config));
if (!record_mediacodec_encoder_video_config(video, 0, &first, &first_size))
return NULL;
mux_config.avcc = first;
mux_config.avcc_size = first_size;
if (config->codec == RECORD_MC_STREAM_AVC
&& record_mediacodec_encoder_video_config(video, 1, &second, &second_size))
{
if (!first || !second || !first_size || !second_size
|| first_size > 65535 || second_size > 65535)
return NULL;
joined = (uint8_t*)malloc(first_size + second_size);
if (!joined)
return NULL;
memcpy(joined, first, first_size);
memcpy(joined + first_size, second, second_size);
mux_config.avcc = joined;
mux_config.avcc_size = first_size + second_size;
}
mux_config.video_timescale = 1000000;
mux_config.max_packet_size = config->max_packet_size;
if (audio)
{
if (!record_mediacodec_encoder_audio_config(audio, &mux_config.asc, &mux_config.asc_size))
{
free(joined);
return NULL;
}
mux_config.audio_timescale = 1000000;
}
if (config->codec == RECORD_MC_STREAM_AVC)
mux = rflv_mux_new_annexb(&mux_config);
else if (config->codec == RECORD_MC_STREAM_HEVC)
mux = rflv_mux_new_hevc_annexb(&mux_config);
else
mux = rflv_mux_new_av1(&mux_config);
free(joined);
if (mux && config->codec != RECORD_MC_STREAM_AVC
&& !rflv_mux_set_color(mux, &config->color, config->bit_depth))
{
rflv_mux_free(mux);
return NULL;
}
return mux;
}
+27
View File
@@ -0,0 +1,27 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef RECORD_MEDIACODEC_STREAM_CONFIG_H
#define RECORD_MEDIACODEC_STREAM_CONFIG_H
#include "record_mediacodec_encoder.h"
#include <formats/rflv_mux.h>
RETRO_BEGIN_DECLS
enum record_mediacodec_stream_codec
{
RECORD_MC_STREAM_AVC, RECORD_MC_STREAM_HEVC, RECORD_MC_STREAM_AV1
};
typedef struct record_mediacodec_stream_config
{
video_color_metadata_t color;
size_t max_packet_size;
enum record_mediacodec_stream_codec codec;
unsigned bit_depth;
} record_mediacodec_stream_config_t;
/* Cold I/O worker only, after all enabled encoders report FORMAT. Copies setup
* data into the shared mux; retains neither encoder nor native format/CSD.
* AVC/HEVC use push_annexb; AV1 uses ordinary push with sized OBUs. AAC is raw.
* Both tracks use MediaCodec packet pts_us (timescale 1000000); no rebasing.
* Caller owns the fresh mux. Packet/output lifetimes are a separate contract. */
rflv_mux_t *record_mediacodec_stream_mux_new(record_mediacodec_encoder_t *video,
record_mediacodec_encoder_t *audio, const record_mediacodec_stream_config_t *config);
RETRO_END_DECLS
#endif
+160 -22
View File
@@ -1,6 +1,7 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier:
* GPL-3.0-or-later */
#include "record_mediacodec_worker.h"
#include "record_aac_stream_timing.h"
#include <stdlib.h>
struct record_mediacodec_worker
@@ -9,13 +10,17 @@ struct record_mediacodec_worker
int64_t end_us[2];
record_mediacodec_encoder_t *encoder[2];
record_audio_queue_t *pcm;
record_mux_mediacodec_t *mux;
record_mediacodec_output_ops_t output;
void *output_context;
record_mediacodec_packet_t pending[2];
record_aac_probe_t *probe;
record_aac_stream_timing_t live_timing;
size_t consumed, prefix_sent;
unsigned channels, rate;
bool format[2], drained[2], ended[2];
bool format[2], drained[2], ended[2], pending_live[2];
bool active, closing, video_eos, audio_eos, pcm_end;
bool started, finished, failed;
bool live_probe, timing_ready;
};
static bool record_mcw_samples_us(uint64_t frames, unsigned rate,
@@ -41,13 +46,24 @@ bool record_mediacodec_worker_calibrate(record_mediacodec_worker_t *w,
return true;
}
bool record_mediacodec_worker_calibrate_live(record_mediacodec_worker_t *w,
record_aac_probe_t *probe)
{
record_aac_stream_timing_t timing;
if (!w || !record_aac_stream_timing(w->rate, 0, &timing)
|| !record_mediacodec_worker_calibrate(w, probe))
return false;
w->live_probe = true;
return true;
}
bool record_mediacodec_worker_audio_edit(
const record_mediacodec_worker_t *w, uint64_t *skip,
uint64_t *frames, unsigned *roll)
{
unsigned delay;
if (!w || !skip || !frames || !roll || !w->finished || w->failed
|| !w->probe || !record_aac_probe_delay(w->probe, &delay))
|| !w->probe || w->live_probe || !record_aac_probe_delay(w->probe, &delay))
return false;
*skip = RECORD_AAC_PROBE_FRAMES + (uint64_t)delay;
*frames = w->audio_end_sample;
@@ -55,13 +71,40 @@ bool record_mediacodec_worker_audio_edit(
return true;
}
record_mediacodec_worker_t *record_mediacodec_worker_new(
static bool record_mcw_file_track(void *context, bool audio, void *format)
{
return record_mux_mediacodec_track((record_mux_mediacodec_t*)context, audio, format);
}
static int record_mcw_file_start(void *context)
{
return record_mux_mediacodec_start((record_mux_mediacodec_t*)context);
}
static int record_mcw_file_write(void *context, bool audio,
const record_mediacodec_packet_t *packet)
{
return record_mux_mediacodec_write((record_mux_mediacodec_t*)context, audio, packet) ? 1 : -1;
}
static bool record_mcw_file_end(void *context, bool audio, int64_t end_us)
{
return record_mux_mediacodec_end((record_mux_mediacodec_t*)context, audio, end_us);
}
static int record_mcw_file_finish(void *context)
{
return record_mux_mediacodec_finish((record_mux_mediacodec_t*)context);
}
static bool record_mcw_file_retire(void *context)
{
(void)context;
return true;
}
record_mediacodec_worker_t *record_mediacodec_worker_new_output(
record_mediacodec_encoder_t *video, record_mediacodec_encoder_t *audio,
record_audio_queue_t *pcm, record_mux_mediacodec_t *mux,
unsigned channels, unsigned rate)
record_audio_queue_t *pcm, const record_mediacodec_output_ops_t *ops,
void *context, unsigned channels, unsigned rate)
{
record_mediacodec_worker_t *w;
if (!video || !mux || (!!audio != !!pcm)
if (!video || !context || !ops || !ops->track || !ops->start || !ops->write
|| !ops->end || !ops->finish || !ops->retire || (!!audio != !!pcm)
|| (audio && (!channels || channels > 2 || !rate)))
return NULL;
w = (record_mediacodec_worker_t *)calloc(1, sizeof(*w));
@@ -70,12 +113,25 @@ record_mediacodec_worker_t *record_mediacodec_worker_new(
w->encoder[0] = video;
w->encoder[1] = audio;
w->pcm = pcm;
w->mux = mux;
w->output = *ops;
w->output_context = context;
w->channels = channels;
w->rate = rate;
w->drained[1] = w->ended[1] = !audio;
return w;
}
record_mediacodec_worker_t *record_mediacodec_worker_new(
record_mediacodec_encoder_t *video, record_mediacodec_encoder_t *audio,
record_audio_queue_t *pcm, record_mux_mediacodec_t *mux,
unsigned channels, unsigned rate)
{
static const record_mediacodec_output_ops_t ops = {
record_mcw_file_track, record_mcw_file_start, record_mcw_file_write,
record_mcw_file_end, record_mcw_file_finish, record_mcw_file_retire
};
return record_mediacodec_worker_new_output(video, audio, pcm, &ops,
mux, channels, rate);
}
bool record_mediacodec_worker_finish(record_mediacodec_worker_t *w,
int64_t video_end_us, uint64_t audio_end_sample)
@@ -89,7 +145,7 @@ bool record_mediacodec_worker_finish(record_mediacodec_worker_t *w,
{
if ((!audio_end_sample && !w->probe)
|| (w->probe && audio_end_sample
> (uint64_t)-1 - RECORD_AAC_PROBE_FRAMES)
> (uint64_t)-1 - (w->live_probe ? 32768u : RECORD_AAC_PROBE_FRAMES))
|| !record_mcw_samples_us(audio_end_sample, w->rate,
&w->end_us[1]))
return false;
@@ -106,7 +162,13 @@ static int record_mcw_output(record_mediacodec_worker_t *w, unsigned at)
int result;
if (w->drained[at])
return 0;
result = record_mediacodec_encoder_pump(w->encoder[at], &packet);
if (w->pending_live[at])
{
packet = w->pending[at];
result = RECORD_MC_PACKET;
}
else
result = record_mediacodec_encoder_pump(w->encoder[at], &packet);
switch (result)
{
case RECORD_MC_BUSY:
@@ -123,7 +185,7 @@ static int record_mcw_output(record_mediacodec_worker_t *w, unsigned at)
|| !record_aac_probe_config(w->probe, config, bytes))
return -1;
}
if (w->format[at] || !record_mux_mediacodec_track(w->mux, at != 0,
if (w->format[at] || !w->output.track(w->output_context, at != 0,
record_mediacodec_encoder_format(w->encoder[at])))
return -1;
w->format[at] = true;
@@ -133,8 +195,11 @@ static int record_mcw_output(record_mediacodec_worker_t *w, unsigned at)
return -1;
if (!w->started)
return 0;
if (at && w->probe && !(packet.flags & 2))
if (!at && w->live_probe && !w->timing_ready)
return 0;
if (!w->pending_live[at] && at && w->probe && packet.size && !(packet.flags & 2))
{
uint64_t coded = w->coded_frames;
if (w->coded_frames > (uint64_t)-1 - 1024
|| !record_mcw_samples_us(w->coded_frames, w->rate,
&packet.pts_us)
@@ -142,10 +207,36 @@ static int record_mcw_output(record_mediacodec_worker_t *w, unsigned at)
packet.size))
return -1;
w->coded_frames += 1024;
if (w->live_probe)
{
if (!w->timing_ready || coded < w->live_timing.coded_origin)
return record_mediacodec_encoder_release(w->encoder[at]) ? 1 : -1;
if (!record_mcw_samples_us(coded - w->live_timing.coded_origin,
w->rate, &packet.pts_us))
return -1;
}
}
if (!record_mux_mediacodec_write(w->mux, at != 0, &packet)
|| !record_mediacodec_encoder_release(w->encoder[at]))
if (!w->pending_live[at])
{
if (!at && w->live_probe)
{
uint64_t maximum = ((uint64_t)1 << 63) - 1;
if (packet.pts_us < 0 || (uint64_t)packet.pts_us
> maximum - w->live_timing.video_epoch_us)
return -1;
packet.pts_us += w->live_timing.video_epoch_us;
}
w->pending[at] = packet;
w->pending_live[at] = true;
}
result = w->output.write(w->output_context, at != 0, &w->pending[at]);
if (result < 0)
return -1;
if (!result)
return 0;
if (!record_mediacodec_encoder_release(w->encoder[at]))
return -1;
w->pending_live[at] = false;
return 1;
case RECORD_MC_END:
if (!w->format[at] || !w->closing
@@ -160,6 +251,7 @@ static int record_mcw_output(record_mediacodec_worker_t *w, unsigned at)
static int record_mcw_input(record_mediacodec_worker_t *w)
{
static const int16_t silence[512 * 2] = {0};
const int16_t *samples = NULL;
size_t frames = 0, accepted = 0;
uint64_t timestamp = 0;
@@ -180,6 +272,25 @@ static int record_mcw_input(record_mediacodec_worker_t *w)
w->prefix_sent += accepted;
return result ? 1 : 0;
}
if (w->live_probe)
{
size_t target = w->timing_ready ? w->live_timing.input_epoch
: RECORD_AAC_PROBE_FRAMES + RECORD_AAC_STREAM_GUARD;
if (w->prefix_sent < target)
{
frames = target - w->prefix_sent;
if (frames > 512)
frames = 512;
result = record_mediacodec_encoder_audio_submit(w->encoder[1],
silence, frames, w->prefix_sent, &accepted);
if (result < 0 || result > 1 || accepted > frames || (!result && accepted))
return -1;
w->prefix_sent += accepted;
return result ? 1 : 0;
}
if (!w->timing_ready)
return 0;
}
if (!w->pcm_end)
{
result = record_audio_queue_peek(w->pcm, &samples, &frames, &timestamp);
@@ -192,13 +303,15 @@ static int record_mcw_input(record_mediacodec_worker_t *w)
if (w->consumed >= frames || frames > (size_t)-1 / w->channels
|| timestamp > (uint64_t)-1 - frames
|| (w->probe && timestamp + frames
> (uint64_t)-1 - RECORD_AAC_PROBE_FRAMES)
> (uint64_t)-1 - (w->live_probe
? w->live_timing.input_epoch : RECORD_AAC_PROBE_FRAMES))
|| (w->closing && timestamp + frames > w->audio_end_sample))
return -1;
result = record_mediacodec_encoder_audio_submit(w->encoder[1],
samples + w->consumed * w->channels, frames - w->consumed,
timestamp + w->consumed
+ (w->probe ? RECORD_AAC_PROBE_FRAMES : 0), &accepted);
+ (w->live_probe ? w->live_timing.input_epoch
: w->probe ? RECORD_AAC_PROBE_FRAMES : 0), &accepted);
if (result < 0 || result > 1 || accepted > frames - w->consumed
|| (!result && accepted))
return -1;
@@ -218,7 +331,8 @@ static int record_mcw_input(record_mediacodec_worker_t *w)
return 0;
result = record_mediacodec_encoder_audio_finish(
w->encoder[1], w->audio_end_sample
+ (w->probe ? RECORD_AAC_PROBE_FRAMES : 0));
+ (w->live_probe ? w->live_timing.input_epoch
: w->probe ? RECORD_AAC_PROBE_FRAMES : 0));
if (result < 0 || result > 2)
return -1;
if (result == 1)
@@ -262,23 +376,35 @@ int record_mediacodec_worker_step(record_mediacodec_worker_t *w)
goto failed;
calibrated = result == 2;
progress |= result == 1;
if (w->live_probe && calibrated && !w->timing_ready)
{
unsigned delay;
if (!record_aac_probe_delay(w->probe, &delay)
|| !record_aac_stream_timing(w->rate, delay, &w->live_timing))
goto failed;
w->timing_ready = true;
progress = true;
}
if (w->drained[1] && calibrated)
{
unsigned delay;
uint64_t skip;
if (!record_aac_probe_delay(w->probe, &delay))
goto failed;
skip = RECORD_AAC_PROBE_FRAMES + (uint64_t)delay;
skip = (w->live_probe ? w->live_timing.input_epoch
: RECORD_AAC_PROBE_FRAMES) + (uint64_t)delay;
if ((w->audio_end_sample && (w->coded_frames < skip
|| w->coded_frames - skip < w->audio_end_sample))
|| !record_mcw_samples_us(w->coded_frames, w->rate,
|| !record_mcw_samples_us(w->live_probe
? w->live_timing.audio_epoch + w->audio_end_sample
: w->coded_frames, w->rate,
&w->end_us[1]))
goto failed;
}
}
if (!w->started)
{
result = record_mux_mediacodec_start(w->mux);
result = w->output.start(w->output_context);
if (result < 0)
goto failed;
if (result)
@@ -291,14 +417,24 @@ int record_mediacodec_worker_step(record_mediacodec_worker_t *w)
if (w->started && w->drained[at] && !w->ended[at]
&& (!at || calibrated))
{
if (!record_mux_mediacodec_end(w->mux, at != 0, w->end_us[at]))
int64_t end = w->end_us[at];
if (!at && w->live_probe)
{
uint64_t maximum = ((uint64_t)1 << 63) - 1;
if (!w->timing_ready)
continue;
if ((uint64_t)end > maximum - w->live_timing.video_epoch_us)
goto failed;
end += w->live_timing.video_epoch_us;
}
if (!w->output.end(w->output_context, at != 0, end))
goto failed;
w->ended[at] = true;
progress = true;
}
if (w->ended[0] && w->ended[1])
{
result = record_mux_mediacodec_finish(w->mux);
result = w->output.finish(w->output_context);
if (result < 0)
goto failed;
if (result)
@@ -323,6 +459,8 @@ bool record_mediacodec_worker_free(record_mediacodec_worker_t *w)
{
if (w && w->active && !w->failed && !w->finished)
return false;
if (w && !w->output.retire(w->output_context))
return false;
free(w);
return true;
}
+32 -2
View File
@@ -14,6 +14,28 @@ enum record_mediacodec_worker_result
RECORD_MC_WORKER_PROGRESS,
RECORD_MC_WORKER_END
};
typedef struct record_mediacodec_output_ops
{
/* Format is borrowed only during this call; copy setup metadata here. */
bool (*track)(void *context, bool audio, void *format);
/* start/finish: -1 error, 0 BUSY, 1 complete. */
int (*start)(void *context);
/* -1 error, 0 BUSY, 1 accepted. BUSY retains the same immutable packet
* descriptor and native bytes; accepted means all output borrows retired. */
int (*write)(void *context, bool audio, const record_mediacodec_packet_t *packet);
bool (*end)(void *context, bool audio, int64_t end_us);
int (*finish)(void *context);
/* Bounded worker-only cancellation/retirement. false requires a later
* free retry; true guarantees no remaining packet/format borrow. */
bool (*retire)(void *context);
} record_mediacodec_output_ops_t;
/* Copies the callback table; borrows context and encoders until free succeeds.
* Output owns no encoder: retire must quiesce all its borrows before caller
* stops or destroys encoders. Audio encoder and PCM queue are paired. */
record_mediacodec_worker_t *record_mediacodec_worker_new_output(
record_mediacodec_encoder_t *video, record_mediacodec_encoder_t *audio,
record_audio_queue_t *pcm, const record_mediacodec_output_ops_t *ops,
void *context, unsigned channels, unsigned rate);
/* One I/O worker borrows all components. Audio encoder and queue must both
* be present or absent. channels/rate match their cold configuration. */
record_mediacodec_worker_t *record_mediacodec_worker_new(
@@ -23,6 +45,12 @@ record_mediacodec_worker_t *record_mediacodec_worker_new(
/* Before service starts; caller retains the probe until worker retirement. */
bool record_mediacodec_worker_calibrate(record_mediacodec_worker_t *worker,
record_aac_probe_t *probe);
/* Live output only, before service. Uses measured delay and silent preroll;
* calibration packets never reach output. Original PCM timestamps stay in
* the source epoch; output video is shifted to match the audible source.
* Probe and encoders remain borrowed through worker retirement. */
bool record_mediacodec_worker_calibrate_live(record_mediacodec_worker_t *worker,
record_aac_probe_t *probe);
/* Completed calibrated recording only; source frames exclude the prefix. */
bool record_mediacodec_worker_audio_edit(
const record_mediacodec_worker_t *worker, uint64_t *skip,
@@ -34,8 +62,10 @@ bool record_mediacodec_worker_finish(record_mediacodec_worker_t *worker,
/* One output action per encoder and one bounded PCM input action. Borrows
* survive backpressure. No per-packet allocation, wait or GPU submission. */
int record_mediacodec_worker_step(record_mediacodec_worker_t *worker);
/* Controller still owns encoder/GPU retirement, mux cancellation and PCM
* drain on failure. Free refuses active service unless aborted. */
/* Controller still owns encoder/GPU retirement and PCM drain on failure.
* Free refuses active service unless aborted, and retries output retirement
* after failure, abort or completion. Keep output context and encoders alive
* until free succeeds; only then may the controller stop/free the encoders. */
void record_mediacodec_worker_abort(record_mediacodec_worker_t *worker);
bool record_mediacodec_worker_free(record_mediacodec_worker_t *worker);
RETRO_END_DECLS
+222
View File
@@ -0,0 +1,222 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#include "record_stream_mediacodec.h"
#include <stdlib.h>
struct record_stream_mediacodec
{
record_mediacodec_encoder_t *video, *audio;
net_rtmp_client_t *client;
net_rtmp_session_t *session;
rflv_mux_t *mux;
record_mediacodec_stream_config_t config;
const record_mediacodec_packet_t *pending;
bool format[2], end[2];
bool ready, sending, pending_audio, accepted, finishing, finished;
bool failed, retired;
};
static bool record_mcs_stream_track(void *context, bool audio, void *format)
{
record_stream_mediacodec_t *s = (record_stream_mediacodec_t*)context;
unsigned at = audio ? 1 : 0;
if (s->failed || s->retired || s->format[at] || !format
|| (audio && !s->audio))
return false;
s->format[at] = true;
return true;
}
static int record_mcs_stream_start(void *context)
{
record_stream_mediacodec_t *s = (record_stream_mediacodec_t*)context;
if (s->failed || s->retired)
return -1;
if (!s->format[0] || !s->format[1] || !s->session)
return 0;
if (!s->mux)
{
s->mux = record_mediacodec_stream_mux_new(s->video, s->audio, &s->config);
if (!s->mux)
{
s->failed = true;
return -1;
}
}
return s->ready ? 1 : 0;
}
static int record_mcs_stream_write(void *context, bool audio,
const record_mediacodec_packet_t *packet)
{
record_stream_mediacodec_t *s = (record_stream_mediacodec_t*)context;
int result;
if (s->failed || s->retired || !s->ready || s->finishing || !packet
|| s->end[audio ? 1 : 0] || (audio && !s->audio))
return -1;
if (s->pending)
{
if (s->pending != packet || s->pending_audio != audio || !s->accepted)
return 0;
s->pending = NULL;
s->accepted = false;
return 1;
}
if (!packet->size && (packet->flags & 4))
return 1;
if (!packet->data || !packet->size || packet->pts_us < 0 || (packet->flags & ~5u))
return -1;
if (!audio && s->config.codec != RECORD_MC_STREAM_AV1)
result = rflv_mux_push_annexb(s->mux, packet->data, packet->size,
(uint64_t)packet->pts_us, (uint64_t)packet->pts_us, !!(packet->flags & 1));
else
result = rflv_mux_push(s->mux, audio ? 2 : 1, packet->data, packet->size,
(uint64_t)packet->pts_us, (uint64_t)packet->pts_us, !!(packet->flags & 1));
if (result == RFLV_BUSY)
return 0;
if (result != RFLV_PROGRESS)
return -1;
s->pending = packet;
s->pending_audio = audio;
return 0;
}
static bool record_mcs_stream_end(void *context, bool audio, int64_t end_us)
{
record_stream_mediacodec_t *s = (record_stream_mediacodec_t*)context;
if (s->failed || s->retired || end_us < 0
|| (s->pending && s->pending_audio == audio))
return false;
s->end[audio ? 1 : 0] = true;
return true;
}
static int record_mcs_stream_finish(void *context)
{
record_stream_mediacodec_t *s = (record_stream_mediacodec_t*)context;
if (s->failed || s->retired || !s->end[0] || !s->end[1])
return -1;
if (s->pending || s->sending)
return 0;
if (!s->finishing)
{
if (!rflv_mux_finish(s->mux))
return -1;
if (!net_rtmp_session_finish(s->session))
return 0;
s->finishing = true;
}
return s->finished ? 1 : 0;
}
static bool record_mcs_stream_retire(void *context)
{
record_stream_mediacodec_t *s = (record_stream_mediacodec_t*)context;
if (s->retired)
return true;
net_rtmp_client_cancel(s->client);
if (s->sending && !net_rtmp_session_media_release(s->session))
return false;
s->sending = false;
rflv_mux_free(s->mux);
s->mux = NULL;
s->pending = NULL;
s->retired = true;
return true;
}
record_stream_mediacodec_t *record_stream_mediacodec_new(
record_mediacodec_encoder_t *video, record_mediacodec_encoder_t *audio,
net_rtmp_client_t *client, const record_mediacodec_stream_config_t *config)
{
record_stream_mediacodec_t *s;
if (!video || !client || !config || !config->max_packet_size
|| (unsigned)config->codec > RECORD_MC_STREAM_AV1)
return NULL;
s = (record_stream_mediacodec_t*)calloc(1, sizeof(*s));
if (!s)
return NULL;
s->video = video;
s->audio = audio;
s->client = client;
s->config = *config;
s->format[1] = s->end[1] = !audio;
return s;
}
const record_mediacodec_output_ops_t *record_stream_mediacodec_ops(void)
{
static const record_mediacodec_output_ops_t ops = {
record_mcs_stream_track, record_mcs_stream_start, record_mcs_stream_write,
record_mcs_stream_end, record_mcs_stream_finish, record_mcs_stream_retire
};
return &ops;
}
int record_stream_mediacodec_step(record_stream_mediacodec_t *s,
uint32_t receive_time, net_rtmp_client_view_t *wait)
{
enum net_rtmp_client_state state;
rflv_tag_t tag;
net_rtmp_span_t spans[2];
int result;
if (!s || !wait || s->failed || s->retired)
return -1;
state = net_rtmp_client_step(s->client, receive_time, wait);
if (state == NET_RTMP_CLIENT_FAILED)
goto failed;
if (state == NET_RTMP_CLIENT_FINISHED)
{
if (!s->finishing)
goto failed;
s->finished = true;
return 2;
}
if (state != NET_RTMP_CLIENT_READY)
return wait->again ? 1 : 0;
s->session = wait->session;
if (!s->session)
goto failed;
if (!s->mux || s->accepted)
return wait->again ? 1 : 0;
if (s->sending)
{
if (!net_rtmp_session_media_release(s->session))
return wait->again ? 1 : 0;
s->sending = false;
if (!rflv_mux_release(s->mux))
goto failed;
s->accepted = s->pending != NULL;
return 1;
}
result = rflv_mux_step(s->mux);
if (result == RFLV_ERROR)
goto failed;
if (result == RFLV_PACKET)
{
if (!rflv_mux_get_tag(s->mux, &tag))
return rflv_mux_release(s->mux) ? 1 : -1;
spans[0].data = tag.spans[0].data;
spans[0].size = tag.spans[0].size;
spans[1].data = tag.spans[1].data;
spans[1].size = tag.spans[1].size;
if (net_rtmp_session_media(s->session, tag.type, tag.timestamp, spans, 2))
{
s->sending = true;
return 1;
}
}
else if (result == RFLV_BUSY)
s->ready = true;
return result == RFLV_PROGRESS || wait->again ? 1 : 0;
failed:
s->failed = true;
return -1;
}
bool record_stream_mediacodec_free(record_stream_mediacodec_t *s)
{
if (s && !record_mcs_stream_retire(s))
return false;
free(s);
return true;
}
+26
View File
@@ -0,0 +1,26 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef RECORD_STREAM_MEDIACODEC_H
#define RECORD_STREAM_MEDIACODEC_H
#include "record_mediacodec_worker.h"
#include "record_mediacodec_stream_config.h"
#include <net/net_rtmp_client.h>
RETRO_BEGIN_DECLS
typedef struct record_stream_mediacodec record_stream_mediacodec_t;
/* Cold I/O owner. Copies config; borrows encoders and client until free.
* The controller must establish stream priming/source epochs separately;
* file calibration prefixes and MP4 edits must not be used for this output.
* No producer stop/free until worker free has retired this output. */
record_stream_mediacodec_t *record_stream_mediacodec_new(
record_mediacodec_encoder_t *video, record_mediacodec_encoder_t *audio,
net_rtmp_client_t *client, const record_mediacodec_stream_config_t *config);
const record_mediacodec_output_ops_t *record_stream_mediacodec_ops(void);
/* One bounded network/mux action, before worker_step. -1 error, 0 BUSY,
* 1 progress, 2 completed. Caller schedules returned socket interests and
* owns setup/drain deadlines. No waits, internal clocks or packet allocation. */
int record_stream_mediacodec_step(record_stream_mediacodec_t *stream,
uint32_t receive_time, net_rtmp_client_view_t *wait);
/* After worker_free succeeds. Retires cancellation before freeing mux bytes;
* false requires another retry. Does not destroy borrowed client/encoders. */
bool record_stream_mediacodec_free(record_stream_mediacodec_t *stream);
RETRO_END_DECLS
#endif
@@ -317,6 +317,8 @@ int main(void)
assert(record_mediacodec_encoder_pump(s, &packet) == RECORD_MC_FORMAT);
assert(record_mediacodec_encoder_audio_config(s, &config_data,
&config_bytes) && config_data == asc && config_bytes == 2);
assert(!record_mediacodec_encoder_video_config(s, 0, &config_data,
&config_bytes) && !config_data && !config_bytes);
for (config_fault = 1; config_fault <= 3; config_fault++)
assert(!record_mediacodec_encoder_audio_config(s, &config_data,
&config_bytes) && !config_data && !config_bytes);
@@ -4,6 +4,7 @@
#include <stdio.h>
#include <string.h>
#include "../../../record/record_mediacodec_worker.h"
#include "../../../record/record_aac_stream_timing.h"
struct record_mediacodec_encoder
{
@@ -26,6 +27,9 @@ static const unsigned char config[] = {0x11, 0x88};
static const unsigned char encoded = 1;
static unsigned fault;
static bool empty;
static bool live_mode;
static unsigned live_delay;
static record_aac_stream_timing_t live_timing;
const int16_t *record_aac_probe_input(record_aac_probe_t *p,
size_t at, size_t *frames)
@@ -63,7 +67,7 @@ bool record_aac_probe_delay(const record_aac_probe_t *p, unsigned *delay)
{
if (!p->ready)
return false;
*delay = fault == 7 ? 4096 : 2048;
*delay = live_mode ? live_delay : fault == 7 ? 4096 : 2048;
return true;
}
bool record_mediacodec_encoder_audio_config(record_mediacodec_encoder_t *e,
@@ -86,6 +90,9 @@ int record_mediacodec_encoder_audio_submit(record_mediacodec_encoder_t *e,
return 0;
if (timestamp < RECORD_AAC_PROBE_FRAMES)
assert(samples == prefix + timestamp);
else if (live_mode && timestamp < live_timing.input_epoch)
for (i = 0; i < count; i++)
assert(samples[i] == 0);
else
for (i = 0; i < count; i++)
assert(samples[i] == 7);
@@ -96,7 +103,7 @@ int record_mediacodec_encoder_audio_submit(record_mediacodec_encoder_t *e,
int record_mediacodec_encoder_audio_finish(record_mediacodec_encoder_t *e,
uint64_t end)
{
assert(e->audio && end == RECORD_AAC_PROBE_FRAMES + (empty ? 0 : 17)
assert(e->audio && end == (live_mode ? live_timing.input_epoch : RECORD_AAC_PROBE_FRAMES) + (empty ? 0 : 17)
&& e->accepted == end);
e->eos = true;
return 1;
@@ -117,8 +124,10 @@ int record_mediacodec_encoder_pump(record_mediacodec_encoder_t *e,
return RECORD_MC_FORMAT;
}
limit = e->audio ? (unsigned)(e->accepted / 1024) : 1;
if (e->audio && live_mode)
limit = e->accepted > live_delay ? (unsigned)((e->accepted - live_delay) / 1024) : 0;
if (e->audio && e->eos)
limit = fault == 5 ? 7 : empty ? 8 : 11;
limit = fault == 5 ? 7 : live_mode ? (unsigned)((e->accepted + live_delay + 1023) / 1024) : empty ? 8 : 11;
if (e->packets < limit)
{
e->held = true;
@@ -159,14 +168,15 @@ bool record_mux_mediacodec_write(record_mux_mediacodec_t *m, bool audio,
assert(p->data == &encoded && p->size == 1);
assert(p->pts_us == (audio
? (int64_t)((uint64_t)m->packets[1] * 1024 * 1000000 / 48000)
: 0));
: live_mode ? live_timing.video_epoch_us : 0));
m->packets[audio]++;
return true;
}
bool record_mux_mediacodec_end(record_mux_mediacodec_t *m,
bool audio, int64_t end)
{
assert(end == (audio ? (empty ? 170666 : 234666) : 1000000));
assert(end == (audio ? live_mode ? (int64_t)((uint64_t)(2048 + (empty ? 0 : 17)) * 1000000 / 48000)
: (empty ? 170666 : 234666) : 1000000 + (live_mode ? live_timing.video_epoch_us : 0)));
m->ends[audio]++;
return true;
}
@@ -176,12 +186,16 @@ int record_mux_mediacodec_finish(record_mux_mediacodec_t *m)
m->finished = true;
return 1;
}
static void exercise(unsigned failure, bool cancel, bool no_pcm)
#include "worker_output_fixture.h"
static void exercise_mode(unsigned failure, bool cancel, bool no_pcm, bool backpressure,
bool live, unsigned delay)
{
record_mediacodec_encoder_t video, audio;
record_mux_mediacodec_t mux;
record_aac_probe_t probe;
record_mediacodec_worker_t *w;
struct record_busy_output output;
record_audio_queue_t *q;
int16_t pcm[17];
const int16_t *samples;
@@ -196,6 +210,9 @@ static void exercise(unsigned failure, bool cancel, bool no_pcm)
audio.audio = true;
fault = failure;
empty = no_pcm;
live_mode = live;
live_delay = delay;
assert(record_aac_stream_timing(48000, delay, &live_timing));
for (i = 0; i < 17; i++)
pcm[i] = 7;
q = record_audio_queue_new(1, 17, 256);
@@ -203,9 +220,17 @@ static void exercise(unsigned failure, bool cancel, bool no_pcm)
if (!empty)
assert(record_audio_queue_push(q, pcm, 17, 0) == RECORD_AUDIO_OK);
assert(record_audio_queue_finish(q));
w = record_mediacodec_worker_new(&video, &audio, q, &mux, 1, 48000);
assert(w && record_mediacodec_worker_calibrate(w, &probe));
memset(&output, 0, sizeof(output));
output.mux = &mux;
output.busy[0] = 2;
output.busy[1] = 4;
w = backpressure
? record_mediacodec_worker_new_output(&video, &audio, q, &busy_ops, &output, 1, 48000)
: record_mediacodec_worker_new(&video, &audio, q, &mux, 1, 48000);
assert(w && (live ? record_mediacodec_worker_calibrate_live(w, &probe)
: record_mediacodec_worker_calibrate(w, &probe)));
assert(!record_mediacodec_worker_calibrate(w, &probe));
assert(!record_mediacodec_worker_calibrate_live(w, &probe));
assert(!record_mediacodec_worker_audio_edit(w, &skip, &source, &roll));
assert(!record_mediacodec_worker_finish(w, 1000000, (uint64_t)-1));
assert(record_mediacodec_worker_finish(w, 1000000, empty ? 0 : 17));
@@ -228,10 +253,20 @@ static void exercise(unsigned failure, bool cancel, bool no_pcm)
else
{
assert(result == RECORD_MC_WORKER_END && mux.finished);
assert(record_mediacodec_worker_audio_edit(w, &skip, &source, &roll));
assert(skip == 10240 && source == (empty ? 0 : 17) && roll == 2);
assert(audio.accepted == (empty ? 8192 : 8209)
&& mux.packets[1] == (empty ? 8 : 11));
if (live)
{
assert(!record_mediacodec_worker_audio_edit(w, &skip, &source, &roll));
assert(audio.accepted == live_timing.input_epoch + (empty ? 0 : 17));
assert(mux.packets[1] == (empty ? 2u : 3u));
assert(audio.packets - mux.packets[1] == live_timing.coded_origin / 1024);
}
else
{
assert(record_mediacodec_worker_audio_edit(w, &skip, &source, &roll));
assert(skip == 10240 && source == (empty ? 0 : 17) && roll == 2);
assert(audio.accepted == (empty ? 8192 : 8209)
&& mux.packets[1] == (empty ? 8 : 11));
}
}
assert(record_mediacodec_worker_step(w) == result);
assert(record_mediacodec_worker_free(w));
@@ -240,17 +275,33 @@ static void exercise(unsigned failure, bool cancel, bool no_pcm)
assert(record_audio_queue_ack(q));
assert(record_audio_queue_free(q));
}
static void exercise(unsigned failure, bool cancel, bool no_pcm, bool backpressure)
{
exercise_mode(failure, cancel, no_pcm, backpressure, false, 2048);
}
int main(void)
{
exercise(0, false, false);
exercise(0, false, true);
exercise(2, false, false);
exercise(3, false, false);
exercise(4, false, false);
exercise(5, false, false);
exercise(7, false, false);
exercise(0, true, false);
static const unsigned delays[] = {0,1,1023,1024,2048,4096};
unsigned i;
exercise(0, false, false, false);
exercise(0, false, true, false);
exercise(2, false, false, false);
exercise(3, false, false, false);
exercise(4, false, false, false);
exercise(5, false, false, false);
exercise(7, false, false, false);
exercise(0, true, false, false);
exercise(0, false, false, true);
exercise(0, false, true, true);
for (i = 0; i < sizeof(delays) / sizeof(delays[0]); i++)
{
exercise_mode(0, false, false, false, true, delays[i]);
exercise_mode(0, false, true, false, true, delays[i]);
exercise_mode(0, false, false, true, true, delays[i]);
exercise_mode(0, false, true, true, true, delays[i]);
exercise_mode(0, true, false, true, true, delays[i]);
}
puts("PASS: calibrated worker prefix backpressure, coded/source clocks, "
"trim ownership, early EOS and cancellation; codecs/probe mocked");
"file trim, live silent priming, source epochs, empty PCM, early EOS and cancellation; codecs/probe mocked");
return 0;
}
+9 -1
View File
@@ -44,7 +44,7 @@ with tempfile.TemporaryDirectory(prefix="mediacodec-encode-") as directory:
audio_fragment.append(shipping_audio[start:end])
(Path(directory) / "audio_tap_fragment.c").write_text("\n".join(audio_fragment))
binary = Path(directory) / ("encoder_test.exe" if os.name == "nt" else "encoder_test")
for source, extra in (("test.c", []), ("audio_test.c", []),
for source, extra in (("test.c", []), ("stream_config_test.c", []), ("stream_output_test.c", []), ("stream_timing_test.c", []), ("audio_test.c", []),
("mux_test.c", ["-DRECORD_MEDIACODEC_MUX_TEST"]),
("worker_test.c", []), ("calibrated_worker_test.c", []),
("surface_test.c", []),
@@ -60,9 +60,17 @@ with tempfile.TemporaryDirectory(prefix="mediacodec-encode-") as directory:
("session_test.c", []), ("dispatch_test.c", []),
("gpu_dispatch_test.c", [])):
command = compiler + flags + extra + [str(here / source), "-o", str(binary)]
if source == "stream_timing_test.c":
command += [str(root / "record/record_aac_stream_timing.c")]
if source in ("stream_config_test.c", "stream_output_test.c"):
command += [str(root / "record/record_mediacodec_stream_config.c"),
str(root / "libretro-common/formats/flv/rflv_mux.c")]
if source == "stream_output_test.c":
command += [str(root / "record/record_stream_mediacodec.c")]
if source in ("worker_test.c", "calibrated_worker_test.c"):
command += ["-DHAVE_THREADS",
str(root / "record/record_mediacodec_worker.c"),
str(root / "record/record_aac_stream_timing.c"),
str(root / "record/record_audio_queue.c"),
str(root / "libretro-common/queues/retro_spsc.c")]
if source == "worker_test.c":
@@ -0,0 +1,168 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "../../../record/record_mediacodec_stream_config.h"
struct record_mediacodec_encoder
{
const void *data[2];
size_t bytes[2];
bool audio;
};
static unsigned queries;
static uint8_t sps[] = {0,0,0,1,103,66,0,30};
static uint8_t pps[] = {0,0,0,1,104};
static uint8_t asc[] = {18,16};
static const uint8_t combined_avc[] = {0,0,0,1,103,66,0,30,0,0,0,1,104};
static const uint8_t avcc[] = {1,66,0,30,255,225,0,4,103,66,0,30,1,0,1,104};
/* Golden parameter-set bytes from the shared FLV mux HEVC fixture. */
static const uint8_t hevc8[] =
{
0,0,0,1,64,1,170,0,0,0,1,66,
1,1,17,17,17,17,17,17,17,17,17,17,
17,17,160,16,32,97,112,0,0,1,68,1,
170
};
static const uint8_t hevc10[] =
{
0,0,0,1,64,1,170,0,0,0,1,66,
1,1,17,17,17,17,17,17,17,17,17,17,
17,17,160,16,32,97,55,0,0,1,68,1,
170
};
static const uint8_t av1[] = {129,0,76,0,10,13,0,0,0,3,55,251,230,215,206,129,1,4,130};
bool record_mediacodec_encoder_video_config(record_mediacodec_encoder_t *encoder,
unsigned index, const void **data, size_t *bytes)
{
queries++;
*data = NULL;
*bytes = 0;
if (!encoder || encoder->audio || index > 1 || !encoder->bytes[index])
return false;
*data = encoder->data[index];
*bytes = encoder->bytes[index];
return true;
}
bool record_mediacodec_encoder_audio_config(record_mediacodec_encoder_t *encoder,
const void **data, size_t *bytes)
{
queries++;
*data = NULL;
*bytes = 0;
if (!encoder || !encoder->audio || !encoder->bytes[0])
return false;
*data = encoder->data[0];
*bytes = encoder->bytes[0];
return true;
}
static void setup(record_mediacodec_encoder_t *video, record_mediacodec_encoder_t *audio,
record_mediacodec_stream_config_t *config)
{
memset(video, 0, sizeof(*video));
memset(audio, 0, sizeof(*audio));
memset(config, 0, sizeof(*config));
video->data[0] = sps;
video->bytes[0] = sizeof(sps);
video->data[1] = pps;
video->bytes[1] = sizeof(pps);
audio->data[0] = asc;
audio->bytes[0] = sizeof(asc);
audio->audio = true;
config->max_packet_size = 1024;
config->bit_depth = 8;
}
static void drain_setup(rflv_mux_t *mux)
{
const rflv_span_t *spans;
size_t count;
unsigned actions = 0;
while (rflv_mux_get(mux, &spans, &count))
{
assert(++actions < 8);
assert(rflv_mux_release(mux));
}
assert(rflv_mux_step(mux) == RFLV_BUSY);
}
int main(void)
{
record_mediacodec_encoder_t video, audio;
record_mediacodec_stream_config_t config;
rflv_mux_t *mux;
rflv_tag_t tag;
unsigned before, i;
static const uint8_t picture[] = {0,0,1,101,136};
setup(&video, &audio, &config);
mux = record_mediacodec_stream_mux_new(&video, &audio, &config);
assert(mux && rflv_mux_release(mux));
sps[4] = pps[4] = asc[0] = 0; /* Native setup may change after constructor return. */
assert(rflv_mux_get_tag(mux, &tag) && tag.type == 9);
assert(tag.spans[1].size == sizeof(avcc) && !memcmp(tag.spans[1].data, avcc, sizeof(avcc)));
assert(rflv_mux_release(mux) && rflv_mux_get_tag(mux, &tag) && tag.type == 8);
assert(tag.spans[1].size == 2 && ((const uint8_t*)tag.spans[1].data)[0] == 18);
drain_setup(mux);
assert(rflv_mux_push_annexb(mux, picture, sizeof(picture), 1000000, 1000000, true) == RFLV_PROGRESS);
for (i = 0; i < 8 && rflv_mux_step(mux) == RFLV_PROGRESS; i++) {}
assert(i < 8 && rflv_mux_get_tag(mux, &tag) && tag.timestamp == 1000);
assert(rflv_mux_release(mux));
assert(rflv_mux_push(mux, 2, picture, 2, 1000000, 1000000, false) == RFLV_PROGRESS);
assert(rflv_mux_step(mux) == RFLV_PACKET && rflv_mux_get_tag(mux, &tag));
assert(tag.timestamp == 1000 && tag.spans[1].data == picture);
rflv_mux_free(mux);
sps[4] = 103;
pps[4] = 104;
asc[0] = 18;
setup(&video, &audio, &config);
audio.bytes[0] = 0;
assert(!record_mediacodec_stream_mux_new(&video, &audio, &config));
video.bytes[1] = 0;
assert(!record_mediacodec_stream_mux_new(&video, NULL, &config));
video.data[0] = combined_avc;
video.bytes[0] = sizeof(combined_avc);
mux = record_mediacodec_stream_mux_new(&video, NULL, &config);
assert(mux);
drain_setup(mux);
rflv_mux_free(mux);
setup(&video, &audio, &config);
before = queries;
config.max_packet_size = 0xffffffu;
assert(!record_mediacodec_stream_mux_new(&video, NULL, &config) && queries == before);
config.max_packet_size = 1024;
config.bit_depth = 10;
assert(!record_mediacodec_stream_mux_new(&video, NULL, &config) && queries == before);
setup(&video, &audio, &config);
config.codec = RECORD_MC_STREAM_HEVC;
video.data[0] = hevc8;
video.bytes[0] = sizeof(hevc8);
mux = record_mediacodec_stream_mux_new(&video, NULL, &config);
assert(mux);
drain_setup(mux);
rflv_mux_free(mux);
video.data[0] = hevc10;
video.bytes[0] = sizeof(hevc10);
assert(!record_mediacodec_stream_mux_new(&video, NULL, &config));
config.bit_depth = 10;
mux = record_mediacodec_stream_mux_new(&video, &audio, &config);
assert(mux);
drain_setup(mux);
rflv_mux_free(mux);
config.codec = RECORD_MC_STREAM_AV1;
video.data[0] = av1;
video.bytes[0] = sizeof(av1);
mux = record_mediacodec_stream_mux_new(&video, NULL, &config);
assert(mux);
drain_setup(mux);
rflv_mux_free(mux);
config.bit_depth = 8;
assert(!record_mediacodec_stream_mux_new(&video, NULL, &config));
for (i = RECORD_MC_STREAM_AVC; i <= RECORD_MC_STREAM_AV1; i++)
{
config.codec = (enum record_mediacodec_stream_codec)i;
config.bit_depth = 8;
config.max_packet_size = i == RECORD_MC_STREAM_HEVC ? 0xfffff8u : 0xfffffbu;
before = queries;
assert(!record_mediacodec_stream_mux_new(&video, NULL, &config) && queries == before);
}
puts("PASS MediaCodec AVC/HEVC/AV1/AAC setup, copied configuration, depth admission and microsecond mux clocks");
return 0;
}
@@ -0,0 +1,138 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#ifdef NDEBUG
#error Output ownership checks require assertions
#endif
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "../../../record/record_stream_mediacodec.h"
struct record_mediacodec_encoder { bool audio; };
struct net_rtmp_client { int unused; };
struct net_rtmp_session { int unused; };
static struct net_rtmp_session session;
static bool credit, live, cancelled, closing, network_fail, connecting;
static unsigned messages, releases, retire_delay;
static const void *borrowed;
static const uint8_t csd[] = {0,0,0,1,103,66,0,30,0,0,0,1,104};
static const uint8_t asc[] = {18,16};
static uint8_t aac[] = {1,2,3,4};
static const uint8_t picture[] = {0,0,0,1,101,1,2};
bool record_mediacodec_encoder_video_config(record_mediacodec_encoder_t *s,
unsigned index, const void **data, size_t *size)
{
*data = NULL; *size = 0;
if (!s || s->audio || index) return false;
*data = csd; *size = sizeof(csd); return true;
}
bool record_mediacodec_encoder_audio_config(record_mediacodec_encoder_t *s,
const void **data, size_t *size)
{
*data = NULL; *size = 0;
if (!s || !s->audio) return false;
*data = asc; *size = sizeof(asc); return true;
}
enum net_rtmp_client_state net_rtmp_client_step(net_rtmp_client_t *c,
uint32_t time, net_rtmp_client_view_t *view)
{
(void)c; (void)time;
memset(view, 0, sizeof(*view)); view->session = &session; view->fd = 7;
view->write = live;
if (network_fail) return NET_RTMP_CLIENT_FAILED;
if (connecting) return NET_RTMP_CLIENT_CONNECTING;
return closing ? NET_RTMP_CLIENT_FINISHED : NET_RTMP_CLIENT_READY;
}
void net_rtmp_client_cancel(net_rtmp_client_t *c)
{
(void)c; cancelled = true;
}
bool net_rtmp_session_media(net_rtmp_session_t *s, unsigned type,
uint32_t time, const net_rtmp_span_t *spans, size_t count)
{
(void)s;
assert(count == 2 && (type == 8 || type == 9));
if (live || !credit) return false;
if (messages >= 2) assert(time == 1000);
if (type == 8 && messages >= 2) assert(spans[1].data == aac);
borrowed = spans[1].data;
live = true; messages++; return true;
}
bool net_rtmp_session_media_release(net_rtmp_session_t *s)
{
(void)s;
if (!live) return false;
if (cancelled && retire_delay) { retire_delay--; return false; }
if (!cancelled && !credit) return false;
assert(borrowed); live = false; borrowed = NULL; releases++; return true;
}
bool net_rtmp_session_finish(net_rtmp_session_t *s)
{
(void)s; assert(!live); closing = true; return true;
}
static void run(bool fail, bool audio_enabled)
{
record_mediacodec_encoder_t video = {false}, audio = {true};
struct net_rtmp_client client;
record_mediacodec_stream_config_t config;
record_stream_mediacodec_t *stream;
const record_mediacodec_output_ops_t *ops = record_stream_mediacodec_ops();
record_mediacodec_packet_t packet, other;
net_rtmp_client_view_t wait;
unsigned i;
memset(&config, 0, sizeof(config)); config.max_packet_size = 1024; config.bit_depth = 8;
credit = true; live = cancelled = closing = network_fail = false;
connecting = true; messages = releases = retire_delay = 0;
stream = record_stream_mediacodec_new(&video, audio_enabled ? &audio : NULL, &client, &config);
assert(stream && !ops->start(stream));
assert(ops->track(stream, false, &video));
assert(!ops->track(stream, false, &video));
if (audio_enabled) assert(ops->track(stream, true, &audio));
else assert(!ops->track(stream, true, &audio));
assert(record_stream_mediacodec_step(stream, 0, &wait) == 0);
assert(!ops->start(stream)); connecting = false;
for (i = 0; i < 40 && !ops->start(stream); i++)
assert(record_stream_mediacodec_step(stream, 0, &wait) >= 0);
assert(i < 40 && messages == (audio_enabled ? 2u : 1u));
memset(&packet, 0, sizeof(packet));
packet.pts_us = 1000000; packet.data = audio_enabled ? aac : picture;
packet.size = audio_enabled ? sizeof(aac) : sizeof(picture); packet.flags = 1;
other = packet;
assert(!ops->write(stream, audio_enabled, &packet));
assert(!ops->write(stream, audio_enabled, &other));
for (i = 0; i < 40 && !live; i++)
assert(record_stream_mediacodec_step(stream, 0, &wait) >= 0);
assert(live && i < 40);
credit = false;
for (i = 0; i < 4; i++)
{
assert(record_stream_mediacodec_step(stream, 0, &wait) == 0);
assert(!ops->write(stream, audio_enabled, &packet)); assert(live);
}
if (fail)
{
network_fail = true;
assert(record_stream_mediacodec_step(stream, 0, &wait) == -1);
retire_delay = 2;
assert(!ops->retire(stream) && live);
assert(!record_stream_mediacodec_free(stream) && live);
assert(ops->retire(stream) && !live && cancelled);
}
else
{
credit = true;
assert(record_stream_mediacodec_step(stream, 0, &wait) == 1);
assert(!live && ops->write(stream, audio_enabled, &packet) == 1);
assert(ops->end(stream, false, 2000000));
if (audio_enabled) assert(ops->end(stream, true, 2000000));
assert(ops->finish(stream) == 0);
assert(record_stream_mediacodec_step(stream, 0, &wait) == 2);
assert(ops->finish(stream) == 1);
}
assert(record_stream_mediacodec_free(stream));
assert(messages == releases);
}
int main(void)
{
run(false, true); run(false, false); run(true, true); run(true, false);
puts("PASS: real FLV setup and packet conversion, borrowed AAC, network backpressure, closure and delayed cancellation retirement; transport mocked");
return 0;
}
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Independent AAC-LC packet-cut experiment; not MediaCodec device acceptance.
Requires CC and FFmpeg. Compiles the actual timing policy, then measures encoder
lag from a chirp and compares independently decoded source after silent preroll.
"""
from pathlib import Path
import array,json,math,os,shlex,shutil,subprocess,tempfile
ff=shutil.which('ffmpeg');assert ff
results=[]
def decode(path,channels):
p=subprocess.run([ff,'-v','error','-i',str(path),'-f','f32le','-'],check=True,stdout=subprocess.PIPE)
a=array.array('f');a.frombytes(p.stdout);return a
with tempfile.TemporaryDirectory(prefix='aac-live-prime-') as tmp:
d=Path(tmp)
here=Path(__file__).resolve().parent; root=here.parents[2]
binary=d/('timing.exe' if os.name=='nt' else 'timing')
subprocess.run(shlex.split(os.environ.get('CC','cc'))+['-O1','-std=c89','-pedantic','-Wall','-Wextra','-Werror','-Wno-long-long','-Wframe-larger-than=2048','-I'+str(root/'libretro-common/include'),str(here/'stream_timing_test.c'),str(root/'record/record_aac_stream_timing.c'),'-o',str(binary)],check=True)
timing={int(row[0]):list(map(int,row[1:])) for row in (line.split() for line in subprocess.check_output([str(binary),'--epochs'],text=True).splitlines())}
for rate in (44100,48000):
for channels in (1,2):
# FFmpeg AAC-LC has a measured 1024-frame delay in this experiment only.
# Android delay remains runtime-calibrated; do not promote this constant.
delay=1024; prefix=8192; guard=4096; preroll=2048
source_input,origin,preroll,video_epoch=timing[rate]
source_decoded=source_input+delay
data=array.array('h')
for i in range(source_input+8192):
for c in range(channels):
value=0
if 2048 <= i < 4096:
t=(i-2048)/2048; value=int(12000*math.sin(math.pi*t)**2*math.sin(2*math.pi*(400+300*c)*(i-2048)/rate))
if source_input <= i:
at=i-source_input;value=int(10000*math.sin(2*math.pi*(997+701*c)*at/rate))
data.append(value)
raw=d/'input.pcm';raw.write_bytes(data.tobytes());encoded=d/'full.aac'
subprocess.run([ff,'-v','error','-f','s16le','-ar',str(rate),'-ac',str(channels),'-i',str(raw),'-c:a','aac','-b:a','192k','-f','adts',str(encoded),'-y'],check=True)
packet=[];blob=encoded.read_bytes();at=0
while at<len(blob):
assert blob[at]==255 and blob[at+1]&240==240
size=((blob[at+3]&3)<<11)|(blob[at+4]<<3)|(blob[at+5]>>5)
assert size>=7 and at+size<=len(blob)
packet.append(blob[at:at+size]);at+=size
stream=d/'live.aac';stream.write_bytes(b''.join(packet[origin//1024:]))
ref=decode(encoded,channels);live=decode(stream,channels)
# Recover delay independently from the encoded chirp, rather than trusting
# the encoder-specific input assumption used to construct this trial.
errors=[]
reference=[data[i*channels]/32768 for i in range(2048,4096)]
for candidate in range(4097):
error=sum((ref[(2048+candidate+i)*channels]-reference[i])**2 for i in range(2048))
errors.append(error)
measured=min(range(len(errors)),key=errors.__getitem__)
assert measured==delay,(rate,channels,measured,delay)
assert errors[measured]<sum(x*x for x in reference)*.02
expected=ref[(origin+preroll)*channels:(origin+preroll+8192)*channels]
actual=live[preroll*channels:(preroll+8192)*channels]
assert len(actual)==len(expected)==8192*channels
delta=max(abs(a-b) for a,b in zip(actual,expected))
silence=max(abs(x) for x in live[:1024*channels])
energy=sum(x*x for x in actual)/len(actual)
assert delta < 0.0001,(rate,channels,delta)
assert silence < 0.0001,(rate,channels,silence)
assert energy > .02
# Removing the offset must put the beginning in preroll, not source time.
assert sum(x*x for x in live[:1024*channels])/(1024*channels)<1e-8
results.append(dict(rate=rate,channels=channels,measured_encoder_delay=measured,input_epoch=source_input,decoded_epoch=source_decoded,drop_frames=origin,preroll=preroll,max_source_error=delta,initial_silence_peak=silence,source_energy=energy))
Path('aac-live-priming-experiment.json').write_text(json.dumps(dict(scope='FFmpeg AAC-LC synthetic encoded-packet experiment; not native MediaCodec validation',cases=results),indent=2)+'\n')
print(json.dumps(results))
@@ -0,0 +1,45 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#ifdef NDEBUG
#error Timing checks require assertions
#endif
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "../../../record/record_aac_stream_timing.h"
int main(int argc, char **argv)
{
static const unsigned rates[] = {8000,32000,44100,48000,96000,192000};
record_aac_stream_timing_t t, zero;
unsigned rate, delay;
(void)argv;
memset(&zero, 0, sizeof(zero));
assert(!record_aac_stream_timing(48000, 1024, NULL));
memset(&t, 255, sizeof(t));
assert(!record_aac_stream_timing(0, 1024, &t));
assert(!memcmp(&t, &zero, sizeof(t)));
assert(!record_aac_stream_timing(192001, 1024, &t));
assert(!record_aac_stream_timing(48000, 4097, &t));
for (rate = 0; rate < sizeof(rates) / sizeof(rates[0]); rate++)
for (delay = 0; delay <= 4096; delay++)
{
uint64_t video, audio;
assert(record_aac_stream_timing(rates[rate], delay, &t));
assert(t.input_epoch >= RECORD_AAC_PROBE_FRAMES + RECORD_AAC_STREAM_GUARD
+ RECORD_AAC_STREAM_PREROLL);
assert(t.input_epoch < RECORD_AAC_PROBE_FRAMES + RECORD_AAC_STREAM_GUARD
+ RECORD_AAC_STREAM_PREROLL + 1024);
assert((t.input_epoch + delay) % 1024 == 0);
assert(t.coded_origin % 1024 == 0);
assert(t.input_epoch + delay - t.coded_origin == t.audio_epoch);
assert(t.audio_epoch == 2048);
video = (uint64_t)t.video_epoch_us * rates[rate];
audio = (uint64_t)t.audio_epoch * 1000000;
assert(video >= audio && video - audio < rates[rate]);
if (argc > 1 && delay == 1024 && (rates[rate] == 44100 || rates[rate] == 48000))
printf("%u %u %u %u %u\n", rates[rate], t.input_epoch,
t.coded_origin, t.audio_epoch, t.video_epoch_us);
}
if (argc == 1)
puts("PASS: all 4097 calibrated AAC delays at six rates, source boundary alignment and sub-microsecond video epoch rounding");
return 0;
}
+34 -2
View File
@@ -21,6 +21,8 @@ static const char *test_mime = "video/hevc";
static unsigned hdr_mode, format_fault, normalized_profile;
static unsigned char hdr_saved[25];
static uint8_t payload[16];
static uint8_t video_csd[2][12];
static unsigned csd_fault;
static int allocation_fail;
static void *test_allocate(size_t count, size_t bytes)
{
@@ -182,8 +184,15 @@ static bool get_integer(AMediaFormat *f, const char *key, int32_t *value)
static bool get_buffer(
AMediaFormat *f, const char *key, void **data, size_t *size)
{
assert((f == &output_formats[0] || f == &output_formats[1]) && hdr_mode
&& !strcmp(key, "hdr-static-info"));
assert(f == &output_formats[0] || f == &output_formats[1]);
if (!strcmp(key, "csd-0") || !strcmp(key, "csd-1"))
{
unsigned index = key[4] == '1' ? 1 : 0;
*data = csd_fault == 2 ? NULL : video_csd[index];
*size = csd_fault == 3 ? 0 : csd_fault == 4 ? 65536 : sizeof(video_csd[index]);
return csd_fault != 1;
}
assert(hdr_mode && !strcmp(key, "hdr-static-info"));
*data = format_fault == 15 ? NULL : hdr_saved;
*size = format_fault == 14 ? 24 : 25;
return format_fault != 13;
@@ -339,6 +348,8 @@ int main(void)
record_mediacodec_encoder_t *s;
record_mediacodec_packet_t packet, again;
int i, before;
const void *csd;
size_t csd_bytes;
for (missing = 1; missing <= 22; missing++)
{
assert(!open_encoder());
@@ -359,7 +370,26 @@ int main(void)
assert(s && record_mediacodec_encoder_surface(s) == &window);
output_mode = 0;
assert(record_mediacodec_encoder_pump(s, &packet) == RECORD_MC_BUSY);
assert(!record_mediacodec_encoder_video_config(s, 0, &csd, &csd_bytes)
&& !csd && !csd_bytes);
format_ready(s);
before = output_calls;
for (i = 0; i < 2; i++)
{
assert(record_mediacodec_encoder_video_config(s, (unsigned)i, &csd, &csd_bytes));
assert(csd == video_csd[i] && csd_bytes == sizeof(video_csd[i]));
}
for (csd_fault = 1; csd_fault <= 4; csd_fault++)
assert(!record_mediacodec_encoder_video_config(s, 0, &csd, &csd_bytes)
&& !csd && !csd_bytes);
csd_fault = 0;
assert(!record_mediacodec_encoder_video_config(s, 2, &csd, &csd_bytes)
&& !csd && !csd_bytes);
assert(!record_mediacodec_encoder_video_config(s, 0, NULL, &csd_bytes)
&& !csd_bytes);
assert(!record_mediacodec_encoder_video_config(s, 0, &csd, NULL) && !csd);
assert(output_calls == before && !owned);
assert(record_mediacodec_encoder_video_config(s, 0, &csd, &csd_bytes));
format_ready(s);
output_mode = -2;
assert(record_mediacodec_encoder_pump(s, &packet) == RECORD_MC_PROGRESS);
@@ -516,6 +546,8 @@ int main(void)
before = stops;
stop_fault = i == 1;
assert(record_mediacodec_encoder_stop(s) == !stop_fault);
assert(!record_mediacodec_encoder_video_config(s, 0, &csd,
&csd_bytes) && !csd && !csd_bytes);
assert(stops == before + 1 && codecs == 1 && windows == 1
&& libraries == 2 && formats == 2);
assert(record_mediacodec_encoder_stop(s) == !stop_fault);
@@ -0,0 +1,66 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef RECORD_WORKER_OUTPUT_FIXTURE_H
#define RECORD_WORKER_OUTPUT_FIXTURE_H
struct record_busy_output
{
record_mux_mediacodec_t *mux;
record_mediacodec_packet_t saved[2];
const record_mediacodec_packet_t *borrow[2];
unsigned busy[2], accepted[2], retire_busy, retires;
};
static bool busy_track(void *data, bool audio, void *format)
{
struct record_busy_output *o = (struct record_busy_output*)data;
return record_mux_mediacodec_track(o->mux, audio, format);
}
static int busy_start(void *data)
{
return record_mux_mediacodec_start(((struct record_busy_output*)data)->mux);
}
static int busy_write(void *data, bool audio, const record_mediacodec_packet_t *packet)
{
struct record_busy_output *o = (struct record_busy_output*)data;
unsigned at = audio ? 1 : 0;
if (o->borrow[at])
{
assert(o->borrow[at] == packet && o->saved[at].data == packet->data);
assert(o->saved[at].size == packet->size && o->saved[at].pts_us == packet->pts_us);
assert(o->saved[at].flags == packet->flags);
}
if (o->busy[at])
{
o->borrow[at] = packet;
o->saved[at] = *packet;
o->busy[at]--;
return 0;
}
if (!record_mux_mediacodec_write(o->mux, audio, packet))
return -1;
o->borrow[at] = NULL;
o->accepted[at]++;
return 1;
}
static bool busy_end(void *data, bool audio, int64_t end_us)
{
return record_mux_mediacodec_end(((struct record_busy_output*)data)->mux, audio, end_us);
}
static int busy_finish(void *data)
{
return record_mux_mediacodec_finish(((struct record_busy_output*)data)->mux);
}
static bool busy_retire(void *data)
{
struct record_busy_output *o = (struct record_busy_output*)data;
o->retires++;
if (o->retire_busy)
{
o->retire_busy--;
return false;
}
o->borrow[0] = o->borrow[1] = NULL;
return true;
}
static const record_mediacodec_output_ops_t busy_ops = {
busy_track, busy_start, busy_write, busy_end, busy_finish, busy_retire
};
#endif
@@ -165,6 +165,8 @@ int record_mux_mediacodec_finish(record_mux_mediacodec_t *m)
return 1;
}
#include "worker_output_fixture.h"
static void exercise(unsigned video_delay, unsigned audio_delay,
unsigned channels, unsigned failure, bool late_close)
{
@@ -237,6 +239,61 @@ static void exercise(unsigned video_delay, unsigned audio_delay,
}
}
static void output_backpressure(bool cancel)
{
record_mediacodec_encoder_t video;
record_mux_mediacodec_t mux;
struct record_busy_output output;
record_mediacodec_output_ops_t ops = busy_ops;
record_mediacodec_worker_t *w;
unsigned i, pumps;
int result = 0;
memset(&video, 0, sizeof(video));
memset(&mux, 0, sizeof(mux));
memset(&output, 0, sizeof(output));
fault = 0;
output.mux = &mux;
output.busy[0] = cancel ? 20 : 4;
output.retire_busy = 2;
w = record_mediacodec_worker_new_output(&video, NULL, NULL, &ops, &output, 0, 0);
assert(w);
memset(&ops, 0, sizeof(ops)); /* Worker owns its callback-table copy. */
assert(record_mediacodec_worker_finish(w, 33333, 0));
assert(record_mediacodec_worker_step(w) == RECORD_MC_WORKER_PROGRESS);
assert(record_mediacodec_worker_step(w) != RECORD_MC_WORKER_ERROR);
assert(video.borrowed && !video.releases && output.borrow[0]);
pumps = video.pumps;
for (i = 0; i < 3; i++)
{
assert(record_mediacodec_worker_step(w) != RECORD_MC_WORKER_ERROR);
assert(video.pumps == pumps && !video.releases && video.borrowed);
}
if (cancel)
{
record_mediacodec_worker_abort(w);
assert(record_mediacodec_worker_step(w) == RECORD_MC_WORKER_ERROR);
}
else
{
for (i = 0; i < 32; i++)
{
result = record_mediacodec_worker_step(w);
if (result == RECORD_MC_WORKER_END || result == RECORD_MC_WORKER_ERROR)
break;
}
assert(result == RECORD_MC_WORKER_END && video.releases == 2);
assert(output.accepted[0] == 2 && !output.borrow[0]);
}
assert(!record_mediacodec_worker_free(w));
if (cancel)
assert(output.borrow[0]->data == &video.data && video.borrowed && !video.releases);
assert(!record_mediacodec_worker_free(w));
assert(record_mediacodec_worker_free(w) && output.retires == 3);
assert(!output.borrow[0]);
puts(cancel ? "PASS worker abort retains output borrow through delayed retirement"
: "PASS worker output BUSY retains immutable packet without repump or early release");
}
static void contracts(void)
{
record_mediacodec_encoder_t video, audio;
@@ -292,6 +349,8 @@ int main(void)
for (failure = 1; failure <= 13; failure++)
exercise(0, 4, 2, failure, false);
contracts();
output_backpressure(false);
output_backpressure(true);
assert(record_mediacodec_worker_free(NULL));
record_mediacodec_worker_abort(NULL);
puts("PASS: Android recording worker PCM backpressure, mux startup and drain");