record: correct AVFoundation AAC queries and append failures

Use the format-ID specifier required by AudioToolbox when selecting AAC sample rates. Validate requested audio settings and retry with the encoder default bitrate when the preset is rejected.

Report refused or failed audio/video appends instead of returning success. Cover the property ABI, buffer release and writer states with portable regressions, and add a native AudioToolbox contract check.
This commit is contained in:
Codex committed 2026-10-29 17:57:36 +01:00
1 parent 99df72ef0a
commit 66977c726a
8 files changed
+316 -27

No files matched your search

@@ -8,6 +8,7 @@ on:
- 'libretro-common/formats/h265/rh265_sps_crop.c'
- 'libretro-common/include/formats/rh265_sps_crop.h'
- 'record/drivers/record_metal.*'
- 'record/drivers/record_avfoundation*'
- 'record/record_driver.*'
- 'gfx/drivers/metal.m'
- 'Makefile.common'
@@ -32,6 +33,7 @@ on:
- 'libretro-common/formats/h265/rh265_sps_crop.c'
- 'libretro-common/include/formats/rh265_sps_crop.h'
- 'record/drivers/record_metal.*'
- 'record/drivers/record_avfoundation*'
- 'record/record_driver.*'
- 'gfx/drivers/metal.m'
- 'Makefile.common'
@@ -66,6 +68,8 @@ jobs:
- name: Portable ownership and failure contracts
working-directory: samples/record/videotoolbox_encode
run: make check thread_test worker_thread_test audio_thread_test av_session_thread_test && ./thread_test && ./worker_thread_test && ./audio_thread_test && ./av_session_thread_test
- name: Native AAC property ABI and advertised rates
run: make -C samples/record/videotoolbox_encode native-audio-check
- name: Native hardware encoder and independent pixels
run: |
rc=0
+28 -7
View File
@@ -292,6 +292,23 @@ static void *avfoundation_record_init(const struct record_params *params)
AVEncoderBitRateKey: @(audioBitRate)
};
if (![handle->assetWriter canApplyOutputSettings:audioSettings
forMediaType:AVMediaTypeAudio])
{
NSMutableDictionary *fallback =
[NSMutableDictionary dictionaryWithDictionary:audioSettings];
[fallback removeObjectForKey:AVEncoderBitRateKey];
audioSettings = fallback;
if (![handle->assetWriter canApplyOutputSettings:audioSettings
forMediaType:AVMediaTypeAudio])
{
RARCH_ERR("[AVFoundation] Unsupported AAC output settings\n");
avfoundation_release_handle(handle);
return NULL;
}
RARCH_WARN("[AVFoundation] AAC bitrate preset unsupported; using encoder default\n");
}
/* Create audio input */
handle->audioInput = [[AVAssetWriterInput alloc]
initWithMediaType:AVMediaTypeAudio
@@ -324,7 +341,7 @@ static void *avfoundation_record_init(const struct record_params *params)
}
RARCH_LOG("[AVFoundation] Initialized recording to %s (%ux%u @ %.2f fps, "
"video %u kbps, audio %u kbps)\n",
"video %u kbps, audio preset %u kbps)\n",
params->filename, handle->width, handle->height,
handle->fps, videoBitRate / 1000, audioBitRate / 1000);
}
@@ -378,6 +395,7 @@ static bool avfoundation_record_push_video(void *data,
record_avfoundation_t *handle = (record_avfoundation_t*)data;
CVPixelBufferRef pixelBuffer = NULL;
CVReturn result;
__block bool success = false;
if (!handle || !video_data)
return false;
@@ -527,15 +545,16 @@ static bool avfoundation_record_push_video(void *data,
/* Append pixel buffer on encoding queue */
dispatch_sync(handle->encodingQueue, ^{
if (handle->videoInput.readyForMoreMediaData)
[handle->pixelBufferAdaptor appendPixelBuffer:pixelBuffer
if (handle->assetWriter.status == AVAssetWriterStatusWriting
&& handle->videoInput.readyForMoreMediaData)
success = [handle->pixelBufferAdaptor appendPixelBuffer:pixelBuffer
withPresentationTime:presentationTime];
});
CVPixelBufferRelease(pixelBuffer);
}
return true;
return success;
}
static bool avfoundation_record_push_audio(void *data,
@@ -546,6 +565,7 @@ static bool avfoundation_record_push_audio(void *data,
CMBlockBufferRef blockBuffer = NULL;
CMFormatDescriptionRef formatDesc = NULL;
OSStatus status;
__block bool success = false;
if (!handle || !handle->audioInput || !audio_data || !audio_data->data)
return false;
@@ -622,14 +642,15 @@ static bool avfoundation_record_push_audio(void *data,
/* Append on encoding queue */
dispatch_sync(handle->encodingQueue, ^{
if (handle->audioInput.readyForMoreMediaData)
[handle->audioInput appendSampleBuffer:sampleBuffer];
if (handle->assetWriter.status == AVAssetWriterStatusWriting
&& handle->audioInput.readyForMoreMediaData)
success = [handle->audioInput appendSampleBuffer:sampleBuffer];
});
CFRelease(sampleBuffer);
}
return true;
return success;
}
static bool avfoundation_record_finalize(void *data)
+3 -13
View File
@@ -8,28 +8,18 @@
static double avfoundation_nearest_aac_sample_rate(double rate)
{
AudioFormatPropertyID prop = kAudioFormatProperty_AvailableEncodeSampleRates;
AudioStreamBasicDescription desc;
UInt32 format = kAudioFormatMPEG4AAC;
UInt32 size = 0;
OSStatus status;
desc.mFormatID = kAudioFormatMPEG4AAC;
desc.mSampleRate = 0;
desc.mFormatFlags = 0;
desc.mBytesPerPacket = 0;
desc.mFramesPerPacket = 0;
desc.mBytesPerFrame = 0;
desc.mChannelsPerFrame = 0;
desc.mBitsPerChannel = 0;
desc.mReserved = 0;
status = AudioFormatGetPropertyInfo(prop, sizeof(desc), &desc, &size);
status = AudioFormatGetPropertyInfo(prop, sizeof(format), &format, &size);
if (status == noErr && size > 0)
{
UInt32 capacity = size;
AudioValueRange *ranges = (AudioValueRange*)malloc(size);
if (ranges)
{
status = AudioFormatGetProperty(prop, sizeof(desc), &desc,
status = AudioFormatGetProperty(prop, sizeof(format), &format,
&size, ranges);
if (status == noErr && size <= capacity && !(size % sizeof(AudioValueRange)))
{
+10 -3
View File
@@ -9,7 +9,7 @@ ifneq ($(SANITIZER),)
CFLAGS += -fsanitize=$(SANITIZER) -fno-omit-frame-pointer
LDFLAGS += -fsanitize=$(SANITIZER)
endif
all: 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)
all: 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_session_thread_test: av_session_thread_test.c $(RA)/record/record_avfoundation_session.c
$(CC) $(CFLAGS) -pthread -o $@ av_session_thread_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)
av_session_test$(EXE): av_worker_test.c $(RA)/record/record_avfoundation_session.c $(RA)/record/record_avfoundation_session.h
@@ -22,6 +22,10 @@ audio_thread_test: audio_queue_test.c $(RA)/record/record_audio_queue.c $(RA)/re
$(CC) $(CFLAGS) -DRECORD_AUDIO_THREAD_TEST -pthread -o $@ audio_queue_test.c $(RA)/record/record_audio_queue.c $(RA)/libretro-common/queues/retro_spsc.c $(LDFLAGS)
audio_rate_test$(EXE): audio_rate_test.c mock/AudioToolbox/AudioToolbox.h $(RA)/record/drivers/record_avfoundation_audio.h
$(CC) $(CFLAGS) -Imock -o $@ $< $(LDFLAGS)
audio_rate_native_test$(EXE): audio_rate_native_test.c $(RA)/record/drivers/record_avfoundation_audio.h
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) $(if $(filter Darwin,$(shell uname -s)),-framework AudioToolbox,)
native-audio-check: audio_rate_native_test$(EXE)
$(RUNNER) ./audio_rate_native_test$(EXE)
crop_test$(EXE): crop_test.c $(RA)/libretro-common/formats/h265/rh265_sps_crop.c $(RA)/libretro-common/include/formats/rh265_sps_crop.h
$(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
@@ -36,7 +40,7 @@ thread_test: slots_test.c $(RA)/gfx/common/videotoolbox_encode_slots.h
$(CC) $(CFLAGS) -DVT_ENCODE_THREAD_TEST -pthread -o $@ $< $(LDFLAGS)
worker_thread_test: worker_thread_test.c $(RA)/record/record_gpu_worker_vt.c $(RA)/record/record_gpu_worker_vt.h $(RA)/record/record_gpu_queue.c
$(CC) $(CFLAGS) -pthread -o $@ worker_thread_test.c $(RA)/record/record_gpu_worker_vt.c $(RA)/record/record_gpu_queue.c $(RA)/libretro-common/queues/retro_spsc.c $(LDFLAGS)
check: all
check: all legacy-check
$(RUNNER) ./session_gpu_refusal_test$(EXE)
$(RUNNER) ./av_session_test$(EXE)
$(RUNNER) ./crop_test$(EXE)
@@ -54,4 +58,7 @@ clean:
rm -f audio_queue_test audio_queue_test.exe audio_thread_test
rm -f slots_test slots_test.exe session_test session_test.exe worker_test worker_test.exe thread_test worker_thread_test
rm -f audio_rate_test audio_rate_test.exe
.PHONY: all check clean
rm -f audio_rate_native_test audio_rate_native_test.exe
legacy-check: legacy_append_run.py legacy_append_test.c $(RA)/record/drivers/record_avfoundation.m
CC="$(CC)" CFLAGS="$(CFLAGS)" LDFLAGS="$(LDFLAGS)" RUNNER="$(RUNNER)" python3 legacy_append_run.py
.PHONY: all check clean legacy-check native-audio-check
@@ -0,0 +1,70 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#include <stdio.h>
#ifdef __APPLE__
#include <assert.h>
#include <stdlib.h>
#include <AudioToolbox/AudioToolbox.h>
static unsigned info_calls, value_calls;
static OSStatus checked_info(AudioFormatPropertyID property, UInt32 bytes,
const void *input, UInt32 *size)
{
assert(property == kAudioFormatProperty_AvailableEncodeSampleRates);
assert(bytes == sizeof(UInt32) && input
&& *(const UInt32*)input == kAudioFormatMPEG4AAC);
info_calls++;
return AudioFormatGetPropertyInfo(property, bytes, input, size);
}
static OSStatus checked_value(AudioFormatPropertyID property, UInt32 bytes,
const void *input, UInt32 *size, void *output)
{
assert(property == kAudioFormatProperty_AvailableEncodeSampleRates);
assert(bytes == sizeof(UInt32) && input
&& *(const UInt32*)input == kAudioFormatMPEG4AAC);
value_calls++;
return AudioFormatGetProperty(property, bytes, input, size, output);
}
#define AudioFormatGetPropertyInfo checked_info
#define AudioFormatGetProperty checked_value
#include "../../../record/drivers/record_avfoundation_audio.h"
#undef AudioFormatGetPropertyInfo
#undef AudioFormatGetProperty
int main(void)
{
static const double inputs[] = {8000, 22050, 44100, 48000};
UInt32 format = kAudioFormatMPEG4AAC;
UInt32 size = 0, capacity;
AudioValueRange *ranges;
unsigned i, j;
assert(!AudioFormatGetPropertyInfo(
kAudioFormatProperty_AvailableEncodeSampleRates,
sizeof(format), &format, &size));
assert(size && !(size % sizeof(*ranges)));
capacity = size;
ranges = (AudioValueRange*)malloc(size);
assert(ranges);
assert(!AudioFormatGetProperty(kAudioFormatProperty_AvailableEncodeSampleRates,
sizeof(format), &format, &size, ranges));
assert(size && size <= capacity && !(size % sizeof(*ranges)));
for (i = 0; i < sizeof(inputs) / sizeof(inputs[0]); i++)
{
double rate = avfoundation_nearest_aac_sample_rate(inputs[i]);
int found = 0;
for (j = 0; j < size / sizeof(*ranges); j++)
if (rate >= ranges[j].mMinimum && rate <= ranges[j].mMaximum)
found = 1;
assert(found && info_calls == i + 1 && value_calls == i + 1);
printf("AAC source %.0f Hz selects advertised output %.0f Hz\n", inputs[i], rate);
}
free(ranges);
puts("PASS: native AudioToolbox property ABI and advertised AAC rates; no encode claim");
return 0;
}
#else
int main(void)
{
puts("UNAVAILABLE: native AudioToolbox requires Apple");
return 77;
}
#endif
@@ -8,14 +8,16 @@ static unsigned mode,allocation_fail;
static AudioValueRange rates[2]={{32000,32000},{48000,48000}};
OSStatus AudioFormatGetPropertyInfo(AudioFormatPropertyID property,UInt32 bytes,const void *input,UInt32 *size)
{
const AudioStreamBasicDescription *desc=(const AudioStreamBasicDescription*)input;
assert(property==1 && bytes==sizeof(*desc) && desc->mFormatID==2
&& !desc->mReserved && !desc->mSampleRate);
assert(property==kAudioFormatProperty_AvailableEncodeSampleRates
&& bytes==sizeof(UInt32) && input
&& *(const UInt32*)input==kAudioFormatMPEG4AAC);
*size=sizeof(rates); return mode==1?-1:0;
}
OSStatus AudioFormatGetProperty(AudioFormatPropertyID property,UInt32 bytes,const void *input,UInt32 *size,void *output)
{
(void)property; (void)bytes; (void)input;
assert(property==kAudioFormatProperty_AvailableEncodeSampleRates
&& bytes==sizeof(UInt32) && input
&& *(const UInt32*)input==kAudioFormatMPEG4AAC);
assert(*size==sizeof(rates));
if (mode==3) return -1;
memcpy(output,rates,mode==2?sizeof(rates[0]):sizeof(rates));
@@ -40,6 +42,11 @@ int main(void)
allocation_fail=0; rates[0].mMinimum=rates[0].mMaximum=0;
rates[1].mMinimum=48000; rates[1].mMaximum=32000;
assert(avfoundation_nearest_aac_sample_rate(22050)==22050);
mode=0;
rates[0].mMinimum=rates[0].mMaximum=8000;
rates[1].mMinimum=rates[1].mMaximum=22050;
assert(avfoundation_nearest_aac_sample_rate(8000)==8000);
assert(avfoundation_nearest_aac_sample_rate(22050)==22050);
puts("PASS: AAC rate ranges, actual response size, malformed ranges and fallback failures");
return 0;
}
@@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""Run the recorder's submission control with portable Apple message stand-ins."""
import argparse
import os
from pathlib import Path
import re
import shlex
import subprocess
import tempfile
here = Path(__file__).resolve().parent
root = here.parents[2]
parser = argparse.ArgumentParser()
parser.add_argument('--source', type=Path, default=root / 'record/drivers/record_avfoundation.m')
args = parser.parse_args()
source = args.source.read_text(encoding='utf-8')
fragments = []
for kind, buffer, extra in [('video', 'pixelBuffer', ', int presentationTime'),
('audio', 'sampleBuffer', '')]:
start = source.index('static bool avfoundation_record_push_' + kind + '(')
end = source.index('\nstatic bool ', start + 1)
function = source[start:end].rstrip()
tail = function[function.index(' /* Append'):]
declaration = re.search(r'__block bool success\s*=\s*false;', function)
prefix = ('static bool legacy_' + kind + '_submit(record_avfoundation_t *handle, void *'
+ buffer + extra + ')\n{\n'
+ (declaration.group(0) + '\n' if declaration else '') + ' {\n')
fragments.append(prefix + tail)
start = source.find(' if (![handle->assetWriter canApplyOutputSettings:audioSettings')
body = ''
if start >= 0:
body = source[start:source.index(' /* Create audio input */', start)]
fragments.append('static void *legacy_audio_settings(record_avfoundation_t *handle, '
'NSMutableDictionary *audioSettings)\n{\n' + body
+ '\n return audioSettings;\n}\n')
code = '\n'.join(fragments).replace('__block ', '')
code = code.replace('dispatch_sync(handle->encodingQueue, ^{', '{').replace('});', '}')
for kind in ('video', 'audio'):
code = code.replace('handle->' + kind + 'Input.readyForMoreMediaData',
'handle->' + kind + 'Input->ready')
code = code.replace('handle->assetWriter.status', 'handle->assetWriter->status')
code = re.sub(r'\[handle->pixelBufferAdaptor appendPixelBuffer:pixelBuffer\s+'
r'withPresentationTime:presentationTime\]',
'append_video(handle->pixelBufferAdaptor, pixelBuffer, presentationTime)', code)
code = code.replace('[handle->audioInput appendSampleBuffer:sampleBuffer]',
'append_audio(handle->audioInput, sampleBuffer)')
code = re.sub(r'\[handle->assetWriter canApplyOutputSettings:audioSettings\s+'
r'forMediaType:AVMediaTypeAudio\]', 'can_apply(handle->assetWriter, audioSettings)', code)
code = code.replace('[NSMutableDictionary dictionaryWithDictionary:audioSettings]',
'copy_settings(audioSettings)')
code = code.replace('[fallback removeObjectForKey:AVEncoderBitRateKey]',
'remove_bitrate(fallback, AVEncoderBitRateKey)')
assert not any(token in code for token in ('dispatch_sync', '^{', '[handle', '[fallback', '[NSMutableDictionary'))
compiler = shlex.split(os.environ.get('CC', 'cc'))
flags = shlex.split(os.environ.get('CFLAGS', '-std=c89 -pedantic -Wall -Wextra -Werror'))
with tempfile.TemporaryDirectory(prefix='avf-append-') as temp:
temp = Path(temp)
(temp / 'legacy_append.inc').write_text(code, encoding='utf-8')
suffix = '.exe' if os.name == 'nt' or 'mingw' in compiler[0] else ''
binary = temp / ('append' + suffix)
command = compiler + flags + ['-I' + str(temp), '-I' + str(root / 'libretro-common/include'),
str(here / 'legacy_append_test.c'), '-o', str(binary)]
command += shlex.split(os.environ.get('LDFLAGS', ''))
subprocess.run(command, cwd=here, check=True)
subprocess.run(shlex.split(os.environ.get('RUNNER', '')) + [str(binary)],
cwd=here, check=True, timeout=60)
@@ -0,0 +1,124 @@
/* Copyright (C) 2026 The RetroArch team. SPDX-License-Identifier: GPL-3.0-or-later */
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <boolean.h>
enum { AVAssetWriterStatusWriting = 1, AVEncoderBitRateKey = 1 };
typedef struct
{
int status;
bool preset_supported, default_supported;
unsigned probes;
} writer_t;
typedef struct
{
bool ready, accepted;
unsigned calls;
} input_t;
typedef struct
{
writer_t *assetWriter;
input_t *videoInput, *audioInput, *pixelBufferAdaptor;
} record_avfoundation_t;
typedef struct { bool bitrate; } NSMutableDictionary;
static NSMutableDictionary fallback_settings;
static unsigned released, freed, removed;
static void release_sample(void *sample)
{
assert(sample);
released++;
}
#define CVPixelBufferRelease release_sample
#define CFRelease release_sample
static bool append_video(input_t *input, void *sample, int timestamp)
{
assert(sample && timestamp == 7);
input->calls++;
return input->accepted;
}
static bool append_audio(input_t *input, void *sample)
{
assert(sample);
input->calls++;
return input->accepted;
}
static bool can_apply(writer_t *writer, NSMutableDictionary *settings)
{
writer->probes++;
return settings->bitrate ? writer->preset_supported
: writer->default_supported;
}
static NSMutableDictionary *copy_settings(NSMutableDictionary *settings)
{
fallback_settings = *settings;
return &fallback_settings;
}
static void remove_bitrate(NSMutableDictionary *settings, int key)
{
assert(key == AVEncoderBitRateKey);
settings->bitrate = false;
removed++;
}
static void avfoundation_release_handle(record_avfoundation_t *handle)
{
assert(handle);
freed++;
}
static void trace(const char *format, ...)
{
assert(format);
}
#define RARCH_ERR trace
#define RARCH_WARN trace
#include "legacy_append.inc"
int main(void)
{
writer_t writer = {0};
input_t input = {0};
record_avfoundation_t handle;
NSMutableDictionary settings;
unsigned status, ready, accepted, audio, cases = 0;
handle.assetWriter = &writer;
handle.videoInput = handle.audioInput = handle.pixelBufferAdaptor = &input;
for (audio = 0; audio < 2; audio++)
for (status = 0; status < 5; status++)
for (ready = 0; ready < 2; ready++)
for (accepted = 0; accepted < 2; accepted++)
{
bool expected = status == AVAssetWriterStatusWriting
&& ready && accepted;
bool result;
writer.status = (int)status;
input.ready = ready != 0;
input.accepted = accepted != 0;
input.calls = released = 0;
result = audio ? legacy_audio_submit(&handle, &input)
: legacy_video_submit(&handle, &input, 7);
assert(result == expected && released == 1);
assert(input.calls == (unsigned)(status
== AVAssetWriterStatusWriting && ready));
cases++;
}
for (ready = 0; ready < 2; ready++)
for (accepted = 0; accepted < 2; accepted++)
{
void *result;
settings.bitrate = true;
writer.preset_supported = ready != 0;
writer.default_supported = accepted != 0;
writer.probes = freed = removed = 0;
result = legacy_audio_settings(&handle, &settings);
assert(!!result == !!(ready || accepted));
assert(settings.bitrate);
assert(writer.probes == (ready ? 1u : 2u));
assert(removed == (ready ? 0u : 1u));
assert(freed == (unsigned)(!ready && !accepted));
if (result)
assert(((NSMutableDictionary*)result)->bitrate == (ready != 0));
}
printf("PASS: %u production append/result cases and AAC preset/default/refusal paths; Apple messages are stand-ins\n", cases);
return 0;
}