Compare commits

..
Author SHA1 Message Date
Codex 09b678a61f tests: gate native iOS recording on the production application link
After real SDK unit admission succeeds, build the existing iOS Release application without signing. Assert resolved Metal/AVFoundation/thread/configuration and RTMP/FLV flags, reject FFmpeg, retain Xcode diagnostics and inspect the linked executable's libraries. Use structured subprocess arguments for signing overrides. Keep application-link scope distinct from physical device acceptance.
2026-11-12 19:03:46 +01:00
Codex a31bdf288f record: wire eligible Apple SDKs to native recording and shared RTMP
Derive the native Metal backend from a shared SDK floor rather than a desktop-only target check. Add shared RTMP/FLV flags to the existing Apple network configuration and let griffin include the existing native implementation units. Extend the real SDK audit to compile the actual C frontend using automatic backend admission, with ARC kept out of C compilation.

This is an unlanded integration candidate. SDK compilation, full Apple application linking and physical-device recording/streaming acceptance are pending.
2026-11-12 19:03:46 +01:00
Codex 0e3284a884 record: connect native Metal session and dispatcher on mobile Apple SDKs
Compile the real session and dedicated worker on eligible iOS/tvOS SDKs and reject older runtimes before startup. Mobile uses verified hardware admission without assuming a desktop GPU registry ID. Preserve desktop enumeration and affinity selection. Expand the opt-in SDK audit to compile capture, session, dispatcher and muxer units, with explicit non-stub and native-slot assertions.

The SDK audit remains a pending gate. This candidate does not enable mobile application feature defines or claim native-device recording acceptance.
2026-11-12 19:03:46 +01:00
Codex 7f106a3736 record: admit advertised hardware encoders before iOS 17.4
Guard mobile hardware require/query keys at their actual SDK and runtime floor. On older supported mobile systems, retain an advertised hardware encoder ID matching the requested codec and verify the created session reports the same ID. Reject malformed, missing and software-only advertisements. Preserve desktop hardware requirement and query behavior.

Portable strict-C89 VideoToolbox fixture suite passes on native MinGW, including hardware-ID admission and retained-list ownership checks. Real iOS SDK compilation and physical-device acceptance remain separate gates; this does not yet enable the mobile recording frontend.
2026-11-12 19:03:46 +01:00
Codex c7dc2e57b2 tests: add real iOS SDK admission audit for native VT headers
Compile the production wrapper for arm64 iPhoneOS and simulator at iOS 13 and 17.4 deployment floors. Preserve compiler diagnostics and distinguish SDK acceptance from unavailable physical-device encoding tests. The opt-in workflow dispatch isolates this audit without disabling normal default coverage.
2026-11-12 19:03:45 +01:00
35 changed files with 556 additions and 1305 deletions

No files matched your search

