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
35 changed files with 1305 additions and 556 deletions

No files matched your search

@@ -81,10 +81,6 @@ on:
- 'libretro-common/include/retro_atomic.h'
workflow_dispatch:
inputs:
ios_sdk_only:
description: 'Audit production VideoToolbox declarations against real iOS SDKs'
type: boolean
default: false
native_aac_only:
description: 'Run native AudioToolbox AAC acceptance without GPU/frontend jobs'
type: boolean
@@ -97,7 +93,7 @@ permissions:
contents: read
jobs:
stream-native:
if: ${{ !inputs.ios_sdk_only && !inputs.native_aac_only && !inputs.frontend_stream_only }}
if: ${{ !inputs.native_aac_only && !inputs.frontend_stream_only }}
runs-on: macos-latest
timeout-minutes: 10
steps:
@@ -156,7 +152,7 @@ jobs:
${{ runner.temp }}/vt-metal-dispatch-mrc
if-no-files-found: ignore
aac-native:
if: ${{ !inputs.ios_sdk_only && (inputs.native_aac_only || !inputs.frontend_stream_only) }}
if: ${{ inputs.native_aac_only || !inputs.frontend_stream_only }}
runs-on: macos-latest
timeout-minutes: 10
steps:
@@ -188,7 +184,7 @@ jobs:
path: ${{ runner.temp }}/audiotoolbox-src-evidence
if-no-files-found: ignore
encode:
if: ${{ !inputs.ios_sdk_only && !inputs.native_aac_only && !inputs.frontend_stream_only }}
if: ${{ !inputs.native_aac_only && !inputs.frontend_stream_only }}
runs-on: macos-latest
timeout-minutes: 20
steps:
@@ -289,7 +285,7 @@ jobs:
if-no-files-found: warn
frontend-build:
if: ${{ !inputs.ios_sdk_only && !inputs.native_aac_only && !inputs.frontend_stream_only }}
if: ${{ !inputs.native_aac_only && !inputs.frontend_stream_only }}
runs-on: macos-latest
timeout-minutes: 40
steps:
@@ -326,7 +322,7 @@ jobs:
if-no-files-found: warn
frontend-stream-build:
if: ${{ !inputs.ios_sdk_only && !inputs.native_aac_only }}
if: ${{ !inputs.native_aac_only }}
runs-on: macos-latest
timeout-minutes: 20
strategy:
@@ -376,46 +372,9 @@ jobs:
if-no-files-found: ignore
frontend-contracts:
if: ${{ !inputs.ios_sdk_only && !inputs.native_aac_only }}
if: ${{ !inputs.native_aac_only }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Source-clock capture and backpressure under sanitizers
run: make -C samples/record/metal_frontend check SANITIZER=address,undefined
ios-sdk:
if: ${{ inputs.ios_sdk_only }}
runs-on: macos-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- name: Compile production VideoToolbox headers for iOS and simulator
run: python3 samples/record/videotoolbox_encode/ios_sdk_check.py --output-dir "${{ runner.temp }}/vt-ios-sdk"
- name: Save actual SDK diagnostics
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: vt-ios-sdk
path: ${{ runner.temp }}/vt-ios-sdk
if-no-files-found: error
ios-app:
if: ${{ inputs.ios_sdk_only }}
needs: ios-sdk
runs-on: macos-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v6
- name: Build and link actual iOS application with native recording
run: python3 samples/record/videotoolbox_encode/ios_app_check.py --output-dir "${{ runner.temp }}/vt-ios-app"
- name: Save full application build evidence
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: vt-ios-app
path: |
${{ runner.temp }}/vt-ios-app/*.json
${{ runner.temp }}/vt-ios-app/*.log
${{ runner.temp }}/vt-ios-app/*.txt
${{ runner.temp }}/vt-ios-app/build.xcresult
if-no-files-found: error
+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 \
+5 -38
View File
@@ -9,8 +9,6 @@
#else
#define VT_ENCODE_AVAILABLE
#endif
#include "videotoolbox_encode_hardware.h"
typedef struct videotoolbox_encode_config
{
unsigned width, height, slots, bitrate, key_interval, timescale, bit_depth;
@@ -59,15 +57,7 @@ static INLINE VT_ENCODE_AVAILABLE videotoolbox_encode_t *videotoolbox_encode_new
videotoolbox_encode_t *encoder = NULL;
CFMutableDictionaryRef spec = NULL, attrs = NULL, props = NULL;
CFTypeRef hardware = NULL, selected_id = NULL;
CFStringRef admitted_id = NULL;
#ifdef VT_ENCODE_MOBILE
bool query_hardware = false;
#endif
OSStatus status = kVTParameterErr;
#ifdef VT_ENCODE_MOBILE
if (VT_ENCODE_HARDWARE_KEYS_AVAILABLE)
query_hardware = true;
#endif
if (error) *error = status;
if ((encoder_id && !CFStringGetLength(encoder_id)) || !config || !config->width || !config->height || (config->width & 1)
|| (config->height & 1) || config->width > 16384 || config->height > 16384
@@ -78,20 +68,6 @@ static INLINE VT_ENCODE_AVAILABLE videotoolbox_encode_t *videotoolbox_encode_new
|| (config->bit_depth != 8 && config->bit_depth != 10)
|| (config->bit_depth == 10 && !config->hevc)
|| (config->hdr && config->bit_depth != 10)) return NULL;
#ifdef VT_ENCODE_MOBILE
if (!query_hardware)
{
admitted_id = videotoolbox_encode_hardware_id(encoder_id,
config->hevc ? kCMVideoCodecType_HEVC : kCMVideoCodecType_H264);
if (!admitted_id)
{
if (error)
*error = kVTCouldNotFindVideoEncoderErr;
return NULL;
}
encoder_id = admitted_id;
}
#endif
encoder = (videotoolbox_encode_t*)calloc(1, sizeof(*encoder));
spec = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
attrs = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
@@ -100,10 +76,7 @@ static INLINE VT_ENCODE_AVAILABLE videotoolbox_encode_t *videotoolbox_encode_new
encoder->config = *config;
encoder->pool = videotoolbox_encode_slots_new(config->slots);
if (!encoder->pool) { status = kVTAllocationFailedErr; goto done; }
#ifdef VT_ENCODE_HAS_HARDWARE_KEYS
if (VT_ENCODE_HARDWARE_KEYS_AVAILABLE)
CFDictionarySetValue(spec, kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder, kCFBooleanTrue);
#endif
CFDictionarySetValue(spec, kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder, kCFBooleanTrue);
if (encoder_id) CFDictionarySetValue(spec,kVTVideoEncoderSpecification_EncoderID,encoder_id);
if (gpu_registry_id)
{
@@ -159,15 +132,10 @@ static INLINE VT_ENCODE_AVAILABLE videotoolbox_encode_t *videotoolbox_encode_new
if (status) goto done;
status = VTCompressionSessionPrepareToEncodeFrames(encoder->session);
if (status) goto done;
#ifdef VT_ENCODE_HAS_HARDWARE_KEYS
if (VT_ENCODE_HARDWARE_KEYS_AVAILABLE)
{
status = VTSessionCopyProperty(encoder->session,
kVTCompressionPropertyKey_UsingHardwareAcceleratedVideoEncoder, NULL, &hardware);
if (!status && (!hardware || CFGetTypeID(hardware) != CFBooleanGetTypeID()
|| !CFBooleanGetValue((CFBooleanRef)hardware))) status = kVTCouldNotFindVideoEncoderErr;
}
#endif
status = VTSessionCopyProperty(encoder->session,
kVTCompressionPropertyKey_UsingHardwareAcceleratedVideoEncoder, NULL, &hardware);
if (!status && (!hardware || CFGetTypeID(hardware) != CFBooleanGetTypeID()
|| !CFBooleanGetValue((CFBooleanRef)hardware))) status = kVTCouldNotFindVideoEncoderErr;
if (!status && encoder_id)
{
status=VTSessionCopyProperty(encoder->session,kVTCompressionPropertyKey_EncoderID,NULL,&selected_id);
@@ -175,7 +143,6 @@ static INLINE VT_ENCODE_AVAILABLE videotoolbox_encode_t *videotoolbox_encode_new
|| !CFEqual(selected_id,encoder_id))) status=kVTCouldNotFindVideoEncoderErr;
}
done:
if (admitted_id) CFRelease((CFTypeRef)admitted_id);
if (selected_id) CFRelease(selected_id);
if (hardware) CFRelease(hardware);
if (props) CFRelease(props);
-85
View File
@@ -1,85 +0,0 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef RETROARCH_VIDEOTOOLBOX_ENCODE_HARDWARE_H
#define RETROARCH_VIDEOTOOLBOX_ENCODE_HARDWARE_H
#if defined(__APPLE__) && !defined(VIDEOTOOLBOX_ENCODE_TEST_TYPES)
#include <TargetConditionals.h>
#if TARGET_OS_IPHONE
#define VT_ENCODE_MOBILE
#endif
#endif
#if !defined(VT_ENCODE_MOBILE) \
|| (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 170400) \
|| (defined(__TV_OS_VERSION_MAX_ALLOWED) && __TV_OS_VERSION_MAX_ALLOWED >= 170400) \
|| (defined(__VISION_OS_VERSION_MAX_ALLOWED) && __VISION_OS_VERSION_MAX_ALLOWED >= 10100)
#define VT_ENCODE_HAS_HARDWARE_KEYS
#endif
#if defined(VT_ENCODE_MOBILE) && defined(VT_ENCODE_HAS_HARDWARE_KEYS)
#define VT_ENCODE_HARDWARE_KEYS_AVAILABLE \
__builtin_available(iOS 17.4, tvOS 17.4, macCatalyst 17.4, visionOS 1.1, *)
#elif defined(VT_ENCODE_HAS_HARDWARE_KEYS)
#define VT_ENCODE_HARDWARE_KEYS_AVAILABLE true
#else
#define VT_ENCODE_HARDWARE_KEYS_AVAILABLE false
#endif
#if defined(VT_ENCODE_MOBILE) || defined(VIDEOTOOLBOX_ENCODE_HARDWARE_LIST_TEST)
/* Cold admission only. Return a retained, advertised hardware encoder ID;
* the caller must independently confirm the session selected that same ID. */
static INLINE CFStringRef videotoolbox_encode_hardware_id(
CFStringRef requested, uint32_t codec_type)
{
#if defined(VIDEOTOOLBOX_ENCODE_HARDWARE_LIST_TEST) \
|| (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000) \
|| (defined(__TV_OS_VERSION_MAX_ALLOWED) && __TV_OS_VERSION_MAX_ALLOWED >= 130000) \
|| defined(__VISION_OS_VERSION_MAX_ALLOWED)
#ifndef VIDEOTOOLBOX_ENCODE_HARDWARE_LIST_TEST
if (__builtin_available(iOS 13.0, tvOS 13.0, *))
#endif
{
CFArrayRef list = NULL;
CFStringRef selected = NULL;
CFIndex i, count;
OSStatus status = VTCopyVideoEncoderList(NULL, &list);
if (status || !list)
{
if (list)
CFRelease(list);
return NULL;
}
count = CFArrayGetCount(list);
if (count >= 0 && count <= 4096)
for (i = 0; i < count; i++)
{
CFDictionaryRef entry = (CFDictionaryRef)CFArrayGetValueAtIndex(list, i);
CFTypeRef hardware, codec, identifier;
int64_t value = 0;
if (!entry || CFGetTypeID(entry) != CFDictionaryGetTypeID())
continue;
hardware = CFDictionaryGetValue(entry, kVTVideoEncoderList_IsHardwareAccelerated);
codec = CFDictionaryGetValue(entry, kVTVideoEncoderList_CodecType);
identifier = CFDictionaryGetValue(entry, kVTVideoEncoderList_EncoderID);
if (!hardware || CFGetTypeID(hardware) != CFBooleanGetTypeID()
|| !CFBooleanGetValue((CFBooleanRef)hardware)
|| !codec || CFGetTypeID(codec) != CFNumberGetTypeID()
|| !CFNumberGetValue((CFNumberRef)codec, kCFNumberSInt64Type, &value)
|| value != (int64_t)codec_type
|| !identifier || CFGetTypeID(identifier) != CFStringGetTypeID()
|| !CFStringGetLength((CFStringRef)identifier)
|| (requested && !CFEqual(identifier, requested)))
continue;
selected = (CFStringRef)CFRetain(identifier);
break;
}
CFRelease(list);
return selected;
}
#endif
(void)requested;
(void)codec_type;
return NULL;
}
#endif
#endif
+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"
+1 -1
View File
@@ -7,7 +7,7 @@
// The cleanroom network stack, as on the other platforms: the crypto,
// the encrypted keychain, the TLS 1.2/1.3 client and the SMB2/3 client
// with Kerberos, plus the NFSv3/v4 client.
NETWORK_STACK_FLAGS = -DHAVE_RTMP -DHAVE_RFLV -DHAVE_CRYPTO -DHAVE_KEYCHAIN -DHAVE_RETROSSL -DHAVE_SMBCLIENT -DHAVE_RETROSMB -DHAVE_NFSCLIENT -DHAVE_RETRONFS
NETWORK_STACK_FLAGS = -DHAVE_CRYPTO -DHAVE_KEYCHAIN -DHAVE_RETROSSL -DHAVE_SMBCLIENT -DHAVE_RETROSMB -DHAVE_NFSCLIENT -DHAVE_RETRONFS
OTHER_CFLAGS = $(inherited) $(NETWORK_STACK_FLAGS)
+2 -2
View File
@@ -3,8 +3,8 @@
#define RECORD_METAL_H
#include "../record_driver.h"
#ifdef __APPLE__
#include "../record_metal_platform.h"
#if defined(RECORD_METAL_NATIVE_SDK) && defined(HAVE_METAL) && defined(HAVE_AVF) \
#include <TargetConditionals.h>
#if TARGET_OS_OSX && defined(HAVE_METAL) && defined(HAVE_AVF) \
&& defined(HAVE_THREADS) && defined(HAVE_CONFIGFILE)
#define HAVE_RECORD_METAL
#endif
+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
+1 -7
View File
@@ -15,12 +15,11 @@
#undef RECORD_METAL_DISPATCH_UNDEF_INLINE
#endif
#include "record_gpu_dispatch_metal.h"
#include "record_metal_platform.h"
#include <retro_atomic.h>
#include <defines/cocoa_defines.h>
#include <stdlib.h>
#include <string.h>
#if defined(RECORD_METAL_NATIVE_SDK) && defined(HAVE_THREADS) && defined(RETRO_ATOMIC_LOCK_FREE)
#if TARGET_OS_OSX && defined(HAVE_THREADS) && defined(RETRO_ATOMIC_LOCK_FREE)
#include <pthread.h>
#if !defined(__cplusplus) && (!defined(__STDC_VERSION__) || __STDC_VERSION__<199901L) && !defined(inline)
#define inline __inline__
@@ -448,11 +447,6 @@ static record_gpu_dispatch_metal_t *record_metal_dispatch_new(void *device,
id<MTLDevice> metal=(__bridge id<MTLDevice>)device;
size_t length;
int error;
if (RECORD_METAL_NATIVE_RUNTIME)
{
}
else
return NULL;
if (!metal || !config || (!output && !stream)
|| (output && output->video_format) || !path || !*path) return NULL;
#ifndef RECORD_METAL_STREAM
+10 -22
View File
@@ -17,22 +17,16 @@
#endif
#include <string.h>
#include "record_gpu_session_metal.h"
#include "record_metal_platform.h"
#include "record_avfoundation_session_native.h"
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && defined(RECORD_METAL_NATIVE_SDK)
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && TARGET_OS_OSX \
&& defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED>=101400
/* Enumerated IDs prevent silent default/software/other-GPU selection. A
* hardware encoder with no advertised GPU is a separate explicit category,
* as used by the hosted paravirtualized media engines. It is not evidence of
* physical same-GPU encoding; CVPixelBuffer/Metal interop remains gated. */
static RECORD_METAL_NATIVE_AVAILABLE videotoolbox_encode_t *record_metal_select_encoder(
static __attribute__((availability(macos,introduced=10.14))) videotoolbox_encode_t *record_metal_select_encoder(
const videotoolbox_encode_config_t *config,uint64_t renderer_gpu,OSStatus *error)
{
#if TARGET_OS_IPHONE
/* Mobile admission verifies hardware and the selected ID in the VT adapter.
* CVPixelBuffer/Metal compatibility is still checked by capture creation. */
(void)renderer_gpu;
return videotoolbox_encode_new(config, error);
#else
CFArrayRef list=NULL;
CFIndex i,count;
unsigned priority;
@@ -68,7 +62,6 @@ static RECORD_METAL_NATIVE_AVAILABLE videotoolbox_encode_t *record_metal_select_
}
done:
CFRelease(list); *error=status; return encoder;
#endif
}
#endif
record_avfoundation_session_t *record_gpu_session_metal_new_output(void *device,
@@ -80,8 +73,8 @@ record_avfoundation_session_t *record_gpu_session_metal_new_output(void *device,
if (queue_out) *queue_out=NULL;
if (stage) *stage=RECORD_METAL_STARTUP_UNSUPPORTED;
if (native_status) *native_status=0;
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && defined(RECORD_METAL_NATIVE_SDK)
if (RECORD_METAL_NATIVE_RUNTIME)
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && TARGET_OS_OSX \
&& defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED>=101400
@autoreleasepool
{
videotoolbox_encode_config_t config;
@@ -101,12 +94,8 @@ record_avfoundation_session_t *record_gpu_session_metal_new_output(void *device,
|| !c->width || !c->height || c->width>16384 || c->height>16384
|| ((c->width|c->height)&1) || c->slots<2 || c->slots>VIDEOTOOLBOX_ENCODE_SLOTS
|| c->mode>2 || (c->mode && c->bit_depth!=10)) goto done;
#if TARGET_OS_IPHONE
gpu = 0;
#else
gpu=[(__bridge id<MTLDevice>)device registryID];
if (!gpu) goto done;
#endif
config.width=c->width; config.height=c->height; config.slots=c->slots;
config.bitrate=c->bitrate; config.key_interval=c->key_interval; config.timescale=c->timescale;
config.bit_depth=c->bit_depth; config.hevc=c->hevc; config.hdr=c->mode!=0;
@@ -139,20 +128,20 @@ done:
if (native_status) *native_status=(int32_t)status;
return session;
}
return NULL;
#else
(void)device; (void)c; (void)bind; (void)context;
return NULL;
#endif
}
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && defined(RECORD_METAL_NATIVE_SDK)
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && TARGET_OS_OSX \
&& defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED>=101400
typedef struct record_metal_file_binding
{
const record_mux_avfoundation_config_t *output;
const char *path;
size_t audio_capacity;
} record_metal_file_binding_t;
static RECORD_METAL_NATIVE_AVAILABLE record_avfoundation_session_t *record_metal_file_bind(
static __attribute__((availability(macos,introduced=10.14))) record_avfoundation_session_t *record_metal_file_bind(
void *data,record_gpu_queue_t *queue,record_gpu_capture_metal_t *capture,
struct videotoolbox_encode *encoder,unsigned width,unsigned height)
{
@@ -170,8 +159,8 @@ record_avfoundation_session_t *record_gpu_session_metal_new(void *device,
if (queue_out) *queue_out=NULL;
if (stage) *stage=RECORD_METAL_STARTUP_UNSUPPORTED;
if (native_status) *native_status=0;
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && defined(RECORD_METAL_NATIVE_SDK)
if (RECORD_METAL_NATIVE_RUNTIME)
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && TARGET_OS_OSX \
&& defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED>=101400
{
record_metal_file_binding_t binding;
if (!c || !output || output->video_format || c->timescale!=output->timescale || !path || !*path)
@@ -183,7 +172,6 @@ record_avfoundation_session_t *record_gpu_session_metal_new(void *device,
return record_gpu_session_metal_new_output(device,c,record_metal_file_bind,&binding,
capture_out,queue_out,stage,native_status);
}
return NULL;
#else
(void)device; (void)c; (void)output; (void)audio_capacity_bytes; (void)path;
return NULL;
+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
-21
View File
@@ -1,21 +0,0 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef RECORD_METAL_PLATFORM_H
#define RECORD_METAL_PLATFORM_H
#include <TargetConditionals.h>
#include <Availability.h>
#if (TARGET_OS_OSX && defined(__MAC_OS_X_VERSION_MAX_ALLOWED) \
&& __MAC_OS_X_VERSION_MAX_ALLOWED >= 101400) \
|| (TARGET_OS_IPHONE && defined(__IPHONE_OS_VERSION_MAX_ALLOWED) \
&& __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000) \
|| (TARGET_OS_TV && defined(__TV_OS_VERSION_MAX_ALLOWED) \
&& __TV_OS_VERSION_MAX_ALLOWED >= 130000)
#define RECORD_METAL_NATIVE_SDK
#endif
#define RECORD_METAL_NATIVE_AVAILABLE \
__attribute__((availability(macos,introduced=10.14), \
availability(ios,introduced=13.0), availability(tvos,introduced=13.0)))
#define RECORD_METAL_NATIVE_RUNTIME \
__builtin_available(macOS 10.14, iOS 13.0, tvOS 13.0, *)
#endif
+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");
@@ -26,6 +26,3 @@
/session_gpu_refusal_test
/session_gpu_refusal_test.exe
/hardware_id_test
/hardware_id_test.exe
+3 -6
View File
@@ -9,7 +9,7 @@ ifneq ($(SANITIZER),)
CFLAGS += -fsanitize=$(SANITIZER) -fno-omit-frame-pointer
LDFLAGS += -fsanitize=$(SANITIZER)
endif
all: hardware_id_test$(EXE) av_output_test$(EXE) audio_lend_test$(EXE) packet_test$(EXE) session_gpu_refusal_test$(EXE) av_session_test$(EXE) crop_test$(EXE) av_worker_test$(EXE) audio_queue_test$(EXE) slots_test$(EXE) session_test$(EXE) worker_test$(EXE) audio_rate_test$(EXE) audio_rate_native_test$(EXE)
all: av_output_test$(EXE) audio_lend_test$(EXE) packet_test$(EXE) session_gpu_refusal_test$(EXE) av_session_test$(EXE) crop_test$(EXE) av_worker_test$(EXE) audio_queue_test$(EXE) slots_test$(EXE) session_test$(EXE) worker_test$(EXE) audio_rate_test$(EXE) audio_rate_native_test$(EXE)
av_output_test$(EXE): av_output_test.c av_worker_test.c $(RA)/record/record_avfoundation_session.c $(RA)/record/record_avfoundation_session.h
$(CC) $(CFLAGS) -Wno-unused-function -o $@ av_output_test.c $(RA)/record/record_avfoundation_session.c $(RA)/record/record_avfoundation_worker.c $(RA)/record/record_gpu_worker_vt.c $(RA)/record/record_gpu_queue.c $(RA)/record/record_audio_queue.c $(RA)/libretro-common/queues/retro_spsc.c $(LDFLAGS)
audio_lend_test$(EXE): audio_lend_test.c $(RA)/record/record_audio_queue.c $(RA)/record/record_audio_queue.h
@@ -34,11 +34,9 @@ crop_test$(EXE): crop_test.c $(RA)/libretro-common/formats/h265/rh265_sps_crop.c
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
worker_test$(EXE): worker_test.c $(RA)/record/record_gpu_worker_vt.c $(RA)/record/record_gpu_worker_vt.h $(RA)/record/record_gpu_queue.c
$(CC) $(CFLAGS) -o $@ worker_test.c $(RA)/record/record_gpu_worker_vt.c $(RA)/record/record_gpu_queue.c $(RA)/libretro-common/queues/retro_spsc.c $(LDFLAGS)
session_gpu_refusal_test$(EXE): session_test.c $(RA)/gfx/common/videotoolbox_encode.h $(RA)/gfx/common/videotoolbox_encode_hardware.h
session_gpu_refusal_test$(EXE): session_test.c $(RA)/gfx/common/videotoolbox_encode.h
$(CC) $(CFLAGS) -DVIDEOTOOLBOX_ENCODE_TEST_NO_GPU_SELECTION -o $@ $< $(LDFLAGS)
session_test$(EXE): session_test.c $(RA)/gfx/common/videotoolbox_encode.h $(RA)/gfx/common/videotoolbox_encode_slots.h $(RA)/gfx/common/videotoolbox_encode_hardware.h
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
hardware_id_test$(EXE): hardware_id_test.c $(RA)/gfx/common/videotoolbox_encode_hardware.h
session_test$(EXE): session_test.c $(RA)/gfx/common/videotoolbox_encode.h $(RA)/gfx/common/videotoolbox_encode_slots.h
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
slots_test$(EXE): slots_test.c $(RA)/gfx/common/videotoolbox_encode_slots.h
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
@@ -60,7 +58,6 @@ check: all legacy-check
$(RUNNER) ./worker_test$(EXE)
$(RUNNER) ./audio_rate_test$(EXE)
clean:
rm -f hardware_id_test hardware_id_test.exe
rm -f av_output_test av_output_test.exe
rm -f audio_lend_test audio_lend_test.exe
rm -f packet_test packet_test.exe
@@ -1,149 +0,0 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <boolean.h>
#include <retro_inline.h>
typedef struct object object_t;
typedef object_t *CFTypeRef;
typedef object_t *CFArrayRef;
typedef object_t *CFDictionaryRef;
typedef object_t *CFStringRef;
typedef object_t *CFNumberRef;
typedef object_t *CFBooleanRef;
typedef long CFIndex;
typedef int OSStatus;
struct object
{
unsigned type, refs;
int64_t number;
const char *text;
object_t *values[3];
};
#define kVTVideoEncoderList_IsHardwareAccelerated 0
#define kVTVideoEncoderList_CodecType 1
#define kVTVideoEncoderList_EncoderID 2
#define kCFNumberSInt64Type 1
static object_t list, entry, hardware, codec, identifier, requested;
static CFIndex count = 1;
static OSStatus list_status;
static unsigned visits;
static CFTypeRef CFRetain(CFTypeRef object)
{
assert(object && object->refs);
object->refs++;
return object;
}
static void CFRelease(CFTypeRef object)
{
assert(object && object->refs > 1);
object->refs--;
}
static OSStatus VTCopyVideoEncoderList(void *options, CFArrayRef *output)
{
(void)options;
*output = CFRetain(&list);
return list_status;
}
static CFIndex CFArrayGetCount(CFArrayRef array)
{
assert(array == &list);
return count;
}
static CFTypeRef CFArrayGetValueAtIndex(CFArrayRef array, CFIndex index)
{
assert(array == &list && index == 0);
visits++;
return &entry;
}
static unsigned CFGetTypeID(CFTypeRef object)
{
return object->type;
}
static unsigned CFDictionaryGetTypeID(void)
{
return 1;
}
static unsigned CFBooleanGetTypeID(void)
{
return 2;
}
static unsigned CFNumberGetTypeID(void)
{
return 3;
}
static unsigned CFStringGetTypeID(void)
{
return 4;
}
static CFTypeRef CFDictionaryGetValue(CFDictionaryRef object, unsigned key)
{
assert(object == &entry && key < 3);
return object->values[key];
}
static bool CFBooleanGetValue(CFBooleanRef value)
{
return value->number != 0;
}
static bool CFNumberGetValue(CFNumberRef value, unsigned type, void *output)
{
assert(type == kCFNumberSInt64Type);
*(int64_t*)output = value->number;
return true;
}
static unsigned CFStringGetLength(CFStringRef value)
{
return (unsigned)strlen(value->text);
}
static bool CFEqual(CFTypeRef first, CFStringRef second)
{
return !strcmp(first->text, second->text);
}
#define VIDEOTOOLBOX_ENCODE_HARDWARE_LIST_TEST
#include "../../../gfx/common/videotoolbox_encode_hardware.h"
int main(void)
{
CFStringRef selected;
list.refs = entry.refs = hardware.refs = codec.refs = identifier.refs = requested.refs = 1;
entry.type = 1; hardware.type = 2; codec.type = 3; identifier.type = requested.type = 4;
hardware.number = 1; codec.number = 7;
identifier.text = requested.text = "hardware-encoder";
entry.values[0] = &hardware;
entry.values[1] = &codec;
entry.values[2] = &identifier;
selected = videotoolbox_encode_hardware_id(NULL, 7);
assert(selected == &identifier && identifier.refs == 2 && list.refs == 1);
CFRelease(selected);
selected = videotoolbox_encode_hardware_id(&requested, 7);
assert(selected == &identifier);
CFRelease(selected);
requested.text = "other-encoder";
assert(!videotoolbox_encode_hardware_id(&requested, 7));
assert(!videotoolbox_encode_hardware_id(NULL, 8));
hardware.number = 0;
assert(!videotoolbox_encode_hardware_id(NULL, 7));
hardware.number = 1;
hardware.type = 3;
assert(!videotoolbox_encode_hardware_id(NULL, 7));
hardware.type = 2;
identifier.text = "";
assert(!videotoolbox_encode_hardware_id(NULL, 7));
identifier.text = "hardware-encoder";
entry.values[2] = NULL;
assert(!videotoolbox_encode_hardware_id(NULL, 7));
entry.values[2] = &identifier;
list_status = -1;
assert(!videotoolbox_encode_hardware_id(NULL, 7) && list.refs == 1);
list_status = 0;
visits = 0;
count = 4097;
assert(!videotoolbox_encode_hardware_id(NULL, 7) && !visits);
count = -1;
assert(!videotoolbox_encode_hardware_id(NULL, 7) && !visits);
assert(identifier.refs == 1 && list.refs == 1);
puts("PASS advertised hardware encoder ID, codec admission, malformed lists and retained ownership");
return 0;
}
@@ -1,59 +0,0 @@
#!/usr/bin/env python3
"""Build and link the production iOS application; never report device acceptance."""
import argparse
import json
from pathlib import Path
import shlex
import subprocess
import sys
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output-dir', type=Path, required=True)
args = parser.parse_args()
if sys.platform != 'darwin':
parser.error('full iOS application linking requires macOS/Xcode')
root = Path(__file__).resolve().parents[3]
output = args.output_dir.resolve()
output.mkdir(parents=True, exist_ok=True)
common = ['xcodebuild', '-project', 'pkg/apple/RetroArch_iOS13.xcodeproj',
'-scheme', 'RetroArch iOS Release', '-configuration', 'Release',
'-destination', 'generic/platform=iOS', '-xcconfig', 'pkg/apple/GitHubCI.xcconfig',
'-derivedDataPath', str(output / 'derived'),
'CODE_SIGNING_ALLOWED=NO', 'CODE_SIGNING_REQUIRED=NO',
'CODE_SIGN_IDENTITY=', 'PROVISIONING_PROFILE_SPECIFIER=']
settings = subprocess.run(common + ['-showBuildSettings', '-json'], cwd=root,
check=True, capture_output=True, text=True, timeout=180)
(output / 'build-settings.json').write_text(settings.stdout)
rows = json.loads(settings.stdout)
main_target = next(row['buildSettings'] for row in rows if row['target'] == 'RetroArchiOS')
flags = shlex.split(main_target['OTHER_CFLAGS'])
defines = {flag[2:].split('=', 1)[0] for flag in flags if flag.startswith('-D')}
required = {'HAVE_METAL', 'HAVE_AVF', 'HAVE_THREADS', 'HAVE_CONFIGFILE',
'HAVE_NETWORKING', 'HAVE_CRYPTO', 'HAVE_RTMP', 'HAVE_RFLV'}
assert required <= defines, sorted(required - defines)
assert 'HAVE_FFMPEG' not in defines
command = common + ['-resultBundlePath', str(output / 'build.xcresult'), 'build']
report = dict(command=command, required_defines=sorted(required),
scope='Full unsigned device application build/link only; no physical playback or recording acceptance',
result='FAIL')
(output / 'results.json').write_text(json.dumps(report, indent=2) + '\n')
with (output / 'xcodebuild.log').open('w') as log:
result = subprocess.run(command, cwd=root, stdout=log, stderr=subprocess.STDOUT, timeout=3000)
report['exit_code'] = result.returncode
if not result.returncode:
executable = output / 'derived/Build/Products/Release-iphoneos/RetroArch.app/RetroArch'
assert executable.is_file() and executable.stat().st_size > 0
linked = subprocess.run(['otool', '-L', str(executable)], check=True,
capture_output=True, text=True, timeout=60).stdout
(output / 'linked-libraries.txt').write_text(linked)
assert not any(name in linked.lower() for name in ('libavcodec', 'libavformat', 'libavutil', 'libswscale'))
report.update(result='PASS', executable_bytes=executable.stat().st_size)
(output / 'results.json').write_text(json.dumps(report, indent=2) + '\n')
print(report['result'], report['scope'], flush=True)
return int(bool(result.returncode))
if __name__ == '__main__':
sys.exit(main())
@@ -1,67 +0,0 @@
#!/usr/bin/env python3
"""Compile production VT headers against real iOS SDKs; no device acceptance claim."""
import argparse
import json
from pathlib import Path
import subprocess
import sys
import tempfile
def check(sdk, minimum, output):
root = Path(__file__).resolve().parents[3]
sdk_path = subprocess.check_output(['xcrun', '--sdk', sdk, '--show-sdk-path'], text=True).strip()
clang = subprocess.check_output(['xcrun', '--sdk', sdk, '--find', 'clang'], text=True).strip()
target = 'arm64-apple-ios' + minimum + ('-simulator' if sdk == 'iphonesimulator' else '')
with tempfile.TemporaryDirectory(prefix='vt-ios-sdk-') as tmp:
probe = Path(tmp) / 'probe.m'
probe.write_text('#include "gfx/common/videotoolbox_encode.h"\n'
'#include "record/drivers/record_metal.h"\n'
'#ifndef HAVE_RECORD_METAL\n#error native frontend unavailable\n#endif\n'
'#ifndef RECORD_METAL_NATIVE_SDK\n#error mobile recording compiled as a stub\n#endif\n'
'#ifndef VIDEOTOOLBOX_ENCODE_SLOTS\n#error native encoding slots unavailable\n#endif\n'
'VT_ENCODE_AVAILABLE void *record_vt_ios_sdk_probe(\n'
' const videotoolbox_encode_config_t *config, OSStatus *status)\n'
'{\n return videotoolbox_encode_new(config, status);\n}\n')
command = [clang, '-target', target, '-isysroot', sdk_path, '-std=gnu89',
'-DHAVE_THREADS', '-DHAVE_METAL', '-DHAVE_AVF', '-DHAVE_CONFIGFILE', '-DHAVE_NETWORKING',
'-DHAVE_RTMP', '-DHAVE_RFLV', '-DHAVE_CRYPTO', '-fblocks', '-fobjc-arc', '-I' + str(root), '-I' + str(root / 'libretro-common/include'),
'-Wall', '-Wextra', '-Werror', '-Wunguarded-availability-new',
'-Wno-unused-function', '-fsyntax-only', str(probe)]
units = [probe] + [root / ('record/' + name) for name in (
'record_gpu_capture_metal.m', 'record_gpu_session_metal.m',
'record_gpu_dispatch_metal.m', 'record_mux_avfoundation.m', 'drivers/record_metal.c')]
results = []
for unit in units:
invocation = command[:-1] + [str(unit)]
if unit.suffix == '.c':
invocation.remove('-fobjc-arc')
result = subprocess.run(invocation, text=True, capture_output=True, timeout=120)
name = sdk + '-' + minimum + '-' + unit.stem
(output / (name + '.log')).write_text(result.stdout + result.stderr)
results.append(dict(sdk=sdk, minimum=minimum, target=target, unit=unit.name,
command=invocation, result='PASS' if not result.returncode else 'FAIL',
exit_code=result.returncode))
return results
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output-dir', type=Path, required=True)
args = parser.parse_args()
if sys.platform != 'darwin':
parser.error('real Apple SDK compilation requires macOS/Xcode; no local pass is reported')
args.output_dir.mkdir(parents=True, exist_ok=True)
results = [case for sdk in ('iphoneos', 'iphonesimulator')
for minimum in ('13.0', '17.4')
for case in check(sdk, minimum, args.output_dir)]
(args.output_dir / 'results.json').write_text(json.dumps(dict(
scope='SDK production unit compilation only; no full application link or device acceptance',
cases=results), indent=2) + '\n')
for result in results:
print(result['result'], result['target'], result['unit'], flush=True)
return int(any(result['exit_code'] for result in results))
if __name__ == '__main__':
sys.exit(main())