@@ -81,6 +81,10 @@ 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
@@ -93,7 +97,7 @@ permissions:
contents: read
jobs:
stream-native:
if: ${{ !inputs.native_aac_only && !inputs.frontend_stream_only }}
if: ${{ !inputs.ios_sdk_only && !inputs.native_aac_only && !inputs.frontend_stream_only }}
runs-on: macos-latest
timeout-minutes: 10
steps:
@@ -152,7 +156,7 @@ jobs:
${{ runner.temp }}/vt-metal-dispatch-mrc
if-no-files-found: ignore
aac-native:
if: ${{ inputs.native_aac_only || !inputs.frontend_stream_only }}
if: ${{ !inputs.ios_sdk_only && (inputs.native_aac_only || !inputs.frontend_stream_only) }}
runs-on: macos-latest
timeout-minutes: 10
steps:
@@ -184,7 +188,7 @@ jobs:
path: ${{ runner.temp }}/audiotoolbox-src-evidence
if-no-files-found: ignore
encode:
if: ${{ !inputs.native_aac_only && !inputs.frontend_stream_only }}
if: ${{ !inputs.ios_sdk_only && !inputs.native_aac_only && !inputs.frontend_stream_only }}
runs-on: macos-latest
timeout-minutes: 20
steps:
@@ -285,7 +289,7 @@ jobs:
if-no-files-found: warn
frontend-build:
if: ${{ !inputs.native_aac_only && !inputs.frontend_stream_only }}
if: ${{ !inputs.ios_sdk_only && !inputs.native_aac_only && !inputs.frontend_stream_only }}
runs-on: macos-latest
timeout-minutes: 40
steps:
@@ -322,7 +326,7 @@ jobs:
if-no-files-found: warn
frontend-stream-build:
if: ${{ !inputs.native_aac_only }}
if: ${{ !inputs.ios_sdk_only && !inputs.native_aac_only }}
runs-on: macos-latest
timeout-minutes: 20
strategy:
@@ -372,9 +376,46 @@ jobs:
if-no-files-found: ignore
frontend-contracts:
if: ${{ !inputs.native_aac_only }}
if: ${{ !inputs.ios_sdk_only && !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_aac_stream_timing.o \
record/record_mux_mediacodec.o record/record_mediacodec_worker.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 \
+38 -5
View File
@@ -9,6 +9,8 @@
#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;
@@ -57,7 +59,15 @@ 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
@@ -68,6 +78,20 @@ 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);
@@ -76,7 +100,10 @@ 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; }
CFDictionarySetValue(spec, kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder, kCFBooleanTrue);
#ifdef VT_ENCODE_HAS_HARDWARE_KEYS
if (VT_ENCODE_HARDWARE_KEYS_AVAILABLE)
CFDictionarySetValue(spec, kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder, kCFBooleanTrue);
#endif
if (encoder_id) CFDictionarySetValue(spec,kVTVideoEncoderSpecification_EncoderID,encoder_id);
if (gpu_registry_id)
{
@@ -132,10 +159,15 @@ static INLINE VT_ENCODE_AVAILABLE videotoolbox_encode_t *videotoolbox_encode_new
if (status) goto done;
status = VTCompressionSessionPrepareToEncodeFrames(encoder->session);
if (status) goto done;
status = VTSessionCopyProperty(encoder->session,
kVTCompressionPropertyKey_UsingHardwareAcceleratedVideoEncoder, NULL, &hardware);
if (!status && (!hardware || CFGetTypeID(hardware) != CFBooleanGetTypeID()
|| !CFBooleanGetValue((CFBooleanRef)hardware))) status = kVTCouldNotFindVideoEncoderErr;
#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
if (!status && encoder_id)
{
status=VTSessionCopyProperty(encoder->session,kVTCompressionPropertyKey_EncoderID,NULL,&selected_id);
@@ -143,6 +175,7 @@ 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
@@ -0,0 +1,85 @@
/* 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,7 +1723,6 @@ 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_CRYPTO -DHAVE_KEYCHAIN -DHAVE_RETROSSL -DHAVE_SMBCLIENT -DHAVE_RETROSMB -DHAVE_NFSCLIENT -DHAVE_RETRONFS
NETWORK_STACK_FLAGS = -DHAVE_RTMP -DHAVE_RFLV -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 <TargetConditionals.h>
#if TARGET_OS_OSX && defined(HAVE_METAL) && defined(HAVE_AVF) \
#include "../record_metal_platform.h"
#if defined(RECORD_METAL_NATIVE_SDK) && defined(HAVE_METAL) && defined(HAVE_AVF) \
&& defined(HAVE_THREADS) && defined(HAVE_CONFIGFILE)
#define HAVE_RECORD_METAL
#endif
-22
View File
@@ -1,22 +0,0 @@
/* 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
@@ -1,22 +0,0 @@
/* 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
+7 -1
View File
@@ -15,11 +15,12 @@
#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 TARGET_OS_OSX && defined(HAVE_THREADS) && defined(RETRO_ATOMIC_LOCK_FREE)
#if defined(RECORD_METAL_NATIVE_SDK) && 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__
@@ -447,6 +448,11 @@ 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
+22 -10
View File
@@ -17,16 +17,22 @@
#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) && TARGET_OS_OSX \
&& defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED>=101400
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && defined(RECORD_METAL_NATIVE_SDK)
/* 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 __attribute__((availability(macos,introduced=10.14))) videotoolbox_encode_t *record_metal_select_encoder(
static RECORD_METAL_NATIVE_AVAILABLE 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;
@@ -62,6 +68,7 @@ static __attribute__((availability(macos,introduced=10.14))) videotoolbox_encode
}
done:
CFRelease(list); *error=status; return encoder;
#endif
}
#endif
record_avfoundation_session_t *record_gpu_session_metal_new_output(void *device,
@@ -73,8 +80,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) && TARGET_OS_OSX \
&& defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED>=101400
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && defined(RECORD_METAL_NATIVE_SDK)
if (RECORD_METAL_NATIVE_RUNTIME)
@autoreleasepool
{
videotoolbox_encode_config_t config;
@@ -94,8 +101,12 @@ 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;
@@ -128,20 +139,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) && TARGET_OS_OSX \
&& defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED>=101400
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && defined(RECORD_METAL_NATIVE_SDK)
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 __attribute__((availability(macos,introduced=10.14))) record_avfoundation_session_t *record_metal_file_bind(
static RECORD_METAL_NATIVE_AVAILABLE 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)
{
@@ -159,8 +170,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) && TARGET_OS_OSX \
&& defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED>=101400
#if defined(VIDEOTOOLBOX_ENCODE_SLOTS) && defined(RECORD_METAL_NATIVE_SDK)
if (RECORD_METAL_NATIVE_RUNTIME)
{
record_metal_file_binding_t binding;
if (!c || !output || output->video_format || c->timescale!=output->timescale || !path || !*path)
@@ -172,6 +183,7 @@ 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,24 +466,6 @@ 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)
{
@@ -684,17 +666,6 @@ 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,14 +51,6 @@ 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
@@ -1,65 +0,0 @@
/* 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
@@ -1,27 +0,0 @@
/* 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
+22 -160
View File
@@ -1,7 +1,6 @@
/* 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
@@ -10,17 +9,13 @@ struct record_mediacodec_worker
int64_t end_us[2];
record_mediacodec_encoder_t *encoder[2];
record_audio_queue_t *pcm;
record_mediacodec_output_ops_t output;
void *output_context;
record_mediacodec_packet_t pending[2];
record_mux_mediacodec_t *mux;
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], pending_live[2];
bool format[2], drained[2], ended[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,
@@ -46,24 +41,13 @@ 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 || w->live_probe || !record_aac_probe_delay(w->probe, &delay))
|| !w->probe || !record_aac_probe_delay(w->probe, &delay))
return false;
*skip = RECORD_AAC_PROBE_FRAMES + (uint64_t)delay;
*frames = w->audio_end_sample;
@@ -71,40 +55,13 @@ bool record_mediacodec_worker_audio_edit(
return true;
}
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_worker_t *record_mediacodec_worker_new(
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)
record_audio_queue_t *pcm, record_mux_mediacodec_t *mux,
unsigned channels, unsigned rate)
{
record_mediacodec_worker_t *w;
if (!video || !context || !ops || !ops->track || !ops->start || !ops->write
|| !ops->end || !ops->finish || !ops->retire || (!!audio != !!pcm)
if (!video || !mux || (!!audio != !!pcm)
|| (audio && (!channels || channels > 2 || !rate)))
return NULL;
w = (record_mediacodec_worker_t *)calloc(1, sizeof(*w));
@@ -113,25 +70,12 @@ record_mediacodec_worker_t *record_mediacodec_worker_new_output(
w->encoder[0] = video;
w->encoder[1] = audio;
w->pcm = pcm;
w->output = *ops;
w->output_context = context;
w->mux = mux;
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)
@@ -145,7 +89,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 - (w->live_probe ? 32768u : RECORD_AAC_PROBE_FRAMES))
> (uint64_t)-1 - RECORD_AAC_PROBE_FRAMES)
|| !record_mcw_samples_us(audio_end_sample, w->rate,
&w->end_us[1]))
return false;
@@ -162,13 +106,7 @@ static int record_mcw_output(record_mediacodec_worker_t *w, unsigned at)
int result;
if (w->drained[at])
return 0;
if (w->pending_live[at])
{
packet = w->pending[at];
result = RECORD_MC_PACKET;
}
else
result = record_mediacodec_encoder_pump(w->encoder[at], &packet);
result = record_mediacodec_encoder_pump(w->encoder[at], &packet);
switch (result)
{
case RECORD_MC_BUSY:
@@ -185,7 +123,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] || !w->output.track(w->output_context, at != 0,
if (w->format[at] || !record_mux_mediacodec_track(w->mux, at != 0,
record_mediacodec_encoder_format(w->encoder[at])))
return -1;
w->format[at] = true;
@@ -195,11 +133,8 @@ static int record_mcw_output(record_mediacodec_worker_t *w, unsigned at)
return -1;
if (!w->started)
return 0;
if (!at && w->live_probe && !w->timing_ready)
return 0;
if (!w->pending_live[at] && at && w->probe && packet.size && !(packet.flags & 2))
if (at && w->probe && !(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)
@@ -207,36 +142,10 @@ 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 (!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)
if (!record_mux_mediacodec_write(w->mux, at != 0, &packet)
|| !record_mediacodec_encoder_release(w->encoder[at]))
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
@@ -251,7 +160,6 @@ 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;
@@ -272,25 +180,6 @@ 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);
@@ -303,15 +192,13 @@ 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 - (w->live_probe
? w->live_timing.input_epoch : RECORD_AAC_PROBE_FRAMES))
> (uint64_t)-1 - 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->live_probe ? w->live_timing.input_epoch
: w->probe ? RECORD_AAC_PROBE_FRAMES : 0), &accepted);
+ (w->probe ? RECORD_AAC_PROBE_FRAMES : 0), &accepted);
if (result < 0 || result > 1 || accepted > frames - w->consumed
|| (!result && accepted))
return -1;
@@ -331,8 +218,7 @@ 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->live_probe ? w->live_timing.input_epoch
: w->probe ? RECORD_AAC_PROBE_FRAMES : 0));
+ (w->probe ? RECORD_AAC_PROBE_FRAMES : 0));
if (result < 0 || result > 2)
return -1;
if (result == 1)
@@ -376,35 +262,23 @@ 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 = (w->live_probe ? w->live_timing.input_epoch
: RECORD_AAC_PROBE_FRAMES) + (uint64_t)delay;
skip = 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->live_probe
? w->live_timing.audio_epoch + w->audio_end_sample
: w->coded_frames, w->rate,
|| !record_mcw_samples_us(w->coded_frames, w->rate,
&w->end_us[1]))
goto failed;
}
}
if (!w->started)
{
result = w->output.start(w->output_context);
result = record_mux_mediacodec_start(w->mux);
if (result < 0)
goto failed;
if (result)
@@ -417,24 +291,14 @@ int record_mediacodec_worker_step(record_mediacodec_worker_t *w)
if (w->started && w->drained[at] && !w->ended[at]
&& (!at || calibrated))
{
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))
if (!record_mux_mediacodec_end(w->mux, at != 0, w->end_us[at]))
goto failed;
w->ended[at] = true;
progress = true;
}
if (w->ended[0] && w->ended[1])
{
result = w->output.finish(w->output_context);
result = record_mux_mediacodec_finish(w->mux);
if (result < 0)
goto failed;
if (result)
@@ -459,8 +323,6 @@ 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;
}
+2 -32
View File
@@ -14,28 +14,6 @@ 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(
@@ -45,12 +23,6 @@ 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,
@@ -62,10 +34,8 @@ 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 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. */
/* Controller still owns encoder/GPU retirement, mux cancellation and PCM
* drain on failure. Free refuses active service unless aborted. */
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
@@ -0,0 +1,21 @@
/* 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
@@ -1,222 +0,0 @@
/* 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
@@ -1,26 +0,0 @@
/* 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,8 +317,6 @@ 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,7 +4,6 @@
#include <stdio.h>
#include <string.h>
#include "../../../record/record_mediacodec_worker.h"
#include "../../../record/record_aac_stream_timing.h"
struct record_mediacodec_encoder
{
@@ -27,9 +26,6 @@ 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)
@@ -67,7 +63,7 @@ bool record_aac_probe_delay(const record_aac_probe_t *p, unsigned *delay)
{
if (!p->ready)
return false;
*delay = live_mode ? live_delay : fault == 7 ? 4096 : 2048;
*delay = fault == 7 ? 4096 : 2048;
return true;
}
bool record_mediacodec_encoder_audio_config(record_mediacodec_encoder_t *e,
@@ -90,9 +86,6 @@ 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);
@@ -103,7 +96,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 == (live_mode ? live_timing.input_epoch : RECORD_AAC_PROBE_FRAMES) + (empty ? 0 : 17)
assert(e->audio && end == RECORD_AAC_PROBE_FRAMES + (empty ? 0 : 17)
&& e->accepted == end);
e->eos = true;
return 1;
@@ -124,10 +117,8 @@ 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 : live_mode ? (unsigned)((e->accepted + live_delay + 1023) / 1024) : empty ? 8 : 11;
limit = fault == 5 ? 7 : empty ? 8 : 11;
if (e->packets < limit)
{
e->held = true;
@@ -168,15 +159,14 @@ 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)
: live_mode ? live_timing.video_epoch_us : 0));
: 0));
m->packets[audio]++;
return true;
}
bool record_mux_mediacodec_end(record_mux_mediacodec_t *m,
bool audio, int64_t end)
{
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)));
assert(end == (audio ? (empty ? 170666 : 234666) : 1000000));
m->ends[audio]++;
return true;
}
@@ -186,16 +176,12 @@ int record_mux_mediacodec_finish(record_mux_mediacodec_t *m)
m->finished = true;
return 1;
}
#include "worker_output_fixture.h"
static void exercise_mode(unsigned failure, bool cancel, bool no_pcm, bool backpressure,
bool live, unsigned delay)
static void exercise(unsigned failure, bool cancel, bool no_pcm)
{
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;
@@ -210,9 +196,6 @@ static void exercise_mode(unsigned failure, bool cancel, bool no_pcm, bool backp
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);
@@ -220,17 +203,9 @@ static void exercise_mode(unsigned failure, bool cancel, bool no_pcm, bool backp
if (!empty)
assert(record_audio_queue_push(q, pcm, 17, 0) == RECORD_AUDIO_OK);
assert(record_audio_queue_finish(q));
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)));
w = record_mediacodec_worker_new(&video, &audio, q, &mux, 1, 48000);
assert(w && 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));
@@ -253,20 +228,10 @@ static void exercise_mode(unsigned failure, bool cancel, bool no_pcm, bool backp
else
{
assert(result == RECORD_MC_WORKER_END && mux.finished);
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_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));
@@ -275,33 +240,17 @@ static void exercise_mode(unsigned failure, bool cancel, bool no_pcm, bool backp
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)
{
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]);
}
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);
puts("PASS: calibrated worker prefix backpressure, coded/source clocks, "
"file trim, live silent priming, source epochs, empty PCM, early EOS and cancellation; codecs/probe mocked");
"trim ownership, early EOS and cancellation; codecs/probe mocked");
return 0;
}
+1 -9
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", []), ("stream_config_test.c", []), ("stream_output_test.c", []), ("stream_timing_test.c", []), ("audio_test.c", []),
for source, extra in (("test.c", []), ("audio_test.c", []),
("mux_test.c", ["-DRECORD_MEDIACODEC_MUX_TEST"]),
("worker_test.c", []), ("calibrated_worker_test.c", []),
("surface_test.c", []),
@@ -60,17 +60,9 @@ 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":
@@ -1,168 +0,0 @@
/* 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;
}
@@ -1,138 +0,0 @@
/* 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;
}
@@ -1,69 +0,0 @@
#!/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))
@@ -1,45 +0,0 @@
/* 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;
}
+2 -34
View File
@@ -21,8 +21,6 @@ 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)
{
@@ -184,15 +182,8 @@ 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]);
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"));
assert((f == &output_formats[0] || f == &output_formats[1]) && hdr_mode
&& !strcmp(key, "hdr-static-info"));
*data = format_fault == 15 ? NULL : hdr_saved;
*size = format_fault == 14 ? 24 : 25;
return format_fault != 13;
@@ -348,8 +339,6 @@ 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());
@@ -370,26 +359,7 @@ 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);
@@ -546,8 +516,6 @@ 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);
@@ -1,66 +0,0 @@
/* 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,8 +165,6 @@ 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)
{
@@ -239,61 +237,6 @@ 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;
@@ -349,8 +292,6 @@ 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,3 +26,6 @@
/session_gpu_refusal_test
/session_gpu_refusal_test.exe
/hardware_id_test
/hardware_id_test.exe
+6 -3
View File
@@ -9,7 +9,7 @@ ifneq ($(SANITIZER),)
CFLAGS += -fsanitize=$(SANITIZER) -fno-omit-frame-pointer
LDFLAGS += -fsanitize=$(SANITIZER)
endif
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)
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)
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,9 +34,11 @@ 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
session_gpu_refusal_test$(EXE): session_test.c $(RA)/gfx/common/videotoolbox_encode.h $(RA)/gfx/common/videotoolbox_encode_hardware.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
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
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
slots_test$(EXE): slots_test.c $(RA)/gfx/common/videotoolbox_encode_slots.h
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
@@ -58,6 +60,7 @@ 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
@@ -0,0 +1,149 @@
/* 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;
}
@@ -0,0 +1,59 @@
#!/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())
@@ -0,0 +1,67 @@
#!/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())